amber: writing bash scripts in amber instead. pt. 1: commands and error handling
Tags: bash, linux
Published: 2024-06-18 14:38:26

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 <a href="https://devopedia.org/transpiler">transpiles</a> 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](https://gbh.fruitbat.io/wp-content/uploads/2024/06/meme_amber1.jpg "the fred reveals meme")<figcaption>
the elegant syntax of amber is pulled away to reveal the messy bash underneath
</figcaption>

### 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 <a href="https://en.wikipedia.org/wiki/HipHop_for_PHP">hiphop</a>, 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.

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

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

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

1. the command itself, enclosed between `$` signs </li>
2. the `failed` block that executes if the command fails for any reason</li>

the template for this is:

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

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

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

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

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

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

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

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

```dart
silent unsafe $touch /etc/passwd$
```

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

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

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

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

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

```dart
let user_input = $read input &amp;&amp; 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:

```dart
unsafe {
    let me = $whoami$
    let here = $pwd$
}

echo me
```

will result in that `echo` command erroring with:

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

```dart
// 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`&#8216;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 <a href="https://www.codingdrills.com/tutorial/strings-data-structure/string-interpolation">string interpolation</a>. 

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