Como lidar com dados binários brutos em um pipe do bash?

15

Eu tenho uma função bash que usa um arquivo como parâmetro, verifica se o arquivo existe e, em seguida, grava qualquer coisa saindo do stdin no arquivo. A solução ingênua funciona bem para texto, mas estou tendo problemas com dados binários arbitrários.

echo -n '' >| "$file" #Truncate the file
while read lines
do  # Is there a better way to do this? I would like one...
    echo $lines >> "$file"
done
David Souther
fonte

Respostas:

15

Seu caminho é adicionar quebras de linha a tudo o que ele escreve no espaço de qualquer separador ( $IFS) usado para dividir a leitura. Em vez de dividi-lo em novas linhas, pegue a coisa toda e a repasse. Você pode reduzir todo o código acima para isso:

 cat - > $file

Você não precisa do bit truncado, isso truncará e gravará todo o fluxo STDIN nele.

Edit: Se você estiver usando o zsh, basta usar > $fileno lugar do gato. Você está redirecionando para um arquivo e truncando-o, mas se houver algo esperando por algo para aceitar STDIN, ele será lido nesse momento. Eu acho que você pode fazer algo assim com o bash, mas você teria que definir um modo especial.

Caleb
fonte
Não consegui fazer o exemplo de redirecionamento stdin funcionar, mas alterei o exemplo de gato para> | (Eu tenho um conjunto de noclobber) funciona como um encanto. Obrigado por fazer o meu dia ^. ^
David Souther
+1 para a versão sem gatos. Sempre evitar gatos inúteis;)
rozcietrzewiacz
@rozcietrzewiacz: É verdade, exceto que foi uma reflexão tardia e eu estava errado. Isso pode não ser um uso inútil de gato. A única coisa que você pode fazer é > $file. Isso funciona apenas como a primeira coisa que procura stdin no script do shell pai. Basicamente, todo o código de David pode ser reduzido a um único caractere, mas acho que cat -é mais elegante e menos problemático, porque é compreendido à vista.
Caleb
Às vezes eu catpasso quatro ou cinco segundos juntos, apenas para irritar os fanáticos do UUOC
Michael Mrozek
@MichaelMrozek: Às vezes, eu nomeio meus arquivos de dados catapenas para que as pessoas que insistem em usá-lo necessariamente tenham que fazer ginástica mental para ler o código. Os pipes nomeados também são bons alvos.
Caleb
7

Para ler um arquivo de texto literalmente, não use plain read, que processa a saída de duas maneiras:

  • readinterpreta \como um caractere de escape; use read -rpara desativar isso.
  • readdivide-se em palavras nos caracteres $IFS; defina IFScomo uma string vazia para desativar isso.

O idioma usual para processar um arquivo de texto linha por linha é

while IFS= read -r line; do 

Para obter uma explicação desse idioma, consulte Por que é while IFS= readusado com tanta frequência, em vez de IFS=; while read..? .

Para escrever uma string literalmente, não use simplesmente plain echo, que processa a string de duas maneiras:

  • Em algumas conchas, os echoprocessos de barra invertida escapam. (No bash, depende se a xpg_echoopção está definida.)
  • Algumas seqüências de caracteres são tratadas como opções, por exemplo, -nou -e(o conjunto exato depende do shell).

Uma maneira portátil de imprimir uma string é literalmente printf. (Não há maneira melhor no bash, a menos que você saiba que sua entrada não parece uma opção echo.) Use o primeiro formulário para imprimir a sequência exata e o segundo formulário se desejar adicionar uma nova linha.

printf %s "$line"
printf '%s\n' "$line"

Isso é adequado apenas para o processamento de texto , porque:

  • A maioria dos shells engasga com caracteres nulos na entrada.
  • Quando você lê a última linha, não tem como saber se havia uma nova linha no final ou não. (Alguns shells mais antigos podem ter problemas maiores se a entrada não terminar com uma nova linha.)

Você não pode processar dados binários no shell, mas as versões modernas de utilitários na maioria das unidades podem lidar com dados arbitrários. Para passar toda a entrada para a saída, use cat. Entrar na tangente echo -n ''é uma maneira complicada e não portátil de não fazer nada; echo -nseria tão bom (ou não, dependendo do shell) e :é mais simples e totalmente portátil.

: >| "$file"
cat >>"$file"

ou, mais simples,

cat >|"$file"

Em um script, você geralmente não precisa usar, >|pois noclobberestá desativado por padrão.

Gilles 'SO- parar de ser mau'
fonte
obrigado por apontar xpg_echo, esse é realmente um problema que eu estava tendo em outro lugar no meu código e nem percebi. Re noclobber, eu tenho o hábito de ligá-lo no meu bashrc.
David Souther
0

Isso fará exatamente o que você deseja:

( while read -r -d '' ; do
    printf %s'\0' "${REPLY}" ;
  done ;

  # When read hits EOF, it returns non-zero which exits the while loop.
  # That data still needs to be output:
  printf %s "${REPLY}"
) >> ${file}

Observe o uso de memória. Isso lê a entrada de maneira delimitada por nulo.

Se não houver bytes \0 nulos na entrada, o bash precisará primeiro ler todo o conteúdo da entrada na memória e, em seguida, imprimi-lo.

Em relação à sua etapa truncada:

echo -n '' >| "$file" #Truncate the file

muito mais simples e equivalente é:

> ${file}   #Truncate the file
Marc Tamsky
fonte