两个独立 shell 历史列表通过 HISTFILE 连接:会话 A 用 -a 追加,会话 B 用 -n 导入。

bash history 是可以搜索的命令日志

命令历史能减少重复输入,却不是所有已执行操作的完整记录。Bash 通常在交互 Shell 中启用历史功能,配置还可以过滤条目或限制保留数量。运行中 Shell 的可搜索列表,与磁盘上的历史文件,是两个独立状态。理解这个区别,才能解释为什么某条命令在一个终端能找到,在另一个终端却没有,以及为什么意外结束会话可能丢掉尚未保存的记录。 每个运行中的 shell 都有独立历史列表,只有显式文件操作才连接这些状态。追加不会刷新另一会话,读入整个文件可能产生重复,替换则可能丢失其他会话记录。共享历史不是可信的全局执行日志。 会话内存与历史文件不同 HISTSIZE 限制内存中的历史条目数量,HISTFILESIZE 使用按行计算的目标控制文件保留。当前 Bash 手册允许为了保留完整多行条目而超过这一目标,避免留下半条记录。多行命令与时间戳记录也会让原始文件行数不等于命令数,不能据此直接判断保留了多少次操作。HISTFILE 决定保存位置,通常指向用户的 Bash 历史文件。变量未设置或为空时不会正常保存;目标不可写时,当前会话仍可能查到记录,却没有对应的持久化副本。 Bash 在参数展开之前、历史展开之后记录命令文本,并受到 HISTCONTROL 与 HISTIGNORE 的影响。因此,保存下来的变量引用并不一定显示原来执行时的变量值。当前目录、别名、函数、环境及外部文件,也都可能在下次复用命令之前发生变化。历史保存的是有用的输入,不是完整的可复现运行环境,更不是可以直接信任的操作审计证据。 追加保存不会自动刷新其他终端 Bash 正常退出时保存历史,histappend 决定追加到文件还是覆盖文件。如果此前没有主动保存,异常终止可能丢掉全部未写入的条目,不只是最后一条。history -a 追加当前会话新输入、且尚未追加过的历史行。提前保存可以缩小丢失窗口,但不等于承诺任何系统故障之后都能完整恢复,更不能把一次文件写入等同于业务日志的可靠持久化。 另一个仍在运行的 Shell 持有自己的列表,需要通过 history -n 等操作读取新追加的行。仅仅启用追加选项,不会让另一个终端的搜索结果实时更新。history -r 把整个文件内容追加到现有列表,重复运行可能制造重复条目;history -w 则用当前列表替换文件,可能丢掉其他会话留下的记录。这些命令的方向和覆盖语义不同,不能当作几种等价的同步写法。 ...

二月 28, 2028 · 1 分钟 · XB Field Notes
未引用的 report final.md 拆成两个参数,引用展开则保留一个。

Shell 的字段拆分把未加引号的变量拆成多个参数

变量保存的是一个字符串,但没有引号的展开可能把它变成多个命令参数。Bash 的分词阶段使用 IFS 指定的分隔字符检查展开结果,因此,手动引用后能正常使用的路径,放入变量再传递时却可能突然找不到文件。排查时首先应观察接收命令实际拿到的参数数量和每个参数的内容,也要区分空字符串与完全没有参数,不能仅凭终端上看起来相同的一行文本判断输入一致。 IFS 控制分词,而引用还能阻止文件名展开。空参数和没有参数仍是不同情况。 是否分词取决于上下文 默认 IFS 包含空格、制表符和换行符,它们的连续组合会分隔字段,开头和结尾的这类空白也会被忽略。未引用变量里有两个单词时,命令通常收到两个参数;变量为空时,则通常连一个参数都没有。使用 "$value" 会保留一个参数,即使值是空字符串。对于命令行接口,显式传入空值和省略选项值可能触发完全不同的行为。 分词针对适用上下文中的未引用参数展开、命令替换和算术展开结果,并不是重新解析整条命令。普通赋值语句 copy=$value 会保留原字符串,不执行分词和文件名展开。但随后把 $copy 不加引号地作为命令参数使用,仍会重新引入两个问题。Bash 双方括号条件表达式也具有不同于普通命令参数的展开规则,所以赋值或条件判断成功,不能证明后面的外部程序调用也安全。 IFS 是一组分隔字符,不是多字符分隔串,也不是正则表达式。非空白分隔符和空白的处理并不相同,例如连续两个冒号可以产生一个空字段,而连续默认空白通常只是一个字段边界。把 IFS 设为空字符串会关闭分词,但不会把未引用的空展开变成显式空参数。若数据格式有引号、转义或嵌套结构,应使用理解该格式的解析器,不能把调整 IFS 当作通用解析方法。 坏掉的样子 关闭分词并没有关闭随后的文件名展开。即使 IFS 为空,变量里的星号仍可能匹配当前目录下的文件;目录里刚增加一个文件,同一条命令的参数数量就会改变。引用变量能够同时阻止这两种转换,而修改 IFS 只处理了其中一个阶段。控制通配符的 shell 选项也独立存在,不能把它们和分词配置混为一谈。 ...

