<?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://www.harttle.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://www.harttle.com/" rel="alternate" type="text/html" /><updated>2026-04-02T04:18:02+00:00</updated><id>https://www.harttle.com/feed.xml</id><title type="html">Harttle Land</title><subtitle>关于 Web、Linux、Vim、算法的笔记、教程和常见问题指南。</subtitle><author><name>Harttle</name></author><entry><title type="html">【译】做好工作：Doing a Job</title><link href="https://www.harttle.com/2023/07/06/doing-a-job.html" rel="alternate" type="text/html" title="【译】做好工作：Doing a Job" /><published>2023-07-06T00:00:00+00:00</published><updated>2023-07-06T00:00:00+00:00</updated><id>https://www.harttle.com/2023/07/06/doing-a-job</id><content type="html" xml:base="https://www.harttle.com/2023/07/06/doing-a-job.html"><![CDATA[<p>Hyman Rickover 是美国海军上将，被称为核动力海军之父。这里翻译1982年他在哥伦比亚大学的演讲，关于他的管理哲学。现代管理学虽然有很多新的方法，但文中他的很多观点仍然切中要害。一起学习下！</p>

<h2 id="如何管理下属">如何管理下属</h2>

<p>事情最终是人办成的，不是组织或管理系统办成的。因此必须尽早给予下属足够的授权和责任，这样他们会快速成长并能够帮助管理者。当然经理最终还要负责，为下属的过错承担指责。</p>

<p>随着下属的成长，工作也要相应地增加，使他永远无法完成所有工作。这既是鞭策也是挑战：在提升了下属的能力的同时也让管理者能腾出精力去承担更多的责任。组织的成员变得能够承担更多新的困难的任务时，他们会为自己的工作感到自豪，这风气会在组织内传播。</p>

<p>我们需要允许员工去寻求更多的工作和更大的责任。在我的组织里，没有正式的职责描述或组织结构。责任的定义很宽泛，这样就不会把人限制住。只要员工认为最好的事就可以去做，他们可以找任何人或去任何地方寻求帮助。每个人的限制只有自己的能力。</p>

<!--more-->

<h2 id="工作的连续性">工作的连续性</h2>

<p>如果人们在轮岗，复杂的工作就没法有效地完成。因此，管理者必须让工作充满挑战和回报，这样他的员工才能在组织中任职多年。让他们充分地发挥他们的知识、经验和企业记忆。</p>

<p>国防部却不认为重要的工作需要有连续性。总部和外勤人员都是每隔几年就会轮换，他们的文职上级官员也是一样。</p>

<p>该系统实际上助长了缺乏经验和不负责任。当一名军官终于开始上手时，就该轮换了。在这种制度下，现任可以将自己的问题归咎于前任。在他们的工作成果显现出来之前，就会被调走。下属永远都处于适应新工作或新老板，我们实在不能指望他们投入地有效地工作。</p>

<p>任何工作想要做好，都需要让员工认为他完全负责这项工作，并且就像是他要负责一辈子一样。需要让他认真对待工作，就像对待他自己的事和自己的钱一样。如果他觉得自己只是一个临时的保管人，或者这份工作只是通往更高职位的垫脚石，那么他就不会考虑组织的长远利益。他的漫不经心会被他的下属察觉到，同样他们也会漫不经心。太多人一生都在寻找下一份工作。只有让他认为这项工作完全地属于他，他才能不去操心下家。</p>

<h2 id="责任感">责任感</h2>

<p>要直接地参与到工作中去，这是每个经理的责任。不仅要发现问题，还要解决问题。这一职责优先于所有其他事项、个人野心或个人的舒适圈。</p>

<p>我们的政府系统，甚至工业界的一个主要缺陷是留有不尽职的余地。官员们往往愿意接受并适应他们明知是错误的情况。人们的倾向是淡化问题而不是积极尝试纠正问题。一旦认识到这一点，许多下属就会放弃，将自己的观点隐藏在自己内心，等待别人采取行动。当这种情况发生时，经理就无法得到下属的经验和想法，而下属通常比经理更了解其特定领域。</p>

<p>经理必须向员工灌输个人责任感，以确保工作顺利完成。不幸的是情况仍在变差，尤其是大型组织。工作没做好时，人们经常会说：“不是我负责的”。确实是这样的，这样说的人确实没有负责，因为他就是不负责的人。事实上，虽然他可能确实不负法律责任，工作也可能不是直接指定给他的，但任何人都不能推卸其参与的工作失败的责任。</p>

<p>除非在出现问题时能找到真正负责的人，否则就没有人真正负责。随着现代管理理论的出现，组织通过将项目划分为子项目，以集体方式处理问题变得越来越普遍，就出现了没有人对整个项目负责的情况。还有一种趋势是管理层级越来越多，理论上更利于控制。但这也是共同负责的一种形式，很容易导致无人负责。大公司经常出现这种情况，国防部也不例外。</p>

<p>第二次世界大战前，当我来到华盛顿担任船舶局电气部门负责人时，我发现一个人负责设计，另一个人负责生产，第三个负责维护，第四个负责财务。整个局都是这样运作的。我认为很不合理：设计问题会出现在生产中，生产错误会出现在维护中，而财务问题则涉及到各个领域。我改了这个结构：让一个人负责他的整个设备领域——设计、生产、维护和承包。如果出现任何问题，我清楚地知道该找谁。我按照同样的原则管理我现在的组织。</p>

<h2 id="决心和毅力">决心和毅力</h2>

<p>好的管理者必须有不可动摇的决心和毅力。决定需要做什么很容易，但完成它却很困难。好的想法不会被自动采纳，需要勇敢地、迫切地去实施这些想法。实施之后它们还容易因没人关心或缺乏维护而被推翻，因此需要持续的努力。很多时候，我们意识到了这些重要的问题，但没有人愿意持续付出努力来解决这些问题。</p>

<p>没有决心就不可能完成任何有价值的事情。例如，在核电发展的早期，获得批准建造第一艘核潜艇——鹦鹉螺号——几乎与设计和建造它一样困难。海军中的许多人反对建造核潜艇。</p>

<p>同样，海军曾经认为核动力航母和巡洋舰过于昂贵，尽管它们具有无限航程和不需要脆弱的支援舰艇的明显优势。但今天我们的核潜艇舰队被广泛认为是对核战争最有效的威慑力量。我们的核动力航空母舰和巡洋舰通过捍卫我们在世界各地的利益而证明了自己的价值，即使是在印度洋等偏远地点。在那里，燃油船舶的能力会受到燃料供应的严重限制。</p>

<h2 id="关注细节">关注细节</h2>

<p>负责人必须关注细节。如果他不关注细节，他的下属也不会。“细节决定成败”，关注看似微不足道的事情是困难而单调的。在我的工作中，我大概把百分之九十九的时间花在别人可能称之为琐碎的细节上。大多数管理者更愿意关注宏大的愿景。但如果忽略细节，项目就会失败。任何宏大愿景都没法挽救失败。</p>

<p>为了保持适当的控制，我们必须有简单而直接的方法来了解正在发生的事情。虽然有很多方法，但都涉及持续的苦差事。因此，负责人经常发明“管理信息系统”，旨在从运营中提取细节，以供忙碌的高管去了解。通常这个事做得太过，以至于高管与他的员工和正在进行的工作失去了联系。</p>

<p>注重细节并不要求经理亲自做所有事情，你每天只能工作二十四小时。因此要放大自己的产出，需要创造一个让下属能够发挥最大能力的环境。一些管理专家主张严格限制下属人数——通常是五到七人。但如果有能力的人只需要你一天几分钟的时间，也没必要严格限制。大概有四十个人直接向我汇报。这样我就能跟上正在发生的事情，并使他们能够快速采取行动。后一个方面尤为重要。如果不能从上级那里得到及时的决策和行动，有能力的人不会长待下去。</p>

<h2 id="工作方法">工作方法</h2>

<h3 id="扁平管理">扁平管理</h3>
<p>我需要核计划中许多关键人物的频繁口头和书面报告。其中包括核舰艇的指挥官、学校和实验室的负责人以及制造商工厂和商业造船厂的代表。我坚持要求他们直接向我报告他们发现的问题——并且用简单的英语。这为他们的主要工作提供了无限的灵活性（高度结构化的管理系统通常无法做到这一点），直接向我传达他们的问题和建议，不会经过其他人的过滤。这方面管理层级过多的国防部却是反例，因为高层决策者通常与拥有第一手知识的下属被隔离开来。</p>

<h3 id="优先级">优先级</h3>
<p>为了有效地完成工作，必须确定优先级。太多的人让他们的已经在做的事情决定了优先事项。每天办公室都会有一些不重要但有趣的琐事，决不能让这些事占据时间。人类倾向于将时间消磨在不需要脑力或精力的不重要的事情上。由于这些问题很容易解决，因此会给人一种虚假的成就感。经理必须自律，以确保他的精力集中在真正需要的地方。</p>

<h3 id="独立审查">独立审查</h3>
<p>所有工作都应独立地、公正地进行审查。在工程和制造领域，工业界在质量控制上花费了大量资金。但公正地审查和监督的概念在其他领域也很重要。即使是最敬业的人也会犯错误，而且许多员工不够敬业。政府和工业界产生的这许多糟糕的成果和纯粹的胡言乱语，就因为缺乏适当的检查。</p>

<h3 id="公开讨论">公开讨论</h3>
<p>要培养员工清晰地、有力地论证的能力。要鼓励公开地讨论分歧，以便充分探讨问题的各个方面。此外，重要问题应以书面形式提出。写下自己的论点更能锐化思维过程，口头讨论中容易忽视的弱点在书面讨论中会非常明显。</p>

<h3 id="书面报告">书面报告</h3>
<p>如果重要的决定没有记录下来，就只能依赖个人记忆，而随着人们离开或改变工作内容，这记忆会很快消失。在我的工作中，回溯多年前做出决定时考虑了哪些事实非常重要。有了正确的视角，遇到新问题时就可以轻易地解决。这还可以最大限度地减少重复过去错误的风险。此外，如果重要的沟通和行动没有清晰地记录下来，人们就会不确定是否理解过或者执行过。</p>

<h3 id="承认错误">承认错误</h3>
<p>人类倾向于希望事情能够顺利进行，即使有证据或怀疑事实并非如此。一个成功的管理者必须抵制这种倾向。尤其是当你投入了大量的时间和精力时，做到这一点会更加困难。虽然承认曾经的错误观点并不容易，但必须约束自己来客观地面对事实并做出必要的改变——无论自己面临什么后果。负责人必须在这方面亲自做出表率。这要求他在必要时干掉自己的项目，并且要求他的下属也这样做。出于技术原因，我曾经去国会建议终止一个由我同意资助的项目。这不是一项令人愉快的任务，但工作中必须保持绝对客观。</p>

<h2 id="结语">结语</h2>
<p>任何管理制度都无法替代辛勤的工作。管理者自己不努力工作就不能指望他的下属努力工作，他需要以身作则。经理可能不是最聪明或最有知识的人，但如果他全身心投入工作并付出必要的努力，他的员工就会跟随他的领导。</p>

<p>我提到的这些想法并不新鲜——前几代人都认识到努力工作、注重细节、个人责任和决心的价值。在今天这些仍然是做好工作的最重要的管理方法，而不是那些大肆吹捧的现代管理技术。它们体现的是一种管理常识，这是你无法在管理学课堂上学到的。</p>

<p>我并不反对商业教育。会计、金融、商业法等知识在商业环境中可能很有价值。我认为有害的是管理学给人的印象，即通过某些管理技术以及一些简单的学术规则，一个人就能管理好任何工作。</p>]]></content><author><name>Harttle</name></author><category term="工作方法" /><category term="管理方法" /><summary type="html"><![CDATA[Hyman Rickover 是美国海军上将，被称为核动力海军之父。这里翻译1982年他在哥伦比亚大学的演讲，关于他的管理哲学。现代管理学虽然有很多新的方法，但文中他的很多观点仍然切中要害。一起学习下！ 如何管理下属 事情最终是人办成的，不是组织或管理系统办成的。因此必须尽早给予下属足够的授权和责任，这样他们会快速成长并能够帮助管理者。当然经理最终还要负责，为下属的过错承担指责。 随着下属的成长，工作也要相应地增加，使他永远无法完成所有工作。这既是鞭策也是挑战：在提升了下属的能力的同时也让管理者能腾出精力去承担更多的责任。组织的成员变得能够承担更多新的困难的任务时，他们会为自己的工作感到自豪，这风气会在组织内传播。 我们需要允许员工去寻求更多的工作和更大的责任。在我的组织里，没有正式的职责描述或组织结构。责任的定义很宽泛，这样就不会把人限制住。只要员工认为最好的事就可以去做，他们可以找任何人或去任何地方寻求帮助。每个人的限制只有自己的能力。]]></summary></entry><entry><title type="html">Linux 下共享代理到局域网</title><link href="https://www.harttle.com/2022/11/22/linux-share-proxy-over-lan.html" rel="alternate" type="text/html" title="Linux 下共享代理到局域网" /><published>2022-11-22T00:00:00+00:00</published><updated>2022-11-22T00:00:00+00:00</updated><id>https://www.harttle.com/2022/11/22/linux-share-proxy-over-lan</id><content type="html" xml:base="https://www.harttle.com/2022/11/22/linux-share-proxy-over-lan.html"><![CDATA[<p>这个博客在八年前分享过 <a href="https://www.harttle.com/2014/10/08/linux-route.html">用 Linux 做 WiFi 热点</a>，今天的设备和软件已经完全不同了。可以很方便地做曾经很复杂的事情，同时这个世界也更复杂了。现在我们要实现的是 <strong>把代理分享给局域网，连上 WiFi 即连上了代理</strong>。</p>

<p>这件事最简单的办法是直接刷一个 OpenWrt，较为复杂的办法是刷一个 unRaid、PVE 或 ESXi 上面装一个 OpenWrt，最复杂的办法是在 ArchLinux 里手动配各种服务。本文就来介绍这个最复杂的办法。因为一来笔者更熟悉这老旧的东西，Linux 不需要赶潮流也能用得很好，即便几十年前的 Linux 技术今天仍然管用；二来手动搭建起来虽然麻烦，但是单 OS 的架构比较简单，后期维护时不容易忘掉。整个架构的思路大概是：</p>

