﻿---
title: 用 Carapace 接管 Nushell 外部补全
date: 2026-06-09
excerpt: Nushell 外部补全交给 Carapace 后，可以用 choice 与 user spec 解决 brew services 漏补全，并把 Fish 桥接从 Nushell 配置中移走，适合统一维护多 shell 补全策略。
tags:
  - Shell
  - Nushell
  - Carapace
updated: 2026-06-09 14:22:13
---

书接[[nu_completion#Step 1. 桥接 Fish 与 Carapace|上回]]。早先配置 Nushell 补全时，我在 `external_completer` 里维护了一份白名单：Fish 补全较完善就走 `fish_completer`，否则走 `carapace_completer`。核心逻辑大概如下：

```nu
match $spans.0 {
    nu | tv | bun | git | rclone => $fish_completer
    _ => $carapace_completer
} | do $in $spans
```

这段配置可以工作。只是选择逻辑被写进了 Nushell 配置，其他 shell 无法复用；迁移到 Carapace 这个中间层，则不需要在每个 shell 里重复维护同一套分发规则。

当然，我大概率不会再使用其他 shell，有种为了折腾而折腾的感觉😇

## 从 brew services 说起 🍺

最近从 Clash Verge Rev 转向 mihomo 裸核，启动服务时会频繁用到 `{sh} brew services start mihomo`。

Carapace 暂时不支持为 `brew services` 子命令补全服务名；比较惊喜地发现 Fish 里可以补全：

![使用 Fish 补全 brew services 命令](https://assets.vluv.space/2026-06-08-~Projectsvluvthemesgnix-000080.avif)

按照前面的做法，只需要把 `brew` 的补全转发到 `fish_completer` 即可，形如：

```nu
# [!code word:brew]
match $spans.0 {
    nu | tv | bun | git | rclone | brew => $fish_completer
    _ => $carapace_completer
} | do $in $spans
```

这能修好当前问题，但补全策略还是散落在 Nushell 里。下面讲一下Carapace的做法

## Carapace 的选择顺序 🔢

Carapace 现在把 completer 分成不同 group。以 macOS 为例，一个命令同时存在多个补全来源时，默认优先级大致是：

1. `user`
2. `system`
3. `darwin`
4. `bsd`
5. `unix`
6. `common`
7. `bridge`

这解释了为什么只启用 Fish bridge（即设置了环境变量 `CARAPACE_BRIDGES=fish`）不一定能解决 `brew` 的问题；例如运行`{sh} carapace brew nushell brew se | jq`，尝试查看`brew se`的补全结果，会发现只有`search`子命令 —— `services`被漏掉了

```sh
[
  {
    "value": "search ",
    "display": "search",
    "description": "Perform a substring search of cask tokens and formula names for <text>",
    "style": {
      "fg": "blue"
    }
  }
]
```

原因在于`brew` 已经有 Carapace 内置补全，属于 `common`；Fish bridge 如果通过 `CARAPACE_BRIDGES` 发现，则属于 `bridge`。默认情况下，`common` 会压过 `bridge`。

可以用 `{sh} carapace --list brew` 看实际顺序：

```json
{
  "brew": [
    {
      "name": "brew",
      "description": "The missing package manager for macOS",
      "group": "common",
      "package": "github.com/carapace-sh/carapace-bin/completers_release/common/brew_completer/cmd",
      "url": "https://brew.sh/"
    },
    {
      "name": "brew",
      "description": "The missing package manager for macOS",
      "group": "bridge",
      "variant": "fish"
    }
  ]
}
```

对此，可以使用`carapace choices` 调整命令补全来源的优先级，参考下面代码，将`fish`补全移到前面，再次运行`{sh} carapace brew nushell brew se | jq .`，可以发现除了`search`，还提供了`services`和`setup-ruby`的补全入口：

```sh
$ carapace --choice brew/fish@bridge
$ carapace brew nushell brew se | jq .
[
  {
    "value": "search ",
    "display": "search",
    "description": "Perform a substring search of cask tokens and formula names for text"
  },
  {
    "value": "services ",
    "display": "services",
    "description": "Integrates Homebrew formulae with macOS's launchctl manager"
  },
  {
    "value": "setup-ruby ",
    "display": "setup-ruby",
    "description": "Installs and configures Homebrew's Ruby"
  }
]
```

## 另一种解决方案: 用 user spec 覆写 brew 👥

前面优先级中可以看到，`user group` 是优先级最高的补全来源。那么自然可以想到在`user group`中定义，让`brew`走fish的补全方案。

Carapace 会自动加载 [配置目录](https://carapace-sh.github.io/carapace-bin/setup/userConfigDir.html) 下的 `specs`。

个人设置了`XDG_CONFIG_HOME = ~/.config`，所以在`~/.config/carapace/specs`新建 `brew.yaml`：

```yaml ~/.config/carapace/specs/brew.yaml
# yaml-language-server: $schema=https://carapace.sh/schemas/command.json
name: brew
description: The missing package manager for macOS
parsing: disabled
completion:
  positionalany: ["$carapace.bridge.Fish([brew])"]
```

注意`{yaml} name: brew` 必须和文件名 `brew.yaml` 对上。

结束后，新开一个 shell，运行`{sh} carapace brew nushell brew se | jq`，会看到同上一节一致的输出：补全结果包含完整的三个子命令

## Nushell 补全配置的清理 🫧

完成上述工作，Nushell 侧就不需要维护「哪些命令走 Fish」的白名单了。保留别名展开逻辑即可，最后统一交给 Carapace：

```nu
let carapace_completer = {|spans: list<string>|
    CARAPACE_LENIENT=1 carapace $spans.0 nushell ...$spans | from json
}

let external_completer = {|spans|
    let expanded_alias = scope aliases
    | where name == $spans.0
    | get -o 0.expansion

    let spans = if $expanded_alias != null {
        $spans
        | skip 1
        | prepend ($expanded_alias | split row ' ' | take 1)
    } else {
        $spans
    }

    match $spans.0 {
        _ => $carapace_completer
    } | do $in $spans
}

$env.config.completions = {
  case_sensitive: false
  quick: true # set this to false to prevent auto-selecting completions when only one remains
  partial: true # set this to false to prevent partial filling of the prompt
  algorithm: "prefix" # prefix or fuzzy
  external: {
    enable: true
    completer: $external_completer
  }
  use_ls_colors: true
}
```
