in this series we're looking at using the amber language to write scripts that transpile into bash. so far we've covered using shell commands and handling error cases. in this installment, we'll be looking at control structures: loops and if
s, basically.
the basic if
statement (with else
)
writing an if
statement in bash is an aggressively user-hostile experience. there are developers out there who have been writing bash for thirty years who still have to refer to a cheat sheet of all the various condition statements (ahem).
by comparison, amber's syntax is gloriously mundane. if you have any experience in php or python or javascript, if
statements look exactly like you expect them to.
let dir = unsafe $pwd$
if(dir == "/home/ghorwood") {
echo "you're home"
}
else {
echo "you're lost"
}
and all of the comparison operators are the standard ones: !=
, <
, and so on.
a shorter, terser, if
syntax
amber also offers a terser syntax, allowing us to get that three-line if
statement down to one by forgoing the braces in favour of a colon.
if(dir == "/home/ghorwood") : echo "you're home"
else : echo "your'e lost"
ternery expressions: terser still.
if we're really committed to lowering the line count, there's also ternery expressions. i'm a huge fan of terneries and it's my preference to construct conditionals this way, however public opinion on this does vary. a lot.
the basic template for an amber ternery is:
<some condition> then <some value if true> else <some value if false>
note here that the code in the then
and else
blocks are values. we can't put execution statements in here. for instance, this will not run:
// this will not work!
dir == "/home/ghorwood" then echo "you're home" else echo "you're lost"
instead, we can use the ternery to return a value that we pass to echo
, like so:
echo dir == "/home/ghorwood" then "you're home" else "you're lost"
terneries can also be used to assign default values to variables. for instance, if we want to create a variable called is_root
that is set to true
if the user is root and false
if the user is anyone else, we can write it in a ternery.
let me = unsafe $whoami$
let is_root = me == "root" then true else false
one thing to note when doing this is that the values in the then
and else
blocks must both be of the same type. we can't return a string in then
and a number in else
; amber will complain. and rightfully so.
the if
chain. sort of like switch
.
amber doesn't do switch
statements, but it does allow us to chain a stack of conditionals in one if
block.
if {
birth_year >= 1981 and birth_year <= 1996 {
echo "millennial"
}
birth_year >= 1946 and birth_year <= 1964 {
echo "boomer"
}
else {
echo "whatever"
}
}
only the first condition that evaluates to true in an if
chain will execute. the following example will output 'greater than 3', but not 'greater than 4'.
let somenumber = 7
if {
somenumber > 3 {
echo "greater than 3"
}
somenumber > 4 {
echo "greater than 4"
}
else {
echo "the else block"
}
}
using if
and status
in the installment that covered commands and error handling, we went over how amber puts the exit code of a command in a special, global variable called status
. most people who write bash scripts never check exit codes; they just let the shell handle any errors. but with amber, we can leverage status
to improve our error handling.
for instance, we want a script that runs three shell commands. if any command fails, we want to terminate the script (we'll cover terminating the script in the next installment). however, we want to show all the error messages. if two commands fail, we want to show both fails to the user and then quit.
we can do this by tracking the status
of each command after it executes, like so:
let stop_execution = 0
// whoami
silent $whoami$ failed {
echo "error: cannot whoami"
}
stop_execution += status
// touch /etc/passwd (this fails)
silent $touch /etc/passwd$ failed {
echo "error: cannot touch"
}
stop_execution += status
// pwd
silent $pwd$ failed {
echo "error: cannot pwd"
}
stop_execution += status
// test if any command failed
if(stop_execution) {
echo "fail case"
}
here, we declared a variable called stop_execution
that holds the sum of all the status
variable after each command is run. if all commands pass they all have an exit code of 0, and their sum in stop_execution
is 0. we proceed.
however, if one command (or more) has a non-zero exit code in status
, the sum in stop_execution
is greater than zero and we execute our error-handling if
statement.
loops
loops in bash aren't actually that bad. there's a standard for
and a standard while
and they work mostly like we expect.
amber takes a slightly different approach to looping, dividing them into two broad categories:
infinite loops: that run forever until
break
is called.iterator loops: that iterate over an iterable data structure. you know, an array.
let's look at both of them.
infinite loops
infinite loops run until something stops them. this makes them dangerous, but it also makes them useful.
in this example, we poll the user for some text input until we get the single character 'q'; then we quit.
let user_input = ""
loop {
user_input = unsafe $read input && echo \$input$
if(user_input == "q") {
break
}
}
the key here is the break
statement that terminates our loop block when the condition is met.
loops that iterate
other programming languages are overflowing with ways to loop over things like arrays: there are for
s and foreach
es, various while
implementations, and all manner of map
s. in amber, we use the same loop
command as we do with infinite loops, but with modifications to make it work a bit like a mixture of a traditional for
that tracks the array's index and a foreach
that assigns the current element to a variable. here's an example:
let users = ["ghorwood", "mnle", "mflewitt"]
loop index, user in users {
echo index
echo user
}
if we've used languages like php or python or javascript, this construction should look familiar. it's basically php's foreach($collection as $item)
or python's for item in collection:
, but with the addition of index
. if we run the above code, we get what we expect:
0
ghorwood
1
mnle
2
mflewitt
a short note on arrays
we saw an array in the example above, and we should probably address that.
amber does arrays. and while the syntax and their behaviour is pretty much what we would expect coming from other modern languages, there are some notable restrictions:
type consistency is required: every element of an array has to be of the same type. you can have an array of strings or an array of numbers, but you cannot mix the two.
there are no associative arrays: associative arrays or dictionaries or whatever we want to call them do not exist in amber. keys are numbers starting at zero.
you cannot nest arrays: an array cannot have another array as an element.
there is no subscripting: when we get to functions later on, we will have functions that return arrays and we may be tempted to do something like
get_all_users()[0]
. this doesn't work. at least not yet.
lastly, using defined arrays, like we did above, is probably what we're least interested in. what we want is to be able to get a directory listing or lines from a file as an array. we'll cover that in the next installment that goes over the standard library.
what's next
amber has a bunch of commands in it's 'standard library' for doing things like string manipulation and file access. useful things we will probably want to do. none of this is covered in the official documentation (as of yet!). in the next installment, we will go over all of these commands and how to use them.
๐ this post was originally written in the grant horwood technical blog