amber: writing bash scripts in amber instead. pt. 3: the standard library
Tags: linux, bash
Published: 2024-06-28 15:00:45

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](https://gbh.fruitbat.io/wp-content/uploads/2024/06/meme_amber3-1-903x1024.jpg "the flextape meme")<figcaption>a developer uses a standard library function to preform a basic but important task
</figcaption>

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

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

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

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

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

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

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

```dart
import { replace_once } from "std"

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

### `replace_regex()`
like `replace`, but using [`sed`](https://www.man7.org/linux/man-pages/man1/sed.1.html) for patter matching and replacement

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

```dart
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`](https://www.man7.org/linux/man-pages/man1/eval.1p.html) 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). 

```
https://github.com/Ph0enixKM/Amber/blob/master/src/std/main.ab
```
> 🔎  this post was originally written in the [grant horwood technical blog](https://gbh.fruitbat.io/2024/06/27/amber-writing-bash-scripts-in-amber-instead-pt-3-the-standard-library/)