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.
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.
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. There are many of builtin formatters in syshell:
bool, int, float,json and csv.colons,
commas, dashes, dots,
lines, semicolons, slashes,
spaces, tabs.time, duration,
days, hours, minutes,
milliseconds (ms), microseconds
(µs), nanoseconds (ns).str ensures that value has a string type without any
convertion.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
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
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
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.