dd-ex: arithmetic functions

The editor needs to be able to increment and decrement the line number, and also sometimes to add and subtract the lenght of the current line from an offset in the current file. Most implementations of the Bourne shell don't have built-in arithmetic, but it is fairly easy to implement the four operations (although I don't use multiply and divide, there was no reason to leave them off!)

These function are called as

add variable numbers
subtract variable numbers
multiply variable numbers
divide variable numbers
where variable is the name of a shell variable where the result will be stored.

Addition

To add two numbers, say a and b, we generate two files with length, respectively, a and b. The lenght of the concatenated file is the result. This can be easily extended to the sum of a list of numbers, by adding them one at a time to the previous result.

To generate the "operand" files, we use the function zero.

We need to make a special case for 0, because dd won't allow us to copy "the first 0 bytes" of a file. But, of course, it is easy to add 0 to a number!

At the end of the processing, we use dd again to copy the result file to /dev/null - as a side effect, we get on the standard error file the number of bytes copied, which is the required result. Well, we actually get the number of records copied, but we set the record size to 1. The result looks like:

   125+0 records in
   125+0 records out
so the text up to the first "+" is the required length. We split the result assigning "+" to the shell's fields separator variable.

Finally, the code:

  add () {
    result="$1"
    shift

    # start with an empty file (copy /dev/null)
    $dd if=/dev/null of=$tmp bs=1 2>/dev/null

    # for each number given, add the required size to the current file
    for n in "$@"
    do
      case "$n" in
	0) ;;
	*) # generate a file of length $n
	   zero | $dd of=$tmp.1 bs=1 "count=$n" 2>/dev/null

	   # append it to the current file
	   ( $dd if=$tmp; $dd if=$tmp.1 ) 2>/dev/null | $dd of=$tmp.2 2>/dev/null
	   $dd if=$tmp.2 of=$tmp 2>/dev/null
	   ;;
      esac
    done

    # figure out the size of the result
    IFS="+"
    set `$dd if=$tmp bs=1 of=/dev/null 2>&1`
    IFS="$saveIFS"

    # assign to the desired variable
    eval $result='$1'
  }

Subtraction

This uses the same techniques as the addition. If we want to subtract b from a, we first need a file of size a, then we can copy it skipping the first b bytes and the result is a file of size a-b. Some versions of "dd" return a zero length file when b is greater than a, other return an error. Since this is a portability problem, we check for the error and return 0 if the result would be negative.
  subtract () {
    result="$1"

    # generate a file of size n1
    zero | $dd of=$tmp bs=1 "count=$2" 2>/dev/null

    # compute the length of the result when we skip n2 bytes
    IFS="+"
    set `$dd if=$tmp bs=1 of=/dev/null "skip=$3" 2>&1`
    IFS="$saveIFS"

    # check if "dd" produced an error
    case "$1" in
        dd*) set 0 ;;
    esac

    # and assign to the required variable
    eval $result='$1'
  }

Multiplication

This is even simpler! When generating a file of fixed length, we required dd to count "n" records of length 1. If we set the length to something else, the resulting file's length will be the product of the two.
  multiply () {
    result="$1"

    # generate a file with n1 records of size n2
    zero | $dd "bs=$2" of=$tmp "count=$3" 2>/dev/null

    # the resulting file has size n1 * n2 bytes - get its size
    IFS="+"
    set `$dd if=$tmp bs=1 of=/dev/null 2>&1`
    IFS="$saveIFS"

    # and assign the result to the required variable
    eval $result='$1'
  }

Division

This time we change the record size the other way around. Given a file of size n1, we try to read it with record size n2, and the number of records is the integer part of n1/n2. If there is a reminder, dd will report the partial record as "33+1 records in", but unfortunately it always says "+1" (it is the number of partial records, not the number of bytes in the record), so we need to use divide, multiply, and subtract to get the reminder.
  divide () {
    result="$1"

    # generate a file of size n1
    zero | $dd bs=1 of=$tmp "count=$2" 2>/dev/null

    # and read it with record size n2 - the number of complete
    # records is int(n1/n2)
    IFS="+"
    set `$dd if=$tmp "bs=$3" of=/dev/null 2>&1`
    IFS="$saveIFS"

    # assign the result
    eval $result='$1'
  }