Introduction

Ion is a modern system shell that features a simple, yet powerful, syntax. It is written entirely in Rust, which greatly increases the overall quality and security of the shell, eliminating the possibilities of a ShellShock-like vulnerability, and making development easier. It also offers a level of performance that exceeds that of Dash, when taking advantage of Ion's features. While it is developed alongside, and primarily for, RedoxOS, it is a fully capable on other *nix platforms, and we are currently searching for a Windows developer to port it to Windows.

Goals

Syntax and feature decisions for Ion are made based upon three measurements: is the feature useful, is it simple to use, and will it's implementation be efficient to parse and execute? A feature is considered useful if there's a valid use case for it, in the concept of a shell language. The syntax for the feature should be simple for a human to read and write, with extra emphasis on readability, given that most time is spent reading scripts than writing them. The implementation should require minimal to zero heap allocations, and be implemented in a manner that requires minimal CPU cycles (so long as it's also fully documented and easy to maintain!).

It should also be taken into consideration that shells operate entirely upon strings, and therefore should be fully equipped for all manner of string manipulation capabilities. That means that users of a shell should not immediately need to grasp for tools like cut, sed, and awk. Ion offers a great deal of control over slicing and manipulating text. Arrays are treated as first class variables with their own unique @ sigil. Strings are also treated as first class variables with their own unique $ sigil. Both support being sliced with [range], and they each have their own supply of methods.

Why Not POSIX?

If Ion had to follow POSIX specifications, it wouldn't be half the shell that it is today, and there'd be no solid reason to use Ion over any other existing shell, given that it'd basically be the same as every other POSIX shell. Redox OS itself doesn't follow POSIX specifications, and neither does it require a POSIX shell for developing Redox's userspace. It's therefore not meant to be used as a drop-in replacement for Dash or Bash. You should retain Dash/Bash on your system for execution of Dash/Bash scripts, but you're free to write new scripts for Ion, or use Ion as the interactive shell for your user session. Redox OS, for example, also contains Dash for compatibility with software that depends on POSIX scripts.

That said, Ion's foundations are heavily inspired by POSIX shell syntax. If you have experience with POSIX shells, then you already have a good idea of how most of Ion's core features operate. A quick sprint through this documentation will bring you up to speed on the differences between our shell and POSIX shells. Namely, we carry a lot of the same operators: $, |, ||, &, &&, >, <, <<, <<<, $(), $(()). Yet we also offer some functionality of our own, such as @, @(), $method(), @method(), ^|, ^>, &>, &|. Essentially, we have taken the best components of the POSIX shell specifications, removed the bad parts, and implemented even better features on top of the best parts. That's how open source software evolves: iterate, deploy, study, repeat.

Features

Miscellaneous

Implicit cd

Like the Friendly Interactive Shell, Ion also supports executing the cd command automatically when given a path. Paths are denoted by beginning with .///~, or ending with /.

~/Documents # cd ~/Documents
..          # cd ..
.config     # cd .config
examples/   # cd examples/

XDG App Dirs Support

All files created by Ion can be found in their respective XDG application directories. In example, the init file for Ion can be found in $HOME/.config/ion/initrc on Linux systems; and the history file can be found at $HOME/.local/share/ion/history. On the first launch of Ion, a message will be given to indicate the location of these files.

Quoting Rules

In general, double quotes allow expansions within quoted text, whereas single quotes do not. An exception to the rule is brace expansions, where double quotes are not allowed. When arguments are parsed, the general rule is the replace newlines with spaces. When double-quoted expansions will retain their newlines. Quoting rules are reversed for heredocs and for loops.

Multi-line Arguments

If a line in your script becomes too long, you may signal to Ion to continue reading the next line by appending an \ character at the end of the line. This will ignore newlines.

command arg arg2 \
    arg3 arg4 \
    arg 5

Multi-line Comments

If a comment needs to contain newlines, you may do so by having an open quote, as Ion will only begin parsing supplied commands that are terminated. Either double or single quotes may be used for this purpose, depending on which quoting rules that you need.

echo "This is the first line
    this is the second line
    this is the third line"

Prompt Function

The prompt may optionally be generated from a function, instead of a string. Take note, however, that prompts generated from functions aren't as efficient, due to the need to perform a fork and capture the output of the fork to use as the prompt. To use a function for generating the prompt, simply create a function whose name is PROMPT, and the output of that command will be used as the prompt. Below is an example:

fn PROMPT
    echo -n "${PWD}# "
end

General Tips

Let Arithmetic vs Arithmetic Expansions

Using let arithmetic is generally faster than $(()) expansions. The arithmetic expansions should be used for increasing readability, or more complex arithmetic; but if speed is important, multiple let arithmetic statements will tend to be faster than a single arithmetic expansion.

Variables

The let builtin is used to create local variables within the shell, and apply basic arithmetic to variables. The export keyword may be used to do the same for the creation of external variables. Variables cannot be created the POSIX way, as the POSIX way is awkard to read/write and parse.

let string_variable = "hello string"
let array_variable = [ hello array ]

Multiple Assignments

Ion also supports setting multiple values at the same time

let a b = one two
echo $a
echo $b

let a b = one [two three four]
echo $a
echo @b

Output

one
two
one
two three four

Type-Checked Assignments

It's also possible to designate the type that a variable is allowed to be initialized with. Boolean type assignments will also normalize inputs into either true or false. When an invalid value is supplied, the assignment operation will fail and an error message will be printed. All assignments after the failed assignment will be ignored.

let a:bool = 1
let b:bool = true
let c:bool = n
echo $a $b $c


let a:str b[str] c:int d:float[] = one [two three] 4 [5.1 6.2 7.3]
echo $a
echo @b
echo $c
echo @d

Output

true
true
false
one
two three
4
5.1 6.2 7.3

Dropping Variables

Variables may be dropped from a scope with the drop keyword. Considering that a variable can only be assigned to one type at a time, this will drop whichever value is assigned to that type.

let string = "hello"
drop string
let array = [ hello world ]
drop array

Supported Primitive Types

  • str: A string, the essential primitive of a shell.
  • bool: A value which is either true or false.
  • int: An integer is any whole number.
  • float: A float is a rational number (fractions represented as a decimal).

Arrays

The [T] type, where T is a primitive, is an array of that primitive type.

Maps

Likewise, hmap[T] and bmap[T] work in a similar fashion, but are a collection of key/value pairs, where the key is always a str, and the value is defined by the T.

String Variables

Using the let builtin, a string can easily be created by specifying the name, and an expression that will be evaluated before assigning it to that variable.

let git_branch = $(git rev-parse --abbrev-ref HEAD ^> /dev/null)

Calling a string variable.

To call a string variable, you may utilize the $ sigil along with the name of the variable. For more information on expansions, see the expansions section of this manual.

echo $git_branch

Slicing a string.

Strings can be sliced in Ion using a range.

let foo = "Hello, World"
echo $foo[..5]
echo $foo[7..]
echo $foo[2..9]

String concatenation

The ++= and ::= operators can be used to efficiently concatenate a string in-place.

let string = "ello"
let string ::= H
let string ++= ", world!"
echo $string
Hello, world!

Array Variables

The [] syntax in Ion is utilized to denote that the contents within should be parsed as an array expression. Array variables are also created using the same let keyword, but let makes the distinction between a string and an array by additionally requiring that all array arguments are wrapped within the [] syntax. If an array is supplied to let that is not explicitly declared as an array, then it will be coerced into a space-separated string. This design decision was made due to the possibility of an expanded array with one element being interpreted as a string.

Once created, you may call an array variable in the same manner as a string variable, but you must use the @ sigil instead of $. When expanded, arrays will be expanded into multiple arguments, so it is possible to use arrays to set multiple arguments in commands. Do note, however, that if an array is double quoted, it will be coerced into a string, which is a behavior that is equivalent to invoking the $join(array) method.

NOTE: Brace expansions also create arrays.

Create a new array

Arguments enclosed within brackets are treated as elements within an array.

let array = [ one two 'three four' ]

Indexing into an array

Values can be fetched from an array via their position in the array as the index.

let array = [ 1 2 3 4 5 6 7 8 9 10 ]
echo @array[0]
echo @array[5..=8]

Copy array into a new array

Passing an array within brackets enables performing a deep copy of that array.

let array_copy = [ @array ]

Array join

This will join each element of the array into a string, adding spaces between each element.

let array = [ hello world ]
let other_array = [ this is the ion ]
let array = [ @array @other_array shell ]
let as_string = @array
echo @array
echo $array
hello world this is the ion shell
hello world this is the ion shell

Array concatenation

The ++= and ::= operators can be used to efficiently concatenate an array in-place.

let array = [1 2 3]
let array ++= [5 6 7]
let array ::= 0
echo @array
0 1 2 3 4 5 6 7

Expand array as arguments to a command

Arrays are useful to pass as arguments to a command. Each element will be expanded as an individual argument, if any arguments exist.

let args = [-l -a --color]
ls @args

Maps

Maps, (AKA dictionaries), provide key-value data association. Ion has two variants of maps: BTree and Hash. Hash maps are fast but store data in a random order; whereas BTree maps are slower but keep their data in a sorted order. If not sure what to use, it's best to go with Hash maps.

Creating maps uses the same right-hand-side array syntax. However for design simplicity, users must annotate the type to translate the array into a map.

Please note, the map's inner type specifies the value's type and not of the key. Keys will always be typed str.

Create a HashMap

let hashmap:hmap[str] = [ foo=hello bar=world fizz=I buzz=was bazz=here ]

Create a BTreeMap

let btreemap:bmap[str] = [ foo=hello bar=world fizz=I buzz=was bazz=here ]

Fetch a variables by key

let x = bazz
echo @hashmap[bar] @hashmap[$x]

Insert a new key

let x[bork] = oops

Iterate keys in the map

echo @keys(hashmap)

Iterate values in the map

echo @values(hashmap)

Iterate key/value pairs in the map

echo @hashmap

Iterate key/value paris in a loop

for key value in @hashmap
    echo $key: $value
end

Let Arithmetic

Ion supports applying some basic arithmetic, one operation at a time, to string variables. To specify to let to perform some arithmetic, designate the operation immediately before =. Operators currently supported are:

  • [x] Add (+)
  • [x] Subtract (-)
  • [x] Multiply (*)
  • [x] Divide (/)
  • [x] Integer Divide (//)
  • [ ] Modulus (%)
  • [x] Powers (**)

Individual Assignments

The following examples are a demonstration of applying a mathematical operation to an individual variable -- first assigning 0 to the variable, then applying arithmetic operations to it.

let value = 0
let value += 5
let value -= 2
let value *= 3
let value //= 2
let value **= 10
let value /= 2

Multiple Assignments

It's also possible to perform a mathematical operation to multiple variables. Each variable will be designated with a paired value.

let a b = 5 5
let a b += 3 2
let a b -= 1 1
echo $a $b

This will output the following:

7 6

Exporting Variables

The export builtin operates identical to the let builtin, but it does not support arrays, and variables are exported to the OS environment.

export GLOBAL_VAL = "this"

Scopes

A scope is a batch of commands, often ended by end. Things like if, while, etc all take a scope to execute.

In ion, just like most other languages, all variables are destroyed once the scope they were defined in is gone. Similarly, variables from other scopes can still be overriden. However, ion has no dedicated keyword for updating an existing variable currently, so the first invokation of let gets to "own" the variable.

This is an early implementation and will be improved upon with time

let x = 5 # defines x

# This will always execute.
# Only reason for this check is to show how
# variables defined inside it are destroyed.
if test 1 == 1
  let x = 2 # updates existing x
  let y = 3 # defines y

  # end of scope, y is deleted since it's owned by it
end

echo $x # prints 2
echo $y # prints nothing, y is deleted already

Functions

Functions have the scope they were defined in. This ensures they don't use any unintended local variables that only work in some cases. Once again, this matches the behavior of most other languages, apart from perhaps LOLCODE.

let x = 5 # defines x

fn print_vars
  echo $x # prints 2 because it was updated before the function was called
  echo $y # prints nothing, y is owned by another scope
end

if test 1 == 1
  let x = 2 # updates existing x
  let y = 3 # defines y
  print_vars
end

Expansions

Expansions provide dynamic string generation capabilities. These work identical to the standard POSIX way, but there are a few major differences: arrays are denoted with an @ sigil, and have their own variant of process expansions (@()) which splits outputs by whitespace; the arithmetic logic is more feature-complete, supports floating-point math, and handles larger numbers; and Ion supports methods in the same manner as the Oil shell.

Variable Expansions

Expansions provide dynamic string generation capabilities. These work identical to the standard POSIX way, but there are a few major differences: arrays are denoted with an @ sigil, and have their own variant of process expansions (@()) which splits outputs by whitespace; and our arithmetic logic is destined to be both more feature-complete, supports floating-point math, and handles larger numbers.

String Variables

Like POSIX shells, the $ sigil denotes that the following expression will be a string expansion. If the character that follows is an accepted ASCII character, all characters that follow will be collected until either a non-accepted ASCII character is found, or all characters have been read. Then the characters that were collected will be used as the name of the string variable to substitute with.

$ let string = "example string"
$ echo $string
> example string
$ echo $string:$string
> example string:example string

NOTE:

  • Accepted characters are characters ranging from A-Z, a-z, 0-9, and _.
  • If not double quoted, newlines will be replaced with spaces.

Array Variables

Unlike POSIX, Ion also offers support for first class arrays, which are denoted with the @ sigil. The rules for these are identical, but instead of returning a single string, it will return an array of strings. This means that it's possible to use an array variable as arguments in a command, as each element in the array will be treated as a separate shell word.

$ let array = [one two three]
$ echo @array
> one two three
$ cmd @args

However, do note that double-quoted arrays are coerced into strings, with spaces separating each element. It is equivalent to using the $join(array) method. Containing multiple arrays within double quotes is therefore equivalent to folding the elements into a single string.

Braced Variables

Braces can also be used when you need to integrate a variable expansion along accepted ASCII characters.

echo ${hello}world
echo @{hello}world

Aliases

Ion also supports aliasing commands, which can be defined using the alias builtin. Aliases are often used as shortcuts to repetitive command invocations.

alias ls = "ls --color"

Process Expansions

Ion supports two forms of process expansions: string-based process expansions ($()) that are commonly found in POSIX shells, and array-based process expansions (@()), a concept borrowed from the Oil shell. Where a string-based process expansion will execute a command and return a string of that command's standard output, an array-based process expansion will split the output into an array delimited by whitespaces.

let string = $(cmd args...)
let array = @(cmd args...)

NOTES:

  • To split outputs by line, see @lines($(cmd)).
  • @(cmd) is equivalent to @split($(cmd))
  • If not double quoted, newlines will be replaced with spaces

Brace Expansions

Sometimes you may want to generate permutations of strings, which is typically used to shorten the amount of characters you need to type when specifying multiple arguments. This can be achieved through the use of braces, where braced tokens are comma-delimited and used as infixes. Any non-whitespace characters connected to brace expansions will also be included within the brace permutations.

NOTE: Brace expansions will not work within double quotes.

$ echo filename.{ext1,ext2}
> filename.ext1 filename.ext2

Multiple brace tokens may occur within a braced collection, where each token expands the possible permutation variants.

$ echo job_{01,02}.{ext1,ext2}
> job_01.ext1 job_01.ext2 job_02.ext1 job_02.ext2

Brace tokens may even contain brace tokens of their own, as each brace element will also be expanded.

$ echo job_{01_{out,err},02_{out,err}}.txt
> job_01_out.txt job_01_err.txt job_02_out.txt job_02_err.txt

Braces elements may also be designated as ranges, which may be either inclusive or exclusive, descending or ascending, numbers or latin alphabet characters.

$ echo {1..10}
> 1 2 3 4 5 6 7 8 9

$ echo {1...10}
> 1 2 3 4 5 6 7 8 9 10

$ echo {10..1}
> 10 9 8 7 6 5 4 3 2

$ echo {10...1}
> 10 9 8 7 6 5 4 3 2 1

$ echo {a..d}
> a b c

$ echo {a...d}
> a b c d

$ echo {d..a}
> d c b

$ echo {d...a}
> d c b a

It's also important to note that, as brace expansions return arrays, they may be used in for loops.

for num in {1..10}
    echo $num
end

Arithmetic Expansions

We've exported our arithmetic logic into a separate crate calculate. We use this library for both our calc builtin, and for parsing arithmetic expansions. Use calc if you want a REPL for arithmetic, else use arithmetic expansions ($((a + b))) if you want the result inlined. Variables may be passed into arithmetic expansions without the $ sigil, as it is automatically inferred that text references string variables. Supported operators are as below:

  • Add ($((a + b)))
  • Subtract($((a - b)))
  • Divide($((a / b)))
  • Multiply($((a * b)))
  • Powers($((a ** b)))
  • Square($((a²)))
  • Cube($((a³)))
  • Modulus($((a % b)))
  • Bitwise XOR($((a ^ b)))
  • Bitwise AND($((a & b)))
  • Bitwise OR($((a | b))))
  • Bitwise NOT($(a ~ b)))
  • Left Shift($((a << b)))
  • Right Shift($((a >> b)))
  • Parenthesis($((4 * (pi * r²))))

