Wie kann ich mit Binary in Bash arbeiten, um Bytes ohne Konvertierung wörtlich zu kopieren?

14

Ich versuche ehrgeizig, einen C ++ - Code aus einer Vielzahl von Gründen in Bash zu übersetzen.

Dieser Code liest und manipuliert einen für mein Unterfeld spezifischen Dateityp, der vollständig binär geschrieben und strukturiert ist. Meine erste Aufgabe im Zusammenhang mit Binärdateien besteht darin, die ersten 988 Bytes des Headers so wie sie sind zu kopieren und sie in eine Ausgabedatei zu schreiben, in die ich weiter schreiben kann, während ich den Rest der Informationen generiere.

Ich bin mir ziemlich sicher, dass meine derzeitige Lösung nicht funktioniert, und realistisch gesehen habe ich keinen guten Weg gefunden, dies festzustellen. Selbst wenn es richtig geschrieben ist, muss ich wissen, wie ich das testen würde, um sicherzugehen!

Das mache ich gerade:

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

Wenn ich diesen Teil der Datei mit hexdump / xxd auschecke, obwohl ich das meiste nicht genau lesen kann, scheint etwas nicht in Ordnung zu sein. Und der Code, den ich zum Vergleich geschrieben habe, sagt mir nur, ob zwei Zeichenfolgen identisch sind, nicht, ob sie so kopiert werden, wie ich sie haben möchte.

Gibt es eine bessere Möglichkeit, dies in Bash zu tun? Kann ich einfach binäre Bytes in native-binary kopieren / lesen, um sie wörtlich in eine Datei zu kopieren? (und idealerweise auch als Variable speichern).

Neurocoder
quelle
Sie können mit ddeinzelnen Bytes kopieren (seine Einstellung countzu 1). Ich bin mir jedoch nicht sicher, ob ich sie aufbewahren soll.
DDPWNAGE
Schlagen Sie nicht auf die C-Art, da dies zu vielen Kopfschmerzen führt. Verwenden Sie stattdessen geeignete Bash-Konstrukte
Ferrybig

Antworten:

22

Der Umgang mit Binärdaten auf einer niedrigen Ebene in Shell-Skripten ist im Allgemeinen eine schlechte Idee.

bashVariablen dürfen das Byte 0 nicht enthalten. Dies zshist die einzige Shell, die dieses Byte in ihren Variablen speichern kann.

In jedem Fall dürfen Befehlsargumente und Umgebungsvariablen diese Bytes nicht enthalten, da es sich um durch NUL getrennte Zeichenfolgen handelt, die an den execveSystemaufruf übergeben werden.

Beachten Sie auch, dass:

var=`cmd`

oder seine moderne Form:

var=$(cmd)

entfernt alle nachfolgenden Zeilenumbrüche aus der Ausgabe von cmd. Also, wenn das binäre Ausgangsenden in 0xa Bytes, wird sie verstümmelt werden , wenn in gespeichert $var.

Hier müssten Sie die Daten verschlüsselt speichern, zum Beispiel mit xxd -p.

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

Sie können Hilfsfunktionen definieren wie:

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 -pDie Ausgabe ist nicht platzsparend, da sie 1 Byte in 2 Bytes codiert. Sie erleichtert jedoch die Manipulationen (Verketten, Extrahieren von Teilen). base64ist eine, die 3 Bytes in 4 codiert, aber nicht so einfach zu bearbeiten ist.

Die ksh93Shell hat ein eingebautes Codierungsformat (verwendet base64), das Sie mit den Dienstprogrammen readund printf/ verwenden können print:

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

Wenn jetzt keine Übertragung über Shell- oder Umgebungsvariablen oder Befehlsargumente erfolgt, sollten Sie in Ordnung sein, solange die von Ihnen verwendeten Dienstprogramme einen beliebigen Bytewert verarbeiten können. Beachten Sie jedoch, dass für Textdienstprogramme die meisten Nicht-GNU-Implementierungen NUL-Bytes nicht verarbeiten können und Sie das Gebietsschema auf C korrigieren möchten, um Probleme mit Mehrbyte-Zeichen zu vermeiden. Das letzte Zeichen, das kein Zeilenvorschubzeichen ist, kann auch Probleme sowie sehr lange Zeilen verursachen (Folgen von Bytes zwischen zwei 0xa-Bytes, die länger sind LINE_MAX).

head -cwo es verfügbar ist, sollte hier OK sein, da es mit Bytes arbeiten soll und keinen Grund hat, die Daten als Text zu behandeln. So

head -c 988 < input > output

sollte in Ordnung sein. In der Praxis sind zumindest die in GNU, FreeBSD und ksh93 eingebauten Implementierungen in Ordnung. POSIX gibt die -cOption nicht an, sagt jedoch, dass headZeilen beliebiger Länge unterstützt werden sollen (nicht beschränkt auf LINE_MAX).

