Comment puis-je travailler avec binaire en bash, pour copier des octets mot pour mot sans aucune conversion?

14

J'essaie ambitieusement de traduire un code c ++ en bash pour une myriade de raisons.

Ce code lit et manipule un type de fichier spécifique à mon sous-champ qui est écrit et structuré complètement en binaire. Ma première tâche liée aux binaires consiste à copier les 988 premiers octets de l'en-tête, exactement tels quels, et à les placer dans un fichier de sortie dans lequel je peux continuer à écrire pendant que je génère le reste des informations.

Je suis à peu près sûr que ma solution actuelle ne fonctionne pas et, en réalité, je n'ai pas trouvé un bon moyen de le déterminer. Donc, même s'il est écrit correctement, j'ai besoin de savoir comment je testerais cela pour être sûr!

Voici ce que je fais en ce moment:

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 j'utilise hexdump / xxd pour extraire cette partie du fichier, bien que je ne puisse pas en lire la plupart exactement, quelque chose semble mal. Et le code que j'ai écrit à titre de comparaison ne me dit que si deux chaînes sont identiques, pas si elles sont copiées comme je le souhaite.

Y a-t-il une meilleure façon de le faire en bash? Puis-je simplement copier / lire des octets binaires en natif-binaire, pour copier dans un fichier mot pour mot? (et idéalement pour le stockage en tant que variables également).

neurocoder
la source
Vous pouvez utiliser ddpour copier des octets individuels (en le définissant countsur 1). Je ne suis pas sûr de les stocker, cependant.
DDPWNAGE le
Ne faites pas de coups bas en C, cela créera de nombreux maux de tête. Utilisez plutôt des constructions bash appropriées
Ferrybig

Réponses:

22

Traiter des données binaires à un bas niveau dans les scripts shell est généralement une mauvaise idée.

bashles variables ne peuvent pas contenir l'octet 0. zshest le seul shell qui peut stocker cet octet dans ses variables.

Dans tous les cas, les arguments de commande et les variables d'environnement ne peuvent pas contenir ces octets car ce sont des chaînes délimitées NUL passées à l' execveappel système.

Notez également que:

var=`cmd`

ou sa forme moderne:

var=$(cmd)

supprime tous les caractères de nouvelle ligne de fin de la sortie de cmd. Donc, si cette sortie binaire se termine par 0xa octets, elle sera altérée lorsqu'elle sera stockée dans $var.

Ici, vous devez stocker les données encodées, par exemple avec xxd -p.

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

Vous pouvez définir des fonctions d'assistance comme:

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 sortie n'est pas efficace en termes d'espace car elle code 1 octet en 2 octets, mais elle facilite les manipulations avec elle (concaténation, extraction de parties). base64est celui qui code 3 octets en 4, mais n'est pas aussi facile à utiliser.

Le ksh93shell a un format de codage intégré (utilise base64) que vous pouvez utiliser avec ses utilitaires readet printf/ print:

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

Maintenant, s'il n'y a pas de transit via des variables shell ou env, ou des arguments de commande, vous devriez être OK tant que les utilitaires que vous utilisez peuvent gérer n'importe quelle valeur d'octet. Mais notez que pour les utilitaires de texte, la plupart des implémentations non GNU ne peuvent pas gérer les octets NUL, et vous voudrez corriger les paramètres régionaux en C pour éviter les problèmes avec les caractères multi-octets. Le dernier caractère n'étant pas un caractère de nouvelle ligne peut également provoquer des problèmes ainsi que des lignes très longues (séquences d'octets entre deux octets 0xa qui sont plus longs LINE_MAX).

head -coù il est disponible devrait être OK ici, car il est censé fonctionner avec des octets, et n'a aucune raison de traiter les données comme du texte. Donc

head -c 988 < input > output

ça devrait être bon. En pratique, au moins les implémentations intégrées GNU, FreeBSD et ksh93 sont OK. POSIX ne spécifie pas l' -coption, mais indique qu'il headdevrait prendre en charge les lignes de n'importe quelle longueur (sans s'y limiter LINE_MAX)

Avec zsh:

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

Ou:

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

