<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://hedzr.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://hedzr.com/" rel="alternate" type="text/html" /><updated>2026-04-03T07:50:47+08:00</updated><id>https://hedzr.com/feed.xml</id><title type="html">hzSomthing</title><subtitle>There&apos;s something, recording by hedzr.</subtitle><author><name>hedzr</name></author><entry><title type="html">Claude Code 泄露我所想</title><link href="https://hedzr.com/life/style/claude-code-sources-leaked-and-more/" rel="alternate" type="text/html" title="Claude Code 泄露我所想" /><published>2026-04-01T08:00:00+08:00</published><updated>2026-04-01T09:00:00+08:00</updated><id>https://hedzr.com/life/style/claude-code-sources-leaked-and-more</id><content type="html" xml:base="https://hedzr.com/life/style/claude-code-sources-leaked-and-more/"><![CDATA[<h2 id="claude-code-源码泄露事件">Claude Code 源码泄露事件</h2>

<h3 id="原始信源">原始信源</h3>

<table>
  <tbody>
    <tr>
      <td><a href="https://www.hndigest.com/m/RbztJyVBMsODLoST_v9B2w==/s/613237">Claude Code’s source code has been leaked via a map file in their NPM registry</a>[twitter.com</td>
      <td>points: 1786</td>
      <td>comments: 878](https://www.hndigest.com/m/RbztJyVBMsODLoST_v9B2w==/c/613237)</td>
    </tr>
  </tbody>
</table>

<p><img src="https://cdn.jsdelivr.net/gh/hzimg/blog-pics@master/upgit/2026/04/20260401_1775008681.png" alt="image-20260401095756200" /></p>

<blockquote>
  <p>Chaofan Shou / @Fried_rice</p>

  <p>https://x.com/Fried_rice</p>

  <p>Claude code source code has been leaked via a map file in their npm registry!  Code: <a href="https://t.co/jBiMoOzt8G">https://pub-aea8527898604c1bbb12468b1581d95e.r2.dev/src.zip</a></p>

  <p><img src="https://cdn.jsdelivr.net/gh/hzimg/blog-pics@master/upgit/2026/04/20260401_1775008630.jpeg" alt="Image" /></p>

  <p><a href="https://x.com/Fried_rice/status/2038894956459290963">4:23 PM · Mar 31, 2026</a> <strong>25.1M</strong> Views</p>
</blockquote>

<h3 id="时间线">时间线</h3>

<ul>
  <li>2026-03-31 凌晨：Anthropic发布Claude Code v2.1.88版本。</li>
  <li>2026-03-31 凌晨：npm包中意外包含57-60MB的cli.js.map（Source Map文件），任何人安装该版本即可完整还原所有TypeScript源码。</li>
  <li>2026-03-31 08:23：安全研究员Chaofan Shou (@Fried_rice) 在X上首发披露，附带src.zip下载链接，帖子迅速破百万浏览。</li>
  <li>上午10点后：GitHub上出现多个镜像仓库，星标几小时内从0冲到2万+；系统提示词、Undercover模式、权限系统等核心逻辑被迅速提取。</li>
  <li>目前状态 04-01 07:xx：Anthropic刚刚推送了修复版本并撤销了v2.1.88，但镜像和解读已经独自存活。</li>
</ul>

<h3 id="之我见">之我见</h3>

<p>对于 Claude Code 来说，这可能是个意外，也可能是一个莫测的局。</p>

<p>即使这是个意外，CC 也能够很轻易地消弭负面分数。既然它们原本掌控着 CC 源代码前后端的整体架构，那么调整 cli 前端的实现架构也是一件很轻松的事情，因为这一部分子系统的占比大概也只有整体架构的 10% ~ 20%，应该不可能更多了。</p>

<p>如果我是 CC 掌门人，CLI 前端正进行一次翻江倒海的全面重构，并且这个重构的流程实质上已经开始了数月，并且到达了稳定的状态，只需要给予一定的时间进行沉淀。那么此时旧版本最后一次发布增量更新版，却发生了泄露，又有什么可怕的呢？或者这正是有意而为之，正是为了要期待竞争对手误判，指导他们走向错误的方向，这种可能性大不大呢。</p>

<p>无从判定，内幕太少，烟雾甚多。</p>

<h3 id="aiagi">AI，AGI</h3>

<p>作为自以为掌握了编码与架构这件事的整体精髓的人，我不认为现阶段的 AI 是 AI。这只不过是又一个劣币驱逐良币的例证。</p>

<p>2005-2017 年间，NLP 技术得到了长足的发展，这时候没有学者敢于将其拿出来冠以 AI 之名。后来，突然就如此了，这是多方面因素导致的：商人，如黄等，需要卖显卡，政客，需要发起新的军备竞赛来诱导东大解体，等等。</p>

<blockquote>
  <p>上述时间段是一个概数，没有查证确切。本来就只是随想。</p>
</blockquote>

<p>这就是现在这些“AI”的背景。</p>

<p>它们并不是一无是处，70 分的伪 AI 给你提供的“伪”建议还是像模像样地具备一定的参照性的。上世纪 90 年代的蚁群思想，现在也被无人机规模化军事 AI 所使用，用来更富有效率地杀人，挺好的——必欲使其疯狂。这种蚁群思想，实际上是衍生于六七十年代的群体智慧理论，只是在九十年代因为一些原因而被大众所知。</p>

<p>但是，获得这 70 分的代价就不正常了，电力、算力、社会成本等等。</p>

<p>当你意识到作为意识的唯一代言人，人类，它们的大脑的功率仅仅是数瓦特的功耗的时候（即使爱因斯坦的脑功耗大约也只不过是这个挡位），你才会明白现阶段这些“AI”们，以及种种相互蒸馏的复制品们，统统都是笑话。现阶段的研究表明，人类大脑的功耗大概在 20W 以内，其中约 9W 以热量形式耗散，皮层灰质约 3W，其余能量才是大脑思考和记忆储存所需。</p>

<p>“涌现”，是最可笑的用词。可惜了这么好的一个词，它的本意是指超出意料的收获。但在 AI 大模型里面，它成了你不知道如何发生的，我也不知道如何发生的，但我们需要一个说法，我们需要创造一个新词来 cover 这些尴尬的状况，的遮羞布。</p>

<p>可怜！</p>]]></content><author><name>hedzr</name></author><category term="life" /><category term="style" /><category term="life" /><category term="ai" /><category term="leak" /><summary type="html"><![CDATA[随意想想……]]></summary></entry><entry><title type="html">我是即将到来的日子</title><link href="https://hedzr.com/lifestyle/records/i-am-the-day-soon-to-be-born/" rel="alternate" type="text/html" title="我是即将到来的日子" /><published>2026-02-16T13:00:00+08:00</published><updated>2026-02-16T13:25:00+08:00</updated><id>https://hedzr.com/lifestyle/records/i-am-the-day-soon-to-be-born</id><content type="html" xml:base="https://hedzr.com/lifestyle/records/i-am-the-day-soon-to-be-born/"><![CDATA[<p>作为全年终结的最后一日，不由得忆及小时候读过的<em>约翰·克利斯朵夫</em>了。</p>

<blockquote>
  <p>开头半句前不久用过，但那是公历年的终结；今次是农历新年前夜，除夕夜，并不算误用。</p>
</blockquote>

<p>我记得全书结尾，约翰·克利斯朵夫濒死时的升华，是背着一个小孩子过河。</p>

<blockquote>
  <p>快要倒下来的克利斯朵夫终于到了彼岸。于是他对孩子说：<br />
“咱们到了！唉，你多重啊！孩子，你究竟是谁呢？”<br />
孩子回答说：<br />
“我是即将来到的日子。”</p>
</blockquote>

<p>我记得的，就是这最后一句话。</p>

<p>嗯，我记得的是“到来”，但傅雷的译文是“来到”。</p>

<blockquote>
  <p>Almost falling Christophe at last reaches the bank, and he says to the Child:<br />
“Here we are! How heavy thou wert! Child, who art thou?”<br />
And the Child answers:<br />
“I am the day soon to be born.”</p>
</blockquote>

<p>这本书是罗曼·罗兰所著，傅雷翻译。我小时候在爸爸的书架上翻下来看完的，半懂不懂。少年时代我曾多次复读，克利斯朵夫的形象和贝多芬有一定的重合。</p>

<p>那时候我第一次听交响乐，磁带上是贝多芬第一和第五交响曲，我翻来覆去地听，直到每个音符背下来为止，为此我训练自己的听力，音准，节奏，学习乐理，……，成年后凭着记忆听了很多版本的贝一贝五，并且确定了少年时的磁带是卡拉扬指挥的。</p>

<p>一个人幼年所涉，对其一生的确有很大的影响。</p>

<p>我的童年还跟随父亲深入学习书法篆刻国画。</p>

<p>当然，这部分爱好在高考时粉碎了。不是什么其它原因，只是简简单单的体检时红绿色弱。</p>

<p>人生的荒诞，有时候只是以玩笑出现。</p>

<p>所以我们也并不绝望。</p>

<p>现在，我的一生基本上着落在写代码上了，基本上这个爱好起始于初二时代，我在杂志上看到一篇文章，描述的是一个美国的天才儿童搞黑客引来抓捕的故事，那个儿童玩蓝盒子，调制解调器拨号，做很多事情。
然后我又碰巧得到一本微机书籍，大体上就是这么入坑的。</p>

<p>那是 1985 年的事情了。</p>

<p>OK.</p>

<p>同样令我记忆深刻的小说结束语，是围城。</p>

<blockquote>
  <p>那只祖传的老钟从容自在地打起来，仿佛积蓄了半天的时间，等夜深人静，搬出来一
一细数：“当、当、当、当、当、当”响了六下。六点钟是五个钟头以前，那时候鸿渐在回
家的路上走，蓄心要待柔嘉好，劝他别再为昨天的事弄得夫妇不欢；那时候，柔嘉在家里等
鸿渐回家来吃晚饭，希望他会跟姑母和好，到她厂里做事。这个时间落伍的计时机无意中包
涵对人生的讽刺和感伤，深于一切语言、一切啼笑。</p>
</blockquote>

<p>这也是我高中时分读的。对于高中生来说，1985 的高中生实在是太纯洁了，对于这种小说根本没有抵抗力。</p>

<p>即使是今天，我回忆这本小说，依然认为它不仅仅是对年轻的我有力量，对于现在的我也不会失去色彩：</p>

<p>一小时前我曾抱有希望打算怎么样，但一小时后任何动作都是徒劳，只有自己的煎熬一分一秒还在度量。</p>

<p>有多少次你会遇到这样的情形呢？</p>

<p>一个人一生中连这样的煎熬都没遇到过，大概也并不是什么幸运吧。</p>

<p>我说不定会称之为白过的一生。</p>

<p>然而，没有白过的一生，你过的好吗？</p>

<p>🔚</p>]]></content><author><name>hedzr</name></author><category term="lifestyle" /><category term="records" /><category term="life" /><category term="style" /><summary type="html"><![CDATA[记忆里的约翰·克利斯朵夫，等等；新年夜记述 ...]]></summary></entry><entry><title type="html">对 dotfiles 备份的几种方法</title><link href="https://hedzr.com/devops/bash/about-dotfiles/" rel="alternate" type="text/html" title="对 dotfiles 备份的几种方法" /><published>2025-12-31T01:00:00+08:00</published><updated>2025-12-31T12:25:00+08:00</updated><id>https://hedzr.com/devops/bash/about-dotfiles</id><content type="html" xml:base="https://hedzr.com/devops/bash/about-dotfiles/"><![CDATA[<p>作为全年终结的最后一日，姑且将 dotfiles 备份问题梳理一遍记录下来吧。</p>

<h2 id="dotfiles-管理">dotfiles 管理</h2>

<h3 id="配置文件有哪些">配置文件有哪些</h3>

<p>对于 macOS/Linux 作为工作环境的人来说，自己惯用的工作环境基本上可以包含如下这些文件：</p>

<ul>
  <li>Shell 环境文件，例如 .bashrc，.zshrc 等等</li>
  <li>终端应用的配置，通常在 ~/.config 中，或者 ~/.npmrc 这样，也有的在 ~/.local/share/nvim 这样的文件夹中</li>
  <li>XDG 文件夹，例如 XDG_CONFIG_DIR 和 XDG_DATA_DIR，实际上在大多数 Linux 和 macOS，它们被解释到 <code class="language-plaintext highlighter-rouge">~/.config/&lt;app&gt;</code> 和 <code class="language-plaintext highlighter-rouge">~/.local/share/&lt;app&gt;</code> 等位置。</li>
</ul>

<p>对于 macOS 来说，下面的位置也可能包含 app 的有效配置数据：</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">~/Library/Preferences</code></li>
  <li><code class="language-plaintext highlighter-rouge">~/Library/Application Support</code></li>
</ul>

<p>少数 macOS apps 可能需要系统级别的配置数据如下列位置：</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">/Library/Preferences</code></li>
  <li>…</li>
</ul>

<p>例如 VMware Fusion 就需要系统级别的关于虚拟网络设置方面的配置数据在 <code class="language-plaintext highlighter-rouge">/Library/Preferences</code> 中。</p>

<h3 id="如何备份它们">如何备份它们</h3>

<p>对于上面这些位置的配置数据来说，比较流行的备份与恢复方案有以下三种</p>

<ul>
  <li>GNU Stow - link mode</li>
  <li>mackup - copy mode</li>
  <li>barerepo+alias (裸仓库别名法)</li>
</ul>

<p>Mackup 其实有两种工作模式：copy 模式和 link 模式。</p>

<p>copy 模式将 dotfilles 复制到另一地点，实现的是一种备份机制；link 模式则是将 dotfiles 移动到另一位置，然后反向链接回到原位置，从而将各种 dotfiles 的原始内容集中到一个统一的地点，以便于纳入 git 管理。</p>

<p>mackup 的 copy 模式不仅仅备份你的 <code class="language-plaintext highlighter-rouge">.config/fish</code>这样的目录，同时也能够备份你的 macOS apps preferences，且能深入备份诸如 Typora，Brave Browser 等等的专有 preferences（通常存储于 <code class="language-plaintext highlighter-rouge">~/Library/Application Support</code> 之中）。</p>

<p>Stow 完全实施 link 模式。</p>

<h3 id="裸仓库别名法">裸仓库别名法</h3>

<p>ArchWiki 推荐的是另一种名为裸仓库别名法的方案，这种方案在 <code class="language-plaintext highlighter-rouge">$HOME/.dotfiles</code> 建立一个 git bare repository，然后将诸如 <code class="language-plaintext highlighter-rouge">~/.config/fish</code>，<code class="language-plaintext highlighter-rouge">~/.bashrc</code> 以及 <code class="language-plaintext highlighter-rouge">~/zshrc</code> 等配置文件 add 到该仓库中纳入版本化管理体系。为了便于操作，通常你应该像这样初始化：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="nv">$ </span>git init <span class="nt">--bare</span> ~/.dotfiles
<span class="nv">$ </span><span class="nb">alias </span><span class="nv">dotfiles</span><span class="o">=</span><span class="s1">'/usr/bin/git --git-dir="$HOME/.dotfiles/" --work-tree="$HOME"'</span>
<span class="nv">$ </span>dotfiles config status.showUntrackedFiles no
</pre></td></tr></tbody></table></code></pre></div></div>

<p>其中 alias 命令应该被加入到你的 <code class="language-plaintext highlighter-rouge">~/.bashrc</code> 以及 <code class="language-plaintext highlighter-rouge">~/zshrc</code> 文件中以便今后使用。</p>

<p>上面的命令序列完成了 <code class="language-plaintext highlighter-rouge">$HOME/.dotfiles</code> 的初始化，<code class="language-plaintext highlighter-rouge">showUntrackedFiles=no</code> 的设定是为了防止 git 尝试去列举 HOME 文件夹下的其它子文件夹，这可能会是相当庞大的和耗费性能与时间的。</p>

<p>然后，例如你需要备份 fish 配置，那么执行：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre>dotfiles add ~/.config/fish
dotfiles commit <span class="nt">-m</span> <span class="s1">'added fish config files'</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>就可以了。</p>

<p>当你的 fish 配置修改后，使用 <code class="language-plaintext highlighter-rouge">dotfiles</code> status 能够查看到相应的变更，此时：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre>dotfiles add <span class="nb">.</span>
dotfiles commit <span class="nt">-m</span> <span class="s1">'fish config modified: updated background color'</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>就可以版本化存储上述变更。</p>

<p>无需担忧 <code class="language-plaintext highlighter-rouge">~/.config</code> 下其它文件夹的干扰，无需额外配置 .gitignore 文件去排除 <code class="language-plaintext highlighter-rouge">~.config</code>下的其它文件夹。</p>

<p>对于 bash 配置文件也可以同样处理：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre>dotfiles add ~/.bashrc ~/.bash_profile ~/.profile
dotfiles commit <span class="nt">-m</span> <span class="p">;</span>added bash config files<span class="s1">'
</span></pre></td></tr></tbody></table></code></pre></div></div>

<p>你甚至也可以版本化命令历史：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre>dotfiles add ~/.bash_history
dotfiles commit <span class="nt">-m</span> <span class="p">;</span>added bash <span class="nb">history </span>files<span class="s1">'
</span></pre></td></tr></tbody></table></code></pre></div></div>

<p>不过，这个文件的变化非常频繁，因为你每输入一条命令它就会储存一次变化（不考虑性能原因的内部缓存的话），所以将其纳入管理需要审慎决定。</p>

<h4 id="参考">参考</h4>

