最近看《Unix 编程艺术》的时候看到一段,

也许 Unix 最持久的异议恰恰来自 Unix 哲学的一个特性,这一条特性是 X window 设计者首先明确提出的。X 致力于提供一套“机制,而不是策略”,以支持一套极端通用的图形操作,从而把使用工具箱和界面的“观感”(策略)推后到应用层。 Unix 其它系统级的服务也有类似的倾向:行为的最终逻辑被尽可能推后到使用端。Unix用户可以在多种 shell 中进行选择。而 Unix 应用程序通常会提供很多的行为选项和令人眼花缭乱的定制功能。

于是我想要尝试一些 zsh 以外的 shell。让豆包推荐了几个 shell 以后,选择了跨平台的 Nushell,作为老 Windows 用户不得不支持一下。

豆包推荐的几个里很有特色的:

  1. Nushell (Nu) — 最现代、数据型 Shell
    • 主打:像写代码一样用 Shell,结构化数据
    • 输出不是纯文本,是表格 / JSON / 对象
    • 天生支持:ls 彩色、语法高亮、自动补全
    • 跨平台:Linux/macOS/Windows 完全一致
    • 语法干净、现代,比 Bash 强太多
    • 适合:开发者、喜欢清爽结构化命令的人
  2. Xonsh — Python 风格的 Shell
    • 主打:Python + Shell 混合写
    • 直接在命令行写 Python 代码
    • 兼容 Bash 命令
    • 可高度可编程
    • 适合:Python 开发者

但是 Nushell 也有缺点:

  • Nushell 目前在高延迟 SSH 场景下,输入延迟相比 POSIX shell 高很多nushell#14474 (opens new window)),于是我在服务器上又换回了 zsh,再观望一段时间。
  • Nushell 不兼容 POSIX 标准,导致不支持 export source eval 等语法,无法直接使用 source ~/.profile 完全无痛迁移 shell 通用的配置,一些工具链配置 shell 环境的时候也会有问题。有开发者实现了一个 Nushell 用于导入其它脚本(如 ~/.profile)的变量,可以参考GitHub (opens new window)
  • 基于 bash 写的工具没法用(例如 nvm),需要用的时候还是要切回 bashzsh 来执行。
  • 基于 bash 写的命令补全工具(例如 docker systemctl)也没法用,不过可以用 Carapace 来补全这些工具的命令。
  • 目前还没有成熟的依赖/插件/模块管理器,nushell/nupm (opens new window) 还在开发中,现在一切都依靠手动下载,然后在配置文件里 usesource 来加载。没有一键脚本,这样配环境还是很麻烦。

Preview Nushell

# 安装 Nushell

  • Linux:
    • 从包管理器安装 nushell
    • 如果官方源没有的话,可以从 brew 安装:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

brew install nushell starship carapace
  • Windows:
    • 使用 Chocolatey 安装:
choco install -y nushell starship

# Nushell 启动

nu

# Linux 设置 Nushell 为默认 shell

sudo $nu.current-exe -c '$nu.current-exe + "\n" | save -a /etc/shells'
chsh -s $nu.current-exe

# Nushell 配置图标 (Starship)

  • Windows 安装:如果上面没有用 choco 安装的话,还可以用 winget 安装:winget install --id Starship.Starship。Windows 安装完以后可能需要重新启动终端。
  • Linux 安装:从包管理器安装 starship,或者从 brew 安装 starship,又或者 curl -sS https://starship.rs/install.sh | sh

安装好以后,执行下面的命令来配置 Nushell 的 Starship:

mkdir ($nu.data-dir | path join "vendor/autoload")
starship init nu | save -f ($nu.data-dir | path join "vendor/autoload/starship.nu")

'eval "$(starship init bash)"' | save -f ~/.bashrc  # 顺便配置一下 bash 的 starship

nu  # 重启 Nushell

我个人喜欢在 Git Status 里显示数量,所以在 ~/.config/starship.toml 里添加了下面的配置:

'[git_status]
disabled = false
format = '([\[$all_status$ahead_behind\]]($style) )'
style = "red bold"
stashed = '\$${count}'
ahead = "⇡${count}"
behind = "⇣${count}"
diverged = "⇕${count}"
conflicted = "=${count}"
deleted = "✘${count}"
renamed = "»${count}"
modified = "!${count}"
staged = "+${count}"
untracked = "?${count}"
' | save -f ~/.config/starship.toml

# Linux Nushell 导入其它 Shell 的环境变量

以下摘自官方文档 (opens new window)

nu 的一个常见问题是,其他应用程序将环境变量或功能导出为 shell 脚本,这些脚本期望由你的 shell 运行。 但许多应用程序只考虑最常用的 shell,如 bash 或 zsh。不幸的是,nu 与这些 shell 的语法完全不兼容,因此无法直接运行或 source 这些脚本。 通常,通过调用 zsh 本身(如果已安装)来运行 zsh 脚本没有任何障碍。但不幸的是,这将不允许 nu 访问导出的环境变量:

# 这可以工作,使用 zsh 打印 "Hello"
'echo Hello' | zsh -c $in

# 这会退出并报错,因为 $env.VAR 未定义
'export VAR="Hello"' | zsh -c $in
print $env.VAR

文档提供了一个函数来解决这个问题。它的原理是启动一个 shell 来运行脚本,捕获运行前后的环境变量,然后比较两者的差异来确定哪些变量被更改了,最后把这些更改的变量导入到 nushell 的环境中。

不过在 GitHub tesujimath/bash-env-nushell (opens new window) 上有另一个实现,下面的配置将会使用这个实现。

# Nushell 安装命令补全工具 (Carapace)

  • Windows 安装:winget install -e --id rsteube.Carapace
  • Linux 安装:从包管理器安装 carapace,或者从 brew 安装 carapace,又或者从 releases (opens new window) 下载后配置到 PATH 里。