十二月 31, 2027 · 2 分钟 · XB Field Notes
$0 身份单独保留;shift 移除第一个空位置参数,让 two words 从 $2 移到 $1。

Shell 位置参数是一份有编号的清单

Shell 脚本接收的是有顺序的参数列表,不是需要重新按空格拆分的一整条命令字符串。位置参数通过 $1、$2 等编号暴露这份列表,$# 表示数量。固定且简单的接口可以直接使用位置,带可选设置或开关的接口则适合明确解析。无论采用哪种形式,都需要规定允许几个参数、是否允许空值,以及输入错误时怎样失败。 先保留参数列表,再解释含义。语法解析与业务验证是两道独立检查;重复调用需要新的局部解析状态。此图表示原文实验预期,本轮未重新执行。 解释含义之前,先保住列表边界 $0 表示脚本名称等特殊身份,不属于位置参数,也不计入 $#。使用 bash -c 时,命令字符串之后的第一个参数会成为 $0,包装脚本若误以为它是 $1,就会看起来少收到一个参数。超过第九个位置必须使用花括号:${10} 是第十个参数,$10 却是第一个参数再接一个字面数字零,两种写法不能互换。 "$@" 会为每个传入参数保留一个词,包括空字符串和包含空格的值。未引用的展开可能再次分词并展开通配符,"$*" 则会把列表连成一个字符串。转发或遍历时应使用带引号的完整列表。set -- 替换当前的位置参数,shift 删除前面的若干项;这些操作在对应脚本或函数作用域中生效,不会反向改写外部调用者原来的参数列表。 空参数仍然占据一个位置。检查 $# 可以区分完全没有提供参数,与明确传入 '' 两种情况。${1:?message} 同时拒绝缺失和空值,只有接口要求非空时才合适。应先验证数量,再读取必需的位置,最后判断内容是否合法,避免在参数本来不存在时就触发与接口说明无关的展开错误。 ...

十一月 29, 2027 · 2 分钟 · XB Field Notes
未设置和空字符串都进入带冒号的默认分支;去掉冒号则保留空字符串。

Shell 参数展开产生替代值

