Basics & Output
Hello, World
Nushell is a shell, so a bare command at the top of a script is the program.
print writes a value followed by a newline, exactly like Ruby's puts.puts "Hello, World!"print "Hello, World!"Nushell is a shell whose pipelines carry structured data rather than bytes of text. That single decision is what makes the rest of this page look less like Bash and more like Ruby's
Enumerable.The Last Value Is Printed
A pipeline that ends without being consumed has its value rendered automatically — there is no
print here. Nushell formats a list as a table with an index column.numbers = [1, 2, 3]
p numbers[1 2 3]This is the behavior a Rubyist knows from IRB, promoted to the language itself. A script's final pipeline value is rendered through the
table command, which is why structured data shows up as a drawn table instead of an inspect string.Comments
Nushell has only line comments, introduced with
# — the same character Ruby uses.# This is a comment
puts "code" # trailing comment# This is a comment
print "code" # trailing commentThere is no block-comment syntax, so Nushell has no equivalent of Ruby's
=begin/=end. A # inside a quoted string is a literal character, not a comment.Printing Several Values
print accepts any number of arguments and renders each on its own line. Parentheses around an expression, as in (2 + 2), are how Nushell embeds a sub-expression in an argument position.puts "one", "two", 2 + 2print "one" "two" (2 + 2)Bare words in argument position are strings, so
2 + 2 without parentheses would be read as three separate arguments. The parentheses tell Nushell to evaluate rather than to quote.Inspecting a Value
describe reports a value's type, and it is the fastest way to learn what a pipeline is actually carrying.value = [1, 2, 3]
puts value.class
puts({ name: "Alice" }.class)print ([1 2 3] | describe)
print ({name: "Alice"} | describe)Nushell's types are structural rather than nominal, so
describe reports a shape such as list<int> or record<name: string> instead of a class name.Variables & Immutability
Immutable Binding
let creates an immutable binding. Reading it back requires a $ sigil — the name without the sigil would be parsed as a bare-word string.greeting = "Hello"
puts greetinglet greeting = "Hello"
print $greetingThe sigil appears on use, not on definition — the reverse of Ruby's instance and global variables, where the sigil is part of the name everywhere. Rebinding with a second
let is allowed and shadows the first.Mutable Binding
Assigning to a
let binding is an error. A variable you intend to reassign must be declared with mut.counter = 0
counter += 1
counter += 1
puts countermut counter = 0
$counter = $counter + 1
$counter += 1
print $counterRuby has no such distinction — every local is mutable. Nushell making immutability the default is a functional-language habit, and it is what allows a closure to be handed safely to a parallel command such as
par-each.Compile-Time Constants
const binds a value at parse time, before anything runs. Only expressions the parser can fold are allowed.MAXIMUM_RETRIES = 3
puts MAXIMUM_RETRIESconst MAXIMUM_RETRIES = 3
print $MAXIMUM_RETRIESBecause a
const is known before evaluation, it can be used where a runtime value cannot — most importantly as the path in a source or use statement. Ruby constants, by contrast, are ordinary runtime values that merely warn when reassigned.Block Scope
A binding lives until the end of the block that introduced it. The braces below open a new scope, and
inner does not escape it.outer = "visible"
[1].each do
inner = "hidden"
puts inner
end
puts outer
puts defined?(inner).inspectlet outer = "visible"
do {
let inner = "hidden"
print $inner
}
print $outer
print ("inner" in (scope variables | get name | each {|name| $name | str replace "$" ""}))Nushell scoping is strictly lexical, and
scope variables lets a script ask what is currently in scope. Ruby's block-local behavior is similar, but Ruby leaks a variable first assigned inside a block only when it was already defined outside.Nothing (Ruby's nil)
Nushell's empty value is
null, and its type is called nothing. The default command supplies a fallback when the piped value is null.value = nil
puts value.nil?
puts (value || "default")let value = null
print ($value == null)
print ($value | default "default")
print ($value | describe)Unlike Ruby, where only
nil and false are falsy, Nushell has no general truthiness — a conditional requires an actual boolean, so if $value on a string is a type error rather than a truthy test.Strings
String Interpolation
An interpolated string is prefixed with
$, and the holes are parentheses rather than Ruby's #{}.name = "World"
puts "Hello, #{name}!"
puts "Sum: #{2 + 3}"let name = "World"
print $"Hello, ($name)!"
print $"Sum: (2 + 3)"The parenthesis form is deliberate: a hole holds a full Nushell expression, including an entire pipeline, so
$"total: ([1 2 3] | math sum)" is valid. A plain "..." without the $ prefix never interpolates.Quoting Rules
Nushell has three string forms: double quotes process escapes, single quotes are fully literal, and a bare word with no spaces is also a string.
puts "escapes\tprocessed"
puts 'literal \t backslash-t'
puts "bare-ish"print "escapes\tprocessed"
print 'literal \t backslash-t'
print bare-ishThe bare-word form is the shell heritage showing through — it is what lets
str replace read as a command and two arguments rather than as a function call. Ruby has no equivalent; every Ruby string needs delimiters.String Methods Are Commands
Nushell has no methods. String operations are commands under the
str namespace, and the value arrives through the pipeline instead of before a dot.text = "hello world"
puts text.upcase
puts text.capitalize
puts text.length
puts text.reverselet text = "hello world"
print ($text | str uppercase)
print ($text | str capitalize)
print ($text | str length)
print ($text | str reverse)The reordering is the single biggest adjustment for a Rubyist:
text.upcase becomes $text | str uppercase. In exchange, every one of these commands works on a whole list or a table column without any change.Splitting and Joining
split row turns a string into a list of rows; str join collapses a list back into one string.sentence = "one,two,three"
parts = sentence.split(",")
p parts
puts parts.join(" | ")let sentence = "one,two,three"
let parts = ($sentence | split row ",")
print $parts
print ($parts | str join " | ")There is also
split column, which splits into a table with named columns rather than a list — the command that turns line-oriented text output into queryable data.Replacing Substrings
str replace substitutes the first match by default; the --all flag replaces every occurrence, and --regex treats the pattern as a regular expression.text = "cat hat bat"
puts text.sub("at", "og")
puts text.gsub("at", "og")
puts text.gsub(/[cb]at/, "pet")let text = "cat hat bat"
print ($text | str replace "at" "og")
print ($text | str replace --all "at" "og")
print ($text | str replace --all --regex "[cb]at" "pet")Nushell's flags map cleanly onto Ruby's method pair: no flag behaves like
sub, and --all behaves like gsub. Regular expressions are opt-in rather than implied by a literal's type.Searching Within a String
Nushell spells substring tests as infix operators:
=~ matches a regular expression, and in tests containment.text = "hello world"
puts text.include?("world")
puts (text =~ /w.rld/ ? true : false)
puts text.start_with?("hello")let text = "hello world"
print ("world" in $text)
print ($text =~ "w.rld")
print ($text | str starts-with "hello")The
in operator reads in the opposite order from Ruby's include? — the needle comes first, the haystack second. The same operator also tests list membership and record-key presence.Trimming and Padding
str trim removes surrounding whitespace, and fill pads a value to a width with an alignment flag.padded = " hello "
puts padded.strip.inspect
puts "7".rjust(3, "0")
puts "hi".ljust(5, ".")let padded = " hello "
print ($padded | str trim)
print ("7" | fill --alignment right --width 3 --character "0")
print ("hi" | fill --alignment left --width 5 --character ".")The
fill command is width-aware in a way ljust is not: it counts printable width, so it aligns correctly even when a cell contains wide characters or ANSI escapes.Numbers & Math
Arithmetic
Arithmetic operators are infix and familiar. Integer division uses
//, and mod is spelled as a word.puts 7 + 3
puts 7 / 3
puts 7.fdiv(3)
puts 7 % 3
puts 2 ** 10print (7 + 3)
print (7 // 3)
print (7 / 3)
print (7 mod 3)
print (2 ** 10)Note the inversion relative to Ruby:
/ is true division in Nushell and yields a float, while // is the truncating one. In Ruby, 7 / 3 is the integer operation.Aggregating a List
Aggregate functions live under the
math namespace and consume a list from the pipeline.numbers = [3, 1, 4, 1, 5]
puts numbers.sum
puts numbers.min
puts numbers.max
puts numbers.sum.fdiv(numbers.size)let numbers = [3 1 4 1 5]
print ($numbers | math sum)
print ($numbers | math min)
print ($numbers | math max)
print ($numbers | math avg)Because these are pipeline commands rather than methods, they compose directly with a query:
$employees | get salary | math avg needs no intermediate variable, and reads like the SQL it replaces.Converting Types
Conversions live under
into, and the target type is the command name.puts "42".to_i + 1
puts "3.5".to_f * 2
puts 42.to_s + "!"print (("42" | into int) + 1)
print (("3.5" | into float) * 2)
print ((42 | into string) + "!")Unlike Ruby's
to_i, which silently returns 0 for unparseable input, into int raises an error. That strictness is deliberate — a shell pipeline that quietly turns bad data into zero is a bug waiting to be shipped.Units Are Built Into the Language
Nushell has first-class filesize and duration literals. A number followed by a unit suffix is a distinct type, not a string.
kilobytes = 3 * 1024
puts "#{kilobytes} bytes"
seconds = 90
puts "#{seconds / 60} min #{seconds % 60} sec"let size = 3kB + 512B
print $size
let elapsed = 1min + 30sec
print $elapsed
print ($size | into int)This is something Ruby has no equivalent of without a gem. Because the units are typed,
3kB > 500B compares correctly and ls | where size > 1MB is an ordinary numeric filter rather than a parsing exercise.Lists
List Literals
A list literal uses square brackets, and the separating commas are optional — whitespace is enough.
numbers = [1, 2, 3]
words = ["a", "b", "c"]
p numbers
p wordslet numbers = [1 2 3]
let words = [a b c]
print $numbers
print $wordsOptional commas are another piece of shell heritage — a command's argument list already separates on whitespace, so a list literal does too. Commas are accepted, and mixing both styles is legal but unidiomatic.
Indexing
A list is indexed by appending
. and the position to the variable — the same cell-path syntax used for record fields.numbers = [10, 20, 30, 40]
puts numbers[0]
puts numbers[-1]
p numbers[1..2]let numbers = [10 20 30 40]
print $numbers.0
print ($numbers | last)
print ($numbers | slice 1..2)Nushell has no negative-index syntax;
last and first take that role. The slice command uses the same inclusive range literal Ruby writes as 1..2.Appending and Prepending
append and prepend return a new list; they never modify the original, because a let binding is immutable.numbers = [1, 2, 3]
more = numbers + [4]
p more
p [0] + numberslet numbers = [1 2 3]
let more = ($numbers | append 4)
print $more
print ($numbers | prepend 0)
print $numbersThe final line proves the original list is untouched. Ruby's
<< mutates in place and is the common idiom; Nushell has no in-place equivalent, so a loop that builds a list either rebinds a mut variable or — better — is rewritten as an each.Length and Emptiness
length counts pipeline items; is-empty answers the emptiness question directly.numbers = [1, 2, 3]
puts numbers.size
puts numbers.empty?
puts [].empty?let numbers = [1 2 3]
print ($numbers | length)
print ($numbers | is-empty)
print ([] | is-empty)The command is
length for every container — lists, tables, and strings via str length. Ruby offers size, length, and count as near-synonyms; Nushell picks one name and keeps it.Ranges
Range literals look much like Ruby's:
.. is inclusive and ..< excludes the end. A three-part range gives the second element rather than a step size, so 1..3..9 counts by two.p (1..5).to_a
p (1...5).to_a
p (1..9).step(2).to_aprint (1..5)
print (1..<5)
print (1..3..9)Ruby writes the exclusive form as
1...5, with three dots; Nushell writes 1..<5, putting the comparison operator where it is visible. The stepped form differs more sharply — Ruby names the stride, Nushell names the next value.Sorting and Reversing
sort orders a plain list, and --reverse flips the direction.words = ["pear", "apple", "fig"]
p words.sort
p words.sort.reverse
p words.sort_by(&:length)let words = [pear apple fig]
print ($words | sort)
print ($words | sort --reverse)
print ($words | sort-by {|word| $word | str length})The distinction between
sort and sort-by mirrors Ruby's sort and sort_by: the first compares values directly, the second compares the result of a closure. On a table, sort-by also accepts a bare column name.Uniqueness and Flattening
uniq removes duplicates and flatten collapses one level of nesting.p [1, 2, 2, 3, 1].uniq
p [[1, 2], [3, 4]].flatten
p [1, 2, 3].zip([4, 5, 6])print ([1 2 2 3 1] | uniq)
print ([[1 2] [3 4]] | flatten)
print ([1 2 3] | zip [4 5 6])There is also
uniq --count, which returns a table of each distinct value with how often it occurred — the equivalent of Ruby's tally, but already in the table shape the rest of the pipeline expects.Records
Record Literals
A record is Nushell's Hash. It uses braces with
key: value pairs, and — as with lists — the commas are optional.person = { name: "Alice", age: 30 }
p personlet person = {name: "Alice", age: 30}
print $personA record's keys are always strings and its field order is preserved. It is closer to a Ruby
Struct than to a general Hash, because Nushell's type system tracks the field names as part of the type.Reading Fields
Fields are read with a cell path — a dotted trail of names — or with the
get command, which takes the same path.person = { name: "Alice", address: { city: "Ithaca" } }
puts person[:name]
puts person[:address][:city]
puts person.dig(:address, :city)let person = {name: "Alice", address: {city: "Ithaca"}}
print $person.name
print $person.address.city
print ($person | get address.city)The dotted path descends through records and lists alike, so
$data.users.0.name is one expression rather than a chain of dig calls. Accessing a missing field is an error, not nil — get --optional opts into the lenient behavior.Updating a Field
update replaces an existing field and insert adds a new one. Both return a new record.person = { name: "Alice", age: 30 }
older = person.merge(age: 31)
p older
p person.merge(city: "Ithaca")let person = {name: "Alice", age: 30}
print ($person | update age 31)
print ($person | insert city "Ithaca")
print ($person | merge {age: 31, city: "Ithaca"})The split between
update and insert is a guard rail: update errors on a missing field and insert errors on an existing one, so a typo in a field name is caught rather than silently creating a new key the way Ruby's merge would.Keys and Values
columns lists a record's field names and values lists its values — the same two commands used on a table.person = { name: "Alice", age: 30 }
p person.keys
p person.values
puts person.key?(:name)let person = {name: "Alice", age: 30}
print ($person | columns)
print ($person | values)
print ("name" in $person)The vocabulary is table-flavored on purpose: a record is a one-row table, so
columns means the same thing for both. Ruby keeps Hash and Struct vocabulary separate.Iterating a Record
items runs a two-parameter closure over each key/value pair, which is the direct analogue of Ruby's each on a Hash.person = { name: "Alice", age: 30 }
person.each do |key, value|
puts "#{key}: #{value}"
endlet person = {name: "Alice", age: 30}
$person | items {|key, value|
print $"($key): ($value)"
}Piping a record into
each instead would treat the whole record as a single item, because a record is one value rather than a collection. The items command is what explicitly opens it up.Selecting and Rejecting Fields
select keeps only the named fields; reject removes them.person = { name: "Alice", age: 30, secret: "x" }
p person.slice(:name, :age)
p person.except(:secret)let person = {name: "Alice", age: 30, secret: "x"}
print ($person | select name age)
print ($person | reject secret)These are the same two commands used to project columns out of a table, which is the recurring theme of this page — one vocabulary covers the single-row and many-row cases.
Tables
The Table — Nushell's Signature Type
A table is a list of records. The literal form puts the column names in the first bracket, then a semicolon, then one bracket per row.
employees = [
{ name: "Alice", department: "Engineering" },
{ name: "Bob", department: "Sales" },
]
employees.each { |row| puts "#{row[:name]} — #{row[:department]}" }let employees = [[name, department]; ["Alice", "Engineering"], ["Bob", "Sales"]]
$employeesThis is the type that has no Ruby counterpart. Ruby models tabular data as an array of hashes and then prints it by hand; in Nushell the table is the native currency of every pipeline, and rendering it is the default behavior rather than a formatting chore.
A Table Is a List of Records
The literal syntax above is sugar. A plain list of records with matching keys is a table, and renders as one.
rows = [{ name: "Alice", age: 30 }, { name: "Bob", age: 25 }]
p rowslet rows = [{name: "Alice", age: 30}, {name: "Bob", age: 25}]
print ($rows | describe)
$rowsBecause the two forms are identical, any command that produces records — a
from json of an API response, an each that builds records — produces something immediately queryable.Projecting Columns
select narrows a table to the named columns; get extracts a single column as a plain list.employees = [
{ name: "Alice", age: 30, city: "Ithaca" },
{ name: "Bob", age: 25, city: "Boston" },
]
p employees.map { |row| row.slice(:name, :age) }
p employees.map { |row| row[:name] }let employees = [[name, age, city]; ["Alice", 30, "Ithaca"], ["Bob", 25, "Boston"]]
print ($employees | select name age)
print ($employees | get name)The difference matters:
select keeps the table shape while get unwraps to a bare list. Ruby needs an explicit map with a block for either.Selecting Rows
first, last, and skip slice rows off a table without touching its columns. Note that a bare range is its own lazy type, so it is materialized into a list here before being sliced.numbers = (1..10).to_a
p numbers.first(3)
p numbers.last(2)
p numbers.drop(7)let numbers = (1..10 | each {|number| $number})
print ($numbers | first 3)
print ($numbers | last 2)
print ($numbers | skip 7)These read the same on a list of integers and on a hundred-column table, which is the point — row operations and column operations are separate vocabularies that compose freely.
Computed Columns
Piping a table into
insert with a closure adds a column computed per row. Inside the closure, $row is the whole record.employees = [
{ name: "Alice", salary: 100 },
{ name: "Bob", salary: 80 },
]
withBonus = employees.map { |row| row.merge(bonus: row[:salary] * 0.1) }
p withBonuslet employees = [[name, salary]; ["Alice", 100], ["Bob", 80]]
$employees | insert bonus {|row| $row.salary * 0.1}The result is still a table, so the new column is immediately available to
where, sort-by, or another insert. This is the pipeline equivalent of adding a derived attribute in a SQL SELECT.Transposing
transpose swaps rows and columns, and takes the new column names as arguments.counts = { apples: 3, pears: 5 }
rows = counts.map { |fruit, count| { fruit: fruit.to_s, count: count } }
p rowslet counts = {apples: 3, pears: 5}
$counts | transpose fruit countThis is the standard way to turn a record into a two-column table so the rest of the table vocabulary applies to it — a shape change that Ruby expresses as a
map over a Hash.Pipelines & Queries
Filtering With where
where takes a bare condition in which column names are used unqualified — the row is implicit, so you write age > 26, not $row.age > 26.employees = [
{ name: "Alice", age: 30 },
{ name: "Bob", age: 25 },
]
p employees.select { |row| row[:age] > 26 }let employees = [[name, age]; ["Alice", 30], ["Bob", 25]]
$employees | where age > 26This is the closest Nushell gets to SQL, and it is the command that most repays a Rubyist's attention:
ls | where size > 1MB | sort-by modified is an ordinary query over real data, not a pipeline of text-mangling utilities.where With a Closure
When the condition is more than a comparison,
where also accepts a closure, and then the row is named explicitly.words = ["apple", "fig", "cherry"]
p words.select { |word| word.length > 3 }let words = [apple fig cherry]
print ($words | where {|word| ($word | str length) > 3})The bare-condition form is sugar over this one. Reaching for the closure form is the right move whenever the predicate needs a command call rather than an operator.
Sorting a Table
On a table,
sort-by takes bare column names — several of them, applied left to right.employees = [
{ name: "Cara", age: 30 },
{ name: "Alice", age: 30 },
{ name: "Bob", age: 25 },
]
p employees.sort_by { |row| [row[:age], row[:name]] }let employees = [[name, age]; ["Cara", 30], ["Alice", 30], ["Bob", 25]]
$employees | sort-by age nameRuby needs an array-returning block to sort on two keys; Nushell just lists the columns. Adding
--reverse flips every key at once.Grouping
group-by returns a record whose keys are the distinct values and whose values are the matching sub-tables.employees = [
{ name: "Alice", department: "Engineering" },
{ name: "Bob", department: "Sales" },
{ name: "Cara", department: "Engineering" },
]
grouped = employees.group_by { |row| row[:department] }
grouped.each { |department, rows| puts "#{department}: #{rows.size}" }let employees = [[name, department]; ["Alice", "Engineering"], ["Bob", "Sales"], ["Cara", "Engineering"]]
$employees | group-by department | items {|department, rows|
print $"($department): ($rows | length)"
}The shape matches Ruby's
group_by exactly — keys to collections. Following it with transpose turns the result back into a table, which is the usual next step.Counting Occurrences
uniq --count produces a two-column table of each distinct value and how often it appeared.votes = ["yes", "no", "yes", "yes"]
p votes.tallylet votes = [yes no yes yes]
$votes | uniq --countRuby's
tally hands back a Hash that still needs formatting; Nushell hands back a table that is already sortable by the count column.Chaining a Real Query
Everything above composes. This is the shape of a typical Nushell one-liner: filter, derive, sort, narrow.
employees = [
{ name: "Alice", department: "Engineering", salary: 120 },
{ name: "Bob", department: "Sales", salary: 90 },
{ name: "Cara", department: "Engineering", salary: 140 },
]
result = employees
.select { |row| row[:department] == "Engineering" }
.map { |row| row.merge(monthly: row[:salary] / 12.0) }
.sort_by { |row| -row[:salary] }
.map { |row| row.slice(:name, :monthly) }
p resultlet employees = [[name, department, salary];
["Alice", "Engineering", 120],
["Bob", "Sales", 90],
["Cara", "Engineering", 140]]
$employees
| where department == "Engineering"
| insert monthly {|row| $row.salary / 12}
| sort-by salary --reverse
| select name monthlyA pipeline may be broken across lines at the
|, which is what keeps a long query readable. The Ruby version needs an explicit block at every step; the Nushell version names the columns and lets the row stay implicit.Reducing
reduce takes a two-parameter closure. The first parameter is the item and the second is the accumulator — the opposite order from Ruby's inject.numbers = [1, 2, 3, 4]
puts numbers.inject(0) { |accumulator, item| accumulator + item }
puts numbers.inject(1) { |accumulator, item| accumulator * item }let numbers = [1 2 3 4]
print ($numbers | reduce --fold 0 {|item, accumulator| $accumulator + $item})
print ($numbers | reduce --fold 1 {|item, accumulator| $accumulator * $item})The reversed parameter order is a genuine trip hazard for a Rubyist. The
--fold flag supplies the initial value; without it, the first item seeds the accumulator, exactly as a bare inject does.Closures & Iteration
Closures
A closure is written in braces with its parameters between pipes — visually almost identical to a Ruby block.
double = ->(number) { number * 2 }
puts double.call(21)let double = {|number| $number * 2}
print (do $double 21)The difference is in how it is invoked: Ruby's lambda has
call, while Nushell uses the do command. A closure stored in a variable is a value, not a command, so it cannot be called by name alone.each
each maps a closure over every pipeline item and returns the results — so it behaves like Ruby's map, not Ruby's each.numbers = [1, 2, 3]
p numbers.map { |number| number * 2 }let numbers = [1 2 3]
print ($numbers | each {|number| $number * 2})The naming is the trap. Nushell has no separate
map; each fills that role, and a genuinely side-effect-only loop is written with for instead.The Implicit Parameter
A closure that declares no parameters can refer to the current item as
$in.numbers = [1, 2, 3]
p numbers.map { |item| item * 10 }
p numbers.map { _1 * 10 }let numbers = [1 2 3]
print ($numbers | each {|item| $item * 10})
print ($numbers | each { $in * 10 })$in is the analogue of Ruby's _1 or it, and it also names the pipeline input inside a custom command — the one variable that always refers to "whatever arrived from the left".Iterating With an Index
enumerate wraps each item in a record with index and item fields.words = ["a", "b", "c"]
words.each_with_index do |word, index|
puts "#{index}: #{word}"
endlet words = [a b c]
$words | enumerate | each {|entry|
print $"($entry.index): ($entry.item)"
}Because
enumerate produces an ordinary table, the index is available to where and sort-by too, rather than being confined to the iteration that produced it.Filtering While Mapping
An
each closure that returns null contributes nothing to the result, which gives Nushell a direct filter_map.numbers = [1, 2, 3, 4, 5, 6]
p numbers.filter_map { |number| number * 10 if number.even? }let numbers = [1 2 3 4 5 6]
print ($numbers | each {|number|
if ($number mod 2) == 0 { $number * 10 }
})An
if with no else evaluates to null when the condition fails, so the filtering falls out of the language rather than needing its own command.Parallel Iteration
Swapping
each for par-each runs the closure across threads. Because bindings are immutable, this needs no other change.numbers = [1, 2, 3, 4]
results = numbers.map { |number| number * number }
p results.sortlet numbers = [1 2 3 4]
print ($numbers | par-each {|number| $number * $number} | sort)The result order is not guaranteed, hence the trailing
sort. That a one-word edit buys parallelism is a direct dividend of immutability — the same change in Ruby would demand a threading library and a mutable-state audit. This row is display-only because the browser build cannot start a thread pool; run it under a real nu to see it work.Control Flow
Conditionals
if takes a condition and a brace-delimited block. It is an expression, so it evaluates to the value of the branch taken.temperature = 30
if temperature > 25
puts "warm"
elsif temperature > 15
puts "mild"
else
puts "cold"
endlet temperature = 30
if $temperature > 25 {
print "warm"
} else if $temperature > 15 {
print "mild"
} else {
print "cold"
}The chained keyword is
else if as two words, not Ruby's elsif. There is no trailing end — braces delimit the blocks, and the condition needs no parentheses.if as an Expression
Because
if returns a value, it can be bound directly to a variable.score = 85
grade = if score >= 90 then "A" elsif score >= 80 then "B" else "C" end
puts gradelet score = 85
let grade = if $score >= 90 { "A" } else if $score >= 80 { "B" } else { "C" }
print $gradeThis works the same way in both languages. The one asymmetry is that an
if without an else yields null in Nushell, which is exactly the property the filtering idiom above depends on.Pattern Matching
match compares a value against patterns, using | for alternatives and _ for the catch-all.value = 3
described = case value
in 1 then "one"
in 2 | 3 then "a couple"
else "many"
end
puts describedlet value = 3
let described = match $value {
1 => "one",
2 | 3 => "a couple",
_ => "many"
}
print $describedRuby 3's
case/in and Nushell's match are close cousins; both destructure rather than merely compare. Nushell writes the arms with => and separates them with commas.Destructuring in match
A pattern may describe a record's shape and bind its fields to names in one step.
event = { type: "click", x: 10, y: 20 }
case event
in { type: "click", x:, y: }
puts "click at #{x},#{y}"
in { type: "key", code: }
puts "key #{code}"
endlet event = {type: "click", x: 10, y: 20}
match $event {
{type: "click", x: $x, y: $y} => { print $"click at ($x),($y)" }
{type: "key", code: $code} => { print $"key ($code)" }
}The bound names carry a
$ in the pattern, which makes the binding sites obvious at a glance — Ruby's x: shorthand leaves them easier to miss.for Loops
for is the side-effect loop. Unlike each, it produces no value, so it is the honest choice when nothing is being collected.(1..3).each do |number|
puts "count #{number}"
endfor number in 1..3 {
print $"count ($number)"
}Nushell deliberately keeps the two apart:
each is for building a result and for is for doing something. Ruby's each covers both cases and returns the receiver.while and loop
while repeats while a condition holds, and break leaves the loop — both need a mut variable to make progress.countdown = 3
while countdown > 0
puts countdown
countdown -= 1
endmut countdown = 3
while $countdown > 0 {
print $countdown
$countdown -= 1
}There is also a bare
loop that runs forever until break, matching Ruby's loop do. In practice a Nushell script reaches for a pipeline far more often than for either.Custom Commands
Defining a Command
def introduces a custom command. Parameters are listed in square brackets, and calling it uses space-separated arguments — no parentheses, no commas.def greet(name)
"Hello, #{name}!"
end
puts greet("Ruby")def greet [name] {
$"Hello, ($name)!"
}
print (greet "Ruby")A custom command is indistinguishable from a built-in at the call site, which is the whole point —
greet "Ruby" looks exactly like str uppercase. The last expression is the return value, as in Ruby.Typed Parameters
A parameter may carry a type annotation after a colon, and the command may declare its input and output types with
: input -> output.def double(number)
raise TypeError, "not an Integer" unless number.is_a?(Integer)
number * 2
end
puts double(21)def double [number: int]: nothing -> int {
$number * 2
}
print (double 21)The annotations are checked at parse time, so a wrong argument type is caught before the pipeline runs. Ruby has no equivalent without a runtime guard like the one above, or an external type checker.
Optional Parameters and Defaults
A default value goes after
=, and a trailing ? marks a parameter optional (it arrives as null when omitted).def greet(name, greeting = "Hello")
"#{greeting}, #{name}!"
end
puts greet("Ruby")
puts greet("Ruby", "Howdy")def greet [name, greeting = "Hello"] {
$"($greeting), ($name)!"
}
print (greet "Ruby")
print (greet "Ruby" "Howdy")Positional defaults work exactly as they do in Ruby. What differs is that Nushell also has real flags, which is where a shell-shaped API usually wants its optional arguments.
Flags
A parameter beginning with
-- is a flag. A bare flag is a boolean switch; giving it a type makes it take a value, and (-x) adds a short form.def announce(message, loud: false, times: 1)
text = loud ? message.upcase : message
times.times { puts text }
end
announce("hello")
announce("hello", loud: true, times: 2)def announce [message, --loud, --times (-t): int = 1] {
let text = if $loud { $message | str uppercase } else { $message }
for _ in 1..$times { print $text }
}
announce "hello"
announce "hello" --loud --times 2This is where the shell heritage pays off: Ruby keyword arguments read as
loud: true, while Nushell flags read as --loud — the same spelling a user already types at a prompt, and help announce documents them automatically.Rest Parameters
A parameter prefixed with
... collects the remaining arguments into a list — Ruby's splat, with the dots on the other side of the name.def total(*numbers)
numbers.sum
end
puts total(1, 2, 3, 4)def total [...numbers: int] {
$numbers | math sum
}
print (total 1 2 3 4)Ruby writes
*numbers and Nushell writes ...numbers, but the semantics match. The type annotation applies to each element rather than to the collected list.Commands That Take Pipeline Input
A command reads whatever was piped into it through
$in. Declaring the input type in the signature is what makes it compose like a built-in.def shout(text)
text.upcase + "!"
end
puts shout("hello")def shout []: string -> string {
$"($in | str uppercase)!"
}
print ("hello" | shout)This is the idiom that makes a custom command a first-class citizen of the pipeline. Ruby's nearest equivalent is monkey-patching
String, which is far more invasive than declaring an input type.Documentation Comments
Comments immediately above a
def, and after each parameter, become the command's built-in help text.# Repeats a message.
#
# @param message [String] the text to repeat
def repeat(message, times)
Array.new(times, message).join(" ")
end
puts repeat("hi", 3)# Repeats a message.
def repeat [
message: string # The text to repeat.
times: int # How many copies.
] {
1..$times | each { $message } | str join " "
}
print (repeat "hi" 3)Running
help repeat prints those comments alongside the generated signature. Ruby needs RDoc or YARD plus a separate tool to get the same result; in Nushell it is part of the runtime.Types & Signatures
Structural Types
describe reports the full structural type, including the element type of a list and the field types of a record.p [1, 2, 3].class
p({ name: "Alice" }.class)
p [{ name: "Alice" }].classprint ([1 2 3] | describe)
print ({name: "Alice"} | describe)
print ([[name]; ["Alice"]] | describe)A Nushell type describes a value's shape —
table<name: string> — rather than naming a class. Nothing is nominal, so there is no inheritance and no is_a?.Converting Between Shapes
The
into family converts values, and into record/into value reshape structured data.pairs = [["a", 1], ["b", 2]]
p pairs.to_hlet pairs = [[key, value]; [a, 1], [b, 2]]
print ($pairs | transpose --header-row | into record)Ruby's
to_h on an array of pairs has no single-command Nushell equivalent, because the table is the more general shape — the conversion goes through transpose instead.Missing Data
Accessing an absent field is an error by default. The
? suffix on a cell path makes it yield null instead.person = { name: "Alice" }
p person[:city]
p person.fetch(:city, "unknown")let person = {name: "Alice"}
print ($person.city? | describe)
print ($person | get --optional city | default "unknown")Ruby's
[] returns nil for a missing key and fetch raises; Nushell inverts the defaults, so the strict behavior is what you get unless you ask for leniency.Inspecting a Signature
Every command carries its signature at runtime, and
scope commands can be queried like any other table.def greet(name, greeting = "Hello")
"#{greeting}, #{name}"
end
p method(:greet).parametersdef greet [name, greeting = "Hello"] {
$"($greeting), ($name)"
}
scope commands | where name == "greet" | get 0.signatures | to jsonThat the command table is itself queryable data is the recurring theme — Nushell's introspection needs no reflection API, because introspection results are ordinary tables.
Data Formats
Parsing JSON
from json turns a JSON string into records and tables. There is nothing to require.require "json"
parsed = JSON.parse('{"name": "Alice", "age": 30}')
puts parsed["name"]let parsed = ('{"name": "Alice", "age": 30}' | from json)
print $parsed.nameThe result is a native record, so the whole query vocabulary applies immediately. This is the command that most often replaces a
curl | jq incantation.Producing JSON
to json serializes any structured value; the --raw flag emits it on one line.require "json"
employees = [{ name: "Alice", age: 30 }]
puts JSON.pretty_generate(employees)let employees = [[name, age]; ["Alice", 30]]
print ($employees | to json)
print ($employees | to json --raw)Every
from command has a matching to, and the pair round-trips. Pretty-printing is the default rather than a separate method name.Converting Between Formats
Because every format decodes to the same internal representation, converting between two of them is a
from followed by a to.require "json"
require "yaml"
data = JSON.parse('[{"name":"Alice"},{"name":"Bob"}]')
puts data.to_yaml'[{"name":"Alice"},{"name":"Bob"}]'
| from json
| to yamlNushell ships converters for JSON, YAML, TOML, CSV, TSV, XML, and more. The set is open-ended in the same way Ruby's serialization libraries are, but they all meet in one data model rather than each defining its own.
CSV as a Table
from csv reads a header row and produces a table directly.require "csv"
text = "name,age\nAlice,30\nBob,25"
rows = CSV.parse(text, headers: true)
rows.each { |row| puts "#{row["name"]} (#{row["age"]})" }let text = "name,age\nAlice,30\nBob,25"
$text | from csv | where age > 26The
age column is inferred as an integer, which is why the comparison works without a conversion. Ruby's CSV library hands back strings until told otherwise.Parsing Unstructured Text
parse extracts named fields from lines using a template with {name} placeholders, producing a table.lines = ["alice:30", "bob:25"]
rows = lines.map do |line|
name, age = line.split(":")
{ name: name, age: age.to_i }
end
p rows["alice:30", "bob:25"] | parse "{name}:{age}"This is how text from an external command becomes queryable data. The
--regex flag accepts a full regular expression with named capture groups when the template form is not expressive enough.Error Handling
try / catch
try takes a block, and catch takes a closure whose parameter is the error record.begin
raise ArgumentError, "something failed"
rescue => failure
puts "Caught: #{failure.message}"
endtry {
error make {msg: "something failed"}
} catch {|failure|
print $"Caught: ($failure.msg)"
}The error arrives as an ordinary record with fields such as
msg, so inspecting it uses the same cell paths as any other data. There is no exception class hierarchy to match against.Raising an Error
error make builds an error from a record. Adding a label gives it a source span, which is what produces Nushell's underlined diagnostics.def divide(numerator, denominator)
raise ZeroDivisionError, "denominator must not be zero" if denominator == 0
numerator / denominator
end
puts divide(10, 2)def divide [numerator: int, denominator: int] {
if $denominator == 0 {
error make {msg: "denominator must not be zero"}
}
$numerator // $denominator
}
print (divide 10 2)Nushell's error rendering is one of its most polished features — a raised error points at the exact span of source that produced it, in the style of a Rust compiler diagnostic.
try as an Expression
try evaluates to the block's value, so a catch that returns a value makes it a fallback expression.value = begin
Integer("not a number")
rescue ArgumentError
0
end
puts valuelet value = try {
"not a number" | into int
} catch {
0
}
print $valueThis is the idiomatic way to give a failing conversion a default, and it reads much like Ruby's
begin/rescue in an assignment position.Inspecting the Error
The caught error is a record;
columns shows which fields it carries.begin
raise ArgumentError, "bad input"
rescue => failure
puts failure.class
puts failure.message
endtry {
error make {msg: "bad input"}
} catch {|failure|
print ($failure | columns)
print $failure.msg
}Because there are no error classes, dispatching on the kind of failure means matching on the message or on a field you put there yourself — a real trade-off against Ruby's
rescue SpecificError.Assertions
The standard library ships an
assert command, pulled in with use std/assert — the path form, which imports just that submodule.total = [1, 2, 3].sum
raise "expected 6" unless total == 6
puts "ok"use std/assert
let total = ([1 2 3] | math sum)
assert ($total == 6)
print "ok"The
std library is written in Nushell itself and loaded on demand, so importing one submodule costs only that submodule. The bare use std form, which loads everything, does not work in this page's browser build — it needs the $nu constant, which cannot be constructed in WebAssembly.Modules & Scripts
Defining a Module
module groups commands, and only those marked export are visible outside it.module Greetings
def self.hello(name) = "Hello, #{name}!"
def self.goodbye(name) = "Goodbye, #{name}!"
end
puts Greetings.hello("Ruby")module greetings {
export def hello [name] { $"Hello, ($name)!" }
export def goodbye [name] { $"Goodbye, ($name)!" }
}
use greetings
print (greetings hello "Ruby")A module's commands are namespaced by the module name at the call site —
greetings hello, with a space. That is a subcommand, exactly the shape of built-ins like str uppercase.Importing Selectively
use module command pulls one command into the current scope unqualified; use module * imports everything.module Greetings
def self.hello(name) = "Hello, #{name}!"
end
include Greetings rescue nil
puts Greetings.hello("Ruby")module greetings {
export def hello [name] { $"Hello, ($name)!" }
}
use greetings hello
print (hello "Ruby")The import is lexically scoped and takes effect at parse time, which is why the module path in a
use must be a constant rather than a computed value.Exporting Values
A module can export constants with
export const, not just commands.module Configuration
MAXIMUM_RETRIES = 3
end
puts Configuration::MAXIMUM_RETRIESmodule configuration {
export const MAXIMUM_RETRIES = 3
}
use configuration
print $configuration.MAXIMUM_RETRIESThe exported constants arrive as a record named after the module, so they are read with an ordinary cell path rather than Ruby's
:: scope operator.The Standard Library
Nushell's
std library is itself a module, imported the same way as your own.require "set"
unique = Set.new([1, 2, 2, 3])
puts unique.sizeuse std/iter
print ([1 2 2 3] | uniq | length)
print ([1 2 3] | iter find {|item| $item > 1})Because
std is written in Nushell rather than in Rust, reading its source is an effective way to learn idiomatic style — something a Rubyist will recognize from Ruby's own largely-Ruby standard library.Files & External Commands
Listing Files Returns a Table
ls returns a real table with typed columns — size is a filesize and modified is a date — so it can be queried rather than parsed.entries = Dir.glob("*").map do |path|
{ name: path, size: File.size(path) }
end
p entries.select { |entry| entry[:size] > 1024 }.first(3)# ls needs something to list, so: one big file
# and one small one.
(0..1199 | each { "x" } | str join) | save --force report.txt
"ok" | save --force flag.txt
# ls returns a TABLE, so this is a query rather
# than text processing.
ls *.txt | where size > 1kb | select name sizeThis is the example that sells Nushell. In Bash the same query is a fragile
ls -l | awk pipeline; here it is a typed query, because ls never produced text in the first place — size is a filesize, so > 1kb is a comparison rather than string surgery, and modified would be a date. The Ruby column reads a directory it did not create, which is why it is shown rather than run.Opening a File Parses It
open chooses a parser from the file extension, so a .json file arrives as records rather than as a string.require "json"
data = JSON.parse(File.read("config.json"))
puts data["name"]'{"name":"atlas","port":8080}' | save --force config.json
# open infers the format from the extension
# and returns a record, not text.
open config.json | get nameBoth columns print
atlas, and the difference is the require "json" and the explicit JSON.parse. Passing --raw suppresses the parsing and yields the bytes. That the parsing is the default is the clearest statement of Nushell's design position: data should stay structured for as long as possible.Calling External Commands
An external program is called by name, or with a leading
^ when the name collides with a built-in. Its output arrives as a string.output = `echo hello`
puts output.striplet output = (^echo hello)
print ($output | str trim)The boundary is explicit: everything inside Nushell is structured, and crossing to an external process means falling back to text — which is exactly where
parse, from json, and split column earn their place.Environment Variables
$env is a record, so environment variables are read and written with ordinary cell paths.puts ENV.fetch("CODECOMPARED_DEMO", "(unset)")
ENV["CODECOMPARED_DEMO"] = "on"
puts ENV["CODECOMPARED_DEMO"]print ($env.CODECOMPARED_DEMO? | default "(unset)")
$env.CODECOMPARED_DEMO = "on"
print $env.CODECOMPARED_DEMOBecause
$env is a record, $env | columns | sort lists every variable and $env.PATH is a real list rather than a colon-delimited string that has to be split. The variable here is one that is deliberately never set, so the first line prints (unset) everywhere; reading $HOME instead would print a different answer on every machine, and nothing at all in the browser, where the process environment is empty.