<ul>
  <li>
    <p><a href="https://wiki.archlinuxcn.org/zh-hk/Dotfiles">dotfiles - Arch Linux 中文维基</a></p>
  </li>
  <li>
    <p><a href="https://news.ycombinator.com/item?id=11071754">Ask Hacker News: What do you use to manage your dotfiles?</a></p>
  </li>
  <li>
    <p><a href="https://gitlab.com/dwt1/dotfiles">Derek Taylor / Dotfiles · GitLab</a></p>

    <p>DT 介绍了裸仓库别名法的一种实作方法，并且提供了他自己的 dotfiles 仓库供你验证。</p>
  </li>
  <li>
    <table>
      <tbody>
        <tr>
          <td>[How to Store Dotfiles - A Bare Git Repository</td>
          <td>Atlassian Git Tutorial](https://www.atlassian.com/git/tutorials/dotfiles)</td>
        </tr>
      </tbody>
    </table>
  </li>
  <li>
    <p><a href="https://distrotube.com/guest-articles/managing-dotfiles-with-rcm.html">Managing dotfiles with style with rcm</a> (Ronnie Nissan)</p>
  </li>
  <li><a href="https://distrotube.com/guest-articles/interactive-dotfile-management-dotbare.html">Interactive dotfile management with dotbare</a> (Kevin Zhuang)</li>
</ul>

<h3 id="stow-方法---link-模式">Stow 方法 - link 模式</h3>

<p>按照 GNU Stow 自己的想法，它是一个软件包发布时刻的符号链接管理器，它是为了便于在按照 GNU 软件包如 GNU emacs 等的时候方便地建立符号链接的工具。对于 emacs 而言，其所有的 released files 通常被安装到 <code class="language-plaintext highlighter-rouge">/usr/local/emacs</code> 之下，并构成如下的目录结构：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre>/usr/local/emacs
  bin
  include
  share
    man
</pre></td></tr></tbody></table></code></pre></div></div>

<p>而在安装时刻，stow 可以帮助建立 <code class="language-plaintext highlighter-rouge">/usr/local/emacs/bin/emacs</code> 到 <code class="language-plaintext highlighter-rouge">/usr/bin/emacs</code> 的符号连接，类似地，<code class="language-plaintext highlighter-rouge">/usr/local/emacs/share/man/man1</code> 也被链接到 <code class="language-plaintext highlighter-rouge">/usr/man/man1/emacs</code>  以便于系统能够搜索到 emacs 所提供的 macpages。</p>

<p>但是同时，stow 也能被拓展应用到管理 dotfiles。</p>

<p>前面也扼要介绍过，可以通过 <code class="language-plaintext highlighter-rouge">stow --adopt fish</code> 将 <code class="language-plaintext highlighter-rouge">$HOMNE/.config/fish</code> 的相关配置文件移动到 target dir，对于 target dir = <code class="language-plaintext highlighter-rouge">$HOME/dotfiles</code> 来说，该命令将会移动 fish 文件夹中的文件到 <code class="language-plaintext highlighter-rouge">$HOME/dotfiles/fish/</code> 之下，然后逐一反向符号链接到原来的位置。例如 <code class="language-plaintext highlighter-rouge">$HOME/.config/fisg/config.fish</code> 会被移动为 <code class="language-plaintext highlighter-rouge">$HOME/dotfiles/fish/config.fish</code>，然后新的符号链接文件 <code class="language-plaintext highlighter-rouge">$HOME/.config/fisg/config.fish</code>  将被建立为指向到 <code class="language-plaintext highlighter-rouge">$HOME/dotfiles/fish/config.fish</code>，从而完成转换过程。</p>

<p>你可以将 fish，bash，zsh，nvim，mc 等软件的配置文件一一 adopt 到自己的 target dir 之下。当这一过程全部完成之后，你的 dotfiles 文件的内容实体就全部被移入了 <code class="language-plaintext highlighter-rouge">$HOME/dotfiles</code> 之下了。现在，你无需再去做其它的工作了，只需要将 <code class="language-plaintext highlighter-rouge">$HOME/dotfiles</code> 建立为一个 git repo，就可以集中管理这些配置文件的变更了。</p>

<p><a href="https://www.gnu.org/software/stow/">gnu.org/software/stow</a> 也有一系列辅助命令来帮助进行相应的管理工作。例如：</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">-d</code> 指定 stow 文件夹，通常为 <code class="language-plaintext highlighter-rouge">$HOME</code></li>
  <li><code class="language-plaintext highlighter-rouge">-t</code> 指定 target 文件夹，取决你的需要，但常常为 <code class="language-plaintext highlighter-rouge">$HOME/dotfiles</code></li>
  <li><code class="language-plaintext highlighter-rouge">-D</code> 移除已创建的文件树</li>
  <li><code class="language-plaintext highlighter-rouge">-S</code> 创建指定的文件树</li>
  <li><code class="language-plaintext highlighter-rouge">-R</code> 移除并重新创建指定的文件树</li>
  <li><code class="language-plaintext highlighter-rouge">--ignore=regexp</code> 忽略<code class="language-plaintext highlighter-rouge">stow dir</code>下指定匹配模式的文件</li>
  <li><code class="language-plaintext highlighter-rouge">--defer=regexp</code> 跳过<code class="language-plaintext highlighter-rouge">target dir</code>下指定匹配模式的文件</li>
  <li><code class="language-plaintext highlighter-rouge">--override=regexp</code> 强制替换<code class="language-plaintext highlighter-rouge">target dir</code>下指定匹配模式的文件</li>
  <li><code class="language-plaintext highlighter-rouge">--no-folding</code> stow 默认创建最少的符号链接。这一选项会使 stow 逐一创建每一个文件的符号链接，而不是创建一整个文件夹的链接。</li>
  <li><code class="language-plaintext highlighter-rouge">--dotfiles</code> 在 stow dir 下的文件名如果有<code class="language-plaintext highlighter-rouge">dot-</code>前缀，在创建链接时，链接名字会替换为以<code class="language-plaintext highlighter-rouge">.</code>为前缀， 比如：<code class="language-plaintext highlighter-rouge">～/.zshrc -&gt; dotfiles/zsh/dot-zshrc</code></li>
</ul>

<p>GNU Stow 有一定的破坏性，你需要小心解决冲突，并记住不要重复 adopt 某个配置文件夹。</p>

<h3 id="mackup-方法---copy-模式">Mackup 方法 - copy 模式</h3>

<p>Mackup 是专门进行配置文件备份与恢复的工作的工具。</p>

<p>我们已经提及 Mackup 有两种备份逻辑，即 copy 模式和 link 模式。</p>

<p>Link 模式工作类似于 Stow 所做的那样，将配置文件移入 target file 然后符号链接回去。这种模式通常使用如下一组命令：</p>

<ul>
  <li>
    <p><code class="language-plaintext highlighter-rouge">mackup backup</code></p>

    <p>进行备份操作</p>
  </li>
  <li>
    <p><code class="language-plaintext highlighter-rouge">mackup restore</code></p>

    <p>进行数据的恢复</p>
  </li>
  <li>
    <p><code class="language-plaintext highlighter-rouge">mackup list</code></p>

    <p>查看支持的软件列表</p>
  </li>
  <li>
    <p><code class="language-plaintext highlighter-rouge">mackup -h</code></p>

    <p>查看帮助命令</p>
  </li>
  <li>
    <p><code class="language-plaintext highlighter-rouge">mackup uninstall</code></p>

    <p>将配置文件拷贝回原来的系统目录并放弃备份副本</p>
  </li>
</ul>

<p>此外，你需要在 <code class="language-plaintext highlighter-rouge">~/.mackup.cfg</code> 中指定 target dir，当然也可以在命令行参数中指定。</p>

<p>Copy 模式则不会修改你的原始 dotfiles，而是将它们复制到目的地（除了指定本地文件夹之外，也可以指定 iCloud，Google Drive，Dropbox 等等网络存储位置）。</p>

<p>Mackup 作为专门备份配置文件的工作，有一套自己的逻辑，即通过建立 <code class="language-plaintext highlighter-rouge">~/mackup/app.cfg</code> 的方式来自定义你的 app 应该有哪些文件被收集和备份。例如 Kitty 终端的配置文件 kitty.cfg 可以这样自定义：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="rouge-code"><pre><span class="o">[</span>application]
name <span class="o">=</span> kitty

<span class="o">[</span>configuration_files]
<span class="c"># .config/kitty</span>

<span class="o">[</span>xdg_configuration_files]
kitty
</pre></td></tr></tbody></table></code></pre></div></div>

<p>而 ssh 的配置文件可以定义如下：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="o">[</span>application]
name <span class="o">=</span> SSH

<span class="o">[</span>configuration_files]
.ssh
</pre></td></tr></tbody></table></code></pre></div></div>

<p>如此一来，无论你的 apps 有什么样奇葩的位置，都可以被纳入 mackup 的管理范畴中。</p>

<p>更为友好的消息是，mackup 有庞大的社区贡献的配置集合，多数都被整合到了 mackup 的安装包中，安装后即可得。所以大多数常见的软件的配置文件在哪里，如何被收集都已经设定好了，无需你再来自行重定义了。</p>

<p>当然如果你有一个小众软件，就可能需要上面的方法来自行定义一个了。</p>

<p>另外一个特色是，例如你在使用 macOS，那么 GUI apps，例如 Google Chrome，Brave Browser，Typora，VSCode，VSCode Insider，VSCodium，Sublime Text，TextMate 等等均能够被自动备份。Mackup 能够深入到 <code class="language-plaintext highlighter-rouge">$HOME/Library/Preferences</code> 和 <code class="language-plaintext highlighter-rouge">$HOME/Library/Application Support</code> 中去收集这些 apps 的配置文件并加以备份，而不是仅仅限于 <code class="language-plaintext highlighter-rouge">$HOME/.config</code> 之下。</p>

<p>对于 Copy 模式来说，通常你需要执行命令：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>mackup <span class="nt">--verbose</span> <span class="nt">--force</span> backup
</pre></td></tr></tbody></table></code></pre></div></div>

<p>它将会收集上述的各种配置数据，然后备份到指定的 target，无论你指定了什么样的 provider。</p>

<h2 id="比较">比较</h2>

<p>Mackup 适用于如下场景：</p>

<ul>
  <li>需要全面备份各类 apps（terminal or gui）的 macOS 配置数据
    <ul>
      <li>实际上也适合于 windows 或者 linux</li>
    </ul>
  </li>
  <li>具有多态 macOS 设备并希望共享配置数据，或者希望实时同步变更</li>
  <li>Linux 工作环境</li>
</ul>

<p>Mackup 可以向多种 storage providers 实施备份工作，包括 iCloud，S3，Dropbox 等等，同时它支持自动识别相当一部分 gui apps（例如 Chrome 的 settings）的配置数据，而且社区贡献了大量的 apps 的配置文件提取方案（这些方案被整合到 Mackup 中，所以才能自动识别出不同 apps 的配置文件在哪里能被找到），实际上你也往往会需要对个人的特殊数据编辑方案来提示 Mackup 对其进行备份。</p>

<p>由于这些原因，Mackup 成为独一无二的备份软件。</p>

<p>然而， Mackup 像云存储备份的时候，存在一些边界性的问题。</p>

<blockquote>
  <p>在向 iCloud、Dropbox、Google Drive 等公网同步存储备份时，需要特别注意不要连续发布 mackup backup 命令，因为过于密集的 mackup backup 命令可能会导致将相同的文件在 iCloud Storage 的本地映射文件夹中重复创建数个副本（例如重复拷贝 config 可能导致 config 2 新文件被创建）。</p>

  <p>这是因为 iCloud Storage 可能尚未实施该文件（例如 config）的上传任务，所以该文件在本地映射文件夹中可能是一个名为 .config.xS6dhkZQ 的占位符文件，这就导致第二次 mackup backup 指令判定需要重复复制一份到 iCloud Storage 中，此时 iCloud Storage 接收到新请求的同时发现 config 是存在的（存在于上传队列中和虚拟视图中），于是就是创建一个副本文件 config 2。</p>

  <p>这不是仅有 iCloud 的错误或者存在的问题，所有的远程存储都难免发生这样的时间差因素的错误或者问题。当你越过本地文件系统和远程存储的本地映射盘边界进行大批量文件复制操作时，上述问题较为容易发生，因为文件系统的拦截器将会拦住你的请求，重新解释为远程存储的推送请求，中间的时间差问题是导致错误的根源，且这样的根源性问题基本上是无解的。</p>

  <p>解决的办法是降低你的手动行为的频次。</p>

  <p>而对于由系统管理的正常的修改和更新，iCloud 等远程存储并不会出错，因为通过 File System 的变更日志报告，它们并不会获得不正确的请求。</p>

  <p>上述错误的技术性探讨涉及到 File System，FUSE，inode 等操作系统中的特定子系统的 hook 与实现细节，本文不再加以展开。</p>
</blockquote>

<p>Mackup 主要的 copy mode 的能力，从技术上存在如下问题：</p>

<ol>
  <li>无法实时备份</li>
  <li>借助于云存储实现的全自动备份可能存在重名隐患</li>
  <li>你可能需要设置 crontab 计划任务来自动化备份</li>
  <li>采用类似于 inotify 的技术进行准实时备份，存在不稳定或通知事件丢失的问题</li>
</ol>

<p>对于 Stow 的 link 方式，存在一定的风险，你需要时刻谨记自己的原始文件在何处，当前出于何种状态，以防止某些误操作将原始文件清除：</p>

<ol>
  <li>app 不能正确识别 linked file 而发生错误</li>
  <li>app 无法正确识别 linked directory 从而产生较为严重的错误</li>
  <li>意外的操作导致 links 覆盖 originals，从而带来 contents 丢失</li>
</ol>

<h3 id="后记">后记</h3>

<p>如果你希望做 macOS 中配置文件的全面备份，则 Mackup copy 模式是优胜选项。备份的内容可以很方便地在你的多台 macOS 设备之间进行同步，并且非常安全。</p>

<p>如果你只有有限的配置文件和文件夹需要被集中管理，也 可以考虑采用 GNU Stow，简便易用，只需要牢记这些文件被集中的状态即可。</p>

<p>如果你的 dotfiles 可能有一定的规模，而且有时候你需要在各种设备之间共享一部分或全部这些配置文件时，那么裸仓库别名法可能是比较好的选择。</p>

<p>此外，我们还没有提及敏感信息的问题。</p>

<p><a href="https://github.com/AGWA/git-crypt">git-crypt</a> 提供一种透明加密的能力，可以让你的公开 repo 中的敏感信息不至于泄露。</p>

<p>🔚</p>]]></content><author><name>hedzr</name></author><category term="devops" /><category term="bash" /><category term="shell" /><category term="skills" /><summary type="html"><![CDATA[罗列几种流行的备份 dotfiles 的方法 ...]]></summary></entry><entry><title type="html">正则式简史</title><link href="https://hedzr.com/algorithm/regexp/history-of-regular-expression/" rel="alternate" type="text/html" title="正则式简史" /><published>2025-11-20T01:00:00+08:00</published><updated>2025-11-20T07:17:00+08:00</updated><id>https://hedzr.com/algorithm/regexp/history-of-regular-expression</id><content type="html" xml:base="https://hedzr.com/algorithm/regexp/history-of-regular-expression/"><![CDATA[<h2 id="正则式简史">正则式简史</h2>

<p>我在一篇旧文章 <a href="https://hedzr.com/golang/regexp/lets-talk-about-golang-regexp/"><em>从 Golang 正则式讲起</em></a> 中探寻了正则式的一些粗浅知识，主要是为了罗列语法，以便于个人速查。这是因为有时候搜索不是一个很好办的事情，如何搜索和去伪存真是一种技能，这就不展开说了。</p>

<p>要点是，我那篇旧文章在正则式简史方面没有认真考察整个历史渊源，所以将一些关系张冠李戴了，例如 RE2 的承接关系，PCRE 的发展线索等等。</p>

<p>所以早前某个时刻我修正了旧文章，简单地移除了简史部分。</p>

<p>但这当然就遗留了一个扣子在那儿而没有扣上。</p>

<p>今天，就是为了扣上它。</p>

<h3 id="历史渊源">历史渊源</h3>

<p>正则表达式，是术语 Regular Expression 的译名，它是描述字符串规律的一种表达式格式。历史上它有多种译名，例如规律表达式，规则表达式，正则表示式，常规表达式，等等。新世纪以来，在一些出版物中出现了正规表达式或其它的“创新型”译名，基本上我们认为这是一种哗众取宠的行为，然而时代的变迁导致一些词汇的演变又属正常，所以这些较新的译名的未来如何，仍需留待未来而定。</p>

<p>正则表达式在一般场所中常常被简称为正则式，这是常见的简略称呼，在英文环境中将其缩写为 regexp，regex，re 等等，均相当常见，另外缩略语 RE 也常常被使用。</p>

<p>正则式本身是随着 <a href="https://zh.wikipedia.org/wiki/%E5%BD%A2%E5%BC%8F%E5%8C%96%E8%AF%AD%E8%A8%80">形式化语言</a> 理论和 <a href="https://zh.wikipedia.org/wiki/%E8%87%AA%E5%8A%A8%E6%8E%A7%E5%88%B6">自动控制</a> 理论的发展而形成的，在学术界它通常被归属到<em>形式化语言与自动机</em>这样的课程（教材）中并成为一个基本章节，它通常是形式文法（Formal  Grammar）的一种有力的描述手段和实现手段。计算机科学中为了学习和理解编译原理这门课程，通常也有一个章节介绍正则式，并伴随着一系列的求解过程（例如 NFA 和 DFA 等）。</p>

<p>编译器领域中 LEX 和 YACC 所领导的“编译器的编译器”路线，其核心思想即是通过一批正则式所定义的产生式规则来描述一种高级语言的文法，然后通过自动机理论指导将该文法“转换”为编译器的词法分析和语法分析的 parser 部分，然后利用该 parser 来编译源代码并获得抽象语法树（AST），进而生成机器代码。</p>

<p>在编译原理中，将源代码中的关键字、字面量、控制逻辑等词法单位分解后以符号标记（token）代替，就得到了一系列符号的流，进一步的语义分析过程就是吃进这些 tokens，然后从初始状态不断因应 token 而切换到下一状态，直至所有 tokens 被吃完后来到终止状态。这个过程就是状态机的运转过程，它和正则式的匹配过程基本上是等价的，区别在于正则式匹配过程中吃进的是字符，例如 Unicode 字符或者 ASCII 字符，而编译器吃进的是源代码经过词法分析后得到的 tokens 流。但注意其中的要点在于，一个 token 实际上是通过一条正则式分析得到的，例如 <code class="language-plaintext highlighter-rouge">[a-zA-Z_][a-zA-Z0-9_]+</code> 这条正则式将会从源代码中匹配得到 <code class="language-plaintext highlighter-rouge">identifier</code> 这个标识符 token。</p>

<p>一个典型的正则式如 <code class="language-plaintext highlighter-rouge">ab*</code> 能够匹配这些的字符串 <code class="language-plaintext highlighter-rouge">a</code>, <code class="language-plaintext highlighter-rouge">ab</code>, <code class="language-plaintext highlighter-rouge">abb</code> 等等，而 <code class="language-plaintext highlighter-rouge">ab+</code> 能匹配的是 <code class="language-plaintext highlighter-rouge">ab</code>, <code class="language-plaintext highlighter-rouge">abb</code> 等等，区别在于后一个正则式要求 <code class="language-plaintext highlighter-rouge">b</code> 至少出现 1 次到多次。<code class="language-plaintext highlighter-rouge">[a-zA-Z_]</code> 代表着一个字符应该为字母和下划线，<code class="language-plaintext highlighter-rouge">[a-zA-Z0-9_]+</code> 代表着一系列字符应该为字母、数字和下划线。两者组合起来的正则式  <code class="language-plaintext highlighter-rouge">[a-zA-Z_][a-zA-Z0-9_]+</code> 就代表着标识符 token 应该是首字符为字母和下划线，后续字符为字母、数字和下划线。</p>

<p>正则式的由来已久，最早可以追溯到 1940 年，<a href="https://zh.wikipedia.org/wiki/沃伦·麦卡洛克">沃伦·麦卡洛克 / Warren Sturgis McCulloch</a> 与 <a href="https://zh.wikipedia.org/wiki/沃尔特·皮茨">小沃尔特·皮茨 / Walter Harry Pitts Jr.</a> 将 <a href="https://zh.wikipedia.org/wiki/神经系统">神经系统</a> 中的神经元描述成小而简单的自动控制元。这两位神人是计算神经科学家，实际上我其实不懂这门学科，只知道它是一种跨领域的学科，学成者即典型的复合型人才。</p>

<p>学界基本上认同上两位科学家尽管没有显式提出正则式和 NFA 的有关术语，但他们描述的自动控制元在渊源和行为等各方面都满足 NFA 状态推进的画像。</p>

<p>NFA（非确定性有限自动机）和非确定性概念，是由 迈克尔·拉宾（ <a href="https://en.wikipedia.org/wiki/Michael_O._Rabin">Michael O. Rabin</a>） 和 达纳·斯科特（<a href="https://en.wikipedia.org/wiki/Dana_Scott">Dana Scott</a>） 于 1959 年提出并完成了相关证明的，这一证明的关键之处在于确认了每一 DFA（确定性有限自动机） 状态都对应于一组 NFA 状态，换言之，任何一个 NFA 都可以等价转换为一个 DFA。两人因此获得了 1976 年图灵奖。</p>

<p>有限自动机是一种非常关键的概念。它的含义是一个自动机代表着一组状态的变化模式，有限是指总的步数是有限的，即自动机从开始状态起，受到事件的触发而迁移到下一状态，直至最终迁移的结束状态为止，这个过程中的迁移的总步数是有限的。换句话说，一个有限自动机在接收事件后总能在有限的步数内抵达其结束状态。而这一关键的“有限”实际上约束了整个迁移过程是“可计算的”，这在空间上和时间上保证了有限的资源能够使该自动机运转完毕。</p>

<p>非确定性是指给定起始状态，和给定事件的前提下，下一状态是否确定。对于 NFA 来说，同一事件可能导致多选的下一状态，但 DFA 则只有唯一可选的下一状态。如前所述，经过结构性变换之后，NFA 能够无损地转换为 DFA。</p>

<blockquote>
  <p>NFAs were introduced in 1959 by <a href="https://en.wikipedia.org/wiki/Michael_O._Rabin">Michael O. Rabin</a> and <a href="https://en.wikipedia.org/wiki/Dana_Scott">Dana Scott</a>, who also showed their equivalence to DFAs. NFAs are used in the implementation of <a href="https://en.wikipedia.org/wiki/Regular_expression">regular expressions</a>: <a href="https://en.wikipedia.org/wiki/Thompson's_construction">Thompson’s construction</a> is an algorithm for compiling a regular expression to an NFA that can efficiently perform pattern matching on strings. Conversely, <a href="https://en.wikipedia.org/wiki/Kleene's_algorithm">Kleene’s algorithm</a> can be used to convert an NFA into a regular expression (whose size is generally exponential in the input automaton).</p>
</blockquote>

<p>在 1950 年代，数学家 <a href="https://en.wikipedia.org/wiki/Stephen_Cole_Kleene">Stephen Cole Kleene</a> 首次描述了正则式概念并定义了该术语，由于他的贡献，闭包（即 <code class="language-plaintext highlighter-rouge">b*</code> 这样的子表达式）这一子表达式实际上被称作 Kleene 闭包。</p>

<blockquote>
  <p>一般认为，R. McNaughton 和 H. Yamada 和 Ken Thompson 分别共同提出了将正则表达式转换为 NFA 的构造方法，所以 Thompson 算法在外文学界常被称为 <strong>McNaughton–Yamada–Thompson algorithm</strong>，但这也并非唯一称谓，<strong>Thompson’s construction algorithm</strong> 也经常被使用。在中文场所为了简便，大多数刊物都使用 <strong>Thompson 算法</strong> 这一术语。</p>
</blockquote>

<p>该理论经过一段时间的发酵之后，在 1968 年前后，<a href="https://en.wikipedia.org/wiki/Ken_Thompson">Ken Thompson</a> 在实现 QED 编辑器的 IBM 7094 版本时引入了这一符号系统。随后 regexp 被实现到编辑器 ed 中，进一步地被实现到 grep 中，从此时起，Unix 中开始大量使用正则式相关实现，并在晚些时候（二十年后的 1986 年）成为了 POSIX 规范的一部分。实际上，Thompson 对此进行了深入研究，并在 1968 年发表了 <a href="http://www.fing.edu.uy/inco/cursos/intropln/material/p419-thompson.pdf">Regular Expression Search Algorithm</a> 论文。在正则式引擎的实现步骤中，从 Pattern 构造 NFA 自动机的算法即被称为 Thompson 构造（这一提法在龙书第二版被最后定型）。</p>

<p>稍微后于 Thompson，Dennis Ritchie 实现了另一个 QED。</p>

<blockquote>
  <p><a href="https://en.wikipedia.org/wiki/QED_(text_editor)">QED (text editor)</a> 是一种命令行环境下的行编辑器（早期的 Berkeley 版本的 QED 是面向字符的），每次针对文本文件中的一行进行编辑。它的继任者包括 Unix 中的 ed，Linux 中的 edline 等等。vi 当然也是其继任者，但 vi 以及后来的 vim 都是控制台全屏幕编辑器，而非命令行模式下的行编辑器。</p>

  <p><a href="https://github.com/CharlesHawkins/qed">GitHub - CharlesHawkins/qed: Re-implementation of Berkley QED, a 1967 text editor ancestral to ed and vim</a></p>

  <p>Nokia 有一篇文章介绍了 QED 的历史：<a href="https://www.nokia.com/bell-labs/about/dennis-m-ritchie/qed.html">History of QED - An incomplete history of the QED Text Editor</a>，这是 Ritchie 的一篇回忆录。Ritchie 还提到该版本的 QED 在一份内部技术备忘录中进行了描述，该备忘录可以以略微精简的 <a href="https://www.nokia.com/bell-labs/about/dennis-m-ritchie/qedman.html">HTML 可浏览格式 </a> 获取，也可以以扫描的（且很大：1.1MB） <a href="https://www.nokia.com/bell-labs/about/dennis-m-ritchie/qedman.pdf">PDF</a> 图像形式复制 ，后者是一份宝贵的史料。</p>
</blockquote>

<p>注意，Thompson 和 Ritchie 是 C 语言的设计者，Unix 系统的开发者。此外，Thompson 还是 Plan 9 和 Golang 的设计者和开发者。</p>

<p>早期的 Unix 工具如 grep，vi，sed 均只实现了基本型正则式（BRE，Basic Regular Expression），而较晚一点出现的如 egrep，awk 等实现了增强型正则式（ERE，Extended Regular Expression），从而形成了 POSIX 规范中的两大流派。注意 grep 工具现在同时支持 BRE、ERE（<code class="language-plaintext highlighter-rouge">-E</code>）和 PCRE（<code class="language-plaintext highlighter-rouge">-P</code>）。</p>

<p>大约 1986 年，Henry Spencer 发布了 C 语言实现的正则式库。该代码的中心思想在后继的各类实现中被参考和重新实现。</p>

<p>几乎同一时间，Larry Wall 开发并发布了 Perl，其中自行实现了正则式引擎，但比较简陋且 bug 较多。大约 1988 年，Perl 2 时代，Larry 采纳了 Henry Spencer 的代码，但直到 1994 年 Perl 5 时重新实现的该正则式包才具有现在的 Perl 正则式的第一形态。</p>

<p>在大约 1997 年，Phili Hazel 开发了 <a href="https://en.wikipedia.org/wiki/Perl_Compatible_Regular_Expressions">PCRE</a>，PCRE 也是一个 C 语言实现的正则式库，它全面仿制了 Perl 的正则式风味，提供为其它软件提供正则式功能，包括 PHP，Apache 2 等流行软件均集成了 PCRE。</p>

<p>相比较与 Perl 5 之前的版本，PCRE 的引擎质量很高，Perl 反过来与 PCRE v1 协作并共同推进这一流派的正则式语法风味。在 PCRE 7.x 和 Perl 5.9.x 阶段，这两个项目协调开发，功能在它们之间双向移植。</p>

<p>晚些时候，2015年，PCRE 分叉了 v2 版本开发独立演化。旧分支作为 PCRE v1 继续进行有限的更新，并最终停止。当前 PCRE 的活动分支为 v2。</p>

