Add/subtract variables in a really dumb shell
Asked Answered
I

7

7

I am writing a shell script which works on my local /bin/sh fine (dash on Ubuntu 13.04), but I unltimately need to run it on a dumb box where I'm getting an error because of an operation on variables:

$((n2 - n1 + 1))

doesn't work, I get an error like:

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

I don't know a lot about the sh on there but I think this thing is busybox. How can I do maths on this dumb shell?


edit with list of applets

~ # 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
Ideography answered 2/5, 2013 at 5:43 Comment(11)
Yes, it's busybox. Since your ash is so stripped down, I guess your busybox doesn't have the awk applet either? Or an external bc?Sigurd
No results with find / -name awk or find / -name bcIdeography
expr maybe? You might also want to check $ busybox awk and $ busybox expr in case there are just no symlinks.Sigurd
Yeah I already tried expr too, no good ...Ideography
Then you're pretty much stuck. Is it an option to replace the busybox with one that has maths support compiled in (guess not, otherwise you wouldn't ask)?Sigurd
Maybe on one dev box ... but probably not on 500,000+ boxes out in the wild ;)Ideography
Where does your busybox binary come from. Several tricks might be possible but depend on some busybox applet or other, so knowing the exact configuration of your binary will help a lot. It will give people something to toy around with. If no link can be provided, busybox --list will at least give the list of available applets.Adust
yes - finally someone asked for a list of applets...Meenen
Does your uniq have -u? Then I have a new solution for you in the waiting.Sigurd
Yes, -u option works!Ideography
@Ideography Didn't get a notification for your comment, so sorry for the late reply. I posted the solution which should finally work for you based on the applets you have available :-)Sigurd
L
3

Another specific solution to your problem (n2 - n1 + 1) based on seq, sort -nr and uniq -u (POSIX-compliant).

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

$ foo 100 2000
1901
Lilianaliliane answered 17/5, 2013 at 11:38 Comment(1)
This one works. I'm very impressed with the amount of time and effort you've put into this silly little problem, thanks!Ideography
L
4

Generic addition/subtraction/multiplication/division with seq+grep+sort

Notes:

  • All of these are POSIX-compliant, but there is a slightly faster non-POSIX subtract_nonposix which relies on a grep supporting -w and -B (non-POSIX, but even busybox' grep supports them)
  • add/subtract support only unsigned integers as input
  • multiply/divide support signed integers as input
  • subtract/multiply/divide can deal with negative results
  • depending on the input multiply/divide might be very costly (see comments)
  • subtract/multiply may pollute your namespace (they use $__x and $__y respectively) if not used in a subshell

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

Example run:

$ ./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>
Lilianaliliane answered 3/5, 2013 at 14:5 Comment(5)
Favoriting this one. Maybe onetime will understand how it works. ;)Raffarty
@jm666 I was hoping my attempt at an explanation was good enough ;-) which part don't you understand?Sigurd
@jm666 Well, my code makes understanding Chinese seem easier than basic arithmetics, but I promise it's actually supereasy ;-) I will dissect the pipe and add the intermediate output tomorrow as part of the explanation.Sigurd
I must have a crippled version of grep, because -B is invalid optionIdeography
Finally posted a solution which does not need grep -B, improved the generic functions and gave them their own post as well as split the original specific solution which was part of this answer for the sake of readbility.Sigurd
A
3

head, tail and wc

If your busybox has head, tail and wc built in, you might try the following:

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

The first will generate a sequence of n2 zero bytes. The second will start at position n1, counting from 1, so it will skip n1 - 1 bytes. Therefore the resulting sequence has n2 - n1 + 1 bytes. This count can be computed using wc -c.

head, tail and ls or stat

Tried this with my busybox, although its configuration might differ from yours. I'm not sure whether wc will be that more likely than expr. If you have head and tail but no wc, then you could probably write the result to a temporary file, and then use stat or ls to obtain the size as a string. Examples for this are included below.

seq and wc

If you have wc but not head and tail, then you could substitute seq instead:

seq $n1 $n2 | wc -l

seq, tr and stat

As your comment indicates you have no wc but do have seq, here is an alternative provided you have sufficuently complete ls and also tr, perhaps even stat. Alas, I just noticed that tr isn't in your list of applets either. Nevertheless, for future reference, here it is:

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

This creates a sequence of n2 - n1 + 1 lines, then removes all digits, leaving only that many newlines, which it writes to a file. We then print its size.

dd and ls

But as you don't have tr, you'll need something different. dd might suite your needs, since you can use it a bit like head or tail.

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

This creates a sequence of n2 null bytes, then skips the first n1 of it. It appends a single newline to add 1 to its size. Then it uses ls to print the size of that file, and sets the positional variables $1, $2, … based on its output. $6 should be the column containing the size. Unless I missed something, this should all be available to you.

Alternative to busybox

