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[] 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
Supported Types
- []
- bool
- bool[]
- float
- float[]
- int
- int[]
- str
- str[]
- hmap[]
- bmap[]
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]
Dropping String Variables
The drop
command may be used to drop string variables.
let variable = "testing"
echo $variable
drop variable
echo $variable
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 as_string = @array
Array concatenation
The ++=
and ::=
operators can be used to efficient concatenate an array in-place.
let array = [1 2 3]
let array ++= [5 6 7]
let array ::= 0
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
Dropping Array Variables
The drop -a
command will drop array variables from the shell.
let array = [one two three]
echo @array
drop -a array
echo @array
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
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
- contains
- starts_with
- basename
- extension
- filename
- join
- find
- len
- len_bytes
- parent
- repeat
- replace
- replacen
- regex_replace
- reverse
- to_lowercase
- to_uppercase
- escape
- unescape
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
- [ ] contains
- [x] exists
- [ ] intersects
- [x] is
- [ ] 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
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
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
Matches
Matching syntax is still being discussed
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 aSIGHUP
. - -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
- If you're only interested in the last X entries, use
- 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
.