Nushell: A Modern Shell That Treats Data as Structured Tables
Nushell: A Modern Shell That Treats Data as Structured Tables
Nushell (Nu) is a shell that treats everything as structured data. Instead of piping text between commands and parsing it with grep, awk, and cut, Nu pipelines pass tables, records, and lists. The result is a shell where data manipulation is readable, composable, and far less error-prone than traditional text munging.

If you have ever written a Bash one-liner that chains grep | awk | sed | cut and then had to debug it when a field contained a space, Nushell solves that entire class of problem.
Installation
# macOS
brew install nushell
# Linux (cargo)
cargo install nu
# Linux (various package managers)
# Fedora
sudo dnf install nushell
# Arch
sudo pacman -S nushell
# Via mise
mise use nu@latest
After installation, run nu to start a Nushell session. To make it your default shell:
# Add to /etc/shells if not already there
echo $(which nu) | sudo tee -a /etc/shells
# Set as default
chsh -s $(which nu)
The Core Concept: Structured Pipelines
In Bash, ls outputs text. In Nushell, ls outputs a table:
> ls
╭───┬──────────────────┬──────┬──────────┬──────────────╮
│ # │ name │ type │ size │ modified │
├───┼──────────────────┼──────┼──────────┼──────────────┤
│ 0 │ Cargo.toml │ file │ 1.2KB │ 2 hours ago │
│ 1 │ README.md │ file │ 3.4KB │ 1 day ago │
│ 2 │ src │ dir │ 4.1KB │ 30 mins ago │
│ 3 │ tests │ dir │ 2.0KB │ 1 week ago │
╰───┴──────────────────┴──────┴──────────┴──────────────╯
This is not just pretty formatting. The output is a real table with typed columns. You can filter, sort, and transform it:
# Files larger than 1KB, sorted by size
> ls | where size > 1kb | sort-by size --reverse
# Just the names
> ls | get name
# Files modified in the last hour
> ls | where modified > (date now | $in - 1hr)
Compare this to the Bash equivalent:
# Bash: files larger than 1KB sorted by size (fragile, locale-dependent)
ls -la | awk '$5 > 1024 {print $5, $9}' | sort -rn
The Nushell version is readable and handles edge cases (filenames with spaces, different locales) correctly because it operates on structured data, not text patterns.
Essential Commands
Working with Tables
# Filter rows
> ps | where cpu > 5.0
# Select columns
> ps | select name cpu mem
# Sort
> ps | sort-by mem --reverse | first 10
# Group and aggregate
> ls **/*.rs | group-by type | transpose key count
# Unique values
> ls | get type | uniq
Working with Files
Nushell can open structured files natively:
# JSON
> open package.json | get dependencies
# TOML
> open Cargo.toml | get package.version
# YAML
> open docker-compose.yml | get services | columns
# CSV
> open data.csv | where revenue > 1000 | math sum revenue
# SQLite
> open database.sqlite | query db "SELECT * FROM users WHERE active = 1"
Every file format produces structured data that feeds into the same pipeline operators. No more jq for JSON, yq for YAML, and csvtool for CSV.
String Operations
# Split and manipulate
> "hello-world-foo" | split row "-"
╭───┬───────╮
│ 0 │ hello │
│ 1 │ world │
│ 2 │ foo │
╰───┴───────╯
# String interpolation
> let name = "world"
> $"Hello, ($name)!"
Hello, world!
# Regex
> "2026-02-14" | parse "{year}-{month}-{day}"
╭───┬──────┬───────┬─────╮
│ # │ year │ month │ day │
├───┼──────┼───────┼─────┤
│ 0 │ 2026 │ 02 │ 14 │
╰───┴──────┴───────┴─────╯
HTTP and APIs
# Fetch JSON from an API
> http get https://api.github.com/repos/nushell/nushell | select stargazers_count forks open_issues
# POST with data
> http post https://httpbin.org/post { name: "test", value: 42 }
Configuration
Nushell's config lives at ~/.config/nushell/config.nu (and env.nu for environment setup):
# config.nu
# Appearance
$env.config = {
show_banner: false
table: {
mode: rounded
index_mode: auto
}
completions: {
case_sensitive: false
quick: true
partial: true
algorithm: "fuzzy"
}
history: {
max_size: 100000
file_format: "sqlite"
}
keybindings: [
{
name: "fuzzy_history"
modifier: control
keycode: char_r
mode: [emacs, vi_normal, vi_insert]
event: {
send: ExecuteHostCommand
cmd: "commandline edit --replace (
history
| get command
| reverse
| uniq
| str join (char -i 0)
| fzf --read0 --layout=reverse --height=40%
| decode utf-8
| str trim
)"
}
}
]
}
Environment Setup
# env.nu
# PATH
$env.PATH = ($env.PATH | split row (char esep) | prepend [
$"($env.HOME)/.local/bin"
$"($env.HOME)/.cargo/bin"
])
# Prompt
$env.PROMPT_COMMAND = {||
let path = ($env.PWD | path relative-to $env.HOME | $"~/($in)")
let git_branch = (do { git branch --show-current } | complete | get stdout | str trim)
if ($git_branch | is-empty) {
$"(ansi green)($path)(ansi reset) > "
} else {
$"(ansi green)($path)(ansi reset) (ansi cyan)($git_branch)(ansi reset) > "
}
}
Scripting
Nushell scripts use the .nu extension and have proper language features:
#!/usr/bin/env nu
# Functions with typed parameters
def greet [name: string, --excited(-e)] -> string {
if $excited {
$"Hello, ($name)!!!"
} else {
$"Hello, ($name)."
}
}
# Error handling
def safe-read [path: path] {
try {
open $path
} catch {
print $"Error reading ($path): ($in)"
null
}
}
# Loops and conditionals
def find-large-files [dir: path, min_size: filesize = 10mb] {
ls ($dir | path join "**/*")
| where type == "file" and size > $min_size
| sort-by size --reverse
| select name size modified
}
# Call it
find-large-files /home/user/projects 5mb
Custom Commands
Add reusable commands to your config:
# Add to config.nu
# Quick project stats
def project-stats [] {
let files = (glob **/*.{ts,js,py,rs,go} | length)
let lines = (glob **/*.{ts,js,py,rs,go} | each { open $in | lines | length } | math sum)
let git_commits = (git rev-list --count HEAD | into int)
{
files: $files
lines: $lines
commits: $git_commits
}
}
# Docker cleanup
def docker-cleanup [] {
docker system prune -af --volumes
docker system df
}
# Quick HTTP server
def serve [port: int = 8080] {
python3 -m http.server $port
}
Nushell vs Other Shells
vs Bash
| Aspect | Bash | Nushell |
|---|---|---|
| Data model | Text streams | Structured tables |
| Error handling | Exit codes, set -e |
Try/catch, typed errors |
| Scripting | Fragile, quoting hell | Proper types, no quoting issues |
| Pipeline debugging | set -x, echo |
Pipeline steps are inspectable |
| External tool compat | Native | Runs everything, wraps output |
| POSIX compliance | Yes | No |
| Server scripts | Standard everywhere | Needs installation |
Bash is ubiquitous and every server has it. Nushell is a better interactive shell and scripting language but requires installation. Use Bash for server automation scripts that need to run everywhere. Use Nushell for interactive work and project-specific scripts.
vs Zsh
Zsh improved on Bash with better completion, globbing, and plugin support (Oh My Zsh). But it still operates on text streams. If you have a working Zsh setup with plugins you rely on, the migration cost to Nushell is significant. The payoff is cleaner data pipelines and scripting.
vs Fish
Fish and Nushell share a philosophy: shells should be user-friendly by default. Fish has better out-of-the-box completions and syntax highlighting. Nushell has the structured data model. Fish is a more conservative improvement over Bash; Nushell is a rethinking of what a shell should be.
Practical Workflow Examples
Log Analysis
# Parse nginx access logs
> open access.log
| lines
| parse '{ip} - - [{date}] "{method} {path} {proto}" {status} {size}'
| where status != "200"
| group-by status
| transpose status count
| sort-by count --reverse
Git Workflow
# Files changed in last 5 commits, grouped by extension
> git log --oneline -5 --name-only --pretty=format:""
| lines
| where ($it | is-not-empty)
| path parse
| group-by extension
| transpose ext files
| sort-by {|r| $r.files | length} --reverse
System Monitoring
# Top memory consumers
> ps | sort-by mem --reverse | first 10 | select name pid mem cpu
# Disk usage by directory
> du /home/user --max-depth 1 | sort-by apparent --reverse | first 10
Limitations and Gotchas
POSIX incompatibility: Nushell is not a POSIX shell. You cannot source .bashrc files, use Bash syntax (if [ -f file ], $(command)), or rely on POSIX utilities behaving exactly the same way. Shell scripts written for Bash will not run in Nu.
External command output: When you run an external command (like git), its output comes in as text, not structured data. You need to parse it. Nushell provides helpers (lines, parse, split column), but you lose the structured advantage when shelling out.
Plugin ecosystem: Smaller than Bash/Zsh/Fish. No equivalent to Oh My Zsh's extensive plugin collection. The built-in functionality is comprehensive, but niche integrations may require manual scripting.
Learning curve: Nushell's syntax is different enough from Bash that muscle memory works against you initially. $env.PATH instead of $PATH. if $condition { } instead of if [ $condition ]; then fi. Plan for a week of adjustment.
Despite these limitations, Nushell represents the most interesting rethinking of the Unix shell in decades. If you spend significant time in the terminal manipulating data, the structured pipeline model is transformative once it clicks.