Linux 文件操作

前言

  • 这篇是「在命令行里干活」的手册:怎么定位自己、怎么看文件、怎么增删改、怎么管用户和权限、怎么搜东西、怎么理解链接。
  • 所有结论都在 Ubuntu 26.04.1 LTS(WSL2) 上实测,日期 2026-11-03。命令输出我尽量原样贴,方便对照。

命令的通用形状

1
命令 [选项] [参数]

方括号表示可选,这是所有 Unix 命令的通用形状。几个约定:

写法 含义 例子
-a 短选项,单字母 ls -a
-a -l 或 -al 短选项可以合并 ls -al 等价 ls -a -l
--all 长选项,双横线 ls --all
-n 5 或 --lines=5 带值的选项 head -n 5
-- 后面的东西一律当参数,不再当选项 rm -- -dash
* ? [0-9] 通配符(由 shell 展开,不是命令自己的功能) rm *.py

两个新手一定会撞的:

  • 通配符没匹配到任何文件时,shell 会把 *.pyz 原样传给命令,于是报错长得像「文件不存在」:
1
2
$ rm *.zzz
rm: cannot remove '*.zzz': No such file or directory
  • 以 - 开头的文件名会被当成选项,正确写法是加 ./ 或用 --:
1
2
$ touch ./-dash && rm ./-dash && echo 删除成功
删除成功

先搞清自己在哪儿:pwd 与 cd

实测四条:

1
2
3
4
5
6
7
8
$ cd /etc && pwd
/etc
$ cd && pwd
/home/loong
$ cd /var && cd - && pwd
/home/loong
$ cd /nope
bash: line 1: cd: /nope: No such file or directory
写法 去哪
cd (不带参数) 当前用户家目录,实测 /home/loong
cd - 上一个目录(靠 shell 变量 OLDPWD 实现,实测 OLDPWD=/var)
cd ~ / cd 同上
cd /a/b 绝对路径:以 / 开头,从根开始数
cd ../x 相对路径:以 . 或 .. 开头,从当前目录开始数
cd x 也是相对路径(相对当前目录),不会去 PATH 里找

最后一条最容易出错:可执行文件才走 PATH,目录永远按相对/绝对路径解析。所以执行当前目录里的脚本必须写 ./s.sh,光写 s.sh 会 command not found。

ls 的输出到底怎么读

先造三个东西(普通文件、目录、软链接),看 ls -l 的真实输出:

1
2
3
4
5
$ ls -l
total 4
drwxr-xr-x 2 loong loong 40 Nov 3 18:11 d1
-rw-r--r-- 1 loong loong 4 Nov 3 18:11 f1.txt
lrwxrwxrwx 1 loong loong 6 Nov 3 18:11 l1 -> f1.txt

用 awk '{print NF}' 数过,一行是 9 个字段(total 4 那行不算):

位置 内容 上面例子里的值
1 类型 + 权限(10 个字符) -rw-r--r--
2 硬链接数 1(目录是 2,因为里面还有 . 和 ..)
3 属主(用户) loong
4 属组 loong
5 大小(字节;-h 才变成 K/M/G) 4
6-8 月、日、时间或年 Nov 3 18:11
9 名字(软链接后面会跟 -> 目标) l1 -> f1.txt

total 4 是这一目录里文件占用的磁盘块总量(单位 KB),不是文件大小之和,新手常误会。

第 2 个字段用 stat 验证更清楚:

1
2
3
4
$ stat -c '%n inode=%i 硬链接数=%h 权限=%A(%a) 属主=%U 属组=%G 大小=%s 类型=%F' f1.txt d1 l1
f1.txt inode=17 硬链接数=1 权限=-rw-r--r--(644) 属主=loong 属组=loong 大小=4 类型=regular file
d1 inode=18 硬链接数=2 权限=drwxr-xr-x(755) 属主=loong 属组=loong 大小=40 类型=directory
l1 inode=19 硬链接数=1 权限=lrwxrwxrwx(777) 属主=loong 属组=loong 大小=6 类型=symbolic link

注意软链接自己的权限永远是 lrwxrwxrwx(777),真正的权限看它指向的文件。

七种文件类型(第一个字符)

原笔记那张类型表是对的,我把每一种都造出来/找出来了:

首字符 类型 实测样本
- 普通文件 -rw-r--r-- 1 loong loong 2 ... reg
d 目录 drwxr-xr-x 2 loong loong 40 ... dir
l 符号(软)链接 lrwxrwxrwx 1 loong loong 3 ... sym -> reg
c 字符设备 crw-rw-rw- 1 root root 1, 3 ... /dev/null
b 块设备(原笔记写成「快设备」,是错字) brw-rw---- 1 root disk 8, 0 ... /dev/sda
s 套接字 srwx------ 1 root root 0 ... /run/systemd/private
p 命名管道(FIFO) prw-r--r-- 1 loong loong 0 ... pipe1(用 mkfifo pipe1 造的)

