数字进制转换的奇技淫巧

在本文中,我将介绍一些稍微深奥的内容:在 shell 脚本中转换数字进制。真正需要考虑的常用数字进制有四种:二进制、八进制、十进制和十六进制。您习惯于使用十进制(base-10),所以 10 = 1 * 10**1 + 0,而 100 = 1 * 10**2 + 0 * 10**1 + 0。
这同样适用于其他数字进制,所以二进制(base-2)的 1010 实际上是 1 * 2**3 + 0 * 2**2 + 1 * 2**1 + 0,即 8 + 0 + 2 + 0 = 十进制的 10。八进制也是如此,所以八进制(base-8)的 33 转换为十进制为 3 * 8**1 + 3 = 27。
十六进制提出了不同的挑战,因为十六进制(base-16)的计数系统不能完全融入我们的阿拉伯数字 0, 1, 2, ... 9。“Hex”(十六进制的非正式名称)添加了 A、B、C、D、E 和 F,因此十进制值 10 在十六进制中表示为 “A”。这就是数学变得有趣的地方,所以十六进制(base-16)的 33 = 3 * 16**1 + 3 = 48 + 3 = 51。
因此,创建进制转换实用程序的冗长而复杂的方法是,分解给定的每个值并应用所示的转换,然后使用一个通用进制(可能是十进制)的内部值,再编写另一个例程将通用进制转换为所需的输出进制。
正如我将要讨论的那样,有更聪明的方法来做到这一点,但现在,让我们看看 bc
命令,它支持用户指定输入和输出的数字进制。bc
,即二进制计算器,使用起来有点棘手,因为它是一个老式的 UNIX 命令。正如我在我的书《Wicked Cool Shell Scripts》中详细讨论的那样,使用简陋但交互式的 bc
程序最常见的方法是使用 echo
向其发送所需的命令,如下所示
$ echo '333 * 0.35' | bc
116.55
很有用(尤其因为 expr
和 $(( ))
不能处理浮点数和十进制值),但真正有趣的是输入和输出的数字进制。
假设我想确认我之前列出的一个转换,将十六进制的 33 转换为十进制。这很容易做到
$ echo 'ibase=16; 33' | bc
51
这很简单。现在,让我们做一些更大更复杂的事情
$ echo 'ibase=16; FEF33D9' | bc
267334617
ibase
是输入的数字进制。输出进制被指定为 obase
。就是这样——很简单!
所以让我们以相同的十六进制值作为输入,但强制输出为八进制,而不是默认的十进制
$ echo 'ibase=16; obase=8; FEF33D9' | bc
1773631731
您更喜欢使用二进制吗?您也可以这样做
$ echo 'ibase=16; obase=2; FEF33D9' | bc
1111111011110011001111011001
确实有很多 1 和 0。这让我想起了《星际穿越》,但这完全是另一篇文章了!
有了这些知识,很容易编写一个基本的 shell 脚本,用于在二进制、八进制、十进制和十六进制之间进行转换
ibase=10; obase=10 # set up defaults
usage() {
echo "Usage: $(basename $0) -i base -o base value" 1>&2
echo " where base can be 2, 8, 10 or 16." 1>&2
exit 1
}
while getopts "i:o:" value ; do
case "$value" in
i) ibase=$OPTARG
(( ibase == 2 || ibase == 8 || ibase == 10 ||
ibase == 16 )) || usage
;;
o) obase=$OPTARG
(( obase == 2 || obase == 8 || obase == 10 ||
obase == 16 )) || usage
;;
*) usage ;;
esac
done
shift $(( OPTIND - 1 ))
echo Converting $1 from base-$ibase to base-$obase\:
echo "obase=$obase; ibase=$ibase; $1" | bc
exit 0
几乎整个程序都涉及到解析和检查输入值,这在编写良好的 shell 脚本中并不少见。请注意我在脚本中包含的一些快捷方式,特别是测试结构
(( condition || condition )) || usage
这与说“if not condition1 and not condition 2 ; then ; usage” 是一样的,只是更简洁。此外,正如我在上一篇文章中讨论的那样,请注意使用 OPTARG
获取参数值,以及使用 OPTIND
和 shift
命令来移除所有参数,以便 $1
将是要转换的值。
对程序进行几次快速运行表明它工作正常
$ bconvert.sh -i 16 33
Converting 33 from base-16 to base-10:
51
$ bconvert.sh -i 16 -o 2 33
Converting 33 from base-16 to base-2:
110011
$ bconvert.sh -i 2 -o 16 110011
Converting 110011 from base-2 to base-16:
33
请注意,最后两个示例演示了在十六进制(base-16)的 33 和二进制(base-2)的 110011 之间进行转换的镜像功能。它有效!
在 Linux 世界中,常见的数字表示法是识别以零开头的数字是八进制,而以 “0x” 开头的数字是十六进制。(二进制不是特别有用,因此未包含在常用表示法中。)以下是一些示例:0700、0xFFc39。您可以修改脚本以接受这些作为输入并推断出适当的进制,但我将把它作为练习留给您,亲爱的读者。
还有另一种方法可以在不涉及 bc
的情况下转换值——通过利用 printf
命令行程序。如果您了解 C 编程,您已经熟悉 printf()
和 scanf()
,但不幸的是,只有输出函数在 shell 命令行中可用。但是,用法非常相似,正如您在这个快速示例中看到的那样
$ printf "> %d <\n" 42
> 42 <
在这种情况下,格式字符串(参数 #1)详细说明了所需的输出,其中 %d
表示将打印十进制值,然后参数 2 是该值,即 42。
有趣的是,您实际上可以在格式字符串中使用其他值来强制输出八进制或十六进制
$ printf "octal: %o\nhex: %x\n" 42 42
octal: 52
hex: 2a
由于之前提到的 shell 中非十进制数字的表示法约定,您也可以指定八进制或十六进制值
$ printf "%o\n" 0500
500
等等,最后一个例子中发生了什么?很简单:我指定我想要八进制(base-8)输出,但通过使用前导零,我也表明我指定的值也是八进制。因此,0500 = 500。
这很好,但没有二进制,这是一个明显的限制。
但是,我还没说完。还有一种方法可以转换值,而且实际上是直接在 shell 中进行。事实证明,使用 $(( ))
表示法,您实际上可以为数字指定数字进制!
这是我最近偶然发现的,我以前根本不知道 shell 还有这种能力,但看看这个,将十六进制(base-16)的 33 快速转换为十进制
$ echo $((16#33))
51
不仅如此,前导零和前导 “0x” 也都有效
$ echo $(( 0xFF ))
255
如果您不关心二进制值,您可以看到有三种完全不同的方法可以从 shell 脚本中转换数字进制。现在运用我在这里展示的内容,做一些真正巧妙的事情吧!
在未来的文章中,我将探讨条件语句的其他一些快捷方式,这些快捷方式可以让您跳过单调的 “if condition ; then XX else XX fi” 表示法序列。
注意:bconvert.sh 脚本可在以下链接下载:https://linuxjournal.cn/files/linuxjournal.com/code/bconvert