Expand

fn

declare a function

<fn>        = fn <name>+ <opt>* <arg>* { <statements> }
<arg>       = <decl>
            | <decl>'?'
            | <decl>'...'
<opt>       = <opt names> <decl>
<opt names> = <opt name> ['|' <opt names>]
<opt name>  = '--' <name>+
            | '-' <letterOrDigit>
<name>      = <letterOrDigit> <name rest>*
<name rest> = <letterOrDigit> | '-'

Functions in syshell behave like programs: they accept parameters and they have input, output and error streams. Functions preserve data types (instead of converting everything to strings as it is done for external programs). Functions also can change global and captured variables.

Functions can be declared in the scope of the current file or inside another function. Unlike other scripting languages, function calls can be done above function declarations. Functions can not be re-declared.

fn main {
    print "Hello, World!"
}

Arguments

Functions can have positional arguments. When function is called, all positional arguments must be specified, unless the argument is marked as optional using ?. Optional positional arguments must come after non-optional. The last positional argument can be specified as variadic using ....

Arguments are passed following syshell's value semantics, which means that parameters can be changed inside function, but outside of the function there will be no visible changes.

add two numbers together
fn add $a $b {
    put ($a + $b)
}

add 3 6 # gives 9
optional and variadic arguments
fn list $type? $names... {
    if $type == null {
        # list all resources
        return
    }

    if (len $names) == 0 {
        # list all resources of $type
        return
    }

    for $name in $names {
        # show resource $name of $type
    }
}
optional and variadic arguments are assigned in the left-to-right order
fn demo $a $b? $c? $d... {
    print $a $b $c $d
}

demo 1
# => 1 null null null

demo 1 2
# => 1 2 null null

demo 1 2 3
# => 1 2 3 null

demo 1 2 3 4
# => 1 2 3 [4]

demo 1 2 3 4 5
# => 1 2 3 [4, 5]
modifying parameters has no effect on original arguments
fn process $map {
    $map.x = 5
}

$m := { x: 0 }
process $m
put $m.x # still 0

Subcommands

There is a builtin way to create subcommands in style of git or kubectl:

# stage files for commit
fn git add $files... {}

# create a commit
fn git commit {}

# update remote repo
fn git push $repo? $refs... {}

# show help
fn git {}

Functions with subcommands should be defined in a single scope.

Formatters

Formatters help to validate or convert parameters before assigning value to a variable. For example, (int)$n enforces that positional argument $n is an int. This syntax is covered in more details in pattern matching.

enforce all parameters to be integer. $step is an optional and can be null.
fn range (int)$begin (int)$end (int)$step? {
    $step ??= 1
    while $begin < $end {
        put $begin
        $begin += $step
    }
}

Options

There is a builtin way to handle options. Options must be specified before positional arguments using --option $var syntax, where $var is a declaration of a variable, possibly with a specified formatter.

Options are always optional, option variable is set to null, if option is not specified during the call.

multiple option aliases can be specified using |
fn git commit
    -m|--message $msg
{
    if $msg == null {
        # start an editor to get
        # a commit message
    }
    # ...
}
use local timestamp formatter to reliably parse dates
fn git log
    --after|--since  (local)$since
    --until|--before (local)$before
    --skip           (int)$skip
    -n|--max-count   (int)$limit
    $branch?
{
    $skip ??= 0
    branch list $branch
        | for $commit {
            if $since != null &&
               $commit.date < $since {
               continue
            }
            if $before != null &&
               $commit.date < $before {
               continue
            }
            if $skip > 0 {
                $skip -= 1
                continue
            }
            if $limit != null {
                if $limit == 0 {
                    return
                }
                $limit -= 1
            }

            # display $commit info
        }
}
bool options are handled like flags: they don't expect immidiate argument during the call
fn git commit
    -v|--verbose (bool)$verbose
{
    if $verbose {
        log "Preparing to make a commit"
    }
    # ...
}

Input Stream

Every function might have an input stream, if it was started inside a pipeline or has a redirect. However, when a sub-function or an external program are executed, they don't have an input stream enabled by default, because the input stream must be enabled explicitly with >>:

# main had stdin (fd 0) as an input stream
fn main {
    # input stream is not enabled, cat will have /dev/null as fd 0
    cat

    # input stream is enabled, cat will inherit stdin
    >> cat
}

Read streams to learn more about it.

square a numbers twice
fn square {
    >> for $n {
        put ($n ** 2)
    }
}

put 1 2 3 | square | square
# => 1 16 81
execute programs that require user input
fn git commit -m $msg {
    if $msg == null {
        $tmp := mktemp
        defer rm -f $tmp

        >> vim $tmp

        $msg = to text < $tmp
    }
}