艾剪讲影 發表於 2026-4-12 22:57:00

记对 xonsh shell 的使用, 脚本编写, 迁移及调优

<p>xonsh 是 python 驱动的 shell, 在操作效率, 交互和外部功能的先进性上比 bash 等优秀, 并且十分容易上手.<br>
但相应地, 它是一个新兴的 shell, 并且不是所谓 "POSIX Shell"(尽管某些行为比较相似), 加上现有中文相关文章属实不深, 故自己做了一些较深的了解, 在此处记录心得, 若有错误请您指出, 不胜感谢.<br>
xonsh 的提示符为 <code>@</code>, 因为它读作 "consh", 脚本文件扩展名为 <code>.xsh</code>, 官方定义的 markdown 代码块标签也是 <code>xsh</code><br>
截至本文最后更新, xonsh 的最高版本是 <code>0.22.8</code></p>

<p>关于 xonsh 的安装和基本自定义, 本文不作赘述, 您可以阅读 Linux 中国等发布的新闻稿或者直接阅读 xonsh 官方安装教程, 安装并使用 <code>xonfig web</code> 等工具</p>
<h2 id="特殊语法">特殊语法</h2>
<p>Shell 与 Python 的集成是 xonsh 易用性的根源, xonsh 向下直接兼容多数 python 脚本语法, 并且有提供独特语法以便将 python 和 shell 集成</p>
<h3 id="命令解释与机制相关">命令解释与机制相关</h3>
<h4 id="引用和处理字符串变量">引用和处理字符串变量</h4>
<p>在 xonsh 中, 你可以直接使用 <code>print(x)</code> 输出一个变量和对象, 但若想将它作为 <code>touch</code> 命令的参数呢? xonsh 能带你告别 bash 丑陋的字符串拼接和 for 语法.</p>
<p>xonsh 使用 <code>@()</code> 操作符在 shell 环境中引用一个变量, 它会自动对字符串对象进行 <code>str()</code> <strong>而不是 <code>repr()</code></strong> 操作(也即直接追加字符串), 可以通过下列代码验证: (注意, 对于其他类型 <code>@()</code> 还有其他优雅的行为, 详见下文)</p>
<pre><code class="language-xsh">tmp @ text = "Hello\nWorld"
tmp @ repr(text)
"'Hello\\nWorld'"
tmp @ str(text)
'Hello\nWorld'
tmp @ touch @(text)
tmp @ ls
'Hello'$'\n''World' # 文件名中真的有换行
tmp @ # 如果是 repr(), 就会创建 'Hello\nWorld' (无转义)
</code></pre>
<p>因此你可以这样创建一组数字序号文件</p>
<pre><code class="language-xsh">tmp @ for i in range(30):
         touch @(i)
   
tmp @ ls
0246810121416182022242628
1357911131517192123252729
tmp @
</code></pre>
<p>或者方便地补0</p>
<pre><code class="language-xsh">tmp @ for i in range(30):
         touch @(str(i).zfill(2))

tmp @ ls
000204060810121416182022242628
010305070911131517192123252729
</code></pre>
<p>同样的简单操作若用 bash 进行, 需要:</p>
<pre><code class="language-bash">for i in {0..29}; do touch $(printf "%02d" $i); done
</code></pre>
<p>或</p>
<pre><code class="language-bash"># 避免命令替换的子 shell 开销
for i in {0..29}; do
    printf -v name "%02d" "$i"
    touch "$name"
done
</code></pre>
<p>python 还有 bash 无法提供的一系列支持库与工具, 可处理多种格式文件而无需引用外部程序; 并且处理含有奇葩字符的文件名, xonsh 可以使用字符串的 repr 集成到命令中很难炸掉</p>
<p>不过, 同样的操作若用纯 python 脚本/REPL进行, 需要:</p>
<pre><code class="language-python">from pathlib import Path

for i in range(0, 30):
    name = str(i).zfill(2)
    path = Path(name)
    path.touch()
