Как использовать нулевые байты в Bash?

33

Я читал, что, поскольку пути к файлам в Bash могут содержать любой символ, кроме нулевого байта (нулевого байта, $'\0'), лучше использовать нулевой байт в качестве разделителя. Например, если выходные данные findбудут отправлены в другую программу, рекомендуется использовать эту -print0опцию (для версий find, у которых она есть).

Но хотя что-то вроде этого работает нормально (печать путей к файлам, разделенных символами новой строки - не волнуйтесь, это всего лишь демонстрация, на самом деле я не делаю это в реальных сценариях):

find -print0 \
  | while IFS= read -r -d $'\0' ; do echo "$REPLY" ; done

как то так не работает

for file in * ; do echo -n "$file"$'\0' ; done \
  | while IFS= read -r -d $'\0' ; do echo "$REPLY" ; done

Когда я пробую только forчасть -loop, я обнаруживаю, что она просто печатает все имена файлов вместе, без нулевого байта между ними.

Почему это? В чем дело?

ruakh
источник

Ответы:

43

Bash внутренне использует строки в стиле C, которые заканчиваются нулевыми байтами. Это означает, что строка Bash (например, значение переменной или аргумент команды) фактически не может содержать нулевой байт. Например, этот мини-скрипт:

foobar=$'foo\0bar'    # foobar='foo' + null byte + 'bar'
echo "${#foobar}"     # print length of $foobar

на самом деле печатает 3, потому что $foobarна самом деле просто 'foo': barидет после конца строки.

Точно так же echo $'foo\0bar'просто печатает foo, потому echoчто не знает о \0barчасти.

Как видите, \0последовательность на самом деле очень вводит в заблуждение в $'...'строке -стиля; он выглядит как нулевой байт внутри строки, но в итоге он не работает таким образом. В вашем первом примере ваша readкоманда имеет -d $'\0'. Это работает, но только потому, что -d ''тоже работает! (Это явно не задокументированная особенность read, но я предполагаю, что она работает по той же причине: ''это пустая строка, поэтому ее завершающий нулевой байт приходит сразу. Задокументировано как использование «первого символа разделителя », и я предполагаю, что даже работает если «первый символ» находится за концом строки!)-d delim

Но , как вы знаете из findпримера, что это возможно для команды , чтобы распечатать нулевые байты, а для этого байта быть передан в другую команду , которая считывает его в качестве входных данных. Никакая часть этого не полагается на сохранение нулевого байта в строке внутри Bash . Единственная проблема с вашим вторым примером заключается в том, что мы не можем использовать $'\0'аргумент команды; echo "$file"$'\0'мог бы счастливо напечатать нулевой байт в конце, если бы знал, что вы этого хотите.

Таким образом, вместо использования echoвы можете использовать printf, который поддерживает те же виды escape-последовательностей, что и $'...'строки -style. Таким образом, вы можете напечатать нулевой байт без необходимости иметь нулевой байт внутри строки. Это будет выглядеть так:

for file in * ; do printf '%s\0' "$file" ; done \
  | while IFS= read -r -d '' ; do echo "$REPLY" ; done

или просто так:

printf '%s\0' * \
  | while IFS= read -r -d '' ; do echo "$REPLY" ; done

(Примечание: на echoсамом деле также есть -eфлаг, который позволил бы ему обрабатывать \0и печатать нулевой байт; но затем он также пытался бы обрабатывать любые специальные последовательности в вашем имени файла. Таким образом, printfподход является более надежным.)


Кстати, есть некоторые оболочки , которые действительно позволяют нулевой байт внутри строки. Например, ваш пример отлично работает в Zsh (при условии настроек по умолчанию). Однако, независимо от вашей оболочки, Unix-подобные операционные системы не обеспечивают способ включения нулевых байтов в аргументы программ (поскольку аргументы программы передаются в виде строк в стиле C), поэтому всегда будут некоторые ограничения. (Ваш пример может работать в Zsh только потому , что echoэто встроенная команда оболочки, так Zsh может вызвать его , не полагаясь на поддержку ОС для вызова других программ. Если вы использовали command echoвместо echo, так что обойти встроенную и использовать автономную echoпрограмму на $PATH, вы увидите то же поведение в Zsh, что и в Bash.)

ruakh
источник
2
Почему IFS не установлен в ноль, если -d ''уже означает разделение на \0? Я нашел объяснение здесь: stackoverflow.com/questions/8677546/…
CMCDragonkai