<p>综观 PCRE 的实现，实际上其效率不够高且存在滥用和溢出问题（某些情况下构成了安全 issue）。这是因为，为了支持贪婪重复、断言以及反向引用等高级特性，引擎的搜索算法必需引入回溯机制，这导致复杂度提升和时间内存的双重消耗，并且在某些特殊构型的正则式 Pattern 上会引发极低的性能。旧的实现中，递归的子表达式和反向引用可能导致无限的回溯，新的实现中采用几种方法（记忆历史回溯，限制最大回溯步数等）来解决该问题，并要求程序员应该了解怎么编写良好的正则式 Pattern。尽管在某些临界点 PCRE 会导致问题，它仍然成为了最广泛使用的代码库，形成了最庞大的正则式流派。这大概有两个原因：语法风味方面它相当强大，其次它是开源的，再一个则是它是 C 代码库。探究其后的原因是有趣的，但那将是另一篇文章要做的事。</p>

<p>同样在 80 年代，Rob Pike 在编写文本编辑器 sam 的时候也实现了一个新的引擎，并被 Dave Presotto 提取和收录到了 Unix v8 中。但是这一引擎的能耐长期被忽略了。由于后来 Henry Spencer 重新实现了 Unix v8 中的相关 API，Pike 的实现方案被遮蔽了，其代码思想在很长一段时间都没有得到重视。</p>

<p>如上文所述，Spencer 将他早期的实现发布到了公共领域，并取得了广泛的成就，并在后来演进为 PCRE 1 和 2（PCRE 的核心引擎采用和保留了 Spencer 的设计思想）。</p>

<p>而 Pike 的实现方案不同于该理论，并且很快，但缺乏名声。不过幸运的是它并未消亡，该方案也被引入并成为了 Plan 9 的 <a href="http://plan9.bell-labs.com/sources/plan9/sys/src/libregexp/">正则表达式库</a>。1999 年，Ville Laurikari 独立发现了 Pike 算法，并建立了理论基础，该算法的思想得以总结。</p>

<blockquote>
  <p>Pike 还用递归下降方法实现了一个简单版本，这是他的早年作品，仅支持连接和闭包。</p>
</blockquote>

<p>还是在 1986 年，POSIX 标准首次发布，正则式语法被定义为三种风味并收录为其中给一个独立的章节：SRE，BRE 和 ERE。这里 SRE 为 Simple RE，由于过于简单所以我们略过了它。</p>

<p>现代开发语言，例如 C#，C++，Golang 等等均原生实现了正则式算法，将其作为标准库的一部分。</p>

<p>大约 1999 年早期，微软研究员的 <a href="mailto:ericne@microsoft.com?subject=greta gripe">Eric Niebler</a> 发布了 GRETA 的首个版本。GRETA 采用 C++ 元编程技术并包含了一些魔改，从而将工作性能显著提升。随后几年里陆续发布了一些改进版本，但是注意到此时 VC++ 相对较为主流，基本上 GRETA 是不能跨 C++ 编译器的，这最终导致了 GRETA 的消亡。GRETA 本身实现了大多数 PCRE，并有微小的区别，尽管它的源代码本身由于兼容性原因不再发展，但它所代表的 PCRE 改版的养分被其它新生代开发语言所吸收，在今天，Javascript，C#，Golang 等所实现的 regexp 标准库大体上都属于这一风格，即融合了 POSIX 和 PCRE 两大规范后又有所修改和增强。</p>

<p>2010 左右，Russ Cox 开源了 <a href="https://github.com/google/re2">RE2</a> 的实现（实际上是由 Google 公司开源的，RE2 原本是一个子系统的支持库，但遵循当时 Google 的开源政策被独立发布了，再后来则被转而在 github 上保持更新），此 RE2 与 GRETA 并不相同，Russ Cox 研究并评估了 Pike 算法以及诸多此前的各种 regexp 引擎实现，并提出了新的方案，即采用虚拟机动作来完成匹配，这有别于 grep 系列的 Thompson NFA 方式，也不同于 PCRE 的基于回溯的匹配方案。最终，该算法被引入 Golang 中，也被用于 Golang 的各种产品中。</p>

<blockquote>
  <p>在 RE2 中采用了相当复杂的评估方案，并非仅有虚拟机方法，它也包含 NFA 方式、仿真 NFA 方式（即 Pike 算法）以及一种改进的三元索引方法，后者面向非内存场景，它也评估 NFA 效能并在可能的情况下编译到 DFA，而后者在一定程度上是速度最快或者次快的方案（但坏处是可能状态爆炸，内存需求超限）。</p>
</blockquote>

<p><a href="https://swtch.com/~rsc/">Russ Cox</a> 同样也是名人，他是 Plan 9，Golang 的开发者之一。</p>

<p>RE2 也被引入 Microsoft 的某些产品中，此外它也被引入除了 Golang 之外的诸多新兴的编程语言中，例如 Rust，Zig 等等。因此，从大约 2010 年开始，RE2 逐渐形成了正则式流派中的新势力。这样的发展并不奇怪，Russ Cox 的实现既能跟上时代（GRETA 在 C++11 之后就湮灭了，因为其实现采用了早期 C++ 编译器的魔法，无法在 C++11 时代被复制和改进），性能又优于其它实现（指的是 PCRE 和 POSIX ERE 等等），而且剔除了 PCRE2 的大量偏门的技能，再加上它易于集成（C++库，并有 C ABI 接口），所以新兴势力选它而不选 PCRE 是很自然的。</p>

<p>新世纪以来，由于各个流派的 CPU 体系（例如 Intel，AMD，ARM 和 RISCV）都在 SIMD 计算方面加大的支持，所以正则式引擎也有这种专用优化的新实现，Intel 的 Hyperscan 就是其中一种，其生产应用主要在基础设施方面，例如某些邮件过滤系统、防火墙等等。但这种风味的实现仅仅提供了性能优化，对于计算机科学的相关学科理论方面的贡献不大。</p>

<p>但是，像 Hyperscan 这样的引擎实现，有其独特的应用场景和独到之处，在大规模正则式匹配的场景下 RE2 是不能与其竞争的。不过 RE2 也并不专门针对像垃圾邮件分类这样的动辄数百万条正则式的场景。所以并不能说 RE2 不行。反之亦然，Hyperscan 目前只能工作在 Intel CPU 上，这很正常，它采用了 Intel CPU 的 SIMD 加速，但同时它就无法在任何其它 CPU 上移植，此外 Hypersan 具有高昂的编译代价，并需要将编译结果储存为一个数据库，所以它的工作负载较高，不适合轻量级工作环境，例如目前的大多数嵌入式场景，即使 CPU 选型不是问题，也难以支持 Hyperscan 引擎的工作要求。</p>

<p>典型的 Hyperscan 工作机器，可能是像 带有 48GB 的 Intel 志强 8180 CPU 之类的服务器。</p>

<p>但 RE2 引擎所能工作的场景从移动设备、家用设备、工业设备等嵌入式场景，到 PC 机或者工作站和服务器，均可移植和运转。</p>

<p>除了上面讨论的新势力之外，POSIX RE 和 PCRE2 自身也在演进。</p>

<p>现在的流行的正则式实现基本上都支持 ERE 和 PCRE2 的融合增强版本（总的来说是抛弃了 PCRE 的某些走火入魔的东西如某些奇奇怪怪的前瞻或者递归模式之类，但吸收和保留了其简洁明快的部分如 <code class="language-plaintext highlighter-rouge">\d</code> 等）。并且所有的这些存活的实现大体上都支持 Unicode 字符集（程度深浅不一）。</p>

<h3 id="关于正则式引擎实现与优化">关于正则式引擎实现与优化</h3>

<p>如今（2025 年）想要实现一个完整全面的正则式代码库是一桩相当不容易的工作，你需要掌握大量的跨越形式文法的其它知识，尤其是各种字符集以及 Unicode 的各种细节，此外你还将面临回溯与性能之间的权衡与挑战，想要别出机杼地设计出一种全新的实现方案非常困难。</p>

<p>关于正则式引擎的优化，取决于生产场景。例如对于偶尔编译但大量匹配（例如邮件过滤系统），那么尽可能地“过”编译到机器码级别是最佳选择，这样使得 match 运算能够最快，而编译的速度如何、耗费如何并不重要。而对于频繁编译然后匹配的情况，则需要在编译速度和匹配速度之间取得平衡，因为显而易见，编译的粒度更细时耗费的编译时间更长（例如事先编译到 DFA 状态），但却能帮助匹配算法更快（DFA 状态表的匹配基本上是查表）。此外，某些场景要求正则式引擎能够同时运转大批量的正则式，而某些场景只需要少数规则单次运行，或者偶尔定期运行即可。</p>

<p>不同的要求决定了你所设计并实现的正则式引擎应该侧重于哪些方面。</p>

<p>当然，由于现代的高级 regexp 语法并不能全都翻译为 NFA 和 DFA，上述的某些思想并不能完全奏效。应对这一挑战通常采用两种方法：</p>

<p>第一是子自动机。带有零宽断言的正则式无法直接编译为 NFA，但零宽断言重点条件部分，以及正则式的主体分别是两个子自动机。对于这两者来说通常是可以编译到 NFA 的，例如 <code class="language-plaintext highlighter-rouge">(?!ab)cde</code> 这个正则式，我们可以拆分为 <code class="language-plaintext highlighter-rouge">ab</code> 和 <code class="language-plaintext highlighter-rouge">cde</code> 两个子自动机，然后采用一个条件语句来串联两者即可完成整个正则式的匹配。</p>

<blockquote>
  <p>毫无疑问，上面的示例不算太恰当，但它将能匹配 “cde” 这样的字符串。它的不恰当在于表达式中的前置断言其实几乎毫无意义。</p>

  <p>但这个例子的意图只是为了解释 <code class="language-plaintext highlighter-rouge">(?!re)re</code> 这样的否定前瞻断言的格式中所包含的两个 <code class="language-plaintext highlighter-rouge">re</code> 子表达式。</p>

  <p>一个类似的但有意义的正则式如 <code class="language-plaintext highlighter-rouge">^(?=&lt;)&lt;[^&gt;]+&gt;\w+</code> 其中的肯定前瞻断言确认了像 <code class="language-plaintext highlighter-rouge">&lt;b&gt;bold&lt;/b&gt;</code> 这样的字符串蒋能被匹配。</p>

  <p>结合 backref 的话，一个可能匹配成对 html 标记的正则式大约像这样：<code class="language-plaintext highlighter-rouge">(?=&lt;)&lt;([^]&gt;+)&gt;.*&lt;/\1&gt;</code>，其中 <code class="language-plaintext highlighter-rouge">\1</code> 要求与前面的捕获组 <code class="language-plaintext highlighter-rouge">([^&gt;]+)</code> 相同，所以它将会匹配 <code class="language-plaintext highlighter-rouge">&lt;b&gt;bold&lt;i&gt;italic&lt;/i&gt;&lt;/b&gt;</code> 这样的成对的 html 标记。</p>
</blockquote>

<p>第二种方法是采用某种虚拟机的变体。这里所谓的虚拟机，是指 Thompson 虚拟机或者 Glushkov 虚拟机，两者各有特点，细节不在这里展开。该种技术的特点是将每个正则式语法规则（运算）映射为一个特殊的虚拟机（我们认为这是一种自动机）的字节码指令，当编译完成之后，我们得到的是该虚拟机的字节码序列。只需运行该虚拟机，并喂给它输入字符串流，就可以完成匹配。这种思路在早年非常典型且经常被用到，而且常常是直接编译到机器指令（例如 Thompson 的 QED），新世纪以来运用这种思路似乎相对较少，v8 引擎是一种。</p>

<p>事实上，在60、70 年代，“编译”当然就是编译到机器指令，只不过在经过 90 年代 CPU 虚拟化和 Java 虚拟机的理论丰富之后，现在我们会首先引入虚拟机这个中间抽象层，将其简单映射到具体 CPU 就得到前面的路线了。</p>

<p>同样地进一步思考，在虚拟机中我们可以设计一条特殊的机器指令如 <code class="language-plaintext highlighter-rouge">xlat &lt;re-index&gt;</code>，其参数为一个预先编译好的 NFA/DFA 状态机，这样就能将某些特定的子表达式的匹配算法转交给按索引指引的子自动机上，例如 <code class="language-plaintext highlighter-rouge">[0-9]+</code> 就可以如此处理，专门的自动机可以在 4-8 条底层（例如 Intel 或者 ARM）机器指令的规模下非常优秀地完成数字字符串的识别（如果包括计算累加在内大概在 20-30 条 Intel 机器指令之内，但肯定比虚拟机字节码所映射的机器指令数少）。</p>

<p><del>类似的设计和优化思想，我们会在后续章节的相关部分进行讨论</del>。</p>

<p>另一种优化思路，是针对图运算的。在计算机科学的相关理论中，图论与图算法是很重要的一个分支。这个方面的理论性较强，优化算法其本身的实现难度不高，挑战来自于判断：你需要判定何时应该采用何种优化，这个方面关乎你的理论知识的储备。</p>

<h3 id="随想">随想</h3>

<p>梳理正则式的整个历史发展链条，你会发现一个有趣的视点，即真理一直掌握在 Thompson 手里，无论是 QED，Unix，C 还是 Golang 和 RE2。Thompson 不但至今健在，还在跨越过去 60~70 年的计算机发展历史中始终站在顶端。</p>

<h3 id="后记">后记</h3>

<p>我还没有决定要如何做，和要做什么。因此本文的正文内容先上。</p>

<p>本文有点草草，所以各种史料以及相应的相关原始网站和出处，我没能罗列出来。</p>

<p>今后某一天或许我会查证后补齐的吧。</p>

<p>🔚</p>]]></content><author><name>hedzr</name></author><category term="algorithm" /><category term="regexp" /><category term="regexp" /><category term="algorithm" /><summary type="html"><![CDATA[如题，用口水话叙述一下正则式的发展简史 ...]]></summary></entry><entry><title type="html">在 MacBook 上尝试 macOS Tahoe</title><link href="https://hedzr.com/lifestyle/review/try-macos-tahoe-with-utm/" rel="alternate" type="text/html" title="在 MacBook 上尝试 macOS Tahoe" /><published>2025-10-03T10:00:00+08:00</published><updated>2025-10-03T01:17:00+08:00</updated><id>https://hedzr.com/lifestyle/review/try-macos-tahoe-with-utm</id><content type="html" xml:base="https://hedzr.com/lifestyle/review/try-macos-tahoe-with-utm/"><![CDATA[<h2 id="尝试-tahoe">尝试 Tahoe</h2>

<p>首先要说的是，升级到 Tahoe 是不建议的。</p>

<p>macOS 的大版本更新从来都是不值得信赖的，至少 3 个正式小版本之后才可以研究要不要更新大版本的问题。</p>

<p>但是要研究，就要亲自尝试。</p>

<p>只有一台主力机的人在这种情景下就不免有点难受。</p>

<p>我还记得 2015 年的时候（在那前后），我只有一台 256GB 磁盘的 MBP，而且剩余空间极其可怜，每日都在清理空间与工作之间挣扎。有一日也是大版本更新，可能是 Catalina，年代久远记不清了，总之然后升级到一半就崩了，变砖不能开机。</p>

<p>后来还是终于解决了，但丢失了新一部分工作内容。</p>

<p>那时候对于恢复模式、故障修复的概念不多，而且一个月未必备份一次 TM，造成了上述结果，起因是剩余盘空间不太充裕。</p>

<p>后来 18,19 年大概还遇到过一次纯硬件问题，现象是下载升级过程中随机失败，如果反复尝试数遍的话则可以成功，但然后正式使用过程中仍会随机重启。</p>

<p>总而言之，对于手边 MBP 类似的 macOS 设备不多的情况，升级大版本绝对是一个需要认真对待、并且需要赌上人品的时刻。</p>

<p>然而，实际上你还有至少两种选择可以让上述状况得到解决。</p>

<p>在保证如下三个前提的情况下，本文将会介绍这两种方案：</p>

<ol>
  <li>确保 Time Machine 具有最新备份</li>
  <li>有一块 U 盘中烧录了当前工作机的 macOS 版本安装盘</li>
  <li>工作机中剩余 SSD 容量大于 60-80GB（我现在日常都会尽量保持 200GB 以上的余量，这对于 SSD 健康也有好处）</li>
</ol>

<p>那么，这两种方案分别是：</p>

<ol>
  <li>使用 UTM 虚拟机来尝试</li>
  <li>在额外的卷分区中尝试</li>
</ol>

<p>下面分别概要介绍，考虑到大家大概都是老玩家了，所以我行文将会从简。如有未尽之处，请尽可留言或者在讨论区 <a href="https://github.com/hedzr/hedzr.github.io/discussions">hedzr/hedzr.github.io · Discussions · GitHub</a> 讨论。</p>

<h2 id="使用-utm-虚拟机">使用 UTM 虚拟机</h2>

<p>UTM 虚拟机，没有玩过的朋友可以安装一份。</p>

<p>注意，如果你需要 Linux 虚拟机的话，我的推荐是 OrbStack。但是对于尝鲜 macOS 各种版本，则 UTM 将会有最佳体验，无论是安装过程还是实际使用。</p>

<p>安装一份 macOS 虚拟机的方法并不困难，请遵循如下步骤：</p>

<ol>
  <li>做 TM 备份（尽管这一条没有必要，但我习惯了）</li>
  <li>保证磁盘空间 100GB 以上</li>
  <li>使用 Mist 下载 macOS 的新版本的安装包</li>
  <li>生成 macOS 系统安装包</li>
  <li>在 UTM 中使用该安装包安装一个新虚拟机</li>
  <li>然后开始体验</li>
</ol>

<p>其中需要略加解释的内容如下：</p>

<p>macOS 系统的安装包大致上在 15 - 20GB 之间，然后安装包需要被执行然后生成 ipsw 安装包，UTM 接受 ipsw 安装包并且将会很顺畅地完成 macOS 系统安装。</p>

<p>所以上述两个安装包以及 Mist 的运作大约吃掉了 40GB，料敌从宽。</p>

<p>而随后的 Tahoe 虚拟机本身，我的建议是预先保留 64GB 磁盘空间来安装，其中为 Tahoe 系统本身保留大约 20GB 空间，安装后系统占用即是如此，然后虚拟机中持有 40 GB 剩余空间你可以进行一部分迁移和软件安装以方便评估。</p>

<p>Mist 下载的系统文件包是一个 installer app，你需要运行它从而生成 ipsw 安装包。</p>

<p>不要试图下载系统的安装 iso。</p>

<p>因为首先你仍然会需要下载若干片段，然后 Mist 将会在本地磁盘中构建出你想要的 iso，期间会消耗大量 CPU 和 SSD 资源。然后得到的 iso 在 UTM 中的安装过程并不优秀，没有 ipsw 来的顺畅。</p>

<p>尽管 Mist 同样也是在本地构建 ipsw，但这个过程显然在消耗上大大小于 iso。</p>

<p>另一个原因，则是因为最新的 macOS 系统，常常没有生成 iso 的按钮，所以你想要 iso 的话需要遵照相应规范自行构建。</p>

<p>但是 macOS 的旧版本则往往额外带有“Create bootable macOS installer”的按钮，在得到 iso 之后你可以烧录到 U 盘上用作启动盘备用。</p>

<p>再来是为什么使用 UTM 虚拟机？</p>

<p>首先，这是因为针对 macOS 虚拟机而言，UTM 采用 Apple Visualization 机制，所以虚拟化中间层的消耗无限接近于 0（略有夸张，实际上大概是 5-15% 之间），目前没有其它虚拟化软件能够优于这一技术栈，顶多也就是持平（如 Parallel Desktop）。</p>

<p>其次同样是因为 Apple 虚拟化层，所以 Tahoe 虚拟机同样可以自动开启特效，因此在虚拟机中你基本上可以获得完整的效果，几乎不弱于原生安装到本机。</p>

<p>最后一点，是第二套方案所不具备的优势，即无限的尝试可能性。你可能没有尝试过安装一份香港语言和身份为基准的 macOS 系统，日语的呢？大概也没有过。UTM 的 macOS 虚拟机给出了这种可能性，这对于面向全球潜在用户的开发者应该是有用的。基本上，每份这样的虚拟机的代价是 64GB 本机磁盘空间，稍微有点浪费。当然如果你手里有 7、8 台 Mac Mini 那就另说了。</p>

<p>值得一提的是，UTM 本身实际上并不特别出彩，一般而言，UTM 比较简陋，功能不强，面对 Linux 虚拟机的时候 io 损耗比较大，而且时常会有 bug 或者无法实现的功能。只不过这些缺点基本上只在 Linux 虚拟机中体现出来，所以对于本文的命题反而是 UTM 占得上风。</p>

<p>如果你的磁盘空间吃紧，理论上 50GB 剩余空间也可以尝试，但并不建议。</p>

<blockquote>
  <p>UTM 最新版本 4.7.4 支持 Tahoe 的全部特效。</p>

  <p>前提是在虚拟机中安装 UTM 的 Guest Tool 软件包，该软件包负责安装一组驱动程序到虚拟机中以提升性能、支持剪贴板交互以及共享文件夹等等在主机和虚拟机之间交互的手段。</p>
</blockquote>

<p>本文只提点要点，也包含我的实际体验，至于说图文并茂和参考链接，这一次就能省则省了，想必爱折腾的朋友对于这点问题难不倒你们。</p>

<h2 id="在额外的卷分区中安装系统">在额外的卷分区中安装系统</h2>

<p>UTM 技术方案虽好，但性能确实是个短板。</p>

<p>尽管我强调了 UTM 的 macOS 虚拟机实际上相当具有实操性，但它的反应、丢帧等等还是很冥想的。</p>

<p>所以第二套技术方案，是在你的本机上原生地同时安装两份系统：你的原始系统保持不变，然后在一个新的卷分区中安装第二个系统，通过“系统设置-启动磁盘”可以选择下一次启动时从哪一个系统中引导。</p>

<p>基于此方案，你可以直接原生地体验新系统，测试兼容性，可选地逐步迁移旧系统的全套内容到新系统中，无痛地完成整个迁移过程。</p>

<p>缺点很明显，如果你旧系统没有分区来隔离日常工作文件的话，那么你需要整个 SSD 容量的一半的剩余空间方可完成旧系统的内容的迁移（由于 APFS 卷分区自动共享整个 SSD 的容量，所以实际需要的剩余空间往往小于一半；如果你采用移动的方式迁移内容，旧系统中的原有内容简单删除的话，则无需保留太多剩余空间，因为 APFS 文件系统令新旧系统分区共享磁盘总容量，所以删除旧文件释放的空间能够被新系统透明地使用）。</p>

<p>关键的步骤如下：</p>

<ol>
  <li>TM 备份，这是必需的</li>
  <li>Mist 下载新系统的安装包，生成 iso 文件并烧录到 U 盘中，然后做出新的磁盘分区，重启并通过 U 盘引导，然后安装新系统到新的卷分区中。</li>
  <li>或者，直接下载 <a href="https://swcdn.apple.com/content/downloads/26/38/093-37779-A_Y4733G5GHI/adlxnkoqzyrrzfl1r5krg7ql0cod8vpl5e/InstallAssistant.pkg">InstallAssistant.pkg</a>，也可以使用 Mist 下载直接得到该 pkg，</li>
  <li>然后做出新磁盘分区</li>
  <li>双击运行该 pkg，然后安装系统到新分区中，</li>
  <li>在“系统设置-启动磁盘”选择新分区作为下一次引导设备，</li>
  <li>重启本机，然后进入新系统</li>
