Using_nushell_with_nixos

Nushell and NixOS

nushell logo
  • I recently switched default shells from zsh to nushell, this post is about some of the challenges and advantages of using nushell with NixOS.

  • Nushell borrows concepts from many shells and languages and is itself both a programming language and a shell. Because of this, it has it’s own way of working with files, directories, websites, and more.

  • Nushell is powerful and has many essential commands built directly into the shell (“internal” commands) rather than a link to an executable. You can use this set of commands across different operating systems, having this consistency is helpful when creating cross-platform code.

ls        # Internal command
^ls       # External command (typically /usr/bin/ls)
  • The Reedline-Editor is powerful and provides good vi-mode or emacs support built in.

  • It’s default Ctrl+r history command is nice and structured.

  • Nushell has helpful rust like error messages

  • Fundamentally designed as a structured data scripting language: and then it acts as a shell on top of that foundation. This “language first” approach is what gives it many of its distinctive features and makes it a powerful scripting language.A few of those features are:

    • Pipelines of structured data: Unlike traditional shells that primarily deal with plain text streams, Nushell pipelines operate on tables of structured data. Each command can understand and manipulate this structured data directly.

    • Consistent syntax: Its syntax is more consistent and predictable compared to the often quirky syntax of Bash and Zsh, drawing inspiration from other programming languages.

    • Strong typing Nushell has a type system, which helps catch errors early and allows for more robust scripting.

    • First-class data types: It treats various data formats (like JSON, CSV, TOML) as native data types, making it easier to work with them. Because of this, Nushell aims to replace the need for external tools like jq, awk, sed, cut, and even some uses of grep and curl.

  • Variables are Immutable by Default: Nushell’s commands are based on a functional-style of programming which requires immutability, sound familiar?

  • Nushell’s Environment is Scoped: Nushell takes many design cues from compiled languages, one is that languages should avoid global mutable state. Shells have commonly used global mutation to update the environment, Nushell attempts to steer clear of this increasing reproducability.

  • Single-use Environment Variables:

FOO=BAR $env.FOO
# => BAR
  • Permanent Environment Variables: In your config.nu
# config.nu
$env.FOO = 'BAR'

The Challenges

  • There are many similarities so it can be easy to forget that some Bash (and POSIX in general) style constructs just won’t work in Nushell. Considering that NixOS seems to have been designed for bash, even Zsh isn’t fully compatable you may want to think twice before you choose Nushell as your default.

  • The documentation is incomplete, it’s not as mature as other shells including fish.

  • && doesn’t work use ; instead.

  • > is used as the greater-than operator for comparisons:

"hello" | save output.txt

is equivalent to the following in bash:

echo "hello" > output.txt
  • If you notice above the nushell command doesn’t require an echo prefix, this is because Nushell has Implicit Return:
"Hello, World" == (echo "Hello, World")
# => true
  • The above example shows that the string, "Hello, World" is equivalent to the output value from echo "Hello, World"

  • Every Command Returns a Value:

let p = 7
print $p  # 7
$p * 6    # 42
  • Multi-Line Editing:

  • When writing a long command you can press Enter to add a newline and move to the next line. For example:

ls            |    # press enter
where name =~ |    # press enter, comments after pipe ok
get name      |    # press enter
mv ...$in ./backups/
  • This allows you to cycle through the entire multi-line command using the up and down arrow keys and then customize different lines or sections of the command.

  • You can manually insert a newline using Alt+Enter or Shift+Enter.

Key Differences Between Nushell & Bash

FeatureBash (Dynamic)Nushell (Static)
Code ExecutionLine-by-lineWhole script parsed first
Error DetectionRuntime errors onlyCatches errors before running
Support for eval✅ Allowed❌ Not supported
Custom ParsingLimitedBuilt-in semantic analysis
IDE FeaturesBasic syntax highlightingAdvanced integration, linting, and formatting

Nushell Showcase

Custom Nushell Commands

  • The following command allows you to choose which input to update interactively with fzf.