数量级感受一下:本机 /dev 里实测 28 个块设备、140 个字符设备、13 个目录、8 个软链接;/run 里 319 个普通文件、119 个软链接。套接字和管道平时不会自己造,它们是进程间通信用的(/dev/log、systemd 的各种 .socket 就是活例子)。

ls 的三个常用选项与 Ubuntu 自带的别名

选项 作用
-a 全部,含 . 开头的隐藏文件(连 . 和 .. 一起显示)
-A 全部但不显示 . 和 ..,脚本里更常用
-l 长格式(上面那 9 个字段)
-h 人类可读的大小(配合 -l 或 -s 才有意义)
-d 只看目录本身,不进目录(ls -ld /tmp)
-t / -S 按时间 / 按大小排序
-R 递归

Ubuntu 的 ~/.bashrc 里默认就配好了别名,实测第 78-93 行:

1
2
3
4
5
6
$ grep -nE "^ *alias" ~/.bashrc
78: alias ls='ls --color=auto'
82: alias grep='grep --color=auto'
91:alias ll='ls -alF'
92:alias la='ls -A'
93:alias l='ls -CF'

但别名只在交互式 shell 里生效,脚本里不生效——这条是实测的:在脚本里 alias ll='ls -l' 之后调用 ll,得到的是 ll: command not found。所以写 shell 脚本时永远用完整命令。临时绕过别名用反斜杠:\ls;想知道某个名字到底是什么,用 type:

1
2
3
4
5
6
$ type ls
ls is aliased to `ls --color=auto'
$ type ll
ll is aliased to `ls -alF'
$ type cd
cd is a shell builtin

同一个 type ls 在脚本(非交互 shell)里跑,结果就变成 ls is /usr/bin/ls——因为 .bashrc 里的别名根本没生效。command -v 的对应行为:command -v cd 输出 cd(内建命令也算存在),查不存在的命令时退出码 1,所以脚本里判断命令存在就写 command -v xxx >/dev/null || { echo 缺这个命令; exit 1; }。

查看文件内容

用一个 11 行的文件实测每个命令(内容形如 L1 one … L11 eleven):

命令 实测输出/行为 什么时候用
cat 文件 从头到尾全部打印 短文件
cat -n 文件 / nl 文件 都带行号,实测 cat -A 看到行号和内容之间是制表符(^I) 想复制带行号的内容
tac 文件 倒着打印,第一行是 L11 eleven 看日志(最新的在前)
head 文件 默认 10 行(实测 head 文件 | wc -l = 10) 看开头
head -3 / head -n 3 / head -c 12 3 行 / 3 行 / 前 12 个字节(-3 这种省掉 n 的老写法实测同样有效) 精确取内容
tail 文件 默认 10 行 看结尾
tail -f 文件 持续跟踪,实测新追加的一行 appended 立刻显示出来 看日志必备
wc 文件 11 22 91 文件名,依次是行数、单词数、字节数 统计
wc -l/-w/-c/-m 11 / 22 / 91 / 91 -m 才是字符数
more、less 本机都装了(/usr/bin/more、/usr/bin/less) 分页看

wc 的字节数与字符数差别,用中文一测就明白:

1
2
3
4
5
$ printf '中文测试\n' > cn.txt
$ wc -c cn.txt
13 cn.txt # UTF-8 下一个汉字 3 字节,4*3+1=13
$ wc -m cn.txt
5 cn.txt # 按字符算是 4 个汉字 + 换行

more / less 的差别(原笔记那几条基本对,补两个实测细节):

  • 两者都是「一屏一屏看」,q 退出,回车 走一行,空格 走一屏。
  • more 看完自动退出;less 不会,而且 less 允许用 ↑/↓ 往回翻,more 只能往前。
  • less 里 /关键词 搜索、n/N 上下跳转、G/g 首尾、q 退出(man 1 less 实测写着 SPACE or ^V or f or ^F 是向前翻一屏)。
  • 管道里 more 不会分页:非终端环境下它直接把内容全打印出来(实测 cat lines.txt | more | head -3 直接给出前三行)。所以「ls /etc | more」这种写法只在真终端里有意义。
  • 现代发行版里 less 常常是默认的 PAGER,man、git log 用的都是它。

日志场景的三板斧,按顺序记:

