Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Bash 5.3 fltexpr 实现极简计算器

在终端中偶尔要做简单计算,却要启用 Python/Node.js 这种重量级解释器,颇感浪费。

虽 Bash 本身支持算术,但是原生语法 $(()) 繁琐且只有整数。我更想要的是 Excel 或者 Alfred 那样的输入体验。

所幸 Bash 5.3 新增 fltexpr 命令,浮点数备矣;繁琐问题交给自定义命令

实现 = 命令

# ~/.bashrc
enable fltexpr # 启用浮点数

function = {
  fltexpr -p "${*/x/\*}"
}

现在可以在终端直接输入:

> = 2 + 3
5

> = 2 + 3 x 5
17

要点:

  1. fltexpr 默认不启用,需要先 enable
  2. fltexpr 默认保存到变量,需要 -p 选项输出
  3. "${*/x/\*}" 将所有参数传入,同时替换 x\*, 后者输入麻烦
  4. 必须使用 function 形式,=(){} 会解析错误

实现求和求平均 +,+/

另一种常见需求是一串数字求和求平均,此时连多输 + 都嫌麻烦:

function + {
  fltexpr -p "${ IFS=+;echo "${*:-0}";IFS=;}"
}

function +/ {
  fltexpr -p "(${ IFS=+;echo "${*:-0}";IFS=;})/$#"
}

使用:

> + 1.5 2.2 33.2
36.900000000000006

> +/ 1.5 2.2 33.2
12.300000000000002

要点:

  1. IFS=+ 自动添加加号
  2. ${ ;}Bash 5.3 新命令格式,能避免进程开销

函数支持 =.

Bash 尚不支持复杂数学函数,一种选项是调用外部程序(calc/numbat/python/node):

function =. {
  numbat -e "${*:-0}"
}
> =. 'sin(2)' + 1
1.9093

或者使用 FFI.

不过两种方案都略显多此一举。

为什么我不用 NixOS

使用 NixOS 两年半后,我认为 NixOS 不能满足我的要求。

主观评价 NixOS

每种 Linux 发行版都有各自的优缺点。

NixOS 最吸引我的优点是:

  1. Flake: 相比 AUR 而言,支持分散式包管理
  2. 包生态丰富之最:特别是 ARM 设备上

代价是 NixOS 日常使用中存在诸多问题

  1. 部署拖沓:nixosConfiguration 已然,home-manager 更甚
  2. CUDA/Python 库的安装非常折磨,即使有所谓 nixos-cuda mirror
  3. No impure: Nix flake 不支持 lazy tree, 无法使用 monorepo
  4. 空间占用大:硬件涨价,磁盘空间越来越金贵
  5. 内存需求高:低内存边缘设备难以部署 nix-daemon (500MB 都不够?!)
  6. 过度抽象:屏蔽底层细节,没有 escape hatch, 难以修改

外部风险

以上缺陷只是疥癣之疾,真正的致命问题来自地缘政治风险:

  1. 地缘摩擦加剧,局部冲突持续扩大
  2. 各国经济下行,全球供应链面临挑战
  3. 全世界右翼思潮抬头

可以预见在网络软件方面:

  1. 国家间网络管控加强:清华等镜像可能不可用
  2. 网络、电力等基础设施遭到破坏
  3. 最极端情况:离线、低功耗 ARM 设备,个人无法得到稳定的主机网络电源
    • 其实是手机
    • 乃至电子管?有点极端了

NixOS 则极度依赖网络和算力

  1. 浪费算力:应用版本间强锁定,依赖项修改需要重新编译,边缘设备无从编译
  2. 难以镜像:NixOS = ~1TB, ArchLinux = ~100GB, 且 nix-store 复制麻烦
  3. 难以修改:PKGBUILD 可以轻松改用本地源码,Nix 修改 fetch 会导致整个缓存无效

替代品:brioche

我对于 Nix 最喜爱的功能是使用 Git 分散式打包,其实可以使用替代品 brioche:

  1. 基于 JS (deno). 合理的模块系统 => 高性能低内存
  2. 支持非 root 构建,无需 daemon

