Skip to content

Pipes and Chains

lash supports standard POSIX pipes. Nothing new here:

cat /var/log/syslog | grep ERROR | sort | uniq -c | sort -rn | head -20

Pipes connect stdout of one process to stdin of the next as byte streams. Redirections work as expected:

`ls -la` > files.txt
`ls -la` >> log.txt
`cat < input.txt`.sort

The real power of lash is the transition from byte-stream pipelines to functional method chains. Wrap a command in backticks and follow it with a dot to enter functional mode:

`ls -la`.sort()
`grep -r "error" src/`.filter(x => x.length > 0).take(10)
`cat access.log`.sort().unique().reverse()

Backticks do NOT perform POSIX command substitution. Use $() for that. The backtick-dot operator converts the command’s stdout into a line-stream that methods operate on.

POSIX pipes inside backticks work normally:

`cat server.log | head -1000`.filter(x => x.contains("WARN"))

A chain’s output can feed into a POSIX pipe:

`ls -la`.filter(x => x.contains(".d")).sort() | wc -l

Parentheses are optional when a method takes no arguments:

`ls`.sort # equivalent to `ls`.sort()
`ls`.sort.take(5) # mix freely

Methods like map and filter accept lambdas:

# explicit single-param
`ls`.filter(x => x.endsWith(".log"))
# multi-param (for reduce)
[1, 2, 3, 4].reduce(0, (acc, x) => acc + x) # 10
# implicit lambda -- the parameter is always `a`
[1, 2, 3].map(a + 10) # [11, 12, 13]
[1, 2, 3, 4].filter(a > 2) # [3, 4]
`ls`.map("-> ${a}") # string interpolation in implicit lambda

When a method receives a non-lambda expression, it wraps it as a => <expr>. The implicit a shadows any outer variable with that name.

By default, backtick captures stdout. Use stream methods to change this:

`make`.stderr > errors.txt
`make`.merge.filter(x => x.contains("error"))
let result = `make`.capture
result["exitCode"]
result["stdout"]
result["stderr"]

Find the 5 largest .log files:

# POSIX pipe approach
ls -la *.log | sort -k5 -rn | head -5
# functional chain approach
`ls -la *.log`.sort().reverse().take(5)

Extract unique error codes from a log:

# POSIX pipe approach
grep "ERROR" server.log | cut -d: -f2 | sort | uniq
# functional chain approach
`grep "ERROR" server.log`.map(x => x.split(":")[1]).sort().unique()

Count word frequencies across files:

# functional chain approach (no POSIX equivalent this clean)
`cat *.md`.wordcount().take(20)
MethodDescription
sortString-based sort
sortNumericNumeric sort
map(fn)Transform each element
filter(fn)Keep elements matching predicate
grep(pattern)Regex-powered filter (no process spawn)
take(n)First N elements
drop(n)Skip first N elements
join(sep?)Concatenate into single string
collectCapture lazy stream into static list
lengthNumber of elements
jsonParse each element as JSON (fails on invalid)
jsonlParse each element as JSON (skips invalid)
buffer(n)Set bounded buffer size
uniqueRemove duplicates, preserve order
reverseReverse element order
flattenFlatten one level of nesting
sumSum numeric elements
wordcountWord frequencies, sorted by count descending
each(fn)Like map but returns original list (side effects)
reduce(init, fn)Aggregate into single value
last(n)Last N elements
firstFirst element (unwrapped)
emptyTrue if list has no elements
column(n, sep?)Extract Nth field (1-indexed)
stdoutSelect stdout stream (default)
stderrSelect stderr stream
mergeCombine stdout and stderr
captureReturn object with stdout, stderr, exitCode
extract(pattern)Pattern matching with # and * wildcards
MethodDescription
lengthString length
toUpper()Convert to uppercase
toLower()Convert to lowercase
trim()Remove leading/trailing whitespace
contains(sub)True if string contains substring
startsWith(prefix)True if string starts with prefix
endsWith(suffix)True if string ends with suffix
jsonParse string as JSON
split(sep)Split by separator, returns list
replace(old, new)Replace all occurrences
toNumber()Convert to int or float
extract(pattern)Pattern matching with wildcards
MethodDescription
lengthNumber of keys
keys()Returns list of key names

Parse JSON from command output by joining lines and calling .json:

let data = `cat config.json`.join.json
data["database"]["host"]

For newline-delimited JSON (one object per line), use .jsonl:

`cat events.ndjson`.jsonl.filter(x => x["level"] == "error").take(10)

Bracket access works with variables:

let field = "name"
`cat users.ndjson`.json[field]

Write values to stdout or stderr:

"Fetching...".writeln(stderr)
[1, 2, 3].writeln(stdout)

Block-scoped file writing:

write to "output.txt" as f {
"hello world".writeln(f)
[1, 2, 3].writeln(f)
}