【问题标题】:Linux IFS environment variable is not used by gcc wordexp Posix C library function for splitting wordsgcc wordexp不使用Linux IFS环境变量用于拆分单词的Posix C库函数
【发布时间】:2021-04-25 13:33:17
【问题描述】:

环境

操作系统:Ubunty 20.4、Centos 8、macOS Catalina 10.15.7
语言:C、C++
编译器:gcc(每个操作系统的最新版本)

问题

我正在使用 wordexp Posix 库函数来获得类似 shell 的字符串扩展。
扩展工作正常,但有一个例外:当我将 $IFS 环境变量设置为除空格以外的其他内容时,例如“:”,它似乎不会影响仅在空格上继续执行的单词的拆分,而不管 IFS 值如何.

bash 测试

wordexp for Linux https://man7.org/linux/man-pages/man3/wordexp.3.html 的手册页指出:

  1. “wordexp() 函数对字符串执行类似 shell 的扩展...”
  2. "字段拆分是使用环境变量$IFS完成的。如果没有设置,字段分隔符是空格、制表符和换行符。"

这就是为什么我希望 wordexp 在这方面表现得与 bash 相同。
在所有列出的操作系统上,当更改用于拆分的字符集时,我得到了完全正确和预期的结果:
使用默认(未设置 IFS)

    read -a words <<<"1 2:3 4:5"
    for word in "${words[@]}"; do echo "$word";  done

在空间上正确分割并产生结果:

    1
    2:3
    4:5

同时将 IFS 设置为 ':'

    IFS=':' read -a words <<<"1 2:3 4:5"
    for word in "${words[@]}"; do echo "$word";  done

在 ':' 上正确拆分并产生结果:

    1 2
    3 4
    5

C代码测试

但是无论是否设置了 IFS 环境变量,运行下面的代码都会产生相同的结果:

C 代码:

    #include <stdio.h>
    #include <wordexp.h>
    #include <stdlib.h>
    
    static void expand(char const *title, char const *str)
    {
        printf("%s input: %s\n", title, str);
        wordexp_t exp;
        int rcode = 0;
        if ((rcode = wordexp(str, &exp, WRDE_NOCMD)) == 0) {
            printf("output:\n");
            for (size_t i = 0; i < exp.we_wordc; i++)
                printf("%s\n", exp.we_wordv[i]);
            wordfree(&exp);
        } else {
            printf("expand failed %d\n", rcode);
        }
    }
    
    int main()
    {
        char const *str = "1 2:3 4:5";
        
        expand("No IFS", str);
    
        int rcode = setenv("IFS", ":", 1);
        if ( rcode != 0 ) {
            perror("setenv IFS failed: ");
            return 1;
        }
    
        expand("IFS=':'", str);
    
        return 0;
    }

所有操作系统的结果都是一样的:

    No IFS input: 1 2:3 4:5
    output:
    1
    2:3
    4:5
    IFS=':' input: 1 2:3 4:5
    output:
    1
    2:3
    4:5

请注意,上面的 sn-p 是为这篇文章创建的 - 我确实使用更复杂的代码进行了测试,验证了环境变量确实设置正确。

源代码审查

我查看了https://code.woboq.org/userspace/glibc/posix/wordexp.c.html 上提供的 wordexp 函数实现的源代码,它似乎确实使用了 $IFS,但可能不一致,或者这是一个错误。
具体来说:
第 2229 行 开始的 wordexp 正文中,它确实获取 IFS 环境变量值并对其进行处理:
第 2273 - 2276 行:

     /* Find out what the field separators are.
       * There are two types: whitespace and non-whitespace.
       */
      ifs = getenv ("IFS");

