amber: writing bash scripts in amber instead. pt. 3: the standard library

amber: writing bash scripts in amber instead. pt. 3: the standard library

a programming language is only ever going to be as good as its standard library. try writing a c program without, for instance, <string.h>. it's not great.

amber's standard library is not large -- it's only 24 commands, and some of those aren't yet available in version 1.0 -- but it provides a lot of the convenience functionality that we want when scripting. there's string manipulation, array handling, some file access stuff. all of these functions are, under the hood, just bash commands, but they provide proven and convenient ways to do the things we want to do.

none of the standard library commands are covered in the official documentation. to find out what they do (or even that they exist), you need to read the source code. so, lets' go over them here so we don't have to do that.

the flextape meme

a developer uses a standard library function to preform a basic but important task

importing commands from the standard library

amber's standard commands are not loaded by default. if we want to use one, we first must import it. we do this with (no surprises), the import command. for instance, to get access to exit, we would write:

import { exit } from "std"

the import command must happen before the command itself is called.

once we have imported exit, we can use it directly:

exit(0)

string commands

the standard library contains most of the functionality we want when dealing with strings. the commands are:

len()

get the length of a string.

import { len } from "std"

let somestring = "this is 26 characters long"

len(somestring) // 26

note that this is the number of characters in the string (usually), not the number of bytes or the display width of the characters.

import { len } from "std"

let japanesestring = "キャラクター";
len(japanesestring) // 6

let chinesestring = "長度"
len(chinesestring) // 2

let emojistring = "❤️ 👍🎉"
len(emojistring) // 5 because emojis are sometimes two characters

the exception here is with emojis. some emojis count as two characters. that's just the way it is.

lower()

set all characters in a string to lowercase where possible.

import { lower } from "std"

lower("I AM NOT SHOUTING") // i am not shouting
lower("hAcKeR cAsE") // hacker case
lower("❤️ 👍🎉") // ❤️ 👍🎉

under the hood, this is a call to tr '[:upper:]' '[:lower:].

replace()

replace all the characters in a string that meet a pattern with a replacement string.

replace(<original string>, <pattern to match>, <replacement text>)

the pattern can be a regular expression. it is case-sensitive.

import { replace } from "std"

replace("to who it may concern", "who", "whom") // to whom it may concern
replace("to who it may concern", "[aeiou]", "v") // tv whv vt mvy cvncvrn
replace("Foo foo", "foo", "bar") // Foo bar
replace("キャラクター", "ラ", "foo") // キャfooクター
replace("❤️ 👍🎉", "👍", "thumbsup"); // ❤️ thumbsup🎉

replace_once()

identical to replace() except that only the first occurrence in a string is replaced.

import { replace_once } from "std"

replace_once("bar bar", "bar", "foo") // foo bar

replace_regex()

like replace, but using sed for patter matching and replacement

import { replace_regex } from "std"

replace_regex("line   with  many    spaces", " \{1,\}", " ") // line with many space

trim()

remove all whitespace from the beginning and end of a string. handles both spaces and tabs.

import { trim } from "std"

let a = trim("  padded line   ") // padded line
let b = trim("\ttab padded line\t") // padded line
let c = trim("\t tab padded line\t  ") // padded line

echo "{a}END"
echo "{b}END"
echo "{c}END"

results in

padded lineEND
tab padded lineEND
tab padded lineEND

trim_left()

identical to trim() except it only removes whitespace from the left side of the string.

import { trim_left } from "std"

let a = trim_left("  padded line   ") // padded line
let b = trim_left("\ttab padded line\t") // padded line
let c = trim_left("\t tab padded line\t  ") // padded line

echo "{a}END"
echo "{b}END"
echo "{c}END"

results in

padded line   END
tab padded line    END
tab padded line      END

trim_right()

identical to trim() except it only removes whitespace from the right side of the string.

import { trim_right } from "std"

let a = trim_right("  padded line   ") // padded line
let b = trim_right("\ttab padded line\t") // padded line
let c = trim_right("\t tab padded line\t  ") // padded line