brioche 目前还是早期,且存在设计问题,可以未来 fork 或者 rewrite:

  1. 包生态不完善
  2. 缺少多版本并存(类 Arch)
  3. nixpkgs 的模式优于其 std
  4. 使用 quickjs 其实更好,构建系统不是瓶颈,省资源更重要。

Arch Linux 安装 bcachefs (dkms)

2026 年硬件价格高企,SSD 和 HDD 混合存储是最实惠的选择。

在传统的文件系统中,混合存储只能手动 mount 或者 ln -s, 或者选择拆分目录,比较麻烦。

解决方案:bcachefs

Linux 最新的 bcachefs 支持 SSD 和 HDD 混合存储,可以完美解决该问题。

可惜 6.18 之后的内核不再包含 bcachefs, 需要自己安装 dkms 模块。

本文记录我在 Arch Linux 上安装 bcachefs-fkms 的过程,以补网上资料阙漏。

Warning

  1. bcachefs 不够稳定,使用时务必注意资料备份。
  2. 我只记录与官方 installation 有差异的章节(括号内)
    • 请确保自己足够熟悉 Arch Linux, 否则请使用 archinstall 简易安装更简单的文件系统

1. 自建 archiso Live Install 镜像 (§1.1)

archiso 官方镜像中默认不包含 bcachefs 支持,无法挂载,需要自己创建。

pacman -S archiso
cp -r /usr/share/archiso/configs/releng/ archlive
cd archlive

packages.x86_64 中添加:

bcachefs-tools
bcachefs-dkms
linux-header

构建镜像并写入硬盘:

mkarchiso -v .
dd if=out/archlinux-xxxx.iso of=/dev/sdX bs=1M status=progress
sync

Note

如果你没有 Arch Linux 环境,或者已经删除了系统,可以在官方 archiso 中扩容

mount -o remount,size=8G /run/archiso/cowspace

然后按照上述构建镜像。

2. 硬盘分区和格式化 (§1.9)

我有两块硬盘 /dev/nvme0n1/dev/sda:

使用 fdisk 创建如下分区:

分区大小说明
/dev/nvme0n1p11Gboot
/dev/nvme0n1p2*bcachefs
/dev/sda1*bcachefs
mkfs.fat -F 32 /dev/nvme0n1p1

bcachefs format \
  --label=ssd.ssd1 /dev/nvme0n1p2 \
  --label=hdd.hdd1 /dev/sda1 \
  --replicas=1 \
  --foreground_target=ssd \
  --promote_target=ssd \
  --background_target=hdd \
  --compression=zstd

Warning

  • 我已经对重要资料另作备份,这里 replicas=1 无碍。
  • 自己按照需求创建 swap 分区

3. pacstrap (§2.2)

pacstrap -K /mnt base linux-zen linux-firmware \
  linux-zen-headers \
  bcachefs-tools bcachefs-dkms \
  neovim networkmanager
  1. 这里我使用 linux-zen, 可以改用自己喜欢的内核
  2. linux-headers 必须安装,否则 dkms 不会构建
  3. neovim networkmanager 用于基本配置

4. 修改 initramfs (§3.6)

由于 bcachefs 目前是 dkms, 需要修改 initramfs.

编辑 /etc/mkinitcpio.conf, 在 HOOKS 中添加 bcachefs.

HOOKS=(... bcachefs filesystems ...)

生成对应 initramfs.

mkinitcpio -P

5. bootloader (§3.8)

这里我使用 systemd-boot.

# /boot/loader/entries/arch.conf 
title Arch Linux
linux /vmlinuz-linux-zen
initrd /initramfs-linux-zen.img
options root=UUID=<uuid> rw rootfstype=bcachefs

Git 镜像与 insteadOf

互联网并不稳定,例如 25 年底 Cloudfare unwrap 大断网, 又或者是未来的地缘风险。

因此对于重要的源码仓库,有必要缓存到本地。

创建 Git 镜像

git clone --mirror $repo

会将复制到本地作为 bare 仓库。

也可以选择放到 NAS 上,配合 mDNS:

git clone nas.local:/repos/github/$repo

用 insteadOf 切换到本地镜像

由于 Git submodules 的局限,日常使用镜像不够方便。最好是日常照常使用 GitHub, 仅在紧急情况换用本地镜像:

git config --global url."nas.local:/repos/github/".insteadOf "https://github.com/"

