amber: writing bash scripts in amber instead. pt. 2: loops and ifs

amber: writing bash scripts in amber instead. pt. 2: loops and ifs

ยท

7 min read

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 ifs, 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 fors and foreaches, various while implementations, and all manner of maps. 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

ย