cert-manager CNAME 问题记录

在研究 cert-manager 使用 webhook 方式调用 dnspod 使用 DNS-01 方式签发 SSL 证书遇到问题,一直得到错误:

I0306 03:48:38.870605       1 controller.go:144] "syncing item" logger="cert-manager.controller"
I0306 03:48:38.870714       1 dns.go:118] "checking DNS propagation" logger="cert-manager.controller.Check" resource_name="test1-tsh1-frytea-com-1-3300738485-2689263791" resource_namespace="default" resource_kind="Challenge" resource_version="
v1" dnsName="test1.tsh1.frytea.com" type="DNS-01" resource_name="test1-tsh1-frytea-com-1-3300738485-2689263791" resource_namespace="default" resource_kind="Challenge" resource_version="v1" domain="test1.tsh1.frytea.com" nameservers=["223.5.5.5:53","8.8.8.8:53"]
I0306 03:48:38.879628       1 wait.go:94] "Updating FQDN" logger="cert-manager.controller" resource_name="test1-tsh1-frytea-com-1-3300738485-2689263791" resource_namespace="default" resource_kind="Challenge" resource_version="v1" dnsName="test
1.tsh1.frytea.com" type="DNS-01" fqdn="_acme-challenge.test1.tsh1.frytea.com." cname="tsh1.frytea.com."
I0306 03:48:38.897174       1 wait.go:145] "Looking up TXT records" logger="cert-manager.controller" resource_name="test1-tsh1-frytea-com-1-3300738485-2689263791" resource_namespace="default" resource_kind="Challenge" resource_version="v1" dns
Name="test1.tsh1.frytea.com" type="DNS-01" fqdn="tsh1.frytea.com."
E0306 03:48:38.897227       1 sync.go:208] "propagation check failed" err="DNS record for \"test1.tsh1.frytea.com\" not yet propagated" logger="cert-manager.controller" resource_name="test1-tsh1-frytea-com-1-3300738485-2689263791" resource_nam
espace="default" resource_kind="Challenge" resource_version="v1" dnsName="test1.tsh1.frytea.com" type="DNS-01"I0306 03:48:38.897688       1 controller.go:164] "finished processing work item" logger="cert-manager.controller"

我使用了以下资源:

在相关仓库找到这些 issue

发现,只要申请证书的域名能够匹配到 CNAME 记录,就会默认跟随,找不到正确的 TXT 记录,导致认证失败。 虽然 cert-manager 提供了这个参数 cnameStrategy: None ,能够在声明 ISSUE 时使用,但是似乎大部分实现的 webhook 都没有实现这个特性:

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: dnspod
spec:
  acme:
    email: xxxx # 在证书过期的时候,会发邮件通知
    preferredChain: ""
    privateKeySecretRef:
      name: example-com-letsencrypt-dev-key # 用于存储ACME帐户私钥的密钥名称
    server: https://acme-staging-v02.api.letsencrypt.org/directory
    #server: https://acme-v02.api.letsencrypt.org/directory # 生产
    solvers:
      - dns01:
          cnameStrategy: None
          webhook:
            config:
              secretId: xxxxxx
              secretKeyRef:
                key: secret-key
                name: dnspod-secret
              ttl: 600
            groupName: acme.imroc.cc
            solverName: dnspod

目前临时的解决办法,只能是 避免 cert-manager 托管域名能够解析到 CNAME 记录,等有空了研究一下 cert-managerwebhook 的实现方法,看能否解决这个问题。

References

ProxmoxVE (PVE) NAT 网络配置方法

伪装允许只有私有 IP 地址的访客使用主机 IP 地址来访问网络,以处理传出流量。每个传出数据包都会被重写 iptables ,使其看起来来自主机,响应也会相应地被重写以路由到原始发件人。

auto lo
iface lo inet loopback

auto eno1
#real IP address
iface eno1 inet static
        address  198.51.100.5/24
        gateway  198.51.100.1

auto vmbr1
#private sub network
iface vmbr0 inet static
        address  10.10.10.1/24
        bridge-ports none
        bridge-stp off
        bridge-fd 0

        post-up   echo 1 > /proc/sys/net/ipv4/ip_forward
        post-up   iptables -t nat -A POSTROUTING -s '10.10.10.0/24' -o eno1 -j MASQUERADE
        post-down iptables -t nat -D POSTROUTING -s '10.10.10.0/24' -o eno1 -j MASQUERADE
        post-up   iptables -t raw -I PREROUTING -i fwbr+ -j CT --zone 1
        post-down iptables -t raw -D PREROUTING -i fwbr+ -j CT --zone 1

其中 vmbr1 为 NAT 网桥,网桥 IP 为 10.10.10.0/24 ,该网段流量会被转换为 eno1 网卡的 IP 发出,并在收到回复保温时转换为原始 IP,实现共享外部 IP 的目标。

重载网络配置使其生效:

ifreload -a

实测立刻可以生效,VM 中配置该段 IP 并将 10.10.10.1 作为网关即可,也可根据需要配置 dnsmasq 之类的实现 DHCP 自动分配 IP。

References

Linux 备份和恢复 docker volume 脚本分享

脚本

Dump

docker-volume-dump.sh

#!/usr/bin/env bash

dump_dir=~/docker-volume-dump

if [ ! -d $dump_dir ]; then
  mkdir -p $dump_dir
fi