镜像自动更新

手动更新比较麻烦,可以配置服务器定期自动从上游镜像更新。

单个镜像的同步方式:

git fetch --all --prune

同步子目录所有镜像:

# sync-mirrors.sh
find /repos -name ".git" -type d -prune -execdir git fetch --all --prune \;

配置为系统服务:

# ~/.config/systemd/user/sync-mirrors.service
[Unit]
Description=Sync Mirrors
Wants=network-online.target
After=network-online.target

[Service]
Type=oneshot
WorkingDirectory=/repos/
ExecStart=sync-mirrors.sh;

[Install]
WantedBy=default.target

定时器:

# ~/.config/systemd/user/sync-mirrors.timer
[Unit]
Description=Sync mirrors every 2 hour

[Timer]
OnBootSec=5min
OnUnitActiveSec=2h
Persistent=true

[Install]
WantedBy=timers.target

playwright 下载 PDF 文件

传统爬虫往往局限于从 HTML/API 获取数据。在大模型时代,二进制文件也往往是爬取目标之一。

以 PDF 格式为例。chromium 点击打开 PDF 文件时,不会触发下载,而是打开内置预览。

长期悬置的 bug

Playwright 上游并非没有发现该问题。

2021 年的 issue #7822 Make PDF testing idiomatic 就提到了相关问题。

该问题需要上游 chromium 提供命令行参数,但是 chromium 上游决定不修复该问题。

该 issue 认为 headless 模式没有该问题,实际上 headless 模式下 PDF preview 问题仍频繁发生。

变通方案

不同相关 issue 下给出了部分变通方案,但是在实际开发中,每种方案都存在各自问题。

方案一:自定义 route

await page.route("**/empty.pdf", async (route) => {
  const response = await route.fetch();
  await route.fulfill({
    response,
    headers: {
      ...response.headers(),
      "Content-Disposition": "attachment",
    },
  });
});

问题: 一旦上游有奇怪的 redirection, 就会完全获取失败

方案二:直接请求 PDF

response = await page.request.get("https://www1.hkexnews.hk/listedco/listconews/gem/2023/0209/2023020900150_c.pdf")
print(response.status)

问题:

  1. 部分 PDF 链接使用 JS 动态构造,如果要逆向分析就没必要 pw 了
  2. 部分网站在下载 PDF 时存在单独的复杂会话检验

方案三:chromium 首选项

const tmpDir = fs.mkdtempSync(`${os.tmpdir()}${path.sep}`);
fs.mkdirSync(`${tmpDir}/userdir/Default`, { recursive: true });

const defaultPreferences = {
  plugins: {
    always_open_pdf_externally: true,
  },
};

fs.writeFileSync(
  `${tmpDir}/userdir/Default/Preferences`,
  JSON.stringify(defaultPreferences),
);

const context = await chromium.launchPersistentContext(`${tmpDir}/userdir`, {
  acceptDownloads: true,
  headless: false,
  viewport: {
    width: 1440,
    height: 900,
  },
});

问题: chromium 并不完全听话,预览还是会薛定谔式地打开。不可预测对于爬虫是灾难性的。

方案四:download 属性

await page.$eval(
  pdfLinkSelector,
  (el) => el.setAttribute("download", "download"),
);

问题: 仅对 a 标签有用

方案五:换用 firefox

Firefox 可以完美解决这一问题。

问题: playwright 对于 firefox 和 webkit 的实现非常敷衍。如果选用,很多高级功能无法使用。

解决方案:自己编译

自己编译 chromium, 完全剔除 PDF 预览相关模块。

Jetson Nano 安装绿联网卡驱动

绿联网卡使用 Realtek RTL8821CU 型号。对于 linux 6.12 以上版本的内核,内置 rtw88_8821cu 驱动。

但是 Jetson Nano 原生系统版本较老,需要自己编译驱动。

git clone https://github.com/brektrou/rtl8821CU
sudo ./dkms-install.sh

sudo usb_modeswitch -KW -v 0bda -p 1a2b

使用 udev 配置持久化:

# /lib/udev/rules.d/40-usb_modeswitch.rules
ATTR{idVendor}=="0bda", ATTR{idProduct}=="1a2b", RUN+="/usr/sbin/usb_modeswitch -K -v 0bda -p 1a2b"

