在一个非常愚蠢的 shell 中添加/减去变量

Posted

技术标签:

【中文标题】在一个非常愚蠢的 shell 中添加/减去变量【英文标题】:Add/subtract variables in a really dumb shell 【发布时间】:2013-04-26 05:11:39 【问题描述】:

我正在编写一个可以在我的本地 /bin/sh 上运行的 shell 脚本(Ubuntu 13.04 上的破折号),但我最终需要在一个由于对变量的操作而出现错误的哑盒上运行它:

$((n2 - n1 + 1))

不起作用,我收到如下错误:

syntax error: you disabled math support for $((arith)) syntax

我对那里的sh 不太了解,但我认为这个东西是busybox。我怎么能在这个笨壳上做数学?


编辑小程序列表

~ # busybox --list
[
arp
ash
cat
chgrp
chmod
chown
chroot
chvt
clear
cmp
cp
cut
date
dd
deallocvt
df
dmesg
du
echo
env
false
find
freeramdisk
ftpget
ftpput
grep
gunzip
gzip
hexdump
hwclock
ifconfig
ln
losetup
ls
md5sum
mkdir
mkfifo
mknod
mkswap
more
mount
mv
nslookup
ping
ping6
ps
pwd
renice
reset
rm
rmdir
route
seq
sh
sha1sum
sha256sum
sleep
sort
swapoff
swapon
switch_root
sync
tar
taskset
tee
telnet
test
tftp
time
top
touch
true
umount
uname
uniq
uptime
usleep
vconfig
vi
wget
whoami
yes

【问题讨论】:

是的,它是busybox。由于您的ash 如此精简,我猜您的busybox 也没有awk 小程序?还是外部bc 没有find / -name awkfind / -name bc 的结果 expr 也许?如果没有符号链接,您可能还需要检查 $ busybox awk$ busybox expr 是的,我也试过expr,不好... 你的busybox二进制文件来自哪里。一些技巧可能是可能的,但取决于一些busybox小程序或其他,因此了解二进制文件的确切配置将有很大帮助。它会给人们一些可以玩弄的东西。如果无法提供链接,busybox --list 至少会给出可用小程序的列表。 【参考方案1】:

通用加法/减法/乘法/除法seq+grep+sort

注意事项:

所有这些都符合 POSIX,但有一个稍快的非 POSIX subtract_nonposix,它依赖于支持 -w-Bgrep(非 POSIX,但甚至是 busybox'@ 987654329@支持他们) add/subtract 仅支持无符号整数作为输入 multiply/divide支持有符号整数作为输入 subtract/multiply/divide可以处理负面结果 取决于输入 multiply/divide 可能非常昂贵(参见 cmets) subtract/multiply 如果不在子shell 中使用,可能会污染您的命名空间(它们分别使用$__x$__y

arith.sh:

#!/bin/sh

is_uint()

    case "$1" in
        ''|*[!0-9]*) return 1
                     ;;
    esac
    [ "$1" -ge 0 ]


is_int()

    case "$1#-" in
        ''|*[!0-9]*) return 1
                     ;;
    esac


# requires seq, grep -n, sort -nr
# reasonably fast
add()

    if   ! is_uint "$1" \
      || ! is_uint "$2"; then
        echo "Usage: add <uint1> <uint2>"
        return 1
    fi
    [ "$1" -eq 0 ] &&  echo "$2"; return; 
    [ "$2" -eq 0 ] &&  echo "$1"; return; 

    
        seq 1 "$1"
        seq 1 "$2"
     \
        | grep -n "" \
        | sort -nr \
        |  read num; echo "$num%[-:]*"; 


# requires seq, grep -n, sort -nr, uniq -u
# reasonably fast
subtract()

    if   ! is_uint "$1" \
      || ! is_uint "$2"; then
        echo "Usage: subtract <uint1> <uint2>"
        return 1
    fi

    if [ "$1" -ge "$2" ]; then
        __x="$1"
        __y="$2"
    else
        __x="$2"
        __y="$1"
    fi

    
        seq 0 "$__x"
        seq 0 "$__y"
     \
        | sort -n \
        | uniq -u \
        | grep -n "" \
        | sort -nr \
        | \
        
            read num
            : $num:=0
            [ "$__x" = "$2" ] && [ "$1" -ne "$2" ] && minus='-'
            echo "$minus$num%:*"
        


