uudecode implemented entirely in bash

Posted on July 31st, 2009 by whinger. Filed under Tech.


With the internet being full of silly but interesting things I would have thought this would be an easy find but I couldn’t find it anywhere – a couple of people asking about it with some vague bits of code here and there but most of the time people would say something dumb like “use (awk|perl|uudecode)”

Not because I think it’s particularly useful or sensible (it certainly isn’t that, since it takes around 200 times as long as the C version!) but just to see if it were possible, here’s my bash uudecode implementation.

#!/bin/bash
bs=0
while read -rs t ; do
  if [ $bs -eq 1 ] ; then
    if [ "a$t" = "aend" ] ; then
      bs=2
    else
      x=1
      i=($(printf "%d " "'${t:0:1}" "'${t:1:1}" "'${t:2:1}" "'${t:3:1}" "'${t:4:1}" "'${t:5:1}" "'${t:6:1}" "'${t:7:1}" "'${t:8:1}" "'${t:9:1}" "'${t:10:1}" "'${t:11:1}" "'${t:12:1}" "'${t:13:1}" "'${t:14:1}" "'${t:15:1}" "'${t:16:1}" "'${t:17:1}" "'${t:18:1}" "'${t:19:1}" "'${t:20:1}" "'${t:21:1}" "'${t:22:1}" "'${t:23:1}" "'${t:24:1}" "'${t:25:1}" "'${t:26:1}" "'${t:27:1}" "'${t:28:1}" "'${t:29:1}" "'${t:30:1}" "'${t:31:1}" "'${t:32:1}" "'${t:33:1}" "'${t:34:1}" "'${t:35:1}" "'${t:36:1}" "'${t:37:1}" "'${t:38:1}" "'${t:39:1}" "'${t:40:1}" "'${t:41:1}" "'${t:42:1}" "'${t:43:1}" "'${t:44:1}" "'${t:45:1}" "'${t:46:1}" "'${t:47:1}" "'${t:48:1}" "'${t:49:1}" "'${t:50:1}" "'${t:51:1}" "'${t:52:1}" "'${t:53:1}" "'${t:54:1}" "'${t:55:1}" "'${t:56:1}" "'${t:57:1}" "'${t:58:1}" "'${t:59:1}" "'${t:60:1}"))
      l=$[${i[0]} -32 & 63 ]
      while [ $l -gt 0 ] ; do
        i0=$[${i[$[x++]]} -32 & 63]
        i1=$[${i[$[x++]]} -32 & 63]
        i2=$[${i[$[x++]]} -32 & 63]
        i3=$[${i[$[x++]]} -32 & 63]
        if [ $l -gt 2 ] ; then
          echo -ne "\0$[$i0 >> 4]$[$i0 >> 1 & 7]$[$i0 << 2 & 4 | $i1 >> 4]\0$[$i1 >> 2 & 3]$[$i1 << 1 & 6 | $i2 >> 5]$[$i2 >> 2 & 7]\0$[$i2 & 3]$[$i3 >> 3 & 7]$[$i3 & 7]"
        elif [ $l -eq 2 ] ; then
          echo -ne "\0$[$i0 >> 4]$[$i0 >> 1 & 7]$[$i0 << 2 & 4 | $i1 >> 4]\0$[$i1 >> 2 & 3]$[$i1 << 1 & 6 | $i2 >> 5]$[$i2 >> 2 & 7]"
        else
          echo -ne "\0$[$i0 >> 4]$[$i0 >> 1 & 7]$[$i0 << 2 & 4 | $i1 >> 4]"
        fi
        l=$[l-3]
      done
    fi
  elif [ "${t:0:5}" = "begin" ]; then
    bs=1
  fi
done

Note that I’ve used as few subprocesses as possible – I’ve got it down to one per line of input (the stupidly long “printf” line – more about that later) because the thing that bash does really badly is spawn off a subprocess: you can do a huge amount of fairly complex string and number manipulation in bash in place of a single subprocess spawn and it will still cut the time used massively. So for example I was using (as recommended across the web)


h=$(printf "%X" $d)

to convert decimal-to-hex: it’s about 10 times quicker (and actually not much less obvious) to create an array (0 1 2 3 4 5 6 7 8 9 a b c d e f) and build the hex string yourself using ${arr[d>>4]}${arr[d&15]} (I suppose using a 256 entry array would actually be quicker still but I gave up on the hex thing anyway to use octal)

The most interesting thing (fairly obvious, when you think about it) is the speed increase when you change from dripping through converting character by character


c = `printf "%d" "'$c"`

to the massive and horrible one-line-at-a-time printf above. You’re talking about a 15x speedup for the entire operation just by doing that.

Anyway, I found it all very challenging; I hope you find it useful/interesting :-).

Feel free to tell me what an idiot I am or (if you’re feeling more constructive) suggesting optimisations. If you can figure out a way of getting a character into a charcode integer without using a subprocess then obviously that would be really useful…