<ol>
  <li>把 SOCKS5/HTTP 代理转成一个透明代理。流量发给它就能走 SOCKS5。</li>
  <li>配置 DHCP 服务器，把网关和 DNS 设置为这台机器。本机的 DNS 走 DoH 或 DoT 到上游。</li>
  <li>iptables 把局域网的流量转发给 redsocks。</li>
  <li>这样默认所有局域网流量都去代理了，可以加一个 ipset 规则来让做黑名单不走代理。</li>
</ol>

<!--more-->

<h2 id="约定">约定</h2>

<p>下文为了方便描述，做如下约定：</p>

<ul>
  <li>本机：是指我们这一台用来做路由的 Linux 机器。</li>
  <li>接口：interface，指 IP 层的接口，可以通过 <code class="language-plaintext highlighter-rouge">ip addr</code> 查看它们的信息。</li>
  <li>本地网段为 <code class="language-plaintext highlighter-rouge">192.168.1.1/24</code>，本机的本地环回（localhost）接口为 <code class="language-plaintext highlighter-rouge">lo</code>。</li>
  <li>我的发行版为 ArchLinux，软件包名为 AUR 或 pacman（没有区分）的包名。</li>
  <li>有些命令需要管理员权限，你可以 <code class="language-plaintext highlighter-rouge">sudo</code>，也可以以管理员身份来操作。行文中忽略。</li>
  <li>systemd 服务（比如 redsocks）设置开机启动需要 <code class="language-plaintext highlighter-rouge">systemctl enable redsocks</code>。下文只管启动，比如 <code class="language-plaintext highlighter-rouge">systemctl start redsocks</code>。</li>
</ul>

<h2 id="socks5-转透明代理">SOCKS5 转透明代理</h2>

<p>这里选用 <a href="https://github.com/darkk/redsocks">redsocks</a> 来实现，它不仅可以接 SOCKS5 代理，HTTP 代理也可以。安装软件包 redsocks 并配置 <code class="language-plaintext highlighter-rouge">/etc/redsocks.conf</code>。几个重要的选项如下：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>redsocks {
    // redsocks 服务的套接字
    local_ip = 0.0.0.0; // 设置为 `0.0.0.0` 较为方便，也可以 iptables 多一次转发给 `lo`
    local_port = 31338;

    // 代理的套接字
    ip = 127.0.0.1;
    port = 8080;

    // 代理的类型: socks4, socks5, http-connect, http-relay
    type = socks5;

    // 代理的用户名密码，可选
    // login = "foobar";
    // password = "baz";
}
</code></pre></div></div>

<p>通过 <code class="language-plaintext highlighter-rouge">systemctl start redsocks</code> 启动。现在把 TCP 请求转发给这个任意接口（interface）的 local_port，这个请求就会走代理了。</p>

<h2 id="dhcp-网络配置">DHCP 网络配置</h2>

<p>DHCP 服务是关键，它指定了局域网中的设备的 IP 和掩码、如何路由、去哪里访问 DNS。在本文的任务中，我们需要：</p>

<ol>
  <li>让局域网的设备把网关（Gateway）设置为我们的 Linux 机器。</li>
  <li>为这些设备设置 DNS 为我们的 Linux 机器，并作为唯一的 DNS（很重要，见下文）。</li>
</ol>

<blockquote>
  <p>这一设置可以在既有的路由器上设置，也可以关掉路由器的 DNS 服务，本机启动一个 dhcpd 可以参考 <a href="https://www.harttle.com/2014/10/08/linux-route.html">这里</a>。</p>
</blockquote>

<p>必须本地开启 DNS 服务是为了避免公网的 DNS 污染。这一点很重要，也是透明代理和浏览器代理（SOCKS5 或 HTTP）的区别：对于后者 DNS 发生在代理服务端，而对于前者 DNS 发生在客户端（也就是局域网的每一台机器上）。如果 DNS 返回的 IP 不正确，即使把流量倒给 redsocks 也无法得到正确的应答。</p>

<p>调试 Tips：</p>

<ul>
  <li>你可能需要时不时地清空 DNS 缓存，来反映最新的效果。Archlinux 上重启 nscd 即可，MacOS 上 <code class="language-plaintext highlighter-rouge">sudo killall -HUP mDNSResponder</code>，Android/iOS 上开关 WiFi 一次。</li>
  <li>如果你怀疑得到的 IP 不正确，可以看看它在不在<a href="https://zh.m.wikiversity.org/zh/%E9%98%B2%E7%81%AB%E9%95%BF%E5%9F%8E%E5%9F%9F%E5%90%8D%E6%9C%8D%E5%8A%A1%E5%99%A8%E7%BC%93%E5%AD%98%E6%B1%A1%E6%9F%93IP%E5%88%97%E8%A1%A8">这个列表</a>里。</li>
</ul>

<p>本地 DNS 服务需要监听端口 53（最原始的、明文的 DNS 服务），上游/upstream 需要是 TLS 或 HTTPS 加密的，这样可以确保（尽量）得到正确的（好很多的）IP。服务端软件有很多可以选，用 coredns 也行，用 dnsmasq + iplist 也行，也可以直接用 <a href="https://wiki.archlinux.org/title/Cloudflared">cloudflared</a>。</p>

<h2 id="iptables-转发">iptables 转发</h2>

<p>安装软件包 iptables，转发局域网来的流量到 redsocks 服务。安装后先启动服务 <code class="language-plaintext highlighter-rouge">systemctl start iptables</code>，然后就可以改路由表了。下面的命令示意了重要的几个 iptables 规则，你可能需要适合自己系统的规则。在执行这些命令之前，你要掌握如何恢复 iptables 规则（重启是一种办法），iptables 配置错误可能导致立即断网（手边准备一台笔记本）。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 创建一个 REDSOCKS_FILTER 规则链，用来过滤不走代理的流量</span>
iptables <span class="nt">-t</span> nat <span class="nt">-N</span> REDSOCKS
<span class="c"># 创建一个 REDSOCKS 规则链，用来设置如何走代理</span>
iptables <span class="nt">-t</span> nat <span class="nt">-N</span> REDSOCKS_FILTER

<span class="c"># REDSOCKS_FILTER 过滤发往内网的流量</span>
iptables <span class="nt">-t</span> nat <span class="nt">-A</span> REDSOCKS_FILTER <span class="nt">-d</span> 0.0.0.0/8 <span class="nt">-j</span> RETURN
iptables <span class="nt">-t</span> nat <span class="nt">-A</span> REDSOCKS_FILTER <span class="nt">-d</span> 127.0.0.0/8 <span class="nt">-j</span> RETURN
iptables <span class="nt">-t</span> nat <span class="nt">-A</span> REDSOCKS_FILTER <span class="nt">-d</span> 192.168.1.1/24 <span class="nt">-j</span> RETURN
iptables <span class="nt">-t</span> nat <span class="nt">-A</span> REDSOCKS_FILTER <span class="nt">-j</span> REDSOCKS <span class="c"># 剩下的流量，进入 REDSOCKS 规则链</span>

<span class="c"># REDSOCKS 把 TCP 流量发给 redsocks 服务，默认端口为 31338</span>
iptables <span class="nt">-t</span> nat <span class="nt">-A</span> REDSOCKS <span class="nt">-p</span> tcp <span class="nt">-j</span> REDIRECT <span class="nt">--to-port</span> 31338

<span class="c"># 来自局域网的流量（由于 DHCP 配置了网关为本机，其他设备的流量因此会过来）</span>
iptables <span class="nt">-t</span> nat <span class="nt">-A</span> PREROUTING <span class="nt">-s</span> 192.168.1.1/24 <span class="nt">-p</span> tcp <span class="nt">-j</span> REDSOCKS_FILTER

<span class="c"># 代理发出的流量，不应该再过 REDSOCKS 规则，不然就循环了</span>
<span class="c"># 这里取决于你的代理发出的包应该怎么匹配，比如我的代理进程是用户 `proxy` 启动的，通过 `owner` 模块就可以匹配到</span>
iptables <span class="nt">-t</span> nat <span class="nt">-I</span> OUTPUT <span class="nt">-p</span> tcp <span class="nt">-m</span> owner <span class="nt">--uid-owner</span> proxy <span class="nt">-j</span> RETURN
</code></pre></div></div>

<p>这样局域网的机器连进来（如果是 WiFi 网络的话），关掉自己的代理（如果有的话），清空自己的 DNS 缓存，就可以全局走本机的 SOCKS5 代理了。 注意 iptables 配置默认不会持久化，可以参考 <a href="https://www.harttle.com/2014/10/08/linux-route.html">用 Linux 做 WiFi 热点</a> 一文。</p>

<h2 id="设置-ip-黑名单">设置 IP 黑名单</h2>

<p>有些网站必须通过本地网络才能访问，走代理后会无法访问或访问速度很慢。我们需要让以这些 IP 为目的地的流量，不走代理。iptables 提供了一个 ipset 的模块，只需要安装 ipset 软件包并启动 <code class="language-plaintext highlighter-rouge">systemctl start ipset</code>。在路由表中用 <code class="language-plaintext highlighter-rouge">-m set --match-set &lt;set name&gt; dst</code> 来匹配目标 IP <code class="language-plaintext highlighter-rouge">dst</code> 是否在集合 <code class="language-plaintext highlighter-rouge">&lt;set name&gt;</code> 中。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 我们的 set name 为 "blacklist"</span>
iptables <span class="nt">-t</span> nat <span class="nt">-A</span> REDSOCKS_FILTER <span class="nt">-m</span> <span class="nb">set</span> <span class="nt">--match-set</span> blacklist dst <span class="nt">-j</span> RETURN
</code></pre></div></div>

<p>然后 <code class="language-plaintext highlighter-rouge">REDSOCKS_FILTER</code> 规则链就在帮我们过滤掉黑名单中的目标 IP 了，接下来就是这个名单如何生成的问题了。创建和生成名单很简单：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 创建一个哈希类型的列表，只需要执行一次</span>
ipset <span class="nt">-N</span> blacklist <span class="nb">hash</span>:net
<span class="c"># 添加一个 IP 到列表中</span>
ipset <span class="nt">-A</span> blacklist 36.51.226.13
</code></pre></div></div>

<p>和 iptables 类似 ipset 也不会自动持久化，每次加完后可以把它存到 <code class="language-plaintext highlighter-rouge">/etc/ipset.conf</code>，下次启动时就会自动读取了。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ipset save <span class="o">&gt;</span> /etc/ipset.conf
</code></pre></div></div>

<p>也可以找一些靠谱的在线 IP 列表，加一个 <a href="https://wiki.archlinux.org/title/Systemd/Timers">systemd timer</a> 去定时拉取。这样黑名单就可以定时更新了，更新脚本很容易写：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">for </span>addr <span class="k">in</span> <span class="nv">$$</span><span class="o">(</span>curl http://example.com<span class="o">)</span> <span class="c"># 换成你的 URL</span>
<span class="k">do
    </span>ipset <span class="nt">-A</span> blacklist <span class="nv">$addr</span>
<span class="k">done</span>
</code></pre></div></div>]]></content><author><name>Harttle</name></author><category term="Linux" /><category term="iptables" /><category term="ipset" /><category term="路由" /><category term="redsocks" /><summary type="html"><![CDATA[这个博客在八年前分享过 用 Linux 做 WiFi 热点，今天的设备和软件已经完全不同了。可以很方便地做曾经很复杂的事情，同时这个世界也更复杂了。现在我们要实现的是 把代理分享给局域网，连上 WiFi 即连上了代理。 这件事最简单的办法是直接刷一个 OpenWrt，较为复杂的办法是刷一个 unRaid、PVE 或 ESXi 上面装一个 OpenWrt，最复杂的办法是在 ArchLinux 里手动配各种服务。本文就来介绍这个最复杂的办法。因为一来笔者更熟悉这老旧的东西，Linux 不需要赶潮流也能用得很好，即便几十年前的 Linux 技术今天仍然管用；二来手动搭建起来虽然麻烦，但是单 OS 的架构比较简单，后期维护时不容易忘掉。整个架构的思路大概是： 把 SOCKS5/HTTP 代理转成一个透明代理。流量发给它就能走 SOCKS5。 配置 DHCP 服务器，把网关和 DNS 设置为这台机器。本机的 DNS 走 DoH 或 DoT 到上游。 iptables 把局域网的流量转发给 redsocks。 这样默认所有局域网流量都去代理了，可以加一个 ipset 规则来让做黑名单不走代理。]]></summary></entry><entry><title type="html">SSH 配置端口转发</title><link href="https://www.harttle.com/2022/05/02/ssh-port-forwarding.html" rel="alternate" type="text/html" title="SSH 配置端口转发" /><published>2022-05-02T00:00:00+00:00</published><updated>2022-05-02T00:00:00+00:00</updated><id>https://www.harttle.com/2022/05/02/ssh-port-forwarding</id><content type="html" xml:base="https://www.harttle.com/2022/05/02/ssh-port-forwarding.html"><![CDATA[<p>SSH 隧道或 SSH 端口转发可以用来在客户端和服务器之间建立一个加密的 SSH 连接，通过它来把本地流量转发到服务器端，或者把服务器端流量转发到本地。比如从本地访问服务器上的 MySQL 管理后台，或者把本地的串流、SMB、CIFS 等服务暴露在服务器所在的公网。本文将介绍 SSH 隧道的本地端口转发、远程端口转发等使用方式，以及如何配置 SSH 允许长连接、开机时自动建立连接。</p>

<!--more-->

<h2 id="本地端口转发">本地端口转发</h2>

<p>本地端口转发用于把本地机器（SSH 客户端）的端口转发到服务器（SSH 服务器），然后发给目标机器的某个端口。比如在 Linux、MacOS 等 Unix 系统上，可以通过 <code class="language-plaintext highlighter-rouge">-L</code> 参数来做本地端口转发。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ssh <span class="nt">-L</span> <span class="o">[</span>LOCAL_IP:]LOCAL_PORT:DESTINATION:DESTINATION_PORT <span class="o">[</span>USER@]SSH_SERVER
</code></pre></div></div>