echo "{a}END"
echo "{b}END"
echo "{c}END"

results in:

  padded lineEND
    tab padded lineEND
     tab padded lineEND

upper()

set all characters in a string to uppercase where possible.

import { upper } from "std"

upper("i am shouting") // I AM SHOUTING
upper("hAcKeR cAsE") // HACKER CASE
upper("❤️ 👍🎉") // ❤️ 👍🎉

under the hood, this is a call to tr '[:lower:]' '[:upper:].

user input commands

amber has one function to poll for and return user keyboard input.

input()

takes user input from keyboard.

import { input } from "std"

let name = input("enter your name: ")

echo name

file commands

amber has a limited number of functions for handling safe file read and write. some functions are defined but do not exist in version 1.0.

dir_exist()

this function is listed in the latest master branch as of 2024-06-26 but is not implemented in amber 1.0.

file_exist()

this function is listed in the latest master branch as of 2024-06-26 but is not implemented in amber 1.0.

file_read()

reads the content of a file and returns it as a string.

note that file_read() requires either a failed block or to be declared as unsafe. in the event of failure to read the file, null is returned.

import { file_read } from "std"

// successful file read
silent let somecontents = file_read("/tmp/somefile.txt") failed {
    echo "fail case"
}
echo somecontents // contents of the file

// unsuccessful file read with error handling. returned value is null
silent let nocontents = file_read("/no/tmp/somefile.txt") failed {
    echo "fail case"
}
echo nocontents // null
echo status // 1

// unsuccessful file read with errors suppressed. returned value is null
silent unsafe let unsafenocontents = file_read("/no/tmp/somefile.txt")
echo unsafenocontents // null
echo status // 1

file_write()

writes a string to a file. this function overwrites the contents of the target file. to append a string to a file, use file_append().

note that file_write() requires either a failed block to be declared as unsafe.

import { file_write } from "std"

let contents = "some contents"

file_write("/tmp/foo", contents) failed {
    echo "could not write to file"
}

unsafe file_write("/tmp/foo", contents)

file_append()

appends a string to the contents of a file.

import { file_append } from "std"

let contents = "more contents"

file_append("/tmp/foo", contents) failed {
    echo "could not write to file"
}

unsafe file_append("/tmp/foo", contents)

note that file_append() requires either a failed block to be declared as unsafe.

array commands

the standard library has several functions for splitting strings into arrays and combining arrays into strings.

split()

splits a string on a delimiter and returns an array of the parts. delimiters must be one character. strings that do not contain the delimiter return an array of one element containing the original string. works with multibyte characters.

import { split } from "std"

// split string by delimiter
split("comma,separated,values", ",") // ["comma", "separated", "values"]

// split string that does not contain the delimiter
split("no commas in string", ",") // ["no commas in string"]

// split multi-byte string by delimiter
split("❤️ ,👍,🎉", ",") // ["❤️ ","👍","🎉"]

// cannot succesfully split string on multi-character delimiter
split("andANDseparatedANDstring", "AND") // ["and", null, null, "separated", null, null, "string"]

lines()

splits a string into an array of lines. the delimiter for the split is \n, however in practice \r\n will work. blank lines in the input string, ie \n\n are ignored in the returned array.

import { lines } from "std"

// multiple lines using \n
let multi_line_string = "line one\nline two\nline three"
lines(multi_line_string);

// multiple lines using \r\n
let multi_line_string = "line one\r\nline two\r\nline three"
lines(multi_line_string);

// single line string
let single_line_string = "one line"
lines(single_line_string);


// blank lines are ignored
let multi_line_string = "line one\n\n\nline two\n\n\nline three"
lines(multi_line_string);

reading a file as an array of lines the lines() function can be combined with the output of file_read() to get the contents of a text file as an array of lines. this process must be done in two steps; attempting to combine lines() with file_read() in one statement does not provide the expected results.

import { file_read } from "std"
import { lines } from "std"

// works as expected
unsafe let some_contents = file_read("/tmp/somefile.txt");

