Expand

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