<p>这时 SSH 客户端会监听本地的端口 <code class="language-plaintext highlighter-rouge">LOCAL_PORT</code>，把所有发给该端口的 TCP 连接都发给指定的服务器，然后再连接到目标机器。这个目标机器通常是服务器自己，也可以是任何其他机器。在目标机器看来，这个请求来自 SSH 服务器，相当于连到了服务器的内网。</p>

<p>例如服务器在 <code class="language-plaintext highlighter-rouge">localhost:33062</code> 上运行着 MySQL 的管理后端，暴露给公网会不够安全，这时就可以把本地 <code class="language-plaintext highlighter-rouge">8080</code> 端口转发给服务器的 <code class="language-plaintext highlighter-rouge">33062</code>：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ssh <span class="nt">-L</span> 8080:localhost:33062 harttle@mysql.example.com
</code></pre></div></div>

<p>然后在本机访问 <code class="language-plaintext highlighter-rouge">http://localhost:8080</code> 就可以了。注意上述命令省略了本地 IP，默认本机所有 IP 都可以访问。注意这个命令会像往常一样，登录到服务器端的 Shell。如果只用来端口转发可以指定 <code class="language-plaintext highlighter-rouge">-N</code> 参数：这样会启动一个阻塞的进程，直到 Ctrl-C 手动终止掉。</p>

<h2 id="远程端口转发">远程端口转发</h2>

<p>远程端口转发和本地端口转发正好相反，用来把服务器（SSH 服务器）上的某个端口转发到本地机器，再转发给对应的服务。通常用于把本地机器的服务暴露给外网使用。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ssh <span class="nt">-R</span> <span class="o">[</span>REMOTE:]REMOTE_PORT:DESTINATION:DESTINATION_PORT <span class="o">[</span>USER@]SSH_SERVER
</code></pre></div></div>

<p>这时 SSH 服务器会监听发往 <code class="language-plaintext highlighter-rouge">REMOTE_PORT</code> 上的请求，转发到本地机器，再发给 <code class="language-plaintext highlighter-rouge">DESTINATION</code> 机器的 <code class="language-plaintext highlighter-rouge">DESTINATION_PORT</code> 端口。例如本地有一个 Plex 媒体串流服务，希望在外网也可以访问。恰好有一台外网可以访问的服务器 <code class="language-plaintext highlighter-rouge">example.com</code>，那么可以：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ssh <span class="nt">-R</span> 8080:localhost:32400 harttle@example.com
</code></pre></div></div>

<p>然后本地的 Plex 服务就可以在外网通过 <code class="language-plaintext highlighter-rouge">example.com:8080</code> 来访问了。注意 <code class="language-plaintext highlighter-rouge">32400</code> 是 Plex 服务启动时绑定的端口，<code class="language-plaintext highlighter-rouge">localhost</code> 是 Plex 服务绑定的域。这里也没有指定远程的 <code class="language-plaintext highlighter-rouge">REMOTE</code>，意味着可以通过远程机器的所有 IP 访问。否则如果指定了 <code class="language-plaintext highlighter-rouge">localhost</code>（注意这是 <code class="language-plaintext highlighter-rouge">REMOTE</code> 的域），则只能在服务器上通过 <code class="language-plaintext highlighter-rouge">localhost</code> 来访问，不能通过服务器的外网 IP 来访问了：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ssh <span class="nt">-R</span> localhost:8080:localhost:32400 harttle@example.com
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">REMOTE</code> 在一台服务器上运行着多个域名时会比较有用，比如你可以分别绑定 <code class="language-plaintext highlighter-rouge">plex.example.com:8080</code> 到本地的 Plex 服务，同时绑定 <code class="language-plaintext highlighter-rouge">smb.example.com:8080</code> 到本地的 SMB 服务。</p>

<h2 id="动态转发--socks-服务">动态转发 / SOCKS 服务</h2>

<p>利用 SSH 端口转发可以实现一个 SOCKS 服务，例如当本地浏览 google.com:80 网页时，把这一对 <code class="language-plaintext highlighter-rouge">DESTINATION:DESTINATION_PORT</code> 发往本地的 <code class="language-plaintext highlighter-rouge">[LOCAL_IP:]LOCAL_PORT</code>。由 SSH 隧道转发到服务器端，从 SSH 服务器发起对 <code class="language-plaintext highlighter-rouge">DESTINATION:DESTINATION_PORT</code>（即 google.com:80）的请求，就实现了网络代理（注意不要用本手段科学上网，100% 概率会被封禁服务器 IP，谨慎尝试）。</p>

<p>由于不同网站的域名和端口（<code class="language-plaintext highlighter-rouge">DESTINATION:DESTINATION_PORT</code>）是不同的，因此不能像本地端口转发那样在写在命令参数里。这就需要“动态转发”，即 <code class="language-plaintext highlighter-rouge">-D</code> 参数：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ssh <span class="nt">-D</span> <span class="o">[</span>LOCAL_IP:]LOCAL_PORT <span class="o">[</span>USER@]SSH_SERVER
</code></pre></div></div>

<p>例如 <code class="language-plaintext highlighter-rouge">ssh -D localhost:8080 harttle@example.com</code> 即可在本地开启一个 SOCKS 协议的代理，代理地址即为 <code class="language-plaintext highlighter-rouge">localhost:8080</code>。</p>

<h2 id="ssh-配置">SSH 配置</h2>

<p>无论是本地端口转发还是远程端口转发，都需要在服务器上配置 <code class="language-plaintext highlighter-rouge">/etc/ssh/sshd_config</code>：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GatewayPorts yes
</code></pre></div></div>

<p>如果长时间保持连接，那么还需要开启：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>TCPKeepAlive yes
</code></pre></div></div>

<p>顾名思义 <code class="language-plaintext highlighter-rouge">TCPKeepAlive</code> 运行在 TCP 层，通过发一个空包来保持连接。如果你的服务器有复杂的防火墙，或者本地所在的网络运营商比较奇怪，这个包可能会被丢掉。这时可以用 <code class="language-plaintext highlighter-rouge">ServerAliveInterval 60</code> 来在 SSH 协议一层保持连接。方便起见这些参数也可以在建立连接时指定，比如：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ssh <span class="nt">-L</span> 8080:localhost:33062 harttle@mysql.example.com <span class="nt">-o</span> <span class="nv">TCPKeepAlive</span><span class="o">=</span><span class="nb">true </span><span class="nv">ServerAliveInterval</span><span class="o">=</span>60
</code></pre></div></div>

<p>也可以装一个 autossh 包，让它来托管 ssh 服务，这样会更稳定：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>autossh <span class="nt">-NR</span> 8080:localhost:32400 harttle@example.com
</code></pre></div></div>

<p>以上 ssh 命令都可以在 Linux 或 MacOS 下工作，如果在 Windows 下也有其他的选择。比如你安装了 WSL，那么可以在 WSL 里执行上述命令。也可以安装一个 SSH 客户端，比如在 PuTTY 下可以在“连接-&gt;SSH-&gt;隧道”里相应地设置本地和远程端口、IP。</p>

<p>关于开机自动建立隧道，在 Linux 下可以把上述命令直接写成一个 systemd 脚本，可参考 <a href="https://www.harttle.com/2016/08/04/systemd-nodejs-app.html">使用 systemd 管理 Node.js 应用</a> 一文；在 Windows 下可以利用任务计划程序建立一个任务，如果本地安装有 WSL，可以添加一个 Action 设置命令为 <code class="language-plaintext highlighter-rouge">wsl</code> 参数为 <code class="language-plaintext highlighter-rouge">autossh -NR 8080:$(cat /etc/resolv.conf | grep nameserver | awk '{print $2}'):32400 example.com</code>。注意 WSL2 启用了 HyperV 运行在某个子网下，宿主机器的 IP 是不确定的，需要用 <code class="language-plaintext highlighter-rouge">cat /etc/resolv.conf | grep nameserver | awk '{print $2}'</code> 来获得 WSL 宿主机器的 IP。</p>]]></content><author><name>Harttle</name></author><category term="SSH" /><category term="端口转发" /><category term="SOCKS" /><summary type="html"><![CDATA[SSH 隧道或 SSH 端口转发可以用来在客户端和服务器之间建立一个加密的 SSH 连接，通过它来把本地流量转发到服务器端，或者把服务器端流量转发到本地。比如从本地访问服务器上的 MySQL 管理后台，或者把本地的串流、SMB、CIFS 等服务暴露在服务器所在的公网。本文将介绍 SSH 隧道的本地端口转发、远程端口转发等使用方式，以及如何配置 SSH 允许长连接、开机时自动建立连接。]]></summary></entry><entry><title type="html">在 Vim 中复制、剪切和粘贴</title><link href="https://www.harttle.com/2022/03/19/vim-copy-paste.html" rel="alternate" type="text/html" title="在 Vim 中复制、剪切和粘贴" /><published>2022-03-19T00:00:00+00:00</published><updated>2022-03-19T00:00:00+00:00</updated><id>https://www.harttle.com/2022/03/19/vim-copy-paste</id><content type="html" xml:base="https://www.harttle.com/2022/03/19/vim-copy-paste.html"><![CDATA[<p>不同于 Emacs，Vim 确实是一个编辑器。那么编辑器能不能复制粘贴呢？肯定是可以的。不过要在 vim 中完成复制粘贴是需要敲键盘的，虽然略复杂但是功能也更强。</p>

<p>这篇文章来详细介绍 Vim 中复制粘贴的设置和使用方法。
包括复制当前字符/当前词/当前行，复制整个文件内容，粘贴刚才复制/剪切的内容；在插入模式/命令行模式粘贴；复制到 Windows/Mac 系统剪贴板。</p>

<!--more-->

<h2 id="复制粘贴">复制粘贴</h2>

<ol>
  <li>进入 normal/正常模式（刚进入 vim 的默认模式），如果你在 insert 模式，按下若干次 Esc 可以进入 normal 模式。</li>
  <li>把光标移动到开始复制的位置。</li>
  <li>按下 <code class="language-plaintext highlighter-rouge">v</code> 来选择字符。（也可以用 <code class="language-plaintext highlighter-rouge">V</code> 来选择整行，<code class="language-plaintext highlighter-rouge">Ctrl-v</code> 来选择矩形块）</li>
  <li><a href="https://www.harttle.com/2015/11/07/vim-cursor.html">光标移动</a> 到结束复制的位置。</li>
  <li>按下 <code class="language-plaintext highlighter-rouge">y</code> 来复制。</li>
  <li>光标移动到想要粘贴的位置，按下 <code class="language-plaintext highlighter-rouge">p</code> 粘贴。（或者 <code class="language-plaintext highlighter-rouge">P</code> 粘贴在当前光标位置之前）。</li>
</ol>

<p>把 <code class="language-plaintext highlighter-rouge">p</code> 换成 <code class="language-plaintext highlighter-rouge">gp</code> 可以在粘贴完成时，把光标移动到粘贴内容结束的位置。<code class="language-plaintext highlighter-rouge">gP</code> 同样适用。</p>

<h2 id="剪切粘贴">剪切粘贴</h2>

<ol>
  <li>进入 normal 模式（刚进入 vim 的默认模式），如果你在 insert 模式，按下若干次 Esc 可以进入 normal 模式。</li>
  <li>把 <a href="https://www.harttle.com/2015/11/07/vim-cursor.html">光标移动</a> 到开始复制的位置。</li>
  <li>按下 <code class="language-plaintext highlighter-rouge">v</code> 来选择字符。（也可以用 <code class="language-plaintext highlighter-rouge">V</code> 来选择整行，<code class="language-plaintext highlighter-rouge">Ctrl-v</code> 来选择矩形块）</li>
  <li>光标移动到结束复制的位置。</li>
  <li>按下 <code class="language-plaintext highlighter-rouge">d</code> 来剪切。</li>
  <li>光标移动到想要粘贴的位置，按下 <code class="language-plaintext highlighter-rouge">p</code> 粘贴。（或者 <code class="language-plaintext highlighter-rouge">P</code> 粘贴在当前光标位置之前）。</li>
</ol>

<h2 id="配合光标移动来复制">配合光标移动来复制</h2>

<p><code class="language-plaintext highlighter-rouge">y</code> 和 <code class="language-plaintext highlighter-rouge">d</code> 分别用于复制和剪切，但除了 <code class="language-plaintext highlighter-rouge">v</code> 还有很多更方便的选区方式。事实上所有光标移动命令都可以用来选区，比如：</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">yy</code> 或 <code class="language-plaintext highlighter-rouge">Y</code> 复制当前行。</li>
  <li><code class="language-plaintext highlighter-rouge">yw</code> 用来复制往后一个词，<code class="language-plaintext highlighter-rouge">y3w</code> 复制往后三个词。</li>
  <li><code class="language-plaintext highlighter-rouge">yiw</code> 复制当前词。</li>
  <li><code class="language-plaintext highlighter-rouge">y$</code> 复制到行尾，<code class="language-plaintext highlighter-rouge">y^</code> 复制到行首。</li>
  <li><code class="language-plaintext highlighter-rouge">yf.</code> 复制直到下一个 <code class="language-plaintext highlighter-rouge">.</code> 字符。</li>
  <li><code class="language-plaintext highlighter-rouge">ggyG</code> 或 <code class="language-plaintext highlighter-rouge">:%y</code> 复制整个文件。</li>
</ul>

<p>以上 <code class="language-plaintext highlighter-rouge">y</code> 改成 <code class="language-plaintext highlighter-rouge">d</code> 就可以用于剪切，更多光标移动方式可以参考 <a href="https://www.harttle.com/2015/11/07/vim-cursor.html">Vim 中如何快速移动光标？</a>。</p>

<h2 id="在-insert插入模式粘贴">在 insert/插入模式粘贴</h2>