1
2
3
$ tail -f /var/log/syslog            # 实时看新内容
$ tail -n 200 /var/log/syslog # 看最后 200 行
$ grep -n "error" /var/log/syslog # 找错误并带行号

文件与目录的增删改

命令 干什么 实测要点
touch a b c 建多个空文件;文件已存在则只更新时间戳 实测两次 touch t1,mtime 从 18:11:57.84 变成 18:11:58.95,内容不变
mkdir d 建一层目录 重名时报 mkdir: dup: File exists;mkdir -p 遇到已存在的目录不报错(实测退出码 0)
mkdir -p a/b/c 连中间层一起建 实测得到 deep deep/a deep/a/b deep/a/b/c
rmdir d 只能删空目录 非空时报 rmdir: failed to remove 'deep_copy': Directory not empty
rm f 删文件 删目录时报 rm: cannot remove 'dup': Is a directory
rm -r d 递归删目录 生效
rm -rf d 强制递归删,不提示 危险命令,路径写错就是灾难
cp a b 复制文件 —
cp -r 目录 新目录 递归复制目录 实测目录结构完整复制
cp -a 源 目标 归档模式,保留权限/属主/时间 实测目标仍是 -rw-r--r-- loong loong
cp t1 t2 t3 dir/ 多个文件拷进目录 实测 dir 里出现 t1 t2 t3
mv a b 移动或改名(同一个命令) 实测 mv copy_of_t1 moved.txt 就是改名
mv 文件 目录/ 移动进目录 —

交互与强制:-i、-f、-n

实测把答案用管道喂进去:

1
2
3
4
5
6
$ echo n | rm -i askme      # 回答 n  -> 文件还在
$ echo y | rm -i askme # 回答 y -> 文件被删
$ rm -f 不存在的文件; echo $?
0 # -f 遇到不存在的文件不报错,退出码 0
$ cp -n src1 dst1
cp: warning: behavior of -n is non-portable and may change in future; use --update=none instead

cp -n(不覆盖已存在的文件)在新版 GNU coreutils 里被标记为「行为不保证可移植」,官方建议改用 cp --update=none——这是个典型的「老教程里的写法正在变」的例子。

-f 的语义在 rm 和 cp 上并不完全一样:rm -f 是「不提示、忽略不存在的文件」,cp -f 是「目标存在就先删掉再拷」。别把它们当同一种「强制」。

用户与组:三个账本文件

Linux 的账号信息全在纯文本文件里,看懂这三行就够了(实测拆解 loong 这条):

1
2
$ grep '^loong:' /etc/passwd
loong❌1000:1000::/home/loong:/bin/bash
字段 含义 实测值
1 用户名 loong
2 密码占位符(真正的哈希不在这里) x
3 UID(用户唯一 id) 1000
4 GID(主组的 id) 1000
5 备注/GECOS(全名之类,可为空) 空
6 家目录 /home/loong
7 登录 shell /bin/bash

awk -F: '{print NF}' /etc/passwd | sort -u 实测只输出一个 7,说明每行都是 7 个字段。

组信息在 /etc/group,格式是 组名:占位:GID:成员列表:

1
2
3
4
5
$ grep -E "^(root|sudo|shadow|loong):" /etc/group
root❌0:
sudo❌27:loong
shadow❌42:
loong❌1000:

sudo<span class="github-emoji" alias="x" style="" fallback-src="https://github.githubassets.com/images/icons/emoji/unicode/274c.png?v8">&#x274c;</span>27:loong 就是「loong 属于 sudo 组,因此能提权」的物理证据,和 id 的输出一致:

1
2
$ id loong
uid=1000(loong) gid=1000(loong) groups=1000(loong),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),100(users)

密码哈希在 /etc/shadow(普通用户读不到,权限 640 属组 shadow,见上一篇):

1
2
3
$ head -2 /etc/shadow
root:*:20692:0:99999:7:::
daemon:*:20692:0:99999:7:::

第 2 列的 * 表示「这个账户没有可用密码、不能密码登录」,! 表示「被锁定」。

UID 的分段(实测本机):0 是 root;1-999 是系统账户(本机 100-999 段有 8 个,如 systemd-network 998、polkitd 987);从 1000 开始才是人(loong = 1000)。另外 /etc/subuid、/etc/subgid 里给每个用户预留了一大段次级 id(实测 loong:100000:65536),容器和无特权用户用,日常不用管。

建用户、删用户:useradd 的真实默认值

原笔记写「useradd:新建用户,-d 指定家目录,-m 不存在,-s 指定 shell」,这句太省了。实测各种写法的结果:

