Como posso trabalhar com binário no bash, para copiar bytes literalmente sem qualquer conversão?

14

Estou ambiciosamente tentando traduzir um código c ++ no bash por uma infinidade de razões.

Este código lê e manipula um tipo de arquivo específico para o meu subcampo que é escrito e estruturado completamente em binário. Minha primeira tarefa relacionada ao binário é copiar os primeiros 988 bytes do cabeçalho, exatamente como estão, e colocá-los em um arquivo de saída no qual eu possa continuar gravando enquanto gere o restante das informações.

Tenho certeza de que minha solução atual não está funcionando e, realisticamente, não descobri uma boa maneira de determinar isso. Portanto, mesmo que esteja realmente escrito corretamente, preciso saber como testaria isso para ter certeza!

Isto é o que estou fazendo agora:

hdr_988=`head -c 988 ${inputFile}`
echo -n "${hdr_988}" > ${output_hdr}
headInput=`head -c 988 ${inputTrack} | hexdump`
headOutput=`head -c 988 ${output_hdr} | hexdump`
if [ "${headInput}" != "${headOutput}" ]; then echo "output header was not written properly.  exiting.  please troubleshoot."; exit 1; fi

Se eu usar o hexdump / xxd para verificar esta parte do arquivo, embora não possa ler exatamente a maior parte, algo parece errado. E o código em que escrevi para comparação apenas me diz se duas strings são idênticas, não se elas são copiadas da maneira que eu quero que elas sejam.

Existe uma maneira melhor de fazer isso no bash? Posso simplesmente copiar / ler bytes binários no binário nativo, para copiar para um arquivo literalmente? (e, idealmente, para armazenar também como variáveis).

neurocoder
fonte
Você pode usar ddpara copiar bytes individuais (configurando countcomo 1). Eu não tenho certeza sobre armazená-los, no entanto.
DDPWNAGE
Não faça bash da maneira C, pois isso criará muitas dores de cabeça. Em vez disso usar construções festança adequadas
Ferrybig

Respostas:

22

Lidar com dados binários em um nível baixo em scripts de shell geralmente é uma má idéia.

bashAs variáveis ​​não podem conter o byte 0. zshé o único shell que pode armazenar esse byte em suas variáveis.

Em qualquer caso, argumentos de comando e variáveis ​​de ambiente não podem conter esses bytes, pois são cadeias delimitadas por NUL passadas para a execvechamada do sistema.

Observe também que:

var=`cmd`

ou sua forma moderna:

var=$(cmd)

retira todos os caracteres de nova linha à direita da saída de cmd. Portanto, se essa saída binária terminar em 0xa bytes, ela será mutilada quando armazenada em $var.

Aqui, você precisará armazenar os dados codificados, por exemplo, com xxd -p.

hdr_988=$(head -c 988 < "$inputFile" | xxd -p)
printf '%s\n' "$hdr_988" | xxd -p -r > "$output_hdr"

Você pode definir funções auxiliares como:

encode() {
  eval "$1"='$(
    shift
    "$@" | xxd -p  -c 0x7fffffff
    exit "${PIPESTATUS[0]}")'
}

decode() {
  printf %s "$1" | xxd -p -r
}

encode var cat /bin/ls &&
  decode "$var" | cmp - /bin/ls && echo OK

xxd -pa saída não é eficiente em termos de espaço, pois codifica 1 byte em 2 bytes, mas facilita a manipulação (concatenação, extração de peças). base64é aquele que codifica 3 bytes em 4, mas não é tão fácil de trabalhar.

O ksh93shell possui um formato de codificação interno (usos base64) que você pode usar com seus reade printf/ printutilitários:

typeset -b var # marked as "binary"/"base64-encoded"
IFS= read -rn 988 var < input
printf %B var > output

Agora, se não houver trânsito por meio de variáveis ​​shell ou env, ou argumentos de comando, você deve ficar bem desde que os utilitários que você usa possam lidar com qualquer valor de byte. Mas observe que para utilitários de texto, a maioria das implementações não-GNU não pode manipular NUL bytes, e você deseja corrigir o código do idioma em C para evitar problemas com caracteres de vários bytes. O último caractere que não é um caractere de nova linha também pode causar problemas e linhas muito longas (sequências de bytes entre dois bytes de 0xa maiores que esse LINE_MAX).

head -conde está disponível, deve estar OK aqui, pois deve funcionar com bytes e não tem motivos para tratar os dados como texto. então

head -c 988 < input > output