# nix.nu
# upgrade system packages
# `nix-upgrade` or `nix-upgrade -i`
def nix-upgrade [
  flake_path: string = "/home/jr/flake", # path that contains a flake.nix
  --interactive (-i) # select packages to upgrade interactively
]: nothing -> nothing {
  let working_path = $flake_path | path expand
  if not ($working_path | path exists) {
    echo "path does not exist: $working_path"
    exit 1
  }
  let pwd = $env.PWD
  cd $working_path
  if $interactive {
    let selections = nix flake metadata . --json
    | from json
    | get locks.nodes
    | columns
    | str join "\n"
    | fzf --multi --tmux center,20%
    | lines
    # Debug: Print selections to verify
    print $"Selections: ($selections)"
    # Check if selections is empty
    if ($selections | is-empty) {
      print "No selections made."
      cd $pwd
      return
    }
    # Use spread operator to pass list items as separate arguments
    nix flake update ...$selections
  } else {
    nix flake update
  }
  cd $pwd
  nh os switch $working_path
}
carapace
  • The ns command is designed to search for Nix packages using nix search and present the results in a cleaner format, specifically removing the architecture and operating system prefix that nix search often includes.
def ns [
    term: string # Search target.
] {

    let info = (
        sysctl -n kernel.arch kernel.ostype
        | lines
        | {arch: ($in.0|str downcase), ostype: ($in.1|str downcase)}
    )

    nix search --json nixpkgs $term
        | from json
        | transpose package description
        | flatten
        | select package description version
        | update package {|row| $row.package | str replace $"legacyPackages.($info.arch)-($info.ostype)." ""}
}
nix search
  • nufetch command:
# `nufetch` `(nufetch).packages`
def nufetch [] {
{
"kernel": $nu.os-info.kernel_version,
"nu": $env.NU_VERSION,
"packages": (ls /etc/profiles/per-user | select name | prepend [[name];["/run/current-system/sw"]] | each { insert "number" (nix path-info --recursive ($in | get name) | lines | length) | insert "size" ( nix path-info -S ($in | get name) | parse -r '\s(.*)' | get capture0.0 | into filesize) | update "name" ($in | get name | parse -r '.*/(.*)' | get capture0.0 | if $in == "sw" {"system"} else {$in}) | rename "environment"}),
"uptime": (sys host).uptime
}
}
nufetch
  • duf command, I have mine aliased to df:
duff
  • ps command:
ps
  • Adding the following to your configuration.nix will show you the diff of the closures on rebuild:
# configuration.nix
# During system activation, compare the closure size difference between the
# current and new system and display a formatted table if significant changes are
# detected.
system.activationScripts.diff = ''
  if [[ -e /run/current-system ]]; then
    ${pkgs.nushell}/bin/nu -c "let diff_closure = (${pkgs.nix}/bin/nix store
     diff-closures /run/current-system '$systemConfig'); let table =
     (\$diff_closure | lines | where \$it =~ KiB | where \$it =~ → | parse -r
     '^(?<Package>\S+): (?<Old>[^,]+)(?:.*) → (?<New>[^,]+)(?:.*), (?<DiffBin>.*)$'
     | insert Diff { get DiffBin | ansi strip | into filesize } | sort-by -r Diff
     | reject DiffBin); if (\$table | get Diff | is-not-empty) { print \"\"; \$table
    | append [[Package Old New Diff]; [\"\" \"\" \"\" \"\"]] | append [[Package Old
     New Diff]; [\"\" \"\" \"Total:\" (\$table | get Diff | math sum) ]]
    | print; print \"\" }"
  fi
'';
obsidian
  • nix-list-system command lists all installed packages:
# list all installed packages
def nix-list-system []: nothing -> list<string> {
  ^nix-store -q --references /run/current-system/sw
  | lines
  | filter { not ($in | str ends-with 'man') }
  | each { $in | str replace -r '^[^-]*-' '' }
  | sort
}
obsidian

Using Just and Justfiles

  • The following is my justfile that I keep right next to my flake.nix it simplifies some commands and makes things work that weren’t working with nushell for my case, you’ll have to change it to match your configuration. It’s not perfect but works for my use case, take whats useful and leave the rest.

  • You’ll first need to install just to make use of justfiles.

# nix shell nixpkgs#just nixpkgs#nushell
set shell := ["nu", "-c"]
flake_path := "/home/jr/flake"
hostname := "magic"
home_manager_output := "jr@magic"

utils_nu := absolute_path("utils.nu")

default:
    @just --list
# Rebuild
[group('nix')]
fr:
    nh os switch --hostname {{hostname}} {{flake_path}}

# Flake Update
[group('nix')]
fu:
    nh os switch  --hostname {{hostname}} --update {{flake_path}}

# Update specific input
# Usage: just upp nixpkgs
[group('nix')]
upp input:
    nix flake update {{input}}
# Test
[group('nix')]
ft:
    nh os test --hostname {{hostname}} {{flake_path}}
# Collect Garbage
[group('nix')]
ncg:
    nix-collect-garbage --delete-old ; sudo nix-collect-garbage -d ; sudo /run/current-system/bin/switch-to-configuration boot

[group('nix')]
cleanup:
    nh clean all

# Clean
[group('nix')]
clean:
    sudo nix profile wipe-history --profile /nix/var/nix/profiles/system --older-than 3d
# Upgrade
[group('nix')]
upd:
    nh os switch -u {{flake_path}} ; nh os switch --hostname {{hostname}} {{flake_path}}

[group('nix')]
eval:
    nix-instantiate --eval --json --strict | jq
# Nix Repl flake:nixpkgs
[group('nix')]
repl:
    nix repl -f flake:nixpkgs

# format the nix files in this repo
[group('nix')]
fmt:
    nix fmt

# Show all the auto gc roots in the nix store
[group('nix')]
gcroot:
    ls -al /nix/var/nix/gcroots/auto/

# Verify all store entries
[group('nix')]
verify-store:
    nix store verify --all


[group('nix')]
repair-store *paths:
    nix store repair {{paths}}

# Usage: `./result/bin/run-*-vm`
# may need to set initialHashedPassword first
[group('nix')]
vm:
    sudo nixos-rebuild build-vm


system-info:
     "This is an {{arch()}} machine"

running:
    ps | where status == Running

help:
    help commands | explore


# =================================================
#
# Other useful commands
#
# =================================================

[group('common')]
path:
   $env.PATH | split row ":"

[group('common')]
trace-access app *args:
  strace -f -t -e trace=file {{app}} {{args}} | complete | $in.stderr | lines | find -v -r "(/nix/store|/newroot|/proc)" | parse --regex '"(/.+)"' | sort | uniq

[linux]
[group('common')]
penvof pid:
  sudo cat $"/proc/($pid)/environ" | tr '\0' '\n'

# Remove all reflog entries and prune unreachable objects
[group('git')]
ggc:
  git reflog expire --expire-unreachable=now --all
  git gc --prune=now

# Amend the last commit without changing the commit message
[group('git')]
game:
  git commit --amend -a --no-edit

[group('git')]
push:
    git push -u origin main

# Delete all failed pods
[group('k8s')]
del-failed:
  kubectl delete pod --all-namespaces --field-selector="status.phase==Failed"

[linux]
[group('services')]
list-inactive:
  systemctl list-units -all --state=inactive

[linux]
[group('services')]
list-failed:
  systemctl list-units -all --state=failed

[linux]
[group('services')]
list-systemd:
  systemctl list-units systemd-*

# List journal
[linux]
[group('services')]
jctl:
  ^jctl = "journalctl -p 5 -xb";
  • To list available commands type, (you must be in the same directory as the justfile): just
just
  • So just list-failed will list any failed systemd services for example.

  • A lot of the .nu files came from this repo by BlindFS:

    • modern-dot-files he uses Nix Darwin so there are many changes for NixOS.

    • my-nu-config Warning, it’s very complex and hard to understand. Just know that from my shells directory I import the nushell directory which contains a default.nix which is the entrypoint for this configuration. The default.nix has configFile.source = ./config.nu; which integrates all the .nu files. I know it’s a mess, I’ll refactor shortly.

    • The examples use my-starship-config the logic at the end works for bash, zsh, and nushell.

    • If you wan’t to use my config you’ll have to enable the experimental-feature pipe-operators in the same place you enable flakes and nix-command.

  • There are still situations where I need to switch to zsh or bash to get something to work i.e. nix-shell and a few others.

Resources