Use the Plan 9 shell

I’m a big fan of the Plan 9 shell, rc (originally built for 10th Edition Research Unix, but mostly known now from the Plan 9 port).

It takes the good parts of the Bourne shell, but cleaned up:

  • C-like syntax, rather than Algol-like. if (cond) cmd rather than if cond; then cmd; fi, and more {} blocks.
  • Sane backtick syntax: `{prog args} rather than `prog args`, which is hard to nest, or $(prog args), which works, but looks like a Makefile variable!/li>
  • Better quoting syntax. Single quotes only; no interpolations. The only character that needs to be escaped inside quotes is ', and it can be escaped just by doubling it (Basic-style): ''. The best alternative I’ve found in Bourne shell is to use double quotes, which potentially require a lot more escapes, or the very ugly '\'' to end the quote, insert an escaped quote, and resume it.

    Instead of interpolation, there’s an explicit concatenation operator ^ available, as in 'some string with '^$foo^' included'. I normally really like string interpolation in programming languages, but there’s something nice about concatenation in a radically simple syntax.

  • Variables store lists internally, rather than strings. That means variables don’t get re-split when you use them, which eliminates a bunch of your quoting of variables, and of course, the quoting bugs when you get it wrong.

Here’s my current T script, mostly for testing Perl code:

if (~ $#* 0) {
    echo put | 9p write acme/$winid/ctl
    *=$%
}

for (file in $*) {
    switch ($file) {
        case *.tex
            pdflatex $file
        case *greeter/*.t
            test_file=`{echo $file | sed 's,.*/(t/.*),\1,'}
            cd `{echo $file | sed 's,/t/.*,,'} &&
            vagrant up &&
            echo 'cd /vagrant && prove -l '^$"test_file^'; exit' | vagrant ssh web
        case *greeter/*.pm
            pkg=`{pkg $file}
            cd `{echo $file | sed 's,/lib/.*,,'} &&
            test_file=`{find t -name '*.t' | xargs rg -l '(^|[^:\w])'^$pkg^'([^:\w]|$)'}
            vagrant up &&
            echo 'cd /vagrant && prove -l '^$"test_file^'; exit' | vagrant ssh web
        case *DBIx-Class-Relationship-Abbreviate/*.t
            test_file=`{echo $file | sed 's,.*/(t/.*),\1,'}
            cd `{echo $file | sed 's,/t/.*,,'} &&
            sh -c 'perlbrew use perl-5.26.1@abbreviate && prove -l "$test_file"'
        case *DBIx-Class-Relationship-Abbreviate/*.pm
            pkg=`{pkg $file}
            cd `{echo $file | sed 's,/lib/.*,,'} &&
            test_file=`{find t -name '*.t' | xargs rg -l '(^|[^:\w])'^$pkg^'([^:\w]|$)'}
            sh -c 'perlbrew use perl-5.26.1@abbreviate && prove -l "$test_file"'
    }
}

and a Bash translation:

if [ "$#" -eq 0 ]; then
    echo put | 9p write acme/$winid/ctl
    set $samfile
fi

for file in "$@"; do
    case $file in
        *.tex)
            pdflatex "$file"
            ;;
        *greeter/*.t)
            test_file=`echo "$file" | sed 's,.*/(t/.*),\1,'`
            cd `echo "$file" | sed 's,/t/.*,,'` &&
            vagrant up &&
            echo "cd /vagrant && prove -l $test_file; exit" | vagrant ssh web
            ;;
        *greeter/*.pm)
            pkg=`pkg "$file"`
            cd `echo "$file" | sed 's,/lib/.*,,'` &&
            test_file=`find t -name '*.t' | xargs rg -l '(^|[^:\w])'"$pkg"'([^:\w]|$)'`
            vagrant up &&
            echo "cd /vagrant && prove -l $test_file; exit" | vagrant ssh web
            ;;
        *DBIx-Class-Relationship-Abbreviate/*.t)
            test_file=`echo "$file" | sed 's,.*/(t/.*),\1,'`
            cd `echo "$file" | sed 's,/t/.*,,'` &&
            (perlbrew use perl-5.26.1@abbreviate && prove -l "$test_file")
            ;;
        *DBIx-Class-Relationship-Abbreviate/*.pm)
            pkg=`pkg "$file"`
            cd `echo "$file" | sed 's,/lib/.*,,'` &&
            test_file=`find t -name '*.t' | xargs rg -l '(^|[^:\w])'"$pkg"'([^:\w]|$)'`
            (perlbrew use perl-5.26.1@abbreviate && prove -l "$test_file"')
            ;;
    esac
done

~ is an rc built-in for doing glob matching. $*, like in sh, is the current program’s arguments. (Not a lot of people know that, since sh re-splits it whenever you use it, so it isn’t terribly useful.) Putting a # in front of a variable name gives you the length of the list, so $#* is the number of arguments to the current script.

This script can be run from the command-line, giving it the name of a file to test, but I normally run it without arguments from the Acme tag. The if statement detects that case, saves the current file for me, and then sets the script’s arguments to the name of the current file, which Acme has exported as $%:

*=$%

The ability to assign to $* and the availability of $% are both bits of syntactic orthogonality that rc adds over sh.

The for syntax is more C/Perl-like, which I think is nice. The switch statement also looks better to my eye; for some reason, sh decides this is the point where it needs to start looking like line noise. No accounting for taste, I guess.

The rest of the should be pretty self-explanatory, or at least off-topic for this blog post; I’ll just point out two things:

  • rc doesn’t need the ugly "$file" defensive quoting everywhere; and
  • For whatever reason, perlbrew doesn’t support rc, so we do have to delegate back to sh to use it. It’s still worth it to simplify the rest of the script.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s