<p>在 normal 模式按下 <code class="language-plaintext highlighter-rouge">i</code> 或者 <code class="language-plaintext highlighter-rouge">a</code> 可以进入插入模式，也就是键入内容的模式。<code class="language-plaintext highlighter-rouge">p</code> 快捷键不可用于插入模式，但是插入模式可以通过 <code class="language-plaintext highlighter-rouge">Ctrl+r</code> 来访问所有的寄存器，插入寄存器里的内容。所有剪切、拷贝、删除的内容都会存在不同的 Vim 寄存器里。比如：</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">Ctrl+r</code> <code class="language-plaintext highlighter-rouge">"</code> 插入最近一次复制/剪切/删除的内容。<code class="language-plaintext highlighter-rouge">"</code> 是 Vim 的匿名寄存器。</li>
  <li><code class="language-plaintext highlighter-rouge">Ctrl+r</code> <code class="language-plaintext highlighter-rouge">0</code> 插入最近一次复制的内容。其中 <code class="language-plaintext highlighter-rouge">0</code> 属于 Vim 的编号寄存器，保存最近一次拷贝的内容。</li>
</ul>

<p>此外寄存器还保存有当前文件名、最近一次执行的命令、最近一次搜索内容、最近一次插入文本等。可以参考 <a href="https://www.harttle.com/2016/07/25/vim-registers.html">Vim 寄存器完全手册</a>。</p>

<h2 id="在-command-line命令行模式粘贴">在 command-line/命令行模式粘贴</h2>

<p>在 normal 模式按下 <code class="language-plaintext highlighter-rouge">:</code> 可以进入命令行模式，用来执行比如切换文件，保存关闭等操作。这个模式下仍然可以使用 <code class="language-plaintext highlighter-rouge">Ctrl-r</code>，但还可以编辑每一条命令：</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">Ctrl+r</code> <code class="language-plaintext highlighter-rouge">"</code> 插入最近一次复制/剪切/删除的内容。<code class="language-plaintext highlighter-rouge">"</code> 是 Vim 的匿名寄存器。</li>
  <li><code class="language-plaintext highlighter-rouge">Ctrl+r</code> <code class="language-plaintext highlighter-rouge">0</code> 插入最近一次复制的内容。其中 <code class="language-plaintext highlighter-rouge">0</code> 属于 Vim 的编号寄存器，保存最近一次拷贝的内容。</li>
  <li><code class="language-plaintext highlighter-rouge">Ctrl+f</code> 选择一条历史命令（包括当前正在键入的命令）来编辑。然后就进入了 normal 模式，编辑完成后回车来执行命令。</li>
</ul>

<h2 id="复制多项内容复制历史">复制多项内容/复制历史</h2>

<p>复制内容是没有历史的，但删除历史保存在编号寄存器 1-9 中（以行为单位的删除或者超过一行的删除才会进入编号寄存器）：</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">"1</code> 保存上一次删除的内容。</li>
  <li><code class="language-plaintext highlighter-rouge">"2</code> 保存上上次删除的内容。</li>
  <li><code class="language-plaintext highlighter-rouge">"3</code> 保存上上上次删除的内容。</li>
  <li>以此类推……</li>
</ul>

<p>Vim 有 26 个命名寄存器（<code class="language-plaintext highlighter-rouge">"a</code>-<code class="language-plaintext highlighter-rouge">"z</code>），可以在删除或复制之前加寄存器名字，来保存在某个寄存器中，这样可以同时保存很多拷贝的内容。比如：</p>

<ul>
  <li>按下 <code class="language-plaintext highlighter-rouge">v</code> 选则需要复制的区域。</li>
  <li><code class="language-plaintext highlighter-rouge">"ay</code> 把选区复制到 <code class="language-plaintext highlighter-rouge">"a</code> 寄存器中。</li>
  <li><code class="language-plaintext highlighter-rouge">"ap</code> 把寄存器 <code class="language-plaintext highlighter-rouge">"a</code> 的内容粘贴出来。</li>
</ul>

<h2 id="复制到系统剪贴板从系统剪贴板粘贴">复制到系统剪贴板/从系统剪贴板粘贴</h2>

<p>寄存器 <code class="language-plaintext highlighter-rouge">"*</code> 和 <code class="language-plaintext highlighter-rouge">"+</code> 在 Mac 和 Windows 中，都是指系统剪贴板（clipboard），例如 <code class="language-plaintext highlighter-rouge">"*yy</code> 即可复制当前行到系统剪贴板。
其他程序中复制的内容也会被存储到这两个寄存器中。
在 X11 系统中（绝大多数带有桌面环境的 Linux 发行版），二者是有区别的：</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">"*</code> 指 X11 中的 PRIMARY 选区，即鼠标选中区域。在桌面系统中可按鼠标中键粘贴。</li>
  <li><code class="language-plaintext highlighter-rouge">"+</code> 指 X11 中的 CLIPBOARD 选区，即系统剪贴板。在桌面系统中可按 Ctrl+V 粘贴。</li>
</ul>

<p>通常 <code class="language-plaintext highlighter-rouge">set clipboard=unnamed</code> 即可和系统共享剪贴板，但也和你的具体环境有关，可以参考：</p>

<ul>
  <li><a href="https://www.harttle.com/2020/09/04/vim-clipboard.html">Vim 使用系统剪贴板</a></li>
  <li><a href="https://www.harttle.com/2017/06/23/vim-tmux-clipboard.html">Vim、Tmux、系统共用剪贴板</a></li>
</ul>]]></content><author><name>Harttle</name></author><category term="Vim" /><category term="剪贴板" /><category term="寄存器" /><category term="快捷键" /><summary type="html"><![CDATA[不同于 Emacs，Vim 确实是一个编辑器。那么编辑器能不能复制粘贴呢？肯定是可以的。不过要在 vim 中完成复制粘贴是需要敲键盘的，虽然略复杂但是功能也更强。 这篇文章来详细介绍 Vim 中复制粘贴的设置和使用方法。 包括复制当前字符/当前词/当前行，复制整个文件内容，粘贴刚才复制/剪切的内容；在插入模式/命令行模式粘贴；复制到 Windows/Mac 系统剪贴板。]]></summary></entry><entry><title type="html">iPhone 各机型的 iOS 和 Safari 版本</title><link href="https://www.harttle.com/2022/03/06/iPhone-iOS-versions.html" rel="alternate" type="text/html" title="iPhone 各机型的 iOS 和 Safari 版本" /><published>2022-03-06T00:00:00+00:00</published><updated>2022-03-06T00:00:00+00:00</updated><id>https://www.harttle.com/2022/03/06/iPhone-iOS-versions</id><content type="html" xml:base="https://www.harttle.com/2022/03/06/iPhone-iOS-versions.html"><![CDATA[<p>Web 开发时关心 iPhone 的兼容性，其实就是关心各 Safari 版本的市场占有率。而后者取决于 iPhone 各版本的市场占有率。比如某些 iPhone 已经绝版了，那么它对应的 Safari 版本也不再需要去支持了。此外更现实的问题是，老板说“我的 iPhone X 下这个页面挂了”，这时就需要猜他的 Safari 版本，再对应 <a href="https://caniuse.com/">caniuse.com</a> 就能知道哪些特性把页面搞挂了。iOS Safari 的发版有这么几个规律：</p>

<ol>
  <li><strong>iPhone 的 Safari 是随着 iOS 发版的</strong>。也就是说你的 iOS 版本直接决定了 Safari 版本，而想要更新 Safari 版本，就得更新 iOS 版本。</li>
  <li><strong>每个 iPhone 有支持的 iOS 范围</strong>。这个范围从它搭载的首个 iOS 版本直到它不能再支持的最高 iOS 版本。比如：
    <ul>
      <li>iPhone 12 发布时搭载的初始系统是 iOS 14.1，但可以升级到最新的 iOS 15.3.1，那么 iPhone 12 的 iOS 版本范围就是 [14.1, 15.3.1]。</li>
      <li>一些旧的 iPhone 无法支持 iOS 13，所以它们的最高 iOS 版本就锁定在了 12.5.5，它搭载的 Safari 12.1.2 就成了分水岭，类似 IE6 的角色。</li>
    </ul>
  </li>
  <li><strong>最近的 iOS 和 Safari 主版本是一致的</strong>。比如 Safari 13 随着 iOS 13 发布，Safari 14 随着 iOS 14 发布。</li>
</ol>

<!--more-->

<table class="full-width">
  <tbody><tr>
    <th>设备</th>
    <th>发布日期</th>
    <th>初始 iOS</th>
    <th>最高 iOS</th>
    <th>最低 Safari</th>
    <th>最高 Safari</th>
  </tr>
  <tr>
    <td>iPhone 13 Pro / 13 Pro Max</td>
    <td rowspan="2">2021</td>
    <td rowspan="2">15</td>
    <td rowspan="14">15 (latest)</td>
    <td rowspan="2">15.0</td>
    <td rowspan="14">15.3 (latest)</td>
  </tr>
  <tr>
    <td>iPhone 13 / 13 mini</td>
  </tr>
  <tr>
    <td>iPhone 12 Pro / 12 Pro Max</td>
    <td rowspan="3">2020</td>
    <td rowspan="2">14</td>
    <td rowspan="2">14.0</td>
  </tr>
  <tr>
    <td>iPhone 12 / 12 mini</td>
  </tr>
  <tr>
    <td>iPhone SE (gen 2)</td>
    <td rowspan="3">13</td>
    <td rowspan="3">13.0</td>
  </tr>
  <tr>
    <td>iPhone 11 Pro / 11 Pro Max</td>
    <td rowspan="2">2019</td>
  </tr>
  <tr>
    <td>iPhone 11</td>
  </tr>
  <tr>
    <td>iPhone XS / XS Max</td>
    <td rowspan="2">2018</td>
    <td rowspan="2">12</td>
    <td rowspan="2">12.0</td>
  </tr>
  <tr>
    <td>iPhone XR</td>
  </tr>
  <tr>
    <td>iPhone X</td>
    <td rowspan="2">2017</td>
    <td rowspan="2">11</td>
    <td rowspan="2">11.0</td>
  </tr>
  <tr>
    <td>iPhone 8 / 8 Plus</td>
  </tr>
  <tr>
    <td>iPhone 7 / 7 Plus</td>
    <td rowspan="2">2016</td>
    <td>10</td>
    <td>10.0</td>
  </tr>
  <tr>
    <td>iPhone SE (gen 1)</td>
    <td rowspan="2">9</td>
    <td rowspan="2">9.0</td>
  </tr>
  <tr>
    <td>iPhone 6s / 6s Plus</td>
    <td>2015</td>
  </tr>
  <tr>
    <td>iPhone 6 / 6 Plus</td>
    <td>2014</td>
    <td>8</td>
    <td rowspan="2">12</td>
    <td>8.0</td>
    <td rowspan="2">12.1.2</td>
  </tr>
  <tr>
    <td>iPhone 5s</td>
    <td rowspan="2">2013</td>
    <td rowspan="2">7</td>
    <td rowspan="2">7.0</td>
  </tr>
  <tr>
    <td>iPhone 5c</td>
    <td rowspan="2">10</td>
    <td rowspan="2">10.0</td>
  </tr>
  <tr>
    <td>iPhone 5</td>
    <td>2012</td>
    <td>6</td>
    <td>6.0</td>
  </tr>
  <tr>
    <td>iPhone 4s</td>
    <td>2011</td>
    <td>5</td>
    <td>9</td>
    <td>5.1</td>
    <td>9.0</td>
  </tr>
  <tr>
    <td>iPhone 4</td>
    <td>2010</td>
    <td>4</td>
    <td>7</td>
    <td>4.0.5</td>
    <td>7.0</td>
  </tr>
  <tr>
    <td>iPhone 3GS</td>
    <td>2009</td>
    <td>3</td>
    <td>6</td>
    <td>4.0</td>
    <td>6.0</td>
  </tr>
  <tr>
    <td>iPhone 3G</td>
    <td>2008</td>
    <td>2</td>
    <td>4</td>
    <td>3.1.1</td>
    <td>5.0.2</td>
  </tr>
  <tr>
    <td>iPhone (gen 1)</td>
    <td>2007</td>
    <td>1</td>
    <td>3</td>
    <td>3.0</td>
    <td>4.0</td>
  </tr>
</tbody></table>

<p>更多链接</p>

<ul>
  <li>Safari Release Notes: <a href="https://developer.apple.com/documentation/safari-release-notes">https://developer.apple.com/documentation/safari-release-notes</a></li>
  <li>Safari version history: <a href="https://en.wikipedia.org/wiki/Safari_version_history#iOS">https://en.wikipedia.org/wiki/Safari_version_history#iOS</a></li>
  <li>iOS version by device: <a href="https://iosref.com/ios#iphone">https://iosref.com/ios#iphone</a></li>