</code></pre>
<p>这两种对比足以显示 xonsh 的优越性</p>
<h4 id="别名与-aliases">别名与 aliases</h4>
<p>在 xonsh 中, 别名被存储在全局的 <code>aliases</code> -- 一个兼容字典 API 的 <code>xonsh.aliases.Aliases</code> 对象中.<br>
像这样设置一个别名:</p>
<pre><code class="language-xsh">~ @ aliases['la'] = 'ls -a'
</code></pre>
<p>字符串形式建立的别名将自动转换为列表, 也即对象的智能性:</p>
<pre><code class="language-xsh">~ @ aliases['la'] = 'ls -a'
~ @ aliases['la']
['ls', '-a']
</code></pre>
<p>若要取消一个别名, 只需:</p>
<pre><code class="language-xsh">~ @ del aliases['la']
~ @ la
xonsh: subprocess mode: command not found: 'la'
</code></pre>
<hr>
<p>此外, 扩展 <code>xontrib-abbrevs</code> (需要安装 <code>xontrib-abbrevs</code> 包) 还提供了一套独立的, 输入后会自动展开的类 alias 系统, 存放于全局的 <code>abbrevs</code> 字典对象中</p>
<pre><code class="language-xsh">~ @ abbrevs['ll'] = 'ls -l'
~ @ ls -l # 输入 ll 回车或空格后自动补全
总计 36
drwxr-x---4 pluv wheel 40964月11日 12:25 Desktop
drwxr-x--- 10 pluv wheel 40964月 8日 00:18 Documents
drwxr-x---5 pluv wheel 40964月12日 09:03 Downloads
drwxr-x---2 pluv wheel 40962月 6日 15:18 Music
drwxr-x---5 pluv wheel 40963月26日 09:04 Pictures
drwxr-x--- 10 pluv wheel 40964月 9日 19:39 Programs
drwxr-x---2 pluv wheel 40963月10日 21:58 Public
drwxr-x---2 pluv wheel 40962月 6日 15:18 Templates
drwxr-x---3 pluv wheel 40962月18日 21:18 Videos
~ @
</code></pre>
<h4 id="路径匹配-正则表达式和反引号">路径匹配, 正则表达式和反引号</h4>
<p>xonsh 和大多数 shell 一样, 默认使用 glob 匹配路径, 例如:</p>
<pre><code class="language-xsh">~ @ ls
DesktopDocumentsDownloadsMusicPicturesProgramsPublicTemplatesVideos
~ @ echo D*
Desktop Documents Downloads
~ @
</code></pre>
<p>在 xonsh 中, <code>``</code> 不像在bash中一样代表进程替换. 考虑到反引号本身在 python3 中已经失去了作用(这玩意在 python2 中曾用于表示 repr), 于是 xonsh 的开发者十分聪明地给反引号分配了正则路径匹配功能, 无需用户去导入并繁琐地使用 <code>re</code> 和 <code>pathlib</code> 模块<br>
这样你就可以用反引号括住正则表达式以对路径进行正则匹配</p>
<p>例如:</p>
<pre><code class="language-xsh">~ @ touch report2023.txt
~ @ ls `report.*\.txt`
report2023.txt
</code></pre>
<p>同理:</p>
<pre><code class="language-xsh"># 匹配 /etc 目录下所有 .conf 结尾的文件
ls `/etc/.*\.conf`
# 匹配 /tmp 目录下任意深度的 .log 文件
ls `/tmp/.*\.log`
# 匹配家目录下所有 .py 文件
ls `~/.*\.py`
</code></pre>
<p>可以用这个程序探索匹配功能是怎么运作的</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main(int argc, char* argv[]) {
    if (argc &lt;= 1) {
      std::cout &lt;&lt; "没有提供任何参数" &lt;&lt; std::endl;
      return 0;
    }
    std::cout &lt;&lt; "共 " &lt;&lt; argc - 1 &lt;&lt; " 个参数" &lt;&lt; std::endl; // 文件名本身占一个 argv
    for (int i = 1; i &lt; argc; i++) {
      std::cout &lt;&lt; "argv[" &lt;&lt; i &lt;&lt; "] = " &lt;&lt; argv &lt;&lt; std::endl;
    }
    return 0;
}
</code></pre>
<p>如下:</p>
<pre><code class="language-xsh">~ @ ls
Desktop    DownloadsPicturesPublic   Videostest.cc
DocumentsMusic      ProgramsTemplatesa.out
~ @ ./a.out `^D.*$` # 这个正则表达式匹配所有 D 开头的串
共 3 个参数
argv = Desktop
argv = Documents
argv = Downloads
~ @ ./a.out `^D.*$` Music
共 4 个参数
argv = Desktop
argv = Documents
argv = Downloads
argv = Music
~ @ ./a.out Vid* `^D.*$` Musi*
共 5 个参数
argv = Videos
argv = Desktop
argv = Documents
argv = Downloads
argv = Music
~ @
</code></pre>
<p>可见, 不同使用 glob, 正则匹配到的串都按照顺序优雅的串联起来成为命令的参数!<br>
你可能想知道原理是什么, 这将在下一节介绍</p>
<h4 id="再认识--与类型解析系统">再认识 <code>@()</code> 与类型解析系统</h4>
<p>在上文我们提到, <code>``</code> 包裹的内容可以对路径进行正则匹配, 但在 xonsh 眼中, <code>``</code> 只是一个操作符, 这意味着它也是一个对象:</p>
<pre><code class="language-xsh">~ @ `^D.*$`
['Desktop', 'Documents', 'Downloads']
~ @ ``
[]
</code></pre>
<p>你可以像在 python 中一样调用 <code>type()</code> 函数查看对象类型, 最终发现 <code>``</code> 实际上被优雅地解析为一个列表</p>
<pre><code class="language-xsh">~ @ type(``)
list
~ @
</code></pre>
<p>你还能直接给命令传递一个列表, 但注意要用 @() 包裹, 列表内容会按顺序作为命令的参数</p>
<pre><code class="language-xsh">~ @ ./a.out @(['0', '2', '1'])
共 3 个参数
argv = 0
argv = 2
argv = 1
~ @
</code></pre>
<p>由此引出 xonsh 的对象化类型解析系统<br>
实际上, xonsh 并不通过 "list" 这个类型名处理列表展开, 而是对具有 <code>__iter__</code> 属性的对象展开</p>
<p>验证如下:</p>
<pre><code class="language-xsh">~ @ class CountDown:
      """倒计时可迭代对象, 这不是列表哦"""

      def __init__(self, start):
            self.start = start

      def __iter__(self):
            current = self.start
            while current &gt; 0:
                yield current
                current -= 1

