syshell

sy·shell is a shell like bash, fish or nushell, but better. The language is simple and has a great support for lists, maps, formats like JSON and smaller things like dates and versions. It is safe and fast.

syshell stands for the system shell and it is currenty under active development.

Basics

Here is how you run a command:

>> git commit -v

or

>> curl https://syshell.org/

To create a new variables use := like that:

>> $msg := "Hello, World!"
>> print $msg
Hello, World!

Unlike POSIX shells, there's no expansion phase. The following will create a single folder:

>> $name := "May 2024"
>> mkdir $name

Explicit expands, however, are possible, but you need to use lists for them:

>> $months := ["May 2024", "June 2024", "July 2024"]
>> mkdir $months...

Here is how to assign program's stdout to a variable:

>> $hash := git rev-parse --short HEAD

This expects git to output a single line. You can define several variables at once to capture multiple lines or use ... to capture all the other lines:

>> $shebang, $content... := cat helloWorld.sy

Here $shebang will be a string, while $content will be a list of strings:

>> print $shebang
#!/bin/sysh
>> print $content # converts to string and prints
["", "print Hello, World!"]

Generally, if line starts with a word, then it's a command, otherwise if it starts with a number or a special symbol like $ or ( it's an assignment or an expression:

>> $name = "git" # assignment statement
>> $name         # expression, only valid in the interactive mode
git

To run a command specified in a variable, use run syntax, the following will run git:

>> run $name

The same approach works inside assignments:

>> $name = "git" # assign string "git" into $name
>> $name =  git  # run command "git" and assign its output to $name
>> $name = run "git" # same as the previous
>> $name = run  git  # same as the previous

If assignment right hand side starts with a number or a variable, then it's an expression, not a command:

>> $r := 10
>> $area := $r ** 2 * 3.14 # binary operations work as expected

The builtin print command converts all its arguments to strings and prints them space separated:

>> print $area
314
>> print $r ** 2 * 3.14
10 ** 2 * 3.14

In the last example, syshell treats $r ** 2 * 3.14 as command arguments; you can use parens to compute it as an expression:

>> print ($r ** 2 * 3.14)
314

Parens can be used to run commands:

>> mkdir (date --rfc-3339=date)
>> ls
2026-01-11

When running commands, parens expect a single output line, otherwise they cause an error.

Values

Here is how you use maps:

>> $countries := {
       India: 1_417_492_000,
       China: 1_409_280_000,
       "United States": 340_110_988,
   }
>> print $countries["India"]
1417492000
>> print $countries.India
1417492000
>> print $countries
{India: 1417492000, China: 1409280000, "United States": 340110988}

Unlike languages like Python, maps in syshell remember insertion order, which is useful to deal with configuration files:

>> $pod := {
       apiVersion: "v1",
       kind: "Pod",
       metadata: {
           name: "busybox",
       },
       spec: {
           containers: [{ image: "alpine/busybox" }],
       },
   }
>> put $pod
{
    apiVersion: "v1",
    kind: "Pod",
    metadata: {name: "busybox"},
    spec: {containers: [{image: "alpine/busybox"}]}
}

In syshell there are no references, only values, so, for example, if you set another variable to $pod and then change it, the $pod stays the same:

>> $pod2 := $pod
>> $pod2.metadata.name = "postgres"
>> print $pod.metadata.name
busybox
>> print $pod2.metadata.name
postgres

Same way with lists and nested data structures:

>> $pod2.spec.containers[0].image = "postgresql"
>> print $pod.containers[0] $pod2.containers[0]
{image: "alpine/busybox"} {image: "postgresql"}

Formatters

Here is how to convert value to a proper JSON:

>> print $pod@json
{"apiVersion": "v1", "kind": "Pod", "metadata": {"name": "busybox"}, "spec": {"containers": [{"image": "alpine/busybox"}]}}

Use : to convert back from JSON:

>> $parsed := "[1, 2, 3]":json
>> print $parsed[2]
3

This $expr:json and $expr@json syntaxes are called formatters and they can be used similar to index ($expr[$idx]) and field ($expr.field) accessors to access nested data:

>> $data := «{"blob": «{"key": "value"}»}»
>> print $data:json.blob:json.key
value

Just like index and field accessors, formatters can be used on the left side of the assignments:

>> $data:json.id = 10
>> print $data
{"blob": "{\"key\": \"value\"}", "id": 10}

There're other builtin formatters. Generally speaking, : converts a string from a given format to a syshell value, and @ converts a syshell value to a formatted string. For example, colons splits a string by the : character creating a list:

>> $path := "/bin:/usr/bin:/sbin:/usr/sbin"
>> print $path:colons
["/bin", "/usr/bin", "/sbin", "/usr/sbin"]
>> print [1, 2, 3]@colons
1:2:3