虚拟机内 Waydroid 转发 adb

Waydroid 是 Linux 下最佳的 Android “模拟器”。但是其图形加速依赖安卓 mesa 驱动,导致 N 卡用户完全不可用。

目前主流的解决方案是先使用 QEMU 将 host GPU 抽象为 virtio-gpu,内层 guest 再使用 Waydroid 或者 redroid 运行安卓设备。虽然会有较大性能损失,但是至少可用,且对于安卓程序够用。

但是两层桥接导致 guest waydroid 无法直接被 host adb 连接。需要在 guest 中额外设置转发规则。

环境变量

  • LXC_ADDR=192.168.240.112 容器地址
  • INTERFACE_ADDR=192.168.240.1 waydroid0 网卡地址

配置 guest 转发

# 1. 配置外部和主机访问 5555 端口转发到 LXC
sudo iptables -t nat -A PREROUTING -p tcp --dport 5555 -j DNAT --to-destination ${LXC_ADDR}:5555
sudo iptables -t nat -A OUTPUT -p tcp --dport 5555 -j DNAT --to-destination ${LXC_ADDR}:5555

# 2. 允许出入转发流量
sudo iptables -I FORWARD -d ${LXC_ADDR} -p tcp --dport 5555 -j ACCEPT
sudo iptables -I FORWARD -s ${LXC_ADDR} -p tcp --sport 5555 -j ACCEPT

## 3. 回程流量
sudo iptables -t nat -A POSTROUTING -s ${LXC_ADDR} -p tcp --sport 5555 -j SNAT --to-source ${INTERFACE_ADDR}

配置 host forward

QEMU 命令中需要额外参数:

  -net user,hostfwd=tcp::5555-:5555

未曾设想的道路?在 Mesa/NVK 中直接支持 NVIDIA provided 驱动

Note

NVIDIA 驱动版本较多,在内核驱动开发中通常如此称呼区分:

  • nvidia: NVIDIA proprietary
  • nvidia-open: NVIDIA provided
  • nouveau/NOVA: open source

由于 NOVA 并不准备支持 GSP. Turing 之前的显卡,不适用于本文情况。

历史

Linux 上的 N 卡驱动一直处于开闭源不兼容的状态。

传统上,开源驱动在内核空间使用 nouveau, 用户空间使用 mesa. mesa 仅支持 nouveau, 不支持 nvidia 或者 nvidia-open 驱动。

较新的的 Rust 驱动则使用内核 NOVA + 用户空间 NVK. 但是 NVK 依然仅支持开源驱动 NOVA, 不支持 nvidia-open.

转机

2025 年,Russel Greene 在 mesa 中提交了两个 PR:

旨在在 NVK 中直接强行兼容 NVIDIA-provided 驱动。其中 nvidia.ko 对应服务器和消费级主机显卡,nvgpu.ko 对应 Jetson 边缘设备。我们主要关注前者。

尽管还是草案状态,其已经初步支持了 Turing, Ampere, Backwell 架构显卡,并且能初步运行 vkcube 等程序。

但是稳定性还不足,dmabuf 等支持还没有实现。

Waydroid 受益

Waydroid 通过 mesa 支持主机显卡。

由于此前 NVIDIA 使用 DRI 的方式与 mesa 不兼容,导致 Waydroid 安卓驱动无法利用 N 卡(#1883)。

一旦 NVK 对于 nvidia.ko 的支持成熟,就可以重新打包 mesa 层,实现 N 卡上运行 Waydroid.

不止 binary cache, nixpkgs.git 也可以使用清华源

清华 TUNA 镜像本很提供了 Nix Channels 镜像, 提供了 NixOS 的 binary cache。

问题

但如果使用 Nix flake, 会需要从 GitHub 拉取 nixpkgs,依然需要特殊的网络才能运行。

解决

实际上 TUNA 除了同步 binary, 也同步上游源码。

可以通过以下方式配置 nix flake 也使用清华源:

inputs.nixpkgs.url = "git+https://mirrors.tuna.tsinghua.edu.cn/git/nixpkgs.git";

这样可以在受限网络中使用 Nix flake.

当然 nixos-cuda 等镜像的问题依然无法解决