~ @ ./a.out @(CountDown(10))
共 10 个参数
argv = 10
argv = 9
argv = 8
argv = 7
argv = 6
argv = 5
argv = 4
argv = 3
argv = 2
argv = 1
~ @
</code></pre>
<p>由此可知, 对于 <code>iterable</code> (意为 "可迭代", 即具有 <code>__iter__</code> 方法的)对象, xonsh 会在命令行中将其展开, 既不是 <code>repr</code> 也不是 <code>str</code>, 而是空格分隔的字符串.</p>
<p>现在你可能会想当然认为:</p>
<pre><code class="language-xsh">~ @ ./a.out @({'cat':'tom', 'mouse':'jerry', 'dog':'spark'})
</code></pre>
<p>的行为等同于</p>
<pre><code class="language-xsh">~ @ ./a.out --cat tom --mouse jerry --dog spark
</code></pre>
<p>然而现实很骨感</p>
<pre><code class="language-xsh">~ @ ./a.out @({'cat':'tom', 'mouse':'jerry', 'dog':'spark'})
共 3 个参数
argv = cat
argv = mouse
argv = dog
~ @ ./a.out --cat tom --mouse jerry --dog spark
共 6 个参数
argv = --cat
argv = tom
argv = --mouse
argv = jerry
argv = --dog
argv = spark
~ @
</code></pre>
<hr>
<p>对于 <code>@()</code> 操作符的行为, 此处有一个严谨的定义:</p>
<p>在子进程模式(也就是 xonsh 的上下文)下, <code>@()</code> 运算符形式可以工作, 并且会执行任意的 Python 代码. 执行结果会被追加到子进程的命令列表中.<br>
如果结果是字符串或字节类型, 则直接追加到参数列表中;<br>
如果结果是 <code>iterable</code>, 则将其内容转换为字符串并按顺序追加到参数列表中;<br>
如果结果出现在"第一个位置"且为一个 python 函数, 则该函数会作为别名(<code>ExecAlias</code> 对象)运行.(见下文)<br>
在其他情况下, 结果会自动转换为字符串.</p>
<p>由于字典对象具有的 <code>__iter__</code> 是对 <code>keys</code> 的迭代, 所以才有如上的结果</p>
<h4 id="使用函数">使用函数</h4>
<p>上文我们提到, 如果 <code>@()</code> 的结果出现在"第一个位置"且为一个 python 函数, 则该函数会作为别名(<code>ExecAlias</code> 对象)运行, 这也即是 xonsh 集成函数的方式, 让你可以像使用命令行工具一样使用函数.</p>
<p>这里的"第一个位置"并非"第一个参数", 而是"xonsh 收到的参数列表", 举个例子:<br>
<code>./a.out 1</code> 中, <code>./a.out</code> 是 "第一个位置"</p>
<p>xonsh 的函数集成是通过 <code>ExecAlias</code> 对象实现的, 因为 xonsh 将内置命令看作一个 "可执行别名", 我们无需了解底层原理, 只需一个例子即可:</p>
<pre><code class="language-xsh">~ @ def say_meow(args):
      num = int(args) if args else 0
      print(num * 'Meow')

~ @ @(say_meow) 3
MeowMeowMeow
~ @
</code></pre>
<p>在例子中 我们创建了一个 <code>say_meow</code> 函数, 需要注意的是, 能作为命令行使用的函数必须接受唯一的 <code>args</code> 参数, 它是一个以字符串存储命令行参数的列表</p>
<p>和 C 程序的 argv 不同的是, args 存放的是第一个命令行参数(如上例中 3), 而不是 "函数/程序名"</p>
<p>你还可以使用别名系统注册这个函数到 aliases['cmd'] 中, 这样就可以无需 <code>@()</code> 像运行命令行工具一样运行函数</p>
<h4 id="会话对象">会话对象</h4>
<p>xonsh 使用 <code>@</code> 指代当前会话, 其为一个 <code>XonshSessionInterface</code> 对象, 具有以下公有属性:</p>
<p><code>@.env</code> 是一个 <code>xonsh.environ.Env</code> 对象, 兼容字典 API, 用于存储或设置当前会话的环境变量<br>
<code>@.history</code> 是一个 <code>xonsh.history.json.JsonHistory</code> 对象(当使用 JSON 后端时), 用于存储历史记录, 兼容列表 API<br>
<code>@.imp</code> 是一个 <code>xonsh.built_ins.InlineImporter</code> 包导入器对象, 用于导入包<br>
例如:</p>
<pre><code class="language-xsh">@ json = @.imp.json # 实际上等同于 import json
</code></pre>
<p><code>@.lastcmd</code> 是一个 <code>xonsh.procs.pipelines.HiddenCommandPipeline</code> 或 <code>xonsh.procs.pipelines.CommandPipeline</code> 对象, 存储上一个命令的捕捉结果(无论显式或隐式捕捉, 具体见下文)</p>
<h3 id="子进程相关">子进程相关</h3>
<p>shell 本质上是人类友好且带命令解释功能的子进程与内建命令启动工具, xonsh 对 subprocess 的处理和其他 shell 不同</p>
<h4 id="发起子进程而并不进行捕获">发起子进程而并不进行捕获</h4>
<p>为了避免歧义, xonsh 实际上对任何子进程都是捕获的(包括不外包操作符地运行命令, 使用 <code>$[]</code>, <code>![]</code>, <code>$()</code>, <code>!()</code>), 此节只是介绍了一种 xonsh 不提供任何捕获后信息的子进程发起方法.</p>
<p>例如, 即使你不外套操作符运行一个命令, 命令本身也会被隐式捕获, xonsh 会读取捕获后的 <code>HiddenCommandPipeline(隐藏式命令管线)</code> 对象读取返回值, 并根据你的配置显示或不显示在你的下一个提示符, 并把那个对象放入 <code>@.lastcmd</code> 中, 详情请看下下节.</p>
<p>xonsh 提供了 <code>$[]</code> 操作符以显式发起一个子进程, 它不会捕获或隐藏式地捕获关于子进程的任何信息给任何程序乃至 xonsh 本身(返回一个 <code>None</code> 对象)</p>
<pre><code class="language-xsh">~ @ $
DesktopDocumentsDownloadsMusicPicturesProgramsPublicTemplatesVideos
~ @ $
yeah
</code></pre>
<p>这通常被用于脚本中, 例如只需要调用 time 命令让用户读它的输出, 又怕它和 python 的 time 包重名的情境</p>
<p><code>![]</code> 会自动对包含内容进行变量引用解析后套上引号, 本身也可以包含字符串, 但字符串内的内容将不会被解析<br>
一个例子:</p>
<pre><code class="language-xsh">~ @ text = 1
~ @ $['ls @(text)']
xonsh: subprocess mode: command not found: 'ls @(text)'
~ @
~ @ $
ls: 无法访问 '1': 没有那个文件或目录
~ @
</code></pre>
<p>另一个例子, 说明即使不套字符串, <code>$[]</code> 内的内容也不会作为 python 表达式被解析</p>
<pre><code class="language-xsh">~ @ $
time: missing program to run
Try 'time --help' for more information.
~ @ $