If everything else fails, you might still implement your own digit-wise subtraction algorithm, using a lot of case distinctions. But that would require a lot of work, so you might be better of shipping a statically linked expr binary, or something specifically designed for your use case, instead of a scripted approach.

Adust answered 2/5, 2013 at 12:41 Comment(6)
This looks fun.. but unfortunately, I don't have head tail or wc! :) seq was something I forgot about and I was actually able to use that for something else I needed though !Ideography
@wim: The above suggestion using dd should work for you, I think.Adust
I think you partly mixed n2 and n1 in your answers (n2 - n1 vs n1 - n2). Other than that, those are very neat even though they won't work if the subtracting number is greater than the other, which I hope wim does not require :-) Care to elaborate on the dummy in your dd approach?Sigurd
@AdrianFrühwirth: I noticed that configure scripts created by autoconf will use that dummyidiom a lot, and I'm not sure what the intention is. Might be only neccessary for cases where the called program returns no words at all, but until I'm sure I decided to simply copy that idiom as is. And you are right, I mixed variable order in several places, will try to fix them consistently.Adust
@Adust you can use dd if=/dev/zero bs=1 count=$n >>$memory_file so is possible adding into existing memory_file with dd (for the consistency, instead of the echo) :). So, is possible make small shell scripts like fc_add m1 100, fc_sub m1 10, fc_add m1 40, fc_show m1 :) :) Congratz: REALLY CREATIVE IDEA.Raffarty
@Adust Thanks for the clarification! This would seem logical and is probably the reason, although it is unnecessary since POSIX states that "The command 'set --' without argument shall unset all positional parameters and set the special parameter '#' to zero." (as opposed to printing the list of vars like without any arguments). Maybe this used to be unspecified earlier or some crappy shells ignore(d) the standard and autoconf is just being nice to them.Sigurd
L
3

Another specific solution to your problem (n2 - n1 + 1) based on seq, sort -nr and uniq -u (POSIX-compliant).

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

$ foo 100 2000
1901
Lilianaliliane answered 17/5, 2013 at 11:38 Comment(1)
This one works. I'm very impressed with the amount of time and effort you've put into this silly little problem, thanks!Ideography
R
2

Really weird idea - usable only if you have network connection:

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

so you can do calculations over the network. You must setup one simple web server will get the PATH_INFO from the request and return only the result.

the server part (very simplified - without any error handling etc.) can be like next 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" ] ];
};

run with plackup -p 6000 app.psgi

or can use any other simple CGI or php script.

Raffarty answered 3/5, 2013 at 6:52 Comment(1)
I had the same basic idea but it seemed so crazy and kludgy that I did not dare to actually post it. I am pretty certain it won't help the OP in his scenario but maybe someone else later might find this helpful, so +1.Sigurd
E
1

Alternatively, if you can reconfigure and rebuild BusyBox and enable "bash-compatible extensions", which should give you the ability to do mathematics. You will have to cross-compile your BusyBox again and replace the old binaries with your new one on your target (assuming you have the environment to do so). The BusyBox executable is only one binary, so you will only need to deal with simple replacement of one file.

I have BusyBox 1.19.4 and the mathematics evaluation works just fine.

Emersonemery answered 2/5, 2013 at 15:49 Comment(1)
According to his comment recompiling busybox is not an option though.Sigurd
B
1

Add/Subtract numbers using only printf

For me, the previous answers didn't work since I don't have seq, nor grep, nor wc, head or tail, not even dd.
My bash syntax doesn't support the math syntax $((n1+n2)), and not even the range syntax {1..N}. so it definitely was a tough environment.

I did manage to have basic add/subtract operations with small numbers (up to few thousands) using the following technique (calculate n1-n2):

n1=100
n2=20
str_n1=`printf "%${n1}s"` # "prints" 100 spaces, and store in str_n1
str_n2=`printf "%${n2}s"` # "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

Same technique can be used also for adding numbers (continue from last example):

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

Now, in my case, I had to work with bigger numbers (up to ten of millions), and it cannot work with this method like that since it actually needs to print millions of spaces, and it takes forever.
Instead, since I don't need to whole number, but just a part of it, i took the middle of the number using the substring syntax:

n1=10058000
n2=10010000

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

Then calculate what I need with smaller numbers (of course needs to take more parameters in consideration for cases like n1=10158000, and n2=10092000)

Barathea answered 12/1, 2016 at 12:47 Comment(0)
L
0

Here is the original solution I posted to your problem (n2 - n1 + 1) based on seq and grep.

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

$ foo 100 2000
1901

How it works:

  • First we generate a sequence of numbers from 0 to n2
  • Then we grep for n2 and include the leading n1 lines in the output. The first line holds our result now. We add the line number so the zero-based sequence accounts for the +1 (the line numbers and actual numbers will be off-by-one)
  • Then we fetch the first line with read (basically emulating head -n 1) and
  • discard the actual number from the output - the line number is the proper result
Lilianaliliane answered 17/5, 2013 at 11:42 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.