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

14

Eu estou ambiciosamente tentando traduzir um código c + + para o bash por uma miríade de razões.

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

Tenho certeza de que minha solução atual não está funcionando e, de forma realista, não descobri uma boa maneira de determinar isso. Então, mesmo que seja escrito corretamente, eu preciso saber como eu testaria isso para ter certeza!

Isso é 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 essa parte do arquivo, embora não possa ler exatamente a maior parte, algo parece errado. E o código que escrevi para comparação apenas me diz se duas cadeias de caracteres são idênticas, não se forem 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 em binários nativos, para copiar para um arquivo na íntegra? (e idealmente para armazenar também como variáveis).

    
por neurocoder 23.04.2016 / 22:18

3 respostas

21

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

bash variables 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 strings delimitadas por NUL transmitidas à chamada de sistema execve .

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 . Então, se a saída binária terminar em 0xa bytes, ela será desconfigurada quando armazenada em $var .

Aqui, você precisa 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 -p output não é eficiente em termos de espaço, pois codifica 1 byte em 2 bytes, mas facilita a manipulação com ele (concatenando, extraindo partes). base64 é aquele que codifica 3 bytes em 4, mas não é tão fácil de trabalhar.

O shell ksh93 tem um formato de codificação incorporado (usa base64 ) que você pode usar com seus utilitários read e printf / print :

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ê deverá estar OK, desde que os utilitários utilizados possam manipular qualquer valor de byte. Mas note que, para utilitários de texto, a maioria das implementações não-GNU não pode manipular bytes NUL, e você vai querer corrigir o código de idioma para C para evitar problemas com caracteres multi-byte. O último caractere que não é um caractere de nova linha também pode causar problemas, bem como linhas muito longas (seqüências de bytes entre dois bytes de 0xa que são maiores que LINE_MAX ).

head -c onde está disponível deve estar OK aqui, já que funciona 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 embutidas GNU, FreeBSD e ksh93 estão OK. POSIX não especifica a opção -c , mas diz que head deve suportar linhas de qualquer tamanho (não limitado 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 em zsh , se $var contiver bytes NUL, você pode passá-lo como argumento para zsh builtins (como print acima) ou funções, mas não como argumentos para executáveis, como argumentos passados para executáveis são strings delimitadas por NUL, que é uma limitação do kernel, independente do shell.

    
por 23.04.2016 / 22:33
11

I am ambitiously trying to translate a c++ code into bash for a myriad of reasons.

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 processar dados binários, e nenhum dos utilitários padrão UNIX / LINUX são os mais utilizados.

Seria melhor ficar com o C ++ ou usar linguagem de script como Python, Ruby ou Perl, capaz de lidar com dados binários.

Is there a better way to do this in bash?

A melhor maneira é não fazer isso no bash.

    
por 24.04.2016 / 08:29
6

Da sua pergunta:

copy the first 988 lines of the header

Se você está copiando 988 linhas, então parece um arquivo de texto, não binário. No entanto, seu código parece assumir 988 bytes, e 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 eliminados, porque você usa ${hdr_988} como um argumento de linha de comando e os argumentos de linha de comando não podem conter NUL. Os backticks podem estar fazendo o espaço em branco também (não tenho certeza disso). (Na verdade, como echo é um built-in, a restrição NUL pode não se aplicar, mas eu diria que ainda é duvidoso.)

Por que não apenas escrever o cabeçalho diretamente do arquivo de entrada no arquivo de saída, sem passá-lo através de uma variável shell?

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

Ou mais portável,

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

Já que você mencionou que está usando bash , não o shell POSIX, você tem a substituição do processo disponível para você, então que tal isso como um teste?

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

Por fim: considere usando $( ... ) em vez de backticks.

    
por 23.04.2016 / 22:32