Take note, however, that these expressions are evaluated to adhere to order of operation rules. Therefore, expressions are not guaranteed to evaluate left to right, and parenthesis should be used when you are unsure about the order of applied operations.

Method Expansions

There are two forms of methods within Ion: array methods, and string methods. Array methods are methods which return arrays, and string methods are methods which return strings. The distinction is made between the two by the sigil that is invoked when calling a method. For example, if the method is denoted by the $ sigil, then it is a string method. Otherwise, if it is denoted by the @ sigil, then it is an array method. Example as follows:

echo $method_name(variable)
for elem in @method_name(variable); echo $elem; end

Methods are executed at the same time as other expansions, so this leads to a performance optimization when combining methods with other methods or expansions. Ion includes a number of these methods for common use cases, but it is possible to create and/or install new methods to enhance the functionality of Ion. Just ensure that systems executing your Ion scripts that require those plugins are equipped with and have those plugins enabled. If you have ideas for useful methods that would be worth including in Ion by default, feel free to open a feature request to discuss the idea.

Methods Support Inline Expressions

So we heard that you like methods, so we put methods in your methods. Ion methods accept taking expressions as their arguments -- both for the input parameter, and any supplied arguments to control the behavior of the method.

echo $method($(cmd...) arg)

let string_var = "value in variable"
echo $method(string_var)

