amber: writing bash scripts in amber instead. pt. 4: functions

a while ago, i blogged about uploading files to s3 using curl and provided the solution as two functions written in bash, and basically the only feedback was “wait. you can write functions in bash?”

you can. but in reality, you probably don’t want to. the syntax for defining (and calling) bash functions is horrible. there’s a reason people genrally don’t use them. writing and using functions in amber, by comparison, is a borderline delight of sanity. if you’ve ever written a function in php or python or javascript, amber’s function syntax will feel familiar and, well, ‘normal’.

a look at the syntax of functions in bash

posts in this series

a simple function to start

let’s start with a basic hello-world calibre function:

fun hello() {
    echo "hello"
}

hello()

functions are defined with the keyword fun to keep things nice and terse, and have a body inside of braces. very comortable, standard stuff. when we call this function, the string ‘hello’ gets printed to STDOUT.

accepting arguments

arguments can be passed to a function as a comma-separated list. again, no surprises here.

fun personalized_hello(name) {
    echo "hello {name}"
}

personalized_hello("gbhorwood")

note here that we’re using string interpolation in our echo statement to output our variable.

return statements

we can return a value from a function using the return statement, just as we would expect.

fun get_personalized_hello(name) {
    return "hello {name}"
}

echo personalized_hello("gbhorwood")

a little bit of type safety

everybody loves some type safety in their programming language, and amber obliges us by accepting optional types for both arguments and return values.

fun sum(a: Num, b: Num): Num {
    return a + b
}

types are defined using the colon syntax.

amber has five types:

  • Text: strings, basically.
  • Num: either integers or floats.
  • Bool: the standard true or false
  • Null: the nothing type. amber uses Null as a return types for functions that do not return values.
  • [<some type>]: the array type. in amber, arrays cannot contain mixed types, so the type definition for an array includes the type of the array’s elements. if we want to define an array of numbers, for instance, we would type it as [Num].

important note: if we define one type in a function, we have to define all the types. for instance, we cannot define just the argument type without also defining the return type. not setting a return type of Null here throws an error.

// this errors because there is no return type
fun say_my_name(name: Text) {
    echo name
}

say_my_name("gbhorwood") // WARN Function has typed arguments but a generic return type

we would fix this error by writing our function as:

// this works
fun say_my_name(name: Text): Null {
    echo name
}

likewise, if we define the type of one argument, we have to define them all:

// this error because last_name has no type
fun say_my_name(first_name: [Text], last_name): Null {
    echo "hello"
}

say_my_name("grant", "horwood") // ERROR Function 'say_my_name' has a mix of generic and typed arguments

and, of course, if we define a type, we have to obey it.

fun say_my_name(name: Text) {
    echo name
}

say_my_name(9) // 1st argument 'name' of function 'say_my_name' expects type 'Text', but 'Num' was given

throwing errors with fail

functions in amber can ‘throw’ an error by using the fail statement with an exit code. in this example, we want our function to fail if the user is not root.

fun root_only_function() {
    unsafe if($whoami$ != "root") {
        fail 1
    }

    echo "only root can do this"
}

in the first installment, we covered handling bash errors using the failed block. we can ‘catch’ the errors ‘thrown’ by fail the same way.

root_only_function() failed {
    echo "failed condition"
}

likewise, we can also ignore the errors we fail from our functions by using unsafe.

unsafe root_only_function()

trapping failed cases in our functions

we can also, of course, handle errors from commands by using the failed block inside our functions. this function, for example, attempts a shell command and, on failure, throws it’s own fail.

fun failing_touch() {
    silent $touch /etc/passwd$ failed {
        fail 1
    }
}

failing_touch() failed {
    echo "function failing_touch failed"
}

note that we applied silent to our shell command to suppress bash’s output. we only want users to see our error messages, not the shell’s.

pushing failed cases up to our function call

trapping an error and throwing an explicit fail is a bit clumsy. amber also allows us to automatically fail up to our to where our function is called by replacing the failed block in our function with ?.

fun failing_touch() {
    silent $touch /etc/passwd$?
}

failing_touch() failed {
    echo "function failing_touch failed"
}

in this example, our function, when called, fails exactly the same way as it would if we’d called $touch /etc/passwd$ directly. very handy.

conclusion

this series has covered calling shell commands; handling errors; composing if statements and writing loops; using the convenience commands in the standard library; and writing functions. is that all amber can do? no. but it is certainly enough for us to start using this language to do useful, meaningful things.

a note about vim

writing code in vim is a joyful thing (or, at least, that’s my opinion), but not having syntax highlighting in this modern day and age is intolerable, so i composed an amber syntax file for vim. i’ve never written a syntax file before and the effort there is clearly sophomoric, but it does work.

Posted by: grant horwood

co-founder of fruitbat studios. cli-first linux snob, metric evangelist, unrepentant longhair. all the music i like is objectively horrible. he/him.

Leave a Reply