1
2
3
4
5
6
7
8
$ useradd probe01; echo $?
0
$ grep probe01 /etc/passwd /etc/group /etc/shadow
/etc/passwd:probe01:x:1001:1001::/home/probe01:/bin/sh
/etc/group:probe01:x:1001:
/etc/shadow:probe01:!:20760:0:99999:7:::
$ ls -ld /home/probe01
ls: cannot access '/home/probe01': No such file or directory

三个和直觉不同的点:

  1. 不加 -m 不建家目录(passwd 里照样写着 /home/probe01,但目录不存在)。
  2. 默认 shell 是 /bin/sh,不是 /bin/bash。
  3. 影子条目第 2 列是 !:新账户没有密码,登录不了。

各选项实测:

写法 实测结果
useradd -m probe 建家目录,并从 /etc/skel 复制 .bash_logout .bashrc .profile,权限 750(drwxr-x---)
useradd -M probe 明确不建家目录
useradd -d /home/x probe 指定家目录路径
useradd -s /bin/zsh probe 指定 shell;zsh 没装时提示 useradd: Warning: missing or non-executable shell '/bin/zsh',但照样写进 passwd
useradd -r probe 系统账户,实测 UID 999、GID 986
useradd -g probe01 -G sudo probe05 -g 主组、-G 附加组,实测 id 显示 gid=1001(probe01) groups=1001(probe01),27(sudo)

删用户:

1
2
3
4
$ userdel probe04                    # 只删账户
$ userdel -r probe01 # -r 连家目录和邮件池一起删
$ userdel probe05
userdel: user probe05 is currently used by process 629

最后那条报错很常见:该用户还有进程在跑(比如你正 su 在他身份下),必须先把进程结束或加 -f,否则删不掉。

组的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ groupadd probegrp
$ getent group probegrp
probegrp❌1004:
$ gpasswd -a probe01 probegrp
Adding user probe01 to group probegrp
$ getent group probegrp
probegrp❌1004:probe01
$ gpasswd -d probe01 probegrp
Removing user probe01 from group probegrp
$ usermod -aG probegrp probe05
$ id probe05
uid=1004(probe05) gid=1001(probe01) groups=1001(probe01),27(sudo),1004(probegrp)
$ groupdel probegrp

gpasswd -a/-d 和 usermod -aG 都能加组,差别是前者改的是组成员列表,后者改的是用户的附加组集合(效果一样,usermod 记得带 -a,不带 -a 的 usermod -G 会覆盖原有附加组,这是经典事故)。

原笔记说 groups 是「查看指定用户组的信息」——不准确,实测:

1
2
3
4
$ groups probe01
probe01 : probe01
$ getent group probe01
probe01❌1001:

groups 用户 列出的是这个用户属于哪些组;要看某个组本身的信息用 getent group 组名。本机 newgrp、sg、groupmems 都没安装(实测),chage(改密码有效期)在。

切用户与提权:su、sudo、visudo

su 是「切换身份并在那个身份下跑命令」,实测三种用法:

1
2
3
4
5
6
7
8
9
$ su - probe01 -c 'id; echo $HOME; pwd; whoami'
su: warning: cannot change directory to /home/probe01: No such file or directory
uid=1001(probe01) gid=1001(probe01) groups=1001(probe01)
HOME=/home/probe01
/mnt/e/Desktop/Test/fw-demos
probe01

$ su probe01 -c 'echo $HOME'
HOME=/home/probe01

su -(或 su -l)是登录 shell:会读 /etc/profile 和用户的 ~/.profile,并尝试进入它的家目录(上面那句 warning 就是它进不去 probe01 那个不存在的家目录)。不带 - 时只换身份、基本沿用当前环境。日常习惯:切用户就带 -,否则你继承的还是自己那套 PATH。

被禁用的账户(shell 设成 nologin)登录时得到的是:

1
2
3
$ su - probex
su: warning: cannot change directory to /home/probex: No such file or directory
This account is currently not available.

chsh 改 shell 有个细节,实测:

1
2
$ chsh -s /usr/sbin/nologin probe05
chsh: Warning: /usr/sbin/nologin is an invalid shell

因为合法 shell 列表在 /etc/shells,本机实测内容是:

1
2
3
4
5
6
7
8
9
10
11
$ cat /etc/shells
# /etc/shells: valid login shells
/bin/sh
/usr/bin/sh
/bin/bash
/usr/bin/bash
/bin/rbash
/usr/bin/rbash
/usr/bin/dash
/usr/bin/screen
/usr/bin/tmux

nologin 不在表里,所以 chsh 会警告(但仍然写进去了)。想彻底禁止某账户登录,除了 shell 设 nologin,更规范的是 usermod -L 或 passwd -l 锁定。

