Присвоить строку, содержащую нулевой символ (\0), переменной в Bash

При попытке правильно обработать список file-/foldernames ( см. Другие мои вопросы), используя символ NULL в качестве разделителя, я наткнулся на странное поведение Bash, которое я не понимаю:

При назначении переменной, содержащей один или несколько символов NULL, переменные NULL теряются / игнорируются / не сохраняются.

Например,

echo -ne "n\0m\0k" | od -c   # -> 0000000   n  \0   m  \0   k

Но:

VAR1=`echo -ne "n\0m\0k"`
echo -ne "$VAR1" | od -c   # -> 0000000   n   m   k

Это означает, что мне нужно было бы записать эту строку в файл (например, в /tmp) и прочитать ее оттуда, если прямая передача нежелательна или невозможна.

При выполнении этих сценариев в оболочке Z (zsh) строки, содержащие \0, сохраняются в обоих случаях, но, к сожалению, я не могу предположить, что zsh присутствует в системах, где выполняется мой сценарий, в то время как Bash должен быть.

Как можно эффективно хранить или обрабатывать строки, содержащие символы \0, без потери (мета) символов?

5 ответов

Решение

В Bash вы не можете хранить NULL-символ в переменной.

Однако вы можете сохранить обычный шестнадцатеричный дамп данных (а затем снова выполнить эту операцию снова), используя xxd команда.

VAR1=`echo -ne "n\0m\0k" | xxd -p | tr -d '\n'`
echo -ne "$VAR1" | xxd -r -p | od -c   # -> 0000000    n  \0   m  \0   k

Как уже говорили другие, вы не можете хранить / использовать NUL char:

  • в переменной
  • в аргументе командной строки.

Однако вы можете обрабатывать любые двоичные данные (включая NUL-символ):

  • в трубах
  • в файлах

Итак, чтобы ответить на ваш последний вопрос:

Кто-нибудь может дать мне подсказку, как строки, содержащие символы \0, могут эффективно храниться или обрабатываться без потери (мета) символов?

Вы можете использовать файлы или каналы для эффективного хранения и обработки любой строки с любыми метасимволами.

Если вы планируете обрабатывать данные, вам следует дополнительно отметить, что:

  • Только NUL-символ будет съеден переменной и аргументом командной строки, вы можете проверить это.
  • Будьте осторожны, что подстановка команд (как $(command..) или же `command..`) имеет дополнительный поворот над тем, чтобы быть переменной, так как она съест ваши новые строки.

Обход ограничений

Если вы хотите использовать переменные, то вы должны избавиться от NUL-символа, кодируя его, и различные другие решения здесь предлагают умные способы сделать это (очевидный способ - использовать, например, кодирование / декодирование base64).

Если вас беспокоит память или скорость, вы, вероятно, захотите использовать минимальный синтаксический анализатор и указывать только символ NUL (и символ цитирования). В этом случае это поможет вам:

quote() { sed 's/\\/\\\\/g;s/\x0/\\0/g'; }

Затем вы можете защитить свои данные, прежде чем сохранять их в переменных и аргументе командной строки, передавая ваши конфиденциальные данные в quote, который выведет безопасный поток данных без символов NUL. Вы можете получить исходную строку (с NUL-символами), используя echo -en "$var_quoted" который отправит правильную строку в стандартный вывод.

Пример:

## Our example output generator, with NUL chars
ascii_table() { echo -en "$(echo '\'0{0..3}{0..7}{0..7} | tr -d " ")"; }
## store
myvar_quoted=$(ascii_table | quote)
## use
echo -en "$myvar_quoted"

Примечание: использовать | hd чтобы получить четкое представление ваших данных в шестнадцатеричном формате и убедиться, что вы не потеряли NUL-символов.

Смена инструментов

Помните, что вы можете пойти довольно далеко с каналами без использования переменных или аргументов в командной строке, не забудьте, например, <(command ...) конструкция, которая создаст именованный канал (вид временного файла).

РЕДАКТИРОВАТЬ: первая реализация quote был неверным и не справился бы правильно с \ специальные символы, интерпретируемые echo -en, Спасибо @xhienne за то, что заметил это.

использование uuencode а также uudecode для портативности POSIX

xxd а также base64 не POSIX 7, но есть uuencode.

VAR="$(uuencode -m <(printf "a\0\n") /dev/stdout)"
uudecode -o /dev/stdout <(printf "$VAR") | od -tx1

Выход:

0000000 61 00 0a
0000003

К сожалению, я не вижу альтернативы POSIX 7 для процесса Bash <() расширение подстановки, кроме записи в файл, и они не установлены в Ubuntu 12.04 по умолчанию (sharutils пакет).

Поэтому я думаю, что реальный ответ таков: не используйте Bash для этого, используйте Python или какой-то другой более разумный интерпретируемый язык.

Я люблю ответ Джеффа. Я бы использовал кодирование Base64 вместо xxd. Это экономит немного места и было бы (я думаю) более узнаваемым относительно того, что предназначено.

VAR=$(echo -n "foo\0bar" | base64)
echo -n $VAR | base64 -d | xargs -0 ...

Что касается -e, он не нужен, потому что оболочка уже интерпретирует escape до того, как она доходит до эха. Я также, кажется, вспоминаю что-то о том, что "echo -e" небезопасно, если вы повторяете какие-либо пользовательские данные, поскольку они могут вводить escape-последовательности, которые echo будет интерпретировать и приводить к плохим вещам.

Вот максимально эффективное по памяти решение, которое просто экранирует байты NULL с помощью .
(Поскольку я не был доволен base64 или подобным. :)

      esc0() { sed 's/\xFF/\xFF\xFF/g; s/\x00/\xFF0/g'; }
cse0() { sed 's/\xFF0/\xFF\x00/g; s/\xFF\(.\)/\1/g'; }

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

Вот пример, который рисует градиенты в буфере кадра (не работает в X), используя переменные для предварительного рендеринга блоков и линий для ускорения:

      width=7680; height=1080; # Set these to your framebuffer’s size.
blocksPerLine=$(( $width / 256 ))
block="$( for i in 0 1 2 3 4 5 6 7 8 9 A B C D E F; do for j in 0 1 2 3 4 5 6 7 8 9 A B C D E F; do echo -ne "\x$i$j"; done; done | esc0 )"
line="$( for ((b=0; b < blocksPerLine; b++)); do echo -en "$block"; done )"
for ((l=0; l <= $height; l++)); do echo -en "$line"; done | cse0 > /dev/fb0

Обратите внимание, как$blockсодержит экранированные значения NULL (плюс\xFFs), и в конце, перед записью всего во фреймбуфер,cse0не ускользает от них.

Другие вопросы по тегам