Every Bash script you’ve ever written that contains awk '{print $3}' or grep | sed | cut -d':' -f2 is a small act of violence against your own time. You’re taking structured data — a process list, a JSON API response, a directory listing — turning it into a blob of text, then parsing it back into structure with brittle string manipulation. One extra space in the output, one locale change, one version bump of the tool, and your script is silently wrong.
Nushell (github.com/nushell/nu) takes a different bet: what if the shell never lost the structure in the first place?
This isn’t a toy. Nushell has been in active development since 2019, it’s written in Rust, and it runs on Linux, macOS, and Windows. I’ve been using it as my primary shell for over a year on workstations and in CI pipelines, and I’ll show you where it actually earns its keep — and where it’ll bite you if you’re not careful.
The Core Idea in Two Minutes
In Bash, ls -l gives you text. In Nushell, ls gives you a table:
> ls
╭───┬──────────────┬──────┬──────────┬────────────────╮
│ # │ name │ type │ size │ modified │
├───┼──────────────┼──────┼──────────┼────────────────┤
│ 0 │ docker-compose.yml │ file │ 1.2 KiB │ 2 days ago │
│ 1 │ src │ dir │ 4.1 KiB │ 3 hours ago │
│ 2 │ Makefile │ file │ 842 B │ 5 days ago │
╰───┴──────────────┴──────┴──────────┴────────────────╯
That’s not pretty-printed text — it’s an actual table with typed columns. size is bytes, modified is a datetime. You can query it like a database:
> ls | where type == "file" | sort-by size --reverse | first 5
No awk, no sort -k5, no explaining to your future self what -k5 meant. The pipeline operates on structured records all the way through.
That’s the whole philosophy. Let’s see where it matters.
Installation
# via the official install script (Linux/macOS)
curl -fsSL https://get.nushell.sh | sh
# or via cargo if you already have Rust
cargo install nu
# Arch Linux
pacman -S nushell
# macOS Homebrew
brew install nushell
Start it with nu. Don’t set it as your default shell yet — get comfortable first.
Real Use Case 1: Wrangling JSON APIs Without jq Gymnastics
jq is great, but the filter syntax is its own language you have to relearn every time. Nushell natively understands JSON:
> http get https://api.github.com/repos/nushell/nushell/releases
| select tag_name published_at assets
| first 5
The response is parsed into a table automatically. No jq '.[] | {tag: .tag_name, date: .published_at}'.
Working with a local file is the same:
> open packages.json | get dependencies | transpose package version
transpose turns an object’s keys and values into rows — something that takes a paragraph of jq to do cleanly.
Gotcha: http get follows redirects and parses Content-Type automatically. If an endpoint returns text/plain even though the body is JSON, Nushell won’t parse it. Use | from json explicitly in that case:
> http get https://example.com/data | from json
Real Use Case 2: Process Management That Doesn’t Feel Like Archaeology
Finding and killing processes in Bash usually looks like:
kill $(ps aux | grep '[n]ginx' | awk '{print $2}')
Cursed. In Nushell:
> ps | where name =~ "nginx"
╭───┬───────┬───────────┬──────────╮
│ # │ pid │ name │ cpu │
├───┼───────┼───────────┼──────────┤
│ 0 │ 12341 │ nginx │ 0.0 % │
│ 1 │ 12342 │ nginx │ 0.1 % │
╰───┴───────┴───────────┴──────────╯
> ps | where name =~ "nginx" | each { |p| kill $p.pid }
The =~ operator is a regex match. each iterates over rows and gives you a record — $p.pid is typed, not a string you’re hoping is a number.
Production-ready: Filter by memory thresholds for a quick "what’s eating my RAM" diagnostic:
> ps | where mem > 500mb | sort-by mem --reverse
That 500mb is a literal file-size value Nushell understands. Not 500 * 1024 * 1024. Not parsing /proc/meminfo yourself.
Real Use Case 3: Docker and Kubernetes — Finally Readable
> docker ps | from ssv --aligned-columns
from ssv (space-separated values) turns Docker’s tabular text output into a proper table. Then you can filter:
> docker ps | from ssv --aligned-columns | where STATUS =~ "Up" | select "CONTAINER ID" IMAGE PORTS
For Kubernetes, kubectl with -o json is your friend:
> kubectl get pods -o json | from json | get items | select metadata.name status.phase status.podIP
This is dramatically cleaner than kubectl get pods --no-headers | awk '{print $1, $3}'. And it doesn’t break when someone adds a column to the kubectl output.
Gotcha: kubectl column widths and header alignment vary between versions. Always use -o json or -o yaml when scripting. Never rely on kubectl get pods | awk in automation — it’s a time bomb.
Real Use Case 4: CSV and TSV Without Importing a Library
Half of the data you deal with in DevOps is CSV dumps from monitoring tools, billing exports, database snapshots. In Nushell:
> open costs.csv | where service == "EC2" | sort-by cost --reverse | first 10
That’s it. open detects CSV by extension and parses it. You have typed columns, sortable, filterable.
Transforming and re-exporting:
> open costs.csv
| where cost > 100
| update cost { |row| $row.cost * 1.2 } # apply 20% markup
| to csv
| save adjusted_costs.csv
The update command takes a closure. $row.cost is a float — no bc, no python -c.
Gotcha: Nushell’s open uses the file extension to decide the parser. A CSV file named .txt will open as raw text. Either rename it or pipe through | from csv explicitly.
Real Use Case 5: System Auditing and Reporting
This is where Nushell really shines — exploratory work that you’d normally reach for Python for.
Find all files larger than 100 MB, modified in the last 7 days, owned by root:
> ls **/* | where { |f| $f.size > 100mb and $f.modified > (date now) - 7day }
The glob **/* recurses. date now returns a datetime, and you can do arithmetic on it directly.
Generate a quick disk usage report grouped by file extension:
> ls **/*
| where type == "file"
| group-by { |f| $f.name | path parse | get extension }
| items { |ext, files| { extension: $ext, total_size: ($files | math sum --column size), count: ($files | length) } }
| sort-by total_size --reverse
This is a one-liner that would take 30 lines of Python or a painful chain of du | sort | head combinations. And the result is a table you can pipe further.
Nushell Scripting: When You Need to Automate
Nushell has a real scripting language — typed functions, error handling, modules. Not just a sequence of commands.
A simple deployment health check script:
#!/usr/bin/env nu
# Check all services are responding
def check_service [name: string, url: string] {
let result = try {
http get $url --max-time 5 | ignore
{ service: $name, status: "ok" }
} catch {
{ service: $name, status: "FAILED" }
}
$result
}
let services = [
[name, url];
["api", "https://cd-linux.club/health"],
["metrics", "https://cd-linux.club:9090/-/healthy"],
["db-proxy", "https://cd-linux.club:5432"],
]
let results = $services | each { |s| check_service $s.name $s.url }
$results | table
if ($results | where status == "FAILED" | length) > 0 {
print "ALERT: Some services are down"
exit 1
}
Notice: typed function parameters (name: string), structured data in variables, try/catch, and a clean table-driven service list. This is miles ahead of Bash for anything non-trivial.
Gotcha: Nushell is not POSIX. You cannot source a Bash script into Nushell. You cannot use $() subshell syntax. If you need to call external Bash scripts, just run them as external commands — Nushell handles that fine. But don’t expect to port Bash scripts line by line.
Environment Variables and Config
Nushell’s config lives in ~/.config/nushell/config.nu and env.nu. Environment manipulation is explicit:
# env.nu
$env.PATH = ($env.PATH | split row (char esep) | prepend "~/.local/bin" | str join (char esep))
# Persistent env var
$env.EDITOR = "nvim"
The char esep is the platform’s path separator — : on Linux, ; on Windows. This is one of those things that makes Nushell scripts genuinely cross-platform without if [ "$(uname)" == "Darwin" ] soup.
Gotcha: Variables in Nushell are immutable by default. You mutate with mut:
mut counter = 0
$counter += 1
If you try let x = 0; $x += 1 you’ll get an error. This trips up every Bash veteran on day one.
Interoperability: Living in a Mixed World
You will run external commands constantly. Nushell handles this cleanly — external commands return text, and you pipe it through from X to get structure back:
# git log as a table
git log --pretty=format:"%H|%an|%s" -20
| from csv --separator "|" --noheaders
| rename hash author subject
Or wrap the whole thing in a custom command and define it in your config:
def git-log-table [] {
git log --pretty=format:"%H|%an|%ae|%s" -50
| from csv --separator "|" --noheaders
| rename hash author email subject
}
Now git-log-table | where author == "Nikita Bezmen" works anywhere.
Gotcha: When an external command fails, Nushell captures the exit code but doesn’t automatically throw an error unless you use | complete and check $in.exit_code. If you’re scripting critical paths, always handle this:
let result = git pull | complete
if $result.exit_code != 0 {
error make { msg: $"git pull failed: ($result.stderr)" }
}
Production Gotchas: The Honest List
Startup time. Nushell is slower to start than Bash. On a fast machine it’s 50-100ms — fine for interactive use, noticeable in tight loops where you’re spawning subprocesses. Don’t replace Bash in your CMD Docker entrypoints with Nushell just because you can.
Plugin ecosystem is still maturing. The plugin API works, but it’s not CPAN or npm. For specialized formats you might need to install a Nu plugin or fall back to an external tool.
Completions aren’t always there. Bash completions don’t transfer. Nushell has its own completion system and the community has written completions for common tools (git, docker, kubectl), but you may hit gaps. Write your own with extern definitions.
It’s a different language. The learning investment is real. Budget a week of friction before you’re faster than you were in Bash. It pays off, but don’t fool yourself into thinking there’s no curve.
Avoid it for /etc/shells-registered login shell on servers. If Nushell breaks or isn’t installed, you’re locked out. Keep Bash as the system login shell. Use Nushell as your interactive shell via your terminal emulator or .bashrc exec trick.
Worth It?
For interactive work: unambiguously yes. The structured pipeline model eliminates a whole class of bugs and makes exploratory data work fast. Querying JSON, filtering process lists, massaging CSV — these go from multi-step curse-laden pipelines to readable one-liners.
For scripting: yes, with caveats. Nushell scripts are more maintainable than Bash for anything over 50 lines. The type system catches bugs early. But you need to commit — half-Nushell, half-Bash codebases are confusing.
For automation on shared infrastructure: be conservative. Pin the version, document the dependency, and make sure your team is on board. Nushell on a server where only you have access is a good call. Nushell as a requirement in a shared CI system needs a conversation first.
The project is at github.com/nushell/nu — read the changelog before upgrading, the language has had breaking changes historically (though it’s stabilized significantly in the 0.9x series). The official book at nushell.sh/book is genuinely well-written and the best place to go deeper.
Shell tooling hasn’t fundamentally changed since the 1970s. Nushell is one of the few projects that actually rethinks the model instead of just adding syntax sugar. That’s worth taking seriously.