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

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

出于多种原因,我雄心勃勃地尝试将 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

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

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

答案1

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

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

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

另请注意:

var=`cmd`

或其现代形式:

var=$(cmd)

从 的输出中删除所有尾随换行符cmd。那么,如果那样的话二进制输出以 0xa 字节结尾,存储在$var.

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

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

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

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输出的空间效率不高,因为它将 1 个字节编码为 2 个字节,但它使使用它进行操作(连接、提取部分)变得更容易。base64是将 3 个字节编码为 4 个字节的一种,但使用起来并不容易。

shellksh93有一个内置的编码格式 (uses base64),您可以将其与它的readprintf/print实用程序一起使用:

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

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

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

head -c 988 < input > output

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

zsh

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

或者:

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

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

答案2

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

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

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

在 bash 中是否有更好的方法来做到这一点?

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

答案3

从你的问题来看:

复制标题的前 988 行

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

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

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

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

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

或者,更便携的是,

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

既然您提到您正在使用bash,而不是 POSIX shell,您可以使用进程替换,那么将此作为测试怎么样?

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

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

相关内容