sudo 是「用别人的身份执行一条命令」(默认 root),实测:

1
2
3
4
5
$ sudo -l -U loong
User loong may run the following commands on Y9000P:
(ALL : ALL) ALL
$ sudo -u probe01 id
uid=1001(probe01) gid=1001(probe01) groups=1001(probe01)

规则文件 /etc/sudoers 里和本机相关的三行:

1
2
3
4
$ grep -nE "^(root|%admin|%sudo)" /etc/sudoers
47:root ALL=(ALL:ALL) ALL
50:%admin ALL=(ALL) ALL
53:%sudo ALL=(ALL:ALL) ALL

%sudo 表示 sudo 这个组,所以「把用户加进 sudo 组」=「给他 sudo 权限」,链条是:usermod -aG sudo 用户 → /etc/group 里出现他 → 匹配 %sudo 这行 → 能跑所有命令。

改 sudoers 必须用 visudo,它会先做语法检查再原子写入,直接 vim /etc/sudoers 写错一行,全系统提权就废了。不想打开编辑器也能验证:

1
2
$ visudo -c
/etc/sudoers: parsed OK

visudo 用哪个编辑器打开,由 alternatives 决定,实测本机默认是 nano:

1
2
3
4
$ readlink -f /usr/bin/editor
/usr/bin/nano
$ update-alternatives --list editor
/bin/ed /bin/nano /usr/bin/vim.basic /usr/bin/vim.tiny

想换成 vim:sudo update-alternatives --config editor 按提示选编号,或者 export VISUAL=vim(原笔记那句「用 update-alternatives --config editor 可以修改系统默认编辑器(nano)」是对的)。

密码:passwd、chpasswd、锁定与状态

操作 命令 实测
改自己的密码 passwd 交互输入两次,输入时不回显
改别人的密码 sudo passwd 用户名 需要 root
脚本里非交互设密码 echo '用户:密码' | chpasswd 实测退出码 0
RHEL 的写法 passwd --stdin 用户 Ubuntu 上没有这个选项,别照抄 CentOS 教程
锁定账户 passwd -l 用户 实测 shadow 第 2 列前面多了 !
解锁 passwd -u 用户 实测对没设过密码的账户会拒绝:见下
查状态 passwd -S 用户 实测 probey L ...(L=锁定)、probe01 P ...(P=有可用密码)

passwd -u 那条拒绝信息很有教育意义:

1
2
3
$ passwd -u probe05
passwd: unlocking the password would result in a passwordless account.
You should set a password with usermod -p to unlock the password of this account.

意思是:这个账户从来没设过密码,只是被 -l 加了个 ! 前缀;直接解锁会变成空密码账户,所以它拒绝,让你先设密码。

/etc/shadow 一行有 9 列,前 3 列日常够用:用户名:哈希:最后修改日期(天):最小间隔:最大有效期:警告天数:失效宽限::(实测 root:*:20692:0:99999:7:::,20692 是从 1970-01-01 起算的天数)。改有效期用 chage,比手改文件安全。

权限:r、w、x 对文件和目录不是一回事

三组权限对应三种身份,这个大家都背得熟:

身份 字母 含义
属主 u(user) 文件属于谁
属组 g(group) 文件属于哪个组
其他人 o(other) 剩下的所有人
全部 a(all) u+g+o

但同一个字母对文件和目录的意义不同,这一点原笔记没写,而它恰恰是最容易看错的地方。实测:

1
2
3
4
5
6
7
8
9
$ mkdir secdir && printf 'inside\n' > secdir/inner.txt
$ chmod 600 secdir
$ cd secdir
bash: cd: secdir: Permission denied
$ ls secdir
inner.txt # 能列出名字
$ chmod 700 secdir
$ cat secdir/inner.txt
inside # 加了 x 才能访问里面的文件

结论:

  • 对文件:r 能读内容,w 能改内容,x 能当成程序执行。
  • 对目录:r 只允许列出里面的名字,x 才允许进入并按名字访问(cd、cat dir/文件)。所以「有 r 没 x」的目录你能看到文件名但打不开任何东西——这是排查「明明 ls 得到文件了为什么 cat 说 Permission denied」的关键。
  • 对目录的 w:能不能在里面创建/删除文件(删除时看的是父目录的写权限,不是文件自己的)。

脚本能不能跑,实测:

1
2
3
4
5
6
7
$ printf '#!/bin/bash\necho ran\n' > s.sh
$ ./s.sh
bash: ./s.sh: Permission denied
$ chmod +x s.sh && ./s.sh
ran
$ bash s.sh # 显式交给解释器,不需要 x
ran

所以「bash 脚本.sh」永远能跑,而「./脚本.sh」必须有 x。

