← All articles
TERMINAL Nushell: A Modern Shell That Treats Data as Structur... 2026-02-14 · 7 min read · nushell · shell · terminal

Nushell: A Modern Shell That Treats Data as Structured Tables

Terminal 2026-02-14 · 7 min read nushell shell terminal bash pipelines cli

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.

Nushell logo

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.