Foreword
This is the official guide of Hush, a modern shell scripting language. A shell scripting language is a domain specific language which provides constructs for easily invoking and interconnecting external programs. These kind of languages are typically used for integrations in Unix systems and infrastructure programming. GNU Bash, Zsh and Fish are some of the most commonly used ones.
Hush's logo
But why do we need a new shell scripting language?
Traditional shell scripting languages are notoriously limited, error prone, and frequently result in programs that are hard to maintain. Even Google mentions it on their Shell Style Guide:
If you are writing a script that is more than 100 lines long, or that uses non-straightforward control flow logic, you should rewrite it in a more structured language now. Bear in mind that scripts grow. Rewrite your script early to avoid a more time-consuming rewrite at a later date.
Hush strives to enable the development of robust and maintainable shell scripts. It does so by providing industry proved programming constructs, support for beyond trivial data structures, sane error handling and variable expansion. While most shells are command interpreters that strive to become programming languages, Hush is more like a general purpose programming language with first class shell capabilities.
Overview
In order to attain it's goals, Hush takes great inspiration in Lua, the industry proven embedded scripting language. If you're familiar with Lua, you'll notice that, except for the shell capabilities, Hush feels a lot like a simplified version of it.
As such, Hush provides:
- Static scoping
- Strong dynamic typing
- Garbage collection
- First class functions
- Good support for functional programming
- Basic support for object oriented programming
- First class shell capabilities
While being somewhat similar to scripting languages like Lua, Python and Ruby, even though it is much simpler than those, Hush strives to feel pretty much like Bash when it comes to shell capabilities. There are only minor syntax differences regarding invoking and interconnecting external programs, and therefore you won't have to learn all the shell syntax again.
But while most shell and even generic purpose script languages focus a lot on flexibility, Hush favors robustness, which may come at cost of some flexibility. As such, the language will make it's best to empower the programmer to write robust scripts that work diverse scenarios. It does so by preventing, by design, whole classes of bugs that often occur in shell scripts.
Being a shell script language, the typical use cases for Hush are operating systems instrumenting and infrastructure programming. In practice, Hush should be a good fit in any scenario where the heavy lifting is done by external programs, and you just need to put them to work together.
Installation
Hush comes as a single binary, which can be installed on Unix systems without much hassle.
Packaged distributions
Currently, Hush is packaged for the following operating systems:
- Arch Linux: AUR
Binary download
Precompiled binaries may be downloaded from the Github releases page. Currently, there is only a x86 Linux binary. If you would like to help by providing binaries for other architectures or operating systems, please let me know.
Building from source
Hush can be installed from source using cargo:
cargo install hush
Editor support
The following editors have plugins available:
- Emacs: hush-mode.el. I'll release it on Melpa as soon as I find the time.
- VsCode: marketplace
Introduction
In this section, we'll learn more about the basic constructs provided by Hush. That'll enable you to implement Hush scripts using functions, data structures, and control flow.
Hello World
With Hush installed, we're able to execute scripts. Here's the traditional Hello world program in Hush:
#!/usr/bin/env hush
std.print("Hello world!")
The first line is a Shebang. It tells the operating system which interpreter to use for the script. The second line is a function call of the print
function from the standard library. When executed, this script will output Hello world!
to the standard output.
Making the script executable
To execute this script, save it to a file named hello-world.hsh
, give it execution permission, and then run it:
$ chmod +x hello-world.hsh
$ ./hello-world.hsh
Hello world!
Calling Hush directly
You can also execute a given script calling Hush with the script path as argument. When executing a script using this method, the Shebang is unnecessary, and will be disregarded as an ordinary comment.
$ hush hello-world.hsh
Hello world!
Alternatively, when given no aguments, Hush will start a non-interactive shell. You can go ahead and write your Hush script into the terminal. When you're done, press Ctrl-D and Hush will execute everything you've typed. Ctrl-D sends a special End of File (EOF) signal to the Hush interpreter so that it knows where the script ends.
Tooling
Hush provides some tools for static analyses of scripts, which can be invoked by passing flags to the shell. The most useful one is the --check
flag, which will check the script for syntax and semantic errors, without executing it.
Consider the following script.hsh
, which attempts to use the undeclared variable value
:
value = "Hello world!"
std.print(value)
We can check verify that the script has semantic errors:
$ hush --check script.hsh
Error: script.hsh (line 1, column 0) - undeclared variable 'value'
Error: script.hsh (line 2, column 10) - undeclared variable 'value'
To get a list of other useful flags, run hush --help
.
Type System
Hush is strongly dynamically typed, which means all values have a well formed type, but variables are untyped. Therefore, you may assign values of distinct types to a given variable. Python, Ruby, and Lua are also dynamically typed languages, in case you're familiar with any of them.
As in Lua, Hush proposes only a handful of built-in types, and no user-defined types. This makes the type system extremely simple, and yet still it remains quite expressive. The following types are available:
nil
: the unit type, usually for representing missing values.bool
: the boolean type.int
: a 64 bit integer type.float
: a 64 bit floating point type.char
: a C-like unsigned char type, 0-255.string
: a char-array like immutable string.array
: a heterogeneous array, 0-indexed (unlike in Lua).dict
: a heterogeneous hash map.function
: a callable function.error
: a special error type, to ease distinction of errors from other values. This type can only be instantiated by the built-instd.error
function.
Although it may seem like a limitation to only have a handful of types, Hush provides facilities that enable these types to be extremely flexible. We'll get more in depth about that in practice on the Paradigms section.
Basic Constructs
As Hush aims to be simple language, only standard control flow constructs and operations are supported.
Comments
Comments start with a #
:
# This is a comment.
Variables
In Hush, all variables must be declared. This slightly increases verbosity, but makes scope rules way more reasonable, and will prevent you from ever debugging why the value of the collcetion
variable is nil
(note the typo).
let x # Introduces the variable in the local scope
let pi = 3.14 # Syntax sugar for assignment
let string = "hello!"
let byte = 'a', # this is a single byte, not a string. Note the single quotes.
let array = [ 1, 2, 3, 4 ]
let dictionary = @[
key: pi,
items: array,
nested: @[ one: 1, two: 2 ],
]
Identifiers must start with a letter or underscore, and may contain further letters, digits and underscores.
Operators
Hush provides standard arithmetic, logical, relational and indexing operators.
- Arithmetic
+
,-
,*
,/
: int or float;%
: int only. These operators do not promote ints to float, and will panic with mismatching types. You may explicitly convert your ints to float prior to applying the operators. - Logical
and
,or
,not
: bool only. Logical operators are short-circuiting. - Relational
==
,!=
: all types;<
,<=
,>
,>=
: int, float, byte, string only. - String concatenation
++
: string only. - Indexing
[]
,.
: array, dict, string, error only. Attempts to access an index out of bounds will result in a panic. Additionally, dicts and errors may be indexed with the dot.
operator, as long as the key is a valid identifier.
let array = [
1 + 2,
3.0 * std.float(4), # explicit conversion to float.
21 % std.int(5.2), # explicit conversion to int.
false and not true, # will short circuit.
1 == 1.0, # false, int and float are always distinct.
"abc" > "def",
"hello" ++ " world",
]
std.assert(array[0] == 3)
# this would cause a panic:
# let x = array[20]
let dictionary = @[
age: 20,
height: 150,
greet: function()
std.print("hello!")
end,
]
std.assert(dictionary["age"] == 20)
std.assert(dictionary.height == 150)
dictionary.greet() # prints "hello!"
As you may be wondering, the standard library, which can be used through the std
global variable, is nothing but a dictionary full of useful functions.
Control Flow and Functions
If expressions
In Hush, conditionals expect a condition of type bool. Attempt to use values of other types, such as nil, will result in a panic.
If expressions may assume three forms, with and without the else
or elseif
fragment:
let condition = true
if condition then
# ...
end
if condition then
# ...
else
# ...
end
if condition then
# ...
elseif condition then
# ...
end
As they are expressions, they will evaluate to whatever is the value resulting in the last statement of the executed block. If the else
/elseif
block is omitted and the condition evaluates to false
, the expression will result in nil
.
let condition = false
let x = if condition then
1
else
2
end
std.assert(x == 2)
x = if condition then
3
end
std.assert(x == nil)
Functions
Functions are first class citizens, which means they are values like any other. The following are equivalent:
let fun = function ()
# ...
end
function fun()
# ...
end
They must declare how many arguments they expect, which is enforced when calling a function. Calling a function with less or more arguments than expected will result in a panic.
function takes_one(x)
# ...
end
function takes_two(x, y)
# ...
end
takes_one(1)
takes_two("a", 2)
Contrary to Lua, functions in Hush always return a single value, which is the result of the last statement in their body. They also may return early with the return
keyword. The following are equivalent:
function fun(x)
if x >= 2 then
return # implicitly returns `nil`
else
return "lower than 2"
end
end
function fun(x)
if x < 2 then
"lower than 2"
end
end
Hush implements lexical scoping, which means variables are enclosed in the body in which they are declared, just like in Python and Lua. It also supports closures, which are functions that capture variables from the enclosing scope:
function adder(x)
let y = x + 1
return function (z) # `return` may be ommited here
y + z # captures `y` from the parent scope.
end
end
std.assert(adder(1)(2) == 4)
Closures may even mutate the captured variables:
let x = 0
function increment()
x = x + 1
end
increment()
increment()
increment()
std.assert(x == 3)
Functions can also be recursive. As they are values, recursive functions are actually closures on themselves (they capture the variable to which they are assigned).
function factorial(n)
if n == 0 then
1
else
n * factorial(n - 1)
end
end
std.assert(factorial(5) == 120)
Self
Hush provides one further facility for functions: the self
keyword. When calling a function inside a dictionary using the dot operator, self
will be an alias to that dictionary. If the function is called through other means, self
will be nil
. This is frequently used in object oriented code.
let dict = @[
value: 5,
method: function()
# `self` is a reference to the dictionary which contains the function, if any.
std.print(self)
end
]
dict.method() # @[ "value": 5, "method": function<...> ]
# Isolate the method from the object, which will cause `self` to be `nil`:
let method = dict.method
method() # nil
# But we can bind it back to the object using `std.bind(obj, method)`:
method = std.bind(dict, dict.method)
method() # @[ "value": 5, "method": function<...> ]
While loops
While loops are statements, and therefore cannot be used as expressions.
let condition = true
let i = 0
while condition do
condition = false
i = i + 1
end
std.assert(i == 1)
For loops
For loops are also statements, but opposed to While loops, they do not expect a boolean condition. First, they expect a variable name, which will be scoped to the loop's body. Second, they expect an iterator function.
An iterator function is a function that may be called repeatedly without arguments, and always returns a dictionary with at least one field:
finished
: a boolean indicating whether the loop should stop.value
: the value to be assigned to the loop variable. May be omitted iffinished
istrue
.
# A function to generate an iterator to the given array.
function iter(array)
let i = -1
let len = std.len(array)
function ()
i = i + 1 # captures `i`, incrementing it on every call.
if i == len then # check if we reached the captured `len`.
@[ finished: true ]
else
@[ finished: false, value: array[i] ]
end
end
end
let array = [1, 2, 3]
let sum = 0
for item in iter(array) do
sum = sum + item
end
std.assert(sum == 6)
Fortunately, the iter
function defined above is present in the standard library, as std.iter(collection)
. For numeric iterations, the standard library also supplies the std.range(from, to, step)
function, which returns an iterator:
let sum = 0
for i in std.range(1, 4, 1) do
sum = sum + i
end
std.assert(sum == 6)
Break statement
One may also interrupt loops using the break
statement:
while true do # this will not run forever.
if 1 + 2 < 4 then
break
end
end
Wrapping up
With these constructs, you should be able to write basic programs in Hush. Next, we'll learn how to implement proper error handling, as robustness is one of the core values of the language.
Error Handling
Hush implements two error mechanisms: panics and errors.
Panic
A Panic is an irrecoverable1 error, and will occur when there is a logic issue in your code. Panics will cause the whole program to crash, and execution will be terminated.
let x = 1 / 0
std.print(x)
Running the above script, you'll get:
Panic in <stdin> (line 2, column 10): division by zero
Examples of errors that cause a panic:
- Syntax error.
- Integer division by zero.
- Index out of bounds.
- Attempt to call a value that is not a function.
- Missing or exceeding arguments in function call.
Error
Recoverable errors may be expressed through values of the error type. This is a special type in the language, and it is the only which cannot be expressed through a literal. Values of the error type can only be created using the std.error
function, which expects a string description and a arbitrary context:
function check_limit(x)
if x > 10 then
std.error("x cannot be bigger than 10", x)
else
x
end
end
One can then check whether the function has returned an error:
let result = check_limit(11)
if std.type(result) == "error" then
std.print("Error: ", result)
else
std.print("Success: ", result)
end
The description and context fields can be accessed as in a dictionary, but unlike in dictionaries, those cannot be assigned to:
let error = std.error("oh no!", 42)
std.print(error.description) # oh no!
error.description = "you shouldn't do that!" # panic
Examples of errors should be recoverable:
- File not found.
- Insufficient permission
- Invalid format
- Command not found
- Command returned non-zero exit status
Try operator
The try (?
2) operator may be used to early return from a function if an error occurs. It is nothing but syntax sugar for an if expression, and therefore it may be used in any expression:
function safe_div_mod(x, y)
if y == 0 then
std.error("division by zero", nil)
else
@[ div: x / y, mod: x % y ]
end
end
# The following are equivalent:
function foo()
let result = safe_div_mod(5, 0)
let value = if std.type(result) == "error" then
return result
else
result
end
std.print(value)
end
function bar()
let value = safe_div_mod(5, 0)?
std.print(value) # this won't be executed, as `?` will trigger an early return.
end
# The `?` operator may be used in any expression:
function run()
std.print("div: ", safe_div_mod(5, 0)?.div)
end
1 One can actually catch a panic using the std.catch
function, but that should be used sparingly.
2 If you're familiar with both languages, Hush's try operator might feel like the unholy child of Rust's ?
operator and Go's if err != nil { return err }
.
Command Blocks
Command blocks is the feature that distinguishes Hush from ordinary programming languages. They allow Hush scripts to seamlessly invoke and interconnect external programs.
Great effort has been put to make command syntax in Hush as similar as we're used to in Bash, but some key aspects have been changed in order to favor robustness of scripts. You should never have to use something like the unofficial bash strict mode in Hush:
#!/bin/bash
set -euo pipefail
IFS=$'\n\t'
The unofficial bash strict mode
Let's see why you won't need any of this in Hush:
set -e
: interrupt execution immediately if a command has non-zero exit status. This is the default behavior for command blocks in Hush.set -u
: exit with an error on any attempt to use an undeclared variable. Hush won't even start to execute your script if you mention an undeclared variable.set -o pipefail
: if a command in a pipeline fails, make the whole pipeline fail. This is the default behavior in Hush, and can even be controlled on a per-command basis in pipelines.IFS=$'\n\t'
: do word-splitting using only newlines and hard tabs. Hush does no word splitting whatsoever, so this will never be a source of confusion or bugs.
In the next sections, we'll learn how to use command blocks in Hush.
Command Blocks
In Hush, command blocks are delimited with curly braces:
{ echo Hello world! }
They are valid expressions, and will result in error or nil. Therefore, we can use this result to check whether the block was successfully executed:
let result = {
touch /etc/config # we may not have permission for that directory!
}
if std.type(result) == "error" then
std.print("Error: ", result)
end
They may even be combined with the try operator:
function run()
{ mkdir /etc/config/ }?
std.print("Success!") # This won't be executed if the command block fails.
end
let result = run()
if std.type(result) == "error" then
std.print("Error: ", result)
end
As they are blocks, they may have multiple commands, which must be delimited by semicolons. This enables us to include line comments in between their arguments:
{
find . # search in the current directory.
-type f # only files.
-iname '*.png'; # case insensitive match on name.
ls ../some-other-dir/images; # list files from another directory.
cat additional-files.txt # The semicolon in the last command is optional.
}
Pipelines and redirections
Pipelines and redirections use standard syntax:
{
echo Hello world! | sed s/world/universe/g | tr '!' '.';
echo overwrite file using stdout > file.txt;
echo overwrite file using stderr 2> file.txt;
echo append to file using stdout >> file.txt;
echo stderr too 2>> file.txt;
cat < file.txt; # redirect file to stdin
cat << "here's an inline string"; # string to stdin
rm file.txt 2>1; # redirect stderr to stdout. Opposed to Bash, we don't need an `&`
# We may compose as many of those as we need:
cat file.txt # Read a file.
<< "Hello world!" # Concat it with data from stdin.
2>> errors.txt # Save errors to file.
| tee output.txt # Dump data to another file.
2>> errors.txt # Save errors to file.
| curl localhost:8080 -d @- # HTTP POST data to server.
2>> errors.txt; # Save errors to file.
}
But there's an additional requirement for redirections: they may not precede arguments:
{ echo Hello 2>1 } # Ok.
{ echo 2>1 Hello } # Syntax error.
Variables
As in most shells, Hush provides variable substitution in commands. But opposed to traditional shells, variables don't undergo word splitting. As Hush has first class support for arrays, there's really no need to do automatic word splitting.
Variables can be used inside command blocks using dollar syntax:
let var = "hello world"
{
echo $var; # hello world
echo "$var"; # hello world
echo ${var}s; # hello worlds
echo "${var}s"; # hello worlds
echo '$var'; # $var
}
Hush uses the following rules when doing variable substitution:
- nil: converted to an empty argument. Note that this is different than skipping the argument.
- bool, char, int, float, string: converted to string and passed as a single argument, regardless of containing spaces, asterisks, and whatnot.
- array: each element will be converted to a single argument, using the other rules. If the array is empty, no argument is produced. This way, arrays can be used to programmatically build lists of command arguments.
- dict, function, error: won't be converted, causing a panic instead.
Considering the file args.hsh
:
let args = std.args() # Returns an array of command line arguments.
for arg in std.iter(args) do
std.print(arg)
end
The following script
let args = [ "1 2", 3, nil, 4.0 ]
{ hush args.hsh $args }
will output:
1 2
3
4.0
Expansions
In order to provide ergonomic manipulation of the file system, most shells provide a mechanism named expansions. It allows the programmer to refer to multiple file names using a regex-like syntax.
Hush provides automatic expansion only for literal arguments. That means you won't have to worry if your variables contains characters that may be expanded.
let var = "*"
{
echo *; # Will print all files/directories in the current directory.
# The following will print just an asterisk.
echo "*";
echo $var;
}
Hush currently provides the following expansions:
%
: matches zero or one character, except for the path separator.*
: matches zero or more characters, except for the path separator.**
: matches zero or more directories.~/
: matches the$HOME
directory, only when in the prefix of an argument.
Opposed to traditional shells, Hush will always expand relative paths prefixed with ./
:
{
touch test.txt; # Create a file
echo *; # Will print "./test.txt"
}
You won't have to worry about flag injection from file names ever again.
Errors
By default, whenever a command fails in a block, the whole block will be interrupted. This behavior can be disabled on a per-command basis with the ?
operator (not to be confused with the try operator outside of command blocks).
{
echo Hello world!;
# `false` is a command that always fails. As it's suffixed with `?`,
# it won't cause the whole block to abort.
false ?;
echo "This will be printed";
# If a command fails, and it makes no use of the `?` operator,
# no further commands will be executed.
false;
echo "This will not be printed";
}
Command blocks will always result in an error whenever one or more of their commands fail. This is true even for commands that use the ?
operator.
let result = { false?; }
std.assert(std.type(result) == "error")
An error will be produced for each command that fails. This error will contain a dict with two fields:
pos
: a string describing the source position of the command.status
: the numeric exit status of the program. Always non-zero.
There are scenarios where more than one command may fail, such as when using pipelines or the ?
operator. Whenever more than one command fails, the block will result in a generic error. This generic error will contain as context an array of the errors of each command that failed.
let result = { false?; false }
std.print(result.context[0])
# command returned non-zero (@[ "status": 1, "pos": "<stdin> (line 1, column 15)" ])
std.print(result.context[1])
# command returned non-zero (@[ "status": 1, "pos": "<stdin> (line 1, column 23)" ])
Environment variables
Commands may be prefixed by environment assignments, which will apply only for the given command.
{
VAR=value COLOR=false command; # Set environment variables only for this command.
another-command;
}
If you wish to set environment variables permanently, you may use the std.export
function:
std.export("VAR", "value")
std.export("COLOR", "false")
{
# The environment setting will now affect both commands.
command;
another-command;
}
Capture
One of the most important features of a shell is to be able to manipulate the standard I/O streams of programs. There are three main ways of doing so: pipes, redirection, and capturing. The first two are commonly used when we just want to forward the output or input of a program. But if we want to preprocess the output, or use it as an argument to another program, we'll usually reach for capturing the output stream as a string in the shell, so that we can handle it ourselves.
In Hush, we can capture the output of a whole command block by prefixing it with a dollar:
let result = ${ echo Hello world! }
Instead of resulting in nil or error, as does the standard command block, a capture block will result in either a dict containing the stdout and stderr string fields, or an error if the block fails.
function get_first_word()
let output = ${ echo Hello world! }?.stdout
let first_word = std.split(std.trim(output), " ")[0]
if std.is_empty(first_word) then
std.error("output was empty", output)
else
first_word
end
end
std.print(get_first_word()) # Hello
The separation of the stdout and stderr fields in the resulting dict enables the programmer to properly handle the standard error stream separately from the standard output stream, something that is more complicated than it should in traditional shells.
If the command block fails, the resulting error will contain the output that was captured before the block failed:
let result = ${
echo Hello world!;
echo The next command wil fail 1>2;
this-command-does-not-exists;
}
std.assert(std.type(result) == "error")
std.assert(result.context.stdout == "Hello world!\n")
std.assert(result.context.stderr == "The next command wil fail\n")
Async
Hush also provides a way to launch commands asynchronously, and then wait for their result at a later time. Traditional shells provide similar functionality through the ampersand operator.
To run a command block asynchronously, prefix it with an ampersand:
let handle = &{ echo Hello world! }
# Do some other work before calling join.
# This may be printed before or after "Hello world!".
std.print("Doing some work...")
# This will wait until the block runs to completion, and will return it's result.
let result = handle.join()
std.assert(result == nil)
Async blocks will start executing immediately, but Hush won't wait for their completion until join is called, and will continue script execution instead. An async block will always result in a dict containing the join method, which will then return the block result (nil or error) when called.
Builtins
Some commands must be implemented as shell builtins, so that they can mutate the shell state, a thing that would not be possible for an external program. A good example of such kind is the cd
command, which must change the shell's working directory.
Hush currently provides the following builtin commands:
cd TARGET_DIR
: change the working directory to TARGET_DIR.exec PROGRAM [ARG1, ARG2, ARG3...]
: replace hush process with PROGRAM (argument 0 = PROGRAM).exec0 PROGRAM ARG0 [ARG1, ARG2, ARG3...]
: replace hush process with PROGRAM (argument 0 = ARG0).spawn0 PROGRAM ARG0 [ARG1, ARG2, ARG3...]
: spawn PROGRAM (argument 0 = ARG0).
As Hush has no such thing as subshells, builtin commands may not be used in pipes, redirections, or capture blocks.
Standard Library
Hush's standard library is available through the std
global variable, which is implicitly available to all scripts. It is nothing but a dictionary of predefined functions, which can be used like any other user defined functions.
Currently, the following functions are available:
std.args()
Gets an array of command line arguments.
std.assert(value)
Asserts that value
is true
, panics otherwise.
std.base64.encode(string)
Convert the given value to a base64 string.
std.base64.decode(string)
Parse the given base64 string
. Returns an error if parsing fails.
std.bind(obj, function)
Returns a new function that binds function
to obj
, so that self
will always refer to obj
when the function is called.
std.bytes(string)
Convert the given string to an array of chars.
std.catch(function)
Runs the given function
, and returns an error if it panics. This should be used sparingly, as panics shouldn't be used for recoverable errors.
std.cd(dir)
Change the current directory to dir
.
std.contains(collection, value)
Checks if collection
contains an item that is equal to value
. May be used with strings, arrays and dicts.
std.cwd()
Returns the current working directory.
std.env(key)
Gets the value of the environment variable key
, or nil if it's not defined.
std.error(description, context)
Create a new error with the given description and context. The description must be a string.
std.exit(int)
Terminates execution immediately with the given status code. Panics if the status code is not in the range [0, 255].
std.export(key, value)
Set the environment variable key
to value
. Both arguments must be strings.
std.float(value)
Convert value
to float. Accepts string, int and float.
std.glob(path)
Expands the given path
in the current directory, using the shell expansion rules (*
, %
, etc).
std.has_error(value)
Recursively checks if value
contains a value of type error.
std.hex.encode(string)
Convert the given value to a hex string.
std.hex.decode(string)
Parse the given hex string
. Returns an error if parsing fails.
std.import(path)
Load the Hush script from the given path, relative to the current file.
std.int(value)
Convert value
to int. Accepts string, int and float.
std.is_empty(collection)
Checks if the given collection
is empty. Accepts strings, arrays and dicts.
std.iter(collection)
Returns an iterator function for the given collection
. Accepts strings, arrays and dicts.
std.json.encode(value)
Convert the given value to a JSON string. Panics if value
contains a value that cannot be serialized as JSON (function or error).
std.json.decode(string)
Parse the given json string
. Returns an error if parsing fails.
std.len(collection)
Returns the amount of elements in the given collection. Accepts strings, arrays and dicts.
std.panic(value)
Panics with the given value
as description.
std.pop(array)
Removes the last element from the given array
, returning it. Panics if the array is empty.
std.print(value)
Prints the given value to stdout, including a newline.
std.push(array, value)
Adds the given value
to the end of array
.
std.range(from, to, step)
Returns an iterator function that yields numbers in the given range.
std.read(prompt)
Read a line from stdin, using the given prompt
, which must be a string.
std.regex(pattern)
Build a regex object from pattern
, which must be a string. If pattern
is not a valid regex, an error will be returned. Otherwise, returns a dict with the following methods:
match(string)
: returns a bool indicating whether the pattern matches the givenstring
.split(string)
: splitsstring
using the pattern, returning an array of strings.replace(string, replace)
: returns a string with replaced occurrences of pattern instring
.replace
must be a string.
std.replace(string, seach, replace)
Replace occurrences of search
with replace
in string
. All parameters must be strings.
std.sleep(ms)
Sleep for the given amount of milliseconds. Accepts positive integers only.
std.sort(array)
Sorts the given array.
std.split(string, pattern)
Splits the given string
by occurrences of pattern
, returning a non-empty array. Both arguments must be strings.
std.substr(string, from, length)
Slice the given string
. The two last parameters must be positive integers.
std.to_string(value)
Converts the given value to string.
std.trim(string)
Removes whitespace from the start and end of the given string.
std.typecheck(value, type)
Checks if the given value
has type type
, panics otherwise. type
must be a string.
std.try_typecheck(value, type)
Checks if the given value
has type type
, returns an error otherwise. type
must be a string.
std.type(value)
Returns a string describing the type of value
: "nil"
, "bool"
, "char"
, "int"
, "float"
, "string"
, "array"
, "dict"
, "function"
, or "error"
.
Paradigms
There are three main programming paradigms that can be used in Hush: procedural, functional, and object oriented programming. As the language has functions, statements and basic flow control, procedural programming comes pretty naturally. For functional programming, the support for first class and higher order functions are the core features that enable usage of such paradigm. As for object oriented programming, the language provides basic mechanisms for defining objects with fields and methods.
In the following sections, we'll explore examples on how to use functional and object oriented programming in Hush.
Object Oriented
Hush provides basic support for object oriented programming. This means you can write object oriented code in Hush, but the language won't give you fancy features for free. Objects can be modeled through dictionaries, using keys as accessors and values as member data and methods. To ease the use of dictionaries as objects, Hush provides the self
keyword, which is described in the functions section.
# This function acts like a combination of a class and a constructor. It'll take any
# arguments relevant to the construction of a `MyCounter` object, and will return an
# instance of such object, which is nothing but a dictionary.
let MyCounter = function(start)
@[
# A member field. Using the same convention as Python, a field starting with an
# underscore should be considered private.
_start: start,
# A method. Here, we can use the `self` keyword to access the object.
get: function()
self._start
end,
# Another method.
increment: function()
self._start = self._start + 1
end,
]
end
let counter = MyCounter(1) # object instantiation.
std.print(counter.get()) # 1
counter.increment()
std.print(counter.get()) # 2
Inheritance
Single inheritance can be implemented using a similar technique:
# A derived class.
let MySuperCounter = function (start, step)
# Instantiate an object of the base class. We'll then augment this object with
# derived functionality.
let counter = MyCounter(start)
counter._step = step # Add new fields to the object.
# Override a method. Make sure not to change the number of parameters here!
counter.increment = function ()
self._start = self._start + self._step
end
# In order to override a method and call the parent implementation, you'll need to
# bind it to the current object, and then store it to a variable:
let super_get = std.bind(counter, counter.get)
counter.get = function()
let value = super_get() # call the parent method.
std.print(value)
value
end
counter
end
let super_counter = MySuperCounter(2, 3)
super_counter.get() # 2
super_counter.increment()
super_counter.get() # 5
Functional
Hush provides first class higher order functions, which is the fundamental building block for functional programming. As such, the paradigm integrates very well with the language, and it is recommend as the main approach for solving problems in Hush.
Here's an example of a simple iterator library, using functional programming and implementing basic laziness:
# Iterator: traversal utilities.
# Construct an iterator.
# An Iterator is an object capable of successively yielding values.
# Iterators are *mutable* (they may mutate their inner state as they iterate).
# Parameters:
# * next: function() -> @[ finished: bool, value: any ] # the iterator function
# Returns the Iterator instance.
function Iterator(next)
@[
# Provide the next method.
next: next,
# Get the iterator function.
# Iterating mutates the iterator, which will successively yield the next value.
# This should be used in for loops, as in:
# for item in iterator.iter() do
# ...
# end
iter: function ()
self.next
end,
# Get the nth element in the iterator.
# This is equivalent to calling self.next() `n + 1` times,
# but might be optimized for some iterators.
# Parameters:
# * n: the desired element index
# Returns the nth element.
nth: function (n)
for _ in std.range(0, n, 1) do
let iteration = self.next()
if iteration.finished then
return iteration
end
end
self.next()
end,
# Map a function over an iterator.
# This function is lazy.
# Parameters:
# * fun: function(any) -> any # the function to apply
# Returns a new iterator, consuming self.
map: function (fun)
let map = function (iteration)
if iteration.finished then
iteration
else
@[ finished: false, value: fun(iteration.value) ]
end
end
let base = self
let it = Iterator(
function ()
map(base.next())
end
)
# Elide mapping unnecessary elements when jumping.
# This is an important optimization for, e.g.:
# Array([1,2,3,4,5])
# .map(fun)
# .skip(3)
# .collect()
it.nth = function (n)
map(base.nth(n))
end
it
end,
# Filter the iterator by the given predicate.
# This function is lazy.
# Items for which the predicate returns true are kept.
# Parameters:
# * pred: function(any) -> bool # the predicate
# Returns a new iterator, consuming self.
filter: function (pred)
let base = self
Iterator(
function ()
let iteration = base.next()
while not (iteration.finished or pred(iteration.value)) do
iteration = base.next()
end
iteration
end
)
end,
# Skip elements from the iterator.
# Equivalent to calling `nth(size - 1)` on an iterator.
# Returns a new iterator, consuming self.
skip: function(size)
if size > 0 then
self.nth(size - 1)
end
self
end,
# Iterate up to some elements.
# This function is lazy.
# The iterator that will stop after `size` elements.
# Parameters:
# * size: int # how many elements to keep
# Returns a new iterator, consuming self.
take: function (size)
let base = self
let it = Iterator(
function ()
if size == 0 then
@[ finished: true ]
else
size = size - 1
base.next()
end
end
)
it.nth = function(n)
if n + 1 > size then
size = 0
@[ finished: true ]
else
size = size - n
base.nth(n)
end
end
it
end,
# Fold elements into a single value.
# Parameters:
# * merge: function(any, any) -> any # the function to merge elements
# * acc: any # the default argument for merge
# Returns the resulting any, consuming self.
fold: function (merge, acc)
for item in self.iter() do
acc = merge(item, acc)
end
acc
end,
# Collect the iterator's items.
# Parameters:
# * target: nil, array or function(any) # where to collect
# If target is nil, the elements are collected into a new array.
# If target is an array, the elements are collected by pushing to it.
# If target is a function, the elements are collected by calling the function.
# Returns nil if target is a function, or the resulting array otherwise.
collect: function (target)
let result = []
let push
let type = std.type(target)
if type == "function" then
result = nil
push = target
else
if type == "array" then
result = target
end
push = function (item)
std.push(result, item)
end
end
for item in self.iter() do
push(item)
end
result
end
]
end
# Construct an iterator for the given array.
# Returns the Iterator instance.
function Array(array)
let ix = 0
# Don't use `std.iter` in order to be able to optimize `nth`.
let it = Iterator(
function ()
if ix >= std.len(array) then
@[ finished: true ]
else
let value = array[ix]
ix = ix + 1
@[ finished: false, value: value ]
end
end
)
# Override nth with a O(1) implementation:
it.nth = function (n)
ix = ix + n
return it.next()
end
return it
end
# Construct an iterator for the given table.
# There is no guarantee of iteration order.
# Returns the Iterator instance.
function Dict(dict)
return Iterator(std.iter(dict))
end
# Construct an empty iterator.
# Returns the Iterator instance.
function Empty()
return Iterator(
function ()
return @[ finished: true ]
end
)
end
Wrapping up
This was an all around quick tutorial on Hush, a modern shell programming language. We hope that it has enabled you to write and maintain robust shell scripts, and that the language proves itself to be a solid tool for system and infrastructure programming. If you have any criticism, suggestions, or want to contribute to the project, feel free to reach us out on Github.