chmod:符号写法和八进制写法

符号写法 chmod [身份][操作][权限] 文件:

操作符 意思
+ 加上这个权限
- 去掉
= 设成(其他未写的权限会被清掉)

实测一串(起始权限 754):

1
2
3
4
5
6
7
8
9
10
$ chmod 754 p1 && stat -c '%a %A' p1
754 -rwxr-xr--
$ chmod u+x,g-w,o-r p1 && stat -c '%a %A' p1
750 -rwxr-x---
$ chmod 0755 p1 && stat -c '%a %A' p1
755 -rwxr-xr-x
$ chmod a=r p1 && stat -c '%a %A' p1
444 -r--r--r--
$ chmod u=rwx,go=r p1 && stat -c '%a %A' p1
744 -rwxr--r--

八进制的原理(原笔记这段是对的,补一张速查):r=4、w=2、x=1,三位二进制合成一个八进制数字。

八进制 二进制 符号
0 000 ---
1 001 --x
2 010 -w-
3 011 -wx
4 100 r--
5 101 r-x
6 110 rw-
7 111 rwx

所以 755 = 属主 rwx、属组 r-x、其他人 r-x。递归改目录用 -R:sudo chmod -R 755 目录。

改属主/属组是另外两个命令:

1
2
3
4
$ sudo chown probe01 file          # 只改属主
$ sudo chown probe01:probegrp file # 同时改属主和属组(冒号或点都行)
$ sudo chgrp probegrp file # 只改属组
$ sudo chown -R 用户:组 目录 # 递归

umask:新建文件的默认权限

umask 是「从默认全权限里扣掉的位」。默认值:目录按 777 起算、文件按 666 起算(文件永远不默认给执行位)。实测三组:

1
2
3
4
5
6
7
8
9
10
11
$ umask
0022
$ ( umask 022; touch f; mkdir d; stat -c '%a %n' f d )
644 f
755 d
$ ( umask 002; touch f; mkdir d; stat -c '%a %n' f d )
664 f
775 d
$ ( umask 077; touch f; mkdir d; stat -c '%a %n' f d )
600 f
700 d

算法就是「默认值按位与上 umask 取反」,日常口算用「7 减去每一位」即可:umask 022 → 目录 755、文件 644;umask 077 → 目录 700、文件 600。

原笔记用 0002 举例(目录 775、文件 664)算得没错,只是本机 Ubuntu 的默认 umask 是 0022,不是 0002(RHEL 的「用户私有组」设置才会给到 0002)。

特殊权限位:/tmp 末尾那个 t

1
2
3
4
$ ls -ld /tmp
drwxrwxrwt 9 root root 180 Nov 3 18:11 /tmp
$ stat -c '%a %A' /tmp
1777 drwxrwxrwt

第 10 个字符是 t(大写 T 表示没有其他人执行位时的 sticky),八进制第 1 位是 1。含义是:谁都能在里面建文件,但只有属主能删自己的。共享目录就靠它。另外两个特殊位是 s(setuid/setgid,八进制 4/2)和 t 之外的组合,日常见到 rws 别慌,那是 setuid。

找文件:find

格式是 find [起始目录] [条件] [动作],目录不写就默认从当前目录开始——这条实测很重要:

1
2
3
4
5
6
7
8
$ find -maxdepth 2 -name passwd        # 原笔记的写法,少写了路径
(没有输出)
$ find . -maxdepth 1 -name "*.txt"
./cn.txt
./lines.txt
./f1.txt
$ find /etc -maxdepth 1 -name passwd
/etc/passwd

所以「sudo find -maxdepth 2 -name passwd」并不是「全盘搜索」,它搜的是你当前所在目录。要全盘搜必须写 /。

常用条件,全部实测过:

条件 作用 实测例子
-name / -iname 按名字(-iname 忽略大小写) find /etc -maxdepth 2 -iname "*passwd*" 找到 /etc/passwd、/etc/passwd-、/etc/security/opasswd
-maxdepth N 只往下找 N 层 -maxdepth 1 只看当前层
-type 按类型(f 普通、d 目录、l 链接、s 套接字、p 管道、c/b 设备) 当前目录实测:f 17 个、d 12 个、l 2 个、p 1 个
-size 按大小,+/- 表示大于/小于,单位 c 字节、k、M、G 造了个 9 KB 的 big.bin,-size +5k 命中它
-mmin / -mtime 最近 N 分钟/天修改过 -mmin -5 列出了刚建的文件
-amin / -atime 访问过 注意很多文件系统挂了 relatime,访问时间不会每次都更新
-cmin / -ctime 状态改变(改权限、改属主)过 用 chmod 之后再查就能命中
-user / -group 按属主/属组 find /etc -maxdepth 1 -user root
-exec 命令 {} \; 对每个结果执行一条命令 find . -name "*.txt" -exec basename {} \; 输出 two.txt、one.txt
-exec 命令 {} + 同上但一次传多个参数(快得多) find . -name "*.txt" -exec wc -l {} + 输出带 total
-delete 直接删掉找到的 实测执行后目录空了