</ol>

<h3 id="mist-下载新系统安装文件">Mist 下载新系统安装文件</h3>

<h4 id="获得-install-macos-xxxapp">获得 <code class="language-plaintext highlighter-rouge">Install macOS [xxx].app</code></h4>

<p>Mist 刷新固件和安装器列表，在 Installers 面板中将会得到 Tahoe 26.0.1，然后点它的右侧下载按钮（<code class="language-plaintext highlighter-rouge">Download and export macOS Installer</code>）下载：</p>

<p><img src="https://cdn.jsdelivr.net/gh/hzimg/blog-pics@master/upgit/2025/10/20251003_1759472529.png" alt="Screenshot 2025-10-03 at 10.45.28" /></p>

<p>弹出的文件夹对话框中，有几个勾选框：</p>

<p><img src="https://cdn.jsdelivr.net/gh/hzimg/blog-pics@master/upgit/2025/10/20251003_1759472522.png" alt="image-20251003105011884" /></p>

<p>其中：</p>

<ul>
  <li>Application，将会得到 <code class="language-plaintext highlighter-rouge">Install macOS Tahoe 26.0.1_25A362.app</code> 文件，双击可以就地开始安装新系统的流程（期间将会生成 <code class="language-plaintext highlighter-rouge">Install macOS Tahoe.app</code> 并放入 <code class="language-plaintext highlighter-rouge">/Applications</code> 中）。</li>
  <li>Disk Image，将会得到 dmg 文件，双击它将会得到 <code class="language-plaintext highlighter-rouge">Install macOS Tahoe.app</code> 文件，将其发到 <code class="language-plaintext highlighter-rouge">/Applications</code> 文件夹中，然后双击即可启动安装新系统的流程。</li>
  <li>ISO，将会得到 iso 文件，可以烧录到 U 盘中用于引导和安装新系统。</li>
  <li>Package，将会得到 <code class="language-plaintext highlighter-rouge">Install macOS Tahoe 26.0.1_25A362.pkg</code> 文件，双击运行该文件，将会得到 <code class="language-plaintext highlighter-rouge">Install macOS Tahoe.app</code> 文件。</li>
</ul>

<p>注意，上述 4 个格式，每个大约 17GB 大小，所以注意你的剩余磁盘空间。</p>

<h4 id="creat-bootable-macos-installer">Creat bootable macOS installer</h4>

<p>在 Mist 的系统列表中，Tahoe 的右侧有两个按钮，前者在上面介绍了，而另一个按钮 <code class="language-plaintext highlighter-rouge">Create bootable macOS installer</code> 将会下载系统文件并直接烧录到 U 盘中，所以你需要准备一个 U 盘，然后选中它即可。</p>

<h4 id="取得-ipsw-文件">取得 ipsw 文件</h4>

<p>ipsw 格式是一种固件格式，现在仅使用于 Apple Silicon 芯片。</p>

<p>在 Mist 中可以选中 Firmwares 面板，然后找到 Tahoe 点击其后的下载按钮，等待完成之后即可得到 <code class="language-plaintext highlighter-rouge">Install macOS Tahoe 26.0.1_25A362.ipsw</code> 文件。</p>

<p>UTM 使用该文件安装虚拟机最为流畅。</p>

<p>但此过程略微慢于使用恢复固件安装新系统。</p>

<h4 id="使用-mist-cli-工具">使用 Mist cli 工具</h4>

<p><code class="language-plaintext highlighter-rouge">brew install mist-cli</code> 可以得到 mist-cli 命令行工具。</p>

<h4 id="使用恢复固件">使用恢复固件</h4>

<p>你可以在某些网站下载得到 <code class="language-plaintext highlighter-rouge">UniversalMac_26.0_25A354_Restore.ipsw</code> 恢复固件，同样地，UTM 也可以直接使用此固件安装新系统。</p>

<p>该恢复固件也可以用于 DFU 恢复模式进行系统升降级，这是强制性地，不受网络端口限制（Apple 常常会关闭旧系统的降级端口，所以你的 macOS，iPhoneOS 可能会升级后无法通过在线恢复功能降级，此时这个恢复固件就相当有用了）。</p>

<h3 id="准备本地安装程序">准备本地安装程序</h3>

<p>在上面的链接地址下载得到 <code class="language-plaintext highlighter-rouge">InstallAssistant.pkg</code> 文件后，双击执行它，即可得到本地安装程序 <code class="language-plaintext highlighter-rouge">Install macOS [xxx].app</code>，你可以在 <code class="language-plaintext highlighter-rouge">/Applications</code> 文件夹中找到它。</p>

<p>比较简单的方法是通过 Mist 下载 DMG 格式文件，然后双击挂载 DMG，将其中的 <code class="language-plaintext highlighter-rouge">Install macOS [xxx].app</code> 拖拽到 <code class="language-plaintext highlighter-rouge">/Applications</code> 中。</p>

<h3 id="准备第二分区">准备第二分区</h3>

<p>制作新磁盘分区的方法是使用“Disk Utilities”工具，选择 <code class="language-plaintext highlighter-rouge">Machintosh HD</code> 宗卷，然后创建一个新的 Volume（点击 + 号），</p>

<p><img src="https://cdn.jsdelivr.net/gh/hzimg/blog-pics@master/upgit/2025/10/20251003_1759472499.png" alt="image-20250918021337371" /></p>

<p>输入名字，例如 <code class="language-plaintext highlighter-rouge">Tahoe26</code>，然后点击 Add 按钮添加新的 Volume 到 <code class="language-plaintext highlighter-rouge">Machintosh HD</code> 宗卷容器中：</p>

<p><img src="https://cdn.jsdelivr.net/gh/hzimg/blog-pics@master/upgit/2025/10/20251003_1759472491.png" alt="image-20250918021410995" /></p>

<p>然后得到这个新卷</p>

<p><img src="https://cdn.jsdelivr.net/gh/hzimg/blog-pics@master/upgit/2025/10/20251003_1759472440.png" alt="image-20251003101424545" /></p>

<p>备注：</p>

<p>截图中我们原本已有一个分区 VolHack，所以实际上准备的是第三分区。</p>

<p>日常工作中，建立另一个分区来放置工作文件也是有意义的，例如我们出于各种目的，在 VolHack 分区创建时为其指定了文件名大小写敏感，这样我们就能拖回一些 Linux Kernel 源代码共查阅了。</p>

<h3 id="安装到新的-tahoe26-卷中">安装到新的 Tahoe26 卷中</h3>

<p>现在双击 <code class="language-plaintext highlighter-rouge">Install macOS.app</code> 安装程序，并将 macOS Tahoe 安装到刚刚新建的 Tahoe26 卷中。</p>

<p>安装完成之后整个 SSD 中就同时存在了两个可引导系统，一是你的当前工作系统，一是刚刚安装的 Tahoe 系统。</p>

<p>这样就能保持当前工作系统不受影响。</p>

<p>今后可以通过切换引导磁盘的方式选择引导两个可引导系统中的哪一个。</p>

<h2 id="参考材料">参考材料</h2>

<ul>
  <li><a href="https://brew.sh/zh-tw/">為 macOS（或 Linux）添上套件管理工具 — Homebrew</a></li>
  <li><a href="https://support.apple.com/zh-cn/108900">如何修复或恢复 Mac 固件 - 官方 Apple 支持 (中国)</a> - 关于 DFU 恢复</li>
  <li><a href="https://support.apple.com/zh-cn/102518">如何从“macOS 恢复”启动 - 官方 Apple 支持 (中国)</a></li>
  <li><a href="https://github.com/ninxsoft/Mist">ninxsoft/Mist: A Mac utility that automatically downloads macOS Firmwares / Installers.</a></li>
  <li><a href="https://support.apple.com/zh-cn/guide/mac-help/mh15638/26/mac/26">从时间机器备份中恢复所有文件 - 官方 Apple 支持 (中国)</a></li>
  <li><a href="https://support.apple.com/zh-cn/guide/mac-help/mchlp1599/26/mac/26">重新安装 macOS - 官方 Apple 支持 (中国)</a></li>
  <li><a href="https://support.apple.com/zh-cn/guide/mac-help/mh27903/mac">抹掉并重新安装 macOS - 官方 Apple 支持 (中国)</a></li>
  <li><a href="https://seaphages.org/media/forums/attachments/56c9bf43-93bc-46c6-bc09-a94088bee0fa.pdf">The Unofficial Fusion 13 for Apple Silicon Companion Guide (pdf)</a></li>
  <li>
    <table>
      <tbody>
        <tr>
          <td>[Sharing</td>
          <td>UTM Documentation](https://docs.getutm.app/settings-qemu/sharing/)</td>
        </tr>
      </tbody>
    </table>
  </li>
  <li>
    <table>
      <tbody>
        <tr>
          <td>[Linux</td>
          <td>UTM Documentation](https://docs.getutm.app/guest-support/linux/)</td>
        </tr>
      </tbody>
    </table>
  </li>
</ul>

<h2 id="后记">后记</h2>

<h3 id="为何采用上面的方案">为何采用上面的方案？</h3>

<p>仅仅只是为了尝鲜，和评估升级风险，上面的方法对于硬件设备有限的用户是基本上无损的，唯一的损失大约是磁盘剩余空间。另外则是时间，但严格来说，实际上是节省了时间和减少了意外发生的可能性。</p>

<h3 id="应该升级到-tahoe-吗">应该升级到 Tahoe 吗？</h3>

<p>当然不。</p>

<p>本文开篇提示过，无论你是谁，26.0.3 以后再来考虑升级到 Tahoe 的问题才是合适的时机。</p>

<p>而对于我这样的用户来说，已经放弃了升级了。</p>

<p>Tahoe 给出的 UI 新外观，和更强的多设备整合，对我来说一钱不值。</p>

<p>我更看重的是，更低的系统开销，更好的面向开发者的细节处理。然而这些小众要求不是 Apple 的考虑方向。</p>

<p>从中美关系上来说，我手中的 MBP 也很可能成为遗产了。</p>

<h3 id="tahoe-一无是处吗">Tahoe 一无是处吗？</h3>

<p>这倒不至于。</p>

<p>我实际使用的一大感慨是，这一次 Apple 是真的没做什么额外的核心功能。因为基本上我所有的旧系统上的 apps 全都无痛地在 Tahoe 中直接可用。当然，不少 apps 也提供了更新版本来适配 Tahoe 的 UI 风格就是了。</p>

<p>甚至包括我的 iStatsMenu 也没有任何更新，直接 copy 一下就开工了。</p>

<p>所以担心升级后旧 apps 的兼容性，尤其是双击无法运行这种以前的痛点，这次是没有了，至少我没有遇上。</p>

<p>不过正因为如此，我已经失去了升级的欲望。</p>

<p>能得到更好的什么呢？</p>

<p>更好的 Apple Music，可能吗？</p>

<p>显然，捆绑式推进策略对我是没有吸引力的。</p>

<p>🔚</p>]]></content><author><name>hedzr</name></author><category term="lifestyle" /><category term="review" /><category term="macOS" /><category term="tahoe" /><category term="utm" /><category term="visualization" /><summary type="html"><![CDATA[在宿主机上远程接入到虚拟机中进行开发，同一份代码 ...]]></summary></entry><entry><title type="html">让 VMware Fusion 共享文件夹在 Apple Silicon 上工作</title><link href="https://hedzr.com/devops/opensuse/vmware-shared-folder-on-apple-silicon/" rel="alternate" type="text/html" title="让 VMware Fusion 共享文件夹在 Apple Silicon 上工作" /><published>2025-05-29T20:00:00+08:00</published><updated>2025-05-30T01:17:00+08:00</updated><id>https://hedzr.com/devops/opensuse/vmware-shared-folder-on-apple-silicon</id><content type="html" xml:base="https://hedzr.com/devops/opensuse/vmware-shared-folder-on-apple-silicon/"><![CDATA[<h2 id="使能-shared-folder">使能 Shared Folder</h2>

<p>在 VMware Fusion 中新建一个虚拟机，安装 openSUSE 16.0 beta（你可以使用其它 Linux 发行版），设置好 Shared Folder，然而无论你安装的是 Linux Server 还是 Desktop，在 Apple Silicon 上基本上都无法直接工作。</p>

<p>此前很多时候，我只通过 ssh rsync 等方法来同步代码到虚拟机。</p>

<p>但是最近突然想到，UTM 的一些经验可以搬移到 VMware Fusion 中来。首先是 UTM 使用 9p 驱动程序来完成共享文件夹的装载（mount），但在 VMware Fusion 中，我发现没有必要这么“麻烦”，直接使用 fuse 就可以。其次是 UTM 中介绍了 bingfs 的实用方案，这简直是神器，owner 和 permissons 现在就不再是难题了。</p>

<p>所以下面介绍我现在的共享文件夹的做法。</p>

<h3 id="for-vmware-fusion-on-apple-silicon-only">For VMware Fusion on Apple Silicon Only</h3>

<h4 id="bindfs">bindfs</h4>

<p>总体上需要用到 bindfs 软件包：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nb">sudo </span>zypper <span class="k">in </span>bindfs
</pre></td></tr></tbody></table></code></pre></div></div>

<p>如果你不是在使用 openSUSE，那么使用恰当的包管理器，例如在 Ubuntu 中这样安装：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nb">sudo </span>apt-get <span class="nb">install </span>bindfs
</pre></td></tr></tbody></table></code></pre></div></div>

<h4 id="mount-the-shared-folder">Mount the shared folder</h4>

<p>首先在 <code class="language-plaintext highlighter-rouge">/etc/fstab</code> 中装载 Shared Folder：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="rouge-code"><pre><span class="nv">SUDO</span><span class="o">=</span><span class="nb">sudo

grep</span> <span class="nt">-q</span> <span class="s1">'/mnt/hgfs'</span> /etc/fstab <span class="o">||</span>
	<span class="nb">echo</span> <span class="s2">"vmhgfs-fuse   /mnt/hgfs fuse defaults,allow_other,_netdev   0 0"</span> | <span class="nv">$SUDO</span> <span class="nb">tee</span> <span class="nt">-a</span> /etc/fstab

<span class="o">[</span> <span class="nt">-d</span> /mnt/hgfs <span class="o">]</span> <span class="o">||</span> <span class="nv">$SUDO</span> <span class="nb">mkdir</span> /mnt/hgfs

<span class="nv">$SUDO</span> mount <span class="nt">-a</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>现在在 /mnt/hgfs 中就能看到你设置的共享文件夹了。</p>

<p>假设你已经指定了 macOS 上的 <code class="language-plaintext highlighter-rouge">~/Downloads</code> 文件夹为 Shared Folder，那么下面的命令应该能显示它了：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nb">ls</span> <span class="nt">-la</span> /mnt/hgfs/Downloads
</pre></td></tr></tbody></table></code></pre></div></div>

<p>这一策略很关键，而且相当轻便（直接使用 fuse），根本不必编译 vmtools，直接就可以生效。</p>

<p>这是由于 openSUSE 16.0 的内置驱动支持力度加强了。</p>

<p>对于较旧的版本，可能你需要安装 vmtools 包（不必编译）：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nb">sudo </span>zypper <span class="k">in </span>open-vm-tools open-vm-tools-desktop
</pre></td></tr></tbody></table></code></pre></div></div>

<p>对于其它发行版或者较旧的 openSUSE，你可能需要手工编译。</p>

<p>但是，VMware Fusion for Apple Silicon 可能不支持你挂载 vmtools iso 来获取其驱动源码，你需要另行寻找解决方案。</p>

<h4 id="mapping-file-owner-and-permissions">Mapping file owner and permissions</h4>

<p>接下来利用 bindfs 来完成用户身份和文件权限的映射，从而让你的 macOS 上的文件的所属身份转换为虚拟机中的当前用户，这样就可以在虚拟机中直接编辑而无需 sudo 了。下面的实例中，假设你已经指定了 macOS 上的 <code class="language-plaintext highlighter-rouge">~/Downloads</code> 文件夹为 Shared Folder，并且其中有一个 <code class="language-plaintext highlighter-rouge">ops.work</code> 子目录，以及 macOS 和 VM 中的当前用户均为 <code class="language-plaintext highlighter-rouge">hz</code>：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre><span class="nv">SUDO</span><span class="o">=</span><span class="nb">sudo

echo</span> <span class="s2">"/mnt/hgfs/Downloads/ops.work   /home/hz/ops.share fuse.bindfs map=501/1000:@20/@1000,x-systemd.requires=/mnt/hgfs 0 0"</span> | <span class="nv">$SUDO</span> <span class="nb">tee</span> <span class="nt">-a</span> /etc/fstab
<span class="o">[</span> <span class="nt">-d</span> /home/hz/ops.share <span class="o">]</span> <span class="o">||</span> <span class="o">{</span> <span class="nv">$SUDO</span> <span class="nb">mkdir</span> /home/hz/ops.share <span class="o">&amp;&amp;</span> <span class="nv">$SUDO</span> <span class="nb">chown</span> <span class="nt">-R</span> hz: /home/hz/ops.share<span class="p">;</span> <span class="o">}</span>

<span class="nv">$SUDO</span> mount <span class="nt">-a</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>现在，映射成功，操作正常。</p>

<h3 id="结论">结论</h3>

<p>很显然的一点是，你可以在 VMware Fusion 的 VM settings 中指定多个 Shared Folders，它们将被统一挂载到虚拟机中的 <code class="language-plaintext highlighter-rouge">/mnt/hgfs</code> 之下。</p>

<p>所以你可以分别通过第二步（<code class="language-plaintext highlighter-rouge">Mount the shared folder</code>）中的 bindfs 映射方式将这些共享文件夹都映射到 <code class="language-plaintext highlighter-rouge">$HOME</code> 中，并且动态转换其 owner 为当前用户。</p>

<p>另一点是，挂载子目录也毫无问题。</p>

<p>如此，你可以非常容易地将 macOS 中的文件夹映射到虚拟机中，并且可以直接修改、没有延迟、无需 rsync/scp 同步，这对于做 ops 开发，做交叉编译等工作场景来说非常有优势：一旦虚拟机环境准备就绪，你就可以在 macOS 中通过 VSCode Remote Explorer 直接打开虚拟机中的工作文件夹，建立 workspace，直接开始开发。</p>

<h3 id="结束语">结束语</h3>

<p>佚失。</p>

<p>其实我使用了很长一段时间的 UTM，总的感觉还可以。</p>

<p>但是 UTM 确实很多 Bugs，很多怪现象的限制。</p>

<p>自从 VMware Fusion 免费而且支持 Apple Silicon 之后，我发现我还是比较习惯这一边，比较稳定，而且没有多少消耗，CPU 或者 Memory 的压力都基本可以忽略，而且支持虚拟机快照。</p>

<p>所以我可以建立若干节点，等到虚拟机的磁盘空间膨胀了，就抛弃这些变化，返回快照节点，于是磁盘空间也就返还了，这一点对于 UTM 来说就比较无解，没有办法。</p>

<h3 id="参考材料">参考材料</h3>

<ul>
  <li><a href="https://seaphages.org/media/forums/attachments/56c9bf43-93bc-46c6-bc09-a94088bee0fa.pdf">The Unofficial Fusion 13 for Apple Silicon Companion Guide (pdf)</a></li>
  <li>
    <table>
      <tbody>
        <tr>
          <td>[Sharing</td>
          <td>UTM Documentation](https://docs.getutm.app/settings-qemu/sharing/)</td>
        </tr>
      </tbody>
    </table>
  </li>
  <li>
    <table>
      <tbody>
        <tr>
          <td>[Linux</td>
          <td>UTM Documentation](https://docs.getutm.app/guest-support/linux/)</td>
        </tr>
      </tbody>
    </table>
  </li>
</ul>

<p>🔚</p>]]></content><author><name>hedzr</name></author><category term="devops" /><category term="opensuse" /><category term="opensuse" /><category term="shared-folder" /><category term="vm" /><summary type="html"><![CDATA[在宿主机上远程接入到虚拟机中进行开发，同一份代码 ...]]></summary></entry><entry><title type="html">轻量级地克隆一个 Git 仓库</title><link href="https://hedzr.com/devops/git/lite-clone-a-git-repo/" rel="alternate" type="text/html" title="轻量级地克隆一个 Git 仓库" /><published>2025-05-21T05:00:00+08:00</published><updated>2025-05-24T07:27:00+08:00</updated><id>https://hedzr.com/devops/git/lite-clone-a-git-repo</id><content type="html" xml:base="https://hedzr.com/devops/git/lite-clone-a-git-repo/"><![CDATA[<h2 id="git-clone-的多种方法">Git Clone 的多种方法</h2>

<p>按照下载量的大小以及 repo 的完整程度，Git Clone 实际上有很多种方法。下面依次列举一下。</p>

<h3 id="normal-clone">Normal Clone</h3>

<p>传统的 Clone 取回远程仓库的一切内容。这种方法通常被使用，下载量也是最大的。</p>

<p>以 LLVM 项目的源码仓库而言，当前的全仓尺寸大约 4.3GB 上下（其中 <code class="language-plaintext highlighter-rouge">.git</code> 目录的本地磁盘空间占用为 2.2GB左右，working directory 本体大约 2.1GB 的样子），一次完整的标准 Clone 如下：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
</pre></td><td class="rouge-code"><pre><span class="nv">$ </span>git clone https://github.com/llvm/llvm-project
Cloning into <span class="s1">'llvm-project'</span>...
remote: Enumerating objects: 6540723, <span class="k">done</span><span class="nb">.</span>
remote: Counting objects: 100% <span class="o">(</span>4544/4544<span class="o">)</span>, <span class="k">done</span><span class="nb">.</span>
remote: Compressing objects: 100% <span class="o">(</span>1492/1492<span class="o">)</span>, <span class="k">done</span><span class="nb">.</span>
remote: Total 6540723 <span class="o">(</span>delta 3982<span class="o">)</span>, reused 3053 <span class="o">(</span>delta 3052<span class="o">)</span>, pack-reused 6536179 <span class="o">(</span>from 3<span class="o">)</span>
Receiving objects: 100% <span class="o">(</span>6540723/6540723<span class="o">)</span>, 2.02 GiB | 3.16 MiB/s, <span class="k">done</span><span class="nb">.</span>
Resolving deltas: 100% <span class="o">(</span>5417295/5417295<span class="o">)</span>, <span class="k">done</span><span class="nb">.</span>
Updating files: 100% <span class="o">(</span>157588/157588<span class="o">)</span>, <span class="k">done</span><span class="nb">.</span>
<span class="nv">$ </span><span class="nb">du</span> <span class="nt">-sh</span> llvm-project llvm-project/.git
4.3G	llvm-project
2.2G	llvm-project/.git
<span class="err">$</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>这显然还是很有压力的。</p>

