Bash 历史保留下来的可搜索命令列表

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

命令历史能减少重复输入,却不是所有已执行操作的完整记录。Bash 通常在交互 Shell 中启用历史功能,配置还可以过滤条目或限制保留数量。运行中 Shell 的可搜索列表,与磁盘上的历史文件,是两个独立状态。理解这个区别,才能解释为什么某条命令在一个终端能找到,在另一个终端却没有,以及为什么意外结束会话可能丢掉尚未保存的记录。 会话内存与历史文件不同 HISTSIZE 限制内存中的历史条目数量,HISTFILESIZE 限制保存文件的行数。多行命令与时间戳记录会让行数不等于命令数,不能用简单的文件行数判断究竟保留了多少次操作。HISTFILE 决定保存位置,通常指向用户的 Bash 历史文件。变量未设置或为空时不会正常保存;目标不可写时,当前会话仍可能查到记录,却没有对应的持久化副本。 Bash 在参数展开之前、历史展开之后记录命令文本,并受到 HISTCONTROL 与 HISTIGNORE 的影响。因此,保存下来的变量引用并不一定显示原来执行时的变量值。当前目录、别名、函数、环境及外部文件,也都可能在下次复用命令之前发生变化。历史保存的是有用的输入,不是完整的可复现运行环境,更不是可以直接信任的操作审计证据。 追加保存不会自动刷新其他终端 Bash 正常退出时保存历史,histappend 决定追加到文件还是覆盖文件。如果此前没有主动保存,异常终止可能丢掉全部未写入的条目,不只是最后一条。history -a 追加当前会话新输入、且尚未追加过的历史行。提前保存可以缩小丢失窗口,但不等于承诺任何系统故障之后都能完整恢复,更不能把一次文件写入等同于业务日志的可靠持久化。 另一个仍在运行的 Shell 持有自己的列表,需要通过 history -n 等操作读取新追加的行。仅仅启用追加选项,不会让另一个终端的搜索结果实时更新。history -r 把整个文件内容追加到现有列表,重复运行可能制造重复条目;history -w 则用当前列表替换文件,可能丢掉其他会话留下的记录。这些命令的方向和覆盖语义不同,不能当作几种等价的同步写法。 如果把追加和导入接入 PROMPT_COMMAND,还要保留已有提示符钩子,以及它们对上一条命令退出状态的使用。直接覆盖已有钩子可能破坏其他 Shell 行为。多个会话共享一个文件,也不会因此形成可信的全局执行顺序。需要顺序、退出码或责任归属时,应使用独立的操作日志,而不是从交错的历史文本中反推完整事实。 搜索后先检查,再执行 在常见的 Emacs 风格 Readline 绑定中,Ctrl-R 向后增量搜索,再按一次查找更早的匹配。应使用编辑按键退出搜索,先检查所选命令,再决定是否执行;Ctrl-G 可以取消搜索。回车会接受命令,Ctrl-J 也是换行,不能把它说成不会执行的预览键。其他编辑模式或自定义终端的绑定可能不同,应以当前配置为准。 ...

二月 28, 2028 · 1 分钟 · XB Field Notes
未加引号的展开会悄悄把一个字符串拆成多个参数。

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

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

十二月 31, 2027 · 2 分钟 · XB Field Notes
脚本从命令行收到的、按编号排列的参数。

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

Shell 脚本接收的是有顺序的参数列表,不是需要重新按空格拆分的一整条命令字符串。位置参数通过 $1、$2 等编号暴露这份列表,$# 表示数量。固定且简单的接口可以直接使用位置,带可选设置或开关的接口则适合明确解析。无论采用哪种形式,都需要规定允许几个参数、是否允许空值,以及输入错误时怎样失败。 解释含义之前,先保住列表边界 $0 表示脚本名称等特殊身份,不属于位置参数,也不计入 $#。使用 bash -c 时,命令字符串之后的第一个参数会成为 $0,包装脚本若误以为它是 $1,就会看起来少收到一个参数。超过第九个位置必须使用花括号:${10} 是第十个参数,$10 却是第一个参数再接一个字面数字零,两种写法不能互换。 "$@" 会为每个传入参数保留一个词,包括空字符串和包含空格的值。未引用的展开可能再次分词并展开通配符,"$*" 则会把列表连成一个字符串。转发或遍历时应使用带引号的完整列表。set -- 替换当前的位置参数,shift 删除前面的若干项;这些操作在对应脚本或函数作用域中生效,不会反向改写外部调用者原来的参数列表。 空参数仍然占据一个位置。检查 $# 可以区分完全没有提供参数,与明确传入 '' 两种情况。${1:?message} 同时拒绝缺失和空值,只有接口要求非空时才合适。应先验证数量,再读取必需的位置,最后判断内容是否合法,避免在参数本来不存在时就触发与接口说明无关的展开错误。 getopts 检查语法,业务规则检查含义 可移植的 getopts 处理短选项。选项字符后面的冒号表示该选项需要一个值,但不会自动提供 --user 这类长选项支持。选项说明开头的冒号表示采用安静错误报告:未知选项产生问号,缺少值产生冒号,相关选项字符放在 OPTARG 中。脚本必须明确处理这些分支,不能因为解析循环继续运行就误以为输入合法。 OPTIND 记录解析进度。在同一 Shell 中开始解析另一份参数之前,应把它重置为一;解析完成后,使用 shift "$((OPTIND - 1))" 让剩余操作数成为当前列表。-- 结束选项处理,因此以连字符开头的文件名也能被当作操作数。普通的非选项同样会结束扫描,不能假定写在文件名后面的选项会被自动搬到前面。 ...