There are a few of builtin formatters in syshell:

Pipes and for loops

Here is a pipeline:

>> cat cities.csv | grep India

This works as you expect. cities.csv is already sorted by population size:

>> head -4 cities.csv
"city",,"lat","lng","country","iso2","population"
"Tokyo","35.6897","139.6922","Japan","JP","37732000"
"Jakarta","-6.1750","106.8275","Indonesia","ID","33756000"
"Delhi","28.6100","77.2300","India","IN","32226000"

The first city Indian city in cities.csv will be the biggest one:

>> $biggest := cat cities.csv | grep India | head -1
>> print $biggest
"Delhi","28.6100","77.2300","India","IN","32226000"

Pipes for external programs are treated like lines separated by a \n symbol, or, in other words, a stream of strings:

>> $top10... := cat cities.csv | grep India | head -10
>> print $top10[0]
"Delhi","28.6100","77.2300","India","IN","32226000"
>> print $top10[1]
"Mumbai","19.0761","72.8775","India","IN","24973000"

Here, head will output 10 lines, which all be gathered into a list of strings and assigned to $top10. To iterate on this list you can use for-in:

>> for $city in $top10 { print $city:csv[0] }
Delhi
Mumbai
Kolkāta
Bangalore
Chennai
Hyderābād
Pune
Ahmedabad
Sūrat
Lucknow

If you omit the in part, for will start consuming standard input:

>> grep India cities.csv | head -3 | for $city { print $city:csv[0] }
Delhi
Mumbai
Kolkāta

Pipes in syshell are streams of values, meaning you can pass any values without losing their type. Use put instead of print to preserve types:

>> $top10... = head -10 cities.csv | for $city { put $city:csv }
>> print $top10[0][0] $top10[1][0]
city Tokio

In CSV, a single record can span multiple lines (a quoted cell can contain a newline without escaping), and the for above will handle it incorrectly. A more correct and faster way is to use the from csv builtin. In addition, it parses CSV headers, so you can use column names instead of indexes:

>> $top10... = head -10 cities.csv | from csv
>> print $top10[0].city $top10[0].country
Tokio Japan

for behaves like a normal command in the pipeline, i.e. it can consume stdin, produce stdout and stderr, and it runs in parallel with other pipeline commands (in a separate thread). From within for, variables can be accessed and changed:

>> $countries := {}
>> cat cities.csv | from csv | for $city {
       $countries[$city.country] += $city.population:int
   }
>> print $countries.India
481874233

Functions

Here is how you define a function:

>> fn hello { print "Hello, World!" }
>> hello
Hello, World!

Functions can have local variables:

>> fn hello {
       $msg := "Hello, World!"
       print $msg
   }
>> hello
Hello, World!
>> print $msg
sysh: $msg is used, but not declared, use ':=' to declare a new variable

Function can have positional arguments:

>> fn area $radius {
       put ($radius ** 2 * 3.14)
   }
>> area 10
314
>> area
area: got 0 arguments, but there should be exactly 1

It's possible to restrict incoming parameters by specifying a formatter:

>> fn area (float)$radius {
       put ($radius ** 2 * 3.14)
   }

(float)$radius will ensure that $radius is set to a number; it is equivalent to the following:

>> fn area $arg1 {
       $radius := $arg1:float
       put ($radius ** 2 * 3.14)
   }

In both cases, the formatter will guard bad input:

>> area one
area: bad $radius argument: can't parse a float: bad symbol "o" (0x6F)
>> area "10"
314
>> area 10 # 10 is already a number, so it's passed as is
314

Function name can consist of several words, this will automatically create git- or kubectl-style subcommands:

>> fn volume sphere (float)$r {
       put (4/3 * 3.14 * $r**3)
   }
>> fn volume cube (float)$a {
       put ($a ** 3)
   }
>> fn volume parallelepiped (float)$a (float)$b (float)$c {
       put ($a * $b * $c)
   }
>> volume cube 10
1000

Standalone Scripts

If a file has a main function, it will be called, if script is executed. Here is small-script:

#!/bin/sysh

fn main status { print "All good!" }
fn main launch { print "Launching..." }

Then, if you run it:

>> ./small-script status
All good!

Imports

If you have a file volume.sy in the current directory like this:

fn sphere (float)$r {
    put (4/3 * 3.14 * $r**3)
}
fn cube (float)$a {
    put ($a ** 3)
}
fn parallelepiped (float)$a (float)$b (float)$c {
    put ($a * $b * $c)
}

you can import it:

>> import volume

The name of an import becomes a function prefix, hence cube from volumes.py can be called as:

>> volume cube 10
1000

Each file might have global variables, however they are not accessible outside of this file. Each imported file can in turn import other files, however circular dependencies are not allowed.