带前导零的整数(可移植)?

带前导零的整数(可移植)?

shell 的一个“特性”是将带有前导零的数字解释为八进制数:

$ echo "$((00100))"
64

但在许多 shell 中无法禁止此“功能”,因此,强制将数字序列解释为十进制(或其他基数)数字变得很困难。

当只有一个数字需要转换时,有几个外部程序可以进行修剪:

expr "00100" + 0 
echo "00100" | sed 's/^0*//'
echo "00100" | grep -o '[^0].*$'
echo "00100" | awk '{print int($0)}'
echo "00100" | perl -pe '$_=int."\n";'

但每次需要它们时都需要一些时间来执行它们。在多次调用中累积使用此类外部工具,延迟会变得相当大。只是为了测量造成的延迟,重复调用 1000 次以上,您将得到(以秒为单位):

expr      1.934
sed       3.450
grep      3.775
awk       5.291
perl      5.064

当然(expr 除外)大多数工具都可以处理 1000 行的文件:

sed  file 0.004
grep file 0.003
awk  file 0.007
perl file 0.006

如果所有单独的 1000 个值在同一时间点都可用。
事实并非如此。那么,还有待回答的是:

是否有一种本机(对于 shell)提取整数的方法比为每个单独的整数(而不是文件中的列表)调用外部工具更快?

每次调用都会累积,延迟将变得很重要。

如果号码也可能有前导符号并且您想要拒绝无效号码,则处理会变得更加复杂。

答案1

请注意,尽管$((010))POSIX 要求扩展为 8,但某些 shell 默认情况下不会执行此操作(或仅在某些上下文中),除非处于一致性模式,因为这是一个特征你通常不想要。

使用 时zsh,它由选项控制octalzeroes(默认情况下关闭,除了 sh/ksh 模拟)。

$ zsh -c 'echo $((010))'
10
$ zsh -o octalzeroes -c 'echo $((010))'
8
$ (exec -a sh zsh -c 'echo "$((010))"')
8

在 中mksh,这由posix选项控制(默认情况下关闭):

$ mksh -c 'echo "$((010))"'
10
$ mksh -o posix -c 'echo "$((010))"'
8

在 bash 中,没有选项可以将其关闭,但您可以使用$((10#010))ksh 语法强制以十进制解释(在 ksh 和 zsh 中也适用),但在bashmksh -o posix$((10#-010))不起作用(正如10#0 - 010您从扩展中看到的那样)的$((-10#-010))屈服-8),你需要$((-10#010))(或者为了与抱怨是无效基础$((- 10#010))的兼容性)。zsh-10

$ bash -c 'echo "$((10#010))"'
10

ksh93,比较:

$ ksh93 -c 'echo "$((010))"'
8
$ ksh93 -c '((a = 010)); echo "$a"'
8

和:

$ ksh93 -c 'a=010; echo "$((a))"'
10
$ ksh93 -c 'printf "%d\n" 010'
10
$ ksh93 -c 'let a=010; echo "$a"'
10
$ ksh93 -c 'echo "$((010e0))"'
10
$ ksh93 -o letoctal -c 'let a=010; echo "$a"'
8

因此,至少如果您专门为这些 shell 中的任何一个进行编码,那么有一些方法可以解决该“错误功能”。

但是,在编写 POSIX 可移植脚本时,这些都没有帮助,在这种情况下,您需要删除前导零,如您所示。

答案2

类似的事情可以在一行中完成:

$ a=-00100; a=${a%"${a#[+-]}"}${a#"${a%%[!0+-]*}"}; a=${a:-0}
$ echo "$a"
-100

1000 次重复仅需 0.0482,比使用外部程序少 100 倍。

这是基于两个双参数扩展:

  1. 提取符号:
    • ${a#[+-]}删除第一个字符(如果它是一个符号)。
    • ${a%"${a#[+-]}"}保留第一个标志,前提是它是一个标志。
  2. 删除所有前导符号和/或零:
    • ${a%%[!0+-]*}删除从任意位置(不是 0 或 + 或 - )开始到结束的位置。
    • ${a#"${a%%[!0+-]*}"}删除上述内容,即所有前导零和符号。

这会选择一个符号并删除所有前导零。但是它允许(没有错误):

  1. 几个领先的迹象。
  2. 前导符号和零之后的任何字符。
  3. “超出范围”(太大)的数字。

如果需要这些测试,请继续阅读。


可以用以下方法测试标志的数量:

signs=${a%%[!+-]*} 
[ ${#signs} -gt 1 ] && echo "$0: Invalid number $a: Too many signs"

可以使用以下命令检查允许的字符类型:

num=${a#"${a%%[!0+-]*}"}

any=${num%%[!0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ@_]*}
[ "$any" != "$num" ] && echo "$0: Invalid number $a"

hex=${num%%[!0123456789abcdefABCDEF]*}
[ "$hex" != "$num" ] && echo "$0: Invalid hexadecimal number $a"

dec=${num%%[!0123456789]*}
[ "$dec" != "$num" ] && echo "$0: Invalid decimal number $a"

最后,我们可以利用printf打印“超出范围”数字警告的能力(仅适用于 printf 理解的基数):

printf '%d' $sign$dec >/dev/null                            # for a decimal number
printf '%d' "${sign}0x$hex" >/dev/null                      # for hex numbers

是的,所有 printf 都使用%d,这不是拼写错误。

是的,以上所有内容都适用于大多数具有printf.

答案3

这是我的系统上的 x1000 示例:

$ cat shell.sh
#!/bin/dash
q=1
while [ "$q" -le 1000 ]
do
  z=-00100
  z=${z%"${z#[+-]}"}${z#"${z%%[!0+-]*}"}
  z=${z:-0}
  echo "$z"
  q=$((q + 1))
done

结果:

$ time ./shell.sh >/dev/null
real    0m0.047s

现在我对 sed 示例有疑问。我确实看到了一个带有文件的示例,但我没有看到为什么使用文件不可接受的明确原因。另外,使用管道的示例也是有问题的,因为不需要管道 - 也不需要调用 sed 1000 次。如果您出于某种原因无法使用文件 - 此处文档就可以了:

cat > sed.sh <<alfa
sed 's/^0*//' <<bravo
$(yes 00100 | head -1000)
bravo
alfa

结果:

$ time ./sed.sh >/dev/null
real    0m0.047s

所以在我的系统上,速度完全相同,没有什么大惊小怪的。

相关内容