Переменная среды Linux IFS не используется библиотечной функцией gcc wordexp Posix C для разделения слов

Окружающая среда

ОС: Ubunty 20.4, Centos 8, macOS Catalina 10.15.7
Язык: C, C++
Компилятор: gcc (самые последние версии для каждой ОС)

Проблема

Я использую библиотечную функцию wordexp Posix, чтобы получить расширение строк, подобное оболочке.
Расширение работает нормально, за одним исключением: когда я устанавливаю переменную среды $IFS на что-то отличное от пробела, например ':', похоже, это не влияет на разделение слов, которое по-прежнему выполняется только по пробелам, независимо от значения IFS.

bash тест

Справочная страница wordexp для Linux https://man7.org/linux/man-pages/man3/wordexp.3.html гласит:

  1. Функция wordexp() выполняет расширение строки в виде оболочки...
  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 или нет:

Код С:

    #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

Как примечание, приведенный выше фрагмент был создан для этого поста — я провел тест с более сложным кодом, который подтвердил, что переменная среды действительно установлена ​​​​правильно.

Обзор исходного кода

Я просмотрел исходный код реализации функции wordexp, доступный по адресу https://code.woboq.org/userspace/glibc/posix/wordexp.c.html, и похоже, что он действительно использует $IFS, но, возможно, непоследовательно или, возможно, это ошибка.
В частности:
В теле wordexp, которое начинается с строки 2229, оно получает значение переменной среды 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. If the split is only on whitespace, is this a bug in the
    • gcc library implementation or
    • на справочной странице Linux для wordexp, где утверждается, что $IFS можно использовать для определения разделителей

Заранее большое спасибо за все ваши комментарии и идеи!

Резюме ответов и обходной путь

В принятом ответе был намек на то, как добиться разделения непробельных символов из $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

person Leo    schedule 21.01.2021    source источник
comment
Вы можете изучить исходный код GNU libc или MUSL libc, чтобы получить ответ. Вы можете попробовать другую библиотеку C, например. dietlibc. См. также linuxfromscratch.org; вы также можете попробовать другой компилятор C, например. CompCert или Кланг   -  person Basile Starynkevitch    schedule 21.01.2021
comment
Ссылка, которую я разместил для источника, который я искал, утверждает, что это для glibc, что называется библиотекой GNU C. Так что я думаю, что это реальный код (может быть, не совсем последний). У вас есть основания думать, что это не так? В любом случае поведение, которое я наблюдаю, вполне соответствует этому коду... Да, я мог бы попробовать другой компилятор, но я ограничен gcc - это длинная история. Но спасибо, что изучили это.   -  person Leo    schedule 21.01.2021
comment
Вы можете перекомпилировать свою libc (или код C) с помощью gcc -g -Wall -Wextra, а также перекомпилировать GCC из исходного кода. , и вы можете использовать Frama-C либо в исходном коде C, либо в исходном коде вашей libc.   -  person Basile Starynkevitch    schedule 21.01.2021
comment
Конечно, но устранение неполадок с libc для меня выходит за рамки. Я разместил этот вопрос по двум причинам: а) в надежде, что я ошибаюсь и есть способ разбить что-то кроме пробелов, и б) посмотреть, куда поместить отчет об ошибке - в gcc lib или в linux man страницы.   -  person Leo    schedule 21.01.2021
comment
Через несколько недель (весной 2021 г.) вы сможете использовать Bismon, описанный в этого проекта. Вас может заинтересовать DECODER или КОЛЕСНИЦА Европейские проекты. Не стесняйтесь обращаться ко мне по электронной почте (укажите URL вашего вопроса в письме)   -  person Basile Starynkevitch    schedule 21.01.2021
comment
Спасибо за указатель. Я посмотрю.   -  person Leo    schedule 21.01.2021
comment
Используйте также strace(1) и ltrace(1) — в дополнение к gdb(1) и valgrind - на вашем исполняемом файле   -  person Basile Starynkevitch    schedule 21.01.2021
comment
Судя по моему - вы правы. Это очень похоже на баг. Я добавил вызовы getenv ("IFS"), чтобы подтвердить, что среда видит вызов setenv() — и это так. Ваше использование правильное, если я что-то упустил. Такое же поведение на gcc (GCC) 10.2.0 и gcc (SUSE Linux) 7.4.1. (очень хорошая запись вопроса)   -  person David C. Rankin    schedule 21.01.2021
comment
Я на 99% уверен, что вы путаете токенизацию со словами и разделяете результаты расширения этих токенов на более позднем этапе синтаксического анализа, но я не в состоянии откопать соответствующие ссылки и привести несколько примеров. wordexp() не делает то же самое, что встроенный read.   -  person Shawn    schedule 21.01.2021
comment
man bash IFS — это внутренний разделитель полей, который используется для разделения слов после расширения.   -  person stark    schedule 21.01.2021
comment
@ Дэвид С. Рэнкин Большое спасибо, что нашли время изучить это, и за комплимент :-)   -  person Leo    schedule 21.01.2021
comment
@Shawn Спасибо, что изучили это. Я могу быть не на 100% уверен в том, какая разница (и почему) будет отличаться от токенизации слов и разделителей полей, но позвольте мне спросить: возможно ли использовать wordexp для достижения поведения в моем вопросе, т.е. разделить на что-то другое, кроме пробелов?   -  person Leo    schedule 21.01.2021
comment
@stark Спасибо, что изучили это. Пожалуйста, смотрите мой комментарий к Шону выше - я не мог поставить вас обоих в комментарий.   -  person Leo    schedule 21.01.2021
comment
wordexp выполняет только синтаксис командной строки оболочки, а не обычное разделение строк. Используйте для этого strtok.   -  person stark    schedule 21.01.2021
comment
Когда вы устанавливаете IFS=":" и вводите что-то на своем терминале, вы все равно вводите не sh:-c:echo 1, а sh -c 'echo 1', разделенные пробелами и табуляцией. IFS влияет на разделение слов, а не на разделение полей.   -  person KamilCuk    schedule 21.01.2021


