Como usar o script bash para ler o conteúdo do arquivo binário?


15

Eu quero ler um caractere e, em seguida, um comprimento fixo de string (a string não é nula terminada no arquivo e seu comprimento é fornecido pelo caractere anterior).

Como posso fazer isso em um script bash? Como definir a variável string para que eu possa fazer algum pós-processamento nela?

Respostas:


19

Se você deseja manter os utilitários de shell, pode usar headpara extrair um número de bytes e odconverter um byte em um número.

export LC_ALL=C    # make sure we aren't in a multibyte locale
n=$(head -c 1 | od -An -t u1)
string=$(head -c $n)

No entanto, isso não funciona para dados binários. Existem dois problemas:

  • A substituição de comando $(…)retira as novas linhas finais na saída do comando. Existe uma solução bastante fácil: verifique se a saída termina em um caractere diferente de uma nova linha e retire esse caractere.

    string=$(head -c $n; echo .); string=${string%.}
  • Bash, como a maioria das conchas, é ruim em lidar com bytes nulos . A partir do bash 4.1, os bytes nulos são simplesmente eliminados do resultado da substituição do comando. O traço 0.5.5 e o pdksh 5.2 têm o mesmo comportamento, e o ATT ksh para de ler no primeiro byte nulo. Em geral, os shells e seus utilitários não são voltados para lidar com arquivos binários. (Zsh é a exceção, ele foi projetado para suportar bytes nulos.)

Se você possui dados binários, convém mudar para um idioma como Perl ou Python.

<input_file perl -e '
  read STDIN, $c, 1 or die $!;    # read length byte
  $n = read STDIN, $s, ord($c);   # read data
  die $! if !defined $n;
  die "Input file too short" if ($n != ord($c));
  # Process $s here
'
<input_file python -c '
  import sys
  n = ord(sys.stdin.read(1))      # read length byte
  s = sys.stdin.read(n)           # read data
  if len(s) < n: raise ValueError("input file too short")
  # Process s here
'

+1 shell scripts nem sempre são apropriadas
forcefsck

2
exec 3<binary.file     # open the file for reading on file descriptor 3
IFS=                   #
read -N1 -u3 char      # read 1 character into variable "char"

# to obtain the ordinal value of the char "char"
num=$(printf %s "$char" | od -An -vtu1 | sed 's/^[[:space:]]*//')

read -N$num -u3 str    # read "num" chars
exec 3<&-              # close fd 3

5
read -Npára em bytes nulos, portanto, essa não é uma maneira adequada de trabalhar com dados binários. Em geral, shells diferentes de zsh não conseguem lidar com nulos.
Gilles 'SO- stop be evil'

2

Se você deseja lidar com arquivos binários no shell, a melhor opção (apenas?) É trabalhar com a ferramenta hexdump .

hexdump -v -e '/1 "%u\n"' binary.file | while read c; do
  echo $c
done

Somente leitura X bytes:

head -cX binary.file | hexdump -v -e '/1 "%u\n"' | while read c; do
  echo $c
done

Leia o comprimento (e trabalhe com 0 como comprimento) e, em seguida, "string" como valor decimal em bytes:

len=$(head -c1 binary.file | hexdump -v -e '/1 "%u\n"')
if [ $len -gt 0 ]; then
  tail -c+2 binary.file | head -c$len | hexdump -v -e '/1 "%u\n"' | while read c; do
    echo $c
  done
fi

Em vez de apenas apresentar vários comandos, você pode explicar o que eles fazem e como funcionam? O que significam as opções? Que saída o usuário pode esperar de seus comandos? Por favor, não responda nos comentários; edite  sua resposta para torná-la mais clara e completa.
G-Man diz 'Reinstate Monica'

2
Bem, eu posso copiar páginas de manual aqui, mas não vejo o ponto. Existem apenas comandos básicos usados ​​aqui, o único truque é o uso do hexdump.
Clément Moulin - SimpleRezo 16/09

2
Voto negativo porque você não gosta / entende minha resposta, sério?
Clément Moulin - SimpleRezo

1

UPDATE (com retrospectiva): ... Esta pergunta / resposta (minha resposta) me faz pensar no cachorro que continua perseguindo o carro .. Um dia, finalmente, ele alcança o carro .. Ok, ele pegou, mas ele realmente não pode fazer muito com isso ... Esse anser 'pega' as strings, mas você não pode fazer muito com elas, se elas tiverem bytes nulos incorporados ... (portanto, um grande +1 para Gilles responde .. outro idioma pode estar em ordem aqui.)

ddlê todos e quaisquer dados ... Certamente, não será exibido em zero como um "comprimento" ... mas se você tiver \ x00 em qualquer lugar dos seus dados, precisará ser criativo ao lidar com eles;ddnão possui propblems, mas seu script shell terá problemas (mas depende do que você deseja fazer com os dados) ... O seguinte basicamente gera cada "string de dados" em um arquivo com um divisor de linha entre cada strin ...