<p>如果你的镜像选取的不好，那么遇到 EOF 错误的几率相当高，由于 git clone 不能支持断点续传（这毕竟不是单一对象，<code class="language-plaintext highlighter-rouge">.git</code> 下面的数据对象（blob）实际上是以数据库记录的形态组织的），所以每次 EOF 都会导致整个 clone 过程重启，对于像 linux-project, llvm-project 等等这样的大仓库来说，完成首次 clone 几乎成为不可能的事情。</p>

<p><code class="language-plaintext highlighter-rouge">.git</code> 中存储的内容可以被划分为 blob, index 等等几大类，我们已经提及这些内容实际上构成了一个数据库，git 提交历史所形成的版本记录实际上被映射为数据库的表记录。</p>

<p>所以当你需要完整的数据库的时候，各方面的压力均无法轻易释放。</p>

<h3 id="shallow-clone">Shallow Clone</h3>

<p>所以最典型的轻量级克隆方法是 <code class="language-plaintext highlighter-rouge">Shallow Clone</code>。</p>

<p>这种方法的特点是仅下拉给定 refspec （通常为 <code class="language-plaintext highlighter-rouge">HEAD</code>）的相关记录，而放弃一切的其它 branches（以及 tags）。</p>

<p>对于那种分支和 release tags 奇多的大仓库来说，Shallow Clone 的效果立竿见影，能够显著降低下载量。</p>

<p>同样以 LLVM 为例：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
</pre></td><td class="rouge-code"><pre><span class="nv">$ </span>git clone <span class="nt">--depth</span><span class="o">=</span>1 https://github.com/llvm/llvm-project llvm-project-shallowed
Cloning into <span class="s1">'llvm-project-shallowed'</span>...
remote: Enumerating objects: 165365, <span class="k">done</span><span class="nb">.</span>
remote: Counting objects: 100% <span class="o">(</span>165365/165365<span class="o">)</span>, <span class="k">done</span><span class="nb">.</span>
remote: Compressing objects: 100% <span class="o">(</span>139258/139258<span class="o">)</span>, <span class="k">done</span><span class="nb">.</span>
remote: Total 165365 <span class="o">(</span>delta 36896<span class="o">)</span>, reused 61531 <span class="o">(</span>delta 21641<span class="o">)</span>, pack-reused 0 <span class="o">(</span>from 0<span class="o">)</span>
Receiving objects: 100% <span class="o">(</span>165365/165365<span class="o">)</span>, 243.62 MiB | 6.37 MiB/s, <span class="k">done</span><span class="nb">.</span>
Resolving deltas: 100% <span class="o">(</span>36896/36896<span class="o">)</span>, <span class="k">done</span><span class="nb">.</span>
Updating files: 100% <span class="o">(</span>157588/157588<span class="o">)</span>, <span class="k">done</span><span class="nb">.</span>

<span class="nv">$ </span><span class="nb">du</span> <span class="nt">-sh</span> llvm-project-shallowed llvm-project-shallowed/.git
2.3G	llvm-project-shallowed
280M	llvm-project-shallowed/.git

<span class="err">$</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>尽管我们观察到下载量的绝对值依然不小（总体大约 280MB），但这比全仓下载的 2.2GB 已经是巨大的改善了，粗略而论的话已经提速了十倍。</p>

<p>相对于很多代码仓库的 5MB 或者数十 MB 而言，280MB 可以算是一个大数值了。</p>

<p>注意，单论下载量的话，我们只需要考虑 <code class="language-plaintext highlighter-rouge">.git</code> 文件夹的尺寸即可，因为 working directory 中的文件都是通过从 <code class="language-plaintext highlighter-rouge">.git</code> 记录中解包而得来，并不被计算在下载量中。</p>

<h4 id="unshallow">Unshallow</h4>

<p>当完成了浅克隆之后，我们得到了一个单层的本地仓库，单层表示提交记录，Tree 记录，都只有单一的条目，而不是带有历史的，交叉参考的。</p>

<p>所以此时采用 git log 的话将无法查看版本提交历史记录。</p>

<p>要想查看历史记录，则需要 Unshallow 操作：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nv">$ </span>git fetch <span class="nt">--unshallow</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>它将会触发拉回全部的历史记录以及相关对象。</p>

<p>操作完成之后，你将会得到一个完整的本地仓库，一切都和正常的 <code class="language-plaintext highlighter-rouge">git clone</code> 获得的仓库是等价的。</p>

<p>这也意味着，你需要保证你的网络质量了。</p>

<h5 id="lite-unshallow">Lite unshallow</h5>

<p>如果想要稍微轻量级一点的 Unshallow，也不是没有办法，下面的指令可以尝试：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nv">$ </span>git fetch <span class="nt">--unshallow</span> <span class="nt">--filter</span><span class="o">=</span>blob:none
</pre></td></tr></tbody></table></code></pre></div></div>

<p>它的作用是略过 blobs 对象的下拉，通常这会大幅度节省下载量。</p>

<p>与该命令的核心思想相同的是在 <code class="language-plaintext highlighter-rouge">git clone</code> 时就略过 blob 对象，这种方法也被成为 blobless clone，下面将予以介绍。</p>

<h3 id="blobless-clone">Blobless Clone</h3>

<p>所谓的 Blobless Clone，就是指在所有下拉行为中都跳过 blobs 对象。</p>

<p>由于 blobs 对象实质是仓库中工作文件的文件内容，所以大多数情况下这意味着大权重的下载数据量因此而得到节省。正因为对 blobs 对象的略过不会影响到 trees 和 commits 的下载，所以在本地仓库中 git log 将会是完整有效的。</p>

<p>同样地原因，既然没有下拉文件内容 blobs，所以在本地仓库中就不会复原出工作文件拷贝。这一点值得注意：这种情况与 bare repo 是不同的，因为这样的本地仓库相比较于 bare blob 缺失了 blobs 实体。</p>

<p>当你在这样的 blobless 仓库中开始进一步操作时，涉及到的 blob 对象将被自动触发并从 remotes 拉取。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
</pre></td><td class="rouge-code"><pre><span class="nv">$ </span>git clone <span class="nt">--filter</span><span class="o">=</span>blob:none https://github.com/llvm/llvm-project llvm-project-blobless
Cloning into <span class="s1">'llvm-project-blobless'</span>...
remote: Enumerating objects: 4361826, <span class="k">done</span><span class="nb">.</span>
remote: Counting objects: 100% <span class="o">(</span>3285/3285<span class="o">)</span>, <span class="k">done</span><span class="nb">.</span>
remote: Compressing objects: 100% <span class="o">(</span>1195/1195<span class="o">)</span>, <span class="k">done</span><span class="nb">.</span>
remote: Total 4361826 <span class="o">(</span>delta 2853<span class="o">)</span>, reused 2095 <span class="o">(</span>delta 2090<span class="o">)</span>, pack-reused 4358541 <span class="o">(</span>from 3<span class="o">)</span>
Receiving objects: 100% <span class="o">(</span>4361826/4361826<span class="o">)</span>, 602.63 MiB | 13.54 MiB/s, <span class="k">done</span><span class="nb">.</span>
Resolving deltas: 100% <span class="o">(</span>3430550/3430550<span class="o">)</span>, <span class="k">done</span><span class="nb">.</span>
Enumerating objects: 1, <span class="k">done</span><span class="nb">.</span>
Counting objects: 100% <span class="o">(</span>1/1<span class="o">)</span>, <span class="k">done</span><span class="nb">.</span>
Writing objects: 100% <span class="o">(</span>1/1<span class="o">)</span>, <span class="k">done</span><span class="nb">.</span>
Total 1 <span class="o">(</span>delta 0<span class="o">)</span>, reused 1 <span class="o">(</span>delta 0<span class="o">)</span>, pack-reused 0 <span class="o">(</span>from 0<span class="o">)</span>
remote: Enumerating objects: 152470, <span class="k">done</span><span class="nb">.</span>
remote: Counting objects: 100% <span class="o">(</span>113697/113697<span class="o">)</span>, <span class="k">done</span><span class="nb">.</span>
remote: Compressing objects: 100% <span class="o">(</span>99085/99085<span class="o">)</span>, <span class="k">done</span><span class="nb">.</span>
remote: Total 152470 <span class="o">(</span>delta 29815<span class="o">)</span>, reused 14612 <span class="o">(</span>delta 14612<span class="o">)</span>, pack-reused 38773 <span class="o">(</span>from 3<span class="o">)</span>
Receiving objects: 100% <span class="o">(</span>152470/152470<span class="o">)</span>, 238.48 MiB | 1.52 MiB/s, <span class="k">done</span><span class="nb">.</span>
Resolving deltas: 100% <span class="o">(</span>36789/36789<span class="o">)</span>, <span class="k">done</span><span class="nb">.</span>
Updating files: 100% <span class="o">(</span>157590/157590<span class="o">)</span>, <span class="k">done</span><span class="nb">.</span>

<span class="nv">$ </span><span class="nb">du</span> <span class="nt">-sh</span> llvm-project-blobless llvm-project-blobless/.git
3.1G	llvm-project-blobless
1.0G	llvm-project-blobless/.git

<span class="err">$</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>blobless 的速度较慢，这是因为 git clone –filter=blob:none 的时候实际上做了两步： git fetch 和 git checkout。在 fetch 的时候 blob 的确被放弃了，但当 checkout 的时候，为了建立工作拷贝的文件，相关的 blob 不得不被下载回来。所以 git clone –filter=blob:none 节约的份额又被消费了一部分，造成了现在的后果。</p>

<p>很容易想到，如果 bare clone 的话，是不是就能完整地节约了？</p>

<h4 id="blobless--bared">blobless + Bared</h4>

<p>的确如此。所以下面做出了示例：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
</pre></td><td class="rouge-code"><pre><span class="nv">$ </span>git clone <span class="nt">--filter</span><span class="o">=</span>blob:none <span class="nt">--no-checkout</span> https://github.com/llvm/llvm-project llvm-project-blobless-bared
Cloning into <span class="s1">'llvm-project-blobless-bared'</span>...
remote: Enumerating objects: 4361890, <span class="k">done</span><span class="nb">.</span>
remote: Counting objects: 100% <span class="o">(</span>3368/3368<span class="o">)</span>, <span class="k">done</span><span class="nb">.</span>
remote: Compressing objects: 100% <span class="o">(</span>1220/1220<span class="o">)</span>, <span class="k">done</span><span class="nb">.</span>
remote: Total 4361890 <span class="o">(</span>delta 2910<span class="o">)</span>, reused 2164 <span class="o">(</span>delta 2139<span class="o">)</span>, pack-reused 4358522 <span class="o">(</span>from 3<span class="o">)</span>
Receiving objects: 100% <span class="o">(</span>4361890/4361890<span class="o">)</span>, 599.79 MiB | 14.07 MiB/s, <span class="k">done</span><span class="nb">.</span>
Resolving deltas: 100% <span class="o">(</span>3431888/3431888<span class="o">)</span>, <span class="k">done</span><span class="nb">.</span>
Enumerating objects: 1, <span class="k">done</span><span class="nb">.</span>
Counting objects: 100% <span class="o">(</span>1/1<span class="o">)</span>, <span class="k">done</span><span class="nb">.</span>
Writing objects: 100% <span class="o">(</span>1/1<span class="o">)</span>, <span class="k">done</span><span class="nb">.</span>
Total 1 <span class="o">(</span>delta 0<span class="o">)</span>, reused 1 <span class="o">(</span>delta 0<span class="o">)</span>, pack-reused 0 <span class="o">(</span>from 0<span class="o">)</span>

<span class="nv">$ </span><span class="nb">du</span> <span class="nt">-sh</span> llvm-project-blobless-bared llvm-project-blobless-bared/.git
742M	llvm-project-blobless-bared
742M	llvm-project-blobless-bared/.git

<span class="err">$</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>注意看，如果 checkout 工作拷贝的话，<code class="language-plaintext highlighter-rouge">.git</code> 中额外需要下载的 blob 对象大约为 300MB（1.1GB-742MB）。</p>

<h4 id="shllowed--blobless--bared">Shllowed + blobless + bared</h4>

<p>通过限制历史记录（commits 乃至于 trees）的下拉层级为单层（<code class="language-plaintext highlighter-rouge">--depth=1</code>），可以启用浅拷贝机制，这会限制 commits 和 trees 对象的下载量。</p>

<p>将它和前面的 blobless 叠加起来可以进一步削减下载量。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
</pre></td><td class="rouge-code"><pre><span class="nv">$ </span>git clone <span class="nt">--depth</span><span class="o">=</span>1 <span class="nt">--filter</span><span class="o">=</span>blob:none <span class="nt">--no-checkout</span> https://github.com/llvm/llvm-project llvm-project-blobless-bared-shallowed
Cloning into <span class="s1">'llvm-project-blobless-bared-shallowed'</span>...
remote: Enumerating objects: 12897, <span class="k">done</span><span class="nb">.</span>
remote: Counting objects: 100% <span class="o">(</span>12897/12897<span class="o">)</span>, <span class="k">done</span><span class="nb">.</span>
remote: Compressing objects: 100% <span class="o">(</span>10124/10124<span class="o">)</span>, <span class="k">done</span><span class="nb">.</span>
remote: Total 12897 <span class="o">(</span>delta 70<span class="o">)</span>, reused 8154 <span class="o">(</span>delta 55<span class="o">)</span>, pack-reused 0 <span class="o">(</span>from 0<span class="o">)</span>
Receiving objects: 100% <span class="o">(</span>12897/12897<span class="o">)</span>, 5.15 MiB | 1.68 MiB/s, <span class="k">done</span><span class="nb">.</span>
Resolving deltas: 100% <span class="o">(</span>70/70<span class="o">)</span>, <span class="k">done</span><span class="nb">.</span>

<span class="nv">$ </span><span class="nb">du</span> <span class="nt">-sh</span> llvm-project-blobless-bared-shallowed llvm-project-blobless-bared-shallowed/.git
6.6M	llvm-project-blobless-bared-shallowed
6.6M	llvm-project-blobless-bared-shallowed/.git

<span class="err">$</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>成果斐然，对吧？</p>

<p>没什么好说的，这几乎已经够用了。</p>

<p>然而，这种方法的坏处是 git log 没法查看，你只能在 unshallow 之后才能查看 log 和提交历史。</p>

<p>为了让 unshallow 也轻量级，下面对其加上了 <code class="language-plaintext highlighter-rouge">--filter=blob:none</code>。</p>

<blockquote>
  <p>这种做法对于 Code Reviewers 通常是够用的，他们多数时候不必关心文件的完整内容而只需要查看变更的 lines 就足够了，所以 blobs 对他们来讲大多数情况下是无需在本地变现的。</p>
</blockquote>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
</pre></td><td class="rouge-code"><pre><span class="nv">$ </span><span class="nb">cd </span>llvm-project-blobless-bared-shallowed
<span class="nv">$ </span>git fetch <span class="nt">--unshallow</span> <span class="nt">--filter</span><span class="o">=</span>blob:none
remote: Enumerating objects: 4261444, <span class="k">done</span><span class="nb">.</span>
remote: Counting objects: 100% <span class="o">(</span>4261435/4261435<span class="o">)</span>, <span class="k">done</span><span class="nb">.</span>
remote: Compressing objects: 100% <span class="o">(</span>816018/816018<span class="o">)</span>, <span class="k">done</span><span class="nb">.</span>
remote: Total 4253079 <span class="o">(</span>delta 3360725<span class="o">)</span>, reused 4247089 <span class="o">(</span>delta 3354878<span class="o">)</span>, pack-reused 0 <span class="o">(</span>from 0<span class="o">)</span>
Receiving objects: 100% <span class="o">(</span>4253079/4253079<span class="o">)</span>, 548.78 MiB | 14.85 MiB/s, <span class="k">done</span><span class="nb">.</span>
Resolving deltas: 100% <span class="o">(</span>3360725/3360725<span class="o">)</span>, completed with 3841 <span class="nb">local </span>objects.
Enumerating objects: 1, <span class="k">done</span><span class="nb">.</span>
Counting objects: 100% <span class="o">(</span>1/1<span class="o">)</span>, <span class="k">done</span><span class="nb">.</span>
Writing objects: 100% <span class="o">(</span>1/1<span class="o">)</span>, <span class="k">done</span><span class="nb">.</span>
Total 1 <span class="o">(</span>delta 0<span class="o">)</span>, reused 1 <span class="o">(</span>delta 0<span class="o">)</span>, pack-reused 0 <span class="o">(</span>from 0<span class="o">)</span>
remote: Enumerating objects: 12, <span class="k">done</span><span class="nb">.</span>
remote: Counting objects: 100% <span class="o">(</span>12/12<span class="o">)</span>, <span class="k">done</span><span class="nb">.</span>
remote: Compressing objects: 100% <span class="o">(</span>12/12<span class="o">)</span>, <span class="k">done</span><span class="nb">.</span>
remote: Total 12 <span class="o">(</span>delta 0<span class="o">)</span>, reused 12 <span class="o">(</span>delta 0<span class="o">)</span>, pack-reused 0 <span class="o">(</span>from 0<span class="o">)</span>
Receiving objects: 100% <span class="o">(</span>12/12<span class="o">)</span>, 4.21 KiB | 2.11 MiB/s, <span class="k">done</span><span class="nb">.</span>
From https://github.com/llvm/llvm-project
 <span class="k">*</span> <span class="o">[</span>new tag]                   llvmorg-10-init -&gt; llvmorg-10-init
 <span class="k">*</span> <span class="o">[</span>new tag]                   llvmorg-11-init -&gt; llvmorg-11-init
 <span class="k">*</span> <span class="o">[</span>new tag]                   llvmorg-12-init -&gt; llvmorg-12-init
 <span class="k">*</span> <span class="o">[</span>new tag]                   llvmorg-13-init -&gt; llvmorg-13-init
 <span class="k">*</span> <span class="o">[</span>new tag]                   llvmorg-14-init -&gt; llvmorg-14-init
 <span class="k">*</span> <span class="o">[</span>new tag]                   llvmorg-15-init -&gt; llvmorg-15-init
 <span class="k">*</span> <span class="o">[</span>new tag]                   llvmorg-16-init -&gt; llvmorg-16-init
 <span class="k">*</span> <span class="o">[</span>new tag]                   llvmorg-17-init -&gt; llvmorg-17-init
 <span class="k">*</span> <span class="o">[</span>new tag]                   llvmorg-18-init -&gt; llvmorg-18-init
 <span class="k">*</span> <span class="o">[</span>new tag]                   llvmorg-19-init -&gt; llvmorg-19-init
 <span class="k">*</span> <span class="o">[</span>new tag]                   llvmorg-20-init -&gt; llvmorg-20-init
 <span class="k">*</span> <span class="o">[</span>new tag]                   llvmorg-21-init -&gt; llvmorg-21-init

