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.
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.
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"}
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:
bool, int, float,
time, duration.colons,
commas, dashes, dots,
lines, semicolons, slashes.json and csv.for loopsHere 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
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
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!
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.