File "&lt;stdin&gt;", line 1
    $
            ^^^^
SyntaxError: ('code: (',)
</code></pre>
<h4 id="捕获子进程信息">捕获子进程信息</h4>
<p>xonsh 使用 <code>$()</code> 和 <code>!()</code> 操作符捕获子进程, 但它们不是等价的<br>
<code>$()</code> 操作符用于仅捕获子进程的输出<br>
例如:</p>
<pre><code class="language-xsh">tmp @ $(ls -l)
'总计 0\n-rw-r--r-- 1 pluv wheel 04月12日 11:57 iamafile\n'
</code></pre>
<p>值得注意的是 xonsh 通过变量 <code>$XONSH_SUBPROC_OUTPUT_FORMAT</code> 来决定捕获得到的数据结构类型<br>
默认是 <code>'stream_lines'</code>, 即将所有输出行捕获为一个单一字符串<br>
可将其设置为 <code>'list_lines'</code>, 将所有输出行分行捕获为一个列表<br>
例如</p>
<pre><code class="language-xsh">~ @ $XONSH_SUBPROC_OUTPUT_FORMAT = 'list_lines'
~ @ $(ls -l)
['总计 36',
'drwxr-x---4 pluv wheel 40964月11日 12:25 Desktop',
'drwxr-x--- 10 pluv wheel 40964月 8日 00:18 Documents',
'drwxr-x---5 pluv wheel 40964月12日 09:03 Downloads',
'drwxr-x---2 pluv wheel 40962月 6日 15:18 Music',
'drwxr-x---5 pluv wheel 40963月26日 09:04 Pictures',
'drwxr-x--- 10 pluv wheel 40964月 9日 19:39 Programs',
'drwxr-x---2 pluv wheel 40963月10日 21:58 Public',
'drwxr-x---2 pluv wheel 40962月 6日 15:18 Templates',
'drwxr-x---3 pluv wheel 40962月18日 21:18 Videos']
~ @
</code></pre>
<p>不要将 <code>$XONSH_SUBPROC_OUTPUT_FORMAT</code> 设置为 <code>'list_lines'</code> 和 <code>'stream_lines'</code> 之外的其他值, 否则捕获是不会工作的</p>
<pre><code class="language-xonsh">~ @ $XONSH_SUBPROC_OUTPUT_FORMAT = 'list'
~ @ $(ls -l)
~ @ # 啥也没有
</code></pre>
<p>另外, <code>$()</code> 只会捕获 <code>stdout</code> 流的内容, 也就是说, 如果内容是以 stderr 流输出的, xonsh 不会捕获它, 这些内容将被输出(如下例)<br>
以及若命令不会在 stdout 输出结果(如下例), 那么 xonsh 将视 <code>$XONSH_SUBPROC_OUTPUT_FORMAT</code> 来将空字符串 <code>''</code> 或 空列表 <code>[]</code> <strong>而不是 <code>None</code> 对象或者 <code>['']</code> 这种东西</strong>反馈给变量作为回退结果 (应该是考虑到处理类型的一致性吧)</p>
<pre><code class="language-xsh">~ @ $XONSH_SUBPROC_OUTPUT_FORMAT = 'stream_lines'
~ @ $(ls 不存在的目录)
ls: 无法访问 '不存在的目录': 没有那个文件或目录 # -&gt; 这是输出的错误信息, xonsh 没有捕获它
'' # -&gt; 这是捕获到的内容(一个空字符串)
~ @ s = $(ls 不存在的目录)
ls: 无法访问 '不存在的目录': 没有那个文件或目录
~ @ s
'' # -&gt; 这是一个空字符串
~ @ $XONSH_SUBPROC_OUTPUT_FORMAT = 'list_lines'
~ @ s = $(ls 不存在的目录)
ls: 无法访问 '不存在的目录': 没有那个文件或目录
~ @ s
[]
</code></pre>
<p>对于有颜色的内容, xonsh 会捕获其控制码</p>
<p>可以用这个小程序测试</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main(){
    std::cout &lt;&lt; "我是 stdout 的内容" &lt;&lt; std::endl;
    std::cerr &lt;&lt; "我是 stderr 的内容" &lt;&lt; std::endl;
    std::cout &lt;&lt; "我可以被捕获" &lt;&lt; std::endl;
    std::cerr &lt;&lt; "我不会被捕获, 而是被直接输出" &lt;&lt; std::endl;
    std::cout &lt;&lt; "\e[31m我是红色的, 我可以被捕获\e[0m" &lt;&lt; std::endl;
    std::cerr &lt;&lt; "\e[32m我是绿色的, 我不会被捕获\e[0m" &lt;&lt; std::endl;
    return 0;
}
</code></pre>
<pre><code class="language-xsh">~ @ s = $(./a.out)
我是 stderr 的内容
我不会被捕获, 而是被直接输出
我是绿色的, 我不会被捕获
~ @ s
'我是 stdout 的内容\n我可以被捕获\n\x1b[31m我是红色的, 我可以被捕获\x1b[0m\n'
~ @
</code></pre>
<hr>
<p><code>!()</code> 则更为高级, 它捕获关于命令的更多信息, 并将其放入一个 <code>CommandPipeline (命令管线)</code> 对象中, 使其能够捕获所有流的内容及子进程的元数据<br>
这个对象定义在 <code>xonsh.procs.pipelines</code> 中</p>
<pre><code class="language-xonsh">~ @ !(ls 一个不存在的目录)
CommandPipeline(
returncode=2,
pid=40466,
args=['ls', '一个不存在的目录'],
alias=['ls', '--color=auto', '-v', '一个不存在的目录'],
executed_cmd=['ls', '--color=auto', '-v', '一个不存在的目录'],
timestamps=,
input='',
output=[],
errors="ls: 无法访问 '一个不存在的目录': 没有那个文件或目录\n"
)
~ @ !(ls $HOME)
CommandPipeline(
returncode=0,
pid=40483,
args=['ls', '/home/pluv'],
alias=['ls', '--color=auto', '-v', '/home/pluv'],
executed_cmd=['ls', '--color=auto', '-v', '/home/pluv'],
timestamps=,
input='',
output=['Desktop', 'Documents', 'Downloads', 'Music', 'Pictures', 'Programs', 'Public', 'Templates', 'Videos']
)
</code></pre>
<p>以下是一份常用的映射表, 由于 xonsh 不会显示所有对象属性(例如不会显示值为 <code>None</code> 的属性), 所以此表常见但不完整<br>
完整的 CommandPipeline 对象手册见 xonsh.procs.pipelines - xonsh 0.22.8 documentation</p>
<pre><code class="language-python">CommandPipeline(
returncode(int) -&gt; 进程返回值, 0 为正常值
pid(int) -&gt; 进程 PID
args -&gt; 命令及其参数
alias(list/FuncAlias) -&gt; 进程使用的别名信息, 可能是解析后命令序列(对于子进程), 也可能是 FuncAlias 对象(对于内建命令别名), 如果没有使用别名, 此对象为 None
executed_cmd(list) -&gt; 解析后的进程参数
timestamps(list) -&gt; 进程起始及终止 UNIX 时间戳
input(str) -&gt; 进程接受的输入 (stdin)
output(str/list) -&gt; 进程的标准输出 (stdout)
errors(str) -&gt; 进程的错误流输出 (stderr)
)
</code></pre>
<p><code>CommandPipeline.output</code> 的格式依然受 <code>$XONSH_SUBPROC_OUTPUT_FORMAT</code> 控制, <code>error</code> 不受控制, 为 <code>stream_line</code> 格式</p>
<h4 id="隐藏式捕获进程">隐藏式捕获进程</h4>
<p>术语<code>隐藏式捕获进程</code>的意思是: 不捕获命令的输出, 就像命令未被捕获一般, 但通过一个 <code>HiddenCommandPipeline(隐藏式命令管线)</code> 对象获得其输出之外的数据</p>
<p>事实上, xonsh 提供了 <code>![]</code> 用于隐藏式捕获进程, 它的原理被同样应用到不外包操作符的命令执行中, 只是后者对写独立脚本的人几乎无用 -- 你不能将不外包操作符的命令的 <code>HiddenCommandPipeline</code> 对象装进自己的变量里, 因为它的捕获仅仅用于 xonsh 的返回值和历史记录处理, 以及 <code>@.lastcmd</code></p>
<p><code>HiddenCommandPipeline</code> 部分复用了 <code>CommandPipeline</code> 的方法与对象, 以下是一个例子:</p>
<pre><code class="language-xsh">~ @ cap = !
DesktopDocumentsDownloadsMusicPicturesProgramsPublicTemplatesVideos
~ @ cap
~ @ cap.__repr__()
''
~ @ cap.__repr__
&lt;bound method HiddenCommandPipeline.__repr__ of &gt;
~ @ cap.__str__()
''
~ @ cap.__str__
&lt;bound method CommandPipeline.__str__ of &gt;
~ @ cap.output # 空字符串
''
~ @ cap.errors # None
</code></pre>
<hr>
<p>对这些操作的归纳呈现于一个表格:</p>
<table>
<thead>
<tr>
<th>类型</th>
<th>输出</th>
<th>返回值</th>
<th>备注</th>
</tr>
</thead>
<tbody>
<tr>
<td>cmd</td>
<td>不捕获</td>
<td>隐藏命令管道</td>
<td>与 ! 相同</td>
</tr>
<tr>
<td>!</td>
<td>不捕获</td>
<td>隐藏命令管道</td>
<td></td>
</tr>
<tr>
<td>$</td>
<td>不捕获</td>
<td>无</td>
<td></td>
</tr>
<tr>
<td>!(cmd)</td>
<td>捕获</td>
<td>命令管道</td>
<td></td>
</tr>
<tr>
<td>$(cmd)</td>
<td>捕获</td>
<td>字符串</td>
<td>返回标准输出</td>
</tr>
</tbody>
</table>
<h3 id="内建命令相关">内建命令相关</h3>
<p>你可能注意到上文对 <code>FuncAlias</code> 对象的提及, 它出现在"捕获内建命令"的 <code>CommandPipeline</code> 对象中<br>
xonsh 的内建命令实际上是一个 <code>FuncAlias</code> 对象封装的函数对象</p>
<p>对于内建命令, 也可以通过 <code>$()</code> 与 <code>!()</code> 捕获, 但行为有不同, 以下是一些例子</p>
<pre><code class="language-xsh">~ @ !(echo hi,xonsh)
CommandPipeline(
returncode=0,
pid=72351,
args=['echo', 'hi,xonsh'],
executed_cmd=['echo', 'hi,xonsh'],
timestamps=,
input='',
output='hi,xonsh'
)
~ @ !(cd ~)
CommandPipeline(
returncode=0,
args=['cd', '/home/pluv'],
alias=FuncAlias({'name': 'cd', 'func': 'cd', 'return_what': 'result'}),
executed_cmd=['/home/pluv'],
timestamps=,
input='',
output=''
)
</code></pre>
<p>可见 <code>CommandPipeline</code> 的 <code>alias</code> 对于内置命令为命令对应的 FuncAlias 对象, 因为 xonsh 认为内置命令调用是 <code>FuncAlias</code> 的别名</p>
<h2 id="调优">调优</h2>
<h3 id="xonshrc-xontribs-和-xpip">.xonshrc, xontribs 和 xpip</h3>
<p>主目录的 <code>.xonshrc</code> 文件是 xonsh 启动时自动执行的命令列表, 其使用 xonsh 的命令解析器</p>
<p>当然你也可以执行 <code>xonsh --no-rc</code> 来避免加载 <code>.xonshrc</code> 排错</p>
<p>xonsh 还跨平台提供 <code>source-bash</code>, <code>source-zsh</code> 和 <code>source-cmd</code> 来分别解析 bash, zsh 和 windows cmd.exe 命令解析器的运行命令文件, 例如使用 <code>source-bash .bashrc</code> 来迁移 bash 的配置文件<br>
这些命令也有其他的 flag, 请参阅 xonsh 的文档</p>
<hr>
<p>xontribs 是 xonsh 的插件系统<br>
<code>xontrib</code> 命令可用于管理 xontrib<br>
例如直接运行或运行 <code>xontrib list</code> 查看 xontrib 状态</p>
<pre><code class="language-xsh">~ @ xontrib
abbrevs            loaded      manual
argcomplete      loaded      manual
broot            loaded      manual
cd               loaded      auto
coreutils          not-loaded
direnv             not-loaded
dotdot             loaded      auto
free_cwd         loaded      manual
jedi               loaded      manual
sh               loaded      manual
uvox               loaded      auto
zoxide             loaded      manual
zoxide_init_cachenot-loaded
</code></pre>
<p>执行 <code>xontrib load</code> 来加载指定的 xontrib<br>
执行 <code>xontrib reload</code> 来重载指定的 xontrib<br>
执行 <code>xontrib unload</code> 来取消加载指定的 xontrib</p>
<p>例如 <code>xontrib load coreutils</code> 会加载 xonsh 跨平台实现的一套 coreutils (也就是 ls 等命令组件), 它并不能提供"原生对象集成", 性能和功能, 以及底层管理都不如 GNU coreutils 甚至 busybox 优秀, 唯二好处是调用开销较低和跨平台(对于 windows 来说比较方便)</p>
<hr>
<p>xpip 是 xonsh 提供的 pip 别名, 它安装的包将仅安装至运行 xonsh 的 python 解释器包路径内</p>
<blockquote>
<p>xpip 不是 pipx</p>
</blockquote>
<p>可安装的 xontribs 可在 awesome-xontribs 仓库内找到(但这仓库有点老了, 虽然很多插件项目还活着)</p>
<p>以下是用 xpip 安装的比较有用的插件列表:</p>
<pre><code class="language-xsh">~ @ xpip install -U xontrib-abbrevs \
               xontrib-argcomplete \
               xontrib-free-cwd \
               xontrib-zoxide \
               xontrib-dotdot \
               xonsh-direnv \
               xontrib-sh \
               xontrib-bashisms \
               xontrib-uvox \
               xontrib-broot \
               xontrib-jedi \
               xontrib-cd \