然后在 env.nvconfig.nu 里添加下面的配置来启用 carapace(之所以要放到两个文件,是因为 Nushell 更像是编译型语言,不支持动态 source 刚创建的脚本):

## $nu.env-path
$env.CARAPACE_BRIDGES = 'zsh,fish,bash,inshellisense'
mkdir $"($nu.cache-dir)"
carapace _carapace nushell | save --force $"($nu.cache-dir)/carapace.nu"

# $nu.config-path
source $"($nu.cache-dir)/carapace.nu"

# Linux 更新 Nushell 配置文件

我的 Linux Nushell 配置文件做了几件事:

  1. 导入 Carapace 的命令补全功能,这样就可以在 Nushell 里使用 Carapace 来补全各种工具的命令了;
  2. 导入我自己编写的 module-install.nu 模块,这个模块提供了 module-install 函数,用于一键下载模块,并在配置文件中自动导入它;
  3. 导入 bash-env 模块,捕获 ~/.profile 里的环境变量,这样每次启动 Nushell 的时候就会自动导入 ~/.profile 里的环境变量了。
  4. 因为 PATH 变量在 ~/.profile 里是一个字符串,而 Nushell 期望它是一个列表,所以这里做了特殊处理,把它转换成列表。
  5. 手动设置了 LANGLC_ALL 变量,解决了 Nushell 的 locale 错误问题。

Windows 没有遇到这些问题,所以就不需要更新配置文件了,保持默认配置。

由于涉及到多个文件的配置,下面的命令由上往下执行。


下载 module-install.nu 函数以及 bash-env.nu 模块(以及依赖的 bash-env-json 脚本):

cd $nu.default-config-dir
mkdir scripts modules ~/.local/bin
http get https://public.lyh543.cn/setup-server/nushell/module-install.nu | save -f scripts/module-install.nu
http get https://raw.githubusercontent.com/tesujimath/bash-env-nushell/refs/heads/main/bash-env.nu | save -f modules/bash-env.nu
http get https://raw.githubusercontent.com/tesujimath/bash-env-json/refs/heads/main/bash-env-json | save -f ~/.local/bin/bash-env-json
chmod a+x ~/.local/bin/bash-env-json

更新 config.nu 来加载 bash-env.nu 模块,并从 ~/.profile 导入环境变量:

'# 加载 base-env.nu 来从 .profile 中提取环境变量
$env.PATH ++= [($env.HOME | path join ".local/bin")]
$env.NUSHELL_IMPORT_BASH_ENV = 1  # 在 ~/.profile 里可以用这个环境变量来判断是否在 Nushell 里,从而跳过一些不必要的环境变量设置
use modules/bash-env.nu
if ('~/.profile' | path exists) {bash-env ~/.profile | load-env}
hide-env NUSHELL_IMPORT_BASH_ENV  # 导入完以后就删除这个环境变量
# PATH 变量需要特殊处理,因为它是一个字符串,而 nushell 期望它是一个列表
if (($env.PATH | describe) == 'string') { $env.PATH = $env.PATH | split row (char esep) }  

$env.LANG = "zh_CN.UTF-8"
$env.LC_ALL = "zh_CN.UTF-8"

source $"($nu.cache-dir)/carapace.nu"
source scripts/module-install.nu
' | save -f $nu.config-path

'$env.CARAPACE_BRIDGES = 'zsh,fish,bash,inshellisense'
mkdir $"($nu.cache-dir)"
carapace _carapace nushell | save --force $"($nu.cache-dir)/carapace.nu"
' | save -f $nu.env-path

然后重新启动 nu,就可以看到 PATH 被正确地导入了:

     __  ,
 .--()°'.' Welcome to Nushell,
'|, . ,'   based on the nu language,
 !_-(_\    where all data is structured!

Version: 0.110.0 (x86_64-unknown-linux-gnu)
Please join our Discord community at https://discord.gg/NtAbbGn
Our GitHub repository is at https://github.com/nushell/nushell
Our Documentation is located at https://nushell.sh
And the Latest Nushell News at https://nushell.sh/blog/
Learn how to remove this at: https://nushell.sh/book/configuration.html#remove-welcome-message

It's been this long since Nushell's first commit:
6yrs 9months 8days 22hrs 28mins 30secs 150ms 302µs 349ns

Startup Time: 361ms 878µs 756ns


liu in 🌐 Home-Server in ~
❯ echo $env.PATH
╭────┬──────────────────────────────────────────────────╮
│  0 │ /home/liu/.cargo/bin                             │
│  1 │ /home/liu/.local/lib/go/bin                      │
│  2 │ /home/liu/.fvm_flutter/bin                       │
│  3 │ /home/liu/.pyenv/shims                           │
│  4 │ /home/liu/.pyenv/bin                             │
│  5 │ /home/liu/.nvm/versions/node/v22.12.0/bin        │
│  6 │ /usr/lib/jvm/default/bin                         │
│  7 │ /usr/local/sbin                                  │
│  8 │ /usr/local/bin                                   │
│  9 │ /usr/bin                                         │
│ 10 │ /home/liu/.local/bin                             │
│ 11 │ /home/liu/.local/bin                             │
│ 12 │ /home/liu/git/github/dev-tools/shell             │
│ 13 │ /home/liu/.pnpm-global/bin                       │
│ 14 │ /home/liu/.yarn/bin                              │
│ 15 │ /home/liu/.pub-cache/bin                         │
│ 16 │ /home/liu/.local/share/JetBrains/Toolbox/scripts │
│ 17 │ /home/liu/.local/lib/Android/Sdk/platform-tools  │
╰────┴──────────────────────────────────────────────────╯