Установка IFS на нулевой байт неправильно разделяет строки в командной строке

~ ls
A B C

На bash (выглядит неправильно)

~IFS=$'\x00' read -a vars < <(find -type f -print0); echo "${vars}"
ABC

На зш (выглядит хорошо)

~IFS=$'\x00' read -A vars < <(find -type f -print0); echo "${vars}"
A B C

Это баг Баша?


person Pan Ruochen    schedule 06.03.2019    source источник
comment
В любом из случаев read -a или -A будет определять массив, а не переменную-заполнитель. Вам нужно сделать echo "${vars[@]}"   -  person Inian    schedule 06.03.2019
comment
См. также Как я могу сохранить результаты команды «найти» в виде массива в Bash (и, гм, мой ответ, который показывает, как получить вывод find -print0 в массив).   -  person Benjamin W.    schedule 14.03.2019


Ответы (3)


нулевой символ очень особенный и POSIX и bash не допускают его внутри строк (это определение конца строки, поэтому $'\x00' и $'\000' практически никогда не работают; Ответ Иниана здесь даже ссылается на обходной путь для ввода нулевого символа, но опять же вы не можете ожидать, что это будет должным образом сохраняется, когда вы присваиваете его переменной). Похоже, zsh не возражает против этого, но bash против.

Вот тест, который иллюстрирует проблемы с представлением пробелов, табуляции и символов новой строки в именах файлов:

$ touch 'two words' tabbed$'\t'words "two
lines"
$ ls            # GNU coreutils ls displays using bash's $'string' notation
'tabbed'$'\t''words'  'two'$'\n''lines'  'two words'
$ ls |cat       # … except when piped elsewhere
tabbed  words
two
lines
two words
$ find *        # GNU findutils find displays tabs & newlines as questions
tabbed?words
two?lines
two words
$ find * |cat   # … except when piped elsewhere
tabbed  words
two
lines
two words
$ touch a b c   # (more tests for later)

Инструменты GNU очень умны и знают, что это проблема, поэтому они придумывают творческие способы ее решения, но они даже не последовательны. ls предполагает, что вы используете bash или zsh (синтаксис $'…' для литерала нет в POSIX), а find дает вам вопросительный знак (сам по себе допустимый символ имени файла, но это файловый шаблон, который соответствует любой символ, например, rm two?lines tabbed?words удалит оба файла, как и rm 'two'$'\n''lines' 'tabbed'$'\t''words'). Оба представляют правду, когда передаются другой команде, такой как cat.

GNU/BSD/MacOSX/Busybox найти и xargs

Я вижу, вы используете расширения GNU: POSIX и BSD/OSX find не допускают неявный путь, а POSIX find не поддерживает -print0 через Спецификация поиска POSIX упоминает об этом:

В других реализациях были добавлены другие способы обойти эту проблему, в частности, первичный -print0, который записывал имена файлов с нулевым символом конца байта. Здесь это рассматривалось, но не принято. Использование нулевого терминатора означало, что любая утилита, которая собиралась обрабатывать вывод find -print0, должна была добавить новую опцию для анализа нулевых терминаторов, которые она теперь будет считывать.

В спецификации POSIX xargs также отсутствует поддержка -0 (нет ссылки на это тоже), хотя он поддерживается xargs в GNU, BSD/OSX и busybox.

Поэтому вы, вероятно, можете сделать это:

$ find . -type f -print0 |xargs -0
./c ./b ./a ./two
lines ./tabbed  words ./two words

Однако вам может понадобиться массив, поэтому, возможно, я слишком подхожу к вашему упрощенному вопросу.

файл карты

Вы можете использовать mapfile в Bash 4.4 и более поздних версиях:

$ mapfile -d '' vars < <(find . -type f -print0)
$ printf '<%s>\n' "${vars[@]}"
<./c>
<./b>
<./a>
<./two
lines>
<./tabbed   words>
<./two words>

Некоторые команды, в том числе mapfile, read и readarray (синоним mapfile), принимают -d '' как если бы это было -d $'\0', вероятно, [цитата нужна] в качестве обходного пути для вышеупомянутой неспособности оболочки POSIX работать с нулевыми символами. в струнах.

Эта команда mapfile просто считывает входной файл (в данном случае стандартный ввод) в массив $vars, разделенный нулевыми символами. Стандартный ввод заполняется через конвейер с помощью дескриптора файла, созданного подстановкой процесса <(…) в конце строки, которая обрабатывает вывод нашей команды find.

Небольшое отступление: вы могли бы подумать, что можете просто выполнить find … |mapfile …, но это изменяет область действия, и любые переменные, которые вы устанавливаете или изменяете там, теряются, когда команда конвейера завершается. Трюк с заменой процесса не заманивает вас в ловушку таким же образом.

Команда printf просто демонстрирует содержимое массива. Угловые скобки обозначают начало и конец каждого элемента, поэтому вас не смущает новая строка, пробел или табуляция.

person Adam Katz    schedule 14.03.2019

В вашей логике в обеих попытках выше много неправильных представлений. В оболочке bash вы просто не можете сохранить значение NULL байта \x00 в переменной, будь то специальная IFS или любая другая определяемая пользователем переменная. Таким образом, ваше требование разделить результат find на байт NULL никогда не сработает. Из-за этого ваши результаты из find сохраняются в массиве по первому индексу как одна длинная запись, объединенная с нулевым байтом.

Вы можете обойти проблему использования байта NULL в переменной с помощью нескольких приемов, определенных в Как передать \x00 в качестве аргумента программе?< /а>. Вы можете использовать любой другой пользовательский символ для своего IFS просто как

IFS=: read -r -a splitList <<<"foo:bar:dude" 
declare -p splitList

Идеальным способом чтения файлов с разделителями NULL было бы установить поле разделителя в команде read для чтения до тех пор, пока не встретится нулевой байт.

Но тогда, если вы просто сделаете

IFS= read -r -d '' -a files < <(find -type f -print0)

вы читаете только первый файл, за которым следует байт NULL, а массив "${files[@]}" будет содержать только одно имя файла. Вам нужно читать в цикле, пока не будет прочитан последний байт NULL и не будет больше символов для чтения

declare -a array=()
while IFS= read -r -d '' file; do
    array+=( "$file" )
done < <(find -type f -print0)

который выдает результаты, содержащие каждый файл в отдельной записи массива

printf '%s\n' "${array[@]}"
person Inian    schedule 06.03.2019

Если ваш xargs поддерживает -0 и вы просто хотите выполнить итерацию по списку строк с нулевым разделителем в цикле, совместимом с Bourne (sh, dash, bash, zsh, busybox shell, …), вы можете сделать:

find . -type f -print0|xargs -0 sh -c 'while test $# -gt 0;do echo "$1";shift;done' sh

или используя цикл for:

find . -type f -print0|xargs -0 sh -c 'for i;do echo "$i";done' sh
person linuxball    schedule 24.02.2021