Edit: this awk-based implementation is probably more useful (it’s smaller and lots faster!) if you want to include a backup in your script for when uudecode isn’t installed

Tags: , ,



4 Responses to “uudecode implemented entirely in bash”

  1. Sam Kimbrel Says:

    Bash 4.0 offers associative arrays. Assuming these are implemented in a reasonable fashion, you could use one to map chars to int values and get rid of the printfs.

  2. whinger Says:

    Thanks for the comment and nice idea!

    So here’s some timings…

    array version:
    real 4m14.280s
    user 3m46.110s
    sys 0m27.410s

    original (printf) version:
    real 7m31.346s
    user 4m30.580s
    sys 2m41.460s

    C uudecode:
    real 0m0.146s
    user 0m0.130s
    sys 0m0.010s
    (lol)

    So yes, a massive speedup. I suppose you could test $BASH_VERSINFO[0] and do two different versions if it’s -ge 4…

  3. Steve McIntyre Says:

    Awesome, very useful code!

    Just talked a friend through recovering a remote system using this. He managed to break his libc, leaving (thankfully) two root shells open.

    Uuencode a copy of busybox-static then paste it into a shell function using your code, output over the top of an existing binary (to keep the execute bits). Painfully slow, but much better than the alternatives!

  4. G.Baroncelli Says:

    Hi All,

    I managed your script because it didn’t work with the busybox shell (ash). Basycally:
    – replaced the array with the “set” command and the $1, $2, $3 variables
    – replaced the math evaluation $[..] with $((..))

    I know that busybox has the uudecode command, but unfortunately in my case (DDWRT for DIR615) isn’t compiled.

    Below the source
    ———————-

    #!/bin/ash
    bs=0
    while read -rs t ; do
    if [ $bs -eq 1 ] ; then
    if [ “a$t” = “aend” ] ; then
    bs=2
    else
    set $(printf “%d ” “‘${t:0:1}” “‘${t:1:1}” “‘${t:2:1}” “‘${t:3:1}” “‘${t:4:1}” “‘${t:5:1}” “‘${t:6:1}” “‘${t:7:1}” “‘${t:8:1}” “‘${t:9:1}” “‘${t:10:1}” “‘${t:11:1}” “‘${t:12:1}” “‘${t:13:1}” “‘${t:14:1}” “‘${t:15:1}” “‘${t:16:1}” “‘${t:17:1}” “‘${t:18:1}” “‘${t:19:1}” “‘${t:20:1}” “‘${t:21:1}” “‘${t:22:1}” “‘${t:23:1}” “‘${t:24:1}” “‘${t:25:1}” “‘${t:26:1}” “‘${t:27:1}” “‘${t:28:1}” “‘${t:29:1}” “‘${t:30:1}” “‘${t:31:1}” “‘${t:32:1}” “‘${t:33:1}” “‘${t:34:1}” “‘${t:35:1}” “‘${t:36:1}” “‘${t:37:1}” “‘${t:38:1}” “‘${t:39:1}” “‘${t:40:1}” “‘${t:41:1}” “‘${t:42:1}” “‘${t:43:1}” “‘${t:44:1}” “‘${t:45:1}” “‘${t:46:1}” “‘${t:47:1}” “‘${t:48:1}” “‘${t:49:1}” “‘${t:50:1}” “‘${t:51:1}” “‘${t:52:1}” “‘${t:53:1}” “‘${t:54:1}” “‘${t:55:1}” “‘${t:56:1}” “‘${t:57:1}” “‘${t:58:1}” “‘${t:59:1}” “‘${t:60:1}”)

    l=$(($1 -32 & 63 ))
    shift
    while [ $l -gt 0 ] ; do
    i0=$(($1 -32 & 63))
    shift
    i1=$(($1 -32 & 63))
    shift
    i2=$(($1 -32 & 63))
    shift
    i3=$(($1 -32 & 63))
    shift
    if [ $l -gt 2 ] ; then
    echo -ne “$(($i0 >> 4))$(($i0 >> 1 & 7))$(($i0 <> 4))$(($i1 >> 2 & 3))$(($i1 <> 5))$(($i2 >> 2 & 7))$(($i2 & 3))$(($i3 >> 3 & 7))$(($i3 & 7))”
    true
    elif [ $l -eq 2 ] ; then
    echo -ne “$(($i0 >> 4))$(($i0 >> 1 & 7))$(($i0 <> 4))$(($i1 >> 2 & 3))$(($i1 <> 5))$(($i2 >> 2 & 7))”
    true
    else
    echo -ne “$(($i0 >> 4))$(($i0 >> 1 & 7))$(($i0 <> 4))”
    true
    fi
    l=$(($l-3))
    done
    fi
    elif [ “${t:0:5}” = “begin” ]; then
    bs=1
    fi
    done

Trackback URI | Comments RSS

Leave a Reply


Blogroll

Categories