Nushell
About
Nushell makes use of command outputs as data that can be transformed, it makes use of pipes that connnect commands together in a functional-programming usage style
Thinking in Nu
Nushell works with results using pipes, this is similar to > in bash but isn't exactly the same
Immutability
Variables are immutable, however values can be shadowed, so I can create a shadowing x based on a previous x like so:
let x = $x + 1
Scoping
Nushell uses scoped environmments in blocks, so a command can use a value within its scope like so:
ls | each { |it|
cd $it.name
make
}
Fundamentals
Types of Data
The describe command returns the type of a variable
42 | describe
Conversions
The into command can convert variable types
"-5" | into int
"1.2" | into decimal
Strings
Strings can be created as:
- Double quotes:
"hello world" - Single quotes:
'hello: "world"' - Interpolated:
$"my number = (40 + 2)" - Bare:
hello
Bools
Booleans are simply true and false
Dates
Dates can be in the following formats:
2022-02-022022-02-02T14:30:002022-02-02T14:30:00+05:00
Durations
Nushell has the following durations:
nsnanosecondusmicrosecondmsmillisecondsecsecondminminutehrhourdaydaywkweek
And can be used like so:
3.14day
Or in calculations
30day / 1sec
Ranges
Ranges can be done as 1..3 for example, by default the end is inclusive, ranges can also be open ended ..2 or 2..
Records
Records hold key-value pairs, and may or may not have commas between entry names:
{name: john age: 5}
A record is the same as a single row of a table
Records can be iterated over by transposing them into a table:
{name: john age: 5} | transpose key value
And accessing properties can be done like:
{name: john age: 5}.age
Or
{name: john age: 5}."age"
You can also insert/update/upsert into a record as such
$item
| upsert length {|i| $i.name | get length }
The above will work with a single record or a table/list of records
Sometimes you just want to create a new object from a value, this can be done with wrap
ls
| get name
| wrap path # data is now structured like [{path: ....}, {path: ...}]
The above will work with a single record or a table/list of records
Lists
Lists are ordered sequences of data and use [] with optional , separators. The below will create alist of strings
[sam fred george]
A list is the same as a single column table
Indexing lists can be done with a . as with records:
[sam fred george].1
Or using ranges:
[sam fred george] | range 0..1
Tables
Tables can be created with the following syntax:
[[column1, column2]; [Value1, Value2] [Value3, Value4]]
Tables can also be created from json
[{name: sam, rank: 10}, {name: bob, rank: 7}]
Internally tables are just a list of records
Blocks
Blocks of code are denoted using {}, for example:
each { |it| print $it }
Filtering
The most common way to filter data is using where, this has a few different ways it can be used
The structure of where can be some kind of comparison of the form where <field> <comparator> <value>, e.g. ~= does a "contains" comparison
ls | where name =~ "package"
Alternatively, we can use $it to reference to a given input value, like where <expression>:
ls | where ($it.name | str starts-with "package")
$it is used for referencing each row of the input list. If we want to do something more complex, you can also use a closure:
ls | where {|i| $i.name | str starts-with "package"}
Or, inferring the the closure param with $in to make it easier to type:
ls | where {$in.name | str starts-with "package"}
Loading Data
Open Files
Files can be opened with the open command:
open package.json
Nu will parse the file if it can and will return data and not just a plain string
If a file extension isn't what the type usually has, we can still parse the file, we just ned to tell nu that it's a specific format, so we can do this like so:
open Cargo.lock | from toml
Manipulating Strings
String data can be manipulated using things like the lines command which will split each line into a row:
open people.txt | lines
And we can further apply the split command on the column to split it by some specific character:
open people.txt | lines | split column ";"
Additionally, we can use trim:
open people.txt | lines | split column ";" | str trim
And lastly, we can transform it into a table with formal column names with some additional properties on the split command:
open people.txt | lines | split column "|" first_name last_name job | str trim
Fetch Urls
We can also fetch remote files which will then also be converted into data like so:
fetch https://blog.rust-lang.org/feed.xml
Cheatsheet
Moving around the File System
Nushell provides commands for normal file-system related tasks which are similar to common commands such as:
./hello/world # will cd to the directory
Listing Files
ls
Or listing a specific file type
ls *.md
Or even globs
ls **/*.md
Globs
You can also use the glob method directly to find files recursively:
glob **/*.png
The
globmethod returns a list of strings versus thelsmethod which returns a list of file objects
Stopping All Docker Containers
The Docker CLI outputs data that's nicely structured for working with the NuShell table structure.
We can kill all containers by parsing the data into a table and stopping them individually
docker container ls | from ssv | select "CONTAINER ID" | each { |i| docker container stop $i."CONTAINER ID" }
Config
Some utils from my current config.nu, primarily for working with Git
alias gch = git checkout
alias gcb = git checkout -b
alias glg = git log --graph
alias ga = git add
alias gp = git push
alias gf = git fetch
alias gl = git pull
alias gcm = git commit -m
alias gprune = git remote prune origin
alias conf = code $nu.config-path
alias env = code $nu.env-path
## Deletes all branches other than the current branch
def gclean [] {
git branch
| lines
| filter {|l| $l | str contains -n "*"}
| each {|b| $b | str trim}
| each {|b| git branch -d $b}
}
def 'gclean D' [] {
git branch
| lines
| filter {|l| $l | str contains -n "*"}
| each {|b| $b | str trim}
| each {|b| git branch -D $b}
}
def gmaster [] {
let branch = git rev-parse --abbrev-ref HEAD
git checkout master
git pull
git checkout $branch
git merge master
}
def dev [repo:string] {
code $"~/repos/$repo"
}
## Search for a string or regex using `rg -i`
def search [
regex:string, # regex or string to search on
-i # Run the search as case insensitive
] {
if $i {
rg -i $regex
} else {
rg $regex
}
}
Watch Mode
Nushell has builtin support for watching files and running a comand when they change
You can do this using the watch command:
watch /some/path { echo "things have changed" }
watch /some/path {|op, path, new_path| echo "things have changed" }
watch /some/path --glob=**/*.json { echo "things have changed" }
Notifiy
A little script that's also useful to have is this one that will notify you when a task completes. It's a handy way to be notified when a long running task completes
This uses AppleScript as per the example on stackexchange so it will only work on MacOS. I'm sure there's a similar way to accomplish this on other operating systems
def "notify" [title: string = "Task Complete"] {
print $in
let $command = 'display notification "' + $title + '" with title "Shell Process Complete" sound name "Frog"'
osascript -e $command
}
You can include the above in your nushell config and use it as follows:
my-command long-task | notify "My Long Task is Completed"
It will also handle printing the output from the task being run
The $in value and closures
Pass streams as arguments using $in
The $in can be implicitly accessed as the value that's piped into another command. Basically, this means that the following tao commands are equal:
## passing it normally
echo (open hello.txt)
## passing with $in
open hello.txt | echo $in
Passing Multiple Strings
Nushell supports a spread-type operator for passing a list from input into a space-separated command kind of like xargs:
ls *.json | get name | yarn prettier --write ...$in
The
...$inspreads the input stream into a space-separated list
Usage with Functions and Closures
Nushell functions can also use an implicit input parameter, this can be used when defining a function, for example:
def example[] {
echo $in
}
Which can then be used as
"Hello World!" | example
Additionally, note that example "Hello World!" will not work since $in params cannot be passed positionally and can only be used part of a pipeline
It's also possible to use $in when we epect a closure which lets us leave out the parameter definition, for example, we can run ls in all subdirectories of an input like so:
## Using a normal closure
ls | each { |f| ls $f.name }
## Using `$in`
ls | each { ls $in.name }
The { ls $in.name } is the same as a closure like {|f| ls $f.name } so it's a bit easier to type in this scenario as well.
Parsing
The parse function can be used to read some string into a usable data structure, take the following file for example:
john smith, age: 24
jack smith, age: 54
The parse command lets us structure that using:
open data.txt | lines | parse "{name} {surname}, age: {age}"
╭───┬──────┬─────────┬─────╮
│ # │ name │ surname │ age │
├───┼──────┼─────────┼─────┤
│ 0 │ john │ smith │ 24 │
│ 1 │ jack │ smith │ 54 │
╰───┴──────┴─────────┴─────╯
Detecting Columns
In simple cases instead of parsing some text you can also use detect columns. For example using a file like this:
Name Age
Bob Smith 25
Jack Smith 82
We can use detect columns to automatically parse the simple structure for us:
open data.txt | detect columns
╭───┬──────┬─────╮
│ # │ Name │ Age │
├───┼──────┼─────┤
│ 0 │ Bob │ 25 │
│ 1 │ Jack │ 82 │
╰───┴──────┴─────╯
If our table doesn't have headers we can still use detect columns --no-headers to prevent it trying to use the first row as a header:
git status --porcelain | detect columns --no-headers
╭───┬─────────┬──────────╮
│ # │ column0 │ column1 │
├───┼─────────┼──────────┤
│ 0 │ A │ data.txt │
╰───┴─────────┴──────────╯
We can combine this with a rename to improve this structure of our output table:
git status --porcelain | detect columns --no-headers | rename status file
╭───┬────────┬──────────╮
│ # │ status │ file │
├───┼────────┼──────────┤
│ 0 │ A │ data.txt │
╰───┴────────┴──────────╯
Input
You can take in user input using the input function, this allows for dynamic imput. This is handy for doing a search over some list, for example composing it with the above:
open data.txt | lines | parse "{name} {surname}, age: {age}" | input list 'Search for User' --fuzzy
Closures
Nushell does something quite interesting with closures. Since everything is immutable it's possible to do environment-changing operations in a somewhat contained way.
For example, I can do some stuff like moving to a different folder, but I will not be affected outside of the closure
## in the `root` folder
do { cd ./my-child | ls } # within the closure i am inside of the `my-child` folder
## back to the `root` folder
Or I can cd into each folder and ls each of them, while remaining in my parent folder.
## in the `root` folder
ls | where type == dir | each { cd $in.name | ls }
## back to the `root` folder
Parallel
Due to the isolation that closures afford us, we can also run these in parallel, nushell has parallel methods of some commands, e.g. the each command, which can be used with par-each:
ls | where type == dir | par-each { cd $in.name | ls }
This works the same but is much faster for large/complex tasks
Timer
Nushell also has a timeit command that can be used to time the execution of any block, for example:
timeit { ls | each { print $in.name } }
Pipes
Pipes are done using |, for example:
cat myfile.txt | lines
We can also more specifically pipe the stdout or strerr streams using the following syntaxes
## stdout only
dostuff | lines
## stderr only
dostuff err>| lines
dostuff e>| lines
## stderr + stdout
dostuff out+err> lines
dostuff o+e> lines
Completion
You can also capture the stdout, stderr, and exit_code using the complete command like so:
cat myfile.txt | complete
╭───────────┬────────────────────────────────────────────╮
│ stdout │ │
│ stderr │ cat: myfile.txt: No such file or directory │
│ │ │
│ exit_code │ 1 │
╰───────────┴────────────────────────────────────────────╯