amber: writing bash scripts in amber instead. pt. 1: commands and error handling

writing shell scripts is zero fun. the bash syntax is a mess, error handling is difficult, and any script longer than a hundred lines is basically unreadable. but we keep writing bash scripts because they’re the right tool for the job and the job must be done.

amber aims to fix this pain by being a language that gives us a sane, readable syntax that transpiles into messy bash so we don’t have to write messy bash ourselves.

this post is a four-parter that will go over the basic features of amber from the perspective of those of us who actually want to use it. we’ll start with calling shell commands and handling errors, then look at loops and if statements, the standard library of commands, and finally investigate functions.

the elegant syntax of amber is pulled away to reveal the messy bash underneath

posts in this series

is amber a mistake?

languages that transpile into other languages don’t have a great track record of success. coffeescript, elm, even flutter, were all supposed to make struggling with javascript a thing of the past. none of them got any appreciable traction. facebook released hiphop, their php-to-c++ transpiler, with a tremendous amount of hype. nobody used it. not even facebook.

so, is there any reason to expect amber to succeed?

maybe. first off, all those javascript transpilers were hindered by the fact that javascript isn’t actually that bad. bash is a nightmare by comparison. secondly, a lot of those languages have strong paradigm preferences that, themselves, don’t have a lot of popularity. the number of people who actively want to, say, write a monad instead of some vanilla js is not large. by comparison, amber sits firmly in the c-like idiom; a comfortable place for people who know php or python or javascript. finally, a lot of those other transpiler languages didn’t address many of the realities of the developer experience. developers use frameworks and rely on the wealth of documentation and examples for those frameworks. getting off the ground with a fresh vue or laravel project is far easier than riding the elm learning curve or forcing your entire framework through hiphop.

given this, it’s certainly possible that amber will gain at least some user base. its two biggest barriers currently are a) that you have to actually install it (via a curl-to-bash pipeline. apt and yum packages aren’t available) and b) that the community documentation for it is, generously speaking, pretty thin.

with that in mind, let’s walk through getting amber installed and look at doing shell commands and error handling, and maybe learn to love this language.


the recommended installation process for amber is one of those “copy and paste this shell script uncritically” things. the script will ask us to escalate to root, so we’ll need sudo access for this.

curl -s "" | bash

the result is a success message with two emojis thrown in for effect.

Installing Amber... 🚀
[sudo] password for ghorwood: 
Amber has been installed successfully. 🎉
> Now you can use amber by typing `amber` in your terminal.

amber is now installed and we have learned that the transpiler command for amber is amber. a good start.

transpile and run

before we write the first line of code, we’re going to look at how to transpile and run our amber scripts. there are two basic options.

first, to take our amber script and transpile it into a bash file that we can run later, we provide amber with two arguments: our input amber file and the path to our output bash file:

amber /path/to/input/amberscript.ab /path/to/output/

if we would like to transpile and run our script all in one command, we can pass amber just our amber script file as an argument.

amber /path/to/script.ab

the file extension for amber scripts is ab, as it should be.

running shell commands and handling errors

the first, and probably most important, thing people want to do with a shell scripting language is… script their shell. so we’ll start with that.

there are two components to calling a shell command in amber:

  1. the command itself, enclosed between $ signs
  2. the failed block that executes if the command fails for any reason

the template for this is:

$<some shell command>$ failed {
    <some code to run if the command fails>

if we want to call whoami from our script, it would look something like:

$whoami$ failed {
    echo "could not run whoami"

the compelling feature here is the failed block. checking for and handling errors in bash is a hassle. no one wants to do it and, quite frankly, a lot of developers just don’t. with amber, tough, we can just put any error handling we want to inside the braces after failed.

// fails for non-root
$touch /etc/passwd$ failed {
    echo "could not touch /etc/passwd" // any error-handling code here

suppressing bash’s output

by default, amber dumps both STDOUT and STDERR of command calls to the screen. this makes sense, but it can be annoying. for instance, this code

$touch /etc/passwd$ failed {
    echo "my custom error message"

outputs both our custom error message and the error message sent by touch, ie.

touch: cannot touch '/etc/passwd': Permission denied
my custom error message

not what we want.

we can suppress the output of touch (or any shell command) by prepending it with the keyword silent.

silent $touch /etc/passwd$ failed {
    echo "my customer error message"

using silent turns off all output. for instance, the command whoami will not show any output here:

silent $whoami$ failed {
    echo "whoami failed..."

we can also assign silent to a block of code containing multiple commands. in this example, neither whoami nor touch will print their output to screen

silent {
    $whoami$ failed {
        echo "could not whoami"
    $touch /etc/passwd$ failed {
        echo "could not touch /etc/passwd"

ignoring errors

catching errors in the failed block is the default behaviour, but we can turn that off with the keyword unsafe.

unsafe $touch /etc/passwd$

if an unsafe command fails, it displays its error message and our script continues.

if we’re really brave, we can combine unsafe and silent. this results in errors being completely ignored: no output, no handling.

silent unsafe $touch /etc/passwd$

like silent, unsafe can also be applied to a block of commands.

unsafe {
    let me = $whoami$
    let here = $pwd$

getting the exit status of a command

when a shell command completes, it returns an integer as a status code. if everything works, that integer is zero. if there’s an error, it’s some other number.

in amber, the most recent status code is stored in the global variable status

silent $touch /etc/passwd$ failed {
    echo status
echo status

in the above example, touch fails and sets status to 1. we can access that variable both from inside the failed block and anywhere after.

most people who write bash scripts don’t track these codes (probably because most people who write bash scripts don’t do any in-script error handling), so this may seem like a technical detail, but in future posts, we will go over how to leverage status to build more robust amber scripts.

reading input from bash

frequently, we want to take the output from a bash command and put it into a variable. we can do this with a straight assignment using the familiar let command.

let me = $whoami$ failed {
    echo "cannot get your name"

echo me

if our command fails for some reason, our variable will be null.

note that this assignment only works for STDOUT output. STDERR gets dumped to the screen, but is not assigned to the variable. for example, nginx -V outputs version data to STDERR for some terrible reason, so if we do:

let nginx_version = $nginx -V$ failed {
    echo "cannot get nginx version"

echo nginx_version

our nginx_version variable will be null.

since variable assignment is done by reading STDOUT, if we want to accept user input from bash’s read command, we have to echo the input.

let user_input = $read input && echo \$input$ failed {
    echo "error reading"

echo user_input

note here that we need to escape the $ in $input.

a note on variable scope

like just about every other programming language, variables in amber live in a scope. local scopes are enclosed in braces, including those that define the blocks for things like failed and unsafe.

for instance, if we assign two variables inside an unsafe block, those variables only exist inside that local scope. doing:

unsafe {
    let me = $whoami$
    let here = $pwd$

echo me

will result in that echo command erroring with:

Variable 'me' does not exist

we can circumvent this by declaring variables in the global scope and then assigning them in the local scope.

// declare in global scope
let me = ""
let here = ""

unsafe {
    // re-assign global variables in local scope
    me = $whoami$
    here = $pwd$

echo "me is {me}"
echo "here is {here}"

this example works because the variables are assigned globally using the let keyword and then given values in unsafe‘s scope.

of course we all know that global variables are bad and should be avoided, and we will look at how to better handle situations like this when we get to functions.

using variables in commands

we can easily use amber variables in shell commands through the miracle of string interpolation.

let filename = "somefile.txt"

echo "creating /tmp/{filename}"

$touch /tmp/{filename}$ failed {
    echo "could not create {filename}"

in this example, we took our variable filename and put it into the string that is being run as the command by wrapping it in braces. we also used the same technique to include the variable in the string we echoed.

next up

the elegant handling of shell commands and the built-in error handling alone make amber a compelling choice. but, of course, amber does a lot more. the next posts will focus on loops and if statements and then functions.

Posted by grant horwood

co-founder of fruitbat studios. cli-first linux snob, metric evangelist, unrepentant longhair. all the music i like is objectively horrible. he/him.


Leave a Reply