Mit zsh:

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

Oder:

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

Selbst zshwenn $varNUL-Bytes enthalten sind, können Sie diese als Argumente an zshBuiltins (wie printoben) oder Funktionen übergeben, nicht jedoch als Argumente an ausführbare Dateien. Argumente, die an ausführbare Dateien übergeben werden, sind durch NUL getrennte Zeichenfolgen. Dies ist eine Kernel-Einschränkung, unabhängig von der Shell.

Stéphane Chazelas
quelle
zshist nicht die einzige Shell, die ein oder mehrere NUL-Bytes in einer Shell-Variablen speichern kann. ksh93kann das auch. Intern wird ksh93die Binärvariable einfach als base64-codierte Zeichenfolge gespeichert.
fpmurphy
@ fpmurphy1, das nenne ich nicht " Umgang mit Binärdaten" . Die Variable enthält keine Binärdaten. Sie können also zum Beispiel keinen der Shell-Operatoren verwenden und sie nicht an Builtins oder Funktionen in der Variable übergeben dekodierte Form ... Ich würde es eher eingebaute Base64-Kodierungs- / Dekodierungsunterstützung nennen .
Stéphane Chazelas
11

Ich versuche ehrgeizig, einen C ++ - Code aus einer Vielzahl von Gründen in Bash zu übersetzen.

Nun ja. Aber vielleicht sollten Sie einen sehr wichtigen Grund in Betracht ziehen, es NICHT zu tun. Grundsätzlich sind "bash" / "sh" / "csh" / "ksh" und dergleichen nicht für die Verarbeitung von Binärdaten vorgesehen, und die meisten Standard-UNIX / LINUX-Dienstprogramme sind dies auch nicht.

Sie sollten sich lieber an C ++ halten oder Skriptsprachen wie Python, Ruby oder Perl verwenden, die mit Binärdaten umgehen können.

Gibt es eine bessere Möglichkeit, dies in Bash zu tun?

Der bessere Weg ist, es nicht in bash zu tun.

Stephen C
quelle
4
+1 für "Der bessere Weg ist, es nicht in bash zu tun."
Guntram Blohm unterstützt Monica am
1
Ein weiterer Grund, diesen Weg nicht einzuschlagen, besteht darin, dass die resultierende Anwendung erheblich langsamer ausgeführt wird und mehr Systemressourcen verbraucht.
Fpmurphy
Bash-Pipelines können als eine Art domänenspezifische Hochsprache fungieren, die die Verständlichkeit verbessern kann. Es gibt nichts über eine Pipeline , die nicht binär ist, und es gibt verschiedene Dienstprogramme als Kommandozeilen - Tools implementiert , dass interact mit Binärdaten ( ffmpeg, imagemagick, dd). Wenn man programmiert, anstatt Dinge zusammenzukleben, ist die Verwendung einer vollwertigen Programmiersprache der richtige Weg.
Att Righ
6

Aus Ihrer Frage:

Kopieren Sie die ersten 988 Zeilen der Kopfzeile

Wenn Sie 988 Zeilen kopieren, scheint es sich um eine Textdatei zu handeln, nicht um eine Binärdatei. Ihr Code scheint jedoch 988 Bytes und keine 988 Zeilen anzunehmen, daher gehe ich davon aus, dass Bytes korrekt sind.

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

Dieser Teil funktioniert möglicherweise nicht. Zum einen werden alle NUL-Bytes im Stream entfernt, da Sie sie ${hdr_988}als Befehlszeilenargument verwenden und Befehlszeilenargumente nicht NUL enthalten können. Die Backticks tun möglicherweise auch Whitespace-Munging (da bin ich mir nicht sicher). (Da echoes sich um eine integrierte Funktion handelt, gilt die NUL-Einschränkung möglicherweise nicht, aber ich würde sagen, dass sie immer noch zweifelhaft ist.)

Warum nicht einfach den Header direkt aus der Eingabedatei in die Ausgabedatei schreiben, ohne ihn über eine Shell-Variable zu übergeben?

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

Oder tragbarer,

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

Da Sie erwähnen, dass Sie die bashPOSIX-Shell und nicht die POSIX-Shell verwenden, steht Ihnen eine Prozessersetzung zur Verfügung. Wie wäre es also als Test?

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

Schließlich: Sehen Sie verwenden $( ... )anstelle von Backticks.

Celada
quelle
Beachten Sie, dass dd nicht unbedingt headfür nicht reguläre Dateien gleichbedeutend ist . headführt so viele read(2)Systemaufrufe durch, wie erforderlich sind, um diese 988 Bytes zu erhalten, während ddnur einer ausgeführt wird read(2). GNU ddmuss iflag=fullblockversuchen, diesen Block vollständig zu lesen, aber das ist dann noch weniger portabel als head -c.
Stéphane Chazelas