let some_array = lines(some_contents);

loop index, element in some_array {
    echo "{index} {element}"
}

// DOES NOT work as expected
unsafe let some_array = lines(file_read("/tmp/somefile.txt"))

loop index, element in some_array {
    echo "{index} {element}"
}

words()

this function is listed in the latest master branch as of 2024-06-26 but is not implemented in amber 1.0.

join()

joins the elements of an array into a string using a delimiter. the array being joined must be an array of strings. if the delimiter is more than once character, only the first character of the delimter is used.

import { join } from "std"

// join with one char delimiter
let some_array = ["foo", "bar", "baz"]
join(some_array, ",") // foo,bar,baz

let some_array = ["❤️ ", "👍", "🎉"]
join(some_array, ",") // ❤️ ,👍,🎉

// join with empty delimiter
let some_array = ["foo", "bar", "baz"]
join(some_array, "") // foobarbaz

// DOES NOT work as expected
// join with multi-char delimiter
let some_array = ["foo", "bar", "baz"]
join(some_array, "AND") // fooAbarAbaz

// ERROR
// join array of non-text items
let some_array = [1, 2, 3]
join(some_array, ",") // 1st argument 'list' of function 'join' expects type '[Text]', but '[Num]' was given

includes()

tests if an array includes an element.

import { includes } from "std"

let some_array = ["foo", "bar", "baz"]

echo includes(some_array, "bar") // true

echo includes(some_array, "not bar") // false

echo includes(some_array, 2) // false

chars()

splits a string into an array of characters.

import { chars } from "std"

// string split into characters
chars("aeiou") // ["a", "e", "i", "o", "u"]

// spaces count as characters
chars("ae iou") // ["a", "e", " ", "i", "o", "u"]

// multi-byte safe
chars("長度") // ["長", "度"]

// emojis are sometimes two or more characters. be warned.
chars("❤️ 👍🎉") // ["❤", "", "", "👍", "🎉"]

math commands

there are two convenience functions for math and numbers.

sum()

sums all the elements of an array. all array elements must be of type Num, which includes both integers and floating-point numbers.

import { sum } from "std"

// an array of integers
let some_array = [1, 3, 5]
echo sum(some_array) // 9

// an array of floats
let some_array = [2.5, 6.5]
echo sum(some_array) // 9

// results can be floats
let some_array = [3.1, 6.5]
echo sum(some_array) // 9.6

// floats and ints can be mixed because they are both of type Num
let some_array = [1, 1.5, 6.5]
echo sum(some_array) // 9

// strings not allowed
let some_array = [1, "3", 5]
echo sum(some_array) // ERROR: Expected array value of type 'Num'

parse()

attempts to convert a string of digits to a Num value. requires either a failed block or to be declared as unsafe. in the event of failure to parse the string, null is returned.

import { parse } from "std"

// parse a numeric string to a Num 
parse("1") failed { // 1
    echo "coult not parse"
}

unsafe echo parse("1") // 1

// cannot parse non-numeric strings
unsafe echo parse("a") // null

// cannot parse floats
unsafe echo parse("19.0") // null

system commands

commands for script and system management.

exit()

terminates the script and returns a status code. status 0 indicates success, all other numbers indicate an error. status must be an integer

import { exit } from "std"

// exit successfully
exit(0)

// exit with an error code
exit(2)

// ERROR
// exit requires text argument
exit("custom error message") // 1st argument 'code' of function 'exit' expects type 'Num', but 'Text' was given

// ERROR
// must be an integer
exit(3.4)

has_failed()

accepts a command, runs eval on it and returns true if the status code of the command is 0, false otherwise.

for scripting, this function almost certainly serves no purpose and, most likely, is used by amber itself for implementing things like the failed feature.

referencing the std source

the official documentation currently does not cover the standard libary. to further investigate these commands and discover new ones as they are released, the best option is is refer to the std/main.ab source file.

https://github.com/Ph0enixKM/Amber/blob/master/src/std/main.ab

🔎 this post was originally written in the grant horwood technical blog