</code></pre>
<p>以下是说明:</p>
<ul>
<li>xontrib-cd: 让 cd 命令后路径不需要转义</li>
<li>xontrib-abbrevs: 一套独立的 abbrevs 类 alias 系统, 好处是输入后会自动展开</li>
<li>xontrib-jedi: 通过 jedi 语言服务器插件来实现 xonsh 的 python 补全, 需要安装 jedi LSP</li>
<li>xontrib-broot: broot TUI 文件管理器集成(broot 不自带对 xonsh 的集成), 需要安装 broot, 默认配置 <code>ctrl + N</code> 快捷键, 可以自动 cd 到选定目录, 对目录跳转比较有用</li>
<li>xontrib-zoxide: 快速跳转目录工具, 个人认为没啥用</li>
<li>xontrib-dotdot: 可以通过 <code>......</code> 切换到上上上级目录</li>
<li>xonsh-direnv: 自动加载目录下 .env 文件, 这个插件会破坏 <code>@.lastcmd</code> 的行为, 因为 "上一条命令" 将永远是 direnv, 谨慎考虑安装</li>
<li>xontrib-vox: <code>vox</code> 是 xonsh 的一套 python 虚拟环境系统, 这个插件将 xonsh 和 <code>vox</code> 集成 (没用过)</li>
<li>xontrib-uvox: 将 xonsh 和 python <code>uv</code> 包管理器像 vox 一样集成 (很有用)</li>
<li>xontrib-free-cwd: 不让 xonsh 占用工作目录以避免 windows 删除目录提示占用中和 umount 提示目标忙的问题</li>
<li>xontrib-sh: 在 xonsh 内运行 bash, zsh, fish, tcsh, pwsh 命令, 原理依然是子进程, 但方便一些, 可以通过类似 IPython 的 <code>! &lt;命令&gt;</code> 或者 <code>!bash &lt;命令&gt;</code> 快速使用, 效果等同于 <code>bash -c '&lt;命令&gt;'</code></li>
<li>xontrib-bashisms: 在 xonsh 内直接支持部分 bash 风格的命令, 例如 <code>unset</code>, <code>export</code> 等</li>
<li>xontrib-argcomplete: 用于支持 argparse 参数解析器, 来进行部分 python 和 <code>.xsh</code> 脚本的参数补全, 它自己有一套参数生态, 自己写 <code>.xsh</code> 脚本运行时和对于一些 python 脚本很有用, 但对其他脚本来说无用</li>
</ul>
<p>不要安装 <code>xonsh-docker-tabcomplete</code>, 这个插件实现老旧, 无法在高版本 docker 使用<br>
xonfig web 的 xontrib 配置对于第三方 xontrib 有 bug, 最好不要用它, 因此要手动编辑 .xonshrc 来确保启用</p>
<hr>
<p>以下是示例 <code>.xonshrc</code> 配置和一些有用的环境变量设置</p>
<pre><code class="language-text"># XONSH WEBCONFIG START
$PROMPT = '{BOLD_BLUE}{cwd_base}{RESET} @ ' # 提示符主题
$XONSH_COLOR_STYLE = 'default' # 颜色主题
$XONSH_SHOW_TRACEBACK = False # 不让 xonsh 显示 "是否启用 python traceback" 的提示
$SUGGEST_COMMANDS = False # 不让 xonsh 建议打错的命令
$XONSH_SUBPROC_OUTPUT_FORMAT = 'list_lines'
# xontrib load coreutils # 不加载 xonsh 的 coreutils
xontrib load jedi
xontrib load broot
xontrib load abbrevs
xontrib load argcomplete
xontrib load free_cwd
xontrib load zoxide
xontrib load direnv
xontrib load sh
# XONSH WEBCONFIG END

