system shell

syshell 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.

sy·shell stands for the system shell and it is currently in a closed alpha. Fill out the form, if you want to try it out.

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 := "March 2026"
>> 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. 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; 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-03-01

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. There are many of builtin formatters in syshell:

Similar to index ($expr[$idx]) and field ($expr.field) accessors, formatters can be used to access nested data:

>> $data := "{\"blob\": \"id,user,homedir,3\"}"
>> print $data:json.blob:csv[1]
user

Formatters can be used on the left side of the assignments:

>> $data:json.id = 10
>> print $data
{"blob": "id,user,homedir,3", "id": 10}

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

Pipes

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

Instead of converting individual lines from csv, a more correct and faster way is to use the from csv builtin. It knows about CSV header and produces a map/dictionary for each row:

>> head -10 cities.csv | from csv
{city: "Tokyo", lat: "35.6897", lng: "139.6922", country: "Japan", iso2: "JP", population: "37732000"}
{city: "Jakarta", lat: "-6.1750", lng: "106.8275", country: "Indonesia", iso2: "ID", population: "33756000"}
{city: "Delhi", lat: "28.6100", lng: "77.2300", country: "India", iso2: "IN", population: "32226000"}
{city: "Guangzhou", lat: "23.1300", lng: "113.2600", country: "China", iso2: "CN", population: "26940000"}
{city: "Mumbai", lat: "19.0761", lng: "72.8775", country: "India", iso2: "IN", population: "24973000"}
{city: "Manila", lat: "14.5958", lng: "120.9772", country: "Philippines", iso2: "PH", population: "24922000"}
{city: "Shanghai", lat: "31.2286", lng: "121.4747", country: "China", iso2: "CN", population: "24073000"}
{city: "São Paulo", lat: "-23.5500", lng: "-46.6333", country: "Brazil", iso2: "BR", population: "23086000"}
{city: "Seoul", lat: "37.5600", lng: "126.9900", country: "Korea, South", iso2: "KR", population: "23016000"}

Pipes in syshell are streams of values, so it allows to pass them without losing their type:

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

Use to columns to print maps as a table in terminal:

>> head -10 cities.csv | from csv | to columns
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
Guangzhou 23.1300  113.2600 China        CN   26940000
Mumbai    19.0761  72.8775  India        IN   24973000
Manila    14.5958  120.9772 Philippines  PH   24922000
Shanghai  31.2286  121.4747 China        CN   24073000
São Paulo -23.5500 -46.6333 Brazil       BR   23086000
Seoul     37.5600  126.9900 Korea, South KR   23016000

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; this is most equivalent to the following:

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

except it has better error reporting:

>> area one
area: bad $radius argument: can't parse a float: bad symbol "o"
>> 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.