写这篇的起因很实在某天我在 tmux 里跑完一个编译任务屏幕上刷了两百多行日志想给同事发过去一起看。我下意识想“全选”然后用鼠标拖了半天窗口一滚动就乱了最后暴躁地放弃了直接截图了事。后来同样的事情又发生在跑测试脚本、看服务启动日志、整理训练输出的时候我实在忍不了认认真真把“tmux 窗格内容全选输出到文件 / 本地剪切板”这件事给捋清楚了。这个标题写的是“水贴自用”但实际做下来里面的弯弯绕绕比想象中多不少tmux 的复制模型、capture-pane 的参数语义、系统剪切板的几种写入方式每一层都有坑。这篇文章就当作一份自用备忘的完整版把我踩过的、试通的、最后沉淀下来的方案都写出来希望能帮到同样被终端复制折腾过的人。1. 为什么“全选一个窗格”在 tmux 里这么别扭刚接触 tmux 的人只要用过一次复制模式大概都会产生一个疑惑为什么 tmux 没有一个像编辑器里 CtrlA 那样的“全选当前窗格内容”的快捷键这个疑惑背后其实是 tmux 对“内容”的定义和普通文本编辑器完全不同。1.1 copy-mode 的复制逻辑不是“选择文本”而是“标记矩形”tmux 的复制模式copy-mode工作在一种非常“老派”的交互模型上按prefix [进入复制模式按空格开始选择按回车把选中的内容放进 tmux 的 buffer内部剪贴板最后用prefix ]粘贴。这个流程在 vi 模式下还可以用 vim 的快捷键操作但本质上它操作的对象是屏幕上的矩形区域而不是一个流式的文本文件。这一点很关键。你在普通编辑器里“全选”逻辑是“从文档开头选到文档结尾中间有换行、有段落、有不连续的行”。但在 tmux 的窗格里内容被渲染成一帧一帧的屏幕快照既包含当前可见区域也包含隐藏在回退缓冲区history里的那些滚动过去的行。复制模式做选择时你实际上是在一块“虚拟屏幕上”划矩形。想用鼠标拖拽或者快捷键选中“从第一行到最后一行的全部内容”首先要确定“第一行”和“最后一行”在哪这个动作在滚动过的屏幕上下文里就很折腾。更别提在分屏多窗格场景下每个窗格有自己的历史边界手动框选很容易把边界的空白行也算进去。1.2 “全选”的本质其实是“抓取这个窗格的完整快照”想通这一点之后我换了个思路在 tmux 里谈“全选”与其用复制模式去模拟桌面软件的交互不如直接用 tmux 提供的命令去抓取窗格当前状态。这就是capture-pane。它能做的事情本质上和“全选”一模一样把窗格里的所有可见内容、连同回退缓冲区里的历史内容作为一个整体提取出来。区别在于它是确定性的、无状态的不会出现选区选到一半、误触导致选区丢失之类的问题。换句话说你在 GUI 里习惯的“全选 → 复制 → 粘贴”在 tmux 里可以拆成“capture-pane 提取 → 写入文件 / 写入系统剪切板”两个动作。后者的可靠性和可重复性远远高于手动画矩形选区。想明白这一点后面所有的配置和脚本就都顺理成章了。2. capture-pane把窗格内容整体落盘的正确姿势capture-pane是 tmux 里一个非常老牌且稳定的命令但日常使用频率不算高。如果你之前没用过先花三十秒看下面这几条命令就能理解它为什么是“全选导出”方案的核心。2.1 基础命令拆解-p、-S、-E 三个参数组合使用先看一个最简单的用法tmux capture-pane -p-p的意思是“把捕获到的内容打印到标准输出”而不是像默认行为那样存进 tmux buffer。运行之后终端会输出当前窗格可视区域的内容。这个时候如果直接重定向到文件tmux capture-pane -p pane.txt你已经完成了“窗格内容输出到文件”这个动作。但这只是捕获了当前可视区域。如果你想要的是“全选”通常还需要把回退历史里的内容也包含进来于是要加两个参数tmux capture-pane -p -S - -E --S -表示起始位置start为负无限即从回退缓冲区的最早一行开始。在 tmux 的语义里负数是回退区中的行-表示“能抓到的最早那一行”。-E -表示结束位置end为负无限即到当前屏幕的最后一行。等等这里有个细节-E -并不是“无限”而是“当前屏幕的底部”。如果你用-E指定一个正数那是从当前屏幕底部往下数的相对偏移-E -是不指定偏移的意思也就是一直捕获到当前窗格可见区域的最后一行。组合起来的含义就是从回退区的第一行一直抓到最后可视行。这才是真正意义上的“窗格内容全选”。注意一个容易混淆的点tmux capture-pane -p不带参数时捕获范围只有可视区域不包含历史。我在很长时间里默认它是包含历史的结果导出的文件总是少一截后来才发现问题出在没加-S -。如果你希望脚本每次都完整导出建议把-S - -E -写成固定搭配。2.2 落盘保存带时间戳的命名与“窗格内联保存”直接重定向到固定文件名看起来没什么问题但实际用起来有个很不舒服的点多次导出会互相覆盖。我习惯在保存时带上时间戳这样每次执行都是一份新的记录time$(date %Y%m%d-%H%M%S) tmux capture-pane -p -S - -E - pane-${time}.txt echo saved to pane-${time}.txt封装成一个函数放到 shell 配置里或者直接写成一个小脚本放到~/.local/bin/下基本能满足日常 90% 的需求。如果你一次性想把所有窗格的内容都导出来可以配合tmux list-panes来循环for pane in $(tmux list-panes -F #{session_name}:#{window_index}.#{pane_index}); do tmux capture-pane -t $pane -p -S - -E - $(echo $pane | tr :. _).txt done-t参数指定目标窗格格式是会话名:窗口号.窗格号。这种批量导出的场景我用的倒不多但偶尔需要把所有窗格状态打包交给别人时比一个个手动复制省太多事。2.3 关于 buffer 和 save-buffer 的另一个路径capture-pane还有一个不带-p的行为把捕获结果存到 tmux 的 buffer 里然后用save-buffer写入文件tmux capture-pane -S - tmux save-buffer -b 0 pane.txt这条路径的好处是它能保留 tmux buffer 的语义方便你接下来继续用paste-buffer粘贴或者通过后面要讲的 OSC 52 把内容推到系统剪切板。但它多了一层中转日常“存文件”场景下我还是推荐-p加重定向一条命令直达出问题的时候好排查。3. 把内容送到系统剪切板buffer、OSC 52 与剪贴板工具“输出到文件”相对简单真正麻烦的是“输出到本地剪切板”。因为 tmux 自己维护的 buffer 不等于系统剪切板你在 tmux 里复制了内容切到浏览器里按 CtrlV 大概率粘不出东西。要打通这一层得先搞清楚三个概念。3.1 先分清 tmux buffer、X11 剪切板、Wayland 剪切板的区别tmux buffertmux 内部的临时存储区存在 tmux 服务进程里。prefix ]粘贴的是这个东西和系统无关。X11 clipboardLinux 桌面下常见的剪切板又细分为 PRIMARY中键粘贴和 CLIPBOARDCtrlC / CtrlV 用。很多剪辑板工具同时操作多个 selection。Wayland clipboard现代 Linux 发行版默认显示协议规则和 X11 不完全一样某些传统工具比如xclip在纯 Wayland 会话下会失效。在本地终端里tmux 进程和系统剪切板都跑在同一台机器上用xclip/wl-copy这类工具直接写入即可。但如果你通过 SSH 连到远程服务器上的 tmux远程机器的剪切板和你本地桌面的剪切板不是一个东西这时上面的所有本地方案全部失效唯一能靠得住的通道是OSC 52 终端转义序列——它由远程 tmux 输出经终端软件解析写入本地系统剪切板。所以接下来的思路是优先配置 OSC 52让它作为通用方案如果环境不支持再退回用剪贴板工具。3.2 OSC 52远程到本地的剪切板“直通通道”OSC 52 的原理不复杂终端收到一条特定格式的转义序列就把其中携带的内容写入系统剪切板。序列格式大致是printf \033]52;c;%s\a $(printf %s $text | base64 | tr -d \n)由于 OSC 52 走的是终端转义通道它不关心你本地的 tmux 是不是在远程机上。只要你的终端软件支持iTerm2、Windows Terminal、现代版 kitty 和 GNOME Terminal 都支持粘贴动作就是透明的。tmux 这边有个现成开关叫set-clipboardset -g set-clipboard on开启后当你用 tmux copy-mode 复制内容到 buffer 时tmux 会尝试通过 OSC 52 把同一份内容推给终端。如果你的终端支持且允许这么干那 tmux 复制就等于系统复制一下把“本地剪切板”和“tmux buffer”拉通了。不过要注意set-clipboard on只对进入 copy-mode 并完成复制这个流程生效。如果你是用capture-pane直接拿到文本想绕过 copy-mode 把内容推进系统剪切板就得手动走一遍 OSC 52 序列。我封装好的用法长这样payload$(tmux capture-pane -p -S - -E -) printf \033]52;c;%s\a $(printf %s $payload | base64 | tr -d \n)用capture-pane拿全部内容base64 编码后包进 OSC 52 序列里发出去。实测下来iTerm2 和 Windows Terminal 都能正确处理某些老旧的终端或者通过浏览器网页终端访问时这招可能没反应那就只能靠下面的本地方案兜底。3.3 本地兜底xclip / xsel / wl-clipboard / pbcopy 怎么选在纯本地或者你有 X11 转发场景写系统剪切板最直接的是一组小工具。不同系统选择不同系统 / 显示协议推荐工具写入命令备注Linux X11xclipxclip -selection clipboard老牌最通用Linux X11xsel--clipboard --input用法略绕但也很常用Linux Waylandwl-clipboard的wl-copywl-copy纯 Wayland 会话下 xclip 会失效macOSpbcopypbcopy系统自带无需安装一个比较稳的写法是写一个tset函数自动探测可用工具然后把 capture-pane 的输出喂给对应命令tmux capture-pane -p -S - -E - | { if command -v wl-copy /dev/null 21; then cat | wl-copy elif command -v xclip /dev/null 21; then cat | xclip -selection clipboard elif command -v pbcopy /dev/null 21; then cat | pbcopy else cat ~/.tmux-last-capture.txt tmux display-message no clipboard tool, saved to ~/.tmux-last-capture.txt fi }我把它写成脚本后放进 tmux 的绑定里调用最终的效果就是按一下快捷键当前窗格全量内容进入系统剪切板。到这一步这个“本地剪切板”的实际可用性已经远超 copy-mode 手动选中了。4. 一条键位绑定搞定“全选→文件剪切板”工具层面都验证没问题之后真正的目标是把这些操作绑到 tmux 快捷键上做到“按一下就好”。下面是我实际使用的最终方案可以直接抄。4.1 最终绑定配置prefix p 存文件prefix y 写剪切板在~/.tmux.conf里增加以下内容# 窗格全量内容导出到文件带时间戳 bind p run-shell time$(date %Y%m%d-%H%M%S); tmux capture-pane -p -S - -E - $HOME/tmux-pane-$time.txt; tmux display-message saved to ~/tmux-pane-$time.txt # 窗格全量内容导入系统剪切板 bind y run-shell tmux capture-pane -p -S - -E - | { command -v wl-copy /dev/null 21 wl-copy || command -v xclip /dev/null 21 xclip -selection clipboard || command -v pbcopy /dev/null 21 pbcopy || cat ~/.tmux-last-capture.txt; }; tmux display-message pane content copied to clipboard注意几点run-shell后面的命令会在一个临时 shell 里执行所以像time这样的变量赋值和管道都必须写在一行内完成。display-message用来在 tmux 状态栏上给出反馈否则你无法确定到底存没存成功。绑定键位因人而异我把prefix p留给“存文件”p 联想到 pane/printprefix y留给“复制到剪切板”y 联想到 yank。只要你自己记得住用什么字母都行。如果想频繁保存日志可以把prefix p改成一个循环追加文件模式的版本比如不要覆盖而是每次带上窗格编号pane_id$(tmux display-message -p #{session_name}:#{window_index}.#{pane_index}) tmux capture-pane -p -S - -E - $HOME/tmux-log-${pane_id//[:.]/_}-$(date %s).txt4.2 为什么不建议用 copy-mode 做“全选”有人可能会问既然set-clipboard on之后 copy-mode 也能写系统剪切板那手动进 copy-mode 全选一遍不也一样吗理论上可以但实际体验很差。首先“全选”不是 copy-mode 下的一键动作。你需要先prefix [进去然后如果开启了 vi 模式得按gg跳到最顶行再ShiftV开启行选择移到最末行再按y复制。这一串操作里任何一步误触都可能导致选区错乱。而且如果一个窗格的历史回退区特别长光标移动路径会非常长视觉上很难判断终点位置。相比之下capture-pane的脚本方案是完全确定性的不管窗格有多少行、历史有多深它做的就是“取全部”不会有选择边界、滚动位置这类变量。自动化、批量处理、反复触发时脚本方案的优势是碾压性的。4.3 脚本化边界情况处理实际用下来有几个边界情况值得注意窗格完全空白capture-pane -p输出为空文件或空字符串。我的脚本会正常生成一个 0 字节文件不会报错但为了察觉这种状况可以在脚本里加一句判断content$(tmux capture-pane -p -S - -E -) if [ -z $content ]; then tmux display-message pane empty else echo $content file fi回退缓冲区被限制tmux 的 history-limit 默认通常是 2000 行如果窗格内容早就翻过了这个阈值capture-pane -S -也只能抓到缓冲区内保留的行拿不到真正最早的行。需要长期保存完整个会话历史的话可以在启动 tmux 时加大限制set -g history-limit 10000多窗格循环的竞态批量循环导出时如果某个窗格正在被快速刷屏比如跑日志两次 capture 之间数据可能不一致但这属于正常情况单次快照本来就是“某时刻的状态”。5. 实战排坑历史内容、中文编码、以及 gcc 日志场景的存档模板把基本方案跑通之后我在真实使用里陆续碰到过几个问题这里单独列一节每一个都是血泪教训。5.1 终端滚动缓冲区 vs tmux 回退区的差异有一个很常见的误解以为 tmux 窗格里的内容就是终端仿真器整个缓冲区里的内容。实际上当你进入 tmux 后终端仿真器自身的滚动缓冲区基本就被 tmux 接管了tmux 窗格的“回退缓冲区”是一个独立存储和终端模拟器的历史不相通。这带来两个影响在 tmux 里滚鼠标滚轮滚的是 tmux 的回退区不是终端模拟器本身的缓冲区如果capture-pane -S -拿不到你预期的最早内容先检查的是history-limit够不够大而不是终端模拟器的历史设置。我曾经遇到过一个问题在 tmux 里跑了很长的构建任务等它刷了几千行之后想回头找最早的一个报错结果回退区只剩最后 2000 行罪魁祸首就是默认 history-limit。后来在配置文件里写入set -g history-limit 20000重启 tmux 服务之后才彻底解决。如果你已经开着 tmux修改 history-limit 只对新建的窗口和窗格生效旧窗格的缓冲区不会自动扩容——这是个容易让人白折腾半天的点。5.2 中文和特殊字符导致的问题中文用户最容易踩的坑是保存下来的文件用编辑器打开时乱码或者剪切板贴出来的内容和终端显示不一致。这倒不是 tmux 本身的锅根源在 locale。tmux 捕获的是终端上的明文如果终端显示的是 UTF-8那 capture-pane 拿到的也是 UTF-8 字节流。但如果你通过 SSH 连接到远程服务器而远程环境的默认 locale 不是 UTF-8终端展示和 tmux 内部的编码就可能被转义序列影响导致捕获结果和肉眼所见不符。我的规避方法很简单在远程主机的~/.bashrc或~/.zshrc里显式设置export LANGen_US.UTF-8或C.UTF-8保证终端和应用层使用统一的 UTF-8。导出文件时不要偷懒用系统默认名文件名里也别有中文避免存档管理时的编码问题。如果内容里有特殊字符比如 ANSI 颜色转义序列capture-pane默认是不剥离这些转义码的。想要纯文本可以加-e的反向操作tmux capture-pane -p -e -S - -E -会保留转义序列而默认不带-e的版本才是去掉颜色的纯文本。习惯上我默认不带-e因为日志文件要去色才好检索。顺带一提如果你想把 ANSI 颜色去掉再保存简单方案是管道给sed过滤tmux capture-pane -p -S - -E - | sed s/\x1b\[[0-9;]*m//g clean.txt5.3 结合起来看gcc 编译日志输出到文件的场景模板聊到“输出到文件”我最近其实最常干的一件事是把 gcc 日志存下来。平时编译 C/C 项目标准操作是gcc -o app main.c 21 | tee build.log这能同时把日志显示在终端并写入文件。但如果你是在 tmux 的某个窗格里编译编译结束之后还想保存“终端上此刻显示的全部内容”tee就帮不上忙了因为它的管道只针对本次命令的 stdout/stderr而 tmux 窗格里可能还留着之前几条命令的输出。这时候用 capture-pane 反而更符合直觉编译完我按一下prefix p当前窗格从最早的cd命令到最后的编译结果全量落入文件过程一气呵成。我目前比较常用的一个模板是把prefix p绑定扩展一下同时完成三件事prefix p - 保存当前窗格快照到带时间戳的文件 prefix y - 保存当前窗格快照到系统剪切板 prefix P - 批量保存当前窗口所有窗格的快照最终.tmux.conf里相关段落长这样# 保存当前窗格快照到文件 bind p run-shell f$HOME/pane-snapshot-$(date %Y%m%d-%H%M%S).txt; tmux capture-pane -p -S - -E - $f; tmux display-message saved: $f # 保存当前窗格快照到系统剪切板 bind y run-shell tmux capture-pane -p -S - -E - | { command -v wl-copy /dev/null 21 binwl-copy || command -v xclip /dev/null 21 binxclip; if [ -n $bin ]; then tmux capture-pane -p -S - -E - | xargs -0 sh -c for arg do printf %s \$arg\; done | $bin; else tmux display-message no clipboard tool; fi } # 批量保存当前窗口所有窗格 bind P run-shell for p in $(tmux list-panes -F #{window_index}.#{pane_index}); do f$HOME/pane-$p-$(date %H%M%S).txt; tmux capture-pane -t $p -p -S - -E - $f; done; tmux display-message all panes savedprefix P这个绑定我在多窗格调试时用得非常多左边窗格跑服务器右边窗格跑测试下面窗格看日志。调完一轮按一下三个窗格的状态全部落到文件里复盘、贴给别人、标注时间线都方便。5.4 我在这个方案里沉淀下来的几条小经验最后补充几条纯个人体会不一定都写在文档里但实战价值不小。第一给 tmux 绑定键位时run-shell 里的脚本尽量写成一行别急着拆成多行。tmux 的 run-shell 命令在双引号内执行 shell 脚本换行和多行逻辑很容易被格式化问题搞得莫名其妙。复杂逻辑一律写成独立脚本放在~/.local/bin/然后绑定只留一句简单的调用。我一开始图省事把整段代码塞进 .tmux.conf结果调试花的时间比重写脚本还多。第二capture-pane 捕获的是“当前屏幕状态”不是“命令输出流”。如果你需要的是某个进程从开始到结束的完整 stdout更稳的是事先用 tee 或重定向记录而不是事后从 tmux 窗格里捞。两者适合的场景不同实时任务用 tee 保驾护航事后复盘用 capture-pane 补漏。第三OSC 52 和剪贴板工具不要同时配置成强制依赖。我在一台老机器上只装了 xclip没有 wl-copy后来把环境切到 Wayland 后整个绑定长时间处于静默失败状态输出文件倒是正常生成剪切板却毫无反应。后来改成自动探测工具链才算是真正一劳永逸。最后再提醒一句如果你经常需要保存大量终端输出history-limit这类的 tmux 全局配置改动立刻生效的最好方式是重启 tmux server。不重启也行但旧窗格的历史不会自动扩容这个行为很容易误导人排查半天别问我怎么知道的。总的来说这套“capture-pane 全量导出 文件 / 剪切板双通道”方案现在已经是我 tmux 配置里离不开的一组绑定了。哪天你也被终端复制粘贴逼到想砸键盘不妨照着上面的思路自己搭一套多余的折腾都可以省掉。