Como uso bytes nulos no Bash?

33

Eu li que, como os caminhos de arquivo no Bash podem conter qualquer caractere, exceto o byte nulo (byte com valor zero $'\0'), é melhor usar o byte nulo como separador. Por exemplo, se a saída de findfor enviada para outro programa, é recomendável usar a -print0opção (para versões finddele).

Mas, embora algo assim funcione bem (imprimir caminhos de arquivo separados por novas linhas - não se preocupe, isso é apenas uma demonstração, na verdade não estou fazendo isso em scripts reais):

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

algo assim não funciona:

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

Quando tento apenas a forparte -loop, acho que ela apenas imprime todos os nomes de arquivos juntos, sem o byte nulo no meio.

Por que é isso? O que está acontecendo?

ruakh
fonte

Respostas:

43

O Bash usa seqüências de caracteres de estilo C internamente, que são terminadas por bytes nulos. Isso significa que uma string Bash (como o valor de uma variável ou um argumento para um comando) nunca pode realmente conter um byte nulo. Por exemplo, este mini-script:

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

realmente imprime 3, porque $foobarna verdade é apenas 'foo': o barvem após o final da string.

Da mesma forma, echo $'foo\0bar'apenas imprime foo, porque echonão sabe sobre a \0barpeça.

Como você pode ver, a \0sequência é realmente muito enganadora em uma $'...'string de estilo; parece um byte nulo dentro da string, mas não acaba funcionando dessa maneira. No seu primeiro exemplo, seu readcomando possui -d $'\0'. Isso funciona, mas apenas porque -d ''também funciona! (Esse não é um recurso explicitamente documentado read, mas suponho que funcione pelo mesmo motivo: ''é a cadeia vazia, portanto seu byte nulo final vem imediatamente. Está documentado como usando "O primeiro caractere de delim " e acho que até funciona se o "primeiro caractere" já passou do final da string!)-d delim

Mas, como você sabe no seu findexemplo, é possível que um comando imprima um byte nulo e que esse byte seja canalizado para outro comando que o leia como entrada. Nenhuma parte disso depende do armazenamento de um byte nulo em uma sequência dentro do Bash . O único problema com o seu segundo exemplo é que não podemos usar $'\0'um argumento para um comando; echo "$file"$'\0'felizmente poderia imprimir o byte nulo no final, se soubesse que você queria.

Então, em vez de usar echo, você pode usar printf, que suporta os mesmos tipos de seqüências de escape que as seqüências de $'...'estilo. Dessa forma, você pode imprimir um byte nulo sem precisar ter um byte nulo dentro de uma sequência. Isso seria assim:

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

ou simplesmente isso:

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

(Nota: echona verdade, também possui um -esinalizador que permite processar \0e imprimir um byte nulo; mas também tenta processar qualquer sequência especial no seu nome de arquivo. Portanto, a printfabordagem é mais robusta.)


Aliás, existem algumas conchas que não permitem nulo bytes cordas dentro. Seu exemplo funciona bem no Zsh, por exemplo (assumindo as configurações padrão). No entanto, independentemente do seu shell, os sistemas operacionais do tipo Unix não fornecem uma maneira de incluir bytes nulos dentro de argumentos para os programas (como os argumentos do programa são passados ​​como seqüências de caracteres no estilo C), portanto sempre haverá algumas limitações. (Seu exemplo pode funcionar no Zsh apenas porque echoé um shell embutido, portanto, o Zsh pode invocá-lo sem contar com o suporte do SO para chamar outros programas. Se você usou em command echovez de echo, para que ele ignorasse o embutido e usasse o echoprograma independente no $PATH, você veria o mesmo comportamento no Zsh e no Bash.)

ruakh
fonte
2
Por que o IFS está definido como nada se -d ''já significa delimitar \0? Eu encontrei uma explicação aqui: stackoverflow.com/questions/8677546/...
CMCDragonkai