最近看《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 不兼容 POSIX 标准,导致不支持 export source eval 等语法,无法直接使用 source ~/.profile 完全无痛迁移 shell 通用的配置,一些工具链配置 shell 环境的时候也会有问题。Nushell 文档里提供了一个函数来导入其它脚本的变量,可以参考官方文档 (opens new window)

Preview Nushell

# 安装 Nushell

  • Linux:从包管理器安装 nushell,如果官方源没有的话,可以从 brew 安装 brew install nushell starship
  • Windows:choco install -y nushell starship,或者从 GitHub (opens new window) 下载。安装以后重启终端,Nushell 就出现在终端 App 的选项里了。

# Nushell 启动

nu

# Linux 设置 Nushell 为默认 shell

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

# 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 的环境中。

# Linux 更新 Nushell 配置文件

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

  1. 通过文档提供的函数 capture-foreign-env,捕获 ~/.profile 的内容,追加到了 $nu.config-path 里,这样每次启动 Nushell 的时候就会自动导入 ~/.profile 里的环境变量了。
  2. 因为 PATH 变量在 ~/.profile 里是一个字符串,而 Nushell 期望它是一个列表,所以这里做了特殊处理,把它转换成列表。
  3. 手动设置了 LANGLC_ALL 变量,解决了 Nushell 的 locale 错误问题。

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

下面的命令行直接把这些配置写入了 $nu.config-path

'def capture-foreign-env [
    --shell (-s): string = /bin/sh
    # 运行脚本的 shell
    #(必须支持 '-c' 参数和 POSIX 'env'、'echo'、'eval' 命令)
    --arguments (-a): list<string> = []
    # 传递给外部 shell 的额外命令行参数
] {
    let script_contents = $in;
    let env_out = with-env { SCRIPT_TO_SOURCE: $script_contents } {
        ^$shell ...$arguments -c `
        env
        echo '<ENV_CAPTURE_EVAL_FENCE>'
        eval "$SCRIPT_TO_SOURCE"
        echo '<ENV_CAPTURE_EVAL_FENCE>'
        env -0 -u _ -u _AST_FEATURES -u SHLVL` # 过滤掉已知的更改变量
    }
    | split row '<ENV_CAPTURE_EVAL_FENCE>'
    | {
        before: ($in | first | str trim | lines)
        after: ($in | last | str trim | split row (char --integer 0))
    }

    # 不幸的假设:
    # 没有更改的环境变量包含换行符(无法干净解析)
    $env_out.after
    | where { |line| $line not-in $env_out.before } # 只获取更改的行
    | parse "{key}={value}"
    | transpose --header-row --as-record
    | if $in == [] { {} } else { $in }
}

if ('~/.profile' | path exists) {load-env (open ~/.profile | capture-foreign-env --shell bash)}
if (($env.PATH | describe) == 'string') { $env.PATH = $env.PATH | split row (char esep) }  # PATH 变量需要特殊处理,因为它是一个字符串,而 nushell 期望它是一个列表

$env.LANG = "zh_CN.UTF-8"
$env.LC_ALL = "zh_CN.UTF-8"
' | save -f $nu.config-path

# Nushell 配置图标 (Starship)

  • Windows 安装:如果上面没有用 choco 安装的话,还可以用 winget 安装: winget install --id Starship.Starship。Windows 安装完以后可能需要重新启动终端。
  • Linux 安装:从包管理器安装 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