</ul>]]></content><author><name>Harttle</name></author><category term="iOS" /><category term="版本" /><category term="iPhone" /><category term="Safari" /><summary type="html"><![CDATA[Web 开发时关心 iPhone 的兼容性，其实就是关心各 Safari 版本的市场占有率。而后者取决于 iPhone 各版本的市场占有率。比如某些 iPhone 已经绝版了，那么它对应的 Safari 版本也不再需要去支持了。此外更现实的问题是，老板说“我的 iPhone X 下这个页面挂了”，这时就需要猜他的 Safari 版本，再对应 caniuse.com 就能知道哪些特性把页面搞挂了。iOS Safari 的发版有这么几个规律： iPhone 的 Safari 是随着 iOS 发版的。也就是说你的 iOS 版本直接决定了 Safari 版本，而想要更新 Safari 版本，就得更新 iOS 版本。 每个 iPhone 有支持的 iOS 范围。这个范围从它搭载的首个 iOS 版本直到它不能再支持的最高 iOS 版本。比如： iPhone 12 发布时搭载的初始系统是 iOS 14.1，但可以升级到最新的 iOS 15.3.1，那么 iPhone 12 的 iOS 版本范围就是 [14.1, 15.3.1]。 一些旧的 iPhone 无法支持 iOS 13，所以它们的最高 iOS 版本就锁定在了 12.5.5，它搭载的 Safari 12.1.2 就成了分水岭，类似 IE6 的角色。 最近的 iOS 和 Safari 主版本是一致的。比如 Safari 13 随着 iOS 13 发布，Safari 14 随着 iOS 14 发布。]]></summary></entry><entry><title type="html">JavaScript 字符串转数字</title><link href="https://www.harttle.com/2020/11/22/javascript-string-to-number.html" rel="alternate" type="text/html" title="JavaScript 字符串转数字" /><published>2020-11-22T00:00:00+00:00</published><updated>2020-11-22T00:00:00+00:00</updated><id>https://www.harttle.com/2020/11/22/javascript-string-to-number</id><content type="html" xml:base="https://www.harttle.com/2020/11/22/javascript-string-to-number.html"><![CDATA[<p>我们知道 JavaScript 中字符串转为数字有 <code class="language-plaintext highlighter-rouge">parseInt</code>, <code class="language-plaintext highlighter-rouge">Number()</code>, <code class="language-plaintext highlighter-rouge">+</code> 等方式，但它们的转换规则很不一样适用范围也不同。比如 <code class="language-plaintext highlighter-rouge">parseInt</code> 可以解析数字字母的混合字符串而 <code class="language-plaintext highlighter-rouge">Number</code> 和 <code class="language-plaintext highlighter-rouge">+</code> 会直接产生 <code class="language-plaintext highlighter-rouge">NaN</code>，<code class="language-plaintext highlighter-rouge">Number</code> 和 <code class="language-plaintext highlighter-rouge">parseInt</code> 可以操作 <code class="language-plaintext highlighter-rouge">BigInt</code> 而 <code class="language-plaintext highlighter-rouge">+</code> 则会抛出 <code class="language-plaintext highlighter-rouge">TypeError</code>。<strong>TL;DR</strong> 三种字符串先给出对比表格如下：</p>

<table>
  <thead>
    <tr>
      <th>value</th>
      <th>parseInt(value)</th>
      <th>Number(value)</th>
      <th>+value</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>"3.14"/3.14</td>
      <td>3</td>
      <td>3.14</td>
      <td>3.14</td>
    </tr>
    <tr>
      <td>undefined</td>
      <td>NaN</td>
      <td>NaN</td>
      <td>NaN</td>
    </tr>
    <tr>
      <td>null</td>
      <td>NaN</td>
      <td>0</td>
      <td>0</td>
    </tr>
    <tr>
      <td>false</td>
      <td>NaN</td>
      <td>0</td>
      <td>0</td>
    </tr>
    <tr>
      <td>true</td>
      <td>NaN</td>
      <td>1</td>
      <td>1</td>
    </tr>
    <tr>
      <td>"Infinity"/Infinity</td>
      <td>NaN</td>
      <td>Infinity</td>
      <td>Infinity</td>
    </tr>
    <tr>
      <td>"1e3"</td>
      <td>1</td>
      <td>1000</td>
      <td>1000</td>
    </tr>
    <tr>
      <td>"123z"</td>
      <td>123</td>
      <td>NaN</td>
      <td>NaN</td>
    </tr>
    <tr>
      <td>"z123"</td>
      <td>NaN</td>
      <td>NaN</td>
      <td>NaN</td>
    </tr>
    <tr>
      <td>10n</td>
      <td>10</td>
      <td>10</td>
      <td>TypeError</td>
    </tr>
    <tr>
      <td>"0x10"</td>
      <td>16</td>
      <td>16</td>
      <td>16</td>
    </tr>
    <tr>
      <td>"010"</td>
      <td>10</td>
      <td>10</td>
      <td>10*</td>
    </tr>
  </tbody>
</table>

<ul>
  <li>parseInt 在某些浏览器下解释为 8 进制。</li>
</ul>

<!--more-->

<h2 id="parseint">parseInt</h2>

<p><a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/parseInt">parseInt</a> 是从 ES1 开始就有的内置函数。第一个参数是数字字符串，第二个参数是进制，且只支持 2-36。对于 <code class="language-plaintext highlighter-rouge">parseInt</code> 的几点观察：</p>

<ul>
  <li>10 不是默认值。例如 <code class="language-plaintext highlighter-rouge">parseInt('0x10')</code> 为 16，此时 parseInt 采取了 16 进制。</li>
  <li>不是所有数字字符串都能 parseInt。parseInt 只认识 +/- 两种符号，因此 <code class="language-plaintext highlighter-rouge">parseInt((1e80).toString())</code> 会从 <code class="language-plaintext highlighter-rouge">e</code> 截断结果为 1。如果要产生 parseInt 能识别的字符串，需要 <code class="language-plaintext highlighter-rouge">BigInt(1e80).toString()</code>。</li>
  <li>不是所有非数字都会返回 <code class="language-plaintext highlighter-rouge">NaN</code>。第一个字符不是数字时才返回 <code class="language-plaintext highlighter-rouge">NaN</code>，例如 <code class="language-plaintext highlighter-rouge">parseInt('e3')</code> 为 <code class="language-plaintext highlighter-rouge">NaN</code>，但 <code class="language-plaintext highlighter-rouge">parseInt('3e')</code> 为 3。</li>
</ul>

<p>因此使用 <code class="language-plaintext highlighter-rouge">parseInt</code> 时建议遵守以下规则：</p>

<ol>
  <li>使用 parseInt 时应该指定进制。例如 <code class="language-plaintext highlighter-rouge">parseInt('010')</code> 在有的浏览器中是 8 有的是 10，后来 ES5 规定了此时用 10。</li>
  <li>推论：不能直接用于 <code class="language-plaintext highlighter-rouge">Array.prototype.map</code>，例如：</li>
</ol>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="dl">'</span><span class="s1">10</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">10</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">10</span><span class="dl">'</span><span class="p">].</span><span class="nx">map</span><span class="p">(</span><span class="nb">parseInt</span><span class="p">)</span>
<span class="c1">// 返回 [10, NaN, 2]，因为进制参数分别为：0，1，2</span>
</code></pre></div></div>

<h2 id="number">Number</h2>

<p><a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Number">Number</a> 是 "number" 基本类型的对象封装，不加 new 关键字时只做转换可以仍然返回基本类型。例如：</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">new</span> <span class="nb">Number</span><span class="p">(</span><span class="dl">'</span><span class="s1">1</span><span class="dl">'</span><span class="p">)</span> <span class="o">===</span> <span class="mi">1</span>   <span class="c1">// false</span>
<span class="nb">Number</span><span class="p">(</span><span class="dl">'</span><span class="s1">1</span><span class="dl">'</span><span class="p">)</span> <span class="o">===</span> <span class="mi">1</span>       <span class="c1">// true</span>
</code></pre></div></div>

<p>用作类型转换时，<code class="language-plaintext highlighter-rouge">Number</code> 可以支持数字的所有字面表示比如 <code class="language-plaintext highlighter-rouge">Number('1e3')</code>, <code class="language-plaintext highlighter-rouge">Number('Infinity')</code>，<code class="language-plaintext highlighter-rouge">Number('1.3')</code>，但是不能自定义进制。此外不同于 <code class="language-plaintext highlighter-rouge">parseInt</code> 的是，<code class="language-plaintext highlighter-rouge">Number</code> 对任何不合法的数字表示都会返回 <code class="language-plaintext highlighter-rouge">NaN</code>。</p>

<h2 id="-运算符">+ 运算符</h2>

<p>像多数语言一样 JavaScript 定义了 <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Unary_plus">单目运算符 +</a>，它的操作数可以是数字，也可以是其他类型。是其他类型时会被转换为数字。用于 BigInt 时会抛出 <code class="language-plaintext highlighter-rouge">TypeError</code>。</p>

<p><code class="language-plaintext highlighter-rouge">+val</code> 是 JavaScript 中最常见的、最快的转换其他值到数字的方式，它不仅支持浮点数表示，还支持 8 进制和 16 进制，也能转换 <code class="language-plaintext highlighter-rouge">null</code>, <code class="language-plaintext highlighter-rouge">false</code>, <code class="language-plaintext highlighter-rouge">true</code>。转换失败时值为 <code class="language-plaintext highlighter-rouge">NaN</code>。和 <code class="language-plaintext highlighter-rouge">Number</code> 一样不会尝试转换字符串前缀为数字，只要整个字符串是非法的值就是 <code class="language-plaintext highlighter-rouge">NaN</code>。</p>

<p>需要注意的是 <code class="language-plaintext highlighter-rouge">num + str</code> 中的 <code class="language-plaintext highlighter-rouge">+</code> 是双目运算符 +，它不会把 <code class="language-plaintext highlighter-rouge">str</code> 转换为数字。如果这里要使用单目运算符 +，需要写成 <code class="language-plaintext highlighter-rouge">num + +str</code>。注意两个 <code class="language-plaintext highlighter-rouge">+</code> 要有空格，否则会解释为自增运算符进而抛出 <code class="language-plaintext highlighter-rouge">SyntaxError</code>。
但它的操作数可以是表达式，因此 <code class="language-plaintext highlighter-rouge">num + + + + + str</code> 也是合法的，也会把 <code class="language-plaintext highlighter-rouge">str</code> 转为数字。例如：</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">let</span> <span class="nx">num</span> <span class="o">=</span> <span class="mi">3</span><span class="p">,</span> <span class="nx">str</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">3</span><span class="dl">'</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">num</span> <span class="o">+</span> <span class="nx">str</span><span class="p">)</span>              <span class="c1">// '33'</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">num</span> <span class="o">+</span> <span class="o">+</span> <span class="nx">str</span><span class="p">)</span>            <span class="c1">// 6</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">num</span> <span class="o">+</span> <span class="o">+</span> <span class="o">+</span> <span class="o">+</span> <span class="o">+</span> <span class="o">+</span> <span class="nx">str</span><span class="p">)</span>    <span class="c1">// 6</span>
</code></pre></div></div>]]></content><author><name>Harttle</name></author><category term="JavaScript" /><category term="字符串" /><category term="Number" /><category term="类型转换" /><summary type="html"><![CDATA[我们知道 JavaScript 中字符串转为数字有 parseInt, Number(), + 等方式，但它们的转换规则很不一样适用范围也不同。比如 parseInt 可以解析数字字母的混合字符串而 Number 和 + 会直接产生 NaN，Number 和 parseInt 可以操作 BigInt 而 + 则会抛出 TypeError。TL;DR 三种字符串先给出对比表格如下： value parseInt(value) Number(value) +value "3.14"/3.14 3 3.14 3.14 undefined NaN NaN NaN null NaN 0 0 false NaN 0 0 true NaN 1 1 "Infinity"/Infinity NaN Infinity Infinity "1e3" 1 1000 1000 "123z" 123 NaN NaN "z123" NaN NaN NaN 10n 10 10 TypeError "0x10" 16 16 16 "010" 10 10 10* parseInt 在某些浏览器下解释为 8 进制。]]></summary></entry><entry><title type="html">Vim 使用系统剪贴板</title><link href="https://www.harttle.com/2020/09/04/vim-clipboard.html" rel="alternate" type="text/html" title="Vim 使用系统剪贴板" /><published>2020-09-04T00:00:00+00:00</published><updated>2020-09-04T00:00:00+00:00</updated><id>https://www.harttle.com/2020/09/04/vim-clipboard</id><content type="html" xml:base="https://www.harttle.com/2020/09/04/vim-clipboard.html"><![CDATA[<p>Vim 是运行在 Terminal 里的 Shell 程序，所以要把内容拷贝出来可以通过 Terminal，也可以通过 Vim 自己。不配置 Vim 剪贴板时只能通过 Shell 来拷贝粘贴：</p>

<ul>
  <li>通过 Shell 拷贝（比如在 iTerm 里按住 Alt 选取内容）会有问题：比如会包含 Vim 左侧行号、折行变成了换行。</li>
  <li>通过 Shell 粘贴有类似的问题：相比于 <code class="language-plaintext highlighter-rouge">p</code> 命令，在插入模式下 Ctrl+V 时 Vim 会把内容当作字符键入，触发 Vim 的所有处理键入的插件，比如自动补全、语法检查等。不仅会很慢，而且可能会破坏你的内容（比如粘贴一对括号，可能 Vim 会再帮你自动补全一个多余的右括号）。</li>
</ul>

<p>所以完美的拷贝粘贴一定要通过 Vim 本身。Vim 中 <code class="language-plaintext highlighter-rouge">p</code>（paste）、<code class="language-plaintext highlighter-rouge">d</code>（delete）、<code class="language-plaintext highlighter-rouge">y</code>（yank）等拷贝粘贴操作使用的是 <strong>匿名寄存器</strong> <code class="language-plaintext highlighter-rouge">""</code>（unnamed register），本文就来解释怎么在 Mac/Windows/Linux 上把 Vim 的 <strong>匿名寄存器</strong> 映射到操作系统的剪贴板。</p>

<p><strong>TL; DR</strong></p>

<ol>
  <li>确保你的 vim 支持剪贴板，通过 <code class="language-plaintext highlighter-rouge">vim --version | grep clipboard</code> 检查。</li>
  <li>确定你的剪贴板寄存器是 <code class="language-plaintext highlighter-rouge">"+</code>（XA_SECONDARY）还是 <code class="language-plaintext highlighter-rouge">"*</code>（XA_PRIMARY）。</li>
  <li>同步剪贴板和匿名寄存器，在 <code class="language-plaintext highlighter-rouge">~/.vimrc</code> 添加配置比如 <code class="language-plaintext highlighter-rouge">set clipboard=unnamed</code>。</li>
</ol>

<!--more-->

<h2 id="确保你的-vim-支持剪贴板">确保你的 Vim 支持剪贴板</h2>

<p>你的 Vim Build 没有支持 clipboard，那么无论怎样配置都不会生效。
可以用如下命令检查：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>vim <span class="nt">--version</span> | <span class="nb">grep </span>clipboard
</code></pre></div></div>