参数展开根据 Shell 参数产生一个值,随后命令才收到展开后的参数。它可以提供默认值、拒绝缺失配置,或者删除字符串的一部分,不必为这些操作启动额外进程。但“执行命令前展开”并不等于“初次解析脚本时就求值”:函数在被调用时读取变量当前的内容,函数定义之后修改变量,会影响后续调用。区分解析、展开和执行,才能解释为什么相同的表达式在不同时间产生不同结果。 冒号把空字符串也计入缺值条件。普通默认值不改保存状态,赋值默认值会回写。Shell 模式删除改变结果,不改原变量,也不访问文件系统。 默认值要区分未设置与空字符串 ${setting:-default} 在变量未设置或者为空时使用默认值;${setting-default} 只处理未设置的情况,保留调用者明确传入的空字符串。冒号改变的是配置语义,不能当成随意省略的标点。例如空字符串可能表示关闭某个前缀,擅自用默认值替换就会改变用户的选择。${setting:+enabled} 只在变量已设置且非空时产生替代文本,去掉冒号后,已设置但为空也满足条件。 普通默认值展开不会修改变量本身。${setting:=default} 才会在选中默认分支时把结果存进变量,之后再读取就能看到新值。${setting:?message} 则在变量未设置或为空时把诊断写到标准错误,并让非交互 Shell 以非零状态退出。去掉冒号后,这两个运算符也只检查是否设置。应当先确定调用约定:允许为空、允许省略,还是必须提供,然后选择相应形式,而不是把所有配置问题都掩盖成一个默认值。 花括号还负责把变量名与相邻文本分开。"${prefix}_suffix" 是给变量内容追加后缀,"$prefix_suffix" 却读取另一个变量。用几个配置拼文件名时,看似缺值的问题可能只是变量名边界写错。应分别检查各个组成部分,再排查最终路径;一个语法完全合法的字符串,也可能混入来自错误变量的内容。 模式处理字符串,不访问文件 ${name#pattern} 与 ${name##pattern} 分别删除最短和最长匹配前缀;对应的后缀运算符是 % 与 %%。这里使用 Shell 模式,不是正则表达式。匹配不到时保留原字符串,匹配成功也不会自动回写原变量。斜杠在这里就是普通的文本内容,这些运算不会查询目录、确认文件存在或者解析符号链接,因此不能用一次字符串截取证明路径安全。 ...

十一月 16, 2027 · 1 分钟 · XB Field Notes
请求模式按普通 umask 过滤或默认 ACL 继承分支;既有 inode 使用独立路径。

umask 过滤新建权限,默认 ACL 会改变规则

进程的 umask 在普通模式位创建规则下,为新文件和目录移除权限位。应用先提供请求模式,掩码再限制这份请求。它既不是完整访问策略,也不会重写所有既有文件。如果父目录具有默认访问控制列表,创建计算方式还会改变,因此只检查进程掩码,可能无法准确预测最后得到的权限。排查时需要同时看到应用请求和目录环境。 坏掉的样子 没有默认 ACL 时,普通权限结果是请求模式与 umask 按位取反后的值进行按位与。例如请求 0666、掩码 0022,得到 0644。这个计算不是算术减法:相同掩码下请求 0600,仍然得到 0600。应用没有请求执行权限时,宽松掩码也不会主动添加执行位;掩码负责移除,而不是凭空授予应用未请求的普通权限。 限制严格的 umask 不会修复已经存在的全员可写文件。打开并截断该文件,会改变内容,却保留既有模式。后续 chmod 也可以不受当前掩码限制地扩大权限。因此,把 umask 当作权限永远不能超过的上界,会遗漏既有文件与之后权限修改两类情况。必须区分文件首次创建,以及此后围绕同一 inode 发生的操作。 父目录具有默认 ACL 时,Linux 从它派生新对象的访问 ACL,再按应用请求模式限制权限,不再按普通方式应用进程 umask。因此,即使创建者掩码是 0077,共享目录里仍可能生成组可访问文件。遇到与预期不同的模式时,应检查继承的 ACL,而不能只看到严格掩码就断言内核没有遵守权限设置。 普通掩码与默认 ACL 继承是不同创建分支。已有文件截断与后续 chmod 另行处理;案例属于实验预期。 快速验证 把脚本保存为 mask.py,在安装 Python 3 和 setfacl 的 Linux 环境中运行 python3 mask.py。应使用获准且没有既有默认 ACL 的临时目录基址,所在文件系统须支持默认 ACL,并启用断言。历史文章报告 Python 3.11.2、acl 2.3.1,以及先在 tmpfs 被拒绝、再在 ext4 执行的过程;但未找到原始日志,本轮没有更改存储或重跑。该次拒绝不代表所有 tmpfs 配置都不支持 ACL: ...

十月 9, 2027 · 2 分钟 · XB Field Notes
Bash 读取输入时,命令词 ll 展开为 ls -l;带引号的 two words 参数仍随命令保留。

别名免除重复敲击

别名在 Bash 读取命令时,把命令位置的一个词替换成一段 Shell 文本。alias ll='ls -l' 这种个人快捷方式很方便,但别名并不是一个可执行文件,也没有独立的位置参数作用域。非交互 Bash 默认关闭别名展开,脚本却可以明确启用它;“默认不展开”与“脚本中绝对不能使用”是两回事。理解这个区别,才能解释为什么终端里好用的简称到了自动化环境就找不到。 别名在读取输入时替换。调用者的引用可以保留,但别名没有独立参数作用域;已解析的函数可能保留旧的替换文本。此图呈现原文解释与预期行为,本轮没有重新运行示例。 替换发生在读取输入时 通常,命令位置上没有被引用的词会被检查是否需要别名展开。定义 ll 后,ll 'two words' 仍会把带空格的文本作为一个参数传给命令,不能笼统地说别名必然破坏参数引用。真正的限制是:别名没有属于自己的 $1,也不能像函数那样拥有并重新排列一组独立的参数。调用者后面追加的参数,只是继续参与替换后的命令解析。 替换文本甚至可以包含管道或多条命令,因此别名在语法上并不局限于单一程序。代价是读者更难直接看出简称背后的行为。Bash 会继续检查替换文本的第一个词,但不会再次展开当前正在展开的同名别名,所以 alias ls='ls -F' 不会无限递归。还有一个容易忽略的规则:别名值末尾如果留有空白,紧随其后的词即使位于参数位置而非命令名称位置,也会接受别名检查,不应把尾部空格当成无意义的排版。 定义本身的引号还决定另一层求值时机。单引号让美元符号保留到替换文本以后被解释;双引号却可能在定义别名时就展开变量。原本想跟随目录变量变化的快捷方式,于是可能固定住旧值。遇到这种差异时,应检查实际保存的别名定义,而不只是回看自己最初输入的那一行。 ...

十月 4, 2027 · 1 分钟 · XB Field Notes
对于 >log 2>&1,第一个状态让描述符 1 指向日志、2 留在原处,第二个状态让两者都指向日志。对于 2>&1 >log,第一个状态让两者引用原标准输出目标,之后只有 1 移到日志。复制不会建立动态绑定。

重定向重新安排流向

重定向在命令执行前改变它的文件描述符。Shell 从左到右处理每一项,遇到打开文件或复制描述符的操作,就在那个位置完成相应变更。描述符零、一、二通常分别表示标准输入、标准输出和标准错误。把命令行读成对这些句柄的顺序操作,就能解释诊断为什么去了错误的位置,也能解释文件为什么还没有被程序读取就已经变空。 从左到右处理重定向。每次复制保留当时的目标,打开文件本身就可能在命令开始前改变数据。 打开文件本身就可能改变数据 >output 打开输出文件,并且通常把已存在的普通文件截断为零长度;>>output 使用追加方式,<input 则打开文件供读取。这些动作由 Shell 完成,不是命令所指程序的内部行为。如果必须打开的目标失败,命令不会执行,但此前已经成功的重定向仍可能创建或截断其他文件。不能把“程序没有启动”理解成“整条命令完全没有副作用”。 因此,sed expression input >input 会在转换程序读取数据之前破坏原文件。即便先写 <input 打开输入,后续重定向截断同一个底层文件,也不能保住那些内容。应写入独立临时文件,检查转换状态与结果,再根据文件归属、权限和所在文件系统,明确执行替换步骤。输入输出恰好同名时,不能指望 Shell 自动替你实现安全的原地编辑。 复制记住的是当下的目标 >log 2>&1 先让描述符一指向日志,再让描述符二引用同一个已打开目标。2>&1 >log 则先复制原来的标准输出,之后只改变描述符一,因此标准错误仍留在原目标,例如终端或负责捕获输出的管道。这里复制的是描述符引用,不是文件内容,也不是给两个数字建立一个会随之后续变化的动态连接。 Bash 的 &>log 可以简写同时重定向两条流,而 >log 2>&1 也适用于可移植 Shell。不要把它写成 >log 2>log:后者分别打开同一个文件,两份打开状态拥有独立偏移量,输出可能相互覆盖。即使描述符共享一个目标,应用程序自身的缓冲仍会影响消息真正写出的时间,所以合并日志也不自动保证业务事件严格按发生顺序排列。 ...

九月 29, 2027 · 1 分钟 · XB Field Notes
生产者把标准输出通过有限容量的字节管道送到并发消费者的标准输入;标准错误另行流向终端。程序自身的缓冲仍可能延迟字节进入管道。

管道让进程彼此流式连接

shell 管道把一个命令的标准输出连接到下一个命令的标准输入。各段命令可以并发运行,生产者不必全部完成后消费者才开始处理。这种连接支持增量处理,却不保证任一程序真的采用增量算法。用户态缓冲、提前结束以及各阶段的退出状态,都会影响整项操作是否达到预期。理解管道时,需要同时观察数据流、文件描述符生命周期和进程结果,不能只看最后一行输出。 管道在进程间传输字节。缓冲、所有打开端点的生命周期、每个阶段的退出结果,需要分别理解。 有限容量的字节流 管道传输的是字节,不是自带边界的记录。读者不能假设每次读取恰好得到一行,也不能假设一次读取对应生产者的一次写入。需要消息边界的程序必须自己定义和解析。标准错误默认独立于管道连接,因此,上游诊断可能显示在终端,却不会进入下游过滤器。是否合并诊断和数据应当明确决定,避免错误文本污染本来供机器读取的内容。 内核缓冲区容量有限。对于阻塞操作,管道满时写者等待;管道空但仍有写端打开时,读者等待。这种反压限制生产者在这个管道上领先消费者的距离。容量取决于操作系统和配置,并不是普遍固定为六十四千字节。程序自身仍可能维护不断增长的数据结构,因此,使用管道不等于整个处理链的内存已经有界,必须另外检查各段算法。 用户态缓冲还可能引入额外等待。某个程序向终端逐行刷新,改成管道输出后却可能先积累较大的块。此时消费者看起来没有动静,并不代表内核丢了数据;数据可能尚未离开生产者的用户态缓冲。应先检查程序提供的流式处理和刷新控制,再考虑调整内核限制。完整排序等转换本身也需要先看到更多输入,添加随意的睡眠无法改变这种依赖。 原子写入与缓冲容量是不同属性。Linux 对不超过 PIPE_BUF 的适当写入提供不与其他写者交错的保证,并有阻塞模式条件;这仍然不会为读者建立消息边界。几次短写入可能被一次读取取得,一次写入也可能分几次才读完,不能把某次观察到的块大小当作解析协议。 坏掉的样子 只有所有指向写端的文件描述符都关闭,并且缓冲中的剩余字节已经读完,读者才会看到文件结束。关闭写端不会丢弃已经进入管道的数据。如果某个子进程继承了不再使用的写端,即使看起来生产者已经退出,消费者也可能一直等不到结束。反方向上,所有读端关闭后继续写入会产生 SIGPIPE;忽略这个信号时,写操作会以 EPIPE 错误失败。因此,消费者主动提前结束,也可能让原本正常的生产者以非零状态退出。 Bash 默认返回最后一段命令的状态。上游失败、下游 cat 成功读完部分数据时,整体可能仍然成功。开启 pipefail 后,整体返回从左到右最靠右的非零状态,并不是时间上最先失败的进程,也不是最左侧失败命令。PIPESTATUS 数组短暂保留每段结果,应立即保存,因为后面一次赋值或打印就可能替换这份观察记录。 Bash 管道中的阶段通常运行在子 shell 环境。在管道右侧循环里给变量赋值,通常不会修改调用者的变量。lastpipe 选项在关闭作业控制时可以改变最后一段的执行位置,因此依赖状态保留的代码必须明确 shell 配置。需要保留循环状态时,也可以让循环直接通过重定向读取输入,避免把它放进管道带来的作用域变化。 ...

九月 28, 2027 · 1 分钟 · XB Field Notes