# requires seq, grep -wB
# faster than subtract(), but requires non-standard grep -wB
subtract_nonposix()

    if   ! is_uint "$1" \
      || ! is_uint "$2"; then
        echo "Usage: subtract <uint1> <uint2>"
        return 1
    fi

    if [ "$1" -ge "$2" ]; then
        __x="$1"
        __y="$2"
    else
        __x="$2"
        __y="$1"
    fi
    seq 0 "$__x" \
        | grep -w -B "$__y" "$__x" \
        | \
        
            read num
            [ "$__x" = "$2" ] && [ "$1" -ne "$2" ] && minus='-'
            echo "$minus$num"
        


# requires seq, sort -nr, add()
# very slow if multiplicand or multiplier is large
multiply()

    if   ! is_int "$1" \
      || ! is_int "$2"; then
        echo "Usage: multiply <int1> <int2>"
        return 1
    fi
    [ "$2" -eq 0 ] &&  echo 0; return; 
    # make sure to use the smaller number for the outer loop
    # to speed up things a little if possible
    if [ $1 -ge $2 ]; then
        __x="$1"
        __y="$2"
    else
        __x="$2"
        __y="$1"
    fi
    __x="$__x#-"
    __y="$__y#-"

    seq 1 "$__y" \
        | while read num; do
            sum="$(add "$sum:-0" "$__x")"
            echo "$sum"
        done \
        | sort -nr \
        | \
        
            read num
            if   [ "$1" -lt 0 -a "$2" -gt 0 ] \
              || [ "$2" -lt 0 -a "$1" -gt 0 ]; then
                minus='-'
            fi
            echo "$minus$num"
        


# requires subtract()
# very costly if dividend is large and divisor is small
divide()

    if   ! is_int "$1" \
      || ! is_int "$2"; then
        echo "Usage: divide <int1> <int2>"
        return 1
    fi
    [ "$2" -eq 0 ] &&  echo "division by zero"; return 1; 

    (
        sum="$1#-"
        y="$2#-"
        count=
        while [ "$sum" -ge "$y" ]; do
            sum="$(subtract "$sum" "$y")"
            # no need to use add() for a simple +1 counter,
            # this is way faster
            count="$count."
        done

        if   [ "$1" -lt 0 -a "$2" -gt 0 ] \
          || [ "$2" -lt 0 -a "$1" -gt 0 ]; then
            minus='-'
        fi
        echo "$minus$#count"
    )


echo "10 4 14
4 10
10 10
2 -2
-2 -2
0 0
x y" | while read x y; do
    for op in add subtract subtract_nonposix multiply divide; do
        printf -- "$x $y %-17s = %s\n" "$op" "$("$op" "$x" "$y")"
    done
    echo
done

示例运行:

$ ./arith.sh
10 4 add               = 14
10 4 subtract          = 6
10 4 subtract_nonposix = 6
10 4 multiply          = 40
10 4 divide            = 2

4 10 add               = 14
4 10 subtract          = -6
4 10 subtract_nonposix = -6
4 10 multiply          = 40
4 10 divide            = 0

10 10 add               = 20
10 10 subtract          = 0
10 10 subtract_nonposix = 0
10 10 multiply          = 100
10 10 divide            = 1

2 -2 add               = Usage: add <uint1> <uint2>
2 -2 subtract          = Usage: subtract <uint1> <uint2>
2 -2 subtract_nonposix = Usage: subtract <uint1> <uint2>
2 -2 multiply          = -4
2 -2 divide            = -1

-2 -2 add               = Usage: add <uint1> <uint2>
-2 -2 subtract          = Usage: subtract <uint1> <uint2>
-2 -2 subtract_nonposix = Usage: subtract <uint1> <uint2>
-2 -2 multiply          = 4
-2 -2 divide            = 1

0 0 add               = 0
0 0 subtract          = 0
0 0 subtract_nonposix = 0
0 0 multiply          = 0
0 0 divide            = division by zero

x y add               = Usage: add <uint1> <uint2>
x y subtract          = Usage: subtract <uint1> <uint2>
x y subtract_nonposix = Usage: subtract <uint1> <uint2>
x y multiply          = Usage: multiply <int1> <int2>
x y divide            = Usage: divide <int1> <int2>

【讨论】:

收藏这个。也许有一天会明白它是如何工作的。 ;) @jm666 我希望我的解释尝试足够好 ;-) 你不明白哪一部分? @jm666 好吧,我的代码让理解中文看起来比基本算术更容易,但我保证它实际上超级简单;-) 我明天将剖析管道并添加中间输出作为解释的一部分。 我必须有一个残缺的 grep 版本,因为 -B 是无效选项 最后发布了一个不需要grep -B 的解决方案,改进了通用功能并给了他们自己的帖子,以及为了可读性而拆分了作为该答案一部分的原始特定解决方案。 【参考方案2】:

基于seqsort -nruniq -u(符合 POSIX)的另一个特定解决方案 (n2 - n1 + 1)。

foo()

    
        seq 1 "$2"
        seq 0 "$1"
     \
        | sort -n \
        | uniq -u \
        | grep -n "" \
        | sort -nr \
        |  read num; echo "$num%:*"; 


$ foo 100 2000
1901

【讨论】:

这个有效。您为这个愚蠢的小问题付出的时间和精力给我留下了深刻的印象,谢谢!【参考方案3】:

头、尾和厕所

如果您的busybox内置了headtailwc,您可以尝试以下方法:

head -c $n2 /dev/zero | tail -c +$n1 | wc -c

第一个将生成n2 零字节的序列。第二个将从位置n1 开始,从1 开始计数,因此它将跳过n1 - 1 字节。因此生成的序列有n2 - n1 + 1 字节。可以使用wc -c 计算此计数。

head、tail 和 ls 或 stat

用我的busybox试过了,虽然它的配置可能与你的不同。我不确定wc 是否会比expr 更有可能。如果您有headtail 但没有wc,那么您可能会将结果写入临时文件,然后使用statls 以字符串形式获取大小。下面是这方面的例子。

seq 和 wc

如果您有wc 但没有headtail,那么您可以替换为seq

seq $n1 $n2 | wc -l

seq、tr 和 stat

由于您的评论表明您没有wc 但确实有seq,如果您有足够完整的lstr,甚至可能是stat,这是一个替代方案。唉,我刚刚注意到tr 也不在您的小程序列表中。不过,为了将来参考,这里是:

seq $n1 $n2 | tr -d [0-9] > tempfilename
stat -c%s tempfilename

这会创建一系列n2 - n1 + 1 行,然后删除所有数字,只留下那么多换行符,并将其写入文件。然后我们打印它的大小。

dd 和 ls

但由于您没有tr,您需要一些不同的东西。 dd 可能适合您的需求,因为您可以使用它有点像 headtail

dd if=/dev/zero of=tmp1 bs=1 count=$n2 #   n2
dd if=tmp1 of=tmp2 bs=1 skip=$n1       # - n1
echo >> tmp2                           # +  1
set -- dummy `ls -l tmp2`
echo $6
rm tmp1 tmp2

这会创建一个n2 空字节序列,然后跳过它的第一个n1。它附加一个换行符以将 1 添加到其大小。然后它使用ls 打印该文件的大小,并根据其输出设置位置变量$1$2……。 $6 应该是包含大小的列。除非我错过了什么,否则这一切都应该可供您使用。

busybox 的替代品

如果其他一切都失败了,您可能仍会使用大量区分大小写来实现自己的逐位减法算法。但这需要大量工作,因此您最好发布静态链接的expr 二进制文件,或者专门为您的用例设计的东西,而不是脚本方法。

【讨论】:

这看起来很有趣.. 但不幸的是,我没有头尾 wc! :) seq 是我忘记的东西,但我实际上可以将它用于我需要的其他东西! @wim:我认为上述使用dd 的建议应该适合你。 我认为您在答案中部分混合了 n2n1n2 - n1n1 - n2)。除此之外,即使减去的数字大于另一个数字,它们也不会起作用,但它们非常整洁,我希望 wim 不需要 :-) 在您的 dd 方法中详细说明 dummy 吗? @AdrianFrühwirth:我注意到由 autoconf 创建的 configure 脚本会大量使用 dummyidiom,我不确定其意图是什么。可能仅在被调用程序根本不返回任何单词的情况下才需要,但直到我确定我决定简单地复制该习语。你是对的,我在几个地方混合了变量顺序,会尝试一致地修复它们。 @MvG 您可以使用dd if=/dev/zero bs=1 count=$n &gt;&gt;$memory_file,因此可以使用 dd 将其添加到现有的 memory_file 中(为了保持一致性,而不是 echo):)。因此,可以制作像 fc_add m1 100、fc_sub m1 10、fc_add m1 40、fc_show m1 之类的小型 shell 脚本 :) :) 恭喜:非常有创意。【参考方案4】:

非常奇怪的想法 - 仅在您有网络连接时才可用:

a=2,3
b=2.7
res=`wget -q -O - "http://someyourserver:6000/($a+$b)*5/2"`
echo $res