<p>如果输出包含 <code class="language-plaintext highlighter-rouge">+clipboard</code> 或 <code class="language-plaintext highlighter-rouge">+xterm_clipboard</code> 就支持，如果这两项都是 <code class="language-plaintext highlighter-rouge">-</code> 则不支持。例如我的 Vim 输出为（MacOS 上的 macvim）：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>+clipboard         +keymap            +printer           +vertsplit
+emacs_tags        -mouse_gpm         -sun_workshop      -xterm_clipboard
</code></pre></div></div>

<p>如果你的 Vim 不支持剪贴板，则需要重新安装一个带 clipboard 的 Vim：</p>

<ul>
  <li>MacOS 下可以直接用 <a href="https://brew.sh/">brew</a> 安装 macvim，它是支持剪贴板的。</li>
  <li>Linux 下，如果是 Debian 或 Ubuntu 可以安装 vim-gtk、vim-gnome，Redhat/CentOS 则可以安装 vim-X11。</li>
  <li>Windows 下比较复杂，可以参考 <a href="https://vim.fandom.com/wiki/Using_the_Windows_clipboard_in_Cygwin_Vim">https://vim.fandom.com/wiki/Using_the_Windows_clipboard_in_Cygwin_Vim</a>。</li>
</ul>

<p>重新安装后再执行 <code class="language-plaintext highlighter-rouge">vim --version</code> 来查看 clipboard 是否支持。注意：如果安装到了其他路径你需要改 PATH 或重启 Terminal。</p>

<h2 id="确定你的剪贴板寄存器">确定你的剪贴板寄存器</h2>

<p>Vim 有 48 个寄存器，<code class="language-plaintext highlighter-rouge">y</code>, <code class="language-plaintext highlighter-rouge">d</code>, <code class="language-plaintext highlighter-rouge">p</code> 等命令一般使用匿名寄存器 <code class="language-plaintext highlighter-rouge">""</code>，
支持剪贴板的 Vim 会支持额外的选区寄存器 <code class="language-plaintext highlighter-rouge">"*</code> 和 <code class="language-plaintext highlighter-rouge">"+</code>。
更多 Vim 寄存器的信息，可以参考这篇文章：<a href="https://www.harttle.com/2016/07/25/vim-registers.html">Vim 寄存器完全手册</a>。</p>

<p><code class="language-plaintext highlighter-rouge">"*</code> 和 <code class="language-plaintext highlighter-rouge">"+</code> 在 Mac 和 Windows 中，都是指系统剪贴板（clipboard），例如 <code class="language-plaintext highlighter-rouge">"*yy</code> 即可复制当前行到剪贴板。
其他程序中复制的内容也会被存储到这两个寄存器中。
在 X11 系统中（绝大多数带有桌面环境的 Linux 发行版），二者是有区别的：</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">"*</code> 指 X11 中的 PRIMARY 选区，即鼠标选中区域。在桌面系统中可按鼠标中键粘贴。</li>
  <li><code class="language-plaintext highlighter-rouge">"+</code> 指 X11 中的 CLIPBOARD 选区，即系统剪贴板。在桌面系统中可按 Ctrl+V 粘贴。</li>
</ul>

<p>上述哪个寄存器对应于你的剪贴板和 Linux 发行版有关，在配置 Vim 前可以测试一下。
比如用 Vim 打开一个文件，在 normal 模式下（进入 Vim 后默认的模式）键入 <code class="language-plaintext highlighter-rouge">gg"*yG</code>，
来把当前文件内容拷贝到 <code class="language-plaintext highlighter-rouge">"*</code> 寄存器。键入 <code class="language-plaintext highlighter-rouge">gg"+yG</code> 拷贝到 <code class="language-plaintext highlighter-rouge">"+</code> 寄存器。</p>

<p>到目前为止，你已经可以通过命令来拷贝粘贴内容了。接下来我们希望通过 Vim 配置，
让匿名寄存器和系统剪贴板同步。</p>

<h2 id="同步剪贴板和匿名寄存器">同步剪贴板和匿名寄存器</h2>

<p>以下配置可以让主选区寄存器 <code class="language-plaintext highlighter-rouge">"*</code> 和匿名寄存器 <code class="language-plaintext highlighter-rouge">""</code> 保持同步（即共享剪贴板），
一般适用于 Windows 和 MacOS，Linux 下的表现是共享 X11 剪贴板、PRIMARY 选区（鼠标中键粘贴）。</p>

<div class="language-vim highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">set</span> <span class="nb">clipboard</span><span class="p">=</span>unnamed
</code></pre></div></div>

<p>Vim 7.3.74 及以上支持了 unnamedplus：</p>

<div class="language-vim highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">set</span> <span class="nb">clipboard</span><span class="p">=</span>unnamedplus
</code></pre></div></div>

<p>即让剪贴板寄存器 <code class="language-plaintext highlighter-rouge">"+</code> 和匿名寄存器 <code class="language-plaintext highlighter-rouge">""</code> 保持同步，
Linux 下一般对应于桌面系统的剪贴板，比如 GNOME 的系统剪贴板、以及 SECONDARY 选区（Ctrl+V 粘贴）。</p>

<h2 id="不支持-clipboard-的情况">不支持 clipboard 的情况</h2>

<p>如果你的 Vim 不支持 clipboard 且没法升级或其他 clipboard 选项不好使的情况，
可以调用外部命令来实现拷贝粘贴，在 Vim 里直接调用，或设置快捷键调用。
比如 <a href="/2017/06/23/vim-tmux-clipboard.html">让 Tmux 远程 Vim 使用本地系统的剪贴板</a>。</p>

<p>如果在 MacOS 下，可以用 pbcopy/pbpaste 命令来实现。</p>

<ul>
  <li>拷贝一段文本：先按 v 进入 visual 模式选中后执行 <code class="language-plaintext highlighter-rouge">:w !pbcopy</code>。拷贝整个文件可以 <code class="language-plaintext highlighter-rouge">:%w !pbcopy</code></li>
  <li>粘贴一段文本：把光标移动到要插入的行，执行 <code class="language-plaintext highlighter-rouge">:r !pbpaste</code></li>
</ul>

<p>如果在 Linux 下，可以借由 xclip 来实现。用 <code class="language-plaintext highlighter-rouge">xclip -i -sel c</code> 代替上面的 <code class="language-plaintext highlighter-rouge">pbcopy</code>，用 <code class="language-plaintext highlighter-rouge">xclip -o -sel -c</code> 代替上面的 <code class="language-plaintext highlighter-rouge">pbpaste</code>。</p>]]></content><author><name>Harttle</name></author><category term="Vim" /><category term="剪贴板" /><category term="寄存器" /><summary type="html"><![CDATA[Vim 是运行在 Terminal 里的 Shell 程序，所以要把内容拷贝出来可以通过 Terminal，也可以通过 Vim 自己。不配置 Vim 剪贴板时只能通过 Shell 来拷贝粘贴： 通过 Shell 拷贝（比如在 iTerm 里按住 Alt 选取内容）会有问题：比如会包含 Vim 左侧行号、折行变成了换行。 通过 Shell 粘贴有类似的问题：相比于 p 命令，在插入模式下 Ctrl+V 时 Vim 会把内容当作字符键入，触发 Vim 的所有处理键入的插件，比如自动补全、语法检查等。不仅会很慢，而且可能会破坏你的内容（比如粘贴一对括号，可能 Vim 会再帮你自动补全一个多余的右括号）。 所以完美的拷贝粘贴一定要通过 Vim 本身。Vim 中 p（paste）、d（delete）、y（yank）等拷贝粘贴操作使用的是 匿名寄存器 ""（unnamed register），本文就来解释怎么在 Mac/Windows/Linux 上把 Vim 的 匿名寄存器 映射到操作系统的剪贴板。 TL; DR 确保你的 vim 支持剪贴板，通过 vim --version | grep clipboard 检查。 确定你的剪贴板寄存器是 "+（XA_SECONDARY）还是 "*（XA_PRIMARY）。 同步剪贴板和匿名寄存器，在 ~/.vimrc 添加配置比如 set clipboard=unnamed。]]></summary></entry><entry><title type="html">4K 设备配置指南</title><link href="https://www.harttle.com/2020/07/04/4k-device.html" rel="alternate" type="text/html" title="4K 设备配置指南" /><published>2020-07-04T00:00:00+00:00</published><updated>2020-07-04T00:00:00+00:00</updated><id>https://www.harttle.com/2020/07/04/4k-device</id><content type="html" xml:base="https://www.harttle.com/2020/07/04/4k-device.html"><![CDATA[<p>只要 4K 显示器就能拥有 4K 体验吗？这篇文章写在 <a href="https://en.wikipedia.org/wiki/4K_resolution">4K 出现</a> 六年之后的今天，再来重新审视这个问题，以及如何正确地配备显示器、片源、网络和视频线缆。</p>

<!--more-->

<h2 id="4k-的分辨率是多少">4K 的分辨率是多少</h2>

<p>横向在 4000 像素左右的分辨率都叫 4K。4K 显示器最常见的标准是 3840x2160（也叫 2160p），电影业则更常用 4096x2160 标准。我们讨论显示器和电视，也就是说我们讨论的 4K 是 3840x2160，不到这个分辨率的都是假 4K。</p>

<p><strong>总之本文范围内，4K、3840x2160、2160p 是同义词。</strong></p>

<h2 id="4k-需要怎样的电视">4K 需要怎样的电视？</h2>

<p>再说分辨率。视觉残留是 1/24s，因此所有主流显示器的刷新率都在 24Hz 以上。而显示器的刷新率一般要比电视高，用来游戏的 2K 显示器已经达到了 144Hz，4K 显示器则基本还在 60Hz（120 Hz 目前还仅限高端显示器），市面上普通 4K 电视则基本都是 30Hz，只有部分高端电视可以到 60Hz。此外 120Hz 4K 也会超过 HDMI2.0 的带宽（线缆相关见下文）。</p>

<p>除了 4K 之外，HDR 技术可能会显著地影响你的观看体验。
这部分直接按照你的预算来，比如一两千就红米，别考虑 HDR 的事情，五千以上就索尼、三星等有 HDR 的，能够接受广告的可以考虑三四千的小米，也能达到类似效果。</p>

<p><strong>总之普通人用的 4K 显示器基本是 60Hz 的，4K 电视则是 30Hz 的。</strong></p>

<h2 id="4k-需要怎样的视频线">4K 需要怎样的视频线？</h2>

<p>不同的线缆区别主要是带宽，帧率和分辨率和需要的带宽成正比。考虑普通人的情况，4K 显示器分辨率是 3840x2160，24 色，帧率按照 60Hz。那么需要的传输速率为 3840x2160x24x60 = 12Gbps。也就是说 3840x2160 60Hz 要 12Gbps，那么 3840x2160 120Hz 就要 24Gbps。</p>

<p>目前市面上常见的视频线包括 HDMI1.4、HDMI2.x，DP 线，和旧的 VGA、DVI。
其中 HDMI1.4 只支持 30HZ 的 4K，HDMI2.0 开始支持 60Hz 4K；市面上能看到的 DP 则基本都可以支持 60Hz 4K。VGA、DVI 等旧的标准则根本没法支持 4K。见下表：</p>

<table>
  <thead>
    <tr>
      <th>视频线</th>
      <th>速率</th>
      <th>支持分辨率、帧率</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>HDMI1.4</td>
      <td>10.2Gbps</td>
      <td>3840x2160 30Hz</td>
    </tr>
    <tr>
      <td>HDMI2.0</td>
      <td>18Gbps</td>
      <td>3840x2160 60Hz</td>
    </tr>
    <tr>
      <td>HDMI2.1</td>
      <td>48Gbps</td>
      <td>3840x2160 120Hz，7680x4320 60Hz</td>
    </tr>
    <tr>
      <td>DP1.2</td>
      <td>21.6Gbps</td>
      <td>4096x2160 60Hz</td>
    </tr>
    <tr>
      <td>DP1.4</td>
      <td>25.92Gbps</td>
      <td>3840x2160 120Hz，7680x4320 60Hz</td>
    </tr>
    <tr>
      <td>VGA</td>
      <td>388MHz</td>
      <td>2048x1536 85Hz</td>
    </tr>
    <tr>
      <td>DVI</td>
      <td>165MHz+Dual-link</td>
      <td>2560x1600 60Hz, 1920x1080 120Hz</td>
    </tr>
  </tbody>
</table>

<p>需要注意的是 1.4 和 2.0 是同样的接口，区别在于线径和屏蔽工艺，所以 HDMI2.0 的线如果长度大于 10m，也就变成了 HDMI1.4，这种情况下需要做工扎实的线材才能保证 HDMI2.0 的效果。否则就只能 30Hz 帧率了。</p>

<p><strong>总之，普通人买 HDMI2.0 或 DP 线就足够了，但是 120Hz 4K 显示器的土豪则注意要买 HDMI2.1 以上。</strong></p>

<h2 id="4k-需要怎样的网线">4K 需要怎样的网线？</h2>

<p>上面我们知道 3840x2160 60Hz 需要 12Gbps 的速率，因为视频网站都有视频编码，考虑 H265 300-1000 的压缩率，那么网络速率就需要 10-50Mbps。
下表是常见的网络设备支持的最大速率：</p>

<table>
  <thead>
    <tr>
      <th>网络</th>
      <th>速率</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>以太网 CAT5</td>
      <td>100Mbps</td>
    </tr>
    <tr>
      <td>以太网 CAT5e</td>
      <td>1000Mbps</td>
    </tr>
    <tr>
      <td>以太网 CAT6</td>
      <td>10Gbps</td>
    </tr>
    <tr>
      <td>WIFI 802.11g</td>
      <td>54Mbps</td>
    </tr>
    <tr>
      <td>WIFI 802.11a</td>
      <td>54Mbps</td>
    </tr>
    <tr>
      <td>WIFI 802.11b</td>
      <td>11Mbps</td>
    </tr>
    <tr>
      <td>WIFI 802.11</td>
      <td>2Mbps</td>
    </tr>
    <tr>
      <td>4G</td>
      <td>100Mbps 下行，50Mbps 上行</td>
    </tr>
  </tbody>
</table>

<p><strong>总之看 4K 网剧只需要百兆网</strong>。也就是说如果你的电脑或电视：</p>