for volume in $(docker volume ls -q); do
  dump_file=$dump_dir/$volume.tar.gz
  if [ -f $dump_file ]; then
    ( set -x; echo rm $dump_file; )
  fi
  echo "Dump docker volume \"$volume\" to \"$dump_file"\"
  docker run --rm -v $volume:/from alpine sh -c "cd /from; tar -cf - ." | gzip > $dump_dir/$volume.tar.gz
done

Restore

docker-volume-restore.sh

#!/usr/bin/env bash

dump_dir=~/docker-volume-dump

for file in ~/docker-volume-dump/* ; do
  volume=$(basename $file)
  volume=${volume%%.*}
  echo "$volume"
  docker volume inspect $volume &>/dev/null
  if [ $? -eq 0 ]; then
    ( set -x; docker volume rm $volume 1>/dev/null )
  fi
  ( set -x; docker volume create $volume 1>/dev/null )
  cat $file | docker run --rm -i -v $volume:/to alpine sh -c 'tar zxvf - -C /to'
done

References

ArchLinux 休眠到交换文件

来源: ArchLinux 休眠到交换文件

Linux 使用交换分区来休眠,首先冻结所有进程并申请足够的交换内存(位于磁盘),把当前内存都存进去。 然后下次启动时,initramfs 会直接加载上次休眠时的内存状态,跳过内核的 init 过程。 因此首先需要有足够大的交换分区或交换文件;再把内核指向到休眠的分区上;最后再配置 initramfs 让它加载休眠的内存文件。 官方文档请参考 Power_management/Suspend_and_hibernate#Hibernation, 本文细述如何休眠到交换文件,并对其中一些概念和细节进行了解释。

在本文讨论的范围内, 挂起(suspend)是指冻结当前的进程,保留它们的内存,并把几乎除了内存之外的设备都断电。 休眠(hibernate)是指把挂起后的内存写入磁盘并完全关机。 锁定(lock)则只是显示一个模态的全屏软件输入正确的密码才能退出。

交换文件

安装系统 前需要创建交换分区,现在的机器普遍内存较大不太需要交换分区来扩展内存空间, 而且磁盘一般使用读写快速但读写次数有限 SSD,因此我的交换分区也很小根本不够用来休眠。

你可以通过 swapiniss 来让你的交换分区只用于休眠。

所以我们用交换文件来替代交换分区,在创建交换文件之前首先需要知道系统休眠需要多大空间。 可以从 sysfs 来看查看:

cat /sys/power/image_size

可以参考 [官方教程][https://wiki.archlinux.org/index.php/Swap#Swap_file] 来创建:

按照你需要的大小创建,bs * count 是最终文件大小
dd if=/dev/zero of=/swapfile bs=1M count=4096 status=progress
chmod 600 /swapfile
# 检查大小和权限
ls -l /swapfile

# 初始化交换文件并立即应用到系统
mkswap /swapfile
swapon /swapfile

# 在 /etc/fstab 中写入以下内容,交换文件会在重启后生效
/swapfile none swap defaults 0 0

重启后通过 swapon -s 来检查是否生效:

> sudo swapon -s
Filename    Type    Size     Used    Priority
/swapfile   file    12582908    0    -2

让内核找到交换文件

我们需要设置 resume 和 resume_offset 两个内核参数,告诉内核在挂起时把内存写入到哪里。

  • 第一个参数是交换文件所在的磁盘分区,可以用任何 fstab 中接受的名字格式。比如 resume =/dev/sda1,或者 resume = UUID = xxx。如果你在上一步中创建的交换文件和 / 在同一分区下,可以复制已有的 root 内核参数的值。
  • 第二个参数是交换文件的偏移量,就是它在分区中的什么位置。因此这个参数给交换文件用的,如果是交换分区则不需要填写。可以通过 filefrag -v /swapfile | awk '{ if($1=="0:"){print $4} }' 命令得到。

可以通过 cat /proc/cmdline 来查看当前的内核参数,但是在哪里设置取决于你的 Boot Loader。 以 rEFInd 为例,打开 /boot/refind_linux.conf 写入 resume 和 resume_offset:

"Boot with standard options" "ro root=/dev/sda3 resume=/dev/sda3 resume_offset=3192832"

重启后用 journalctldmesg 来找到写入休眠镜像的日志:

Oct 19 13:57:56 harttle.arch.mac kernal: PM: Creating hibernation image:
Oct 19 13:57:56 harttle.arch.mac kernel: PM: Need to copy 596422 pages
Oct 19 13:57:56 harttle.arch.mac kernel: PM: Normal pages needed: 596422 + 1024, available pages: 1477067

如果看到这样的错误说明设置有误,请检查你交换文件所在分区和 filefrag 给出的偏移量:

Oct 19 13:57:56 harttle.arch.mac kernal: PM: Image not found (code -22)

让 initramfs 加载休眠的内存

initramfs 是由 Boot Loader 直接加载的一个早期的用户空间,其中已经加载了一些内核模块。 它会进行设备初始化、挂载文件系统、运行磁盘检查等工作,之后再交给内核的 init 过程。 加载休眠的内存也是它的工作,但 ArchLinux 默认并未开启,需要去 /etc/mkinitcpio.conf 中添加 resume 钩子:

HOOKS=(base udev resume autodetect modconf block filesystems keyboard fsck)

注意因为 resume 参数用到了磁盘设备名称,resume 需要写在 udev 之后。 然后重新编译 initramfs(就像更新内核时一样):

默认使用当前系统的内核,如果你现在位于启动盘的系统则需要指定宿主环境上的内核版本。
mkinitcpio -p linux

至此配置工作都完成了,通过 systemctl hibernate 来休眠,再按下电源键开机来检查休眠功能是否正常。

开着盖子无法挂起

MacBook Pro 的显示器盖子开着默认会阻止挂起,可能会出现挂起屏幕变黑后立即结束挂起。 这是因为显示器盖子默认可以唤醒休眠,在 /proc/acpi/wakeup 中可以查看哪些设备可以唤醒,其中 LID0 是显示器盖子:

> cat /proc/acpi/wakeup 
Device  S-state   Status   Sysfs node
P0P2      S3    *disabled
EC    S4    *disabled  platform:PNP0C09:00
HDEF      S3    *disabled  pci:0000:00:1b.0
RP01      S3    *enabled   pci:0000:00:1c.0
RP02      S3    *enabled   pci:0000:00:1c.1
RP03      S3    *enabled   pci:0000:00:1c.2
ARPT      S4    *disabled  pci:0000:03:00.0
RP05      S3    *enabled   pci:0000:00:1c.4
RP06      S3    *enabled   pci:0000:00:1c.5
XHC1      S3    *enabled   pci:0000:00:14.0
ADP1      S4    *disabled  platform:ACPI0003:00
LID0      S4    *enabled  platform:PNP0C0D:00

我们可以 echo LID0 > /proc/acpi/wakeup 来更改它的状态,然后再试休眠。 我们希望它的状态默认就是 disabled,需要一个这样的 systemd 服务:

[Unit]
Description=Disable LID0 wakeup triggers in /proc/acpi/wakeup

[Service]
Type=oneshot
ExecStart=/bin/sh -c "echo LID0 > /proc/acpi/wakeup"
ExecStop=/bin/sh -c  "echo LID0 > /proc/acpi/wakeup"
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target

把它添加到 /etc/systemd/system 并启用即可。

自动休眠

自动休眠和其他电源管理功能,由很多不同的软件和配置方式来实现。为避免混淆先介绍几个常见的软件:

systemd-logind:ArchLinux 的默认安装包含了 systemd,其中的 systemd-logind 是自动启用的。 它包含了一些非常简单的电源管理功能,比如按下电源按钮时关机、笔记本合上盖子时挂起。 因此 ArchLinux 装好之后就基本可以用了。

acpidacpid 是一个比较基础的电源管理工具,工作方式是响应 ACPI 事件,做相应的处理比如关机还是休眠。注意 acpid 只是电源管理工具,ACPI 是设备配置接口跟它没关系。

dpmsdpms 是 xorg 提供的显示器电源管理服务,用来控制显示器关闭等动作。有 standby, suspend, off 等阶段,跟 systemd 事件一样需要有人来订阅(比如 xss-lock)才能执行具体操作。可以通过 xorg.conf 的 StandbyTime, SuspendTime, OffTime 等来配置,也可以在运行时用 xset s 来配置。

tlptlp 是一个比较无脑的电源管理工具,提供类似电池模式、电源模式、性能优先这样级别的配置。

xss-lock, xidle, xautolock:这些是 X11 下的工具用来在用户无操作时执行挂起等操作,有些还会监听 ACPI 事件,这样在 suspend 时屏幕也能锁定。

无操作自动休眠

自动休眠和自动挂起需要桌面环境(DE)或者 X11 软件的支持。 如果你在用 Gnome 或 KDE 在控制面板中配置后,会把 idle 信息报告给 systemd-logind,后者接管具体操作。

如果你像 Harttle 一样没有桌面系统和登录管理器的话, 需要安装一个类似 xss-lock, xidle 这样的工具来靠 X11 事件计时, 然后调用 systemctl hibernatesystemctl syspend

先挂起后休眠

合上盖子后 systemd-logind 的默认行为是挂起,可以在 /etc/systemd/logind.conf 中把它重新设置为休眠,或先挂起再休眠:

HandleLidSwitch=suspend-then-hibernate

挂起后休眠前的时间可以在 /etc/systemd/sleep.conf 中设置:

HibernateDelaySec=15min

除了合上盖子之外,其他场景也可以直接调用 systemctl suspend-then-hibernate

电量低自动休眠

这件事情需要具体的软件来做,或者直接安装 tlp 并启动 tlp, tlp-sleep 两个 systemd 服务。 下面提供一个简单的 udev 规则,在电量小于等于 5% 时休眠:

SUBSYSTEM=="power_supply", ATTR{status}=="Discharging", ATTR{capacity}=="[0-5]", RUN+="/usr/bin/systemctl hibernate"

把它写入 /etc/udev/rules.d/99-lowbat.rules,重启即可生效。

References

Vim 下大小写敏感的搜索-替换

来源: Vim 下大小写敏感的搜索/替换

Vim 中的搜索默认是大小写敏感的,即搜索 vim 不会匹配到 Vim。 这一点跟多数编辑器/IDE 都不同,因此 Vim 的默认设置其实很不顺手。 本文来分享一些个性化的配置方法,让 Vim 下的大小写敏感/不敏感用起来更加顺手。 比如当搜索词包含大写时应用大小写敏感搜索;其他情况应用大小写不敏感搜索。

TL;DR

以搜索词为 harttle 为例(省略了最后的回车):

  • 强制大小写不敏感搜索:/harttle\c
  • 强制大小写敏感搜索:/harttle\C
  • 强制大小写不敏感替换:s/harttle\c/Harttle
  • 强制大小写敏感替换:s/harttle\C/Harttle
  • 设置为大小写敏感::set ignorecase
  • 设置为大小写不敏感::set noignorecase
  • 设置为智能模式(有大写时敏感否则不敏感)::set smartcase
  • 设置为非智能模式::set nosmartcase

大小写敏感控制字符

正如在正则表达式有类似 i 这样的开关,Vim 也有特殊字符来控制大小写敏感。 在模式末尾加 \c 表示大小写不敏感,加 \C 表示大小写敏感。 例如:

" 大小写不敏感搜索,可以匹配:vim, Vim, VIM
/vim\c
<CR>
" 大小写敏感搜索,只可以匹配:Vim
/Vim\C
<CR>
" 把出现的所有 vim, Vim, VIM 等都替换为 Vim,在写文章时会经常会用到
:%s/vim\c/Vim/g

这一语法的优先级高于下文的 ignorecase, smartcase 等选项, 所以比较万能,在远程机器上、别人的电脑上,一般用这个操作。

ignorecase/smartcase

Vim 中的 ignorecase 用于设置大小写敏感,它将在所有搜索、替换命令中生效。 在 normal 模式中 :set ignorecase 设置为不敏感;:set noignorecase 设置为敏感。 ignorecase 属于选项变量,因此也可以通过 & 来设置,例如::let &ignorecase=1。 把冒号去掉后可以直接放到 .vimrc 文件里持久生效。

更多 Vim 变量赋值和引用的细节,可参考这篇文章:Vim 中的变量赋值、引用与作用域

开启 ignorecase 之后还可以把 smartcase 也打开(后者要求前者出于开启状态), Vim 会启用智能模式:

  • 在你输入的模式中包含大写时,启用大小写敏感模式;
  • 在你输入的模式中只有小写时,启用大小写不敏感模式。

例如:

:set ignorecase
:set smartcase

" 大小写不敏感,可以匹配:vim, Vim, VIM
/vim
<CR>
" 大小写敏感,只可以匹配 Vim
/Vim
<CR>"

当前词搜索

smartcase 只对输入的模式(pattern)生效,其他不需要输入 pattern 的搜索命令不生效。 比如 在 Vim 中优雅地查找和替换 中介绍过可以用 *(向后),#(向前),g*(不切词)等命令来搜索光标所在的词搜索光标所在的词。 为了让它们好使,可以先按下 * 来搜索一次,然后按下 /(向后)再按上箭头找到上次历史(这是一个具体的 pattern)再按回车搜索。同样地,按 ?(向前)也可以。

“展开光标所在词”是存在 Vim 命令的,因此我们可以把 *, # 映射掉来自动化上面的过程:

" respect to smartcase, expand the pattern
:nnoremap * /\<<C-R>=expand('<cword>')<CR>\><CR>
:nnoremap # ?\<<C-R>=expand('<cword>')<CR>\><CR>

这样下次按下 *# 时,Vim 就会展开光标处的词,分别应用 /? 进行搜索。 这样当光标处的词有大写时就用大小写敏感搜索,全小写时就用大小写不敏感搜索。 <C-R>= 用来插入计算表达式并插入到命令里,类似我们在 使用 Vim 寄存器 中介绍的 <C-R>" 可以把匿名寄存器(上次拷贝、剪切、删除)的内容插入到命令里。

References

对 tail -f 使用管道

来源: 对 tail -f 使用管道

最近发现 tail -f 时管道后面的程序都会被卡住,才发现 grep,sed,awk 不直接输出到 TTY 时都是带缓冲的。平时跟在 cat 后使用没问题是因为输入管道关闭触发了 flush。本文详细解释其中的坑,以及怎么让 sed, awk, grep 立即 flush。

TL;DR:grep 添加 --line-buffered,sed 添加 -u,awk 调 fflush()

管道和缓冲

管道 是 Linux/Unix 中进程间通信的一种方式,可以在命令间、进程间传递数据。比如下面的命令用来来打印所有文件不存在的异常。

cat log.txt | grep Error | grep ENOENT

由于 cat 命令会在读完文件后立即退出并关闭 STDOUT,grep 的缓冲会立即 flush,我们会在执行完上述命令后立即看到输出。但如果改成实时打印日志的 tail -f 则会看不到任何输出:

tail -f log.txt | grep Error | grep ENOENT

因为当 grep 的输出不是 TTY(终端) 时,会启用缓冲。输入关闭或缓冲区满时才输出。这个例子中第一个 grep 的输入 tail -f 一直没有关闭,因此缓冲一直不会输出,第二个 grep 也永远不会收到输入。 因此控制台不会有任何输出。

但如果反过来,grep 的输出是 TTY 时就不会缓冲。也就是说 tail -f log.txt | grep Error(注意少了一个 grep)会正常地持续地输出。

检查输出文件

那么 grep 会检查它输出到哪里?虽然理论上有悖于管道的设计,也不那么函数式。 难以想象我们有个函数,它的返回值竟然会取决于这个返回值下一步被用于做什么操作。 不仅是 grep,sed 也有类似的行为,这里不去更多地讨论设计,而是给几个有用的场景:

  1. 当输出到 TTY 时输出带颜色的字符,输出到文件时输出纯文本。
  2. 当输出到 TTY 时执行过程可以提示用户输入,输出到文件时则需要使用默认值或者报错。
  3. 以及 grep 的例子:输出到 TTY 时实时打印,输出到文件或其他程序时缓冲起来(因为尤其是写入磁盘文件时,没必要有输出就写)。

那么怎么判断标准输出的文件描述符呢?

注意 [ 是一个命令,-t 是它的参数,可以 man [ 查看详情。

缓冲区满

既然 tail -f 日志看不到输出是因为缓冲区没有 flush,那么缓冲区什么时候会被 flush 呢?有两种情况:

  1. 写入已经结束(类似 JavaScript 中的 Stream.prototype.end() 调用)。但是 tail -f 的输出流永远不会结束,因为 -f 会永远 follow 文件 append。作为对比,cat 命令的输出流会在读到文件尾时结束。比如执行 cat log.txt | grep Error 会立即 flush 并退出。
  2. 缓冲区满。既然叫做 Buffer 一定是有大小的,tail 写入足够多的内容后,grep 的缓冲区就会满,这时也会发生 flush。

那么 grep 的缓冲区是多大呢?既然 tail 的输出不足以填满缓冲区,我们用输出足够多的 yes 命令:

yes Error ENOENT | grep Error | grep ENOENT

yes 命令用来不断地循环(死循环,直到被 Ctrl-C)输出它的参数,因此缓冲很快会满。果然上面的命令我们可以看到大量的输出。

避免缓冲

grep 提供了 --line-buffered 来按照行缓冲,也就是每写满一行 flush 一次:

--line-buffered
     Force output to be line buffered.  By default, output is line buffered when standard output is
     a terminal and block buffered otherwise.

sed 可以用 --unbuffered 来禁用缓冲:

-u, --unbuffered
     load minimal amounts of data from the input files and flush the output buffers more often

awk 作为一门完整的编程语言,需要调用 fflush() 方法来清空缓冲:

The built-in function fflush(expr) flushes any buffered output for the file or pipe expr.

因此前面的例子中给 grep 添加 --line-buffered 即可让它持续地输出:

tail -f log.txt | grep --line-buffered Error | grep ENOENT

注意第二个 grep 不需要添加 --line-buffered,因为它的标准输出是 TTY,默认不会启用缓冲区。 下面是一个更完整的例子,从 log.txt 文件实时读日志,过滤包含 Error 的行,把 harttle 标记去掉,打印出第一列,再过滤得到 ENOENT 的行:

tail -f log.txt | grep --line-buffered Error | sed -u 's/harttle//' | awk '${print $1; fflush()}' | grep ENOENT

References

Vim 使用系统剪切板

来源:Vim 使用系统剪切板

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

  1. 确保你的 vim 支持剪切板,通过 vim --version | grep clipboard 检查。
  2. 确定你的剪切板寄存器是 "+(XA_SECONDARY)还是 "*(XA_PRIMARY)。
  3. 同步剪切板和匿名寄存器,在 ~/.vimrc 添加配置比如 set clipboard=unnamed

确保你的 Vim 支持剪切板

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

vim --version | grep clipboard

如果输出包含 +clipboard+xterm_clipboard 就支持,如果这两项都是 - 则不支持。例如我的 Vim 输出为(MacOS 上的 macvim):

+clipboard         +keymap            +printer           +vertsplit
+emacs_tags        -mouse_gpm         -sun_workshop      -xterm_clipboard

如果你的 Vim 不支持剪切板,则需要重新安装一个带 clipboard 的 Vim:

重新安装后再执行 vim --version 来查看 clipboard 是否支持。注意:如果安装到了其他路径你需要改 PATH 或重启 Terminal。

确定你的剪切板寄存器

Vim 有 48 个寄存器,y, d, p 等命令一般使用匿名寄存器 "", 支持剪切板的 Vim 会支持额外的选区寄存器 "*"+。 更多 Vim 寄存器的信息,可以参考这篇文章:Vim 寄存器完全手册

"*"+ 在 Mac 和 Windows 中,都是指系统剪切板(clipboard),例如 "*yy 即可复制当前行到剪切板。 其他程序中复制的内容也会被存储到这两个寄存器中。 在 X11 系统中(绝大多数带有桌面环境的 Linux 发行版),二者是有区别的:

  • "* 指 X11 中的 PRIMARY 选区,即鼠标选中区域。在桌面系统中可按鼠标中键粘贴。
  • "+ 指 X11 中的 CLIPBOARD 选区,即系统剪切板。在桌面系统中可按 Ctrl+V 粘贴。

上述哪个寄存器对应于你的剪切板和 Linux 发行版有关,在配置 Vim 前可以测试一下。 比如用 Vim 打开一个文件,在 normal 模式下(进入 Vim 后默认的模式)键入 gg"*yG, 来把当前文件内容拷贝到 "* 寄存器。键入 gg"+yG 拷贝到 "+ 寄存器。

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

同步剪切板和匿名寄存器

以下配置可以让主选区寄存器 "* 和匿名寄存器 "" 保持同步(即共享剪切板), 一般适用于 Windows 和 MacOS,Linux 下的表现是共享 X11 剪切板、PRIMARY 选区(鼠标中键粘贴)。

set clipboard=unnamed

Vim 7.3.74 及以上支持了 unnamedplus:

set clipboard=unnamedplus

即让剪切板寄存器 "+ 和匿名寄存器 "" 保持同步, Linux 下一般对应于桌面系统的剪切板,比如 GNOME 的系统剪切板、以及 SECONDARY 选区(Ctrl+V 粘贴)。

不支持 clipboard 的情况

如果你的 Vim 不支持 clipboard 且没法升级或其他 clipboard 选项不好使的情况, 可以调用外部命令来实现拷贝粘贴,在 Vim 里直接调用,或设置快捷键调用。 比如 让 Tmux 远程 Vim 使用本地系统的剪切板

如果在 MacOS 下,可以用 pbcopy/pbpaste 命令来实现。

  • 拷贝一段文本:先按 v 进入 visual 模式选中后执行 :w !pbcopy。拷贝整个文件可以 :%w !pbcopy
  • 粘贴一段文本:把光标移动到要插入的行,执行 :r !pbpaste

如果在 Linux 下,可以借由 xclip 来实现。用 xclip -i -sel c 代替上面的 pbcopy,用 xclip -o -sel -c 代替上面的 pbpaste

References

Vim 寄存器完全手册

转载自 Vim 寄存器完全手册

常见文本编辑器都会提供剪切板来支持复制粘贴,Vim 也不例外。 不同的是 Vim 提供了 10 类共 48 个寄存器,提供无与伦比的寄存功能。 最常用的 y 操作将会拷贝到默认的匿名寄存器中,我们也可以指定具体拷贝到哪个寄存器中。

一般来讲,可以用 "{register}y 来拷贝到 {register} 中, 用 "{register}p 来粘贴 {register} 中的内容。例如: "ayy 可以拷贝当前行到寄存器 a 中,而 "ap 则可以粘贴寄存器 a 中的内容。

除了 a-z 26 个命名寄存器,Vim 还提供了很多特殊寄存器。合理地使用可以极大地提高效率。例如:

  • "+p 可以粘贴剪切板的内容,
  • ":p 可以粘贴上一个 Vim 命令(比如你刚刚费力拼写的正则表达式),
  • "/p 可以粘贴上一次搜索关键词(你猜的没错,正是 normal 模式下的 /foo 搜索命令)。

在 Vim 中可通过 :reg 来查看每个寄存器当前的值。

寄存器分类

Vim 提供了 10 类寄存器,可在 Vim 中通过 :help registers 查看帮助。

  1. 匿名寄存器 ""
  2. 编号寄存器 "0"9
  3. 小删除寄存器 "-
  4. 26 个命名寄存器 "a"z
  5. 3 个只读寄存器 ":, "., "%
  6. Buffer 交替文件寄存器 "#
  7. 表达式寄存器 "=
  8. 选区和拖放寄存器 "*, "+, "~
  9. 黑洞寄存器 "_
  10. 搜索模式寄存器 "/

1. 匿名寄存器

使用 d, c, s, x 等会删除字符的命令时,被删除字符会进入匿名寄存器 ""。 你可以认为 "" 寄存器是一个指针,指向刚才被存到的寄存器。

如何用 Vim 搭建 IDE ? 一文中提到,Mac 下可通过下列设置来让 Vim 共享系统剪切板, 就是这个原理:所有删除和拷贝操作默认都会到匿名寄存器。

set clipboard=unnamed

使用 y 命令未指定寄存器会存到 "0 寄存器中,同时 "" 会与该寄存器保有同样的值。 这意味着你使用 p"p 总会得到同样的结果。

2. 编号寄存器

编号寄存器从 "0"9 共 10 个,其中 "0 保存着拷贝来的字符串,"1"9 保存着删除掉的字符串。 删除操作符包括 s, c, d, x。 删除掉的字符串会被存到 "1 中,上次删除的则会被存到 "2 中。以此类推,Vim 会保存你最近的 9 次删除。

  • 只有整行整行的删除,和通过段落级别的移动指令(包括 %,(,),/,`,?,n,N,{,}) 的删除才会被放到 "1 中。
  • 当用户指定拷贝操作的寄存器时(如 "ap),"0 不会被写入;但删除操作一定会被写入到 "1 中。

"0 寄存器很有用,比如我们 copy 了一段文本然后用它替换另一段文本。 这时默认寄存器 "" 中的值就变成了被替换文本,如果还需要用 copy 的文本继续替换的话就需要 "0p 了。

3. 小删除寄存器

不足一行的小删除则会被放到小删除寄存器中("-),起作用的删除操作符也包括 s, c, d, x。 例如:

dw    # 删除一个词
d9l   # 删除9个字符
cb    # 向前更改一个词

"0 寄存器类似,当用户指定寄存器并进行删除时,"- 不会被写入。

4. 命名寄存器

命名寄存器有 "a"z 共 26 个,这些寄存器只有当我们指定时才会被使用。 其实我们在录制宏时,所有键盘操作会以字符串的形式存到寄存器中。 例如录制一个宏存到 "a 寄存器中,内容为更改当前行 cc,改为 foo 字符串:

qaccfoo

然后执行 :reg 来查看寄存器,可以发现 a 寄存器的值是 ccfoo

小技巧:当使用小写字母进行操作时会覆盖当前寄存器内容,当使用大写字母进行操作时,会追加当前寄存器内容。

5. 只读寄存器

只读寄存器共 3 个,它们的值是由 Vim 提供的,不允许改变:

  • ".:上次 insert 模式中插入的字符串。还记得吗? . 命令可以重复上次操作,而 ". 存储了上次插入。
  • "%:当前文件名,不是全路径,也不是纯文件名,而是从当前 Vim 的工作目录到该文件的路径。例如此时 Harttle 的 Vim 中,"%p 的结果为 _drafts/vim-registers.md
  • "::上次命令模式下键入的命令。正如 @a 可以执行 "a 寄存器中的宏一样,@: 可以执行上次命令。

6. 交替文件寄存器

交替文件寄存器 "# 存储着当前 Vim 窗口(Window)的交替文件。交替文件(alternate file)是指 Buffer 中的上一个文件,可通过 Ctrl+^ 来切换交替文件与当前文件。

Window 和 Buffer 有什么区别?参见 Vim 多文件编辑:窗口 一文。

7. 表达式寄存器

表达式寄存器 "= 主要用于计算 Vim 脚本的返回值,并插入到文本中。 当我们键入 "= 后光标会移动到命令行,此时我们可以输入任何 Vim 脚本的表达式。 例如 3+2,按下回车并且 p 则会得到 5

这在我们调试 Vim 脚本时非常有用,比如调用一个函数看它是否有正确的返回值。

8. 选择和拖放寄存器

选择和拖放寄存器包括 "*, "+, 和 "~,这三个寄存器的行为是和 GUI 相关的。

"*"+ 在 Mac 和 Windows 中,都是指系统剪切板(clipboard),例如 "*yy 即可复制当前行到系统剪切板。 其他程序中复制的内容也会被存储到这两个寄存器中。 在 X11 系统中(绝大多数带有桌面环境的 Linux 发行版),二者是有区别的:

  • "* 指 X11 中的 PRIMARY 选区,即鼠标选中区域。在桌面系统中可按鼠标中键粘贴。
  • "+ 指 X11 中的 CLIPBOARD 选区,即系统剪切板。在桌面系统中可按 Ctrl+V 粘贴。

上文所述的 Mac 下 set clipboard=unnamed 会使得系统剪切板寄存器 "* 和 Vim 默认的匿名寄存器 "" 始终保有同样的值,即 Vim 和系统共用剪切板。

有文本拖拽到 Vim 时,被拖拽的文本被存储在 "~ 中。Vim 默认的行为是将 "~ 中内容插入到光标所在位置。 当然你可以给 `

` 做键盘映射。 ## 9. 黑洞寄存器 黑洞寄存器 `”_`,所有删除或拷贝到黑洞寄存器的文本将会消失。 这是为了在删除文本的同时不影响任何寄存器的值,`”_` 通常用于 Vim 脚本中。 ## 10. 搜索寄存器 搜索寄存器 `”/` 用于存储上一次搜索的关键词。Vim 中如何进行搜索呢? 在 normal 模式下按下 `/` 即进入 search 模式,输入关键字并按下回车即可。 该寄存器是可写的,例如 `:let @/ = “harttle”` 将会把 `”harttle”` 写入该寄存器。 下次搜索时不输入搜索词直接回车便会搜索 `”harttle”`。 ## 命令行模式拷贝 值得一提的时,任何寄存器中的值都是可以拷贝到命令模式下的。 比如对于寄存器 `”a` 中的值,在 normal 模式下可以通过 `”ap` 来粘贴;在 command-line 模式下通过 `a` 来粘贴。这一操作存在风险,因为寄存器中的值可能是从网页中拷贝来的。 如果寄存器中的字符串存在 ` ` 字符或 “ 字符,则会时 Vim 回到 normal 模式, 并继续执行寄存器中的命令。为了防范 _剪切板劫持_,可以添加下列的 Vim 配置: “` inoremap + u”+gP “` > 该命令的解释请移步:[http://vim.wikia.com/wiki/Pasting_registers](http://vim.wikia.com/wiki/Pasting_registers) ## 扩展阅读 – 剪切板与 X11 选区:[http://stackoverflow.com/questions/11489428/how-to-make-vim-paste-from-and-copy-to-systems-clipboard](http://stackoverflow.com/questions/11489428/how-to-make-vim-paste-from-and-copy-to-systems-clipboard) – Vikia-Pasting Registers: [http://vim.wikia.com/wiki/Pasting_registers](http://vim.wikia.com/wiki/Pasting_registers) – Vim Help: `:help registers`, `:help quotestar`, `:help quoteplus` # References – [Vim 寄存器完全手册](https://harttle.land/2016/07/25/vim-registers.html)

Vim 宏录制和重放

TL;DR

  1. qa: 录制到寄存器 a 中,可使用 a-z 26 个命名寄存器
  2. 开始操作,做的所有操作都会录制下来,重放时自动操作;
  3. q: 结束录制
  4. @a 重放一次;
  5. 100@a 重放100次;

例子

在编辑某个文件的时候,可能会出现需要对某种特定的操作进行许多次的情况,以编辑下面的文件为例:

;=====================================================================================;This is a sample configuration file when upgrading XXX using InstallShield.;Author:        ini_always;Date:          8/24/2011;Last modified: 9/20/2011;Note: Install script does NOT verify whether the configuration file is in a "WELL";format, a WRONG format may lead to installation failure.;If more information is needed, please check the document for details.;=====================================================================================

这是一个 ini 类型的配置文件,可以看到每一行的最前面有一个逗号,现在如果需要将每行前面的逗号去掉,怎么办?在第一行行首按x,然后按j,然后按x…这样重复下去?确实,我最开始也是这样的,但如果这个文件有100行要这样修改呢?或者1000行?

好吧,少废话,进入正题。所谓宏,在vim里面是指某种特定顺序的一系列操作,我们可以录制自己的操作序列,然后重复这个序列多次,以简化某种重复的操作。vim宏有录制和播放的过程,录制就是你教给vim该怎么操作,播放就是vim照着你教的进行自动操作。因此,对于上面的文件处理,首先要进行宏录制:

1.把光标定位在第一行;

2.在normal模式下输入qa(当然也可以输入qb, qc, etc,这里的a, b, c是指寄存器名称,vim会把录制好的宏放在这个寄存器中)(PS:如果不知道什么是vim的寄存器,请自行放狗搜之);

3.正常情况下,vim的命令行会显示“开始录制”的字样,这时候,把光标定位到第一个字符(按或者|),再按x删除,按j跳到下一行;

4.normal模式下输入q,结束宏录制。

好了,经过以上步骤,我们定义了一个存储在寄存器a中的宏,它的操作序列是:0->x->j,也就是跳到行首,删除,跳到下一行。

现在,第一行已经删除了行首的逗号,而且光标也已经在第二行,现在,在normal模式下输入@a,以播放我们刚录制好的存在寄存器a中的宏。于是,第二行行首的逗号也被删除,光标停在了第三行。

这也不简单啊?你肯定会这样想,要删除100行,我还得输入100个@a,我还不如手动删除呢。呵呵,vim早就想到了,输入7@a,好了,剩下的7行全部搞定了。(PS:在命令前面加数字,就是代表要执行这个命令多少次)

当然,这个例子很简单,但也很典型。利用好vim的宏,可以使一些原本很无聊的工作要简单很多。

References

ArchLinux 快速本地部署 DeepSeek

在 ArchLinux 下借助 Ollama 可以快速运行一个小参数体量的 DeepSeek 本地模型,在我的环境上 纯 CPU 推理也可以获得很快的体验。

安装环境

一行命令即可装好 ollama

$ sudo pacman -S ollama

模型

ollama 的 DeepSeek-R1模型页面 可以看到可拉取的模型,包括全量的 671b 模型,以及精炼的小模型表现同样出色。:

  • DeepSeek-R1
  • DeepSeek-R1-Distill-Qwen-1.5B
  • DeepSeek-R1-Distill-Qwen-7B
  • DeepSeek-R1-Distill-Llama-8B
  • DeepSeek-R1-Distill-Qwen-14B
  • DeepSeek-R1-Distill-Qwen-32B
  • DeepSeek-R1-Distill-Llama-70B

模型能力统计

运行

首先打开一个 终端,运行 ollama 服务端

$ ollama serve  
2025/02/18 14:57:36 routes.go:1187: INFO server config env="map[CUDA_VISIBLE_DEVICES: GPU_DEVICE_ORDINAL: HIP_VISIBLE_DEVICES: HSA_OVERRIDE_GFX_VERSION: HTTPS_PROXY: HTTP_PROXY: NO_PROXY: OLLAMA_DEBUG:false OLLAMA_FLASH_ATTENTION:false OLLAMA_GPU_OVERHEAD:0  
OLLAMA_HOST:http://127.0.0.1:11434 OLLAMA_INTEL_GPU:false OLLAMA_KEEP_ALIVE:5m0s OLLAMA_KV_CACHE_TYPE: OLLAMA_LLM_LIBRARY: OLLAMA_LOAD_TIMEOUT:5m0s OLLAMA_MAX_LOADED_MODELS:0 OLLAMA_MAX_QUEUE:512 OLLAMA_MODELS:/home/songtianlun/.ollama/models OLLAMA_MULTIU  
SER_CACHE:false OLLAMA_NOHISTORY:false OLLAMA_NOPRUNE:false OLLAMA_NUM_PARALLEL:0 OLLAMA_ORIGINS:[http://localhost https://localhost http://localhost:* https://localhost:* http://127.0.0.1 https://127.0.0.1 http://127.0.0.1:* https://127.0.0.1:* http://0.0.  
0.0 https://0.0.0.0 http://0.0.0.0:* https://0.0.0.0:* app://* file://* tauri://* vscode-webview://*] OLLAMA_SCHED_SPREAD:false ROCR_VISIBLE_DEVICES: http_proxy: https_proxy: no_proxy:]"  
time=2025-02-18T14:57:36.900+08:00 level=INFO source=images.go:432 msg="total blobs: 11"  
time=2025-02-18T14:57:36.900+08:00 level=INFO source=images.go:439 msg="total unused blobs removed: 0"  
time=2025-02-18T14:57:36.900+08:00 level=INFO source=routes.go:1238 msg="Listening on 127.0.0.1:11434 (version 0.5.7)"  
time=2025-02-18T14:57:36.901+08:00 level=INFO source=routes.go:1267 msg="Dynamic LLM libraries" runners="[cpu cpu_avx cpu_avx2]"  
time=2025-02-18T14:57:36.901+08:00 level=INFO source=gpu.go:226 msg="looking for compatible GPUs"  
time=2025-02-18T14:57:36.944+08:00 level=INFO source=gpu.go:392 msg="no compatible GPUs were discovered"  
time=2025-02-18T14:57:36.944+08:00 level=INFO source=types.go:131 msg="inference compute" id=0 library=cpu variant=avx2 compute="" driver=0.0 name="" total="30.7 GiB" available="7.2 GiB"

不要关闭窗口,在另外一个 shell 窗口运行以下命令启动并使用 deepseek-r1:1.5b 模型:

$ ➜  ~ ollama run deepseek-r1:1.5b  
>>>    
Use Ctrl + d or /bye to exit.  
>>>    
Use Ctrl + d or /bye to exit.  
>>> hello  

<think>  

</think>  

Hello! How can I assist you today? 😊e  

>>>

也可以用 cherry-studio 之类的工具对接使用:

CherryStudio 配置截图

CherryStudio 运行截图

实测 1.5B 模型运行非常流畅,可根据实际情况同样的方法尝试其他更多模型。

其他操作

ollama 其他常用操作如下:

# 在命令行中运行模型
ollama run <模型名称>

# 列出可用模型
ollama list

# 查看模型状态
ollama ps

# 删除模型
ollama rm <模型名称>

# 启动 API 服务
ollama serve

更多详情参见各官网。

References