<span class="nv">$ </span>git log <span class="nt">--oneline</span>
f37c24194e2b <span class="o">(</span>HEAD -&gt; main, origin/main, origin/HEAD<span class="o">)</span> <span class="o">[</span>Clang] Set the final <span class="nb">date </span><span class="k">for </span>workaround <span class="k">for </span>libstdc++<span class="s1">'s `format_kind` (#140831)
2d956d2d4ecd [flang] fix ICE with ignore_tkr(tk) character in explicit interface (#140885)
dc29901efb18 [AMDGPU] PromoteAlloca: handle out-of-bounds GEP for shufflevector (#139700)
d36028120a6e [flang] add -floop-interchange and enable it with opt levels (#140182)
2cf6099cd5fa [NFC][Support] Apply clang-format to regcomp.c (#140769)
fb627e39e28a [LLVM][IR] Replace `unsigned &gt;= ConstantDataFirstVal` with static_assert (#140827)
b5e3d8ec084d [LLVM][TableGen] Use StringRef for various members `CGIOperandList::OperandInfo` (#140625)
a7ede51b556f [mlir][XeGPU] Add XeGPU Workgroup to Subgroup Distribution Pass  (#140805)
...

$ cd ..
$ du -sh llvm-project-blobless-bared-shallowed llvm-project-blobless-bared-shallowed/.git
697M	llvm-project-blobless-bared-shallowed
697M	llvm-project-blobless-bared-shallowed/.git

$
</span></pre></td></tr></tbody></table></code></pre></div></div>

<p>所以上面也展示了相应的后续步骤。此时 –depth=1 带来的优势就基本上被抵消了。</p>

<p>但是，由于 shallow clone 能够加速形成本地副本，所以这种分两步走的方法有时候对于抽象的网络条件仍然是具备意义的。</p>

<h3 id="treeless-clone">Treeless Clone</h3>

<p>Trees 对象代表着工作拷贝的目录结构关系和文件名信息，所以你也可以省去对其的下载，从而更进一步地降低下载量。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
</pre></td><td class="rouge-code"><pre><span class="nv">$ </span>git clone <span class="nt">--filter</span><span class="o">=</span>tree:0 <span class="nt">--no-checkout</span> https://github.com/llvm/llvm-project llvm-project-treeless-bared
Cloning into <span class="s1">'llvm-project-treeless-bared'</span>...
remote: Enumerating objects: 552618, <span class="k">done</span><span class="nb">.</span>
remote: Counting objects: 100% <span class="o">(</span>23/23<span class="o">)</span>, <span class="k">done</span><span class="nb">.</span>
remote: Compressing objects: 100% <span class="o">(</span>23/23<span class="o">)</span>, <span class="k">done</span><span class="nb">.</span>
remote: Total 552618 <span class="o">(</span>delta 1<span class="o">)</span>, reused 10 <span class="o">(</span>delta 0<span class="o">)</span>, pack-reused 552595 <span class="o">(</span>from 4<span class="o">)</span>
Receiving objects: 100% <span class="o">(</span>552618/552618<span class="o">)</span>, 193.18 MiB | 13.17 MiB/s, <span class="k">done</span><span class="nb">.</span>
Resolving deltas: 100% <span class="o">(</span>8873/8873<span class="o">)</span>, <span class="k">done</span><span class="nb">.</span>
Enumerating objects: 1, <span class="k">done</span><span class="nb">.</span>
Counting objects: 100% <span class="o">(</span>1/1<span class="o">)</span>, <span class="k">done</span><span class="nb">.</span>
Writing objects: 100% <span class="o">(</span>1/1<span class="o">)</span>, <span class="k">done</span><span class="nb">.</span>
Total 1 <span class="o">(</span>delta 0<span class="o">)</span>, reused 0 <span class="o">(</span>delta 0<span class="o">)</span>, pack-reused 0 <span class="o">(</span>from 0<span class="o">)</span>

<span class="nv">$ </span><span class="nb">du</span> <span class="nt">-sh</span> llvm-project-treeless-bared llvm-project-treeless-bared/.git
225M	llvm-project-treeless-bared
225M	llvm-project-treeless-bared/.git

<span class="nv">$ </span><span class="nb">cd </span>llvm-project-treeless-bared <span class="o">&amp;&amp;</span> git log <span class="nt">--oneline</span> <span class="o">&amp;&amp;</span> <span class="nb">cd</span> ..
... <span class="o">(</span>ignored<span class="o">)</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>大多数情况下，blobless 和 treeless 两者任选其一已经足以改善你的下载质量了。</p>

<blockquote>
  <p>值得注意的是，当前 git 不支持同时应用 blobless 和 treeless 的 filters 条件。</p>
</blockquote>

<h3 id="by-size">By size</h3>

<p><code class="language-plaintext highlighter-rouge">--filter</code> 还可以指定其它条件，例如限制 blob 不能超过 1MB：<code class="language-plaintext highlighter-rouge">--filter=blob:limit=1m</code>。</p>

<p>完整的 <code class="language-plaintext highlighter-rouge">--filter</code> 说明，可以在下面的链接中找到：</p>

<p><a href="https://git-scm.com/docs/git-rev-list#Documentation/git-rev-list.txt---filterltfilter-specgt">Git - git-rev-list Documentation</a></p>

<h3 id="by-object-type">By object type</h3>

<p><a href="https://git-scm.com/docs/git-sparse-checkout"><code class="language-plaintext highlighter-rouge">git sparse-checkout</code></a> 是一种较为轻量级的 checkout，它比 <code class="language-plaintext highlighter-rouge">--no-checkout</code> 温和一点，但没有 normally checkout 那么多，带上参数 <code class="language-plaintext highlighter-rouge">--sparse</code> 进行 <code class="language-plaintext highlighter-rouge">git clone</code> 即可。</p>

<p>下面的示例来自 GitLab 官方文档：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
</pre></td><td class="rouge-code"><pre><span class="c"># Clone the repo excluding all files</span>
<span class="nv">$ </span>git clone <span class="nt">--filter</span><span class="o">=</span>blob:none <span class="nt">--sparse</span> git@gitlab.com:gitlab-com/www-gitlab-com.git
Cloning into <span class="s1">'www-gitlab-com'</span>...
remote: Enumerating objects: 678296, <span class="k">done</span><span class="nb">.</span>
remote: Counting objects: 100% <span class="o">(</span>678296/678296<span class="o">)</span>, <span class="k">done</span><span class="nb">.</span>
remote: Compressing objects: 100% <span class="o">(</span>165915/165915<span class="o">)</span>, <span class="k">done</span><span class="nb">.</span>
remote: Total 678296 <span class="o">(</span>delta 472342<span class="o">)</span>, reused 673292 <span class="o">(</span>delta 467476<span class="o">)</span>, pack-reused 0
Receiving objects: 100% <span class="o">(</span>678296/678296<span class="o">)</span>, 81.06 MiB | 5.74 MiB/s, <span class="k">done</span><span class="nb">.</span>
Resolving deltas: 100% <span class="o">(</span>472342/472342<span class="o">)</span>, <span class="k">done</span><span class="nb">.</span>
remote: Enumerating objects: 28, <span class="k">done</span><span class="nb">.</span>
remote: Counting objects: 100% <span class="o">(</span>28/28<span class="o">)</span>, <span class="k">done</span><span class="nb">.</span>
remote: Compressing objects: 100% <span class="o">(</span>25/25<span class="o">)</span>, <span class="k">done</span><span class="nb">.</span>
remote: Total 28 <span class="o">(</span>delta 0<span class="o">)</span>, reused 12 <span class="o">(</span>delta 0<span class="o">)</span>, pack-reused 0
Receiving objects: 100% <span class="o">(</span>28/28<span class="o">)</span>, 140.29 KiB | 341.00 KiB/s, <span class="k">done</span><span class="nb">.</span>
Updating files: 100% <span class="o">(</span>28/28<span class="o">)</span>, <span class="k">done</span><span class="nb">.</span>

<span class="nv">$ </span><span class="nb">cd </span>www-gitlab-com

<span class="nv">$ </span>git sparse-checkout <span class="nb">set </span>data <span class="nt">--cone</span>
remote: Enumerating objects: 301, <span class="k">done</span><span class="nb">.</span>
remote: Counting objects: 100% <span class="o">(</span>301/301<span class="o">)</span>, <span class="k">done</span><span class="nb">.</span>
remote: Compressing objects: 100% <span class="o">(</span>292/292<span class="o">)</span>, <span class="k">done</span><span class="nb">.</span>
remote: Total 301 <span class="o">(</span>delta 16<span class="o">)</span>, reused 102 <span class="o">(</span>delta 9<span class="o">)</span>, pack-reused 0
Receiving objects: 100% <span class="o">(</span>301/301<span class="o">)</span>, 1.15 MiB | 608.00 KiB/s, <span class="k">done</span><span class="nb">.</span>
Resolving deltas: 100% <span class="o">(</span>16/16<span class="o">)</span>, <span class="k">done</span><span class="nb">.</span>
Updating files: 100% <span class="o">(</span>302/302<span class="o">)</span>, <span class="k">done</span><span class="nb">.</span>

</pre></td></tr></tbody></table></code></pre></div></div>

<p>For more details, see the Git documentation for <a href="https://git-scm.com/docs/git-sparse-checkout"><code class="language-plaintext highlighter-rouge">sparse-checkout</code></a>.</p>

<table>
  <tbody>
    <tr>
      <td>From: [Clone a Git repository to your local computer</td>
      <td>GitLab Docs](https://docs.gitlab.com/topics/git/clone/)</td>
    </tr>
  </tbody>
</table>

<h3 id="partial-clone-no-checkout">partial clone (<code class="language-plaintext highlighter-rouge">no-checkout</code>)</h3>

<p>实际上，前面的示例中已经提前应用了 <code class="language-plaintext highlighter-rouge">--no-checkout</code>。</p>

<p>有的时候这个参数可能是 <code class="language-plaintext highlighter-rouge">--bare</code>，但是意义不变，都是起到不 checkout 本地工作文件的目的，从而得到的是一个 bared repo。</p>

<p>示例省略。</p>

<h3 id="可能最快的-clone">可能最快的 Clone</h3>

<p>综合前面提到的各种方式，可以得到最快的 clone 方法应该是：不下载 blob 对象，不签出本地文件。相应的操作步骤大致如下：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre><span class="c">#fastest clone possible:</span>
<span class="nv">$ </span>git clone <span class="nt">--filter</span><span class="o">=</span>blob:none <span class="nt">--no-checkout</span> https://github.com/llvm/llvm-project llvm-blobless-bared
<span class="nv">$ </span><span class="nb">du</span> <span class="nt">-sh</span> llvm-blobless-bared llvm-blobless-bared/.git
<span class="nv">$ </span><span class="nb">cd </span>llvm-blobless-bared
<span class="nv">$ </span>git sparse-checkout init <span class="nt">--cone</span>
<span class="nv">$ </span>git read-tree <span class="nt">-mu</span> HEAD
</pre></td></tr></tbody></table></code></pre></div></div>

<p>完整的操作记录如下：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
</pre></td><td class="rouge-code"><pre><span class="nv">$ </span>git clone <span class="nt">--filter</span><span class="o">=</span>blob:none <span class="nt">--no-checkout</span> https://github.com/llvm/llvm-project llvm-blobless-bared
Cloning into <span class="s1">'llvm-blobless-bared'</span>...
remote: Enumerating objects: 4362056, <span class="k">done</span><span class="nb">.</span>
remote: Counting objects: 100% <span class="o">(</span>3418/3418<span class="o">)</span>, <span class="k">done</span><span class="nb">.</span>
remote: Compressing objects: 100% <span class="o">(</span>1268/1268<span class="o">)</span>, <span class="k">done</span><span class="nb">.</span>
remote: Total 4362056 <span class="o">(</span>delta 2964<span class="o">)</span>, reused 2156 <span class="o">(</span>delta 2145<span class="o">)</span>, pack-reused 4358638 <span class="o">(</span>from 3<span class="o">)</span>
Receiving objects: 100% <span class="o">(</span>4362056/4362056<span class="o">)</span>, 599.87 MiB | 16.26 MiB/s, <span class="k">done</span><span class="nb">.</span>
Resolving deltas: 100% <span class="o">(</span>3432005/3432005<span class="o">)</span>, <span class="k">done</span><span class="nb">.</span>
Enumerating objects: 1, <span class="k">done</span><span class="nb">.</span>
Counting objects: 100% <span class="o">(</span>1/1<span class="o">)</span>, <span class="k">done</span><span class="nb">.</span>
Writing objects: 100% <span class="o">(</span>1/1<span class="o">)</span>, <span class="k">done</span><span class="nb">.</span>
Total 1 <span class="o">(</span>delta 0<span class="o">)</span>, reused 1 <span class="o">(</span>delta 0<span class="o">)</span>, pack-reused 0 <span class="o">(</span>from 0<span class="o">)</span>

<span class="nv">$ </span><span class="nb">du</span> <span class="nt">-sh</span> llvm-blobless-bared llvm-blobless-bared/.git
742M	llvm-blobless-bared
742M	llvm-blobless-bared/.git

<span class="nv">$ </span><span class="nb">cd </span>llvm-blobless-bared

<span class="err">$</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>由于 git 自身的限制，因此你无法组合 blobless 和 treeless 条件，而上面列出的方法是当前评估下相对最快的 Clone 方法，最终获得的本地副本至少保证了 commits history 能够被有效地检索。</p>

<h4 id="替换为-treeless">替换为 treeless</h4>

<p>但是如果远程仓库中的文件内容不大，提交历史却超过万条甚至是数百万条，这时候 trees 对象的下载量就不应该忽视了，此时应该采用 treeless 的方式。</p>

<h4 id="替换为-sparse-checkout">替换为 sparse checkout</h4>

<p>如果你需要本地的工作文件副本，那么最快的 clone 是 shallow+sparse 方法：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre><span class="c">#fastest clone possible:</span>
<span class="nv">$ </span>git clone <span class="nt">--filter</span><span class="o">=</span>blob:none <span class="nt">--sparse</span> <span class="nt">--depth</span><span class="o">=</span>1 https://github.com/llvm/llvm-project llvm-sparse
<span class="nv">$ </span><span class="nb">du</span> <span class="nt">-sh</span> llvm-sparse llvm-sparse/.git
<span class="nv">$ </span><span class="nb">cd </span>llvm-sparse
<span class="nv">$ </span>git sparse-checkout init <span class="nt">--cone</span>
<span class="nv">$ </span>git read-tree <span class="nt">-mu</span> HEAD
<span class="nv">$ </span>git fetch <span class="nt">--unshallow</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>它会尽快得到本地工作文件，但无法进行 git log 操作，直到你 unshallow 它为止。</p>

<h2 id="背景知识">背景知识</h2>

<h3 id="blobs-trees-commits--logs">Blobs, Trees, Commits &amp; Logs</h3>

<p>Git 的实现机理中，四种基础对象按照它们承载的意义相互组织在一起，构成了我们所获得的版本历史的拼图；同时，这些基础对象按照适当的关系相互勾稽，也形成了我们所提及过的磁盘存储中的数据库结构。</p>

<p>理解上述的四种对象，可以直接阅读 ProGit Book 的 <a href="https://git-scm.com/book/zh/v2/Git-%e5%86%85%e9%83%a8%e5%8e%9f%e7%90%86-Git-%e5%af%b9%e8%b1%a1">Git - Git 对象</a> 章节。</p>

<p>其中的关系图比较有参考价值：</p>

<p><img src="https://cdn.jsdelivr.net/gh/hzimg/blog-pics@master/upgit/2025/05/20250524_1748041047.png" alt="你的 Git 目录下所有可达的对象。" /></p>

<p>它展示了 commits, trees, filenames, blobs(file contents) 之间的逻辑组织关系。</p>

<p><strong><code class="language-plaintext highlighter-rouge">Blobs</code></strong> 代表着每个工作文件的内容。</p>

<p><strong><code class="language-plaintext highlighter-rouge">Trees</code></strong> 代表着文件夹名称和文件名称，同时也涵盖了文件夹层级结构。</p>

<p><strong><code class="language-plaintext highlighter-rouge">Commits</code></strong> 对应着版本历史中的每一条提交记录，每个提交由一个唯一的 hash 串标识（本质上是一个针对该提交记录的 sha-1 摘要），hash 串被 base64 格式化后的长度 40 chars，通常取其前 6 或 8 位作为短语，从概率上说对于单一的仓库而言前 8 位足以唯一地识别 Commits 了，如 <code class="language-plaintext highlighter-rouge">70460b</code>。</p>

<p><strong><code class="language-plaintext highlighter-rouge">Logs</code></strong> 是关联到每一 Commit 的日志信息，由 <code class="language-plaintext highlighter-rouge">git log</code> 予以呈现。</p>

<p><strong><code class="language-plaintext highlighter-rouge">Refs</code></strong> 是在 Commits 历史上的特定位置的粘连的标记。通用地看，每个 hash 串都是一个 ref，此外诸如 branch，tags 等也都是某个提交的别名，也被视为 ref 的一种，并且有对应的逻辑名字，例如 <code class="language-plaintext highlighter-rouge">refs/heads/master</code>，<code class="language-plaintext highlighter-rouge">refs/tags/v1.0.0</code>，<code class="language-plaintext highlighter-rouge">refs/remotes/origin/master</code> 等等。这方面信息请阅读 Git Pro 的 <a href="https://git-scm.com/book/zh/v2/Git-%E5%86%85%E9%83%A8%E5%8E%9F%E7%90%86-Git-%E5%BC%95%E7%94%A8">Git - Git 引用</a> 章节。</p>

<h3 id="结束语">结束语</h3>

<p>还有没有更快的方法？</p>

<p>的确还有一种：</p>

<p>使用你的 github 身份申请免费的 Azure Linux 主机，然后 SSH 进去做 git clone，秒速！</p>

<p>这样不但能够得到完整仓库，速度上的损失也没有多少。</p>

<p>但这种方法也有限制，因为免费主机的磁盘空间有限，所以太大的 repo 可能你还是无法下拉。</p>

<p>如果 clone 成功了，你可以 zip 那个目录，然后用 sftp 断点续传地下拉到本地在解压缩回来，这可能是最适合网络条件不稳的场所的方案。</p>

<p>🔚</p>]]></content><author><name>hedzr</name></author><category term="devops" /><category term="git" /><category term="git" /><category term="clone" /><summary type="html"><![CDATA[罗列轻量级 Git Clone 的方法并比较 ...]]></summary></entry><entry><title type="html">Rust string(s) 初学之二</title><link href="https://hedzr.com/rust/study/rust-study-about-string-2/" rel="alternate" type="text/html" title="Rust string(s) 初学之二" /><published>2025-05-03T09:58:00+08:00</published><updated>2025-05-03T09:58:00+08:00</updated><id>https://hedzr.com/rust/study/rust-study-about-string-2</id><content type="html" xml:base="https://hedzr.com/rust/study/rust-study-about-string-2/"><![CDATA[<h2 id="前言">前言</h2>

<p>接续上一篇 <a href="https://hedzr.com/rust/study/rust-study-about-string/">Rust string(s) 初学</a> 继续整理字符串。</p>

<h2 id="rust-字符串管理以及操作">Rust 字符串管理以及操作</h2>

<h3 id="针对字符串的操作">针对字符串的操作</h3>

<h4 id="取得字符串长度">取得字符串长度</h4>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="k">let</span> <span class="n">characters_count</span> <span class="o">=</span> <span class="s">"Hi 世界"</span><span class="nf">.to_string</span><span class="p">()</span><span class="nf">.chars</span><span class="p">()</span><span class="nf">.count</span><span class="p">();</span>
<span class="c1">// = 5</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>上一篇提到过，字符计数需要用 <code class="language-plaintext highlighter-rouge">chars()</code>。</p>

<h4 id="取得片段">取得片段</h4>

<p>有多种方式取出字符串的一个片段。</p>

<p>一个典型的方法是在 <code class="language-plaintext highlighter-rouge">chars()</code> 上使用 <code class="language-plaintext highlighter-rouge">take, skip</code> 等操作之后再做搜集（<code class="language-plaintext highlighter-rouge">collect</code>）。</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="rouge-code"><pre>    <span class="nd">#[test]</span>
    <span class="k">fn</span> <span class="nf">test_string_extract</span><span class="p">()</span> <span class="p">{</span>
        <span class="nd">println!</span><span class="p">(</span><span class="s">"</span><span class="se">\n</span><span class="s">"</span><span class="p">);</span>

        <span class="k">let</span> <span class="n">text</span> <span class="o">=</span> <span class="s">"The quick brown fox jumps over the lazy dog"</span><span class="p">;</span>
        <span class="k">let</span> <span class="n">iter</span> <span class="o">=</span> <span class="n">text</span><span class="nf">.chars</span><span class="p">();</span>
        <span class="k">let</span> <span class="n">result</span> <span class="o">=</span> <span class="n">iter</span><span class="nf">.skip</span><span class="p">(</span><span class="mi">4</span><span class="p">)</span><span class="nf">.take</span><span class="p">(</span><span class="mi">5</span><span class="p">)</span><span class="py">.collect</span><span class="p">::</span><span class="o">&lt;</span><span class="nb">String</span><span class="o">&gt;</span><span class="p">();</span>
        <span class="nd">println!</span><span class="p">(</span><span class="s">"{}"</span><span class="p">,</span> <span class="n">result</span><span class="p">);</span>
    <span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>这个方法也适用于 <code class="language-plaintext highlighter-rouge">Vec&lt;char&gt;</code> 表示的字符串，尽管那实际上是字符数组，但例如 <code class="language-plaintext highlighter-rouge">Vec&lt;char&gt;::iter()</code> 获得迭代器之后可以同样方式地操作。</p>

<p>在迭代器上还可以使用诸如 <code class="language-plaintext highlighter-rouge">skip_while(predicate)</code> 等等谓词。</p>

<h4 id="比较两个字符串的相等性">比较两个字符串的相等性</h4>

<p>直接使用 <code class="language-plaintext highlighter-rouge">==</code> 进行比较，即使左右操作数分别是 <code class="language-plaintext highlighter-rouge">String</code> 和 <code class="language-plaintext highlighter-rouge">&amp;str</code> 也可以。所以 <code class="language-plaintext highlighter-rouge">a_string == "hi"</code> 的表达式合法。</p>

<p>但是如果想要不区分大小写地比较相等性呢？</p>

<p>没有办法，只能笨操作：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="rouge-code"><pre><span class="k">let</span> <span class="n">needle</span> <span class="o">=</span> <span class="s">"μτς"</span><span class="p">;</span>
<span class="k">let</span> <span class="n">haystack</span> <span class="o">=</span> <span class="s">"ΜΤΣ"</span><span class="p">;</span>

<span class="k">let</span> <span class="n">needle</span> <span class="o">=</span> <span class="n">needle</span><span class="nf">.to_lowercase</span><span class="p">();</span>
<span class="k">let</span> <span class="n">haystack</span> <span class="o">=</span> <span class="n">haystack</span><span class="nf">.to_lowercase</span><span class="p">();</span>

<span class="k">for</span> <span class="n">i</span> <span class="k">in</span> <span class="n">haystack</span><span class="nf">.matches</span><span class="p">(</span><span class="o">&amp;</span><span class="n">needle</span><span class="p">)</span> <span class="p">{</span>
    <span class="nd">println!</span><span class="p">(</span><span class="s">"{:?}"</span><span class="p">,</span> <span class="n">i</span><span class="p">);</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>或者使用正则式方案：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
</pre></td><td class="rouge-code"><pre><span class="k">use</span> <span class="nn">regex</span><span class="p">::</span><span class="n">RegexBuilder</span><span class="p">;</span>

<span class="k">fn</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">let</span> <span class="n">needle</span> <span class="o">=</span> <span class="s">"μτς"</span><span class="p">;</span>
    <span class="k">let</span> <span class="n">haystack</span> <span class="o">=</span> <span class="s">"ΜΤΣ"</span><span class="p">;</span>

    <span class="k">let</span> <span class="n">needle</span> <span class="o">=</span> <span class="nn">RegexBuilder</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="n">needle</span><span class="p">)</span>
        <span class="nf">.case_insensitive</span><span class="p">(</span><span class="k">true</span><span class="p">)</span>
        <span class="nf">.build</span><span class="p">()</span>
        <span class="nf">.expect</span><span class="p">(</span><span class="s">"Invalid Regex"</span><span class="p">);</span>

    <span class="k">for</span> <span class="n">i</span> <span class="k">in</span> <span class="n">needle</span><span class="nf">.find_iter</span><span class="p">(</span><span class="n">haystack</span><span class="p">)</span> <span class="p">{</span>
        <span class="nd">println!</span><span class="p">(</span><span class="s">"{:?}"</span><span class="p">,</span> <span class="n">i</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>使用高级正则式语法的话，还可以简化：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="k">let</span> <span class="n">re</span> <span class="o">=</span> <span class="nn">Regex</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="s">"(?i)μτς"</span><span class="p">)</span><span class="nf">.unwrap</span><span class="p">();</span>
<span class="k">let</span> <span class="n">mat</span> <span class="o">=</span> <span class="n">re</span><span class="nf">.find</span><span class="p">(</span><span class="s">"ΜΤΣ"</span><span class="p">)</span><span class="nf">.unwrap</span><span class="p">();</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>其中 <code class="language-plaintext highlighter-rouge">(?i)</code> 表示构造正则式时使用大小写不敏感的方式。</p>

<p>一般来说，不必担心 regex 的 UTF-8 适配性。</p>

<h4 id="查找子串">查找子串</h4>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="rouge-code"><pre><span class="k">fn</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">let</span> <span class="n">text</span> <span class="o">=</span> <span class="s">"The quick brown fox jumps over the lazy dog"</span><span class="p">;</span>

    <span class="c1">// Check if the string contains a word</span>
    <span class="k">if</span> <span class="n">text</span><span class="nf">.contains</span><span class="p">(</span><span class="s">"fox"</span><span class="p">)</span> <span class="p">{</span>
        <span class="nd">println!</span><span class="p">(</span><span class="s">"Found the word 'fox'!"</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>and</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="rouge-code"><pre><span class="k">fn</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">let</span> <span class="n">text</span> <span class="o">=</span> <span class="s">"The quick brown fox jumps over the lazy dog"</span><span class="p">;</span>

    <span class="c1">// Find the index of the word "brown"</span>
    <span class="k">if</span> <span class="k">let</span> <span class="nf">Some</span><span class="p">(</span><span class="n">index</span><span class="p">)</span> <span class="o">=</span> <span class="n">text</span><span class="nf">.find</span><span class="p">(</span><span class="s">"brown"</span><span class="p">)</span> <span class="p">{</span>
        <span class="nd">println!</span><span class="p">(</span><span class="s">"'brown' starts at index: {}"</span><span class="p">,</span> <span class="n">index</span><span class="p">);</span> <span class="c1">// Output: 10</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h4 id="追加字符串">追加字符串</h4>

<p><code class="language-plaintext highlighter-rouge">push_str</code> 可以用于追加字符串，而 <code class="language-plaintext highlighter-rouge">push</code> 用于追加单个字符。</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="k">let</span> <span class="k">mut</span> <span class="n">s</span> <span class="o">=</span> <span class="nn">String</span><span class="p">::</span><span class="nf">from</span><span class="p">(</span><span class="s">"abç"</span><span class="p">);</span>
<span class="n">s</span><span class="nf">.push_str</span><span class="p">(</span><span class="s">"123"</span><span class="p">);</span>
<span class="n">s</span><span class="nf">.push</span><span class="p">(</span><span class="sc">'4'</span><span class="p">);</span>
<span class="nd">assert_eq!</span><span class="p">(</span><span class="n">s</span><span class="p">,</span> <span class="s">"abç1234"</span><span class="p">);</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h4 id="插入字符串">插入字符串</h4>

<p>String 可以插入单个字符，也可以插入子串：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="rouge-code"><pre><span class="nd">#[test]</span>
<span class="k">fn</span> <span class="nf">test_string_insert</span><span class="p">()</span> <span class="p">{</span>
    <span class="nd">println!</span><span class="p">(</span><span class="s">"</span><span class="se">\n</span><span class="s">"</span><span class="p">);</span>

    <span class="k">let</span> <span class="k">mut</span> <span class="n">s</span> <span class="o">=</span> <span class="nn">String</span><span class="p">::</span><span class="nf">from</span><span class="p">(</span><span class="s">"abç"</span><span class="p">);</span>
    <span class="n">s</span><span class="nf">.insert</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="sc">'c'</span><span class="p">);</span>
    <span class="nd">assert_eq!</span><span class="p">(</span><span class="n">s</span><span class="p">,</span> <span class="s">"abcç"</span><span class="p">);</span>
    <span class="n">s</span><span class="nf">.insert_str</span><span class="p">(</span><span class="mi">3</span><span class="p">,</span> <span class="s">"def"</span><span class="p">);</span>
    <span class="nd">assert_eq!</span><span class="p">(</span><span class="n">s</span><span class="p">,</span> <span class="s">"abcdefç"</span><span class="p">);</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h4 id="移除子串">移除子串</h4>

<p>String 可以直接移除单个字符。例如：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="k">let</span> <span class="k">mut</span> <span class="n">s</span> <span class="o">=</span> <span class="nn">String</span><span class="p">::</span><span class="nf">from</span><span class="p">(</span><span class="s">"abç"</span><span class="p">);</span>
<span class="nd">assert_eq!</span><span class="p">(</span><span class="n">s</span><span class="nf">.remove</span><span class="p">(</span><span class="mi">0</span><span class="p">),</span> <span class="sc">'a'</span><span class="p">);</span>
<span class="nd">assert_eq!</span><span class="p">(</span><span class="n">s</span><span class="nf">.remove</span><span class="p">(</span><span class="mi">1</span><span class="p">),</span> <span class="sc">'ç'</span><span class="p">);</span>
<span class="nd">assert_eq!</span><span class="p">(</span><span class="n">s</span><span class="nf">.remove</span><span class="p">(</span><span class="mi">0</span><span class="p">),</span> <span class="sc">'b'</span><span class="p">);</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>但没有简单直接的方式移除一个子串，除非你使用替代子串的方法。</p>

<p>如果是想移除字符串首尾的白空格，直接使用 <code class="language-plaintext highlighter-rouge">trim()</code>。</p>

<h4 id="替代子串">替代子串</h4>

<p>替代一个子串的方法是 <code class="language-plaintext highlighter-rouge">replace()</code>：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="rouge-code"><pre>    <span class="nd">#[test]</span>
    <span class="k">fn</span> <span class="nf">test_string_replace</span><span class="p">()</span> <span class="p">{</span>
        <span class="nd">println!</span><span class="p">(</span><span class="s">"</span><span class="se">\n</span><span class="s">"</span><span class="p">);</span>

        <span class="k">let</span> <span class="n">name1</span> <span class="o">=</span> <span class="s">"Hello Free Wills, 
This is a new world!
It is a real world."</span>
            <span class="nf">.to_string</span><span class="p">();</span>
        <span class="k">let</span> <span class="n">name2</span> <span class="o">=</span> <span class="n">name1</span><span class="nf">.replace</span><span class="p">(</span><span class="s">"world"</span><span class="p">,</span> <span class="s">"YOU"</span><span class="p">);</span>
        <span class="nd">println!</span><span class="p">(</span><span class="s">"{}"</span><span class="p">,</span> <span class="n">name2</span><span class="p">);</span>
    <span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>如果想做模式匹配，见后面的 regex 节。</p>

<p>如果想要移除一个子串，也可以使用 replace，只要给出替代品为 “” 就好。</p>

<h4 id="切分字符串">切分字符串</h4>

<p>通过 <code class="language-plaintext highlighter-rouge">split()</code> 来切分字符串。</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
</pre></td><td class="rouge-code"><pre>    <span class="nd">#[test]</span>
    <span class="k">fn</span> <span class="nf">test_string_split</span><span class="p">()</span> <span class="p">{</span>
        <span class="nd">println!</span><span class="p">(</span><span class="s">"</span><span class="se">\n</span><span class="s">"</span><span class="p">);</span>

        <span class="k">let</span> <span class="n">fruits</span> <span class="o">=</span> <span class="s">"apple,banana,orange"</span><span class="p">;</span>
        <span class="k">for</span> <span class="n">token</span> <span class="k">in</span> <span class="n">fruits</span><span class="nf">.split</span><span class="p">(</span><span class="s">","</span><span class="p">)</span> <span class="p">{</span>
            <span class="nd">println!</span><span class="p">(</span><span class="s">"fruit is {}"</span><span class="p">,</span> <span class="n">token</span><span class="p">);</span>
        <span class="p">}</span>

        <span class="c1">//store in a vec</span>
        <span class="k">let</span> <span class="n">tokens</span><span class="p">:</span> <span class="nb">Vec</span><span class="o">&lt;&amp;</span><span class="nb">str</span><span class="o">&gt;</span> <span class="o">=</span> <span class="n">fruits</span><span class="nf">.split</span><span class="p">(</span><span class="s">","</span><span class="p">)</span><span class="nf">.collect</span><span class="p">();</span>
        <span class="nd">println!</span><span class="p">(</span><span class="s">"apple  is {}"</span><span class="p">,</span> <span class="n">tokens</span><span class="p">[</span><span class="mi">0</span><span class="p">]);</span>
        <span class="nd">println!</span><span class="p">(</span><span class="s">"banana is {}"</span><span class="p">,</span> <span class="n">tokens</span><span class="p">[</span><span class="mi">1</span><span class="p">]);</span>
        <span class="nd">println!</span><span class="p">(</span><span class="s">"orange is {}"</span><span class="p">,</span> <span class="n">tokens</span><span class="p">[</span><span class="mi">2</span><span class="p">]);</span>
    <span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>如果是想按照白空格切分，则可以直接使用 <code class="language-plaintext highlighter-rouge">split_whitespace()</code>。</p>

<h5 id="使用迭代器-chars">使用迭代器 <code class="language-plaintext highlighter-rouge">chars</code></h5>

<p>前文有提到过如果想要针对 String 中的 UTF-8 字符进行操作，唯一的方法是透过迭代器 <code class="language-plaintext highlighter-rouge">chars()</code> 来进行。</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="k">let</span> <span class="n">n1</span> <span class="o">=</span> <span class="s">"long long ago there lived a king"</span><span class="nf">.to_string</span><span class="p">();</span>
<span class="k">for</span> <span class="n">ch</span> <span class="k">in</span> <span class="n">n1</span><span class="nf">.chars</span><span class="p">()</span> <span class="p">{</span>
  <span class="nd">println!</span><span class="p">(</span><span class="s">"{}"</span><span class="p">,</span> <span class="n">ch</span><span class="p">);</span>
<span class="p">}</span>
<span class="nd">println!</span><span class="p">(</span><span class="s">"{} characters printed."</span><span class="p">,</span> <span class="n">n1</span><span class="nf">.chars</span><span class="p">()</span><span class="nf">.count</span><span class="p">());</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>然后可以用迭代器上的 <code class="language-plaintext highlighter-rouge">collect()</code> 来搜集字符，抽出其中的 slice 片段重组一个 vec 乃至于 string。</p>

<h3 id="正则式">正则式</h3>

<p>需要添加额外的依赖库 regex。首先修改 <code class="language-plaintext highlighter-rouge">Cargo.toml</code> 加入：</p>

<div class="language-toml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="nn">[dependencies]</span>
<span class="py">regex</span> <span class="p">=</span> <span class="s">"1"</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>然后在代码中可以引用 <code class="language-plaintext highlighter-rouge">regex::Regex</code>：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="k">use</span> <span class="nn">regex</span><span class="p">::</span><span class="n">Regex</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>一个例子是：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
</pre></td><td class="rouge-code"><pre><span class="k">use</span> <span class="nn">regex</span><span class="p">::</span><span class="n">Regex</span><span class="p">;</span>

<span class="k">fn</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">let</span> <span class="n">pattern</span> <span class="o">=</span> <span class="nn">Regex</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="s">r"^\d{4}-\d{2}-\d{2}$"</span><span class="p">)</span><span class="nf">.unwrap</span><span class="p">();</span> <span class="c1">// A pattern for a date in YYYY-MM-DD format</span>
    <span class="k">let</span> <span class="n">date</span> <span class="o">=</span> <span class="s">"2024-09-14"</span><span class="p">;</span>

    <span class="k">if</span> <span class="n">pattern</span><span class="nf">.is_match</span><span class="p">(</span><span class="n">date</span><span class="p">)</span> <span class="p">{</span>
        <span class="nd">println!</span><span class="p">(</span><span class="s">"The date is in the correct format."</span><span class="p">);</span>
    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
        <span class="nd">println!</span><span class="p">(</span><span class="s">"The date is in an incorrect format."</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>进一步的用法请参考正则式语法，以及 regex 库的文档。这里不做展开了。</p>

<h3 id="正确的字符串连接方式">正确的字符串连接方式</h3>

<p>前面在追加字符串部分已经展示了特定的追加操作在增长一个字符串。</p>

<p>更通用的字符串连接操作，一般来说首选两种方式：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre><span class="k">let</span> <span class="n">datetime</span> <span class="o">=</span> <span class="o">&amp;</span><span class="nd">format!</span><span class="p">(</span><span class="s">"{}{}{}"</span><span class="p">,</span> <span class="n">DATE</span><span class="p">,</span> <span class="n">T</span><span class="p">,</span> <span class="n">TIME</span><span class="p">);</span>

<span class="k">let</span> <span class="k">mut</span> <span class="n">datetime</span> <span class="o">=</span> <span class="nn">String</span><span class="p">::</span><span class="nf">new</span><span class="p">();</span>
<span class="n">datetime</span><span class="nf">.push_str</span><span class="p">(</span><span class="n">DATE</span><span class="p">);</span>
<span class="n">datetime</span><span class="nf">.push_str</span><span class="p">(</span><span class="n">T</span><span class="p">);</span>
<span class="n">datetime</span><span class="nf">.push_str</span><span class="p">(</span><span class="n">TIME</span><span class="p">);</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>其中，<code class="language-plaintext highlighter-rouge">format!</code> 宏是最为推荐的方式，灵活且性能优秀。如果场景更单纯则直接使用 <code class="language-plaintext highlighter-rouge">push(ch)</code> 和 <code class="language-plaintext highlighter-rouge">push_str(str)</code> 方法。</p>

<blockquote>
  <p>对此，本文并非性能调优专题，所以仅给出结论。</p>
</blockquote>

<p>如果你要使用 <code class="language-plaintext highlighter-rouge">+</code> 连接字符串，也不是不行，性能上没有太多劣势，但语法上有：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre><span class="k">fn</span> <span class="nf">main</span><span class="p">(){</span>
   <span class="k">let</span> <span class="n">n1</span> <span class="o">=</span> <span class="s">"Tutorials"</span><span class="nf">.to_string</span><span class="p">();</span>
   <span class="k">let</span> <span class="n">n2</span> <span class="o">=</span> <span class="s">"Point"</span><span class="nf">.to_string</span><span class="p">();</span>

   <span class="k">let</span> <span class="n">n3</span> <span class="o">=</span> <span class="n">n1</span> <span class="o">+</span> <span class="o">&amp;</span><span class="n">n2</span><span class="p">;</span> <span class="c1">// n2 reference is passed</span>
   <span class="nd">println!</span><span class="p">(</span><span class="s">"{}"</span><span class="p">,</span><span class="n">n3</span><span class="p">);</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>这挺反直觉的。但 拜rust教 信徒则觉得美极了。</p>

<p>如果对性能有关注，不妨看看 <a href="https://users.rust-lang.org/t/string-concatenation-best-practices-performance/65876">String concatenation best practices/performance?</a> 和 <a href="https://github.com/hoodie/concatenation_benchmarks-rs">hoodie/concatenation_benchmarks-rs</a>，这里做了一些 benchmarks，而且罗列了各种各样的字符串连接方法，全面但冗余。</p>

<p>总的来说，rust 缺乏一个 <code class="language-plaintext highlighter-rouge">StringBuilder</code>，但是利用 reserved spaces 和 <code class="language-plaintext highlighter-rouge">format!</code> 宏的方式也算是很灵活了，甚至有时候还很有优势。</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre><span class="k">fn</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">let</span> <span class="k">mut</span> <span class="n">s</span> <span class="o">=</span> <span class="nn">String</span><span class="p">::</span><span class="nf">with_capacity</span><span class="p">(</span><span class="mi">50</span><span class="p">);</span> <span class="c1">// Preallocate space for 50 characters</span>
    <span class="n">s</span><span class="nf">.push_str</span><span class="p">(</span><span class="s">"Hello, "</span><span class="p">);</span>
    <span class="n">s</span><span class="nf">.push_str</span><span class="p">(</span><span class="s">"world!"</span><span class="p">);</span>

    <span class="nd">println!</span><span class="p">(</span><span class="s">"{}"</span><span class="p">,</span> <span class="n">s</span><span class="p">);</span> <span class="c1">// "Hello, world!"</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h2 id="后记">后记</h2>

<p>有关 Rust 字符串的操作的更多内容，敬请期待下一篇（但我不确定什么时候继续字符串话题）。</p>

<p>图片直接取自网络，此致。</p>

<p>上一篇 <a href="https://hedzr.com/rust/study/rust-study-about-string/">Rust string(s) 初学</a> 中介绍了 Rust 字符串的几种数据类型表示法，但那是面向通用编程的，针对系统编程和互操作性方面的实际上还有 OsString，OsStr，以及 CString 和 CStr，这些类型的特殊性不多，暂不具体介绍，或者留待异日吧。</p>

<p>🔚</p>]]></content><author><name>hedzr</name></author><category term="rust" /><category term="study" /><category term="rust" /><category term="string" /><summary type="html"><![CDATA[Rust 初学笔记，关于字符串之二 [...]]]></summary></entry><entry><title type="html">Rust string(s) 初学</title><link href="https://hedzr.com/rust/study/rust-study-about-string/" rel="alternate" type="text/html" title="Rust string(s) 初学" /><published>2025-05-01T09:58:00+08:00</published><updated>2025-05-02T09:58:00+08:00</updated><id>https://hedzr.com/rust/study/rust-study-about-string</id><content type="html" xml:base="https://hedzr.com/rust/study/rust-study-about-string/"><![CDATA[<h2 id="前言">前言</h2>

<p>梳理我学习 Rust 时的一些感受和体验。</p>

<p>因为 Rust 的语法和 C++ 区别非常大，所以有时候经常陷入混乱，特别是当我同时使用 Rust/C++/Golang/Zig 编码时，尤其前段时间为了发布 <a href="https://docs.hedzr.com/">文档站</a> 我又捡起 Next/Typescript 做了很多工作，各种语法在脑子里糊成一团了。</p>

<p>所以突然想，从头整理一遍，或许也有一定的意义。</p>

<p>对我自己肯定有意义。</p>

<p>当对于看官你嘛，那就取决于你了。</p>

<blockquote>
  <p>由于我是一个深度 C++ 患者，所以各种不适需要调整。也因此，这个重新梳理的所得，这篇文章，也算得上典型的致初学 Rust 的 C++ 程序员的对口说明。</p>
</blockquote>

<h2 id="rust-字符串管理以及操作">Rust 字符串管理以及操作</h2>

<h3 id="几种字符串表达方式">几种字符串表达方式</h3>

<p>正统的字符串表达方式有两种：<code class="language-plaintext highlighter-rouge">String</code> 和 <code class="language-plaintext highlighter-rouge">&amp;str</code>。它们被用于变量声明。</p>

<p>字符串字面量，是一种常量，它总是被编译器抽出来归入初始化数据区，在可执行文件中单独被设置一个区段置于尾部，在可执行文件被加载到内存中时，这个区段被装入一个内存中的只读区段。足够智慧的编译器能够将字面量划分为两个或更多的子分区，其中一个是可丢弃的，即当 app 运行到一个特定点之后（一般是 <code class="language-plaintext highlighter-rouge">c0startup</code> 即将交递控制权给 <code class="language-plaintext highlighter-rouge">main</code> 时 ），这个可丢弃区段的内存区段就被释放。</p>

<p>我们也会顺便讨论相近的一些形态，如 <code class="language-plaintext highlighter-rouge">Vec&lt;char&gt;</code> 等。</p>

<h4 id="str"><code class="language-plaintext highlighter-rouge">&amp;str</code></h4>

<p>Rust 的字符串管理，令 C++/C#/Kotlin 程序员不适。</p>

<p>因为它的互操作特性着实隐晦而且繁琐。</p>

<p>当你有一个字符串字面量时，赋初值给一个变量，你会得到一个 <code class="language-plaintext highlighter-rouge">&amp;str</code>，但是，实际上这是一个 <code class="language-plaintext highlighter-rouge">&amp;'static str</code>，没错，这个生命期声明式的语法着实令人费解，尽管多数人都死记硬背，而且告诉自己习惯了就好，但加上 <code class="language-plaintext highlighter-rouge">&amp;'a str</code> 和 <code class="language-plaintext highlighter-rouge">&amp;'_ str</code> 呢？前者表示一个动态的生命期，带有标识符 <code class="language-plaintext highlighter-rouge">a</code>，所以所有生命期同名的变量被编译器隐含地当作有同样的截止期。而后者代表有一个生命期，但并不那么关心，纯粹是为了语法需要——做过编译器，我是指实用型编译器而非课程上的玩具，的人会明白，一个新语言要能设计的自洽且完美，基本上是不可能的，尤其是当你赋予它众多现代编程语言的特性时。</p>

<p>目前看来，也只有 C 保持了相当的语法层面的完美性，但代价是它的确有点简陋了。而像 C#/Kotlin/Swift 作为当代语言的执牛头者（语法层面），都有各色奇怪难解的部分显得丑陋。至于次一阶级的语言，Zig/Rust 等等，则难看的地方太多了。至于 Golang，就没必要提了，三教九流之外还有一档叫做不入流，就是为它而设的。</p>

<p>哦，对了，继续说那个变量：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="k">let</span> <span class="n">a</span><span class="p">:</span> <span class="o">&amp;</span><span class="nb">str</span> <span class="o">=</span> <span class="s">"hi"</span><span class="p">;</span>
<span class="k">let</span> <span class="n">b</span> <span class="o">=</span> <span class="s">"you"</span><span class="p">;</span> <span class="c1">// = &amp;str or &amp;'static str</span>
<span class="n">_</span> <span class="o">=</span> <span class="n">a</span>
<span class="n">_</span> <span class="o">=</span> <span class="n">b</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>这没什么特别的，重点是，如果 <code class="language-plaintext highlighter-rouge">let mut a</code>，并不意味着这个字符串能够被修改，这个声明只是让 <code class="language-plaintext highlighter-rouge">a</code> 可变，即，<code class="language-plaintext highlighter-rouge">a = "you";</code> 能够让 <code class="language-plaintext highlighter-rouge">a</code> 指向一个新的只读形态的字符串。</p>

<p><img src="https://cdn.jsdelivr.net/gh/hzimg/blog-pics@master/upgit/2025/05/20250502_1746146833.jpg" alt="5ykg9" /></p>

<p>所以，<code class="language-plaintext highlighter-rouge">&amp;str</code> 是一个指向字符串的指针。其目标可以是只读的（程序初始化数据区），也可以是动态的（即 heap 上分配空间的可被修改的字符串）。</p>

<p>精确点说，<code class="language-plaintext highlighter-rouge">&amp;str</code> 是一个不可变参考，指向一个 UTF-8 字符序列的缓冲区。你不能使用一个 <code class="language-plaintext highlighter-rouge">&amp;str</code> 变量来修改目标位置的字符串，即使它指向的目标实质上是一个 heap 的可变字符串对象；此时你只能使用那个 <code class="language-plaintext highlighter-rouge">String</code> 对象的变量来操作和修改字符串本身。</p>

<h4 id="string"><code class="language-plaintext highlighter-rouge">String</code></h4>

<p>那么顺理成章的下一个问题，可被修改的字符串怎么声明？</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre><span class="k">let</span> <span class="n">da</span><span class="p">:</span> <span class="nb">String</span> <span class="o">=</span> <span class="s">"hi"</span><span class="nf">.into</span><span class="p">();</span>
<span class="k">let</span> <span class="n">db</span> <span class="o">=</span> <span class="nn">String</span><span class="p">::</span><span class="nf">from</span><span class="p">(</span><span class="s">"you"</span><span class="p">);</span>
<span class="k">let</span> <span class="n">dc</span><span class="p">:</span> <span class="nb">String</span> <span class="o">=</span> <span class="n">a</span><span class="nf">.to_string</span><span class="p">();</span> <span class="c1">// from a `&amp;str`</span>
<span class="k">let</span> <span class="n">dd</span> <span class="o">=</span> <span class="n">a</span><span class="nf">.into</span><span class="p">();</span>
<span class="k">let</span> <span class="n">de</span> <span class="o">=</span> <span class="n">a</span><span class="nf">.clone</span><span class="p">()</span><span class="nf">.to_string</span><span class="p">();</span> <span class="c1">// make a clone</span>
<span class="k">let</span> <span class="n">df</span> <span class="o">=</span> <span class="s">"hi"</span><span class="nf">.to_string</span><span class="p">();</span> <span class="c1">// also got a String</span>
<span class="nd">println!</span><span class="p">(</span><span class="s">"{}, {}, {}, {}, {}</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">da</span><span class="p">,</span> <span class="n">db</span><span class="p">,</span> <span class="n">dc</span><span class="p">,</span> <span class="n">dd</span><span class="p">,</span> <span class="n">de</span><span class="p">,</span> <span class="n">df</span><span class="p">);</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>这里展示了从字面量得到 <code class="language-plaintext highlighter-rouge">String</code> 声明的方法，以及从一个 <code class="language-plaintext highlighter-rouge">&amp;str</code> 转换为 <code class="language-plaintext highlighter-rouge">String</code> 的方法。</p>

<blockquote>
  <p>当使用 <code class="language-plaintext highlighter-rouge">into()</code> 时，需要指明目标变量类型 <code class="language-plaintext highlighter-rouge">String</code>，编译器才能完成推导。</p>
</blockquote>

<p>反过来，从 <code class="language-plaintext highlighter-rouge">String</code> 得到 <code class="language-plaintext highlighter-rouge">&amp;str</code> 只需要引用借用即可：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="k">let</span> <span class="n">ab</span> <span class="o">=</span> <span class="o">&amp;*</span><span class="n">db</span><span class="p">;</span> <span class="c1">// got a `&amp;str`</span>
<span class="k">let</span> <span class="n">db2</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">db</span><span class="p">;</span> <span class="c1">// got a `&amp;String`</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>是不是“很有意思”？</p>

<p>其实我一直宣称厌烦 Rust，因为我对于那种救世主似的为你好的宣扬者只觉得可笑。如果有谁当我是个傻瓜，给我一个傻瓜套装，处处限制，那么我肯定会觉得其实它才是不是傻？！Rust 的引以为豪的借用所有权，实际上并没有什么有趣的东西，因为做过编译器的人对此习以为常，把这个东西暴露给使用者去解决，这不是现代编译器应该做的。</p>

<p>编译器的发展方向，是让开发者越发地自由表达，同时还能精确表达，绝非是给开发者添上镣铐，让他们跳舞，然后说“啊……你跳得真美！”。</p>

<p>所以为了得到 <code class="language-plaintext highlighter-rouge">&amp;str</code>，你需要在 <code class="language-plaintext highlighter-rouge">String</code> 上前缀以 <code class="language-plaintext highlighter-rouge">&amp;*</code> 借用，否则单个 <code class="language-plaintext highlighter-rouge">&amp;</code> 借用只能得到正宗的 <code class="language-plaintext highlighter-rouge">&amp;String</code> 借用目标。</p>

<p>说来这也不难理解，符合逻辑。只不过 <code class="language-plaintext highlighter-rouge">&amp;*</code> 着实难看。当然，觉得难看还是因为 <code class="language-plaintext highlighter-rouge">C/C++</code> 积习难改，这么一想，我也就心平气和了。</p>

<blockquote>
  <p>如果真心厌烦 <code class="language-plaintext highlighter-rouge">&amp;*</code>，或者你可以使用 <code class="language-plaintext highlighter-rouge">as_str()</code>：</p>

  <div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="k">let</span> <span class="n">example_string</span> <span class="o">=</span> <span class="nn">String</span><span class="p">::</span><span class="nf">from</span><span class="p">(</span><span class="s">"example_string"</span><span class="p">);</span>
<span class="nf">print_literal</span><span class="p">(</span><span class="n">example_string</span><span class="nf">.as_str</span><span class="p">());</span>
</pre></td></tr></tbody></table></code></pre></div>  </div>

  <p>这样也是个办法。</p>
</blockquote>

<p>进一步地，如果需要可变对象呢，那就需要 <code class="language-plaintext highlighter-rouge">&amp;mut str</code> 类型，此时的语法就要更新为：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
</pre></td><td class="rouge-code"><pre><span class="nd">#[test]</span>
<span class="k">fn</span> <span class="nf">test_str</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">let</span> <span class="k">mut</span> <span class="n">db</span> <span class="o">=</span> <span class="nn">String</span><span class="p">::</span><span class="nf">from</span><span class="p">(</span><span class="s">"you"</span><span class="p">);</span>
    <span class="p">{</span>
        <span class="k">let</span> <span class="n">ab</span> <span class="o">=</span> <span class="o">&amp;*</span><span class="n">db</span><span class="p">;</span> <span class="c1">// got a `&amp;str`</span>
        <span class="nd">println!</span><span class="p">(</span><span class="s">"ab:  {}"</span><span class="p">,</span> <span class="n">ab</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="k">let</span> <span class="k">mut</span> <span class="n">mab</span> <span class="o">=</span> <span class="o">&amp;</span><span class="k">mut</span> <span class="o">*</span><span class="n">db</span><span class="p">;</span> <span class="c1">// got a `&amp;mut str`</span>
    <span class="k">let</span> <span class="k">mut</span> <span class="n">dc</span> <span class="o">=</span> <span class="nn">String</span><span class="p">::</span><span class="nf">from</span><span class="p">(</span><span class="s">"her"</span><span class="p">);</span>

    <span class="nd">assert_eq!</span><span class="p">(</span><span class="n">mab</span><span class="p">,</span> <span class="s">"you"</span><span class="p">);</span>
    <span class="n">mab</span> <span class="o">=</span> <span class="o">&amp;</span><span class="k">mut</span> <span class="o">*</span><span class="n">dc</span><span class="p">;</span>
    <span class="nd">assert_eq!</span><span class="p">(</span><span class="n">mab</span><span class="p">,</span> <span class="s">"her"</span><span class="p">);</span>
    <span class="nd">println!</span><span class="p">(</span><span class="s">"mab: {}"</span><span class="p">,</span> <span class="n">mab</span><span class="p">);</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>这段测试代码毫无用处，仅仅向你展示 <code class="language-plaintext highlighter-rouge">&amp;mut *a_str</code> 这种语法。</p>

<h5 id="string-是什么str-呢"><code class="language-plaintext highlighter-rouge">String</code> 是什么？<code class="language-plaintext highlighter-rouge">&amp;str</code> 呢？</h5>

<p>Rust 设计 <code class="language-plaintext highlighter-rouge">String</code> 管理一个内存 buffer 区，就像字节数组一样。</p>

<p>这一点很重要！</p>

<p>所以 <code class="language-plaintext highlighter-rouge">String</code> 的直接操作得到的都是基于 <code class="language-plaintext highlighter-rouge">byte</code> 的。例如 <code class="language-plaintext highlighter-rouge">String::len()</code> 返回的是缓冲区的字节尺寸。</p>

<p>而更重要的是，Rust 约束 <code class="language-plaintext highlighter-rouge">String</code> 中总是存放一个字符串字面量的 <code class="language-plaintext highlighter-rouge">UTF-8</code> 表示。</p>

<p>什么意思？即这块缓冲区中的字符串内容是以 <code class="language-plaintext highlighter-rouge">UTF-8</code> 方式编码的。</p>

<p>所以 <code class="language-plaintext highlighter-rouge">String::len()</code> 并不能得到字符串字面量的字符个数。为了取得字符个数，你需要采用 <code class="language-plaintext highlighter-rouge">Chars</code> 迭代器方式进行计数：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre><span class="k">let</span> <span class="n">da_chs</span> <span class="o">=</span> <span class="nn">String</span><span class="p">::</span><span class="nf">from</span><span class="p">(</span><span class="s">"hi 世界"</span><span class="p">);</span>
<span class="k">let</span> <span class="n">count</span> <span class="o">=</span> <span class="n">da_chs</span><span class="nf">.chars</span><span class="p">()</span><span class="nf">.count</span><span class="p">();</span>
<span class="nd">println!</span><span class="p">(</span><span class="s">"{}"</span><span class="p">,</span> <span class="n">count</span><span class="p">);</span>         <span class="c1">// got '5'</span>
<span class="nd">println!</span><span class="p">(</span><span class="s">"{}"</span><span class="p">,</span> <span class="n">da_chs</span><span class="nf">.len</span><span class="p">());</span>  <span class="c1">// got '9', "hi ".len = 3, "世界".len = 6</span>

<span class="k">let</span> <span class="n">iter</span> <span class="o">=</span> <span class="n">da_chs</span><span class="nf">.chars</span><span class="p">()</span> <span class="c1">// got a `Chars`: i.e., str::iter::Chars&lt;'_&gt;</span>
<span class="n">_</span> <span class="o">=</span> <span class="n">iter</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>如上，<code class="language-plaintext highlighter-rouge">da_chs.chars()</code> 返回一个 <code class="language-plaintext highlighter-rouge">str::iter::Chars&lt;'_&gt;</code> 类型的迭代器，通过计算（实际上会得到优化，立即返回）其方法 <code class="language-plaintext highlighter-rouge">.count()</code> 会得到 utf-8 字符串的字符个数。</p>

<h4 id="vecchar"><code class="language-plaintext highlighter-rouge">Vec&lt;char&gt;</code></h4>

<p>前面解释了如何理解 <code class="language-plaintext highlighter-rouge">String</code> 和 <code class="language-plaintext highlighter-rouge">&amp;str</code>，有时候，也可以用 <code class="language-plaintext highlighter-rouge">Vec&lt;char&gt;</code> 来表示一个 <code class="language-plaintext highlighter-rouge">UTF-8</code> 字符的数组。这种方式有时候可能有点用处，当你需要明确地有性能地操作 UTF-8 字符时，它的优势比较于 <code class="language-plaintext highlighter-rouge">String</code> 和 <code class="language-plaintext highlighter-rouge">String::chars</code> 为无需 utf-8 计算，简单地用数组下标就可以获取数组集合中的每个 utf-8 字符，如果直接在 <code class="language-plaintext highlighter-rouge">String</code> 上操作则难免需要 utf-8 库参与来辨识每个有效的 UTF-8 字符才能完成索引操作，有时候这种代价可能难以接受，而解决的办法就是从 <code class="language-plaintext highlighter-rouge">String</code> 上一次性地将字符串分解为 char 的数组 <code class="language-plaintext highlighter-rouge">Vec&lt;char&gt;</code>，然后后继的大规模的字符索引操作的性能就能得到提升了。</p>

<p>所以，相互转换的方式是：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
</pre></td><td class="rouge-code"><pre><span class="nd">#[test]</span>
<span class="k">fn</span> <span class="nf">test_strings</span><span class="p">()</span> <span class="p">{</span>
    <span class="c1">// &amp;str and String</span>

    <span class="k">let</span> <span class="n">static_str</span><span class="p">:</span> <span class="o">&amp;</span><span class="nb">str</span> <span class="o">=</span> <span class="s">"hello"</span><span class="p">;</span>
    <span class="k">let</span> <span class="k">mut</span> <span class="n">dyn_str</span><span class="p">:</span> <span class="nb">String</span> <span class="o">=</span> <span class="s">"hello, world"</span><span class="nf">.into</span><span class="p">();</span>
    <span class="n">_</span> <span class="o">=</span> <span class="n">dyn_str</span><span class="p">;</span>
    <span class="n">dyn_str</span> <span class="o">=</span> <span class="nn">String</span><span class="p">::</span><span class="nf">from</span><span class="p">(</span><span class="n">static_str</span><span class="p">);</span>
    <span class="n">dyn_str</span><span class="nf">.push_str</span><span class="p">(</span><span class="s">", appended str here."</span><span class="p">);</span>

    <span class="nd">println!</span><span class="p">(</span><span class="s">"{}, {}"</span><span class="p">,</span> <span class="n">static_str</span><span class="p">,</span> <span class="n">dyn_str</span><span class="p">);</span>

    <span class="c1">// Vec&lt;char&gt;</span>

    <span class="k">let</span> <span class="n">vc1</span><span class="p">:</span> <span class="nb">Vec</span><span class="o">&lt;</span><span class="n">_</span><span class="o">&gt;</span> <span class="o">=</span> <span class="n">static_str</span><span class="nf">.to_string</span><span class="p">()</span><span class="nf">.chars</span><span class="p">()</span><span class="nf">.collect</span><span class="p">();</span>
    <span class="k">let</span> <span class="n">vc2</span><span class="p">:</span> <span class="nb">Vec</span><span class="o">&lt;</span><span class="nb">char</span><span class="o">&gt;</span> <span class="o">=</span> <span class="n">vc1</span><span class="p">;</span>
    <span class="nd">println!</span><span class="p">(</span><span class="s">"vec&lt;char&gt; #2, {}"</span><span class="p">,</span> <span class="n">vc2</span><span class="nf">.iter</span><span class="p">()</span><span class="py">.collect</span><span class="p">::</span><span class="o">&lt;</span><span class="nb">String</span><span class="o">&gt;</span><span class="p">());</span>
    <span class="k">let</span> <span class="n">vc3_cloned</span> <span class="o">=</span> <span class="n">vc2</span><span class="nf">.to_vec</span><span class="p">();</span>
    <span class="nd">println!</span><span class="p">(</span><span class="s">"vec&lt;char&gt; #3, {}"</span><span class="p">,</span> <span class="n">vc3_cloned</span><span class="nf">.iter</span><span class="p">()</span><span class="py">.collect</span><span class="p">::</span><span class="o">&lt;</span><span class="nb">String</span><span class="o">&gt;</span><span class="p">());</span>
    <span class="k">let</span> <span class="n">vc4</span><span class="p">:</span> <span class="nb">Vec</span><span class="o">&lt;</span><span class="nb">char</span><span class="o">&gt;</span> <span class="o">=</span> <span class="n">dyn_str</span><span class="nf">.chars</span><span class="p">()</span><span class="nf">.collect</span><span class="p">();</span>
    <span class="nd">println!</span><span class="p">(</span><span class="s">"vec&lt;char&gt; #4, {}"</span><span class="p">,</span> <span class="n">vc4</span><span class="nf">.iter</span><span class="p">()</span><span class="py">.collect</span><span class="p">::</span><span class="o">&lt;</span><span class="nb">String</span><span class="o">&gt;</span><span class="p">());</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>咦？有点累了，不写了，请看代码自行理解。</p>

<blockquote>
  <p><img src="https://cdn.jsdelivr.net/gh/hzimg/blog-pics@master/upgit/2025/05/20250502_1746142520.jpg" alt="1_QSGqF71DU2cEqJ7lFgaGlQ" /></p>

</blockquote>

<p>收尾了。</p>

<h2 id="后记">后记</h2>

<p>有关 Rust 字符串的操作的更多内容，敬请期待下一篇。</p>

<p>图片直接取自网络，此致。</p>

<p>其中，overlay_image 取自 Youtube 一个课程 <code class="language-plaintext highlighter-rouge">String and &amp;str | Learn Rust part 7</code>。不给链接了，请自行去搜索——因为 ads 和内容链接的缘故，微信说我的文档博客站涉嫌非法诱导#$@!&amp;*^$，我觉得吧，我也没指望过微信等等。</p>

<h3 id="如何学习-rust">如何学习 Rust？</h3>

<p>我的经验是，同时使用 vscode + rust-analyser 和 JetBrains RustRover。</p>

<p>前者有点简陋，但调试器比较稳定。</p>

<p>它的问题在于简陋，有的跳转会出现问题。此外一个 bug 是，时不时会抽风，正在编写的代码在存盘时会突然被替换为标准库相关代码，例如 <code class="language-plaintext highlighter-rouge">String</code> 的部分源码，此时代码文件处于修改状态，注意用 <code class="language-plaintext highlighter-rouge">Meta-Z</code> undo 一次，就能找回你的代码，然后再次存盘时就正常保存，而不至于丢失代码。如果你错误地再次存盘，那么正在编写的代码就丢了，由于没有 RustRover 的 Local History 机制能够进行找回，那么就是真的丢了，只能重写。</p>

<blockquote>
  <p>这个 bug 是 vscode 的问题。</p>

  <p>当然，不精确地说，还是 rust 的 for vscode 的 plugin 的问题，未知的崩溃导致 vscode 进行了错误的缓冲区内容传输而毁坏了你的当前编辑缓冲区，所以接下来 undo 能够恢复上一个编辑缓冲区从而找回你原有的代码。</p>

  <p>同样的 bug 在 Zig 开发时也有，只不过 Zig 开发中是 当存盘时，或者在 <code class="language-plaintext highlighter-rouge">fmt</code> 上鼠标飘过或者点击时，你的代码会被 fmt 源码替代，又或者是其他 zig std 标准库源码。解决办法同样是立即 undo，然后存盘。</p>
</blockquote>

<p>后者大部分情况下全都好用。但问题是其执行器和调试器不稳定，经常出现无法明示的意外崩溃。此外调试器中常常无法正确计算变量的值，所以变量数值查看会出现错误。</p>

<p>但 RR 的优点是有利于初学者学习。因为它的 AI 补全功能能够为你提示常见的补全片段，所以本文中提到的各种相互转换在 RR 中常常无需记忆可以无脑补全。只不过这个补全有时候偶尔也会不完全彻底可靠，因为少数情况下它会产生复杂的调用转换，而实际上可能只需要 <code class="language-plaintext highlighter-rouge">&amp;*</code> 即可（只是一个特例，其他情形也有）。对于这个问题，本文就有用了。或者，你可以通过深度挖掘 Rust 程序设计教程以及相关的教科书来做到完全的语法概念理解，也就能发现那些冗余转换，从而给出正确的简化形式。</p>

<p>那么，两者混合起来，对于初学者就比较友善。</p>

<p>这是我个人的体验。皆因我的传统思维过于固化。</p>

<p>🔚</p>]]></content><author><name>hedzr</name></author><category term="rust" /><category term="study" /><category term="rust" /><category term="string" /><summary type="html"><![CDATA[Rust 初学笔记，关于字符串 [...]]]></summary></entry><entry><title type="html">Docsite Released</title><link href="https://hedzr.com/devops/react/docsite-released/" rel="alternate" type="text/html" title="Docsite Released" /><published>2025-03-15T09:58:00+08:00</published><updated>2025-03-16T09:58:00+08:00</updated><id>https://hedzr.com/devops/react/docsite-released</id><content type="html" xml:base="https://hedzr.com/devops/react/docsite-released/"><![CDATA[<h2 id="docshedzrcom">docs.hedzr.com</h2>

<p>如题，<a href="https://docs.hedzr.com">docs.hedzr.com</a> 总算是释出了。</p>

<p>当前主要的成果是初步完成了我所满意的运行框架，然后是初步完成了 cmdr.v2 的文档撰写。
其次是为一些其他的我的开源项目也定下了基本的撰写框架。</p>

<p>因此，未免继续自我拖延，还是先释放出来比较好，自己会更没借口。</p>

<blockquote>
  <p>当前只是先行 public，文档撰写并未彻底完成。</p>

  <p>按照计划，撰写工作将会在余生中保持，也没有 final ending 的时候。</p>

  <p>当然，至少阶段性的内容我仍会保持进度尽快完成。</p>
</blockquote>

<h2 id="内幕">内幕</h2>

<p>经过大量时间的调查、研究和尝试，最终还是自行搭起架子，使用 Nextjs 和 Fumadocs 为基础建立了 docs 框架结构。</p>

<p>我想要的文档、手册撰写框架，其实也未必很私有，还是应该很有代表性的。</p>

<ol>
  <li>支持 Markdown 或者类似的简单标记语言</li>
  <li>Dark 模式自适应</li>
  <li>面向各类终端的响应式布局</li>
  <li>章节结构灵活，支持分部（Parts）</li>
  <li>支持多语种</li>
  <li>支持软件手册多版本</li>
</ol>

<p>在页面章节结构上，我希望要有：</p>

<ol>
  <li>左侧 sidebar 的分部、分章节</li>
  <li>右侧（或者页内）的当前页 headings 结构</li>
  <li>headings 自动编号</li>
</ol>

<p>在 Markdown 支持上，我希望要有：</p>

<ol>
  <li>通用 Markdown 标准或/和Github扩展</li>
  <li>上下标，删除线，脚注</li>
  <li>公式支持</li>
  <li>LaTeX 支持</li>
  <li>mermaid 支持</li>
  <li>Tabs 代码块支持</li>
  <li>代码块行号，高亮。</li>
</ol>

<p>然而这样的要求基本上不可得。也许是我太吹毛求疵了。</p>

<h3 id="做选择题">做选择题</h3>

<p>最为接近要求的是 Sphinx。</p>

<p>事实上，我花费了大量精力来架构一个完整的基于 Sphinx 的撰写框架，它也确实能够很好地工作，上面提到的各种特性也通过各类插件集成和整合的七七八八了。</p>

<p>然后用起来就是不对劲。</p>

<p>可能是我看不上 python，或者 ruby，这些引擎驱动的前端都太莽拙了，无论是反应、细节调整，还是部署，全都差一点点，从而整个效果就差了一个大档次。</p>

<p>然后是 Nodejs 系列的什么 vuepress，vitepress 等等了，挺省事的。cmdr-docs v1 就是使用 vuepress 做的，省心省力。缺点就是功能缺失，结构调整起来基本不可能。</p>

<p>这和我近年来放弃前端开发有关。另一方面，即使我不放弃前端的具体开发，vue 的设计理念也是我看不上的，我是比较喜欢 react 或者 angular 风格的。</p>

<p>其他的工具，包括 MkDocs 等等，我也都有深入实作和研究过。但最终还是一个不满意就终结了。</p>

<p>但是优秀的 docsite 成品很多，很多都能令人眼前一亮，它们基本都是 nodejs 基础上的。</p>

<p>这也很好理解，我所说的 docsite 应该具备的种种特性，大部分都需要优秀的前端界面效果来呈现，需要良好设计的 css module 来予以约束。所以基于后端方式的静态页面 generators 往往难以满足精细要求。</p>

<p>基于 nodejs 的 docsite 工具的典型缺点是稍有编辑活动时，就大量的各种 live 更新以便反应到开发时的 web page 上，消耗大量资源。</p>

<p>这些缺点，在去年我花费精力再度学习 react 时基本得到了改善，谈解决还不至于，nodejs 解决不了虚耗资源的问题。但至少是可以接受了。</p>

<h3 id="选定离手">选定离手</h3>

<p>所以这些年来对 docsite tool 持续不断的研究结果，就是唯有自行建立一套满足个人各种要求的框架。</p>

<p>在进一步地在琳琅满目的前端框架中筛选、比较，冗长的试用、体验，放弃或者进一步深入，之后，我个人的决定是 NextJS 和 MDX 来做自行整合。为了省点力气，又从大批 NextJS templates 中选出了 Fumadocs 来当中间层基础，以免从零开始累死人。</p>

<p>这就是 <a href="https://github.com/hedzr/cmdr-docs-dev">cmdr-docs-dev</a> 了。</p>

<blockquote>
  <p>目前分为两个阶段，一是 dev preview 阶段，会发布到 <a href="https://preview.hedzr.com">preview.hedzr.com</a>，在稳定后在合并到 master 上，即 <a href="https://docs.hedzr.com">docs.hedzr.com</a>。</p>

  <p>你可以访问 preview 网站，但需要经过 vercel 认证，这也很简单，用你的 Github 帐户身份授权一下就行了。</p>
</blockquote>

<p>在选定了 NextJS + MDX + Fumadocs 之后，又有大量的周折，倒也不必提起了。总之，rollback 掉所有周折之后，我想要的 docsite 基本上算是有了，就在 <a href="https://docs.hedzr.com">这里</a>。</p>

<h2 id="后记">后记</h2>

<p>所以我会考虑经常性地在 <a href="https://docs.hedzr.com">这里</a> 做更新。</p>

<p>一方面是组织我的开源项目的文档。</p>

<p>另一方面，是考虑是否要做一些结集工作，相应地 blog 主站就不会怎么更新了，或许。</p>

<p>实际上在 docs.hedzr.com 中我已经整合了 blog 这一模块。但没有真正启用它。因为，一旦我灌入 200 篇 posts，无论是 pnpm build 还是 vercel build 都会陷入难堪的境地。如果是上推到 repo 等待 vercel 的构建，那么结果一定是构建时间超过 45min 而异常终止。</p>

<p>解决问题的方法，一是买 vercel 服务，二是别灌入那么多 posts。</p>

<p>所以，这里就是单纯的 docs，而 blog 继续保持在 Github Pages 中单独发行吧。</p>

<p>🔚</p>]]></content><author><name>hedzr</name></author><category term="devops" /><category term="react" /><category term="nextjs" /><summary type="html"><![CDATA[docs.hedzr.com released [...]]]></summary></entry></feed>