Même dans zsh, s'il $varcontient des octets NUL, vous pouvez le passer comme argument aux commandes zshinternes (comme printci-dessus) ou aux fonctions, mais pas comme arguments aux exécutables, car les arguments passés aux exécutables sont des chaînes délimitées par NUL, c'est une limitation du noyau, indépendante du shell.

Stéphane Chazelas
la source
zshn'est pas le seul shell qui peut stocker un ou plusieurs octets NUL dans une variable shell. ksh93peut aussi le faire. En interne, ksh93stocke simplement la variable binaire sous la forme d'une chaîne codée en base64.
fpmurphy
@ fpmurphy1, ce n'est pas ce que j'appelle gérer des données binaires , la variable ne contient pas les données binaires, donc vous ne pouvez pas utiliser l'un des opérateurs de shell sur eux par exemple, vous ne pouvez pas les passer aux commandes internes ou aux fonctions dans son forme décodée ... Je l'appellerais plutôt un support d'encodage / décodage base64 intégré .
Stéphane Chazelas
11

J'essaie ambitieusement de traduire un code c ++ en bash pour une myriade de raisons.

Hé bien oui. Mais peut-être devriez-vous considérer une raison très importante de NE PAS le faire. Fondamentalement, "bash" / "sh" / "csh" / "ksh" et similaires ne sont pas conçus pour le traitement de données binaires, et la plupart des utilitaires UNIX / LINUX standard ne le sont pas non plus.

Vous feriez mieux de vous en tenir à C ++ ou d'utiliser un langage de script comme Python, Ruby ou Perl capable de traiter des données binaires.

Y a-t-il une meilleure façon de le faire en bash?

La meilleure façon est de ne pas le faire en bash.

Stephen C
la source
4
+1 pour "La meilleure façon est de ne pas le faire en bash."
Guntram Blohm prend en charge Monica le
1
Une autre raison de ne pas suivre cette voie est que l'application résultante s'exécutera beaucoup plus lentement et consommera plus de ressources système.
fpmurphy
Les pipelines Bash peuvent agir comme un langage spécifique à un domaine de haut niveau, ce qui peut améliorer la compréhension. Il n'y a rien d' un pipeline qui n'est pas binaire, et il existe différents utilitaires mis en œuvre comme outils de ligne de commande qui interagissent avec les données binaires ( ffmpeg, imagemagick, dd). Maintenant, si l'on fait de la programmation plutôt que de coller des choses ensemble, alors utiliser un langage de programmation complet est la voie à suivre.
Att Righ
6

De votre question:

copier les 988 premières lignes de l'en-tête

Si vous copiez 988 lignes, cela ressemble à un fichier texte, pas binaire. Cependant, votre code semble supposer 988 octets, pas 988 lignes, donc je suppose que les octets sont corrects.

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

Cette partie peut ne pas fonctionner. D'une part, tous les octets NUL du flux seront supprimés, car vous les utilisez ${hdr_988}comme argument de ligne de commande et les arguments de ligne de commande ne peuvent pas contenir NUL. Les backticks pourraient également faire du munging d'espaces blancs (je n'en suis pas sûr). (En fait, comme il echos'agit d'une fonction intégrée, la restriction NUL peut ne pas s'appliquer, mais je dirais qu'elle est encore incertaine.)

Pourquoi ne pas simplement écrire l'en-tête directement du fichier d'entrée dans le fichier de sortie, sans le passer par une variable shell?

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

Ou, plus facilement,

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

Puisque vous mentionnez que vous utilisez bash, et non le shell POSIX, vous avez la substitution de processus à votre disposition, alors qu'en est-il comme test?

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

Enfin: envisagez d' utiliser $( ... )au lieu de backticks.

Celada
la source
Notez que ce ddn'est pas nécessairement équivalent à headpour les fichiers non réguliers. headfera autant d' read(2)appels système que nécessaire pour obtenir ces 988 octets tandis qu'il ddn'en fera qu'un read(2). GNU dda iflag=fullblockpour essayer de lire ce bloc en entier, mais c'est encore moins portable que head -c.
Stéphane Chazelas