btw: Você diz "caractere" e suponho que você queira dizer "byte" ...
mas a palavra "caractere" tornou-se ambígua nos dias de UNICODE, onde apenas o conjunto de caracteres ASCII de 7 bits usa um único byte por caractere ... E mesmo no sistema Unicode, a contagem de bytes varia de acordo com o método de codificação de caracteres , por exemplo. UTF-8, UTF-16, etc.

Aqui está um script simples para destacar a diferença entre um "caractere" de texto e bytes.

STRING="௵"  
echo "CHAR count is: ${#STRING}"  
echo "BYTE count is: $(echo -n $STRING|wc -c)" 
# CHAR count is: 1
# BYTE count is: 3  # UTF-8 ecnoded (on my system)

Se o seu caractere de comprimento tiver 1 byte de comprimento e indicar um comprimento de byte , esse script deverá executar o truque, mesmo que os dados contenham caracteres Unicode ... ddsó vê bytes independentemente de qualquer configuração de localidade ...

Este script usa ddpara ler o arquivo binário e gera as strings separadas por um divisor "====" ... Consulte o próximo script para obter dados de teste

#   
div="================================="; echo $div
((skip=0)) # read bytes at this offset
while ( true ) ; do
  # Get the "length" byte
  ((count=1)) # count of bytes to read
  dd if=binfile bs=1 skip=$skip count=$count of=datalen 2>/dev/null
  (( $(<datalen wc -c) != count )) && { echo "INFO: End-Of-File" ; break ; }
  strlen=$((0x$(<datalen xxd -ps)))  # xxd is shipped as part of the 'vim-common' package
  #
  # Get the string
  ((count=strlen)) # count of bytes to read
  ((skip+=1))      # read bytes from and including this offset
  dd if=binfile bs=1 skip=$skip count=$count of=dataline 2>/dev/null
  ddgetct=$(<dataline wc -c)
  (( ddgetct != count )) && { echo "ERROR: Line data length ($ddgetct) is not as expected ($count) at offset ($skip)." ; break ; }
  echo -e "\n$div" >>dataline # add a newline for TEST PURPOSES ONLY...
  cat dataline
  #
  ((skip=skip+count))  # read bytes from and including this offset
done
#   
echo

Saída

Este script cria dados de teste que incluem um prefixo de 3 bytes por linha ...
O prefixo é um único caractere Unicode codificado em UTF-8 ...

# build test data
# ===============
  prefix="௵"   # prefix all non-zero length strings will this obvious 3-byte marker.
  prelen=$(echo -n $prefix|wc -c)
  printf \\0 > binfile  # force 1st string to be zero-length (to check zero-length logic) 
  ( lmax=3 # line max ... the last on is set to  255-length (to check  max-length logic)
    for ((i=1;i<=$lmax;i++)) ; do    # add prefixed random length lines 
      suflen=$(numrandom /0..$((255-prelen))/)  # random length string (min of 3 bytes)
      ((i==lmax)) && ((suflen=255-prelen))      # make last line full length (255) 
      strlen=$((prelen+suflen))
      printf \\$((($strlen/64)*100+$strlen%64/8*10+$strlen%8))"$prefix"
      for ((j=0;j<suflen;j++)) ; do
        byteval=$(numrandom /9,10,32..126/)  # output only printabls ASCII characters
        printf \\$((($byteval/64)*100+$byteval%64/8*10+$byteval%8))
      done
        # 'numrandom' is from package 'num-utils"
    done
  ) >>binfile
#

1
Seu código parece mais complicado do que deveria ser, especialmente o gerador de dados de teste aleatório. Você pode obter bytes aleatórios /dev/urandomna maioria dos unices. E dados de teste aleatórios não são os melhores dados de teste, lembre-se de abordar casos difíceis, como, aqui, caracteres nulos e nova linha em locais de fronteira.
Gilles 'SO- stop being evil'

Sim obrigado. Pensei em usar / dev / random, mas achei que a geração de dados de teste não era muito importante e queria testar a unidade 'numrandom' (que você mencionou em outro lugar; 'alguns recursos úteis de alguns utilitários'). Acabei de analisar sua resposta e percebi que você está fazendo praticamente a mesma coisa, exceto que é mais sucinto :) .. Eu não havia notado que você havia declarado os pontos-chave em três linhas! Eu me concentrei em suas referências em outros idiomas . Fazer funcionar foi uma boa experiência, e agora entendo melhor suas referências a outros idiomas! \ x00 pode ser uma rolha de casca
Peter.O

0

Este apenas copia um arquivo binário:

 while read -n 1 byte ; do printf "%b" "$byte" ; done < "$input" > "$output"
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.