# 别名配置
aliases['l'] = 'ls'
aliases['la'] = 'ls -a'
aliases['ll'] = 'ls -l'
aliases['rrm'] = 'rm -rf'
aliases['cpr'] = 'rsync -ah --info=progress2'
aliases['cls'] = 'printf "\033c"'
aliases['lss'] = lambda: __import__('os').system('stat -c "%a %n" *')

source-bash $HOME/.profile

# 加载一些常用的 python 库以便调用
import math
import time as _time # 避免和系统 time 冲突
import os
import xonsh as _xonsh # 避免和 xonsh 冲突
import copy
import random
</code></pre>
<h3 id="从-bash-转换与迁移">从 bash 转换与迁移</h3>
<p>本节来源于 Bash to Xonsh Translation Guide - xonsh 0.22.8 documentation, 提供 bash 中常见语法对应的 xonsh 等效写法, 对原文做了一些修改</p>
<table>
<thead>
<tr>
<th>Bash</th>
<th>Xonsh</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>没有表示会话的特殊对象</td>
<td><code>@</code></td>
<td><code>@</code> 对象包含 <code>@.env</code> - 环境变量, <code>@.imp</code> - 导入器, <code>@.lastcmd</code> - 上一条命令等</td>
</tr>
<tr>
<td><code>script.sh</code></td>
<td><code>script.xsh</code></td>
<td>推荐的文件扩展名是 <code>.xsh</code></td>
</tr>
<tr>
<td><code>#!/bin/bash</code></td>
<td><code>#!/usr/bin/env xonsh</code></td>
<td>在 shebang 中使用 xonsh</td>
</tr>
<tr>
<td><code>echo --arg="val"</code> <code>echo \;</code> <code>echo --arg "val"</code></td>
<td><code>echo {}</code> <code>echo "{}"</code> <code>echo ";"</code></td>
<td>xonsh 中没有像 bash 中反斜杠 (<code>\</code>) 那样的转义字符概念. 可以使用单引号或双引号来移除某些字符、单词或括号的特殊含义.</td>
</tr>
<tr>
<td><code>$NAME</code> 或 <code>${NAME}</code></td>
<td><code>$NAME</code></td>
<td>按名称查找环境变量</td>
</tr>
<tr>
<td><code>export NAME=Peter</code></td>
<td><code>$NAME = 'Peter'</code> 通过<code>$</code>设置环境变量</td>
<td>设置环境变量. 需要注意的是 xonsh 中的环境变量和底层 python 的 os.environ 是默认不同步的, 若要同步需要将 <code>$UPDATE_OS_ENVIRON</code> 设为 <code>True</code></td>
</tr>
<tr>
<td><code>unset NAME</code></td>
<td><code>del $NAME</code></td>
<td>取消环境变量</td>
</tr>
<tr>
<td><code>echo "$HOME/hello"</code> <code>$</code> 后内容可以被解析</td>
<td><code>echo "$HOME/hello"</code></td>
<td>使用环境变量构造参数</td>
</tr>
<tr>
<td><code>echo $HOME/$(uname)</code></td>
<td><code>echo @($HOME + '/' + $(uname))</code></td>
<td>连接字符串</td>
</tr>
<tr>
<td><code>echo 'my home is $HOME'</code>, <code>$</code> 后内容并不会被解析</td>
<td><code>echo @("my home is $HOME")</code></td>
<td>阻止像环境变量引用的字符串被解析</td>
</tr>
<tr>
<td><code>${!VAR}</code> 等通过 <code>$</code> 的间接变量引用</td>
<td><code>${var or expr}</code></td>
<td>通过另一个变量或表达式的值作为名称间接引用环境变量. 在 xonsh 中, 这可以是任何有效的表达式.</td>
</tr>
<tr>
<td><code>ENV1=VAL1 command</code></td>
<td><code>$ENV1=VAL1 command</code> 或 <code>with @.env.swap(ENV1=VAL1): command</code></td>
<td>设置临时环境变量并执行命令. 使用第二种带缩进块的表示法可在同一上下文中执行多条命令.</td>
</tr>
<tr>
<td><code>alias ll='ls -la'</code></td>
<td><code>aliases['ll'] = 'ls -la'</code></td>
<td>xonsh 中的别名可以是作为字符串或参数列表的子进程命令, 也可以是任何 Python 函数.</td>
</tr>
<tr>
<td><code>$(cmd args)</code> 或 <code>`cmd args`</code></td>
<td><code>@$(cmd args)</code></td>
<td>命令替换(允许命令的输出替换该命令本身). 将子进程命令的输出标记化并作为另一个子进程执行.</td>
</tr>
<tr>
<td><code>v=`echo 1`</code> <code>v=$(echo 1)</code></td>
<td><code>v=$(echo 1)</code></td>
<td>在 bash 中, 反引号表示运行捕获的子进程——在 xonsh 中对应 <code>$()</code>. xonsh 中的反引号表示正则表达式 globbing(即 <code>ls `/etc/pass.*`</code>).</td>
</tr>
<tr>
<td><code>echo -e "\033[0;31mRed text\033[0m"</code></td>
<td><code>printx("{RED}Red text{RESET}")</code></td>
<td>xonsh 让你可以尽可能简单地打印彩色文本</td>
</tr>
<tr>
<td><code>shopt -s dotglob</code></td>
<td><code>$DOTGLOB = True</code></td>
<td>使用 <code>*</code> 或 <code>**</code> 进行 globbing 时也会匹配点文件, 或那些名称以字面量 <code>.</code> 开头的“隐藏”文件. 与 bash 类似, 默认情况下此类文件会被过滤掉.</td>
</tr>
<tr>
<td><code>if [ -f "$FILE" ];</code></td>
<td><code>p'/path/to/file'.exists()</code> 或 <code>pf'{file}'.exists()</code> 或 <code>if !(test -f $FILE):</code></td>
<td>可以实例化 Path 对象并使用 p-string 语法直接检查</td>
</tr>
<tr>
<td><code>set -e</code></td>
<td><code>$RAISE_SUBPROC_ERROR = True</code></td>
<td>在非零返回码后引发失败. xonsh 将引发 <code>subprocess.CalledProcessError</code>.</td>
</tr>
<tr>
<td><code>set -x</code></td>
<td><code>trace on</code> 和 <code>$XONSH_TRACE_SUBPROC = True</code></td>
<td>在执行期间启用对 xonsh 源代码行的跟踪.</td>
</tr>
<tr>
<td><code>&amp;&amp;</code></td>
<td><code>&amp;&amp;</code> 或 <code>and</code></td>
<td>子进程的逻辑与运算符</td>
</tr>
<tr>
<td><code>||</code></td>
<td><code>||</code> 以及 <code>or</code></td>
<td>子进程的逻辑或运算符</td>
</tr>
<tr>
<td><code>$$</code></td>
<td><code>os.getpid()</code></td>
<td>获取当前 shell 的 PID</td>
</tr>
<tr>
<td><code>$?</code></td>
<td><code>@.lastcmd.rtn</code></td>
<td>返回上一条命令的退出码或状态. 要在 xonsh 脚本中获取命令的退出码, 对于非交互式进程请使用 <code>!().rtn</code>.</td>
</tr>
<tr>
<td><code>$&lt;n&gt;</code></td>
<td><code>$ARG&lt;n&gt;</code></td>
<td>索引 n 处的命令行参数, 因此 <code>$ARG1</code> 相当于 <code>$1</code></td>
</tr>
<tr>
<td><code>$@</code></td>
<td><code>$ARGS</code></td>
<td>所有命令行参数和字符串的列表</td>
</tr>
<tr>
<td><code>while getopts</code></td>
<td>使用 <code>argparse</code> 或 <code>click</code></td>
<td>另请参阅 awesome-cli-app 和 xontrib-argcomplete</td>
</tr>
<tr>
<td><code>complete</code></td>
<td><code>completer list</code></td>
<td>与许多其他 shell 一样, xonsh 在按下 Tab 键时能够补全部分指定的参数.</td>
</tr>
<tr>
<td><code>IFS</code></td>
<td><code>$XONSH_SUBPROC_OUTPUT_FORMAT</code></td>
<td>更改输出表示和拆分. 另请查看文档中 <code>DecoratorAlias</code> 以获取返回对象的能力, 例如 <code>j = $(@json echo '{}')</code>.</td>
</tr>
<tr>
<td>将补全显示为列表</td>
<td><code>$COMPLETIONS_DISPLAY = 'readline'</code></td>
<td>显示补全将模拟比较原始的 readline 的行为</td>
</tr>
<tr>
<td><code>docker run -it bash</code></td>
<td><code>docker run -it xonsh/xonsh:slim</code></td>
<td>真的有人用容器只为跑一个 shell 吗?</td>
</tr>
<tr>
<td><code>exit 1</code></td>
<td><code>exit 1</code> 或 <code>exit(1)</code></td>
<td>退出当前脚本</td>
</tr>
</tbody>
</table>
<h3 id="更多内容">更多内容</h3>
<p>除了上文提及的特性之外, xonsh 还有可编程的事件系统与补全系统等使你可以对 shell 本身和补全进行 hook, 但这牵扯到 xonsh 的源代码行为, 本文只作脚本编写教程与抛砖引玉之用, 欲知更多信息, 还请参阅 xonsh 的官方文档.</p>
<p>本文完 :3</p><br><br>
来源:https://www.cnblogs.com/pluvium27/p/19856400
頁: [1]
查看完整版本: 记对 xonsh shell 的使用, 脚本编写, 迁移及调优