一、几个简单的内置 shell 命令
前面的内容主要是记住就行,这里开始注重逻辑思维,写稍微复杂点的脚本,本阶段内容都很有用。
1. echo
-n 不换行输出,-e 解析字符串的特殊符号(\n 换行、\r 回车、\t 制表符、\b 退格符)。
echo 命令通过 ; 隔开默认有换行:
ubuntu@VM-0-9-ubuntu:~$ echo "111"; echo "222"
111
222
第一个 echo 加入 -n 会取消换行:
ubuntu@VM-0-9-ubuntu:~$ echo -n "111"; echo "222"
111222
后面 echo 加:
ubuntu@VM-0-9-ubuntu:~$ echo "111"; echo -n "222"
111
222ubuntu@VM-0-9-ubuntu:~$
使用 -e 换行:
ubuntu@VM-0-9-ubuntu:~$ echo -e "111\n222"
111
222
ubuntu@VM-0-9-ubuntu:~$ echo "111\n222"
111\n222
顺便记一下,ubuntu 的 /bin/sh 是 dash,dash 的 echo 默认就解析转义,所以同样的写法在 sh 和 bash 下结果可能不一样,写脚本时想要稳定就用 printf。
2. printf
和 c 一样的:
ubuntu@VM-0-9-ubuntu:~$ printf "11122\n33"
11122
33ubuntu@VM-0-9-ubuntu:~$ printf "11122\n33\n"
11122
33
ubuntu@VM-0-9-ubuntu:~$
3. eval
执行多个命令:
ubuntu@VM-0-9-ubuntu:~$ eval ls ; cd /etc
nohup.out special.sh
ubuntu@VM-0-9-ubuntu:/etc$
eval 的作用是把参数扫描两次,第一次做变量替换和命令展开,第二次执行展开后的命令,上面 eval ls 里没有变量和特殊符号,eval 展开后还是 ls,跟直接敲 ls 没区别。
只有当命令里嵌了变量、反引号、或者类似 echo $(pwd) 这种需要二次解析的情况,eval 才显作用,正常脚本里很少用到。
4. exec
不创建子进程,执行后续命令且执行完毕自动 exit:
ubuntu@VM-0-9-ubuntu:/etc$ su - root
Password:
root@VM-0-9-ubuntu:~# exec date
Tue Jul 21 03:43:14 PM CST 2026
ubuntu@VM-0-9-ubuntu:/etc$
二、变量字串
1. 语法
name="yuchao" 这样先赋值,然后:
${变量}返回变量值${#变量}返回变量值的长度(字符数)${变量:4}从4索引开始找,输出ao${变量:4:1}从4偏移开始提取1长度的子串,输出a${变量#word}从变量开头删除最短匹配的 word 子串,${name#yu}输出chao${变量##word}从变量开头删除最长匹配的 word 子串${变量%word}从变量结尾删除最短匹配的 word 子串${变量%%word}从变量结尾删除最长匹配的 word 子串${变量/pattern/string}用 string 替换第一个匹配的 pattern${变量//pattern/string}用 string 替换所有匹配的 pattern
2. 统计字符串长度
运维需要频繁对文本进行处理,文本存储在文件、变量等复杂形式中。
Shell 变量截取字符串有 2 种:指定位置、指定字符串。
ubuntu@VM-0-9-ubuntu:~/shell$ cat test.txt
server-test
11111111112312313
diuhkjnfdnxcgtyhnm17age
# 统计行数
ubuntu@VM-0-9-ubuntu:~/shell$ cat test.txt | wc -l
3
# 找出最长的一行并输出个数
ubuntu@VM-0-9-ubuntu:~/shell$ cat test.txt | wc -L
23
# 数值计算expr length [字符串]统计长度
ubuntu@VM-0-9-ubuntu:~/shell$ echo $name
yuchao180
ubuntu@VM-0-9-ubuntu:~/shell$ expr length $name
9
# awk对一整行统计长度
ubuntu@VM-0-9-ubuntu:~/shell$ echo $name | awk '{print length($0)}'
9
还有很多方法,但是最快的是 echo ${#name}。
for 循环简单介绍一下:
# for 变量名 in 序列1~100
# for number in {1..100}
# do
# 事情
# done
for num in {1..100};do echo $num;done
1
2
...
100
seq 是生成序列的命令:
ubuntu@VM-0-9-ubuntu:~/shell$ seq 3
1
2
3
-s 指定分隔符(默认换行):
ubuntu@VM-0-9-ubuntu:~/shell$ seq -s ":" 3
1:2:3
# 结合for命令
ubuntu@VM-0-9-ubuntu:~/shell$ for n in {1..3};do str1=`seq -s ":" 10`;echo $str1;done
1:2:3:4:5:6:7:8:9:10
1:2:3:4:5:6:7:8:9:10
1:2:3:4:5:6:7:8:9:10
# 结合time命令,计算统计字符的时间
time for n in {1..10000};do char=`seq -s "666" 100`;echo ${#char} &>/dev/null;done
real 0m28.473s # 实际运行时间
user 0m9.382s # 用户态执行时间
sys 0m19.627s # 内核态执行时间
# 由此可知${#变量}计算时间为28s
# 使用管道符效率一定很低
time for n in {1..10000};do char=`seq -s "666" 100`;echo ${char}|wc -l &>/dev/null;done
real 0m51.802s
user 0m19.627s
sys 0m41.030s
# 使用expr命令length函数统计
time for n in {1..10000};do char=`seq -s "666" 100`;echo ${char}|expr length ${char} &>/dev/null;done
real 0m50.750s
user 0m19.367s
sys 0m41.355s
# awk处理
time for n in {1..10000};do char=`seq -s "666" 100`;echo ${char}|echo $char|awk '{print length($0)}' &>/dev/null;done
real 0m55.953s
user 0m29.133s
sys 1m7.104s
Shell 编程使用 linux 内置的函数、命令效率最高。
反引号把命令执行结果当作字符串(不是单引号),要解析命令就用反引号。
3. 重定向到 /dev/null
/dev/null 是 Linux 里的一个特殊文件,所有写进去的内容都会被直接丢弃。它的作用是让输出“消失”,而不是像 rm 那样删除文件。
Shell 脚本里经常需要控制信息的流向,主要涉及三种标准流:
- 0:标准输入 (stdin)
- 1:标准输出 (stdout)
- 2:标准错误 (stderr)
重定向符号 > 默认操作的是标准输出,所以 > file 等价于 1> file;标准错误要单独写 2> file,两个都要就写 &> file(或者 >file 2>&1)。追加用 >>,不覆盖原内容。
所以脚本里常见的 &>/dev/null 就是把正常输出和报错一起扔进黑洞,屏幕上干干净净。
三、详解字符串
1. 正则会区分大小写
a*c匹配 a 开头、中间任意字符、c 结尾的字符串a*C匹配 a 开头、中间任意字符、C 结尾的字符串
2. 删除匹配的子串
# 索引删除匹配
ubuntu@VM-0-9-ubuntu:~$ name="I ma chaoge"
ubuntu@VM-0-9-ubuntu:~$ echo ${name:2:2}
am
ubuntu@VM-0-9-ubuntu:~$ echo ${name:2:4}
am c
# 从开头匹配字符删除
ubuntu@VM-0-9-ubuntu:~$ name2="abcABC123ABCabc"
ubuntu@VM-0-9-ubuntu:~$ echo ${name2#a*c}
ABC123ABCabc
# 开头必须是a
# 匹配到了最短的a到c,也就是开头的abc部分
ubuntu@VM-0-9-ubuntu:~$ echo ${name2##a*c}
ubuntu@VM-0-9-ubuntu:~$
# 匹配到了开头的a和结尾的c,全删除
# 从结尾匹配字符删除
ubuntu@VM-0-9-ubuntu:~$ echo ${name2%a*c}
abcABC123ABC
# 结尾必须是c
ubuntu@VM-0-9-ubuntu:~$ echo ${name2%%a*c}
ubuntu@VM-0-9-ubuntu:~$
ubuntu@VM-0-9-ubuntu:~$ echo ${name2%%a*C}
abcABC123ABCabc
# 结尾没有大写C,不会删除
3. 替换字符串
# 替换字串(不修改原变量)
ubuntu@VM-0-9-ubuntu:~$ echo ${str1/man/boy}
Hello,boy,i am your brother
# 替换匹配到的第一个
ubuntu@VM-0-9-ubuntu:~$ echo ${str1//o/O}
HellO,man,i am yOur brOther
# 如上
四、删除文件名案例
1. 准备数据

需求:去掉上图所有文件的 _finished 后缀。
文件直接赋给变量,获取的只是文件名,没有内容。
修改变量和修改文件名无关。
2. 单个文件改名的思路
- 不使用 shell 语法:用 mv 一个个改
- 利用变量字符串语法:

替换语法并不改变原有文件名,图片仅仅是演示。
- 利用变量反引号:
ubuntu@VM-0-9-ubuntu:~/sub_str$ echo date
date
ubuntu@VM-0-9-ubuntu:~/sub_str$ echo `date`
Tue Jul 21 05:38:33 PM CST 2026
反引号可以取到命令执行的结果。
ubuntu@VM-0-9-ubuntu:~/sub_str$ echo $f
chaochao_1_finished.jpg
ubuntu@VM-0-9-ubuntu:~/sub_str$ echo ${f/_finished/}
chaochao_1.jpg
ubuntu@VM-0-9-ubuntu:~/sub_str$ ll
total 8
drwxrwxr-x 2 ubuntu ubuntu 4096 Jul 21 17:32 ./
drwxr-x--- 7 ubuntu ubuntu 4096 Jul 21 17:31 ../
-rw-rw-r-- 1 ubuntu ubuntu 0 Jul 21 17:32 chaochao_1_finished.jpg
-rw-rw-r-- 1 ubuntu ubuntu 0 Jul 21 17:32 chaochao_1_finished.png
-rw-rw-r-- 1 ubuntu ubuntu 0 Jul 21 17:32 chaochao_2_finished.jpg
-rw-rw-r-- 1 ubuntu ubuntu 0 Jul 21 17:32 chaochao_2_finished.png
-rw-rw-r-- 1 ubuntu ubuntu 0 Jul 21 17:32 chaochao_3_finished.jpg
-rw-rw-r-- 1 ubuntu ubuntu 0 Jul 21 17:32 chaochao_3_finished.png
-rw-rw-r-- 1 ubuntu ubuntu 0 Jul 21 17:32 chaochao_4_finished.jpg
-rw-rw-r-- 1 ubuntu ubuntu 0 Jul 21 17:32 chaochao_4_finished.png
-rw-rw-r-- 1 ubuntu ubuntu 0 Jul 21 17:32 chaochao_5_finished.jpg
-rw-rw-r-- 1 ubuntu ubuntu 0 Jul 21 17:32 chaochao_5_finished.png
ubuntu@VM-0-9-ubuntu:~/sub_str$ mv $f `echo ${f//_finished/}`
ubuntu@VM-0-9-ubuntu:~/sub_str$ ll
total 8
drwxrwxr-x 2 ubuntu ubuntu 4096 Jul 21 17:47 ./
drwxr-x--- 7 ubuntu ubuntu 4096 Jul 21 17:31 ../
-rw-rw-r-- 1 ubuntu ubuntu 0 Jul 21 17:32 chaochao_1_finished.png
-rw-rw-r-- 1 ubuntu ubuntu 0 Jul 21 17:32 chaochao_1.jpg
-rw-rw-r-- 1 ubuntu ubuntu 0 Jul 21 17:32 chaochao_2_finished.jpg
-rw-rw-r-- 1 ubuntu ubuntu 0 Jul 21 17:32 chaochao_2_finished.png
-rw-rw-r-- 1 ubuntu ubuntu 0 Jul 21 17:32 chaochao_3_finished.jpg
-rw-rw-r-- 1 ubuntu ubuntu 0 Jul 21 17:32 chaochao_3_finished.png
-rw-rw-r-- 1 ubuntu ubuntu 0 Jul 21 17:32 chaochao_4_finished.jpg
-rw-rw-r-- 1 ubuntu ubuntu 0 Jul 21 17:32 chaochao_4_finished.png
-rw-rw-r-- 1 ubuntu ubuntu 0 Jul 21 17:32 chaochao_5_finished.jpg
-rw-rw-r-- 1 ubuntu ubuntu 0 Jul 21 17:32 chaochao_5_finished.png
ubuntu@VM-0-9-ubuntu:~/sub_str$ echo $f
chaochao_1_finished.jpg
这里修改变量的操作成功了,不代表修改变量就是修改了文件,二者是不一样的。这个操作是这样的:
$f 的结果是文件名,而 ${f//_finished/} 是 chaochao_1.jpg,相当于在当前目录执行:
mv chaochao_1_finished.jpg chaochao_1.jpg
最关键的是最后变量没变,只是作为修改文件的字符。
f="chaochao_1_finished.jpg" 是变量赋值,${f//_finished/} 是字符串替换,echo 输出新字符串,这三步都发生在内存里;一直到 mv 旧名 新名 才真正动磁盘上的文件。
3. 批量替换文件名(日志切割经常使用)
需求:去掉 jpg 文件的 _finished。
# 找出需要替换的文件
ubuntu@VM-0-9-ubuntu:~/sub_str$ ls *.jpg
chaochao_1.jpg chaochao_3_finished.jpg chaochao_5_finished.jpg
chaochao_2_finished.jpg chaochao_4_finished.jpg
ubuntu@VM-0-9-ubuntu:~/sub_str$ ls *fin*.jpg
chaochao_2_finished.jpg chaochao_3_finished.jpg chaochao_4_finished.jpg chaochao_5_finished.jpg
ubuntu@VM-0-9-ubuntu:~/sub_str$ for file in `ls *fin*.jpg`;do echo $file;done
chaochao_2_finished.jpg
chaochao_3_finished.jpg
chaochao_4_finished.jpg
chaochao_5_finished.jpg
# 替换
ubuntu@VM-0-9-ubuntu:~/sub_str$ for file in `ls *fin*.jpg`;do mv $file `echo ${file//_finished/}`;done
ubuntu@VM-0-9-ubuntu:~/sub_str$ ls *.jpg
chaochao_1.jpg chaochao_2.jpg chaochao_3.jpg chaochao_4.jpg chaochao_5.jpg
这一页其实就两件事:几个内置命令先把手上顺手了,然后记住 ${var//旧/新} 这种字符串替换根本不碰磁盘, 真想改文件名得靠 mv,日志切割那种批量改名就是这么拼出来的。