echo $method("actual value" arg)

Overloaded Methods

Some methods may also perform different actions when supplied a different type. The $len() method, for example, will report the number of graphemes within a string, or the number of elements within an array. Ion is able to determine which of the two were provided based on the first character in the expression. Quoted expressions, and expressions with start with $, are strings; whereas expressions that start with either [ or @ are treated as arrays.

echo $len("a string")
echo $len([1 2 3 4 5])

Method Arguments

Some methods may have their behavior tweaked by supplying some additional arguments. The @split() method, for example, may be optionally supplied a pattern for splitting. At the moment, a comma is used to specify that arguments are to follow the input, but each argument supplied after that is space-delimited.

for elem in @split("some space-delimited values"); echo $elem; end
for elem in @split("some, comma-separated, values" ", "); echo $elem; end

String Methods

The following are the currently-supported string methods:

ends_with

Defaults to string variables. When supplied with a pattern, it will return one if the string ends with it. Zero otherwise.

Examples

echo $ends_with("FOOBAR" "BAR")
echo $ends_with("FOOBAR" "FOO")

Output

1
0

contains

Defaults to string variables. When supplied with a pattern, it will return one if the string contains with it. Zero otherwise.

Examples

echo $contains("FOOBAR" "OOB")
echo $contains("FOOBAR" "foo")

Output

1
0

starts_with

Defaults to string variables. When supplied with a pattern, it will return one if the string starts with it. Zero otherwise.

Examples

echo $starts_with("FOOBAR" "FOO")
echo $starts_with("FOOBAR" "BAR")

Output

1
0

basename

Defaults to string variables. When given a path-like string as input, this will return the basename (complete filename, extension included). IE: /parent/filename.ext -> filename.ext

Examples

echo $basename("/parent/filename.ext")

Output

filename.ext

extension

Defaults to string variables. When given a path-like string as input, this will return the extension of the complete filename. IE: /parent/filename.ext -> ext.

Examples

echo $extension("/parent/filename.ext")

Output

ext

filename

Defaults to string variables. When given a path-like string as input, this will return the file name portion of the complete filename. IE: /parent/filename.ext -> filename.

Examples

echo $filename("/parent/filename.ext")

Output

filename

join

Defaults to array variables. When given an array as input, the join string method will concatenate each element in the array and return a string. If no argument is given, then those elements will be joined by a single space. Otherwise, each element will be joined with a given pattern.

Examples

let array = [1 2 3 4 5]
echo $join(array)
echo $join(array ", ")

Output

1 2 3 4 5
1, 2, 3, 4, 5

find

Defaults to string variables. When given an string, it returns the first index in which that string appears. It returns -1 if it isn't contained.

Examples

echo $find("FOOBAR" "OB")
echo $find("FOOBAR" "ob")

Output

2
-1

len

Defaults to string variables. Counts the number of graphemes in the output. If an array expression is supplied, it will print the number of elements in the array.

Examples

echo $len("foobar")
echo $len("❤️")
echo $len([one two three four])

Output

6
1
4

len_bytes

Defaults to string variables. Similar to the len method, but counts the number of actual bytes in the output, not the number of graphemes.

Examples

echo $len_bytes("foobar")
echo $len_bytes("❤️")

Output

6
6

parent

Defaults to string variables. When given a path-like string as input, this will return the parent directory's name. IE: /root/parent/filename.ext -> /root/parent

Examples

echo $parent("/root/parent/filename.ext")

Output

/root/parent

repeat

Defaults to string variables. When supplied with a number, it will repeat the input N amount of times, where N is the supplied number.

Examples

echo $repeat("abc, " 3)

Output

abc, abc, abc,

replace

Defaults to string variables. Given a pattern to match, and a replacement to replace each match with, a new string will be returned with all matches replaced.

Examples

let input = "one two one two"
echo $replace(input, one 1)
echo $replace($replace(input one 1) two 2)

Output

1 two 1 two
1 2 1 2

replacen

Defaults to string variables. Equivalent to replace, but will only replace the first N amount of matches.

Examples

let input = "one two one two"
echo $replacen(input "one" "three" 1)
echo $replacen(input "two" "three" 2)

Output

three two one two
one three one three

regex_replace

Defaults to string variables. Equivalent to replace, but the first argument will be treated as a regex.

Examples

echo $regex_replace("FOOBAR" "^F" "f")
echo $regex_replace("FOOBAR" "^f" "F")

Output

fOOBAR
FOOBAR

reverse

Defaults to string variables. Simply returns the same string, but with each grapheme displayed in reverse order.

Examples

echo $reverse("foobar")

Output

raboof

to_lowercase

Defaults to string variables. All given strings have their characters converted to an lowercase equivalent, if an lowercase equivalent exists.

Examples

echo $to_lowercase("FOOBAR")

Output

foobar

to_uppercase

Defaults to string variables. All given strings have their characters converted to an uppercase equivalent, if an uppercase equivalent exists.

Examples

echo $to_uppercase("foobar")

Output

FOOBAR

escape

Defaults to string variables. Escapes the content of the string.

Example

let line = " Mary   had\ta little  \n\t lamb\t"
echo $escape($line)

Output

 Mary   had\\ta little  \\n\\t lamb\\t

unescape

Defaults to string variables. Unescapes the content of the string.

Example

let line = " Mary   had\ta little  \n\t lamb\t"
echo $unescape($line)

Output

 Mary   had	a little
     lamb

Array Methods

The following are the currently-supported array methods.

lines

Defaults to string variables. The supplied string will be split into one string per line in the input argument.

Examples

for line in @lines($unescape("first\nsecond\nthird")
    echo $line
end

Output

first
second
third

split

Defaults to string variables. The supplied string will be split according to a pattern specified as an argument in the method. If no pattern is supplied, then the input will be split by whitespace characters. Useful for splitting simple tabular data.

Examples

for data in @split("person, age, some data" ", ")
    echo $data
end

for data in @split("person age data")
    echo $data
end

Output

person
age
some data
person
age
data

split_at

Defaults to string variables. The supplied string will be split in two pieces, from the index specified in the second argument.

Examples

echo @split_at("FOOBAR" "3")
echo @split_at("FOOBAR")
echo @split_at("FOOBAR" "-1")
echo @split_at("FOOBAR" "8")

Output

FOO BAR
ion: split_at: requires an argument
ion: split_at: requires a valid number as an argument
ion: split_at: value is out of bounds

bytes

Defaults to string variables. Returns an array where the given input string is split by bytes and each byte is displayed as their actual 8-bit number.

Examples

echo @bytes("foobar")

Output

102 111 111 98 97 114

chars

Defaults to string variables. Returns an array where the given input string is split by chars.

Examples

for char in @chars("foobar")
    echo $char
end

Output

f
o
o
b
a
r

graphemes

Defaults to string variables. Returns an array where the given input string is split by graphemes.

Examples

for grapheme in @graphemes("foobar")
    echo $grapheme
end

Output

f
o
o
b
a
r

reverse

Defaults to array variables. Returns a reversed copy of the input array.

Examples

echo @reverse([1 2 3])

Output

3 2 1

Ranges & Slicing Syntax

Ion supports a universal syntax for slicing strings and arrays. For maximum language support, strings are sliced and indexed by graphemes. Arrays are sliced and indexed by their elements. Slicing uses the same [] characters as arrays, but the shell can differentation between a slice and an array based on the placement of the characters (immediately after an expansion).

NOTE: It's important to note that indexes count from 0, as in most other languages.

Exclusive Range

The exclusive syntax will grab all values starting from the first index, and ending on the Nth element, where N is the last index value. The Nth element's ID is always one less than the Nth value.

$ let array = [{1...10}]
$ echo @array[0..5]
> 1 2 3 4 5

$ echo @array[..5]
> 1 2 3 4 5

$ let string = "hello world"
$ echo $string[..5]
> hello
$ echo $string[6..]
> world

Inclusive Range

When using inclusive ranges, the end index does not refer to the Nth value, but the actual index ID.

$ let array = [{1...10}]
$ echo @array[1...5]
> 1 2 3 4 5 6

Descending Ranges

Ranges do not have to always be specified in ascending order. Descending ranges are also supported. However, at this time you cannot provide an descending range as an index to an array.

$ echo {10...1}
> 10 9 8 7 6 5 4 3 2 1
$ echo {10..1}
> 10 9 8 7 6 5 4 3 2

Negative Values Supported

Although this will not work for arrays, you may supply negative values with ranges to create negative values in a range of numbers.i

$ echo {-10...10}
> -10 -9 -8 -7 -6 -5 -4 -3 -2 -1 0 1 2 3 4 5 6 7 8 9 10

Stepping Ranges

Stepped ranges are also supported.

Stepping Forward w/ Brace Ranges

Brace ranges support a syntax similar to Bash, where the starting index is supplied, followed by two periods and a stepping value, followed by either another two periods or three periods, then the end index.

$ echo {0..3...12}
> 0 3 6 9 12
$ echo {1..2..12}
> 0 3 6 9
$ let array = [{1...30}]

Stepping Forward w/ Array Slicing

Array slicing, on the other hand, uses a more Haskell-ish syntax, whereby instead of specifying the stepping with two periods, it is specified with a comma.

$ let array = [{0...30}]
$ echo @array[0,3..]
> 0 3 6 9 12 15 18 21 24 27 30

Stepping In Reverse w/ Brace Ranges

Brace ranges may also specify a range that descends in value, rather than increases.

$ echo {10..-2...-10}
> 10 8 6 4 2 0 -2 -4 -6 -8 -10
$ echo {10..-2..-10}
> 10 8 6 4 2 0 -2 -4 -6 -8

Stepping In Reverse w/ Array Slicing

Arrays may also be sliced in reverse order using the same syntax as for reverse. Of course, negative values aren't allowed here, so ensure that the last value is never less than 0. Also note that when a negative stepping is supplied, it is automatically inferred for the end index value to be 0 when not specified.

$ let array = [{0...30}]
$ echo @array[30,-3..]
> 30 27 24 21 18 15 12 9 6 3 0

Process Expansions Also Support Slicing

Variables aren't the only elements that support slicing. Process expansions also support slicing.

$ echo $(cat file)[..10]
$ echo @(cat file)[..10]

Flow Control

As Ion features an imperative paradigm, the order that statements are evaluated and executed is determined by various control flow keywords, such as if, while, for, break, and continue. Ion's control flow logic is very similar to POSIX shells, but there are a few major differences, such as that all blocks are ended with the end keyword; and the do/then keywords aren't necessary.

Conditionals

Conditionals in a language are a means of describing blocks of code that may potentially execute, so long as certain conditions are met. In Ion, as with every other language, we support this via if statements, but unlike POSIX shells, we have a cleaner syntax that will require less boilerplate, and increase readability.

If Statements

The if keyword in Ion is designated as a control flow keyword, which works similar to a builtin command. The if builtin will have it's supplied expression parsed and executed. The return status of the executed command will then be utilized as a boolean value. Due to the nature of how shells operate though, a logical true result is a 0 exit status, which is an exit status that commands return when no errors are reported. If the value is not zero, it is considered false. Sadly, we can't go back in time to tell early UNIX application developers that 1 should indicate success, and 0 should indicate a general error, so this behavior found in POSIX shells will be preserved.

We supply a number of builtin commands that are utilized for the purpose of evaluating expressions and values that we create within our shell. One of these commands is the test builtin, which is commonly found in other POSIX shells, and whose flags and operation should be identical. TODO: Insert Manual Page Link To Our Implementation We also supply a not builtin, which may be convenient to use in conjuction with other commands in order to flip the exit status; and a matches builtin that performs a regex-based boolean match.

if test "foo" = $foo
    echo "Found foo"
else if matches $foo '[A-Ma-m]\w+'
    echo "we found a word that starts with A-M"
    if not matches $foo '[A]'
        echo "The word doesn't start with A"
    else
        echo "The word starts with 'A'"
    end
else
    echo "Incompatible word found"
end

A major distinction with POSIX shells is that Ion does not require that the if statement is followed with a then keyword. The else if statements are also written as two separate words, rather than as elif which is POSIX. And all blocks in Ion are ended with the end keyword, rather than fi to end an if statement. There is absolutely zero logical reason for a shell language to have multiple different keywords to end different expressions.

Complete List of Conditional Builtins

  • [x] and
  • [z] contains
  • [x] exists
  • [x] eq
  • [ ] intersects
  • [x] is
  • [x] isatty
  • [x] matches
  • [x] not
  • [x] or
  • [x] test
  • [ ] < (Polish Notation)
  • [ ] <= (Polish Notation)
  • [ ] > (Polish Notation)
  • [ ] >= (Polish Notation)
  • [ ] = (Polish Notation)

Using the && and || Operators

We also support performing conditional execution that can be performed within job execution, using the same familiar POSIX syntax. The && operator denotes that the following command should only be executed if the previous command had a successful return status. The || operator is therefore the exact opposite. These can be chained together so that jobs can be skipped over and conditionally-executed based on return status. This enables succintly expressing some patterns better than could be done with an if statement.

if test $foo = "foo" && test $bar = "bar"
    echo "foobar was found"
else
    echo "either foo or bar was not found"
end
test $foo = "foo" && test $bar = "bar" &&
    echo "foobar was found" ||
    echo "either foo or bar was not found"

Loops

Loops enable repeated execution of statements until certain conditions are met. There are currently two forms of loop statements: for loops, and while loops.

For Loops

For loops take an array of elements as the input; looping through each statement in the block with each element in the array. If the input is a string, however, that string will automatically coerce into a newline-delimited array.

for element in @array
    echo $element
end

Breaking From Loops

Sometimes you may need to exit from the loop before the looping is finished. This is achievable using the break keyword.

for element in {1...10}
    echo $element
    if test $element -eq 5
        break
    end
end
1
2
3
4

Continuing Loops

In other times, if you need to abort further execution of the current loop and skip to the next loop, the continue keyword serves that purpose.

for elem in {1...10}
    if test $((elem % 2)) -eq 1
        continue
    end
    echo $elem
end
2
4
6
8
10

While Loops

While loops are useful when you need to repeat a block of statements endlessly until certain conditions are met. It works similarly to if statements, as it also executes a command and compares the exit status before executing each loop.

let value = 0
while test $value -lt 6
    echo $value
    let value += 1
end
0
1
2
3
4
5

Chunked Iterations

Chunked iterations allow fetching multiple values at a time.

for foo bar bazz in {1..=10}
    echo $foo $bar $bazz
end
1 2 3
4 5 6
7 8 9
10

Matches

Matches will evaluate each case branch, and execute the first branch which succeeds. A case which is _ will execute if all other cases have failed.

match $string
    case "this"
        echo "do that"
    case "that"
        echo "else this"
    case _; echo "not found"
end

Matching string input with array cases

If the input is a string, and a case is an array, then a match will succeed if at least one item in the array is a match.

match five
    case [ one two three ]; echo "one of these matched"
    case [ four five six ]; echo "or one of these matched"
    case _; echo "no match found"
end

Matching array input with string cases

The opposite is true when the input is an array, and a case is a string.

match [ five foo bar ]
    case "one"; echo "this"
    case "two"; echo "that"
    case "five"; echo "found five"
    case _; echo "no match found"
end

Match guards

Match guards can be added to a match to employ an additional test

let foo = bar
match $string
    case _; echo "no match found"
    case "this" if eq $foo bar
        echo "this and foo = bar"
    case "this"
        echo "this and foo != bar"
end

Pipelines

Redirection

Redirection will write the output of a command to a file.

Redirect Stdout

command > stderr

Redirect Stderr

command ^> stderr

Redirect Both

command &> combined

Multiple Redirection

command > stdout ^> stderr &> combined

Concatenating Redirect

Instead of truncating and writing a new file with >, the file can be appended to with >>.

command > stdout
command >> stdout

Pipe

Pipe Stdout

command | command

Pipe Stderr

command ^| command

Pipe Both

command &| command

Combined

command | command > stdout

Functions

Functions help scripts to reduce the amount of code duplication and increase readability. Ion supports the creation of functions with a similar syntax to other languages.

The basic syntax of functions is as follos:

fn square
    let x = "5"
    echo $(( x * x ))
end

square
square

Every statement between the fn and the end keyword is part of the function. On every function call, those statements get executed. That script would ouput "25" two times.

If you want the square of something that isn't five, you can add arguments to the function.

fn square x
    echo $(( x * x ))
end

square 3

Type checking

Optionally, you can add type hints into the arguments to make ion check the types of the arguments:

fn square x:int
    echo $(( x * x ))
end

square 3
square a

You'd get as output of that script:

9
ion: function argument has invalid type: expected int, found value 'a'

You can use any of the [supported types](ch04-00-variables.md#Supported Types).

Multiple arguments

As another example:

fn hello name age:int hobbies[]
    echo $name ($age) has the following hobbies:
    for hobby in @hobbies
        echo "  $hobby"
    end
end

hello John 25 [ coding eating sleeping ]

Function piping

As with any other statement, you can pipe functions using read.

fn format_with pat
    read input
    echo $join(@split(input), $pat)
end

echo one two three four five | format_with "-"

Script Executions

Scripts can be created by designating Ion as the interpreter in the shebang line.

#!/usr/bin/env ion

Then writing the script as you would write it in the prompt. When finished, you can execute the shell by providing the path of the script to Ion as the argument, along with any additional arguments that the script may want to parse. Arguments can be accessed from the @args array, where the first element in the array is the name of the script being executed.

#!/usr/bin/env ion

if test $len(@args) -eq 1
    echo "Script didn't receive enough arguments"
    exit
end

echo Arguments: @args[1..]i

Signal Handling

  • SIGINT (Ctrl + C): Interrupt the running program with a signal to terminate.
  • SIGTSTP (Ctrl + Z): Send the running job to the background, pausing it.

Job Control

Disowning Processes

Ion features a disown command which supports the following flags:

  • -r: Remove all running jobs from the background process list.
  • -h: Specifies that each job supplied will not receive the SIGHUP signal when the shell receives a SIGHUP.
  • -a: If no job IDs were supplied, remove all jobs from the background process list.

Unlike Bash, job arguments are their specified job IDs.

Foreground & Background Tasks

When a foreground task is stopped with the Ctrl+Z signal, that process will be added to the background process list as a stopped job. When a supplied command ends with the & operator, this will specify to run the task the background as a running job. To resume a stopped job, executing the bg <job_id> command will send a SIGCONT to the specified job ID, hence resuming the job. The fg command will similarly do the same, but also set that task as the foreground process. If no argument is given to either bg or fg, then the previous job will be used as the input.

Exiting the Shell

The exit command will exit the shell, sending a SIGTERM to any background tasks that are still active. If no value is supplied to exit, then the last status that the shell received will be used as the exit status. Otherwise, if a numeric value is given to the command, then that value will be used as the exit status.

Suspending the Shell

While the shell ignores SIGTSTP signals, you can forcefully suspend the shell by executing the suspend command, which forcefully stops the shell via a SIGSTOP signal.

Builtin Commands

alias

alias NAME=DEFINITION
alias NAME DEFINITION

View, set or unset aliases

and

COMMAND; and COMMAND

Execute the command if the shell's previous status is success

bg

bg [PID]

Resumes a stopped background process. If no process is specified, the previous job will resume.

calc

calc [EXPRESSION]

Calculate a mathematical expression. If no expression is given, it will open an interactive expression engine. Type exit to leave the engine.

cd

cd [PATH]

Change the current directory and push it to the stack. Omit the directory to change to home

contains

contains KEY [VALUE...]

Evaluates if the supplied argument contains a given string

dirs

dirs

Display the current directory stack

disown

disown [-r | -h | -a ][PID...]

Disowning a process removes that process from the shell's background process table. If no process is specified, the most recently-used job is removed

drop

drop VARIABLE
drop -a ARRAY_VARIABLE

Drops a variable from the shell's variable map. By default, this will drop string variables from the string variable map. If the -a flag is specified, array variables will be dropped from the array variable map instead.

echo

echo [ -h | --help ] [-e] [-n] [-s] [STRING]...

Display a line of text

Options

  • -e: enable the interpretation of backslash escapes
  • -n: do not output the trailing newline
  • -s: do not separate arguments with spaces

Escape Sequences

When the -e argument is used, the following sequences will be interpreted:

  • \: backslash
  • \a: alert (BEL)
  • \b: backspace (BS)
  • \c: produce no further output
  • \e: escape (ESC)
  • \f: form feed (FF)
  • \n: new line
  • \r: carriage return
  • \t: horizontal tab (HT)
  • \v: vertical tab (VT)

ends-with

ends-with KEY [VALUE...]

Evaluates if the supplied argument ends with a given string

eq

eq [ -h | --help ] [not]

Returns 0 if the two arguments are equal

eval

eval COMMAND

evaluates the evaluated expression

exists

exists [-a ARRAY] [-b BINARY] [-d PATH] [--fn FUNCTION] [[-s] STRING]

Performs tests on files and text

exec

exec [-ch] [--help] [command [arguments ...]]

Execute a command, replacing the shell with the specified program. The arguments following the command become the arguments to the command.

options

  • -a ARRAY: array var is not empty
  • -b BINARY: binary is in PATH
  • -d PATH: path is a directory
  • -f PATH: path is a file
  • --fn FUNCTION: function is defined
  • -s STRING: string var is not empty
  • STRING: string is not empty

exit

exit

Exits the current session and kills all background tasks

false

false

Do nothing, unsuccessfully

fg

fg [PID]

Resumes and sets a background process as the active process. If no process is specified, the previous job will be the active process.

fn

fn

Print list of functions

help

help COMMAND

Display helpful information about a given command or list commands if none specified

history

history

Display a log of all commands previously executed

ion-docs

ion_docs

Opens the Ion manual

jobs

jobs

Displays all jobs that are attached to the background

matches

matches VARIABLE REGEX

Checks if a string matches a given regex

not

not COMMAND

Reverses the exit status value of the given command.

or

COMMAND; or COMMAND

Execute the command if the shell's previous status is failure

popd

popd

Pop a directory from the stack and returns to the previous directory

pushd

pushd DIRECTORY

Push a directory to the stack.

random

random
random SEED
random START END
random START STEP END
random choice [ITEMS...]

RANDOM generates a pseudo-random integer from a uniform distribution. The range (inclusive) is dependent on the arguments passed. No arguments indicate a range of [0; 32767]. If one argument is specified, the internal engine will be seeded with the argument for future invocations of RANDOM and no output will be produced. Two arguments indicate a range of [START; END]. Three arguments indicate a range of [START; END] with a spacing of STEP between possible outputs. RANDOM choice will select one random item from the succeeding arguments.

Due to limitations int the rand crate, seeding is not yet implemented

read

read VARIABLE

Read some variables

set

set [ --help ] [-e | +e] [-x | +x] [-o [vi | emacs]] [- | --] [STRING]...

Set or unset values of shell options and positional parameters. Shell options may be set using the '-' character, and unset using the '+' character.

OPTIONS

  • e: Exit immediately if a command exits with a non-zero status.

  • -o: Specifies that an argument will follow that sets the key map.

    • The keymap argument may be either vi or emacs.
  • -x: Specifies that commands will be printed as they are executed.

  • --: Following arguments will be set as positional arguments in the shell.

    • If no argument are supplied, arguments will be unset.
  • -: Following arguments will be set as positional arguments in the shell.

    • If no arguments are suppled, arguments will not be unset.

source

source [PATH]

Evaluate the file following the command or re-initialize the init file

starts-with

ends-with KEY [VALUE...]

Evaluates if the supplied argument starts with a given string

suspend

suspend

Suspends the shell with a SIGTSTOP signal

test

test [EXPRESSION]

Performs tests on files and text

Options

  • -n STRING: the length of STRING is nonzero
  • STRING: equivalent to -n STRING
  • -z STRING: the length of STRING is zero
  • STRING = STRING: the strings are equivalent
  • STRING != STRING: the strings are not equal
  • INTEGER -eq INTEGER: the integers are equal
  • INTEGER -ge INTEGER: the first INTEGER is greater than or equal to the first INTEGER
  • INTEGER -gt INTEGER: the first INTEGER is greater than the first INTEGER
  • INTEGER -le INTEGER: the first INTEGER is less than or equal to the first INTEGER
  • INTEGER -lt INTEGER: the first INTEGER is less than the first INTEGER
  • INTEGER -ne INTEGER: the first INTEGER is not equal to the first INTEGER
  • FILE -ef FILE: both files have the same device and inode numbers
  • FILE -nt FILE: the first FILE is newer than the second FILE
  • FILE -ot FILE: the first file is older than the second FILE
  • -b FILE: FILE exists and is a block device
  • -c FILE: FILE exists and is a character device
  • -d FILE: FILE exists and is a directory
  • -e FILE: FILE exists
  • -f FILE: FILE exists and is a regular file
  • -h FILE: FILE exists and is a symbolic link (same as -L)
  • -L FILE: FILE exists and is a symbolic link (same as -h)
  • -r FILE: FILE exists and read permission is granted
  • -s FILE: FILE exists and has a file size greater than zero
  • -S FILE: FILE exists and is a socket
  • -w FILE: FILE exists and write permission is granted
  • -x FILE: FILE exists and execute (or search) permission is granted

true

true

Do nothing, successfully

unalias

unalias VARIABLE...

Delete an alias

wait

wait

Waits until all running background processes have completed

which

which COMMAND

Shows the full path of commands

status

status COMMAND

Evaluates the current runtime status

Options

  • -l: returns true if shell is a login shell
  • -i: returns true if shell is interactive
  • -f: prints the filename of the currently running script or stdio

bool

bool VALUE

If the value is '1' or 'true', returns the 0 exit status

is

is VALUE VALUE

Returns 0 if the two arguments are equal

isatty

isatty [FD]

Returns 0 exit status if the supplied file descriptor is a tty.

Options

  • not: returns 0 if the two arguments are not equal.

Command history

General

  • Ions history can be found at $HOME/.local/share/ion/history
  • The history builtin can be used to display the entire command history
    • If you're only interested in the last X entries, use history | tail -n X
  • The histories' behavior can be changed via various local variables (see section Variables)
  • Unlike other shells, ion saves repeated commands only once:
# echo "Hello, world!"
Hello, world!
# true
# true
# false
# history
echo "Hello, world!"
true
false

Variables

The following local variables can be used to modify Ions history behavior:

HISTORY_SIZE

Determines how many entries of the history are kept in memory. Default value is 1000. Ideally, this value should be the same as HISTFILE_SIZE

HISTORY_IGNORE

Specifies which commands should NOT be saved in the history. This is an array and defaults to an empty array, meaning that all commands will be saved. Each element of the array can take one of the following options:

  • all -> All commands are ignored, nothing will be saved in the history.
  • no_such_command -> Commands which return NO_SUCH_COMMAND will not be saved in the history.
  • whitespace -> Commands which start with a whitespace character will not be saved in the history.
  • regex:xxx -> Where xxx is treated as a regular expression. Commands which match this regular expression will not be saved in the history.
  • duplicates -> All preceding duplicate commands are removed/ignored from the history after a matching command is entered.

Notes

  • You can specify as many elements as you want.
  • Any invalid elements will be silently ignored. They will still be present in the array though.
  • You can also specify as many regular expressions as you want (each as a separate element).
  • However, note that any command that matches at least one element will be ignored.
  • (Currently, ) there is no way to specify commands which should always be saved.
  • When specifying regex:-elements, it is suggested to surround them with single-quotes (')
  • As all variables, HISTORY_IGNORE is not saved between sessions. It is suggested to set it via ions init file.
  • The let HISTORY_IGNORE = [ .. ] command itself is not effected except if the assignment command starts with a whitespace and the whitespace element is specified in this assignment. See the following example:
# echo @HISTORY_IGNORE

# let HISTORY_IGNORE = [ all ] # saved
# let HISTORY_IGNORE = [ whitespace ] # saved
#  true # ignored
#  let HISTORY_IGNORE = [  ] # saved
#  let HISTORY_IGNORE = [ whitespace ] # ignored
# history
echo @HISTORY_IGNORE
let HISTORY_IGNORE = [ all ] # saved
let HISTORY_IGNORE = [ whitespace ] # saved
 let HISTORY_IGNORE = [  ] # saved

Examples

# let HISTORY_IGNORE = [ no_such_command ]
# true # saved
#  true # saved
# false # saved
# trulse # ignored
# let HISTORY_IGNORE = [ 'regex:.*' ] # behaves like 'all'
# true # ignored
#  true # ignored
# false # ignored
# trulse # ignored

Tips

I like to add regex:#ignore$ to my HISTORY_IGNORE. That way, whenever I want to ignore a command on the fly, I just need to add #ignore to the end of the line.

HISTFILE_ENABLED

Specifies whether the history should be read from/written into the file specified by HISTFILE. A value of 1 means yes, everything else means no. Defaults to 1.

HISTFILE

The file into which the history should be saved. At the launch of ion the history will be read from this file and when ion exits, the history of the session will be appended into the file. Defaults to $HOME/.local/share/ion/history

HISTFILE_SIZE

Specifies how many commands should be saved in HISTFILE at most. Ideally, this should have the same value as HISTORY_SIZE. Defaults to 100000.

HISTORY_TIMESTAMP

Specifies whether a corresponding timestamp should be recorded along with each command. The timestamp is indicated with a # and is unformatted as the seconds since the unix epoch. This feature is disabled by default and can be enabled by executing the following command: let HISTORY_TIMESTAMP = 1.