三个提醒:

  • -mtime +7 是「7 天之前修改的」,-mtime -7 才是「最近 7 天内」,+/- 的方向和直觉相反,务必记牢。
  • -name 要加引号(-name "*.txt"),否则会被 shell 先展开,结果完全不同。
  • find / -name xxx 会刷一堆 Permission denied,习惯写法是加 2>/dev/null。

找命令与找内容:whereis、which、grep

找「命令在哪、文档在哪」,四个工具各有分工,实测:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ whereis ls
ls: /usr/bin/ls /usr/share/man/man1/ls.1.gz
$ whereis vim
vim: /usr/bin/vim /etc/vim /usr/share/vim /usr/share/man/man1/vim.1.gz
$ whereis -b ls # 只要二进制
ls: /usr/bin/ls
$ whereis -m ls # 只要手册
ls: /usr/share/man/man1/ls.1.gz

$ which ls
/usr/bin/ls
$ command -v ls
/usr/bin/ls
$ type ls
ls is aliased to `ls --color=auto'
$ which cd
(没有输出,因为 cd 是内建命令)

原笔记里 whereis ls 的结果写的是 /bin/ls,现在实测是 /usr/bin/ls(/bin 只是软链接)。选哪个:

  • 脚本里判断命令存在与否 → command -v xxx(POSIX 标准,最可靠)。
  • 想知道它是别名/内建/文件 → type xxx。
  • 想连文档、配置一起看 → whereis xxx。

grep 是「在文件里按模式找行」,实测:

1
2
3
4
5
6
7
8
9
10
11
12
$ grep -n "/bin/bash" /etc/passwd
1:root:x:0:0:root:/root:/bin/bash
27:loong:x:1000:1000::/home/loong:/bin/bash
$ grep -c "/bin/bash" /etc/passwd
2
$ grep -v "^#" /etc/passwd | head -2 # -v 反选
$ grep -rn "Resolute" /etc/*release # -r 递归
/etc/os-release:4:VERSION="26.04.1 LTS (Resolute Raccoon)"
$ grep -q "绝对不存在xyz" /etc/passwd; echo $?
1 # 没匹配到 = 1
$ grep -q "root" /etc/passwd; echo $?
0 # 匹配到 = 0

退出码这一条在脚本里特别有用:if grep -q "关键词" 文件; then ...; fi。

常用选项:-i 忽略大小写、-n 显示行号、-c 只计数、-v 反选、-r 递归、-w 整词匹配、-A3/-B3 显示匹配行的后/前 3 行(查日志极有用)、--color=auto(Ubuntu 已在 ~/.bashrc 里默认开了别名)。

和 find 的分工:按「名字/属性」找文件用 find,按「内容」找用 grep(内容搜索想跨目录快,可以用 grep -rn,或者装 ripgrep 的 rg,本机未装)。

链接文件:硬链接与软链接

ln 的两种形态,一次全测出来:

1
2
3
4
5
6
7
8
9
10
11
$ ln orig.txt hard.txt          # 硬链接(不加 -s)
$ ln -s orig.txt soft.txt # 软链接
$ ls -l orig.txt hard.txt soft.txt
-rw-r--r-- 2 loong loong 8 Nov 3 18:12 hard.txt
-rw-r--r-- 2 loong loong 8 Nov 3 18:12 orig.txt
lrwxrwxrwx 1 loong loong 8 Nov 3 18:12 soft.txt -> orig.txt

$ stat -c '%n inode=%i 链接数=%h' orig.txt hard.txt soft.txt
orig.txt inode=63 链接数=2
hard.txt inode=63 链接数=2
soft.txt inode=64 链接数=1

看懂这张表就看懂了两者的区别:

  • 硬链接和原文件是同一个 inode(63),只是多了一个名字,链接数从 1 变 2。改任一个,另一个立刻变(实测 printf 'changed\n' > orig.txt 之后 hard.txt 也是 changed)。
  • 软链接是独立文件(inode 64),里面存的是目标路径字符串(readlink soft.txt 实测返回 orig.txt,是相对路径)。

行为差异,全部实测:

场景 硬链接 软链接
删掉原文件 内容还在,cat keep_hard.txt 正常输出(数据要等所有链接都删掉才释放) 变成悬空链接:cat dangling.txt → No such file or directory
指向目录 拒绝:ln: adir: hard link not allowed for directory 可以:ln -s adir dirsoft 正常
跨文件系统 拒绝:ln: failed to create hard link ...: Invalid cross-device link(实测从 ext4 往 /mnt/e 建失败) 可以(存的是路径,不关心目标在哪)
权限显示 和原文件一样 永远是 lrwxrwxrwx(777),真实权限看目标
常见用途 几乎不用(cp -l 做快照省空间时会用) 大量使用:/bin→/usr/bin、update-alternatives、/dev/log

系统里软链接有多普遍,实测 /usr/bin 下 257 个是软链接,例如:

1
2
3
4
5
6
$ find /usr/bin -maxdepth 1 -type l | head -5
/usr/bin/uname
/usr/bin/keep-one-running
/usr/bin/gprof
/usr/bin/vimdiff
/usr/bin/gp-display-text

(vimdiff、gprof 这类都是 alternatives 机制的软链接,指向真正的实现。)

查看链接指向的三个命令:

1
2
3
4
5
6
$ readlink soft.txt          # 只打印链接里存的路径
orig.txt
$ readlink -f dirsoft # -f 一路解析到最终真实路径
/tmp/lops/adir
$ file soft.txt # 说明它是符号链接以及指向哪
soft.txt: symbolic link to orig.txt

原笔记那句「软连接」是错字,应为软链接(soft link / symbolic link),硬链接是 hard link。

常见坑速查

  • ls -l 第二列当成「用户组」:那是硬链接数,属主和属组在第 3、4 列。
  • wc 的第三个数当字符数:是字节数,中文一测就露馅(-m 才是字符数)。
  • find -maxdepth 2 -name xxx 忘了写路径:它从当前目录开始找,不是全盘。
  • -mtime +7 理解成「最近 7 天」:它是「7 天以前」。
  • -name *.txt 不加引号:会被 shell 先展开,结果不可控。
  • 照抄 passwd --stdin:Ubuntu 没有,用 chpasswd。
  • useradd 之后以为有 bash 和家目录:实测默认 shell 是 /bin/sh、不建家目录,要 -m -s /bin/bash。
  • usermod -G 忘了 -a:会覆盖掉该用户原有的附加组。
  • 直接 vim /etc/sudoers:写错一行就没人能 sudo 了,用 visudo(或 visudo -c 检查)。
  • rm -rf 手滑:路径带变量时先 echo 一遍再执行;生产机上考虑 alias rm='rm -i'。
  • 以为目录有 r 就能进:cd 和访问里面的文件需要 x(实测 chmod 600 的目录 ls 能列名但 cd 报 Permission denied)。
  • ./脚本.sh 报 Permission denied:缺执行位,chmod +x 或者直接用 bash 脚本.sh。
  • 删不掉用户报「currently used by process」:他还有进程在跑,先退出那个会话或结束进程。
  • 软链接「坏了」找不到原因:它存的是路径字符串,目标被移动/删除就悬空,用 ls -l、readlink -f、file 三个命令查。

练习

  1. 造一个 11 行的文件,用 wc 的四种统计各跑一次,解释为什么 -c 和 -m 在纯英文文件上数值相同、在中文文件上不同。
  2. 用 mkdir -p 建 a/b/c/d,再想办法只用一条 rmdir 把它们从最深一层开始全部删掉(提示:rmdir 支持多个参数,或者用 find -depth -type d -delete)。
  3. 建一个目录并 chmod 600,验证「能 ls 但不能 cd」;再 chmod 700 验证反过来需要 x。把两次报错原文记下来。
  4. 用 useradd -m -s /bin/bash 建一个练习账户,检查它的家目录权限和从 /etc/skel 复制来的文件;然后 gpasswd -a 用户 sudo 给它 sudo 权限,用 sudo -l -U 用户 验证;最后 userdel -r 清理干净。
  5. 分别用 find、whereis、which、command -v、type 查 ls,把五个输出并排贴出来并说明差别。
  6. 在 /etc 下用 find 找出所有软链接(-type l),数一下有多少个,再挑 3 个用 readlink -f 看它们最终指向谁。
  7. 建硬链接和软链接各一个,然后删掉原文件,分别 cat 两个链接,解释结果为什么不同;再用 stat -c '%i %h' 验证 inode 与链接数。
  8. 写一条命令:找出 /var/log 下最近 1 天内修改过、且大于 10 KB 的普通文件,并对每个执行 ls -lh(用 -exec ... \; 和 -exec ... + 各写一遍,比较执行次数)。

参考