⛺基础概念
shell
Bash(Bourne-Again Shell)是一种为 GNU 计划编写的 Unix shell,由布莱恩·福克斯于 1987 年开发,旨在替代早期的 Bourne Shell(sh)。其名称“Bourne-Again”是一个双关语,既指它是 Bourne Shell 的扩展,也暗示“重生”之意。Bash 结合了 Bourne Shell、Korn Shell(ksh)和 C Shell(csh)的特性,提供了更强大的功能,如命令行编辑、命令历史、自动补全和脚本编写能力。
Bash 的发展历史可以追溯到 Unix 系统的早期。1978 年,史蒂夫·伯恩开发了 Bourne Shell,成为 Unix 的标准 Shell。1989 年,Bash 发布 1.0 版本,逐渐成为 Linux 和 macOS 等操作系统的默认 Shell。Bash 不仅兼容 Bourne Shell 的脚本,还引入了许多新特性,使其成为现代系统管理和自动化任务的核心工具。
Bash 的主要用途包括:
- 命令行交互:用户通过 Bash 与操作系统交互,执行命令。
- 脚本编写:Bash 脚本用于自动化任务,如系统管理、日志分析和批量处理。
- 系统管理:Bash 广泛用于服务器管理、环境配置和监控任务。
由于其便利性和广泛兼容性,Bash 已成为 linux 开发者和系统管理员的首选脚本工具。
即使 bash 相比 fish/zsh 在命令行交互上并不友好,但是现阶段主流的发行版中都默认集成 bash,并且 fish/zsh 各自的语法与 bash 并不兼容,因此并不推荐在实际的生产中使用其他的 shell 环境。
确认 linux 默认的 shell 环境环境
# 输出应该为 /bin/bash echo $SHELL
Point!!!!!!!!
对于 kali 发行版,默认的 shell 是
zsh。在后续的实验中,你可以在执行脚本,或者执行命令前,先输入 bash,切换到 bash shell 后再执行命令。基础概念:文件路径
- 根路径:
/
- 绝对路径: 以根路径开始的路径,每个目录层级之间用
/分割,比如/
- 相对路径: 以当前路径来看的路径
./test.txt - 当前路径: 执行命令/查看文件时,所在的路径。用
.表示当前路径。当你使用相对路径执行命令的时候,的概念很重要。使用命令pwd可以获取当前所在的路径。 - 上一级路径:用
..表示上一级路径
当前路径
- 主目录路径:
~表示当前用户的主目录路径,对于 root 用户,路径为/root;对于普通用户,路径通常为/home/<用户名>。可以使用环境变量$HOME获取当前用户的主目录路径。
echo $HOME
路径通配符
通配符只使用于通配路径,只能用于 shell,被 shell 自解释。
- ,
?,[]
通配符 | 作用 |
? | 匹配任意单个字符 |
* | 匹配任意长度任意字符 |
** | 匹配任意级别目录(bash 4.0 以上版本支持, shopt -s globstar ) |
[] | 匹配一个单字符范围,如[a-z],[0-9] |
[]:匹配范围 [^]:排除匹配范围 [:alnum:]:所有字母和数字 [:alpha:]:所有字母 [:digit:]:所有数字 [:lower:]:所有小写字母 [:upper:]:所有大写字母 [:blank:]:空白字符和TAB制表符 [:space:]:包括空白字符、TAB制表符(\t)、换页(\f) [:cntrl:]:所有控制字符 [:graph:]:可打印并可看到的字符。空格是可打印的,但是不是可看到的。 [:print:]:所有可打印字符 [:punct:]:所有标点符号,非字母、数字、控制字符和space字符。 [:xdigit:]:十六进制数的字符。
比如使用
*.txt 可以表示所有的后缀名为 txt的文件。学习推荐
要想熟练开发 bash 脚本,不仅需要学习 bash 的语法结构,更需要学习并掌握常用的 shell 命令! bash 和编程语言在开发流程上最大的不同点在于多数功能的实现依赖命令调用!。
可以使用
man bash 命令获取 bash 语法的官方文档教程。对下问所有的指令,均可以使用
man <command> 来查看指令的使用方式。如man lsman echo。基础文件操作
在 bash shell 环境内执行下面的命令
路径切换
cd
cd path,切换到对应的路径。
使用 cd -,可以跳转到上一次的路径。
cd ~ pwd cd /tmp pwd cd - pwd
文件信息获取
ls
ls [参数] [路径]
路径名省略,默认为当前路径
.。常用参数 | 作用 |
-a | all |
-F | 文件后缀 |
-1 | 单行输出 |
-t | 按最新时间顺序 |
-r | 逆序 |
-s | size,单位 1024k |
-b | 转义显示特殊字符 |
通过
ls -l显示判断文件的类型文件类型 | 标识符 |
普通文件 | - |
目录 | d |
符号链接 | l |
字符设备 | c |
块设备 | b |
套接字 | s |
命名管道 | p |
# 注意,第一个字符是文件类型的标识符 /bin/ls -l total 12 -rw------- 1 bingo bingo 1533 Apr 15 10:23 mcfly.d7j4SRuO -rw------- 1 bingo bingo 1650 Apr 15 11:20 mcfly.LDsvamgO drwx------ 3 root root 4096 Apr 15 10:23 systemd-private-347480d36c604f9ea465540e6d7e3bf2-systemd-logind.service-176P6q
stat
stat 路径
stat ~ File: /home/bingo Size: 4096 Blocks: 8 IO Block: 4096 directory Device: 8,16 Inode: 53278 Links: 21 Access: (0700/drwx------) Uid: ( 1000/ bingo) Gid: ( 1000/ bingo) Access: 2025-04-15 12:00:22.033245873 +0800 Modify: 2025-04-15 12:00:15.523247533 +0800 Change: 2025-04-15 12:00:15.523247533 +0800 Birth: 2024-07-05 11:23:52.881209240 +0800
- Access Time (atime)\ 含义 :文件内容最后一次被访问(读取)的时间。例如通过 cat、less 等命令读取文件时会更新。 触发条件 :文件内容被访问,但某些文件系统(如 ext4)默认启用 relatime 优化机制,可能不会实时更新。
- Modify Time (mtime) 含义 :文件内容最后一次被修改的时间。例如通过 vim 编辑文件时会更新。 触发条件 :文件内容发生变更。
- Change Time (ctime) 含义 :文件元数据(如权限、所有者、文件名等)最后一次被修改的时间。例如使用 chmod 或 chown 命令时会更新。 注意 :ctime 无法直接修改,它会随元数据变更自动更新。
- Birth Time (btime/crtime) 含义 :文件创建时间。此属性依赖文件系统和内核支持(例如 ext4 支持,但部分旧文件系统不支持)
文件/目录创建
touch
touch [选项] 文件名...
参数 | 作用 | 示例 |
无参数 | 创建文件(若不存在)或更新访问/修改时间 | touch file.txt |
-a | 仅更新访问时间(Access Time) | touch -a log.txt |
-m | 仅更新修改时间(Modify Time) | touch -m config.conf |
-c | 不创建新文件(仅修改已有文件) | touch -c secret.key |
-t | 指定具体时间(格式: [[CC]YY]MMDDhhmm[.ss]) | touch -t 202405201830.00 log.txt |
-r | 同步其他文件的时间戳(参考文件) | touch -r source.txt target.txt |
-d | 使用字符串指定时间(更灵活) | touch -d "2024-05-20 18:30" log.txt |
mkdir
mkdir -p: 递归创建目录,如果目录存在则跳过。(如果不加-p,文件夹存在会报错)mkdir /tmp/test # 再次执行,命令会报错 mkdir /tmp/test # 这样不会报错 mkdir -p test
文件拷贝和转移
cp/install
cp 命令简洁语法命令。而 install 不仅限于拷贝文件(还可以创建目录、设定权限、用户组),但是语义上更贴合安装。
- cp [src_path] [dst_path]
如果 dst_path 的目录不存在,则会出错,你需要保证文件已经创建了
-r:递归复制目录及其子内容。 -a:保留所有文件属性(权限、时间戳等),相当于 -dpr 的组合。 -i:覆盖前提示确认。 -f:强制覆盖,不提示(通常是默认行为)。 -p:保留文件权限、时间戳等属性。
# 复制文件到目录 cp file.txt /target/dir/ # 递归复制目录 cp -r source_dir/ target_dir/ # 保留属性复制文件 cp -p file.txt /backup/
- install
复制文件/目录并自动设置权限、所有者等属性,常用于软件安装。
常用选项 : -m:设置文件权限(如 -m 755)。 -o/-g:指定所有者和所属组。 -d:创建目录(类似 mkdir -p)。 -D:覆盖目标文件前先删除原文件,避免“文本繁忙”错误
示例:
install file.txt /tmp/ # 复制到 /tmp install file.txt /tmp/new.txt # 复制并重命名 install -m 755 file.txt /tmp/ # 权限设为 rwxr-xr-x install -o root -g root file.txt /tmp/ # 指定用户和用户组 install -d /path/to/new_directory # 创建空目录 install -d source_dir /tmp/ # 复制整个目录 install -D file.txt /tmp/ # 覆盖 /tmp/file.txt
mv
mv [选项] 源文件或目录 目标路径
若目标路径为目录,则将源移动至该目录下;
若目标路径为文件名,则重命名源文件/目录
选项 | 功能 | 示例 |
-i | 覆盖前询问用户确认 | mv -i file.txt backup/ |
-f | 强制覆盖目标文件 | mv -f old.txt new.txt |
-v | 显示操作详细信息 | mv -v *.log logs/ |
-n | 禁止覆盖已存在文件 | mv -n data.csv archive/ |
-u | 仅当源文件更新时覆盖目标 | mv -u new_version.txt old/ |
示例
# 移动文件 mv report.pdf ~/Documents/ # 将文件移动至用户文档目录 mv file1.txt file2.txt /backup/ # 多文件移动 mv old_project/ new_projects/ # 若目标目录存在,源目录会成为其子目录,即 new_projects/old_project # 重命名 mv old_name.txt new_name.txt # 重命名文件 mv old_folder/ new_folder/ # 目标目录不存在时直接重命名
文件删除
rm
r递归删除(删除目录时,必须带上)
f强制
# 删除 rm -f /tmp/*.log
bash 基础语法
bash shell 本身即为 bash 语言的运行环境,类似 python 解释器 shell 可以直接运行 python 代码 而无需创建 py 文件,下面所有的命令可以直接在 bash shell 中运行。保存为文件是为了更好的开发/维护脚本代码。
- 变量定义与基本输入输出
- 命名只能使用英文字母,数字和下划线,首个字符不能以数字开头。
- 中间不能有空格,可以使用下划线 _ 。
- 不能使用标点符号。
- 不能使用 bash 里的关键字
- 所有的变量,即使没有定义也可以直接使用,默认初值为空字符串 ""
- bash 作为脚本,没有明确规定变量类型,默认都为字符串
- bash 不适合用于数值运算,尽量不要在 bash 中定义数值变量。
注意,变量名和等号之间不能有空格。同时,变量名的命名须遵循如下规则:
#!/bin/bash # 解释器声明必须首行 # 单行注释 # 变量定义(等号两侧无空格!!!!) name="Alice" readonly PI=3.14 # 只读变量,不可修改 name="Bob" echo $name ${PI}15926 # 变量调用,变量可以使用{},表明与其他字符串 echo $Pi # 可见 bash 中标识符为大小写敏感,并且初值为空字符串 # 输入输出 read -p "Enter IP: " ip # 带提示输入,输入赋值给 echo "Scanning ${ip}..." # 输出变量 printf "%-20s %s\n" "Host" "$ip" # 使用 printf 系统调用进行格式化输出(和 c 的 printf 为同一系统调用)
- 脚本执行
将上述的代码保存为
script.sh。bash script.sh # 方式 1:使用bash解释器直接执行 chmod +x script.sh && ./script.sh # 方式 2:视作可执行文件执行,需要注意 shell 环境最好为 bash
- 伪断点+回显调试
- 临时暂停
- 回显脚本修改如下
bash 无法像程序编程语言一样采用断点调试,只能通过
set -x 和 set +x 命令回显和通过 read -p临时暂停,实现伪断点式的回显调试的方法。即将每个执行的命令打印到终端中。read -p "pause, enter any button to continue"
name="Alice" read -p "pause, enter any button to continue" set -x # 下面的语句,将在终端中回显输出! readonly PI=3.14 # 只读变量,不可修改 name="Bob" set +x # 关闭回显 read -p "pause, enter any button to continue" echo $name ${PI}15926
命令输出赋值给变量
使用
"$(command)",可以将 command 的内容作为字符串,后续可以赋值给变量。不过你需要注意,如果引用变量的时候不加双引号,echo 输出对于换行等字符会替换为空格。cd ~ ls_result="$(ls -la)" # 观察下面两个输出的差异 echo $ls_result echo "$ls_result"
你会看到很多项目中的 bash 脚本并不严谨,这是因为 开发 bash 脚本的时候通常是效率导向,但是一旦需要构建一个大型的 shell 脚本容易造成很多 bug。因此强烈推荐你遵守shell-check规范来避免脚本不规范带来的错误。不要以为 bash 没有大型的脚本,可以搜索 redis 官方为启动 redis 集群编写的脚本。
序列表达式
语法为{start..end..step}
# 生成数字序列 echo {10..20} 10 11 12 13 14 15 16 17 18 19 20 echo {1..20..2} 1 3 5 7 9 11 13 15 17 19 # 逆序生成 echo {20..1..-2} 20 18 16 14 12 10 8 6 4 2 # 支持字符序列 echo {a..z} a b c d e f g h i j k l m n o p q r s t u v w x y z
特殊内置变量
- 位置参数变量
$0:当前脚本的文件名。例如,echo $0 会输出脚本的名称。$1到$9:脚本的命令行参数,分别表示第 1 到第 9 个参数。例如,$1是第一个参数,$2是第二个参数,依此类推。$#:传递给脚本的参数数量。例如,echo$#会输出参数的个数。$*:将所有参数作为一个整体字符串返回。例如,echo $*会输出所有参数,以空格分隔。$@:将所有参数作为独立的字符串返回。与∗不同,@保留每个参数的独立性,适合用于需要逐个处理参数的场景。
∗不同,
#!/bin/bash echo $0 echo $1 $2 echo $@ echo $* echo 命令行参数个数为$#
bash script.sh para1 para2 para3 # output script.sh para1 para2 para1 para2 para3 para1 para2 para3 命令行参数个数为3
- 进程参数变量
$?:上一个命令的退出状态。通常,0 表示成功,非零值表示失败。例如,echo $?会输出上一个命令的执行结果。$$:当前 Shell 进程的 ID(PID)。例如,echo $$会输出当前脚本的进程 ID。$!:最后一个后台命令的进程 ID。例如,echo $!会输出最近在后台运行的命令的 PID。$_:上一个命令的最后一个参数。例如,echo$_会输出上一个命令的最后一个参数。这个变量,一般适合交互命令中使用,快速获取上一个命令最后一个参数。
- 分隔符变量
$IFS:输入字段分隔符,默认是空格、制表符和换行符。例如,定义IFS=','会将分隔符设置为逗号。
分隔符决定了如 read/awk 等命令获取变量的分隔符,类似 python 中
split()的使用。IFS=',' read -p "input two number split by comma: " p1 p2 # 键入 1,2 echo $p1 $p2
字符串
在 bash 中,变量大部分存储的都是字符串型数据(bash 本身不适合处理数据运算)
字符串可以使用单引号,也可以使用双引号。
- 单引号里的任何字符都会原样输出,因此单引号字符串中的变量引用是无效的;
- 双引号里可以有变量引用,将输出变量的值。
foo=bar # 原封不动输出 echo 'foo\n=$foo' # 打印的是 foo = bar echo "foo=$foo" # echo -e 参数,允许转义 echo -e 'foo\t=\tbar' echo -e "foo\t=\tbar"
字符串变量特殊替换
$ {var:-string}:若变量 var 为空,则用在命令行中用 string 来替换
[root@localhost ~]# a=2 [root@localhost ~]# echo $a 2 [root@localhost ~]# echo ${a:-123} 2 [root@localhost ~]# echo ${b:-123} 123
$ {var:=string},若 var 为空,string 替换后赋值给 var
$ {var:+string}:替换规则和上面的相反,即只有当 var 不是空的时候才替换成 string
${var:?string}; 替换规则为:若变量 var 不为空,则用变量 var 的值来替换。若变量 var 为空,则把 string 输出到标准错误中,并从脚本中退出
字符串裁剪
记忆方法:qwerty 布局 键盘上#在$左边,%在右边
#去掉左边,%去掉右边,但不会改变 variable 的值,pattern 可以使用通配符匹配。- ${variable%pattern}:shell 在 variable 中查找,看它是否匹配给定的模式 pattern 结尾,如果是,就从命令行把 variable 中的内容去掉右边最短的匹配模式。
- ${variable%%pattern}:shell 在 variable 中查找,看它是否匹配给定的模式 pattern 结尾,如果是,就从命令行把 variable 中的内容去掉右边最长的匹配模式。
- ${variable#pattern}:shell 在 variable 中查找,看它是否匹配给定的模式 pattern 开始,如果是,就从命令行把 variable 中的内容去掉左边最短的匹配模式。
- ${variable##pattern}:shell 在 variable 中查找,看它是否匹配给定的模式 pattern 结尾,如果是,就从命令行把 variable 中的内容去掉左边最长的匹配模式。
var=testcase echo $var #输出 testcase echo ${var%s*e} #输出 testca echo ${var%%s*e} #输出 te echo ${var#*s} #输出 tcase echo ${var##*s} #输出 e
场景模拟
在场景模拟之前,需要补充一个命令:
date%Y:四位年份(如2023) %m:两位月份(01-12) %d:两位日期(01-31) %F: 等价于 %+4Y-%m-%d %H:小时(00-23) %M:分钟(00-59) %S:秒(00-60) %T: 等价于 %H:%M:%S %a:缩写星期(如Thu) %A:完整星期(如Thursday) %s:时间戳(秒,从1970-01-01起) %z: 时区
# 按格式输出当前的时间 date "+%Y-%m-%d %H:%M:%S" # 输出 unix 时间戳 date +%s # 将指定的时间戳转换为时间格式 date -d @1696504200 "+%F %T" # 使用 -d 参数进行时间计算 date -d "tomorrow" # 明天此时 date -d "+2 days" # 两天后 date -d "next Friday" # 下周五 date -d "2023-12-25 +1 week" "+%A" # 2023年圣诞节后一周的星期几
当脚本在创建文件名是,文件名可以携带时间信息,这样每次执行脚本,都可以创建出不同的文件。
通常以下的情况,文件名中出现时间信息很有帮助:
- 日志文件
- 备份文件
- 临时文件
我应该使用时间戳还是时间格式?
- 使用时间戳作为文件名的一部分,兼容性好(有的文件系统的文件名有特殊要求),但是可阅读性差。
- 如果使用时间格式,最好带上
%z,以防止时区带来理解上的偏差。
垃圾小文件批量制造
#!/bin/bash # create_trash.sh prefix_path="${1:-.trash/.dummy}" # 这里使用了字符串裁剪语法 mkdir -p "${prefix_path%/*}" time=$(date +%Y%m%d_%H%M%S%z)touch "${prefix_path}"_"${time}"_{1..10000}
使用示例
# 默认在当前路径下 trash_file 目录中生成 bash create_trash.sh # 传入参数,在指定路径下生成 bash create_trash.sh .custom_dir/.dummy
Q. 大量小文件会操作系统带来什么影响?
伪造文件访问时间
#!/bin/bash # fake_time.sh file="$1" days="$2" # 计算伪造时间并修改 timestamp=$(date -d "$days days ago" "+%Y%m%d%H%M")timestamp=$(echo "$timestamp" | tr -d '\r')touch -t "$timestamp" "$file"
使用示例
# 将 secret.log 修改为 3 天前 touch date.log stat date.log ls -l date.log bash fake_time.sh date.log 3 stat date.log ls -l date.log
Q.
ls -l中看到的是什么时间(atime\mtime\ctime\btime)?带回收站功能的删除脚本
#!/bin/bash # rm.sh recycle_dir="$HOME/.recycle_bin/$(date +%Y%m%d_%H%M%S%z)" # 创建唯一回收站目录 mkdir -p "$recycle_dir" # 批量移动文件(自动处理多个参数) mv -v "$@" "$recycle_dir" # 输出结果 echo "[+] 已移动 $# 个文件到回收站:" ls "$recycle_dir"
使用示例
touch report.pdf /tmp/{1..10}.log bash rm.sh report.pdf /tmp/{1..10}.log
敏感文件快速备份
通过参数指定关键系统文件、配置文件(如 /etc/passwd),生成带时间戳的备份副本。
#!/bin/bash # backup.sh file="$1" file=$(echo "$file" | tr -d '\r')# 生成备份文件名 backup_file="${file}_bak_$(date +%Y%m%d_%H%M%S%z)" # 创建备份(保留权限) install -C -m 644 "$file" "$backup_file" # 验证备份 echo "[+] 备份完成:" ls -l "$file" "$backup_file"
使用示例
bash backup.sh /etc/passwd
思考题(一)
- shell 和 terminal(终端)的区别是什么?
-终端(Terminal) 是一个程序,提供用户界面让用户与系统交互 负责显示输出和接收输入 是一个图形化或文本界面的窗口环境 例如:Windows Terminal、iTerm2、GNOME Terminal、Konsole等 -Shell 是一个命令解释器,用于解释用户输入的命令并执行 处理用户在终端中输入的命令 是运行在终端内部的程序 常见的Shell:Bash、Zsh、Fish、PowerShell等
- ls -l 输出中,每一列的涵义是什么?
-文件类型和权限 -连接数(第一列) -所有者(第二列) -所属组(第三列) -文件大小(第四列) -最后一次被修改的时间(第六、七、八列) -文件名(第九列)
- ls 如何实现按创建时间顺序输出或者按文件大小排序输出?
-按修改时间排序:ls -lt -按访问时间排序:ls -ltu -按状态变更时间排序:ls -ltc -按创建时间排序:ls -ls --time -birth ------- -按文件大小排序(从大到小):ls -ls -按文件大小逆序排序(从小到大):ls -lSr
- 使用 root 用户 执行
rm -rf /删除根目录能否成功?为什么?
-不能,该命令为删除Linux的根目录“/”,无法成功 -保护机制:现代Linux系统实现了"--preserve-root"安全保护机制,这是rm命令的默认选项,专门防止误删根目录 -某些关键系统文件在使用中会被锁定,即使root用户也无法删除正在使用的系统文件
⛺基础概念(二)
linux 基础文件操作
链接 ln
- 软链接
软链接是 独立的文件 ,存储的是目标文件的 路径名 (类似于 Windows 的快捷方式)。 它有自己的 inode,但数据块中只保存目标文件的路径。
# -s 创建软链接 ln -s [原文件路径] [目标文件路径]
- 硬链接
硬链接是 指向文件数据块的直接引用 ,与原始文件共享相同的 inode(文件在磁盘上的唯一标识符)。 也可以理解为文件的“别名”, 硬链接和原文件地位平等 ,没有主从关系。
硬链接 不可以跨分区使用 ,软链接是可以的。硬链接不可以对目录使用,软链接可以。
ln [原文件路径] [目标文件路径]
readlink
readlink 是一个用于解析符号链接(软链接)实际路径的命令行工具。
-f 解析符号链接并输出绝对路径(自动递归解析) readlink -f /usr/bin/python # 输出 Python 解释器的真实路径(如 /usr/bin/python3)
文件路径查找
dirname
dirname [path]
输出路径所在的目录,如果使用相对路径,则输出也为相对路径,如果使用绝对路径,则输出为绝对路径。
dirname /usr/bin/ -> "/usr" dirname dir1/str dir2/str -> "dir1" "dir2" dirname stdio.h -> "."
find
find path [参数]
type: 指定查找的文件类型,如fdl等
- maxdepth
name/-iname: 指定要查找的文件名,支持通配符,支持!、&逻辑符号
find ! -name '*.sh' ! -type d # 反向匹配
perm: 指定要查找的文件的权限 mode
find -type f -perm 0777 -print # 找出777权限的文件
[amc]time: 指定文件的 atime,mtime,ctime
exec: 对找出的每个文件文件执行外部命令操作
size指定文件大小- n 是比 n 小,
+n 是比 n 大,
n 正好是 n 。
find -size +5M # 找出大于5M的文件
命令路径查找
which/whereis
- whereis : 快速定位命令的二进制文件、源码和手册页
参数 | 作用 | 应用场景 |
-b | 仅搜索二进制文件 | 确认工具安装位置(如 nmap) |
-m | 仅搜索手册页 | 快速查看命令文档路径 |
-s | 仅搜索源代码 | 审计开源工具源码位置 |
- which :定位 PATH 环境变量中的可执行文件,使用
a参数获取全部的路径。
特性 | whereis | which |
搜索范围 | 系统预定义目录(/bin, /usr 等) | 仅$PATH 环境变量 |
输出内容 | 二进制+手册+源码 | 仅可执行文件 |
搜索速度 | 快(依赖数据库) | 实时搜索 |
典型应用场景 | 开发环境搭建/文档审计 | 路径验证/恶意软件检测/环境变量劫持 |
示例:
# 获取系统上 python3 的路径 whereis python3 which -a python3 which python
alias
别名:对命令重新命名
alias 用于定义别名和查找别名
alias 别名='原命令 [选项] [参数]'
alias ll='ls -l' # 输入 `ll` 代替 `ls -l` alias la='ls -la' # 输入 `la` 代替 `ls -la` alias rm='rm -i' # 让 `rm` 删除前询问确认 alias cp='cp -i' # 让 `cp` 覆盖前询问确认 # 列出所有的别名 alias # 删除别名 unalias ll # 删除 `ll` 别名 unalias -a # 删除所有别名
type
type 可以检测命令类型,判断是否存在(成功返回 0)
type ls # 查看 ls 的类型(可能是别名或外部命令) type -t cd # 输出 "builtin"(cd 是 Bash 内置命令) type -a echo # 显示所有可能的 echo 定义(可能同时是内置命令和外部命令) type -p git # 如果是外部命令,显示路径(如 /usr/bin/git)
command
command 主要用于 绕过 shell 的别名和函数查找机制 ,直接执行指定的命令或显示命令的类型。
v,显示命令的类型
p,使用系统默认 PATH 查找命令
V,更详细地描述命令
command -v ls # 可能输出 'ls' 或 'alias ls=...' command -v cd # 输出 'cd'(内置命令) command -v python # 输出 '/usr/bin/python'(外部命令) command -p ls # 使用系统默认的 ls,而不是用户自定义的
文件压缩与解压
归档:将多个文件/目录,归档为一个文件。
压缩包的本质是先归档后压缩,只是 windows 上常见的
.rar等格式把归档的操作给省略了。linux 常见的压缩包格式是 .tar.gz .tar.xz ,其本质是先将多个文件归档为 .tar 格式,之后再利用 gzip,xz 等工具进行压缩。常见的有
tar 和 zip与 unzip等等。由于 zip 命令, 多数发行版中并不集成,而且在 linux 的常见归档类型中也很少用zip归档,如果对 zip 等命令有需求,可以自行通过 deepseek 学习。tar
多数 linux 发行版自带归档工具为(tar),需要结合不同的压缩工具(如 gzip、xz)一并使用。
tar [选项] [归档文件名] [文件或目录...]
选项 | 说明 |
-c | 创建新的归档文件 |
-x | 解压归档文件 |
-f | 指定归档文件名(必须放在最后) |
-v | 显示详细过程(verbose) |
-z | 使用 gzip 压缩/解压(.tar.gz 或.tgz) |
-j | 使用 bzip2 压缩/解压(.tar.bz2) |
-J | 使用 xz 压缩/解压(.tar.xz) |
-t | 查看归档文件内容(不解压) |
-r | 向归档文件追加文件 |
-C | 解压到指定目录 |
# 准备文件与目录 mkdir -p ~/ops/tar-demo && cd ~/ops/tar-demo touch file1.txt file2.txt mkdir -p dir1/ untar/ # 打包文件(不压缩) tar -cvf archive.tar file1.txt file2.txt dir1/ # 打包文件并压缩 tar -czvf archive.tar.gz file1.txt file2.txt dir1/ # 查看归档文件内容,但是不解压 tar -tvf archive.tar.gz # 解压文件到指定目录 tar -xzvf archive.tar.gz -C untar # 查看最终的目录结构 ls -lR
xz or gzip?
总体而言,
xz压缩性能优于 gzip,gzip的兼容性更好。bash 语法
表达式
运算表达式
bash 并不适合用于运算处理,默认只提供整数运算,不支持浮点。如果需要浮点运算能力,可以用
awk命令工具或者调用python脚本实现。((expr)),运算符基本和 c 一致。
((expr)) 表达式 expression 将被求值。如果 表达式的值非零,返回值就是 0;否则返回值是 1。这种做法和 let "expression" 等价。 id++ id-- 变量自增/自减 (在后) ++id --id 变量自增/自减 (在前) - + (单目的) 取负/取正 ! ~ 逻辑和位取反 ** 乘幂 * / % 乘,除,取余 + - 加,减 << >> 左/右位移 <= >= < > 比较 == != 相等/不等 & 位与 (AND) ^ 位异或 (exclusive OR) | 位或 (OR) && 逻辑与 (AND) || 逻辑或 (OR) expr?expr:expr 条件求值 = *= /= %= += -= <<= >>= &= ^= |= 赋值 expr1 , expr2 逗号表达式
# 示例 a=1 ((a++)) # 在运算表达式的时候,是不需要用$来表示变量 echo $a
let
(()) 本质可以看作是 let 的语法糖
x=1 let x++ echo $x
- 错误处理
# (( )) 和 let 的除零错误 (( 10 / 0 )) # 报错:division by 0(返回非零状态码) let "num=5/0" # 同上
条件表达式
[[ expr ]] 是 bash 中条件表达式的关键字,用 0 和非 0 表示真/假布尔值,支持布尔短路运算。- 字符串判断
操作符 | [[ ]] 用法 | 说明 |
字符非空 | [[ -n $s ]] | 字符串 s 非空为真 |
字符空 | [[ -z $s ]] | 字符串 s 空为真 |
相等判断 | [[ $a == "$b" ]] | [[ ]] 中== 支持模式匹配。 |
不等判断 | [[ $a != "$b" ]] | 同上。 |
模式匹配 | [[ $str == *.txt ]] | 检查字符串是否匹配通配符模式。 |
正则匹配 | [[ $str =~ ^[A-Z]+$ ]] | 使用 =~ 匹配正则表达式。 |
- 文件处理判断
[[ -e filename ]] # 如果 filename 存在,则为真 [[ -d filename ]] # 如果 filename 为目录,则为真 [[ -f filename ]] # 如果 filename 为常规文件,则为真 [[ -L filename ]] # 如果 filename 为符号链接,则为真 [[ -r filename ]] # 如果 filename 可读,则为真 [[ -w filename ]] # 如果 filename 可写,则为真 [[ -x filename ]] # 如果 filename 可执行,则为真 [[ -s filename ]] # 如果文件大小不为0,则为真 [[ -h filename ]] # 如果文件是软链接,则为真
- 数值判断
运算符 | 含义 | 示例 |
-eq | 等于 | [[ "$a" -eq "$b" ]] |
-ne | 不等于 | [[ "$a" -ne 0 ]] |
-gt | 大于 | [[ 10 -gt "$b" ]] |
-ge | 大于或等于 | [[ "$a" -ge 5 ]] |
-lt | 小于 | [[ "$a" -lt 10 ]] |
-le | 小于或等于 | [[ "$a" -le "$max" ]] |
- 关于
[]和[[]]
[[…]] is preferred over [ … ]
[] 本质是调用 test 指令,而 [[]] 才是 bash 中的条件表达式语法;[[]] 相比[],性能更快、语法功能更为强大,也更安全。虽然[]对不同 shell 的兼容性更好(因为本质是 test 命令调用),但是出于目前 bash 作为 linux shell 的主流地位,使用 [[]] 作为条件表示式更为合适。分支语句
- if
if [[ 条件表达式 ]]; then # 条件为真时执行的代码 elif [[ 其他条件表达式 ]]; then # 其他条件分支 else # 所有条件均不满足时执行 fi
# 示例 name="Alice" if [[ $name == "Alice" ]]; then echo "Name is Alice" # 变量无需加引号,[[ ]] 自动处理 fi # 模式匹配(支持通配符) file="image.jpg" if [[ $file == *.jpg ]]; then echo "这是 JPG 文件" fi # 正则表达式匹配 email="user@example.com" if [[ $email =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then echo "邮箱格式有效" fi # 组合条件 if [[ -f "$file" && -r "$file" ]]; then echo "文件存在且可读" fi
unix 中,当程序(指令)的运行成功时,返回值默认为
0;执行失败时,返回非 0。因此if 后可以直接接一个指令来判断指令是否运行成功。如果你对指令返回值有具体的判断(因为很多程序,不同的返回值表示不同的结果),你可以用上一次实验讲过的 $? 来获取。
# 判断 docker 命令是否存在,如果不存在,则要求用户下载docker。 if ! command -v docker; then echo "docker command can't find, please download docker!" fi
case
虽然
case 本身不支持 [[]],但可以在模式中使用通配符,但可以在模式中使用通配符case 变量 in 模式1) # 匹配模式1时执行 ;; 模式2|模式3) # 匹配模式2或模式3时执行 ;; *) # 默认分支(其他情况) ;; esac
file="document_v2.pdf" case $file in *.pdf) echo "PDF 文件" ;; *v[0-9]*.txt) echo "带版本号的文本文件" ;; *.jpeg|*.png) echo "图形文件" ;; *) echo "未知文件" ;; esac
循环语句
- for
# c语言风格遍历 for ((初始值; 终止条件; 步长)) do # 循环体代码 done
# 示例 for ((i=1; i<=5; i++))do echo "C-style: $i" done
# 列表风格遍历 for 变量 in 列表; do # 循环体代码 done
# 示例 # 遍历数字序列(固定范围) for i in {1..5}; do echo "Number: $i" done # 遍历命令输出结果(例如遍历当前目录的 .txt 文件) for file in *.txt; do echo "Processing file: $file" done
- while
while [[ 条件 ]]; do # 循环体代码 done
# 找到第一个大于5的数时退出 num=1 while [[ $num -le 10 ]]; do if [[ $num -eq 6 ]]; then break fi echo "Number: $num" ((num++))done # 跳过偶数 num=1 while [[ $num -le 5 ]]; do ((num++))if (( num % 2 == 0 )); then continue fi echo "Odd: $num" done
- until
until 和 while 类似,但是逻辑相反:条件值为假时继续循环;为真时结束循环。
# 直到计数器大于5时停止 count=1 until [[ $count -gt 5 ]]; do echo "Until: $count" ((count++))done # 等待某个文件存在 until [[ -f /tmp/ready.lock ]]; do echo "Waiting for file..." sleep 1 done
数组
- 下标索引数组
# 定义数组(元素用空格分隔) ip_list=("192.168.1.10" "10.0.0.5" "172.16.254.1") # 访问数组元素(索引从0开始) echo "第一个IP:${ip_list[0]}" # 输出192.168.1.10 # 获取所有元素 echo "所有目标:${ip_list[@]}" # 获取数组长度 length=${#ip_list[@]} echo ${length} # 数组下标式遍历 for ((i=0; i<$length; i++)); do echo "正在扫描 ${ip_list[$i]}" done # 数组迭代式遍历 for ip in "${ip_list[@]}"; do echo "正在扫描 $ip" done # 数组追加元素 ip_list+=("192.168.1.20") # 移除元素,通过下标 unset ip_list[0] echo "new ip target: ${ip_list][@]}"
- map
# 使用 declare 显式声明数组 declare -A color # 后续追加元素 color["red"]="#ff0000" color["green"]="#00ff00" color["blue"]="#0000ff" # 也可以在声明时直接赋值 declare -A fruits=(["apple"]=1 ["banana"]=1 ["cherry"]=1) # 获取所有的 value echo ${color[@]} # 获取 key 的数量 echo ${#color[@]} # 获取所有的 key echo ${!color[@]} # 遍历map for k in ${!color[@]}; do echo "key: $k , value: ${color[${k}]}" done # 移除元素,通过key值 unset color["red"] echo $
场景模拟
dd
之前介绍过
touch 命令,这里将介绍一个更为底层的文件创建命令 dd。在后面的场景模拟中,部分的文件需要通过这个命令来生成。dd 命令用于低级别数据转换和复制。它以块为单位操作磁盘数据,常用于磁盘克隆、创建镜像文件、数据擦除等场景。\
参数 | 描述 |
if=FILE | 输入文件(默认为标准输入) |
of=FILE | 输出文件(默认为标准输出) |
bs=BYTES | 设置读写块大小(默认 512 字节) |
count=N | 只复制前 N 个块 |
skip=N | 跳过输入文件的前 N 个块 |
seek=N | 跳过输出文件的前 N 个块 |
conv=CONV | 转换选项(ascii,ebcdic,ucase 等) |
下面是 dd 命令的演示,不要随意执行,因为涉及到了具体的磁盘设备
# 将整个磁盘/dev/sda克隆到/dev/sdb dd if=/dev/sda of=/dev/sdb bs=4M status=progress # 创建磁盘镜像 dd if=/dev/sda of=disk.img bs=4M # 生成1GB测试文件,可用来测试磁盘写入速率 dd if=/dev/zero of=testfile bs=1G count=1 # 测试读取速度 dd if=testfile of=/dev/null bs=1G # 用零填充整个磁盘 dd if=/dev/zero of=/dev/sdX bs=4M status=progress # 安全擦除(使用随机数据) dd if=/dev/urandom of=/dev/sdX bs=4M status=progress
恶意文件溯源检测
#!/bin/bash # malicious_link_check.sh target_dir=${1:-/tmp/malicious_link} echo "[+] 开始扫描可疑软链接文件..." link_files=() # 使用数组存储查找结果 for file in $(find "$target_dir" -type l); do link_files+=("$file") done for link in "${link_files[@]}"; do real_path=$(readlink -f "$link")echo "$link -> $real_path" done echo "[+] 共发现 ${#link_files[@]} 个可疑链接"
仿真场景准备
env_dir="/tmp/malicious_link" mkdir -p "$env_dir"/{bin,etc} touch "$env_dir"/target_file # 创建不同类型符号链接,模拟恶意文件软连接 ln -s /etc/passwd "$env_dir"/etc_link ln -s "$env_dir"/target_file "$env_dir"/valid_link ln -s /non/existent/path "$env_dir"/broken_link
开始模拟
# 扫描目标目录 bash malicious_link_check.sh /tmp/malicious_link # 清除模拟环境(可选) rm -rf /tmp/malicious_link/
Q: 如果是硬链接,脚本还能扫描出来吗?自行编写场景准备代码,并给出仿真结果?
env_dir="/tmp/malicious_hardlink" mkdir -p "$env_dir" touch "$env_dir/target_file" # 创建硬链接 ln "$env_dir/target_file" "$env_dir/hard_link1" ln "$env_dir/target_file" "$env_dir/hard_link2"
bash malicious_link_check.sh /tmp/malicious_hardlink
仿真结果: 脚本不会发现任何“可疑链接”,因为 find -type l 找不到硬链接。 输出类似: [+] 开始扫描可疑软链接文件... [+] 共发现 0 个可疑链接
旧日志归档
#!/bin/bash #log_archive.sh target_dir=${1:-/var/log} overdate_day=${2:-30} # 单位 天 (( overdate_second=overdate_day*24*60*60 ))now=$(date +%s)backup_name="oldlogs_$(date +%Y%m%d).tar.gz" echo "[+] 开始归档${overdate_day}天前的旧日志..." old_files=() # 查找超过30天的日志文件 for file in $(find "$target_dir" -type f -name "*.log"); do if [[ $((now - $(stat -c %Y "$file"))) -gt overdate_second ]]; then old_files+=("$file") fi done if [[ ${#old_files[@]} -gt 0 ]]; then echo "发现 ${#old_files[@]} 个旧日志文件" tar -czf "$backup_name" "${old_files[@]}" echo "[+] 归档完成: $backup_name" else echo "[!] 未找到需要归档的日志" fi
仿真场景准备
env_dir="." mkdir -p "$env_dir/var/log" # 生成不同时间特征的日志文件 for i in {1..5}; do # 超过30天的旧日志 touch -d "31 days ago" "$env_dir/var/log/old_log_$i.log" # 30天内的新日志 touch -d "$((29 - i)) days ago" "$env_dir/var/log/new_log_$i.log" done
开始模拟
bash log_archive.sh ./var/log # 查看最终归档的文件 ls *.tar.gz # 清理模拟的环境(可选) rm -rf ./var
(base) ┌──(root㉿localhost)-[~/exp2] └─# bash log_archive.sh ./var/log [+] 开始归档30天前的旧日志... 发现 5 个旧日志文件 [+] 归档完成: oldlogs_20250508.tar.gz
隐藏文件搜索
#!/bin/bash #hidden_file_detect.sh target_path=${1:-.} file_size_threshold=${2:-1M} # 默认是M #解析单位并转换字节 if [[$input_threshold =~ ^([0~9]+)([GgMmKk]?)$ ]]; then num=${BASH_REMATCH[1]} nuit=${BASH_REMATCH[2]} case "$nuit" in G|g) file_size_threshold=$((num*1024*1024*1024));; M|m) file_size_threshold=$((num*1024*1024));; K|K) file_size_threshold=$((num*1024));; *) file_size_threshold=$((num*1024*1024));; esac else echo "输入格式错误,正确示例: 1G 100M 500K 10" exit 1 fi echo $file_size_threshold echo "[*] 扫描${target_path}路径下的异常隐藏文件中..." suspicious_files=() # 查找大于指定大小的文件 for file in $(find ${target_path} -type f -name "._*"); do if [[ $(stat -c %s "$file") -ge $file_size_threshold ]]; then suspicious_files+=("$file") fi done echo "发现 ${#suspicious_files[@]} 个可疑文件:" for file in "${suspicious_files[@]}"; do echo "$file ($(($(stat -c %s "$file") / 1024))KB)" done # 如果发现可疑文件,则归档并删除 if [[${#suspicious_file[@]}] -gt 0]; then archive_name = "suspicious_file_$(date+%Y%m%d%H%M%S).tar.gz" tar -czf "$archive_name" "${suspicious_files[@]}" echo "[+] 已归档为 $archive_name" #删除文件 for file in "${suspicious_file[@]}"; do rm -f "$file" done echo "[+] 已删除源文件,实现隔离" else echo "[!] 未发现可以文件,无需归档" fi
仿真环境准备
# 正常隐藏文件 env_dir='.' touch "$env_dir"/.hidden_file touch "$env_dir"/._small_file # 可疑隐藏文件 # 创建超1MB文件 dd if=/dev/urandom of="$env_dir"/._cache1 bs=1M count=2 dd if=/dev/urandom of="$env_dir"/._malware bs=1M count=3
bash hidden_file_detect.sh
(base) ┌──(root㉿localhost)-[~/exp2] └─# bash hidden_file_detect.sh . 1048576 [*] 扫描.路径下的异常隐藏文件中... 发现 2 个可疑文件: ./._cache1 (2048KB) ./._malware (3072KB)
Q1. 当找到这些可疑文件后,可以通过 tar 方式将这些可疑文件进行归档隔离,并删除源文件。请完善相关代码!
Q2.
file_size_threshold=${2:1} 假定了文件门限阈值是1M,这个对用户输入很不友好。假定用户的输入可以自行带单位(如果没有带单位则是M),单位可以是 G/M/K。请完善这个用户输入友好的脚本。提示: 可以使用 case 分支+正则匹配语句来处理
思考题(二)
- 文档里描述的
alias,仅在当前的 shell 会话中生效,一旦关闭或者打开新的 shell 会话则失效。那么如何长期保存定义的别名? - 将定义的alias添加进入.bashec(Bash用户) 或 .zshrc(Zsh用户)文件
vim ./.bashrc 文件末尾添加alias: alias ll='ls -l' 保存并关闭文件 source ~/.bashrc #让配置立即生效
- 显然有这个需求:只解压压缩包中的某一个文件而非全部的文件,自行查阅资料,给出对应的指令。
-只解压 tar/tar.gz/tar.xz 包中的某一个文件 假设压缩包名为 archive.tar.gz,你只想解压其中的 dir1/file1.txt 文件: tar -xzvf archive.tar.gz dir1/file1.txt -x:解包 -z:gzip 压缩(.tar.gz/.tgz 用) -v:显示详细过程 -f:指定归档文件 dir1/file1.txt:只解压这个文件 如果是 .tar.xz,用 -J 替换 -z: tar -xJvf archive.tar.xz dir1/file1.txt -只解压 zip 包中的某一个文件 假设压缩包名为 archive.zip,只解压 dir1/file1.txt: unzip archive.zip dir1/file1.txt -tar 包:tar -xzf 包名 文件路径 zip 包:unzip 包名 文件路径 只需在解压命令后加上你想要的文件路径即可,无需解压全部内容。