amber: writing bash scripts in amber instead. pt. 2: loops and ifs
Tags: linux, bash
Published: 2024-06-20 14:43:22

in this series we're looking at using [the amber language](https://amber-lang.com/) to write scripts that transpile into bash. so far we've covered [using shell commands and handling error cases](https://gbh.fruitbat.io/2024/06/18/amber-writing-bash-scripts-in-amber-instead-pt-1-commands-and-error-handling/). in this installment, we'll be looking at control structures: loops and `if`s, basically.

![bicycle fail](https://gbh.fruitbat.io/wp-content/uploads/2024/06/meme_amber_2.png "the bicycle fail meme")<figcaption>a developer writing an ‘if’ statement in bash
</figcaption>

## 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.

```dart
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.

```dart
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](https://en.wikipedia.org/wiki/Ternary_conditional_operator).  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:

```dart
// 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: 

```dart
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.

```dart
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.

```dart
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'.

```dart
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](https://gbh.fruitbat.io/2024/06/18/amber-writing-bash-scripts-in-amber-instead-pt-1-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:

```dart
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.

```dart
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:

```dart
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](https://gbh.fruitbat.io/2024/06/20/amber-writing-bash-scripts-in-amber-instead-pt-2-loops-and-ifs/)