十一月 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 模式,不是正则表达式。匹配不到时保留原字符串,匹配成功也不会自动回写原变量。斜杠在这里就是普通的文本内容,这些运算不会查询目录、确认文件存在或者解析符号链接,因此不能用一次字符串截取证明路径安全。 如果结果必须保持一个参数,就给整个展开加双引号。"${name##*/}" 仍然会应用花括号里的模式,删除直到最后一个斜杠的前缀;外层引号保护的是展开结果,不会让内部星号失去模式含义。这些形式属于可移植的 Shell 操作,而下标截取、全局替换等还可能依赖 Bash 扩展。脚本既然用 sh 启动,就不能仅凭作者的交互环境支持某种写法而假定所有机器都支持。 坏掉的样子 未加引号的展开结果可能继续经历分词与文件名展开。花括号只划定变量名,不保护参数边界;变量包含空格或星号时,命令可能收到额外参数,甚至收到当前目录里碰巧匹配的文件名。另外,默认表达式只有在对应分支被选中时才会展开。如果把命令替换放进默认值,那么缺值分支就可能启动进程,不能把这种表达式当成完全没有副作用的常量。 参数展开也不是命令模板系统。从变量得到的分号、引号等文本,不会自动重新解析成 Shell 语法。需要动态参数列表时,在 Bash 中使用数组,或在可移植 Shell 中明确构造位置参数;不要把整条命令存成字符串,再用 eval 解释。set -u 能发现部分未设置变量,却不能判断业务取值是否合理;pipefail 管理管道状态,也不会改变这里的字符串运算规则。 ...

十一月 16, 2027 · 1 分钟 · XB Field Notes
创建权限由请求模式结合进程掩码或继承的默认 ACL 规则确定。

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

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

十月 9, 2027 · 2 分钟 · XB Field Notes
交互 shell 里一个能展开成长命令的短词。

别名免除重复敲击

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

十月 4, 2027 · 1 分钟 · XB Field Notes
命令在启动之前把输出与错误都指向文件。

重定向重新安排流向

重定向在命令执行前改变它的文件描述符。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:后者分别打开同一个文件,两份打开状态拥有独立偏移量,输出可能相互覆盖。即使描述符共享一个目标,应用程序自身的缓冲仍会影响消息真正写出的时间,所以合并日志也不自动保证业务事件严格按发生顺序排列。 管道会先把标准输出连接到下一阶段,然后才应用命令自己的重定向。因此 producer >file | consumer 的生产者把输出交给了文件,消费者反而读不到那些数据。相反,producer 2>&1 | consumer 会把诊断也送进管道。如果下游要求机器可解析的记录,把面向人的错误文本混进去,就可能破坏原本合法的输入格式。 ...

九月 29, 2027 · 1 分钟 · XB Field Notes
一个生产者向消费者持续传数据的连接。

管道让进程彼此流式连接