这样您就可以通过网络进行计算。您必须设置一个简单的 Web 服务器,它将从请求中获取 PATH_INFO 并仅返回结果。

服务器部分(非常简化 - 没有任何错误处理等)可以像下一个app.psgi

my $app = sub 
    my $env = shift;
    my $calc = $env->PATH_INFO;
    $calc =~ s:^/::; #remove 1.st slash
    $calc =~ s:[^\d\(\)\+\*/\-\.\,]::g; #cleanup, only digits and +-*/()., allowed
    $calc =~ s/,/\./g; #change , to .
    my $res = eval $calc;
        return [ 200, ['Content-Type' => 'text/plain'], [ "$res" ] ];
;

使用plackup -p 6000 app.psgi运行

或者可以使用任何其他简单的 CGI 或 php 脚本。

【讨论】:

我有同样的基本想法,但它看起来如此疯狂和笨拙,以至于我不敢实际发布它。我很确定这对他的场景中的 OP 没有帮助,但也许以后其他人可能会觉得这很有帮助,所以 +1。【参考方案5】:

或者,如果您可以重新配置和重建 BusyBox 并启用“与 bash 兼容的扩展”,那么您应该能够进行数学运算。您将不得不再次交叉编译您的 BusyBox,并在您的目标上用新的二进制文件替换旧的二进制文件(假设您有这样做的环境)。 BusyBox 可执行文件只有一个二进制文件,因此您只需处理一个文件的简单替换。

我有 BusyBox 1.19.4,数学评估工作得很好。

【讨论】:

根据他的评论重新编译busybox不是一个选项。【参考方案6】:

仅使用 printf 加/减数字

对我来说,之前的答案不起作用,因为我没有seq,也没有grep,也没有wcheadtail,甚至没有dd。 我的 bash 语法不支持数学语法 $((n1+n2)),甚至不支持范围语法 1..N。所以这绝对是一个艰难的环境。

我确实设法使用以下技术(计算 n1-n2)对少量(最多几千个)进行基本的加/减运算:

n1=100
n2=20
str_n1=`printf "%$n1s"` # "prints" 100 spaces, and store in str_n1
str_n2=`printf "%$n2s"` # "prints" 20 spaces, and store in str_n2

if [ n1 -gt n2 ]    # if the n1 > n2, then:
then
    str_sub=$str_n1%$str_n2   #delete str_n2 from str_n1
else
    str_sub=$str_n2%$str_n1   #delete str_n1 from str_n2
fi

# until now we created a string with 100 spaces, then a second string with 20 spaces, then we deleted the 20 of 2nd string from 1st string, so now all we left is to:

sub_result=$#str_sub   #check the length of str_sub

同样的技术也可以用于添加数字(继续上一个示例):

str_add=$str_n1$str_n2  # concat the two string to have 120 spaces together
add_result=$#str_add  # check the length of add_result

现在,就我而言,我必须处理更大的数字(最多一千万),而这种方法无法使用这种方法,因为它实际上需要打印数百万个空格,而且需要很长时间。 相反,由于我不需要整数,而只是其中的一部分,所以我使用子字符串语法取了数字的中间:

n1=10058000
n2=10010000

n1=$n1:3:3  # -> 580 (takes 3 chars from the 3rd char)
n2=$n2:3:3  # -> 100

然后用较小的数字计算我需要什么(当然需要考虑更多的参数,例如n1=10158000和n2=10092000)

【讨论】:

【参考方案7】:

这是我基于 seqgrep 发布的针对您的问题 (n2 - n1 + 1) 的原始解决方案。

foo()

  seq 0 "$2" \
    | grep -nw -B "$1" "$2" \
    |  read num; echo "$num%[-:]*"; 


$ foo 100 2000
1901

它是如何工作的:

首先我们生成一个从0n2的数字序列 然后我们将grep 用于n2 并在输出中包含前导n1 行。第一行现在保存了我们的结果。我们添加行号,因此从零开始的序列占+1(行号和实际数字将相差一) 然后我们用read 获取第一行(基本上模拟head -n 1)和 从输出中丢弃实际数字 - 行号是正确的结果

【讨论】:

以上是关于在一个非常愚蠢的 shell 中添加/减去变量的主要内容,如果未能解决你的问题,请参考以下文章

今天的日期,在 shell 脚本中减去 X 天

sh Bash:Square Brackets:添加和减去变量

在zsh shell (Z-shell) 中添加永久环境变量

在zsh shell (Z-shell) 中添加永久环境变量

在zsh shell (Z-shell) 中添加永久环境变量

shell环境变量与变量赋值