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

gbhorwood

grant horwood

Posted on June 18, 2024

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.

fred reveals

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

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.

installation

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 "https://raw.githubusercontent.com/Ph0enixKM/AmberNative/master/setup/install.sh" | bash
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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/shellscript.sh
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
}
Enter fullscreen mode Exit fullscreen mode

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

$whoami$ failed {
    echo "could not run whoami"
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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

silent $whoami$ failed {
    echo "whoami failed..."
}
Enter fullscreen mode Exit fullscreen mode

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"
    }
}
Enter fullscreen mode Exit fullscreen mode

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$
Enter fullscreen mode Exit fullscreen mode

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$
Enter fullscreen mode Exit fullscreen mode

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

unsafe {
    let me = $whoami$
    let here = $pwd$
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 &amp;&amp; echo \$input$ failed {
    echo "error reading"
}

echo user_input
Enter fullscreen mode Exit fullscreen mode

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

a note on variable scop

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
Enter fullscreen mode Exit fullscreen mode

will result in that echo command erroring with:

ERROR 
Variable 'me' does not exist
Enter fullscreen mode Exit fullscreen mode

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}"
Enter fullscreen mode Exit fullscreen mode

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}"
}
Enter fullscreen mode Exit fullscreen mode

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.

πŸ”Ž this post was originally written in the grant horwood technical blog

πŸ’– πŸ’ͺ πŸ™… 🚩
gbhorwood
grant horwood

Posted on June 18, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related