deve estar ok. Na prática, pelo menos as implementações internas GNU, FreeBSD e ksh93 estão OK. O POSIX não especifica a -copção, mas diz que headdeve suportar linhas de qualquer comprimento (não se limitando a LINE_MAX)

Com zsh:

IFS= read -rk988 -u0 var < input &&
print -rn -- $var > output

Ou:

var=$(head -c 988 < input && echo .) && var=${var%.}
print -rn -- $var > output

Mesmo que zsh, se $varcontenha NUL bytes, você pode passá-lo como argumento para zshbuiltins (como printacima) ou funções, mas não como argumentos para executáveis, pois os argumentos passados ​​para executáveis ​​são seqüências delimitadas por NUL, que é uma limitação do kernel, independente do shell.

Stéphane Chazelas
fonte
zshnão é o único shell que pode armazenar um ou mais bytes NUL em uma variável do shell. ksh93pode fazer isso também. Internamente, ksh93simplesmente armazena a variável binária como uma sequência codificada em base64.
precisa saber é
@ fpmurphy1, não é isso que eu chamo de manipulação de dados binários , a variável não contém os dados binários; portanto, você não pode usar nenhum dos operadores de shell neles, por exemplo, não pode passá-los para funções ou funções internas forma decodificada ... Eu chamaria isso de suporte embutido de codificação / decodificação base64 .
Stéphane Chazelas
11

Estou ambiciosamente tentando traduzir um código c ++ no bash por uma infinidade de razões.

Bem, sim. Mas talvez você deva considerar uma razão muito importante para NÃO fazer isso. Basicamente, "bash" / "sh" / "csh" / "ksh" e similares não são projetados para o processamento de dados binários, nem a maioria dos utilitários padrão do UNIX / LINUX.

É melhor você ficar com o C ++ ou usar uma linguagem de script como Python, Ruby ou Perl que seja capaz de lidar com dados binários.

Existe uma maneira melhor de fazer isso no bash?

A melhor maneira é não fazê-lo no bash.

Stephen C
fonte
4
+1 em "A melhor maneira é não fazer isso no bash".
Guntram Blohm suporta Monica
1
Outro motivo para não seguir esse caminho é que o aplicativo resultante será executado significativamente mais devagar e consumirá mais recursos do sistema.
precisa saber é o seguinte
Os pipelines Bash podem atuar como uma linguagem específica de domínio de alto nível, que pode aumentar a compreensibilidade. Não há nada sobre um oleoduto que não é binário, e há vários utilitários implementados como ferramentas de linha de comando que interagem com dados binários ( ffmpeg, imagemagick, dd). Agora, se você está fazendo programação, em vez de colar coisas, usar uma linguagem de programação com potência total é o caminho a percorrer.
Att Righ
6

Da sua pergunta:

copie as primeiras 988 linhas do cabeçalho

Se você estiver copiando 988 linhas, parece um arquivo de texto, não binário. No entanto, seu código parece assumir 988 bytes, não 988 linhas, portanto, assumirei que os bytes estão corretos.

hdr_988=`head -c 988 ${inputFile}`
echo -n "${hdr_988}" > ${output_hdr}

Esta parte pode não funcionar. Por um lado, quaisquer bytes NUL no fluxo serão removidos, porque você usa ${hdr_988}como argumento de linha de comando, e os argumentos de linha de comando não podem conter NUL. Os backticks também podem estar mudando de espaço em branco (não tenho certeza disso). (Na verdade, como echoé um componente interno, a restrição NUL pode não se aplicar, mas eu diria que ainda é duvidoso.)

Por que não escrever o cabeçalho diretamente do arquivo de entrada no arquivo de saída, sem passar por uma variável de shell?

head -c 988 "${inputFile}" >"${output_hdr}"

Ou, de maneira mais portável,

dd if="${inputFile}" of="${output_hdr}" bs=988 count=1

Como você menciona que está usando bash, e não o shell POSIX, você tem a substituição de processo disponível para você, então que tal isso como teste?

cmp <(head -c 988 "${inputFile}") <(head -c 988 "${output_hdr}")

Finalmente: considere usar em $( ... )vez de reticulares.

Celada
fonte
Observe que isso ddnão é necessariamente equivalente a headarquivos não regulares. headfará quantas read(2)chamadas de sistema forem necessárias para obter esses 988 bytes, enquanto ddfará apenas uma read(2). O GNU ddprecisa iflag=fullblocktentar ler esse bloco na íntegra, mas isso é ainda menos portátil que head -c.
Stéphane Chazelas