xonsh — 在同一行里同时写 bash 命令和 Python 代码
是什么
xonsh(读作 “conch”,海螺)是一个用 Python 写、让你在 shell 里直接混用 bash 命令和 Python 表达式的 shell。日常类比:bash 是只懂中文的厨师,Python 是只懂英文的厨师,平时你得用两本菜谱来回切;xonsh 是同时懂两种语言的厨师,一句话里中英文混着说,他都听得懂。
具体看一行:
for f in `.*\.log$`: print(f, $(wc -l @(f)).split()[0])`.*\.log$` 是 xonsh 的正则文件 glob,返回一个 Python 列表;for f in ... 是标准 Python;$(wc -l @(f)) 在 subprocess 模式下跑 wc -l,@(f) 把 Python 变量 f 塞回命令行;.split()[0] 又回到 Python。一行里两种语言来回穿插。
这就是 xonsh 想推的核心:shell 该有的东西不丢,Python 该有的东西也不丢。
为什么重要
不学 xonsh 也不影响干活,但理解它能让你看清三件事:
- shell 和编程语言可以不分家 —— 你写 bash 脚本到第 30 行总会想”要是能直接用 Python 的字典/列表多好”,xonsh 让你不用切语言
- Python 已经事实上是运维语言 —— pip 装的工具、ML 训练脚本、k8s operator 都是 Python,shell 直接会 Python 等于零摩擦调它们
- shell 的语法可以重新设计 —— xonsh 不像 bash 加补丁,它把 Python 语法当主干,bash 风格的部分用
$()!()@()三个标记融进去
如果你日常 python -c "..." 或写一堆 subprocess.run(...) 的小脚本,xonsh 的卖点就是这些直接变成命令行交互。
核心要点
xonsh 的设计可以拆成 三块:
-
双模式自动切换:同一行里
ls -la是 subprocess 模式(按 bash 风格跑外部命令),x = [1, 2, 3]是 python 模式(按 Python 跑)。靠词法上下文判断,不用前缀。 -
三个特殊标记把两边接起来:
$VAR读环境变量(也是 Python dict 的一员,可以$PATH.append(...))$(cmd)跑命令、捕获 stdout 字符串(像 bash 的$())!(cmd)跑命令、捕获完整 CompletedCommand 对象(含 returncode、stderr)@(py_expr)把 Python 表达式的值塞进命令行参数
-
rc 文件就是 Python 文件:
~/.xonshrc是.xsh文件,可以import任何包、写函数、定义别名。配置和脚本同一种语言。
加上一个故意的设计:不兼容 POSIX。老的 .sh 脚本要跑得用 bash old.sh 显式调 bash——这点和 fish、nushell 同源。
实践案例
案例 1:bash 和 Python 在一行里穿插
files = $(ls *.txt).split()for f in files: if 'TODO' in $(cat @(f)): print(f'{f} 还有 TODO')逐部分解释:
$(ls *.txt)是 subprocess 模式,跑ls拿到一个字符串.split()是 Python 字符串方法,拆成列表for f in files:是 Python 循环$(cat @(f))又跳回 subprocess,@(f)把当前f这个 Python 变量塞回命令行f'{f} ...'是 Python 3.6 的 f-string
bash 等价要 for f in *.txt; do grep -l TODO "$f" && echo "$f 还有 TODO"; done——能写但拼接性差,复杂逻辑就崩。
案例 2:环境变量当成 Python 字典用
$PATH.append('/usr/local/bin')$EDITOR = 'vim'del $LESS逐部分解释:
$PATH不是字符串而是一个 list-like 对象,直接append不用:拼$EDITOR = 'vim'是 Python 赋值del $LESS是 Python 的 del 语句,把环境变量删掉
bash 里要 export PATH="$PATH:/usr/local/bin",要会引号、冒号、export 三件事。xonsh 把环境当 Python 对象。
案例 3:用 Python 包写别名
import json
def _pretty_json(args): with open(args[0]) as f: data = json.load(f) print(json.dumps(data, indent=2, ensure_ascii=False))
aliases['pj'] = _pretty_json逐部分解释:
- 直接
import json——shell 里能 import - 别名是一个 Python 函数,不是字符串
aliases['pj'] = _pretty_json把它绑成pj命令
之后 shell 里输 pj data.json 就漂亮打印 JSON。bash 实现等价功能要写 function pj() { python -c "..." } 嵌套引号地狱。
踩过的坑
-
启动慢:每次开 shell 都要把 Python 解释器拉起来 + import xonsh 自身,冷启动 100-200ms。bash 是 5-10ms。频繁开新 shell 的场景(tmux 大量开 pane)会感受到。
-
生态远小于 bash/zsh:oh-my-zsh、starship 这些主流 prompt 框架是 zsh 优先,xonsh 有 xontrib 系统但插件数量是数量级差距。
-
Python 表达式和 subprocess 边界容易混:
ls -la是 subprocess,但ls单独写就被解析成 Python 名字。新人常踩。保险写法:subprocess 命令始终带参数或加!()包起来。 -
CI/Docker 场景几乎不用:Dockerfile 的
RUN默认/bin/sh,安装 xonsh 反而拖慢 image build。xonsh 主要是交互 shell + 个人脚本,不替代/bin/sh。
适用 vs 不适用场景
适用:
- 写 50-200 行的运维小脚本,一半 bash 命令、一半 Python 数据处理
- 已经精通 Python,不想再为 bash 学一套引号/分词/数组规则
- ML/数据工程师,平时 90% 工作在 Python,shell 偶尔一用
不适用:
- 给别人发的 install.sh / Dockerfile / Makefile(必须 POSIX)
- 极致快速冷启动场景(频繁开新 shell)
- 团队里同事不会 Python(脚本读不懂)
学到什么
-
shell 的语法可以从一门通用语言长出来:bash 是为命令行从零设计;xonsh 反过来,把 Python 当主干、命令行风格当扩展。两条路都成立,选哪条看你团队的语言重心。
-
“同一行混两种语言”是真正的 win:传统 shell 脚本到第 50 行总会想换 Python,但又懒得改文件类型。xonsh 让这个切换不存在——一行里随时切。
-
故意不兼容 POSIX 的代价和收益:fish、nushell、xonsh 都选了不兼容。代价是老脚本跑不了,收益是新设计不被 1973 年的妥协绑住。这一代 shell 工具的共同信念。
延伸阅读
- 官网:xon.sh(左侧 Tutorial 是入门最快的路径,比 GitHub README 详)
- xontrib(插件)目录:xontrib-index——看生态大小直接看这个
- 对比阅读:xonsh 文档里的 Bash to Xonsh Translation Guide,把常见 bash 习惯一对一翻译