shell 管道把一个命令的标准输出连接到下一个命令的标准输入。各段命令可以并发运行,生产者不必全部完成后消费者才开始处理。这种连接支持增量处理,却不保证任一程序真的采用增量算法。用户态缓冲、提前结束以及各阶段的退出状态,都会影响整项操作是否达到预期。理解管道时,需要同时观察数据流、文件描述符生命周期和进程结果,不能只看最后一行输出。 有限容量的字节流 管道传输的是字节,不是自带边界的记录。读者不能假设每次读取恰好得到一行,也不能假设一次读取对应生产者的一次写入。需要消息边界的程序必须自己定义和解析。标准错误默认独立于管道连接,因此,上游诊断可能显示在终端,却不会进入下游过滤器。是否合并诊断和数据应当明确决定,避免错误文本污染本来供机器读取的内容。 内核缓冲区容量有限。对于阻塞操作,管道满时写者等待;管道空但仍有写端打开时,读者等待。这种反压限制生产者在这个管道上领先消费者的距离。容量取决于操作系统和配置,并不是普遍固定为六十四千字节。程序自身仍可能维护不断增长的数据结构,因此,使用管道不等于整个处理链的内存已经有界,必须另外检查各段算法。 用户态缓冲还可能引入额外等待。某个程序向终端逐行刷新,改成管道输出后却可能先积累较大的块。此时消费者看起来没有动静,并不代表内核丢了数据;数据可能尚未离开生产者的用户态缓冲。应先检查程序提供的流式处理和刷新控制,再考虑调整内核限制。完整排序等转换本身也需要先看到更多输入,添加随意的睡眠无法改变这种依赖。 坏掉的样子 只有所有指向写端的文件描述符都关闭后,读者才会看到文件结束。如果某个子进程继承了不再使用的写端,即使看起来生产者已经退出,消费者也可能一直等不到结束。反方向上,所有读端关闭后继续写入会产生 SIGPIPE;忽略这个信号时,写操作会以 EPIPE 错误失败。因此,消费者主动提前结束,也可能让原本正常的生产者以非零状态退出。 Bash 默认返回最后一段命令的状态。上游失败、下游 cat 成功读完部分数据时,整体可能仍然成功。开启 pipefail 后,整体返回从左到右最靠右的非零状态,并不是时间上最先失败的进程,也不是最左侧失败命令。PIPESTATUS 数组短暂保留每段结果,应立即保存,因为后面一次赋值或打印就可能替换这份观察记录。 Bash 管道中的阶段通常运行在子 shell 环境。在管道右侧循环里给变量赋值,通常不会修改调用者的变量。lastpipe 选项在关闭作业控制时可以改变最后一段的执行位置,因此依赖状态保留的代码必须明确 shell 配置。需要保留循环状态时,也可以让循环直接通过重定向读取输入,避免把它放进管道带来的作用域变化。 前台 Bash 管道会等待各阶段结束,并不是消费者读到第一行就立即返回。如果生产者捕获断管错误后继续执行其他工作,整体完成时间仍会被拖延。因此,管道应被理解为多个具有独立结果、通过描述符连接的进程,不能假设它们总会像一个不可分割操作一样共同成功或共同停止。 快速验证 下面实验需要 Bash、cat、yes 和 head。它先用主动返回七的生产者比较默认状态和 pipefail,然后构造两段都失败的管道,最后检查提前关闭的消费者。仅在收集预期非零结果时关闭 errexit,取得结果后立即重新开启,再执行断言。 bash --noprofile --norc <<'BASH' set -e set +o pipefail set +e (printf 'partial\n'; exit 7) | cat >/dev/null plain=("$?" "${PIPESTATUS[@]}") set -e [[ ${plain[*]} == '0 7 0' ]] set -o pipefail set +e (printf 'partial\n'; exit 7) | cat >/dev/null strict=("$?" "${PIPESTATUS[@]}") set -e [[ ${strict[*]} == '7 7 0' ]] set +e (exit 7) | (exit 4) multiple=("$?" "${PIPESTATUS[@]}") set -e [[ ${multiple[*]} == '4 7 4' ]] set +e yes | head -n 1 >/dev/null early=("$?" "${PIPESTATUS[@]}") set -e [[ ${early[0]} -ne 0 && ${early[1]} -ne 0 && ${early[2]} -eq 0 ]] printf 'default: %s\npipefail: %s\nrightmost: %s\nearly close: %s\n' \ "${plain[*]}" "${strict[*]}" "${multiple[*]}" "${early[*]}" printf 'PASS: aggregate and individual stage outcomes\n' BASH 每个打印数组的第一项是管道整体状态,后面按源码顺序列出各段状态。前三组预期分别为零七零、七七零、四七四。最后一组中,head 成功,而 yes 返回非零;断言没有硬编码具体信号编号,避免把平台表示方式混入行为要求。生产环境应区分这种主动提前停止与必需输出丢失,不能看到 pipefail 非零就无条件重试全部操作。吞吐量、缓冲延迟和内存峰值仍需独立测量,这些退出状态断言并没有验证性能。 ...

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