如何在 bash 中使用二进制文件,逐字复制字节而不进行任何转换?

neu*_*der 17 bash binary head

由于种种原因,我雄心勃勃地尝试将 c++ 代码转换为 bash。

此代码读取和操作特定于我的子字段的文件类型,该文件类型完全以二进制形式编写和构建。我的第一个与二进制相关的任务是完全按原样复制头的前 988 个字节,并将它们放入一个输出文件中,我可以在生成其余信息时继续写入该文件。

我很确定我当前的解决方案不起作用,实际上我还没有找到确定这一点的好方法。因此,即使它实际上编写正确,我也需要知道如何测试以确保!

这就是我现在正在做的:

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
Run Code Online (Sandbox Code Playgroud)

如果我使用 hexdump/xxd 来检查文件的这一部分,虽然我不能完全阅读其中的大部分内容,但似乎有些不对劲。我为比较而编写的代码只告诉我两个字符串是否相同,而不是按照我希望的方式复制它们。

有没有更好的方法在 bash 中做到这一点?我可以简单地复制/读取本机二进制中的二进制字节,逐字复制到文件吗?(理想情况下也存储为变量)。

Sté*_*las 23

在 shell 脚本中处理低级别的二进制数据通常是一个坏主意。

bash变量不能包含字节 0。zsh是唯一可以将该字节存储在其变量中的 shell。

在任何情况下,命令参数和环境变量都不能包含这些字节,因为它们是传递给execve系统调用的NUL 分隔字符串。

另请注意:

var=`cmd`
Run Code Online (Sandbox Code Playgroud)

或其现代形式:

var=$(cmd)
Run Code Online (Sandbox Code Playgroud)

cmd. 因此,如果该二进制输出以 0xa 字节结尾,则存储在$var.

在这里,您需要存储编码的数据,例如使用xxd -p.

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

您可以定义辅助函数,例如:

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
Run Code Online (Sandbox Code Playgroud)

xxd -p输出空间效率不高,因为它将 1 个字节编码为 2 个字节,但它可以更轻松地对其进行操作(连接、提取部分)。base64是一种将 3 个字节编码为 4 个字节,但不太容易使用。

ksh93外壳具有编码格式(使用内置的base64),你可以用它的使用readprintf/print公用事业:

typeset -b var # marked as "binary"/"base64-encoded"
IFS= read -rn 988 var < input
printf %B var > output
Run Code Online (Sandbox Code Playgroud)

现在,如果没有通过 shell 或 env 变量或命令参数进行传输,只要您使用的实用程序可以处理任何字节值,就应该没问题。但请注意,对于文本实用程序,大多数非 GNU 实现无法处理 NUL 字节,您需要将语言环境修复为 C 以避免多字节字符出现问题。最后一个不是换行符的字符也可能导致问题以及很长的行(两个 0xa 字节之间的字节序列更长LINE_MAX)。

head -c它可用的地方在这里应该没问题,因为它意味着使用字节,并且没有理由将数据视为文本。所以

head -c 988 < input > output
Run Code Online (Sandbox Code Playgroud)

应该可以。在实践中,至少 GNU、FreeBSD 和 ksh93 内置实现是可以的。POSIX 没有指定-c选项,但表示head应该支持任何长度的行(不限于LINE_MAX

zsh

IFS= read -rk988 -u0 var < input &&
print -rn -- $var > output
Run Code Online (Sandbox Code Playgroud)

或者:

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

即使在zsh, 如果$var包含 NUL 字节,您也可以将其作为参数传递给zsh内置print函数(如上)或函数,但不能作为参数传递给可执行文件,因为传递给可执行文件的参数是 NUL 分隔的字符串,这是内核限制,独立于外壳。


小智 11

由于种种原因,我雄心勃勃地尝试将 c++ 代码转换为 bash。

嗯,是。但也许你应该考虑一个非常重要的不这样做的原因。基本上,“bash”/“sh”/“csh”/“ksh”等不是为处理二进制数据而设计的,大多数标准 UNIX/LINUX 实用程序也不是。

您最好坚持使用 C++,或者使用能够处理二进制数据的脚本语言,如 Python、Ruby 或 Perl。

有没有更好的方法在 bash 中做到这一点?

更好的方法是不要在 bash 中执行此操作。

  • +1 表示“更好的方法是不要在 bash 中进行。” (4认同)

Cel*_*ada 6

从你的问题:

复制标题的前 988 行

如果您要复制 988 行,那么它看起来像一个文本文件,而不是二进制文件。但是,您的代码似乎假定为 988 字节,而不是 988 行,因此我假定字节是正确的。

hdr_988=`head -c 988 ${inputFile}`
echo -n "${hdr_988}" > ${output_hdr}
Run Code Online (Sandbox Code Playgroud)

这部分可能不起作用。一方面,流中的任何 NUL 字节都将被剥离,因为您${hdr_988}用作命令行参数,而命令行参数不能包含 NUL。反引号也可能会进行空格处理(我不确定)。(实际上,由于echo是内置的,NUL 限制可能不适用,但我会说它仍然不确定。)

为什么不直接将标头从输入文件写入输出文件,而不通过 shell 变量传递它?

head -c 988 "${inputFile}" >"${output_hdr}"
Run Code Online (Sandbox Code Playgroud)

或者,更便携的是,

dd if="${inputFile}" of="${output_hdr}" bs=988 count=1
Run Code Online (Sandbox Code Playgroud)

既然您提到您使用的是bash,而不是 POSIX shell,那么您可以使用进程替换,那么作为测试如何呢?

cmp <(head -c 988 "${inputFile}") <(head -c 988 "${output_hdr}")
Run Code Online (Sandbox Code Playgroud)

最后:考虑使用$( ... )而不是反引号。