在 Windows 上搞 Terminal 开发环境这件事,我折腾了大半年,最后定下来的配套组合是 Windows Terminal + Nushell + Fresh + coreutils。这套搭配解决了很多实际痛点:默认的 cmd 和 PowerShell 在写脚本、处理文本、管理路径时都差点意思,而 Windows Terminal 只解决了“窗口好看”,真正让命令行用起来顺手,还得靠 shell 本身和命令行工具链。这篇文章就把我的完整方案、踩坑记录和最终配置一次性分享出来,适合那些想在 Windows 上获得类 Unix 开发体验,但又不想完全依赖 WSL 虚拟层的朋友。
先说结论:Nushell 负责日常交互和数据处理,coreutils 补齐了 Windows 缺失的常用 Unix 命令,Fresh 把散落各处的 shell 配置管起来,Windows Terminal 则是承载这一切的界面底座。四者各管一摊,组合起来比单独用其中任何一个都舒服得多。
1. 为什么我放弃了默认终端,转向这套组合
1.1 cmd 和 PowerShell 的局限
先说 cmd。它能做的也就是切目录、跑个 exe,文本处理基本靠 findstr 和 for 循环,写复杂一点逻辑就像在刀耕火种。PowerShell 虽然强大,但它的语法有很强的“历史包袱”,别名系统混乱,管道传的是对象而不是文本流,这本来是优点,可当你习惯了grep、awk、jq这些 Unix 思维工具之后,再回到 PowerShell 里写Get-Content、Where-Object,总觉得绕了一大圈。
还有个很实际的问题:PowerShell 在执行一些命令时会被执行策略挡住,脚本编码格式不对也会出现中文乱码。做 Web 开发的人经常要查端口、杀进程、看日志,这些操作在 PowerShell 里每次都要记 cmdlet 的名字,时间成本非常高。
1.2 这套组合解决的问题
我需要的其实是一个“像 Linux 终端一样顺手”的 Windows 环境,但又不希望为此专门开一台虚拟机或者每天面对 WSL 的文件 IO 损耗。于是我把问题拆成了三层:
- 界面层:需要支持多标签、深色主题、好看的字体渲染,这就是 Windows Terminal 的活。
- 交互层:需要一个语法现代、自带数据结构的 shell,Nushell 恰好符合。
- 命令层:需要补齐
ls、cat、cp、grep、find这些每天都在用的命令,coreutils 就是干这个的。 - 配置层:环境一旦复杂起来,初始化脚本、别名、环境变量就变成一团乱麻,Fresh 可以把这些配置变成可复现的仓库。
说白了,这套组合是针对“纯 Windows 原生开发”场景的,不是要替代 WSL,而是让不依赖 Linux 工具链的那些日常工作也能有很好的体验。试过之后,我觉得这套方案完全可以作为 Windows 上的主力开发环境。
2. 工具各自的分工:Nushell 负责交互,coreutils 补齐命令库,Fresh 管配置
2.1 Nushell:把终端变成数据分析工具
Nushell(简称 nu)是用 Rust 写的现代 shell,它的核心思想是“一切皆数据”。比如ls的输出不是一段普通文本,而是一张结构化的表格,你可以直接对这个表格做where size > 1mb、sort-by modified、select name这类操作。这比传统 shell 里用awk截取列要直观得多,也比 PowerShell 的对象管道更轻快。
我第一次用 nu 的时候,最震撼的是它的管道简直像 SQL。举个例子:
ls | where type == file | sort-by size | reverse | select name size | first 10这条命令在一个 shell 里查当前目录最大的 10 个文件,不用 awk 不用 sort,全是内置命令。配合open、from json、to json这些命令,处理 API 返回的 JSON 数据也变得非常自然。做开发调试时,我经常用它分析日志文件、整理端口信息,效率比之前高一大截。
nu 还有一个很实际的好处:它的报错信息写得非常清楚,不像 bash 那样动不动就command not found敷衍了事。语法错误会直接提示位置和修改建议,对新手也友好。
2.2 coreutils:Windows 缺失的 Unix 命令库
Windows 自带的命令和 Unix 工具集的差距,用过的人都知道。curl在 PowerShell 里是Invoke-WebRequest的别名,grep得用findstr,wc根本没有对应物,tree的参数也不兼容。coreutils 就是来填这个坑的。
我推荐安装 uutils/coreutils,这是 Rust 重写的跨平台版本,Windows 支持非常完善,而且安装简单。装完之后,ls、rm、cp、mv、cat、grep、find、du、df、xargs这些命令全部可用,路径分隔符、参数风格都和 Linux 高度一致。于是我在 Windows 上写脚本时,不用再纠结“这个命令在 Windows 里叫什么”,直接按 Unix 习惯写就行。
不过要注意,uutils 覆盖的是 GNU coreutils 的功能,像git、curl、jq这种不属于 coreutils 的还是要单独安装。我习惯把 Git for Windows 和 coreutils 一起装上,Git 自带的usr/bin里也有不少 Unix 工具,两者配合使用基本能覆盖日常需求。
2.3 Fresh:让一堆配置文件不再失控
Fresh 原本是 fish shell 的配置管理工具,但它的核心机制其实是一个基于 git 仓库的 dotfiles 管理器。你可以在一个仓库里声明需要管理的配置文件、需要安装的插件和需要执行的初始化逻辑,然后用一条fresh命令把它们同步到本地。这对我这种同时维护多台开发机的人来说是刚需。
在我这套方案里,Fresh 的角色不是替代 Nushell 的配置系统,而是作为“配置仓库的入口”存在。每次在 nu 里改了config.nu、env.nu,或者给 fish(备用 shell)加了一个函数,就统一提交到 dotfiles 仓库,通过 Fresh 推到机器上同步。这样重装系统或者换新电脑时,一条命令就能把 shell 环境恢复原样,不需要再从收藏夹里翻安装教程。
很多 Windows 用户觉得 Fresh 是给 fish 用户准备的,跟自己无关。其实不用 fish 也可以用 Fresh 管理 nu 的配置,只要把仓库结构设计好就行。我下面会讲具体的目录规划。
3. 从零开始:安装与配置全流程
3.1 第一步:用 winget 安装 Windows Terminal
Windows 10 1809 之后可以直接用 winget 安装新版终端:
winget install --id Microsoft.WindowsTerminal -e装完之后建议把 Windows Terminal 设为默认终端应用,在它的设置界面里把“默认终端应用程序”从“Windows 控制台主机”改成“Windows Terminal”。这样以后按Win + R输入cmd或wt时,打开的都会是 Windows Terminal。
Windows Terminal 本身不提供 shell,它只是一个壳。接下来的重点是往里面塞一个顺手的 shell。
3.2 第二步:通过 Scoop 安装 Nushell、fish 与 coreutils
包管理器我选了 Scoop,它对非管理员用户非常友好,所有软件都装在用户目录下,不会污染系统。先装 Scoop:
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser irm get.scoop.sh | iex然后安装核心组件:
scoop install nu scoop install fish scoop install uutils-coreutils这里说一下选择:scoop install nu安装的是 Nushell 的主程序;fish是 Fresh 的运行依赖,因为 Fresh 的引导脚本面向 fish shell,而且我把 fish 当作备用 shell,在 nu 偶尔出现兼容性问题时还能顶上;uutils-coreutils提供完整的 Unix 命令集合。
装完之后可以用nu命令进入 Nushell,输入ls看输出是不是表格形式,再验证一下 coreutils 是否生效:
ls | where name == "config.nu"如果不报错,说明 nu 的默认配置目录已经生成了,一般在~\AppData\Roaming\nushell\下。
3.3 第三步:安装 Fresh 并初始化配置仓库
Fresh 的官方安装方式依赖 fish,所以先用 fish 执行引导脚本:
curl -L https://get.freshshell.com | fish安装完成后,Fresh 会在~/.fresh/目录下建立一个源码仓库,并在~/.freshrc文件中记录需要管理的配置项。这里要理解一下.freshrc的作用:它可以声明“从哪个仓库管理哪些文件”,类似一个入口清单。
我的~/.freshrc内容大致是:
# 配置文件来源仓库 set fish_function_path ~/.fresh/source/oh-my-fish/plugins # 本地 dotfiles 仓库 echo "source ~/dotfiles/config.fish" >> ~/.config/fish/config.fish link ~/dotfiles/nushell/config.nu ~/AppData/Roaming/nushell/config.nu link ~/dotfiles/nushell/env.nu ~/AppData/Roaming/nushell/env.nulink是 Fresh 的关键命令,它会在本地和仓库文件之间建立符号链接。这样你改本地文件,git 仓库会自动感知变更;在另一台机器上 clone 仓库后,执行fresh就能把配置链接到正确位置。
3.4 第四步:把 Nushell 配置纳入 Fresh 管理
先找到 Nushell 实际使用的配置文件路径。在 nu 里执行:
$nu.config-path $nu.env-path常见结果是~\AppData\Roaming\nushell\config.nu和env.nu。为了统一管理,我在自己的 dotfiles 仓库里建了nushell/目录,把这两个文件放进去,再写两条link规则,让 Fresh 把它们软链到 nu 的配置目录。
接下来要做的是让 nu 启动时能加载常用的环境变量,包括把~/scoop/shims和 coreutils 的路径加入 PATH。在env.nu里追加:
$env.Path = ($env.Path | split row (char esep) | prepend "C:\Users\你的用户名\scoop\shims")这里有个细节:Nushell 用$env.Path而不是$env:PATH,而且 Windows 的路径分隔符是分号,所以用split row (char esep)来拆分,再用prepend把 scoop 的 shim 目录放到最前面,避免和其他命令冲突。
3.5 第五步:设置 Windows Terminal 默认启用 Nushell
打开 Windows Terminal 的设置界面,在“配置文件”里添加一个 Nushell 配置:
- 名称填
Nushell,命令行填nu.exe的完整路径(一般 scoop 会把它 shim 到~\scoop\shims\nu.exe)。 - 图标可以指向 nu 的安装目录里的图标文件,这一步非必需,但好看。
- 启动参数里可以加
-l,表示以登录 shell 方式启动,这样会加载env.nu和config.nu。 - 为了让中文显示正常,把“外观”里的字体设置成
Cascadia Mono,字体类型选Cascadia Code也可以,重点是要支持中文的字体。
设置完成之后,把 Nushell 设为默认配置文件,下次打开 Windows Terminal 就直接进入 nu 了。到这个阶段,基础环境已经能用了,但真正让体验提升一个档次的是下一步的配置优化。
4. 实战配置:让这套环境更顺手
4.1 定制 Nushell 的 prompt 与别名
Nushell 的 prompt 默认比较简单,我通过修改config.nu里的create_left_prompt函数来显示完整的路径和 git 分支状态。核心逻辑是:
def create_left_prompt [] { let dir = ($env.PWD | str replace $env.HOME "~") let git_branch = (git branch --show-current 2>/dev/null | str trim) if $git_branch != "" { $"(ansi green)($dir)(ansi reset) on (ansi blue)($git_branch)(ansi reset)" } else { $"(ansi green)($dir)(ansi reset)" } }这里用到了str replace、str trim,都是 nu 内置的字符串处理命令,语法比 bash 里的参数扩展直观很多。prompt 显示 git 分支后,在写代码时能避免很多“我在哪个分支”的困惑。
别名是另一个重点。我在config.nu里维护了一组常用别名:
alias ll = ls -l alias la = ls -a alias g = git alias gc = git commit alias gp = git push alias c = clear alias p = ps注意,nu 的别名和 fish 类似,不支持带参数的内联函数,但可以通过自定义命令来实现更复杂的逻辑。比如我想实现一个快速打开 git 仓库根目录的命令:
def gr [] { cd (git rev-parse --show-toplevel) }这种自定义命令可以放在config.nu或单独的commands.nu文件里,再用source引入。我的 dotfiles 仓库里还放了aliases.nu,这样配置结构更清晰。
4.2 日常管道操作示例
配置完成之后,我会用几个高频操作来验证环境是否顺手:
第一个是查端口占用。之前用 PowerShell 得记Get-NetTCPConnection,现在直接用 nu 的管道:
netstat -ano | lines | where {|line| $line =~ "LISTENING"} | select -f 10lines把输出按行拆成列表,where过滤,这种写法比在 bash 里把netstat的结果塞给grep再awk干净太多。
第二个是查最近修改的文件:
ls **/*.rs | sort-by modified | last 5ls **/*.rs是递归匹配,sort-by modified按修改时间排序,last 5取最近 5 条。在大型 Rust 项目或 Node 项目里查“我刚改过哪个文件”非常实用。
第三个是处理 JSON 数据。nu 可以直接读取 JSON 文件并做聚合,比如统计一个package.json里的依赖数量:
open package.json | get dependencies | columns | length这就是前面说的“把终端变成数据分析工具”的实际场景,写脚本排查问题时非常高效。
4.3 让 coreutils 和 Nushell 协同,而不是打架
装了 coreutils 之后,你会发现自己同时拥有两套命令:一套是 nu 内置的ls、rm、cp,另一套是外部命令ls.exe、rm.exe。这其实是一个需要主动处理的矛盾。
我的原则是:交互场景优先用 nu 内置命令,因为它们的输出是结构化的,可以继续放进管道里处理;脚本场景里如果要严格模拟 Linux 行为,则用 coreutils。
为了避免混乱,我没有给外部 coreutils 设置别名,而是通过一个细微的差异来区分它们:nu 内置的ls输出表格,coreutils 的ls输出文本。这样一眼就知道用的是哪个。
更重要的是 PATH 的优先级。Windows 会优先使用当前目录下的 exe,然后是 PATH 里的。我确保C:\Users\...\scoop\shims排在系统System32之前,这样sort、find这些命令默认会走 uutils 的版本,而不是被 Windows 自带的同名命令抢先。如果发现有命令行为不对,可以在 nu 里用which查看真实路径:
which sort which find如果指向的是System32,就需要手动调整 PATH 顺序。
5. 常见问题排查与避坑记录
5.1 Windows Terminal 启动崩溃:The terminal process failed to launch
我在配置初期经常遇到The terminal process failed to launch: A native exception occurred during the launch.这个报错,后来定位到原因:Windows Terminal 配置的 shell 路径有问题,或者 shell 依赖的 PATH 不完整。
排查思路分两步:
- 先确认 shell 能不能从命令行直接启动。比如在现有 PowerShell 里输入
nu,如果能进入 nu 说明程序没问题,问题出在 Windows Terminal 配置的命令行路径或参数。 - 检查 Windows Terminal 的 settings.json 中是否用了带空格的路径,但没有加引号。正确的写法是:
{ "commandline": "\"C:\\Users\\me\\scoop\\shims\\nu.exe\" -l" }另外,如果安装了 uutils-coreutils 后 PATH 被改成了 UTF-16 编码格式,也可能导致启动异常,我遇到过几次。解决方法是把 Windows Terminal 的语言改成中文后重启,让路径编码对齐。
5.2 PATH 与命令冲突
这个坑我踩过太多次了。Windows 原生命令和 Unix 命令同名的情况非常多,比如sort、find、more、where。uutils 装的sort.exe和System32里的sort.exe行为完全不同,前者是 GNU 风格,后者是 Windows 风格。
解决方法是设置一个独立的aliases段来覆盖,比如在config.nu里显式指定:
alias sort = ^sort --numeric-sort注意^前缀表示执行外部命令,这样不会递归调用 nu 内置的 sort。如果想让 coreutils 的find总是优先生效,也可以在 PATH 上做文章,把 scoop 的 shims 目录提前。
我的最终配置是:系统 PATH 的顺序为 scoop shims 优先、Git 的 usr/bin 次之、System32 垫底。这样日常使用的ls、find、grep都来自统一工具链,极少再出现“命令存在但行为不对”的情况。
5.3 中文乱码与编码问题
Nushell 默认使用 UTF-8,Windows 命令行却经常出现 GBK 编码的历史问题。我遇到过两个典型场景:
- 读取包含中文的文件时乱码。解决方法是使用
open时指定编码:open --raw file.txt | decode utf-8,或者直接在 config.nu 里设置默认字符集。 - Windows Terminal 内复制粘贴出现中文错位。这通常和字体渲染有关,我最终选了
Cascadia Mono并关闭了“实验性文本渲染”里的旧版控制台选项,问题就消失了。
还有一个细节:在env.nu里设置环境变量时,如果用中文路径,需要确保整个文件保存为 UTF-8 with BOM,否则 Windows 的控制台 API 可能读不到。这一点在 PowerShell 转 nu 的场景中尤其重要,因为 PowerShell 5 的默认编码不是 UTF-8。
5.4 关于 Fresh 与 fish 的关系澄清
最后集中说一下 Fresh 和 fish 的关系,因为很多读者会卡在这。Fresh 的安装脚本和插件机制确实是面向 fish 的,但它管理的对象不只是 fish 配置,你可以在同一个.freshrc里写link规则,把任意文件软链到任何位置。所以我用 fish 作为 Fresh 的运行环境和备用 shell,但主力 shell 是 Nushell,两者的配置互相独立,Fresh 更像是“配置管家”,而不是某个 shell 的附属品。
如果你完全不想装 fish,也有替代方案:直接把 dotfiles 仓库 clone 到本地,写一个简单的同步脚本,用 git 做版本控制,效果也接近。但 Fresh 的价值在于它有成熟的link命令和仓库结构约定,尤其是多机器同步时,省去自己写脚本的麻烦。我选择加上 fish,是因为它本来也是值得放在 Windows 上备用的现代 shell,两全其美。
另外,Fresh 更新配置的方式是执行fresh命令,它会把.freshrc里声明的所有配置项应用到本地。如果某个文件已经被手动修改过,Fresh 会报冲突,这时候需要手动处理或者用fresh --force强制覆盖。这点不用怕,它避免了很多“配置被悄悄覆盖”的问题。
我个人的体会是,这套组合刚搭起来的第一天会有点陌生,因为 Nushell 的管道语法和传统 shell 差别不小,但坚持用一周之后,再回到普通 PowerShell 会非常不习惯。如果你也在 Windows 上长期做开发,值得花一个下午把环境配起来,用完你会回来感谢我的。