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 three-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, and finally investigate functions.
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
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/shellscript.sh
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:
the command itself, enclosed between
$
signsthe
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 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
will result in that echo
command erroring with:
ERROR
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 echo
ed.
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