一、Shell脚本介绍
1、Shell脚本的作用
(1)在Linux开发中通常涉及终端敲命令,对于一些标准的流程,每次执行都要敲一大串雷同的命令会比较麻烦,Shell脚本就是把多条命令写到纯文本文件中,批量自动执行,类似于Windows的bat批处理文件。
(2)在嵌入式开发中,只需掌握Shell脚本的基础用法即可,通常需要使用Shell脚本完成一键编译、环境配置、测试、烧录辅助等操作。
(3)Shell脚本的约定后缀为.sh,不过Linux系统不依赖后缀识别脚本。
2、Shell脚本的写法和运行
(1)Shell脚本本质是纯文本文件,运行时默认按顺序逐行执行,它支持条件语句和循环语句改变流程。
(2)Shell脚本的第一行必须是"#!/bin/bash",它被称为释伴行(shebang),在命令行中用"./<脚本文件名>"直接执行脚本时,它可告诉操作系统使用bash解释器来解析本脚本。
(3)运行Shell脚本的两种方式(假设脚本名为test.sh):
bash
# 方式1:主动调用bash解释器,不看shebang,不需要脚本有执行权限
bash test.sh # bash -x test.sh,增加选项-x,在执行的时候会打印每一条执行的指令
# 方式2:直接执行,内核读取shebang;脚本必须有执行权限x
./test.sh
二、Shell脚本语法入门
1、注释与信息打印
(1)运行Shell脚本时,如果希望在运行过程中打印一些信息,可以使用echo命令,它的地位相当于C语言中的printf函数,用法如下:
①语法与可选选项:
echo 选项 <字符串> # 方括号表示可选参数,选项用"-"引出
|----|-------------------------|
| 选项 | 作用 |
| -n | 不加该参数时,打印完字符串默认换行 |
| -e | 开启转义字符解析,不加该参数将不会识别转义字符 |
| -E | 关闭转义字符解析,该参数可加可不加 |
常见的转义字符:
|------------|-------------------------------|
| 转义字符 | 含义 |
| \n | 换行 |
| \n | Tab水平制表 |
| \\ | 打印一个反斜杠"\" |
| \" | 打印一个双引号""" |
| \033[xxm | 设置后续打印内容的颜色,xx为待定系数,如果为0则表示重置 |
②基础示例:
1脚本文件:
bash
#!/bin/bash
# ==========================================
# Shell 中 echo 命令的用法示例
# ==========================================
echo "--- 1. 默认,自动换行 ---"
echo hello
echo "(这是一个普通的换行输出)"
echo ""
echo "--- 2. -n 不换行 ---"
echo -n "hello"
echo "world"
echo "(上面两行连在了一起)"
echo ""
echo "--- 3. -e 启用转义 ---"
echo "3.1 换行符 \\n:"
echo -e "aaa\nbbb"
echo ""
echo "3.2 制表符 \\t (Tab):"
echo -e "a\tb"
echo ""
echo "3.3 回车符 \\r (覆盖行首):"
# 注意:\r 会让光标回到行首,后续输出的 "bbb" 会覆盖掉前面的 "aaa"
echo -e "aaa\rbbb"
echo "(上面这行,你只能看到 bbb,因为 aaa 被覆盖了)"
echo ""
2运行结果:
bash
zevalin@zevalin-VMware-Virtual-Platform:~/桌面$ bash test.sh
--- 1. 默认,自动换行 ---
hello
(这是一个普通的换行输出)
--- 2. -n 不换行 ---
helloworld
(上面两行连在了一起)
--- 3. -e 启用转义 ---
3.1 换行符 \n:
aaa
bbb
3.2 制表符 \t (Tab):
a b
3.3 回车符 \r (覆盖行首):
bbb
(上面这行,你只能看到 bbb,因为 aaa 被覆盖了)
③引号的作用:
1如果字符串不用引号包裹,也能输出内容,但是连续的空格和Tab键会被压缩为单个空格,"*"和"?"会被glob通配符展开。
bash
echo hello world
#输出 hello world(多个空格被压缩)
echo *.sh
#打印当前目录所有sh文件名,而不是原样输出*.sh
2如果字符串用双引号包裹,字符串内容能正确输出,通配符不会展开,但"()"命令会被替换("()"命令见下一小节)。
bash
NAME="test"
echo "name = $NAME"
#输出 name = test
echo "hello world"
#保留中间多个空格
3如果字符串用单引号包裹,字符串完全按原样输出,变量不解析、转义字符不解析。
bash
echo 'name = $NAME'
#输出 name = $NAME ,$NAME不会替换成变量值
④输出到文件:
1在语句末尾添加" > 文件名",可以把echo打印的内容以覆盖的方式式写入文件。
2在语句末尾添加" >> 文件名",可以把echo打印的内容以追加的方式式写入文件。
bash
# > 覆盖写入(清空原有内容)
echo "hello" > log.txt
# >> 追加写入(在文件末尾加,不覆盖)
echo "new line" >> log.txt
(2)C语言中使用双斜杠"//"引出注释,而在Shell脚本文件中,则是使用"#"引出注释。
2、变量定义与引用
(1)脚本的位置参数:
①在运行Shell脚本时,用户可以在指令"bash test.sh"后面添加参数,多个参数用空格分隔,这些参数叫做位置参数,Shell脚本可以对它们进行引用。
②Shell脚本引用位置参数的方式:
|-----|----------------------------------|
| () | 含义 |
| 0 | 脚本名 |
| n | 第n个位置参数,n为待定系数 |
| # | 最后一个位置参数的标号 |
| ? | 上一条命令执行是否成功的结果 |
| @ | 等价于1 2 ...... $n,n为最后一个位置参数的标号 |
③基础示例:
1脚本文件:
bash
#!/bin/bash
echo "file name:" $0 # 引号不是echo参数的边界,引号外仍可追加内容
echo "total param num:" $#
echo "whole param:" $@
echo "first param:" $1
echo "second param:" $2
2运行结果:
bash
zevalin@zevalin-VMware-Virtual-Platform:~/桌面$ bash test.sh 114 514
file name: test.sh
total param num: 2
whole param: 114 514
first param: 114
second param: 514
(2)自定义变量:
①除了位置参数以外,Shell脚本允许自定义变量,定义的语法为:
bash
VAR=VALUE # VAR是变量名,VALUE是变量的初值;等号两边不能有空格
# 需要注意Shell本身没有类型,变量默认全是字符串,如果VALUE正好能表示数字,才可以拿去做算数运算
②变量的访问方式有两种,如下所示:
bash
echo $VAR # VAR是变量名
echo ${VAR} # VAR是变量名
③变量值的类型是字符串,如果字符串中有空格或者特殊字符,那就要用双引号把它包起来。
④使用set命令(不加任何参数)可以查看所有变量。
⑤基础示例:
1脚本文件:
bash
#!/bin/bash
# 定义3个测试变量
var_name="Zevalin"
var_num=2026
var_city="Shenzhen"
# 使用set命令查看这三个变量
set
2运行结果:
bash
# 使用set命令同时还会打印系统变量,这可以通过文本处理工具grep过滤,此处暂时先不介绍
(3)环境变量:
①按照常规方式定义变量,那它只是本地变量,仅在当前的shell进程有效,如果脚本里调用了别的程序或者子脚本,它们是无法感知这个变量的存在的。
②如果在定义变量时,在变量定义语句前加关键字export,即可将其定义为环境变量,派生的子进程、子脚本都能读取。
bash
export VAR=VALUE # VAR是变量名,VALUE是变量的初值
③使用env/printenv命令可以查看所有环境变量。
1printenv命令用法:
bash
printenv # 打印全部环境变量
printenv <变量名> # 单独打印指定环境变量
2env命令用法:
bash
env # 打印全部环境变量
env <变量名>=<值> <命令> # 临时设置环境变量(可多个),只给后面这条命令使用
env -i bash # 清空全部环境变量,在干净环境下执行命令
④基础示例:
1脚本文件:
bash
#!/bin/bash
# 定义3个测试变量
export var_name="Zevalin"
export var_num=2026
export var_city="Shenzhen"
# 使用env命令查看这三个变量
env
2运行结果:
bash
# 使用env命令同时还会打印系统变量,这可以通过文本处理工具grep过滤,此处暂时先不介绍
(4)只读变量:
①一些变量被定义后,可能会希望它只被读访问,而不希望它被改写(类似于const变量),这时可以在变量定义语句前加关键字readonly,即可将其定义为只读变量。
bash
readonly VAR=VALUE # VAR是变量名,VALUE是变量的初值
# VAR=NEW_VALUE # 无法修改只读变量
②一旦设置了readonly,变量不能重新赋值,也不能用unset删除,适合脚本里写死的常量(如版本号、路径等)。
(5)变量删除:
①如果希望删除某个变量(或者函数),可以使用unset命令,使用方法如下:
bash
unset [选项] <变量名/函数名>
|----|-----------------|
| 选项 | 作用 |
| -v | 默认选项,表示需要删除的是变量 |
| -f | 表示需要删除的是Shell函数 |
②unset不能删除只读变量和位置参数,但环境变量是可以用unset删除的。如果希望清空位置参数,可运行指令"set --"。
③unset只是删除当前Shell中的变量,不影响父Shell(即调用它的Shell)。
(6)Shell变量的加载顺序:
①Shell变量有加载顺序,对于同名的变量,后加载的会覆盖先加载的,大体的覆盖规则为全局系统配置先加载于用户个人配置。
②bash严格区分三类Shell,三类加载文件的优先级规则不同,具体如下:
1交互式登录Shell(如SSH登录):先加载/etc/profile(面向所有用户),内部会自动source /etc/profile.d/*.sh目录下所有脚本;然后按顺序~/.bash_profile → ~/.bash_login → ~/.profile查找,只加载第一个存在的文件;再加载~/.bashrc(用户终端配置),~/.bashrc内部再加载/etc/bash.bashrc。
2交互式非登录Shell(如直接在Ubuntu打开图形界面终端):先加载~/.bashrc,~/.bashrc内部再加载/etc/bash.bashrc。
3非交互式Shell(直接执行Shell脚本):默认不加载任何/etc/profile、~/.bashrc、~/.profile,如果环境变量BASH_ENV有值,就加载BASH_ENV指定的文件。
③source命令的作用就是在当前Shell进程里面,读取并执行目标文件里的命令(以及自定义变量),不新建子Shell,用法和bash类似,具体如下:
bash
source <文件名> [位置参数]
. <文件名> [位置参数] # 等价简写
3、数组操作
(1)数组的定义方式:
①一次性赋值:
bash
ARR=(VALUE1 VALUE2 VALUE3 ......)
# 数组元素之间用空隔分隔,如果是带空格的字符串,则需要加引号
②逐个按下标赋值:
bash
ARR[0]=VALUE1
ARR[1]=VALUE2
ARR[n]=VALUE3 # n≥0
# 下标可以不连续,中空的部分内容为空
③定义空数组:
bash
ARR=()
(2)数组元素的读取方式:
bash
${ARR[0]} # 读取ARR数组下标0索引的元素
${ARR[n]} # 读取ARR数组下标n索引的元素(n≥0)
${ARR[@]} # 取出ARR数组全部元素(空格分开独立元素,可用于for循环)
${ARR[*]} # 取出ARR数组全部元素,当成一整个字符串(默认IFS空格拼接)
(3)数组长度的获取方式:
bash
${#ARR[@]} # 获取数组ARR元素总个数(常用)
${#ARR[n]} # 获取数组ARR下标n这个字符串的字符长度(n≥0)
(4)数组切片(截取一部分)方式:
bash
# 格式:${<数组名>[@]:<起始下标>:<截取数量>}
ARR=(VALUE1 VALUE2 VALUE3 VALUE4 ......)
echo ${ARR[@]:1:3} # 从下标1开始,取3个元素 → VALUE1 VALUE2 VALUE3
(5)对数组修改或追加元素的方式:
bash
# 修改数组ARR下标n(n≥0)的元素:ARR[n]=NEW_VALUE
ARR[2]="new" # 修改数组ARR下标2的元素为"new"
# 在数组ARR末尾追加若干个元素:ARR+=(VALUE1 VALUE2 ......)
ARR+=("grape" "pear") # 在数组ARR末尾依次追加元素"grape"、"pear"
(6)删除数组元素或整个数组的方式:
bash
unset ARR[1] # 删除ARR数组下标1的元素,数组长度变化,但下标不会自动重排
unset ARR # 删除整个ARR数组
4、字符串操作
(1)引号与命令替换:
|-----|------|----------------------------------------|---------------------------------|
| 符号 | 名称 | 规则 | 示例 |
| ' ' | 单引号 | 包含的字符串原样输出,不解析变量、不解析命令替换、不识别转义,但内部空格压缩 | echo 'PATH' →输出"PATH" |
| " " | 双引号 | 包含的字符串输出前解析变量、解析命令替换,内部空格不压缩,通配符*不展开 | echo "PATH" →输出PATH的真实值 |
| () | 命令替换 | 执行小括号内的命令,命令的输出结果替换进字符串中 | VAR=$(echo VALUE) →等价于VAR=VALUE |
(2)字符串长度的获取方式:
${#<字符串变量名>} # 输出字符个数
(3)字符串子串的截取方式:
${<字符串变量名>:<起始位置>:长度}
起始位置支持填写负数,表示从最后一个字符开始取到倒数第几个字符(正序输出)
长度是可选项,省略则默认取到末尾
(4)字符串部分字符的替换方式(此处的"替换"不改变变量本身的值,相当于引用时先拷贝个副本,做个替换再使用):
bash
str="a b a c"
# ${var/old/new} 只替换第一个匹配old的字符为new
echo ${str/a/1} # 输出1 b a c
# ${var//old/new} 全局替换,全部匹配old的字符都换为new
echo ${str//a/1} # 输出1 b 1 c
(5)在文件名处理中,还可以使用字符串操作实现前缀或后缀的删除(此处的"删除"也不改变变量本身的值):
bash
str="test.txt"
${str#*.} # 删掉最短前缀匹配,输出后缀txt(*是通配符,字符串从左往右数第一个"."开始,取后面的内容,注意不取第一个".")
${str##*.} # 删掉最长前缀匹配,输出后缀txt(*是通配符,字符串从左往右数最后一个"."开始,取后面的内容,注意不取最后一个".")
${str%.*} # 删掉最短后缀匹配,输出前缀test(*是通配符,字符串从左往右数最后一个"."开始,取前面的内容,注意不取最后一个".")
${str%%.*} # 删掉最长后缀匹配,输出前缀test(*是通配符,字符串从左往右数第一个"."开始,取前面的内容,注意不取第一个".")
5、数值计算
(1)整数运算:
①在Shell脚本中,也可实现整数运算,不过需要注意的是,参与整数运算的变量只能是表示整数的字符串,并且有特定的语法。
bash
echo $((a + b)) # 输出a+b的结果
echo $((a - b)) # 输出a-b的结果
echo $((a * b)) # 输出a*b的结果
echo $((a / b)) # 输出a/b的结果,向下取整
echo $((a % b)) # 输出a%b的结果
②可以在定义变量时使用let命令直接使用两个整数的运算结果给变量赋值。
bash
let c=a+b # 把a+b的结果赋给变量c
let c=a-b # 把a-b的结果赋给变量c
let c=a*b # 把a*b的结果赋给变量c
let c=a/b # 把a/b的结果赋给变量c
let c=a%b # 把a%b的结果赋给变量c
(2)小数运算:
①bash本身无法实现小数运算(浮点运算),需要依赖外部工具,比如bc和awk,其中bc为Ubuntu自带的工具。
②在echo时,指令末尾添加"| bc",表示使用bc工具识别小数,而在打印的字符串中,如果涉及小数运算,则需要用字符串"scale=n"声明保留的小数位数n(直接按n位截断,不会四舍五入),语法为------"echo "scale=<小数位数>;<运算表达式>" | bc"。bc工具的使用示例:
bash
a=10
b=3
echo "scale=4; $a / $b" | bc
# 输出 3.3333
echo "scale=2; 2.5 + 3.7" | bc
# 输出 6.20
# 支持比较(bc返回1真,0假)
echo "1.2 > 0.9" | bc
6、逻辑运算
(1)test命令的使用:
①test命令可用于判断表达式是否成立,它还有另一种写法------中括号" ",语法如下,其中对于中括号的写法,表达式和中括号之间必须留空格。(test命令常配合if判断使用)
test <表达式>
\<表达式\>
②test命令可用于字符串的判断:
bash
[ "$s1" = "$s2" ] # 判断字符串s1和s2是否相等(注意使用的是"="而不是"==")
[ "$s1" != "$s2" ] # 判断字符串s1和s2是否不等
[ -z "$s" ] # -z表示判断字符串s长度是否为0(空)
[ -n "$s" ] # -n表示判断字符串s长度是否不为0(非空)
③test命令也常用于测试文件:
bash
[ -e file ] # 判断文件/目录file是否存在
[ -f file ] # 判断文件/目录file是否是普通文件
[ -d file ] # 判断文件/目录file是否是目录
[ -r file ] # 判断文件/目录file是否可读
[ -w file ] # 判断文件/目录file是否可写
[ -x file ] # 判断文件/目录file是否可执行
[ -s file ] # 判断文件/目录file是否存在且大小不为空
(2)整数的比较本质就是一个条件表达式,比较运算符的语法如下:
bash
[ $a -eq $b ] # 判断整数a和b是否相等
[ $a -ne $b ] # 判断整数a和b是否不相等
[ $a -gt $b ] # 判断整数a是否大于b
[ $a -ge $b ] # 判断整数a是否大于等于b
[ $a -lt $b ] # 判断整数a是否小于b
[ $a -le $b ] # 判断整数a是否小于等于b
(3)Shell脚本中的逻辑运算符和C语言有较大出入,在Shell脚本的条件表达式中,"!"表示非,"-a"表示与,"-o"表示或,使用举例如下:
bash
[ ! -f a.txt ] # 判断a.txt是否不是不同文件
[ -d testdir -a -r testdir ] # 判断testdir是否是目录且是否可读,同时成立才为真
[ -f a.txt -o -f b.txt ] # 判断a.txt是否是普通文件或b.txt是否是普通文件,任一条件成立则为真
(4)Shell脚本也有"&&"和"||"的语法,但它们不算是逻辑运算符,而是命令连接符,用法如下:
①cmd1 && cmd2:cmd1返回0(真),才执行cmd2。
②cmd1 || cmd2:cmd1返回非0(假),才执行cmd2。
③举例------查看文件是否存在:
bash
#!/bin/bash
echo "please input file name:"
read -p "file name:" filename # read命令用于交互,用户需输入文件名
test -e $filename && echo "$filename exist" || echo "$filename no exist" #判断用户输入的文件名是否存在,如存在则输出exist,cmd1 && cmd2的结果为真,"||"后面的命令不执行,否则cmd1 && cmd2的结果为假"||"后面的命令执行,输出no exist