Language Intro
Running commands
Here is how you run a command:
>> git commit -v
>> curl https://syshell.org/
Multi-line commands don't need trailing backslashes — indentation is
enough, and # starts a comment:
find /usr/lib64
-type f # ignore links
-maxdepth 1 # skip subdirectories
-name "lib*"
Unlike POSIX shells, there's no expansion phase. In bash,
mkdir $name can silently expand if $name
contains spaces. In syshell it never does:
>> $name := "March 2026"
>> mkdir $name # creates exactly one directory
If you want to expand a list into multiple arguments, use
...:
>> $months := ["May 2024", "June 2024", "July 2024"]
>> mkdir $months...
Parens let you run command inline, but they expect exactly one output line:
>> mkdir (uname -r)
>> ls
7.1.3-201.fc44.x86_64
Parens also allow to do math or other expressions:
>> print "Second in a day:" (24 * 60 * 60)
Second in a day: 86400
Whether it's a command or expression is decided the way you'd guess:
if it starts with a word, it's a command; if it starts with
$, a digit, [, (, {,
a digit, a quoted string or an unary operator, it's an expression.
If a command name would be ambiguous (a path in a variable, or a
command starts with a digit), use run:
>> run "7z" e archive.7z
>> $python := "/usr/bin/python"
>> run $python --version
Variables and value semantics
:= declares a new variable, = reassigns an
existing one. syshell statically checks this, so typos in variable names
are caught before anything executes.
>> $msg := "Hello, World!"
>> print $msg
Hello, World!
>> print $mgs # Error: undefined variable
:= and = can be used to capture command's
standard output. Destructing is built in: one variable captures one
line; ... gathers the rest as a list:
>> $hash := git rev-parse --short HEAD
>> $shebang, $content... := cat helloWorld.sy
>> print $shebang
#!/bin/sy
>> print $content # converts to string and prints
["", "print Hello, World!"]
Maps preserve insertion order, which matters when round-tripping config files:
>> $pod := {
apiVersion: "v1",
kind: "Pod",
metadata: { name: "busybox" },
}
The part that matters most when coming from other languages like Python: from the language perspective, thare are no references, only values, so mutating one variable never changes the other:
>> $pod2 := $pod
>> $pod2.metadata.name = "postgres"
>> print $pod.metadata.name
busybox
>> print $pod2.metadata.name
postgres
This value semantics holds for nested structures too, so no actidental aliasing bugs. Function arguments follow the same rule: mutating a parameter inside a function is invisible to the caller. Internally, syshell uses reference counting and copy-on-write or persistent data structures.
Pipes carry typed values, not just bytes
This is the biggest structural difference from bash. Piping with
external programs still works line-by-line — cat,
grep, and head don't know anything about
syshell types:
>> $top10... := cat cities.csv | grep India | head -10
But piping between syshell function carries actual typed values — maps, lists, numbers — not text that has to be re-parsed at each stage:
fn generate-assets {
put { a: 1, b: 2 }
put [1, 2, 3]
}
$x, $y := generate-assets
# $x is a map, $y is a list
from is the
bridge between two worlds, for example from csv turns a stream of raw CSV lines from an
external program into a stream of maps:
>> head -10 city-country.csv | from csv
{ city: "Tokyo", country: "Japan" }
{ city: "Jakarta", country: "Indonesia" }
{ city: "Delhi", country: "India" }
{ city: "Guangzhou", country: "China" }
{ city: "Mumbai", country: "India" }
{ city: "Manila", country: "Philippines" }
{ city: "Shanghai", country: "China" }
{ city: "São Paulo", country: "Brazil" }
{ city: "Seoul", country: "Korea, South" }
Builtins sort
and to columns
order cities by country name and display it in columns:
>> head -10 city-country.csv | from csv | sort $.country | to columns
city country
São Paulo Brazil
Guangzhou China
Shanghai China
Delhi India
Mumbai India
Jakarta Indonesia
Tokyo Japan
Seoul Korea, South
Manila Philippines
Formatters: the
:/@ pair
Every supported format has to accessors: :format parses
string into a syshell value, @format serializes a value
back into a string. They compose with field and index access, reading
left to right instead of nesting function calls:
>> $data := "{\"metadata\":{\"name\": \"nginx\"}}"
>> print $data:json.metadata.name
nginx
:json/@json
>> $pod := {
apiVersion: "v1",
kind: "Pod",
metadata: { name: "busybox" },
}
>> print $pod@json
{"apiVersion": "v1", "kind": "Pod", "metadata": {"name": "busybox" }}
>> $parsed := "[1, 2, 3]":json
>> print $parsed[2]
3
Formatters also work on the left side of an assignment, e.g. JSON can be modified in-place:
>> $data := "{\"metadata\":{\"name\": \"nginx\"}}"
>> $data:json.metadata.labels = {}
>> print $data
{"metadata":{"name": "nginx", "labels": {}}}
:colons/@colons
The colons formatter works with colon-separated strings,
which are used for the PATH environment variable or in the
/etc/passwd file.
>> put $env.PATH
/usr/local/bin:/usr/bin:/bin
>> put $env.PATH:colons[0]
/usr/local/bin
Operator [] can be used to append list with a new
element:
>> $env.PATH:colons[] = "/home/alice/bin"
>> put $env.PATH
/usr/local/bin:/usr/bin:/bin:/home/alice/bin
Byound :colons there are other simple delimiter spitting
formatters: :commas, :dashes,
:lines, :spaces. There's also
:columns that splits by multiple space like
awk. There are many other formatters in syshell.
Dates and duration
syshell has standard datetime and duration types. :utc/:local
parses a "YYYY-MM-DD hh:mm:ss" string into a real datetime
value (time is optional) and :days, :hours,
:minutes convert a number into a specified amount of days,
hours, minutes, e.g. 5:minutes is 5 minutes:
>> $start := now
>> put $start
"2026-07-21 15:56:58.9631":local
>> $deadline := $start + 7:days
>> put $deadline
"2026-07-28 15:56:58.9631":local
Basic math operations work as expected:
>> $age := "2021-01-03":utc - "2020-12-20":utc
>> print "Ticket was completed in { $age / 1:days } days"
Ticket was completed in 14 days