但后来在函数中似乎没有 使用 $IFS 值进行单词分隔。
这看起来像一个错误,除非 第 2273 行 上有“字段分隔符” 和 第 2396 行上的“单词分隔符”表示不同的含义。
第 2395 - 2398 行:

          default:
            /* Is it a word separator? */
            if (strchr (" \t", words[words_offset]) == NULL)
            {

但无论如何,代码似乎只使用空格或制表符作为分隔符 不像 bash,它尊重 IFS 集拆分器值。

问题

  1. 我是否遗漏了什么,有办法让 wordexp 分割除空格以外的字符吗?
  2. 如果仅在空白处进行拆分,这是否是
    • gcc 库实现或
    • wordexp 的 Linux 手册页中,他们声称 $IFS 可用于定义拆分器

非常感谢您的所有 cmets 和见解!

答案摘要和解决方法

在接受的答案中,有一个关于如何从 $IFS 中实现非空白字符拆分的提示:您必须设置 $IFS 并将要拆分的字符串作为临时环境变量的值然后针对该临时变量调用 wordexp。这在下面的更新代码中得到了证明。
虽然在源代码中可见的这种行为实际上可能不是一个错误,但它对我来说绝对是一个有问题的设计决定……
更新代码:

    #include <stdio.h>
    #include <wordexp.h>
    #include <stdlib.h>
    
    static void expand(char const *title, char const *str)
    {
        printf("%s input: %s\n", title, str);
        wordexp_t exp;
        int rcode = 0;
        if ((rcode = wordexp(str, &exp, WRDE_NOCMD)) == 0) {
            printf("output:\n");
            for (size_t i = 0; i < exp.we_wordc; i++)
                printf("%s\n", exp.we_wordv[i]);
            wordfree(&exp);
        } else {
            printf("expand failed %d\n", rcode);
        }
    }
    
    int main()
    {
        char const *str = "1 2:3 4:5";
        
        expand("No IFS", str);
    
        int rcode = setenv("IFS", ":", 1);
        if ( rcode != 0 ) {
            perror("setenv IFS failed: ");
            return 1;
        }
    
        expand("IFS=':'", str);
        
        rcode = setenv("FAKE", str, 1);
        if ( rcode != 0 ) {
            perror("setenv FAKE failed: ");
            return 2;
        }
    
        expand("FAKE", "${FAKE}");    
    
        return 0;
    }

产生结果:

    No IFS input: 1 2:3 4:5
    output:
    1
    2:3
    4:5
    IFS=':' input: 1 2:3 4:5
    output:
    1
    2:3
    4:5
    FAKE input: ${FAKE}
    output:
    1 2
    3 4
    5

【问题讨论】:

  • 您可以深入了解GNU libcMUSL libc 的源代码以获得答案。您可以尝试不同的 C 库,例如dietlibc。另见linuxfromscratch.org;您也可以尝试使用不同的 C 编译器,例如CompCertClang
  • 我为我正在查看的源发布的链接声称适用于 glibc,这就是 GNU C 库的名称。所以我认为这是实际的代码(也许不是最新的)。你有理由认为它不是吗?无论如何,我观察到的行为与该代码非常匹配......是的,我可以尝试不同的编译器,但我仅限于 gcc - 这是一个很长的故事。但感谢您对此进行调查。
  • 据我所知——你是对的。这看起来很像一个错误。我添加了对getenv ("IFS") 的调用,以确认环境看到setenv() 调用——确实如此。除非我遗漏了什么,否则您的用法是正确的。 gcc (GCC) 10.2.0gcc (SUSE Linux) 7.4.1 的行为相同。 (这个问题写得非常好)
  • 我 99% 确定您将标记化混淆为单词,并在解析的后期将这些标记中的扩展结果拆分,但我无法挖掘相关参考并举出一些例子。 wordexp() 与内置的 read 不同。
  • wordexp 只执行 shell 命令行语法,而不是一般的字符串拆分。为此使用 strtok。

标签: c linux gcc posix ifs


【解决方案1】:

您将苹果与橙子进行比较。 wordexp() 以与 shell 相同的方式将字符串拆分为单独的标记。 shell 内置 read 不遵循相同的算法;它只是分词。您应该将 wordexp() 与如何解析脚本或 shell 函数的参数进行比较:

#!/bin/sh

printwords() {
    for arg in "$@"; do
        printf "%s\n" "$arg"
    done
}

echo "No IFS input: 1 2:3 4:5"
printwords 1 2:3 4:5
echo "IFS=':' input: 1 2:3 4:5"
IFS=:
printwords 1 2:3 4:5

这会产生

No IFS input: 1 2:3 4:5
1
2:3
4:5
IFS=':' input: 1 2:3 4:5
1
2:3
4:5

就像 C 程序一样。


现在,有趣的一点。我无法通过快速扫描在 POSIX 文档中明确提到它,但bash manual 对分词有这样的说法:

注意,如果没有展开,则不进行拆分。

让我们尝试一个在其参数中进行参数扩展的版本:

#!/bin/sh

printwords() {
    for arg in "$@"; do
        printf "%s\n" "$arg"
    done
}

foo=2:3
printf "foo = %s\n" "$foo"
printf "No IFS input: 1 \$foo 4:5\n"
printwords 1 $foo 4:5
printf "IFS=':' input: 1 \$foo 4:5\n"
IFS=:
printwords 1 $foo 4:5

当通过 dashksh93bash 之类的 shell 运行时(但 不是 zsh,除非你打开 SH_WORD_SPLIT 选项),产生

foo = 2:3
No IFS input: 1 $foo 4:5
1
2:3
4:5
IFS=':' input: 1 $foo 4:5
1
2
3
4:5

如您所见,具有参数的参数受字段拆分的影响,但不是字面拆分。对 C 程序中的字符串进行相同的更改并运行 foo=2:3 ./wordexp 会打印出相同的内容。

【讨论】:

  • 当然"$foo" 不会因为引号而受到字段拆分。
  • 再次感谢您的帮助。也许这不是一个错误,而是一种有问题的设计。这就是说,您的回答为我的问题提出了一种解决方法:如果我想在 $IFS 上拆分,我只需将字符串设置为环境变量,然后将 wordexp() 变量设置为变量。这确实产生了我所追求的结果。我将使用解决方法更新问题,以便能够粘贴代码。不确定这是否是一个很好的做法...
  • @Leo 如果您只是想根据分隔符将字符串拆分为数组,那么与使用 strtok_r() 的东西相比,wordexp() 是巨大的杀伤力。
  • 我理解并完全同意。我正在考虑使用 workexp() 在非空格上拆分的原因是能够正确拆分诸如 $PATH 之类的东西,例如,它可能包含其他环境变量、~、带引号的字符串、转义分隔符等。哪个赢了'不要对 strtok_r() 或类似的东西微不足道。它确实在没有特殊情况的情况下拆分了典型的 $PATH,但我仍然需要检查它是否真的适用于复杂的情况。再次感谢您的帮助!
【解决方案2】:

让我们天真地假设 POSIX 是可以理解的并尝试使用它。让我们来wordexp() from posix

words 参数是一个指向包含一个或多个要扩展的单词的字符串的指针。如果单词是表示实用程序参数的命令行的一部分,则扩展应与命令行解释器执行的扩展相同。 [...]

所以让我们转到“命令行解释器”。来自posix shell command language

2.1 外壳介绍

[...]

  1. shell 将输入分解为标记:单词和运算符;请参阅令牌识别。 [.......]

2.3 令牌识别

[...]

  1. 如果当前字符是未加引号的 ,则包含前一个字符的任何标记都将被分隔,并且当前字符应被丢弃。
  2. 如果前一个字符是单词的一部分,则当前字符应附加到该单词。

[...]

基本上整个 2.3 Token Recognition 部分都适用于此 - 这是 wordexp() 所做的事情 - 令牌识别和一些扩展。还有关于field splitting 的最重要的东西,强调我的:

在参数扩展(Parameter Expansion)、命令替换(Command Substitution)和算术扩展(Arithmetic Expansion)之后,shell将扫描扩展和替换的结果在double中没有发生字段拆分的引号,可以产生多个字段。

IFS 影响字段拆分,它影响其他扩展的结果如何被吐成单词。 IFS 不影响如何将字符串拆分为标记,它仍然使用 &lt;blank&gt; 进行拆分 - 制表符或空格。所以你看到的行为。

换句话说,当您在终端中键入IFS=: 时,您不会像echo:Hello:World 那样开始用IFS 分隔标记,而是继续使用空格分隔部分命令。

无论如何,手册页是正确的...:p

我是否遗漏了什么?有办法让 wordexp 在除空格之外的字符上进行拆分吗?

没有。如果您想在单词中包含空格,请引用参数,就像在 shell 中一样。 "a b" "c d" "e".

如果拆分仅在空格上,这是一个错误

无:p

【讨论】:

  • 感谢您对此的帮助。您的回答肯定为我澄清了情况,非常感谢。不幸的是,只能接受一个答案,而我不得不接受另一个答案,因为它提供了让我有解决方法的提示。再次感谢 Leo。
猜你喜欢
  • 2012-01-02
  • 1970-01-01
  • 1970-01-01
  • 2012-09-10
  • 1970-01-01
  • 1970-01-01
  • 2015-09-09
  • 2015-09-16
  • 2013-05-28
相关资源
最近更新 更多