Ответы (2)


Вы сравниваете яблоки с апельсинами. wordexp() разбивает строку на отдельные токены так же, как это делает оболочка. Встроенная оболочка read не следует тому же алгоритму; он просто разбивает слова. Вы должны сравнивать wordexp() с тем, как анализируются аргументы скрипта или функции оболочки:

#!/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

который при запуске через такие оболочки, как dash, ksh93 или bash (но не 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 выводит то же самое.

person Shawn    schedule 21.01.2021
comment
И, конечно же, "$foo" не будет подвергаться разбиению поля из-за кавычек. - person Shawn; 21.01.2021
comment
Еще раз спасибо за вашу помощь в этом. Возможно, это не ошибка, а сомнительный дизайн. При этом ваш ответ предложил обходной путь для моей проблемы: если я хочу разделить $IFS, мне просто нужно установить строку в переменную среды, а затем переменную wordexp(). Это дает результат, к которому я стремился. Я обновлю вопрос с обходным путем, чтобы иметь возможность вставить код. Не уверен, что это хорошая практика для SO... - person Leo; 21.01.2021
comment
@Leo Если вы просто хотите разделить строку на массив на основе разделителя, wordexp() - это огромное излишество по сравнению с чем-то, использующим strtok_r(). - person Shawn; 22.01.2021
comment
Я понимаю и полностью согласен. Причина, по которой я рассматривал возможность использования workexp() для разбиения на непробельные символы, заключается в том, чтобы иметь возможность правильно разбивать такие вещи, как, например, $PATH, которые потенциально могут содержать другие переменные среды, ~, строки в кавычках, экранированные разделители и т. д. Что выиграло не быть тривиальным с strtok_r() или подобным. Он разделяет типичный $PATH без каких-либо особых случаев, но мне все еще нужно проверить, действительно ли это работает правильно со сложным случаем. В очередной раз благодарим за помощь! - person Leo; 22.01.2021

Давайте наивно предположим, что POSIX понятен, и попробуем с ним работать. Возьмем wordexp() из posix:

Аргумент words является указателем на строку, содержащую одно или несколько слов, которые необходимо расширить. Расширения должны быть такими же, как интерпретатор командной строки, если бы слова были частью командной строки, представляющей аргументы для утилиты. [...]

Итак, перейдем к интерпретатору командной строки. Из командного языка оболочки posix:

2.1 Введение в оболочку

[...]

  1. Оболочка разбивает ввод на токены: слова и операторы; см. Распознавание токена. [.......]

2.3 Распознавание токена

[...]

  1. Если текущий символ является не заключенным в кавычки ‹пробелом›, любая лексема, содержащая предыдущий символ, ограничивается, а текущий символ отбрасывается.
  2. Если предыдущий символ был частью слова, текущий символ должен быть добавлен к этому слову.

[...]

В основном здесь применяются все разделы 2.3 Token Recognition — это то, что делает wordexp() — распознавание токена плюс некоторые расширения. А также самое важное о разбиении полей, акцент мой :

После расширения параметра (Parameter Expansion), подстановки команды (Command Substitution) и арифметического расширения (Arithmetic Expansion) оболочка должна сканировать результаты раскрытий и подстановок, которые не встречались в двойных кавычках для поля может возникнуть разделение и несколько полей.

IFS влияет на разделение полей, влияет на то, как результаты других расширений разбиваются на слова. IFS не влияет на то, как строка разбивается на токены, она по-прежнему разбивается с помощью <blank> - табуляции или пробела. Итак, поведение, которое вы видите.

Другими словами, когда вы набираете IFS=: в своем терминале, вы не начинаете разделять токены с помощью IFS, как echo:Hello:World, а продолжаете разделять части команд с помощью пробелов.

В любом случае, справочная страница верна... :p

Я что-то упустил, и есть ли способ заставить wordexp разбиваться на символы, отличные от пробелов?

Нет. Если вы хотите, чтобы в словах были пробелы, заключайте аргументы в кавычки, как в оболочке. "a b" "c d" "e".

Если разделение происходит только по пробелам, является ли это ошибкой в

Нет :р

person KamilCuk    schedule 21.01.2021
comment
Спасибо вам за вашу помощь в этом. Ваш ответ определенно прояснил ситуацию для меня и очень ценен. К сожалению, можно принять только один ответ, и мне пришлось принять другой ответ, потому что он дал подсказку, которая позволила мне найти обходной путь. Еще раз спасибо, Лео. - person Leo; 21.01.2021