Skip to content
Rob Nagler edited this page Aug 1, 2024 · 10 revisions

Bash

Bash is available in all modern systems. Bash has also evolved with new syntax that fixes many of the word splitting and substitution problems.

Declaring variables

Use declare instead of local so can be relocated in any context.

Declare variables simply and then assign to the output of a subshell:

declare foo
foo=$(bar)
declare -a ray
ray=( $(ls foo*) )

This allows the error to cascade so that Bash exits (assumes set -e).

Dockerization

Docker has turned Bash into a mainstream programming language. Bash is the only programming language availabple on all distros, even busybox. That means that Dockerfiles are essentially bash scripts. Entry points are bash scripts. And so on.

However people have not embraced bash. The Docker hub doesn't allow you to build from a bash script. It relies on a Dockerfile existing. This yields very awkward Dockerfile/bash code.

Also, people don't understand how to use Bash well so they are using a mix of modern and old versions. Anything using Docker is building with a modern distro, which has Bash 4.2 or 4.3.

Docker allows us to rely on modern bash.

Modularization

C is still a popular programming language. There are no name spaces, and neither are there in Bash. That's ok. Just be disciplined as in C.

Imports can then just use source (.).

Variables

declare -g is the way you should declare global variables. If you don't, it's like local, and you'll get unbound variable errors if you source the file in a function.

Problems

Bug in unbound variables?

$ set -e
$ set -u
$ x=([y]=1)
bash: y: unbound variable

You can't even do this:

$ x[y]=1
bash: y: unbound variable

Useful Links

http://mywiki.wooledge.org/BashPitfalls

Comparison

Here is an example of a modern Bash script:

set -euo pipefail
vdi=$(realpath -e "${1:?usage: $0 virtual-device-id}")
uuid_re='^UUID: *(.+)'
file_re='^Location: *(.+)'
VBoxManage list hdds | expand | while IFS= read -r l; do
    if [[ $l =~ $uuid_re ]]; then
        u=${BASH_REMATCH[1]}
    elif [[ $l =~ $file_re && ${BASH_REMATCH[1]} == $vdi ]]; then
        VBoxManage closemedium disk "$u" --delete
        break
    fi
done

And, in Python:

import os.path
import re
import subprocess
import sys
assert len(sys.argv) > 1, \
    'usage: {} virtual-device-id'.format(sys.argv[0])
vdi = os.path.abspath(sys.argv[1])
assert os.path.exists(vdi), \
    '{}: file does not exist'.format(vdi)
uuid_re = re.compile(r'^UUID:\s*(.+)')
file_re = re.compile(r'^Location:\s*' + re.escape(vdi) + '$')
o = subprocess.check_output(['VBoxManage', 'list', 'hdds'])
for l in o.splitlines():
    m = uuid_re.search(l)
    if m:
        u = m.group(1)
    elif file_re.search(l):
        subprocess.check_call(['VBoxManage', 'closemedium', 'disk', u, '--delete'])
        break

And, in Perl:

use strict;
use warnings;
use File::Spec ();
die("usage: $0 virtual-device-id\n")
    unless $ARGV[0];
my($vdi) = File::Spec->rel2abs($ARGV[0]);
my($uuid_re) = qr{^UUID:\s*(.+)};
my($file_re) = qr{^Location:\s*\Q$vdi\E$}i;
my($u);
foreach my $l (`VBoxManage list hdds`) {
    chomp($l);
    if ($l =~ $uuid_re) {
        $u = $1;
    }
    elsif ($l =~ $file_re) {
        my($c) = [qw(VBoxManage closemedium disk), $u, '--delete'];
        my($e) = system($c);
        die("@$c: exit=$e\n")
            unless $e == 0;
        last
    }
}

The are some subtle differences between the various implementations, but they all do the same thing. The differences can be subtle, e.g. the Bash version doesn't compare the file insensitively and the Perl version doesn't check the result of the VBoxManage calls.

Mac OS X differences

Mac OS X is stuck on version 3.2.57. Here are some differences:

  • ${foo,,} (make lower case) not supported
  • [[ $a && $b ]] requires backslashes: [[ $a
    && $b ]]
Clone this wiki locally