<ul>
  <li>插着网线。最普通的 5 类线就能满足需求（线上会有黑色点划线写着 CAT5，带宽上线是 100Mbps）；</li>
  <li>连着 Wifi。需要不是特别旧的路由器和电脑，即支持 802.11g 或 802.11a，有些老旧无线路由可能不适用；</li>
  <li>连着 4G。4G 的下行速率理论上相当于百兆网，但国内情况可能只有 20Mbps。4K + 60Hz 的视频可能会卡，但考虑到并不是所有片源都支持 60Hz，所以大概可以凑合看。</li>
</ul>

<h2 id="4k-需要怎样的片源">4K 需要怎样的片源？</h2>

<p>这是另一个现实的问题，为什么体验店里的电视都色彩斑斓而且异常地清晰？
一方面商家会选择能完全发挥显示器色彩的片段，另一方面也会选择能够匹配显示器分辨率的高清片源，即正要讨论的 4K 电影。</p>

<p>4K 电影是指每一帧分辨率横线在 4000 像素左右的，常见的有 4096×2160 和 2048×1080 分辨率。另外一个叫法是 Ultra HD 或 2160p，下表归纳了画质、分辨率的各种叫法：</p>

<table>
  <thead>
    <tr>
      <th>影片类型</th>
      <th>分辨率</th>
      <th>画质</th>
      <th>其他叫法</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>4K</td>
      <td>2160p</td>
      <td>Ultra HD</td>
      <td>超高清</td>
    </tr>
    <tr>
      <td>2K</td>
      <td>1080p</td>
      <td>Full HD</td>
      <td>超清</td>
    </tr>
    <tr>
      <td>-</td>
      <td>720p</td>
      <td>HD (High Definition)</td>
      <td>高清</td>
    </tr>
  </tbody>
</table>

<p>Ultra HD 画质的像素点是 720p 的 9 倍。因此 4K 电影的大小也相当感人，无论制作还是播放成本都很高。至少现在国内还不是很容易看到 4K 电影，通常有两种办法：</p>

<ul>
  <li>
    <p>第一个选项是在线观看，你的网速基本是合格的（见上文）但需要视频网站支持。目前 YouTube 已经支持 8K，但需要你的代理也能满足要求速率。可能因为带宽费用和片源的问题，国内视频网站还很少有 4K。</p>
  </li>
  <li>
    <p>另一个选项就是下载。既然已经知道 2160p、Ultra HD 就是 4K，剩下的事情就是找片源了。但是因为 4K 电影动辄 10G-20G，你还需要大硬盘和足够的时间。找片源时注意不要把 4K 和蓝光混淆，4K 指的是画面的分辨率，而蓝光是一种（25G、50G 容量的）光碟格式。只是蓝光格式通常来存储高清电影，可能是 4K 的，也可能是 1080p 的、720p 的。</p>
  </li>
</ul>]]></content><author><name>Harttle</name></author><category term="网络" /><category term="视频" /><category term="HDMI" /><summary type="html"><![CDATA[只要 4K 显示器就能拥有 4K 体验吗？这篇文章写在 4K 出现 六年之后的今天，再来重新审视这个问题，以及如何正确地配备显示器、片源、网络和视频线缆。]]></summary></entry><entry><title type="html">Bash 转义和引号的使用</title><link href="https://www.harttle.com/2020/06/26/bash-quote-escape.html" rel="alternate" type="text/html" title="Bash 转义和引号的使用" /><published>2020-06-26T00:00:00+00:00</published><updated>2020-06-26T00:00:00+00:00</updated><id>https://www.harttle.com/2020/06/26/bash-quote-escape</id><content type="html" xml:base="https://www.harttle.com/2020/06/26/bash-quote-escape.html"><![CDATA[<p>字面量、转义和引号是任何编程语言的基础，但却少有人认真地学习 Bash 中的转义和 <a href="https://www.gnu.org/software/bash/manual/html_node/Quoting.html#Quoting">引号</a>。
好消息是 Bash 引号的语义非常简单，这篇文章就可以完全描述。
<strong>TL;DR</strong>：</p>

<ol>
  <li>反斜线用来转义除换行之外的所有字符，反斜线加换行为连行;</li>
  <li>单引号用来直出字面量，其内容部分不允许转义，包括单引号转义也不允许；</li>
  <li>双引号内允许 <a href="https://www.gnu.org/software/bash/manual/html_node/Command-Substitution.html#Command-Substitution">命令替换</a> 和对特定几个字符转义，双引号内的反斜线对其他字符没有特殊含义，会被当作字面量处理。</li>
</ol>

<!--more-->

<h2 id="一个例子">一个例子</h2>

<p>如果不熟悉 Bash 引号的语义，尤其是配合管道和 xargs 等命令时，事情会变得很复杂很难以理解。
比如下面的命令把 16 进制 ASCII 转为字符串 <code class="language-plaintext highlighter-rouge">harttle</code>：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">echo</span> <span class="s1">'\\x68\\x61\\x72\\x74\\x74\\x6c\\x65'</span> | xargs <span class="nt">-0</span> <span class="nb">printf</span> <span class="s1">'%b'</span>
</code></pre></div></div>

<p>如果没有加引号或没有加 <code class="language-plaintext highlighter-rouge">-0</code> 都不会得到正确的结果，二者都会影响参数如何转义。
在介绍完规则后我们重新来看这个例子。</p>

<h2 id="bash-转义">Bash 转义</h2>

<p>Bash（Posix）转义规则很简单：</p>

<p>一、<strong>反斜线用来保持字面量</strong>。Bash 里反斜线用来转义下一个字符，保持下一个字符的字面值。
比如 <code class="language-plaintext highlighter-rouge">\$</code> 表示字面量 <code class="language-plaintext highlighter-rouge">$</code>，否则如果没有反斜线 <code class="language-plaintext highlighter-rouge">$</code> 会被 <a href="https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html#Shell-Parameter-Expansion">Bash 参数展开</a>。
例如：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># echo 将收到字面量 \x68\x61\x72\x74\x74\x6c\x65，下一个字符 \ 被保持</span>
<span class="c"># 输出 harttle</span>
<span class="nb">echo</span> <span class="se">\\</span>x68<span class="se">\\</span>x61<span class="se">\\</span>x72<span class="se">\\</span>x74<span class="se">\\</span>x74<span class="se">\\</span>x6c<span class="se">\\</span>x65

<span class="c"># echo 将收到字面量 x68x61x72x74x74x6cx65，下一个字符 x 被保持</span>
<span class="c"># 输出：x68x61x72x74x74x6cx65</span>
<span class="nb">echo</span> <span class="se">\x</span>68<span class="se">\x</span>61<span class="se">\x</span>72<span class="se">\x</span>74<span class="se">\x</span>74<span class="se">\x</span>6c<span class="se">\x</span>65
</code></pre></div></div>

<p>注意 Shell 只负责处理参数和调用命令，不会识别 <code class="language-plaintext highlighter-rouge">\t</code>, <code class="language-plaintext highlighter-rouge">\n</code>，<code class="language-plaintext highlighter-rouge">\x68</code> 等其他编程语言里的 ASCII 特殊字符，这些特殊字符的处理通常在具体的软件中，比如 <code class="language-plaintext highlighter-rouge">echo</code>, <code class="language-plaintext highlighter-rouge">printf</code> 等。
例如下面的命令会输出 <code class="language-plaintext highlighter-rouge">a        b</code>：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">echo</span> <span class="s1">'a\tb'</span>
</code></pre></div></div>

<p>但 <code class="language-plaintext highlighter-rouge">\t</code> 的语义并不是由 Shell 表达的，Shell 只是把这个长度为 4 的字符串 <code class="language-plaintext highlighter-rouge">"a\tb"</code> 传递给 <code class="language-plaintext highlighter-rouge">echo</code> 程序，后者将会收到参数 <code class="language-plaintext highlighter-rouge">argv[1] === "a\\tb"</code>。</p>

<p>二、<strong>反斜线+换行例外</strong>。反斜线后一个字符是换行（<code class="language-plaintext highlighter-rouge">&lt;NL&gt;</code>）时上一条规则例外。
这时 <code class="language-plaintext highlighter-rouge">\&lt;NL&gt;</code> 表示连行，一个命令可以分行写。换句话说 <code class="language-plaintext highlighter-rouge">\&lt;NL&gt;</code> 等效于会在解析时删除。
比如：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cat </span>log.txt |<span class="se">\</span>
<span class="nb">grep </span>a |<span class="se">\</span>
<span class="nb">grep </span>b
<span class="c"># 等价于</span>
<span class="nb">cat </span>log.txt | <span class="nb">grep </span>a | <span class="nb">grep </span>b
</code></pre></div></div>

<h2 id="单引号的使用">单引号的使用</h2>

<p>单引号用来保持引用内容的所有字面量，包括反斜线。也就是说一对单引号中不得出现单引号，它前面有反斜线也不行。
例如下面的命令将会输出 <code class="language-plaintext highlighter-rouge">harttle</code>：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># echo 将收到字面量 \x68\x61\x72\x74\x74\x6c\x65</span>
<span class="nb">echo</span> <span class="s1">'\x68\x61\x72\x74\x74\x6c\x65'</span>
</code></pre></div></div>

<p>如果单引号之间出现单引号，引用内容立即结束（来自其他编程语言的同学注意）。
比如：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ echo 'foo\'bar'   # 回车
quote&gt;              # Shell 继续等待输入，因为第一个引用内容是 foo\
                    # 紧接着是字面量 bar，然后是一个未关闭的 '
quote&gt;'             # 输入 ' 结束第二个引用内容（为空字符串）并回车
foar                # echo 收到的输入为 foo\bar，\b 被 echo 解释为退格
</code></pre></div></div>

<p>这个情况经常在尝试转义单引号里的单引号时发生，比如 <code class="language-plaintext highlighter-rouge">sed 's/\'/"/g'</code>, <code class="language-plaintext highlighter-rouge">grep 'harttle\'s'</code> 都是错误的写法。</p>

<h2 id="双引号的作用">双引号的作用</h2>

<p>双引号也是保持引用内容的字面量，但 <code class="language-plaintext highlighter-rouge">$</code>, <code class="language-plaintext highlighter-rouge">`</code>, <code class="language-plaintext highlighter-rouge">\</code> 除外（POSIX 标准）。
其中：</p>

<ol>
  <li><code class="language-plaintext highlighter-rouge">$</code> 用来做 <a href="https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html#Shell-Parameter-Expansion">Bash 参数展开</a>，比如 <code class="language-plaintext highlighter-rouge">echo "my name is $name."</code>。</li>
  <li><code class="language-plaintext highlighter-rouge">`</code> 表示 <a href="https://www.gnu.org/software/bash/manual/html_node/Command-Substitution.html#Command-Substitution">命令替换</a>，基本等价于 <code class="language-plaintext highlighter-rouge">$()</code>。</li>
  <li><code class="language-plaintext highlighter-rouge">\</code> 是我们讨论的重点，它用来转义。</li>
</ol>

