¿Cómo puedo trabajar con binario en bash para copiar bytes textualmente sin ninguna conversión?

14

Intento ambiciosamente traducir un código de C ++ a bash por una miríada de razones.

Este código lee y manipula un tipo de archivo específico para mi subcampo que está escrito y estructurado completamente en binario. Mi primera tarea relacionada con el binario es copiar los primeros 988 bytes del encabezado, exactamente como está, y ponerlos en un archivo de salida en el que pueda continuar escribiendo mientras genero el resto de la información.

Estoy bastante seguro de que mi solución actual no funciona, y de manera realista no he descubierto una buena manera de determinar esto. Entonces, incluso si está escrito correctamente, ¡necesito saber cómo probaría esto para estar seguro!

Esto es lo que estoy haciendo ahora:

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

Si uso hexdump / xxd para revisar esta parte del archivo, aunque no puedo leer exactamente la mayor parte, algo parece estar mal. Y el código que he escrito para comparar solo me dice si dos cadenas son idénticas, no si están copiadas de la forma en que quiero que estén.

¿Hay una mejor manera de hacer esto en bash? ¿Puedo simplemente copiar / leer bytes binarios en binario nativo, para copiarlos en un archivo literalmente? (e idealmente para almacenar como variables también).

neurocoder
fuente
Puede usar ddpara copiar bytes individuales (estableciendo su counten 1). Sin embargo, no estoy seguro de almacenarlos.
DDPWNAGE
No golpees en la forma C, creará muchos dolores de cabeza. En su lugar, use construcciones de bash adecuadas
Ferrybig

Respuestas:

22

Tratar con datos binarios a un nivel bajo en scripts de shell es generalmente una mala idea.

bashlas variables no pueden contener el byte 0. zshes el único shell que puede almacenar ese byte en sus variables.

En cualquier caso, los argumentos de comando y las variables de entorno no pueden contener esos bytes, ya que son cadenas delimitadas por NUL que se pasan a la execvellamada del sistema.

También tenga en cuenta que:

var=`cmd`

o su forma moderna:

var=$(cmd)

elimina todos los caracteres de nueva línea finales de la salida de cmd. Entonces, si esa salida binaria termina en 0xa bytes, se destruirá cuando se almacene en $var.

Aquí, necesitaría almacenar los datos codificados, por ejemplo con xxd -p.

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

Podría definir funciones 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 -pla salida no es eficiente en cuanto al espacio, ya que codifica 1 byte en 2 bytes, pero hace que sea más fácil hacer manipulaciones con ella (concatenación, extracción de partes). base64es uno que codifica 3 bytes en 4, pero no es tan fácil trabajar con él.

El ksh93shell tiene un formato de codificación incorporado (usos base64) que puede usar con sus ready printf/ printutilidades:

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

Ahora, si no hay tránsito a través de variables de shell o env, o argumentos de comando, debe estar bien siempre que las utilidades que use puedan manejar cualquier valor de byte. Pero tenga en cuenta que para las utilidades de texto, la mayoría de las implementaciones que no son GNU no pueden manejar bytes NUL, y querrá arreglar la configuración regional en C para evitar problemas con los caracteres de varios bytes. El último carácter que no es un carácter de nueva línea también puede causar problemas, así como líneas muy largas (secuencias de bytes entre dos bytes 0xa más largos LINE_MAX).

head -cdonde esté disponible debería estar bien aquí, ya que está destinado a trabajar con bytes y no tiene ninguna razón para tratar los datos como texto. Entonces

head -c 988 < input > output

debería estar bien. En la práctica, al menos las implementaciones incorporadas de GNU, FreeBSD y ksh93 están bien. POSIX no especifica la -copción, pero dice que headdebería admitir líneas de cualquier longitud (no limitado a LINE_MAX)

Con zsh:

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

O:

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

Incluso en el zshcaso de que $varcontenga bytes NUL, puede pasarlo como argumento a las zshfunciones incorporadas (como printarriba) o funciones, pero no como argumentos a los ejecutables, ya que los argumentos pasados ​​a los ejecutables son cadenas delimitadas por NUL, eso es una limitación del núcleo, independiente del shell.

Stéphane Chazelas
fuente
zshno es el único shell que puede almacenar uno o más bytes NUL en una variable de shell. ksh93Puede hacerlo también. Internamente, ksh93simplemente almacena la variable binaria como una cadena codificada en base64.
fpmurphy
@ fpmurphy1, eso no es lo que yo llamo manejo de datos binarios , la variable no contiene los datos binarios, por lo que no puede usar ninguno de los operadores de shell en ellos, por ejemplo, no puede pasarlos a funciones o funciones incorporadas en su forma decodificada ... lo llamaría más bien soporte de codificación / decodificación base64 incorporado .
Stéphane Chazelas
11

Intento ambiciosamente traducir un código de C ++ a bash por una miríada de razones.

Bueno, sí. Pero tal vez debería considerar una razón muy importante para NO hacerlo. Básicamente, "bash" / "sh" / "csh" / "ksh" y similares no están diseñados para procesar datos binarios, y tampoco lo son la mayoría de las utilidades estándar de UNIX / LINUX.

Sería mejor quedarse con C ++ o usar un lenguaje de script como Python, Ruby o Perl que sea capaz de manejar datos binarios.

¿Hay una mejor manera de hacer esto en bash?

La mejor manera es no hacerlo en bash.

Stephen C
fuente
44
+1 para "La mejor manera es no hacerlo en bash".
Guntram Blohm apoya a Monica el
1
Otra razón para no seguir esta ruta es que la aplicación resultante se ejecutará significativamente más lento y consumirá más recursos del sistema.
fpmurphy
Las canalizaciones de bash pueden actuar como un lenguaje de dominio específico de alto nivel que puede aumentar la comprensibilidad. No hay nada acerca de una tubería que no es binaria, y hay varias utilidades implementadas como herramientas de línea de comandos que interactúan con los datos binarios ( ffmpeg, imagemagick, dd). Ahora, si uno está programando en lugar de pegar cosas, entonces usar un lenguaje de programación totalmente desarrollado es el camino a seguir.
Att Righ
6

De tu pregunta:

copie las primeras 988 líneas del encabezado

Si está copiando 988 líneas, entonces parece un archivo de texto, no binario. Sin embargo, su código parece asumir 988 bytes, no 988 líneas, por lo que supondré que los bytes son correctos.

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

Esta parte puede no funcionar. Por un lado, cualquier byte NUL en la secuencia se eliminará, ya que se usa ${hdr_988}como argumento de línea de comando, y los argumentos de línea de comando no pueden contener NUL. Los backticks también podrían estar haciendo espacios en blanco (no estoy seguro de eso). (En realidad, dado que echoestá integrado, la restricción NUL podría no aplicarse, pero yo diría que todavía es dudosa).

¿Por qué no simplemente escribir el encabezado directamente desde el archivo de entrada al archivo de salida, sin pasarlo a través de una variable de shell?

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

O, más portablemente,

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

Dado que usted menciona que está utilizando bash, no el shell POSIX, tiene disponible la sustitución de procesos, entonces, ¿qué tal esto como prueba?

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

Finalmente: considere usar en $( ... )lugar de backticks.

Celada
fuente
Tenga en cuenta que ddno es necesariamente equivalente a headpara archivos no regulares. headhará tantas read(2)llamadas al sistema como sea necesario para obtener esos 988 bytes, mientras ddque solo hará una read(2). GNU ddtiene iflag=fullblockque probar y leer ese bloque completo, pero eso es aún menos portátil que head -c.
Stéphane Chazelas