- CLI
- 开发工具
【免费下载链接】fish-shell
The user-friendly command line shell.
导读
本文围绕 fish-shell 提供的umask内置命令展开,系统讲解文件创建模式掩码(umask)的八进制与符号两种表达方式、-S/--symbolic、-p/--as-command、-h/--help三个选项的用法,并深入其实现:umask在 fish 中是一个由进程实时计算的“电子变量”(electric variable),其设置最终会调用系统umask(2)系统调用;符号模式则由 share/functions/umask.fish 中定义的 fish 函数负责解析与换算。读完本文,你将掌握 umask 的工作原理、fish 特有的变量模型,以及如何通过 tests/checks/umask.fish 中的测试用例验证各种边界行为。
什么是 umask
umask(file creation mode mask,文件创建模式掩码)用于限制新建文件与目录的默认访问权限。当程序(包括 shell 的重定向、touch、mkdir等)创建文件时,系统会从请求的权限中减去掩码中标记的位,从而保证新文件不会意外获得过宽的权限。
fish 官方文档明确指出:本文档描述的是 fish 内置的umask;如果想查看非 fish 版本(如 bash 自带的同名校内建命令)的手册,请使用command man umask(见 doc_src/cmds/umask.rst)。
umask 有两种表达方式:
- 八进制数字:表示“默认会被移除的权限位”。例如
umask 027表示对组移除写权限(2)、对其他用户移除读写执行全部权限(7)。 - 符号形式:表示“默认会被授予的权限位”,即要保留的权限。
访问权限(读r、写w、执行x,以及u/g/o三类主体)的完整说明参见chmod(1)手册页。
语法与选项
umask [OPTIONS] [MASK]不带任何参数时,umask以八进制数字形式打印当前文件创建模式掩码。
| 选项 | 说明 |
|---|---|
-S/--symbolic | 以符号形式(如u=rwx,g=rx,o=)而非八进制形式打印掩码 |
-p/--as-command | 以可重新作为输入使用的命令形式输出(如umask 0027) |
-h/--help | 显示该命令的使用帮助 |
在 share/functions/umask.fish 的实现中,这三个选项通过argparse -n umask h/help p/as-command S/symbolic -- $argv解析,其中h/help对应-h/--help,p/as-command对应-p/--as-command,S/symbolic对应-S/--symbolic。
八进制掩码的语义
传入一个八进制数字作为参数时,当前 shell 的 umask 会被设置为该值,此后新建文件与目录默认会移除该掩码指定的权限。例如:
umask 177177的每一位含义如下(1即二进制001,表示移除执行位x;7即111,表示移除读、写、执行全部权限):
1:属主(u)移除执行权限;7:属组(g)移除全部权限;7:其他用户(o)移除全部权限。
文档给出的等价示例是umask 177或umask u=rw:二者都将文件创建掩码设置为“属主可读可写,其他任何用户没有任何权限”。
数值合法性与底层校验
八进制掩码不是任意数字都能生效。在 fish 的 Rust 实现中,umask是一个可写但值由进程实时计算的电子变量(见 src/env/impl/var.rs 中的writable_var(L!("umask"), Computed(GET_UMASK)))。当用户通过set -g umask ...或umask函数设置它时,会触发 src/env/impl/environment.rs 中的set_umask:
fn set_umask(list_val: &[WString]) -> EnvStackSetResult { if list_val.len() != 1 || list_val[0].is_empty() { return EnvStackSetResult::Invalid; } let Ok(mask) = fish_wcstol_radix(&list_val[0], 8) else { return EnvStackSetResult::Invalid; }; if mask > 0o777 || mask < 0 { return EnvStackSetResult::Invalid; } // Do not actually create a umask variable. On env_stack_t::get() it will be calculated. umask(Mode::from_bits(mask as libc::mode_t).unwrap()); EnvStackSetResult::Ok }这里有两处关键校验:
- 值必须以 8 为基数解析(
fish_wcstol_radix(&list_val[0], 8)),因此umask 228、umask 0282、umask 1234这类包含非法八进制数字的值都会被拒绝; - 解析后的数值必须在
0..=0o777(即 0 到 511)范围内,超出范围同样返回Invalid。
校验通过后,fish 会直接调用 nix 封装下的系统调用umask(2)(umask(Mode::from_bits(mask as libc::mode_t).unwrap()))完成真实掩码的设置,并且不会真正创建名为umask的变量——该变量的值始终在读取时动态计算。这一点被 tests/checks/set.fish 的检查项印证:set umask abc会得到错误set: Tried to modify the special variable 'umask' to an invalid value。
读取掩码:电子变量与 GET_UMASK
当不带参数执行umask时,fish 函数读取$umask变量。这个值并非存储的普通变量,而是由GET_UMASK这个 Getter 实时生成的(见 src/env/impl/var.rs):
const GET_UMASK: Getter = |_env| { // note umask() is an absurd API: you call it to set the value and it returns the old // value. Thus we have to call it twice, to reset the value. The env_lock protects // against races. Guess what the umask is; if we guess right we don't need to reset it. let guess = Mode::S_IWGRP | Mode::S_IWOTH; let res = umask(guess); if res != guess { umask(res); } EnvVar::new_from_name(L!("umask"), sprintf!("0%0.3o", res.bits())) };这段代码值得细读,它揭示了 POSIXumask(2)的一个著名“怪癖”:调用它设置新值的同时会返回旧值,因此查询当前掩码必须先把猜测值设置进去、再恢复原值。fish 的策略是:
- 猜测当前掩码为
S_IWGRP | S_IWOTH(即八进制022,组与其他用户均屏蔽写权限); - 调用
umask(guess)获得真实旧值res; - 若
res != guess,则立即调用umask(res)恢复原值,避免竞态窗口内掩码被篡改; - 将
res格式化为带前导零的四位八进制字符串(sprintf!("0%0.3o", ...),如0027),作为变量值返回。
因此echo $umask输出的总是形如0027的四位八进制字符串,同时读操作本身不会改变掩码。umask因此被 fish 归为“电子变量”(electric variable)体系:它与其他变量一样可读可写,但值由进程状态计算而来,且写入时会被set_umask拦截并转发给内核。
符号掩码的语义
如果传入符号掩码,则直接指定期望的权限位,而不是其反掩码。符号掩码是一个逗号分隔的权利列表,每个权利由三部分组成:
- 适用对象(who):
u(文件属主)、g(文件属组)、o(其他用户)或a(三者全部)。 - 操作模式(mode):
=(将权限设置为新值)、+(在原有基础上追加指定权限)、-(从原有基础上移除指定权限)。 - 权限位(perms):
r、w、x的任意组合,分别表示读、写、执行。
如果省略第一、二部分,则分别默认视为a和=。文档给出的示例是r,u+w,等价于a=r,u+w,含义是:所有用户获得读权限,文件属主额外获得写权限。
注意:fish 官方文档明确提示“符号掩码目前并不能按预期工作”(Note that symbolic masks currently do not work as intended)。原因可以从实现中看出:符号模式的解析与换算全部由 fish 函数 share/functions/umask.fish 完成(而非内核),它与 bash 语义存在已知偏差;同时 tests/checks/umask.fish 的注释也承认,其期望输出“全部基于 bash 执行相同命令时的行为”逐条手工核对而来。因此生产脚本中若对权限有强要求,建议优先使用八进制形式,或先用umask -S/ 测试用例验证符号模式在你所用版本上的实际表现。
符号模式的解析流程
share/functions/umask.fish 中的__fish_umask_parse是符号掩码的核心解析器,流程如下:
- 八进制捷径:先用
string match -qr '^0?[0-7]{1,3}$'判断输入是否为合法八进制掩码,是则补零为四位后直接返回; - 符号校验:否则用正则
([ugoa]*)([=+-]?)([rwx]*)校验整个逗号分隔列表;并额外拒绝“有 who 无 mode”的非法组合(如urw); - 拆分处理:把掩码拆成
u、g、o三个数字位,对每个权利项解析出 scope、mode、perms; - 换算权限值:
r=4、w=2、x=1(与八进制权限位一致),通过预置的换算表__fish_umask_add_table、__fish_umask_remove_table、__fish_umask_set_table完成“符号权限 → umask 数字”的映射; - 输出:最终以
0开头拼接三位数字,返回四位八进制字符串。
而__fish_umask_print_symbolic则负责反向转换,把$umask的每一位数字还原成u=...,g=...,o=...的符号形式,这正是umask -S的输出来源。
顶层 umask 函数
share/functions/umask.fish 定义的顶层umask函数逻辑如下:
- 零参数:若
$umask未设置,则默认取113(即八进制0113,屏蔽属主的执行位以及组、其他用户的全部权限);随后根据选项输出——-p/--as-command打印umask $umask,-S/--symbolic调用__fish_umask_print_symbolic,否则直接打印$umask; - 一个参数:调用
__fish_umask_parse解析并校验,成功后set -g umask $parsed(设置全局变量umask,进而触发set_umask写入内核),失败则打印umask: Invalid mask '...'并返回 1; - 多个参数:打印
umask: Too many arguments并返回 1。
默认值113的引入是为了在$umask从未被设置时也能给出一个合理的回退值。
完整示例
以下示例均基于 tests/checks/umask.fish 中的真实用例整理:
# 设定基线掩码 027,然后查看八进制与符号两种输出 umask 027 umask # 输出 0027 echo umask var = $umask # 输出 umask var = 0027 umask -S # 输出 u=rwx,g=rx,o= # 非法掩码会被拒绝并报错 umask 1234 # 报错:umask: Invalid mask '1234' umask 228 # 报错:umask: Invalid mask '228' umask 0282 # 报错:umask: Invalid mask '0282' # 符号模式:先清空掩码再追加权限 umask 0 umask u-w,o-x umask # 输出 0021 umask -S # 输出 u=rw,g=rwx,o=rw # 符号模式:= 直接设定权限 umask 777 umask u+r,g+w,o=rw umask -S # 输出 u=rwx,g=rwx,o=rw # 省略 who 与 mode:默认 a 与 = umask 0 umask rx # 等价于 umask a=rx umask -S # 输出 u=rx,g=rx,o=rx # 组合操作:=r,g+w,o+x,o-r 逐个调整 umask 777 umask =r,g+w,o+x,o-r umask -S # 显式逐位设置 umask u=rwx,g=rwx,o= umask # 输出 0003(组、其他用户均屏蔽写与执行之外的权限) umask u=rwx,g=,o=rwx umask # 输出 0070关于最后几组输出,可对照umask语义推算:u=rwx,g=rwx,o=表示属主与属组完整权限、其他用户无任何权限,对应的反掩码为0003(仅属主与属组移除执行位1之外的权限)——测试文件中的#CHECK注释给出了逐条期望值,是理解符号换算的最佳参考。
测试验证与已知限制
tests/checks/umask.fish 是 fish 官方对 umask 的回归测试集,覆盖了:
- 基线八进制掩码的输出(
0027)与符号输出(u=rwx,g=rx,o=); - 非法掩码的拒绝路径(
1234、228、0282); - 大量符号模式的
+、-、=组合及其与 bash 行为的对照; - Cygwin 下
noacl挂载点的特殊处理(此时权限由 umask 模拟,测试会跳过依赖cut的符号输出分支)。
测试注释特别说明:所有符号模式的期望值都是编写者以 bash 相同命令的行为为基准逐一验证的,如果 bash 在这些场景下存在 bug,fish 可能同样存在——这也是文档中“符号掩码目前并不能按预期工作”警告的直接来源。此外 tests/checks/set.fish 验证了通过set命令把umask设为非法值时会被拒绝,印证了 src/env/impl/environment.rs 中set_umask的校验逻辑。
与普通 shell 变量的差异小结
| 方面 | 普通变量 | umask(电子变量) |
|---|---|---|
| 存储 | 存在变量栈中 | 不真正存储,读取时由GET_UMASK调用umask(2)实时计算 |
| 写入 | 直接改栈 | 由set_umask拦截,校验后调用umask(2)写入内核 |
| 输出格式 | 原样 | 固定四位八进制(如0027) |
| 默认值 | 无 | 未设置时umask函数回退为113 |
理解这层差异对排查权限问题很有帮助:任何子进程(包括其他 shell、编辑器、构建工具)创建文件时都会继承该掩码,而 fish 之所以用电子变量包装,是为了在保持$umask可读可写接口的同时,确保它与内核状态始终一致。
- CLI
- 开发工具
【免费下载链接】fish-shell
The user-friendly command line shell.
相关推荐
fish shell `ulimit` 内置命令完全指南:查看与设置进程资源限制
fish shell ulimit 内置命令完全指南:查看与设置进程资源限制 ulimit 是 fish 内置命令(builtin),用于读取或修改当前 she
CLI开发工具fish-shell 的 cd 内置命令完全指南:目录切换、CDPATH 与符号链接处理
fish shell 的 cd 内置命令完全指南:目录切换、CDPATH 与符号链接处理 cd 是 fish shell 中切换当前工作目录的内置命令,负责更新
CLI开发工具fish-shell 的 exit 内置命令:退出码语义、source 上下文行为与底层实现解析
fish shell 的 exit 内置命令:退出码语义、source 上下文行为与底层实现解析 本篇指南以 fish shell 官方文档 exit.rst
CLI开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考