<p>Shell 转义奇怪的是反斜线后是 <code class="language-plaintext highlighter-rouge">$</code>, <code class="language-plaintext highlighter-rouge">`</code>, <code class="language-plaintext highlighter-rouge">"</code>, <code class="language-plaintext highlighter-rouge">\</code>, <code class="language-plaintext highlighter-rouge">&lt;NL&gt;</code> 时反斜线才表示转义，否则反斜线没有特殊含义（表示一个反斜线字面量）。
例如下面两个命令都会输出 <code class="language-plaintext highlighter-rouge">harttle</code>，因为 <code class="language-plaintext highlighter-rouge">"\\x"</code> 的第一个反斜线表示转义，解释为 <code class="language-plaintext highlighter-rouge">"\x"</code>，而 <code class="language-plaintext highlighter-rouge">"\x"</code> 中的反斜线没有特殊含义，也解释为 <code class="language-plaintext highlighter-rouge">"\x"</code>。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">echo</span> <span class="s2">"</span><span class="se">\\</span><span class="s2">x68</span><span class="se">\\</span><span class="s2">x61</span><span class="se">\\</span><span class="s2">x72</span><span class="se">\\</span><span class="s2">x74</span><span class="se">\\</span><span class="s2">x74</span><span class="se">\\</span><span class="s2">x6c</span><span class="se">\\</span><span class="s2">x65"</span>
<span class="nb">echo</span> <span class="s2">"</span><span class="se">\x</span><span class="s2">68</span><span class="se">\x</span><span class="s2">61</span><span class="se">\x</span><span class="s2">72</span><span class="se">\x</span><span class="s2">74</span><span class="se">\x</span><span class="s2">74</span><span class="se">\x</span><span class="s2">6c</span><span class="se">\x</span><span class="s2">65"</span>
</code></pre></div></div>

<p>其中 <code class="language-plaintext highlighter-rouge">\&lt;NL&gt;</code> 和单引号一样表示连行，<code class="language-plaintext highlighter-rouge">\"</code> 表示字面量双引号，注意这在单引号语法中是不允许的。</p>

<h2 id="案例分析">案例分析</h2>

<p>现在继续看本文刚开始的例子：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">echo</span> <span class="s1">'\\x68\\x61\\x72\\x74\\x74\\x6c\\x65'</span> | xargs <span class="nt">-0</span> <span class="nb">printf</span> <span class="s1">'%b'</span>
</code></pre></div></div>

<ol>
  <li>由于 echo 的参数使用单引号，echo 收到的参数为字面量 <code class="language-plaintext highlighter-rouge">\\x68\\x61\\x72\\x74\\x74\\x6c\\x65</code>。</li>
  <li>因此 <code class="language-plaintext highlighter-rouge">echo '\\x68\\x61\\x72\\x74\\x74\\x6c\\x65'</code> 的输出为：<code class="language-plaintext highlighter-rouge">\x68\x61\x72\x74\x74\x6c\x65</code>。</li>
  <li>由于 <code class="language-plaintext highlighter-rouge">xargs -0</code> 下标准输入会被当做字面量处理（<code class="language-plaintext highlighter-rouge">\</code> 不再是特殊字符），<code class="language-plaintext highlighter-rouge">xargs</code> 给到 <code class="language-plaintext highlighter-rouge">printf</code> 的第二个参数为字面量 <code class="language-plaintext highlighter-rouge">\x68\x61\x72\x74\x74\x6c\x65</code>，第一个参数为 <code class="language-plaintext highlighter-rouge">%b</code>。</li>
  <li><code class="language-plaintext highlighter-rouge">printf</code> 处理十六进制 ASCII 字面量语法，输出 <code class="language-plaintext highlighter-rouge">harttle</code>。</li>
</ol>

<p>有两点需要注意：</p>

<ul>
  <li>如果 echo 的第一个参数只有单个反斜线（<code class="language-plaintext highlighter-rouge">\x68\x61\x72\x74\x74\x6c\x65</code>），echo 的输出即为 <code class="language-plaintext highlighter-rouge">harttle</code>，经过 <code class="language-plaintext highlighter-rouge">printf</code> 后仍然为 <code class="language-plaintext highlighter-rouge">harttle</code>；</li>
  <li>如果 <code class="language-plaintext highlighter-rouge">xargs</code> 没有添加 <a href="https://www.gnu.org/software/findutils/manual/html_node/find_html/xargs-options.html"><code class="language-plaintext highlighter-rouge">-0</code> 参数</a>，<code class="language-plaintext highlighter-rouge">xargs</code> 会把它的标准输入正常做 Bash 转义，也就是说 <code class="language-plaintext highlighter-rouge">xargs</code> 给到 <code class="language-plaintext highlighter-rouge">printf</code> 的第二个参数将会是 <code class="language-plaintext highlighter-rouge">x68x61x72x74x74x6cx65</code>，因为 Bash 转义中 <code class="language-plaintext highlighter-rouge">\x</code> 的语义（见“转义”一节）和 <code class="language-plaintext highlighter-rouge">printf</code> 转义中 <code class="language-plaintext highlighter-rouge">\x</code> 的语义不同。</li>
</ul>]]></content><author><name>Harttle</name></author><category term="Bash" /><category term="xargs" /><category term="grep" /><category term="sed" /><category term="转义" /><summary type="html"><![CDATA[字面量、转义和引号是任何编程语言的基础，但却少有人认真地学习 Bash 中的转义和 引号。 好消息是 Bash 引号的语义非常简单，这篇文章就可以完全描述。 TL;DR： 反斜线用来转义除换行之外的所有字符，反斜线加换行为连行; 单引号用来直出字面量，其内容部分不允许转义，包括单引号转义也不允许； 双引号内允许 命令替换 和对特定几个字符转义，双引号内的反斜线对其他字符没有特殊含义，会被当作字面量处理。]]></summary></entry><entry><title type="html">状态码很重要</title><link href="https://www.harttle.com/2020/06/25/status-code-matters.html" rel="alternate" type="text/html" title="状态码很重要" /><published>2020-06-25T00:00:00+00:00</published><updated>2020-06-25T00:00:00+00:00</updated><id>https://www.harttle.com/2020/06/25/status-code-matters</id><content type="html" xml:base="https://www.harttle.com/2020/06/25/status-code-matters.html"><![CDATA[<p>我们知道 HTTP <a href="/2015/08/15/http-status-code.html">状态码</a> 用来标识响应的状态，不恰当的状态码可能会影响 SEO，用户体验和可访问性，甚至产生不可恢复的线上问题。
因为状态码不仅仅是客户端 AJAX 的返回值，它对 Web 系统架构有着重要的影响。</p>

<p>但有些网站从不返回 4xx，用 3xx 或 200 来处理错误。可能是为了减少错误报警来提升 KPI（比如有些老板分不清 4xx 和 5xx），可能是为了减少 nginx 返回页面的大小（比如直接 302 到 CDN），也可能是 HTTP 时代 ISP 和路由器会劫持 4xx 打自己的广告（比如 <a href="https://www.zhihu.com/question/30358197?sort=created&amp;page=3">如何看待小米路由进行 404 网页劫持？</a>）。
我们不去细究原因，只把它作为案例来讨论 404/302 状态码的误用对 Web 系统的影响。</p>

<!--more-->

<h2 id="状态码及其语义">状态码及其语义</h2>

<p><a href="https://www.ietf.org/rfc/rfc2616.txt">HTTP</a> 是一种请求/响应协议，除客户端、服务器外还可能涉及代理、网关、隧道，响应状态码会影响各方的处理方式。
正如 R. T. Fielding 的论文中强调的，架构风格对系统的简单性和伸缩性都有重要的影响，而 HTTP 语义是 <a href="https://en.wikipedia.org/wiki/Representational_state_transfer">REST 架构风格</a> 的重要组成部分。
下面是本文中涉及的几个状态码：</p>

<ol>
  <li>200 OK。对于GET，应当返回被请求资源的实体；对于POST，应当返回操作的结果。</li>
  <li>302 Found。被请求的资源暂时位于另一个URI处，并且对于非HEAD/GET请求，用户代理在重定向前必须询问用户确认。RFC 1945 和 RFC 2068 规定客户端不允许更改请求的方法。但很多浏览器会将 302 当做 303 来处理（以 GET 方法重新发起请求）。</li>
  <li>303 See Other。被请求的资源暂时位于另一个URI处，并且应当以GET方法去请求那个资源。</li>
  <li>404 Not Found。服务器未能找到URI所标识的资源。也常被用于服务器希望隐藏请求被拒绝的具体原因。例如 403、401 可能会被统一处理为 404。</li>
</ol>

<h2 id="影响-seo-和爬虫">影响 SEO 和爬虫</h2>

<p><strong>使得搜索引擎索引错误的页面内容</strong>。
爬虫是一种特殊的用户代理，通常用于搜索引擎。虽然 Google 声称他们 "pretty tolerent of mistakes"，但即使 301 和 302 的表现也有<a href="https://www.sistrix.com/blog/want-confuse-google-use-302-redirect/">很大差距</a>。
一般来讲爬虫对状态码的处理倾向于：</p>

<ul>
  <li>404。该页面不存在（死链），不对它进行索引。</li>
  <li>301。URL 被站长永久地改掉了，索引重定向地址并把原页面的权重转移过去，被检索时展示后者。</li>
  <li>200。页面成功获取，即使这个页面内容为“404 未找到”，也会被入库并在搜索结果中展现。</li>
</ul>

<p>301 等价于 <a href="https://www.harttle.com/2015/07/25/bash-file-batch.html">canonical url + meta refresh</a>。302 有更多的不确定性，因为确实重定向了，但又不是永久的。
此外，如果没有采用 404 状态码会让爬虫认为你的网站存在众多重复页面：因为本该 404 的 URL 都以 302、200 的方式返回了同样的页面内容。</p>

<h2 id="用户可见-bug">用户可见 Bug</h2>

<p><strong>HTTP 错误被当作成功处理，产生无法预料的效果</strong>。
3xx 和 4xx 对 AJAX/fetch 的区别在于是否被判定为发生了错误。
比如下面的代码片段功能是，获取用户的富文本个性签名，并显示到页面中：</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">res</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="dl">'</span><span class="s1">/api/user/harttle/bio</span><span class="dl">'</span><span class="p">)</span>
<span class="kd">const</span> <span class="nx">el</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="dl">'</span><span class="s1">.bio</span><span class="dl">'</span><span class="p">)</span>
<span class="nx">el</span><span class="p">.</span><span class="nx">html</span><span class="p">(</span><span class="nx">res</span><span class="p">.</span><span class="nx">text</span><span class="p">())</span>
</code></pre></div></div>

<p>考虑发生 4xx 错误的情况。由于 302 的语义是“Found”，用 302 替代 404 上述请求会成功返回而不抛错。
错误页面的内容会被塞到 DOM 中，产生类似“俄罗斯套娃”的效果。
为此我们需要建设一套 AJAX/fetch 工具库：把 302 当作错误，为了使得其他场景也可以使用 302 状态码，还得只针对特定的 Location 响应头生效这一策略。
这又使得该工具库和 nginx 配置的 redirect 地址产生耦合。</p>

<h2 id="调试信息变得不直观">调试信息变得不直观</h2>

<p><strong>404 Not Found 变成了 <code class="language-plaintext highlighter-rouge">Unexpected token</code></strong>。
302 替代 404 这件事情在浏览器看来，就是失败变成了成功。本来应该失败的过程会继续往后走，不再抛出 404 Not Found，而是抛出后续的具体处理异常，导致页面调试变得困难。分类来讲，本来的 404 Not Found 会变成下面这些错误：</p>

<ul>
  <li>AJAX/fetch JSON 被重定向到错误页面时。HTML 内容会被当做 JSON 解析而产生一个 <code class="language-plaintext highlighter-rouge">Unexpected token</code> 错误。</li>
  <li>AJAX/iconfont 等资源被重定向到错误页面（通常是 CDN URL）时，线下域名调试时会发生跨域错误 <code class="language-plaintext highlighter-rouge">Access-Control-Allow-Origin</code>。</li>
  <li>脚本资源被重定向到错误页面时，会发生类似 <code class="language-plaintext highlighter-rouge">unexpected token &lt;</code> 的解析错误。因为 HTML 文件第一个非空字符是 <code class="language-plaintext highlighter-rouge">&lt;html&gt;</code> 中的 <code class="language-plaintext highlighter-rouge">&lt;</code>，它不是合法的 JavaScript。</li>
  <li>样式资源被重定向到错误页面时，会发生 <code class="language-plaintext highlighter-rouge">Resource interpreted as Stylesheet but transferred with MIME type text/html</code> 报警。</li>
</ul>

<p>此外 Chrome Network 不会把 302 的资源标注为红色（因为 302 的语义不是错误），为了定位产生错误的资源，
你需要去 Chrome DevTools 的 Network 中搜索 <code class="language-plaintext highlighter-rouge">status-code:302</code>。</p>

<h2 id="网站的可访问变差">网站的可访问变差</h2>

<p><strong>强制隐藏错误信息，使网站变得难以使用</strong>。
网站发生 404 错误时，通常是用户 URL 拼写有误，或点击了错误的链接。
此时返回 3xx 会非常令人恼火，考虑下面的场景：</p>

<ol>
  <li>地址栏键入 <code class="language-plaintext highlighter-rouge">https://m.baidu.com/ss?word=harttle</code> 并回车。这里多写了一个 s，我期望百度返回 404 并给我一次改正的机会。结果重定向后地址栏直接变成了 <code class="language-plaintext highlighter-rouge">https://m.baidu.com/error.jsp</code>，前面的 word 白敲了，而且 302 不产生历史记录，无法通过返回按钮来回到我拼写的 URL。</li>
  <li>我想知道某个链接的 URL 所以点击了这个链接。期望从地址栏能够拷贝、编辑或收藏这个 URL。这个流程对失效的、返回 404 的链接也 OK，但如果它被 302 走了，我只能得到一个毫无意义的错误页的 URL，没法编辑或收藏，刷新（一个 URL 是 <code class="language-plaintext highlighter-rouge">/error.html</code> 的页面）也毫无意义。</li>
</ol>

<p>细心的读者可能注意到了，“我想知道某个链接的 URL 所以点击了这个链接”很不专业也很不安全，大可以右键复制嘛。我们来个更好的例子：
比如我是从某个论坛网站/短网址服务上得到的链接，这个链接需要经过一次跳转才能到源站，那么现在它会自动跳转两次。
要想知道它指向的 URL 到底是什么我需要打开 Chrome Network 控制台或者手动 curl。</p>

<p>可是为什么非要查看一个失效的链接呢？因为我假设这个 URL 包含了有用信息（URL 不包含任何有用信息的情况可访问性是零，没法变差了），比如它的域名（这样我就可以去它的网站上搜索），它的路径（比如可能只是拼写错误，我可以纠正它并继续访问）。这些信息不再对用户可用，就意味着这种场景下网站的可访问性已经变差。</p>

<h2 id="不可恢复的资源错误">不可恢复的资源错误</h2>

<p><strong>误用 302 会导致无法恢复的资源错误</strong>。
我们说到 HTTP 涉及多方，涉及到客户端、代理、网关、服务器，HTTP 协议描述了哪些状态码是可缓存的。
比如 4xx 是禁止缓存的，而 302、200 是可缓存的。虽然浏览器不会缓存主文档，但静态资源仍然可以被缓存。
这意味着 302 替代 404 还有一个后果：如果浏览器访问过一个不存在的资源，该 302 会被缓存，即使文件已经存在了。直到用户清除缓存。</p>

<p>例如，我们在 HTML 中引用 JavaScript 文件。因流程错误导致 HTML 首先部署生效，用户访问页面时 JavaScript 被 302 导致功能异常。
即使我们尽快完成了 JavaScript 部署，用户重新访问或刷新页面并不会得到修复：错误的 JavaScript（内容为错误页的 HTML）被缓存了。
适用于这个例子的不只是脚本，还包括样式、图片、字体，即所有可缓存的资源都有问题。</p>

<p>也就是说由于错误地使用状态码，我们无法从错误中恢复，只能寄希望于用户主动清除浏览器缓存。</p>]]></content><author><name>Harttle</name></author><category term="HTTP" /><category term="Web" /><category term="状态码" /><category term="REST" /><summary type="html"><![CDATA[我们知道 HTTP 状态码 用来标识响应的状态，不恰当的状态码可能会影响 SEO，用户体验和可访问性，甚至产生不可恢复的线上问题。 因为状态码不仅仅是客户端 AJAX 的返回值，它对 Web 系统架构有着重要的影响。 但有些网站从不返回 4xx，用 3xx 或 200 来处理错误。可能是为了减少错误报警来提升 KPI（比如有些老板分不清 4xx 和 5xx），可能是为了减少 nginx 返回页面的大小（比如直接 302 到 CDN），也可能是 HTTP 时代 ISP 和路由器会劫持 4xx 打自己的广告（比如 如何看待小米路由进行 404 网页劫持？）。 我们不去细究原因，只把它作为案例来讨论 404/302 状态码的误用对 Web 系统的影响。]]></summary></entry></feed>