摘要:

  主要介绍Shell基本语法。

一、第一个shell脚本

1.什么是Shell?

  Shell是一个用 C 语言编写的程序,它是用户使用 Linux 的桥梁。既是一种命令语言,又是一种程序设计语言Shell 是指一种应用程序,这个应用程序提供了一个界面,用户通过这个界面访问操作系统内核的服务。

2.什么是Shell脚本?

  shell脚本语言是解释型语言,本质是**shell命令的有序集合Shell脚本文件的后缀名称是.sh**。

3.Hello World!

1
2
#!/bin/bash
echo "Hello World!"

  这段代码是一个最简单的Shell脚本程序,就像C语言学习室的第一个程序一样。

  • #! 是一个约定的标记,它告诉系统这个脚本需要什么解释器来执行,即使用哪一种 Shell
  • echo命令用于向命令行窗口输出文本。

4.如何运行Shell脚本?

  • 进入Shell脚本所在目录
1
cd <file_path>
  • 赋予shell文件执行权限
1
chmod permissions file_name.sh

  permissions为要赋予文件的权限(常用777或者740,保证文件具有可执行权限即可)。

  • 执行Shell脚本文件
1
./file_name.sh

二、Shell变量

1.变量的命名

  shell允许用户建立变量存储数据,但不支持数据类型(整型、字符、浮点型),将任何赋给变量的值都解释为一串字符。变量定义格式如下(支持三种形式):

1
2
3
Variable=value   # 第一种
Variable='value' # 第二种
Variable="value" # 第三种

【注意】

  (1)等号两边不允许有空格

  (2)命名只能使用英文字母数字下划线首个字符不能以数字开头,且中间不能有空格。

  (3)不能使用标点符号,而且不能使用bash里的关键字(可用help命令查看保留关键字)。

  (4)第二种与第三种的区别:

  以单引号' '包围变量的值时,单引号里面是什么就输出什么,即使内容中有变量和命令(命令需要反引起来)也会把它们原样输出。这种方式比较适合定义显示纯字符串的情况,即不希望解析变量、命令等的情况。

  以双引号" "包围变量的值时,输出时会先解析里面的变量和命令,而不是把双引号中的变量名和命令原样输出。这种方式比较适合字符串中附带有变量和命令并且想将其解析后再输出的变量定义。

2.变量的赋值

  在 Bash shell 中,每一个变量的值都是字符串,无论给变量赋值时有没有使用引号,值都会以字符串的形式存储。这就意味着,Bash shell 在默认情况下不会区分变量类型,即使将整数和小数赋值给变量,它们也会被视为字符串。

  • 直接赋值
1
Variable=value
  • 用语句赋值
1
2
Variable=`command`
Variable=$(command)

  用语句进行赋值时,语句一定要写在 `` 符号里边,该符号为英文状态下 Esc 按键下边的哪个按键。
  或者也可以使用下边的 $() 的方式,这种方式看起来要更直观一些。

## 3.变量的使用

  使用一个定义过的变量,只要在变量名前面加美元符号($)即可。

1
2
${Variable}
$Variable

【注意】

  (1)变量名外面的花括号是可选的,加不加都行,加花括号是为了帮助解释器识别变量的边界。推荐给所有变量加上花括号。

  (2)定义变量和给变量赋值的时候都不需要加$,使用的时候才需要加上。

4.变量的类型

  • 局部变量

  在脚本或命令中定义,仅在当前shell实例中有效,其他shell启动的程序不能访问局部变量。

  • 环境变量

  所有的程序,包括shell启动的程序,都能访问环境变量,有些程序需要环境变量来保证其正常运行。必要的时候shell脚本也可以定义环境变量。

  • shell变量

  shell变量是由shell程序设置的特殊变量。shell变量中有一部分是环境变量,有一部分是局部变量,这些变量保证了shell的正常运行。

5.Bourne Shell中的常用的四种变量

5.1用户自定义变量

  就是用户自己定义的变量,在shell编程中,通常使用全大写,例如

1
COUNT=1

5.2位置变量 

  在 Linux 的命令行中,当一条命令或脚本执行时,后面可以跟多个参数,可以使用位置参数变量来表示这些参数。

$n n 为数字,$0 代表命令本身,$1-$9 代表第 1-9 个参数,10 以上的参数需要用大括号{}包含, 例如${10},${15}
$* 代表命令行中所有的参数,把所有的参数看成一个整体
$@ 代表命令行中所有的参数,不过 $@ 把每个参数区别对待
$# 代表命令行中所有参数的个数

【注意】

(1)$n中,n大于等于10时,一定要加上{}

(2)$*$@

  当 $*$@ 不被双引号" "包含时,它们之间没有任何区别,都是将接收到的每个参数看做一份数据,彼此之间以空格来分隔,两者都以"$1" "$2" … "$n" 的形式输出所有参数。

  当 $*$@ 被双引号" "包含时,"$*"会将所有的参数从整体上看做一份数据,而不是把每个参数都看做一份数据,以"$1 $2 … $n"的形式输出所有参数;"$@"仍然将每个参数都看作一份数据,彼此之间是独立的,以"$1" "$2" … "$n" 的形式输出所有参数。

变量 输出形式
被 " " 包含 不被 " " 包含
$* "$1 $2 … $n" "$1" "$2" … "$n"
$@ "$1" "$2" … "$n" "$1" "$2" … "$n"

例如:某命令传递了 5 个参数,那么对于"$*"来说,这 5 个参数会合并到一起形成1个数据,这一个数据包含了5个参数,它们之间是无法分割的;而对于"$@"来说,这 5 个参数是相互独立的,它们是 5 个数据。

5.3预定义变量

  预定义变量是在Shell一开始时就定义的变量,这一点和默认环境变量有些类似。不同的是,预定义变量不能重新定义,用户只能根据 Shell 的定义来使用这些变量。严格来说,位置参数变量也是预定义变量的一种,只是位置参数变量的作用比较统一,所以一般把位置参数变量单独划分为一类变量。

$? 最后一次执行的命令的返回状态。如果这个变量的值为 0,则证明上一条命令正确执行;如果这个变量的值为非0 (具体是哪个数由命令自己来决定),则证明上一条命令执行错误。
$$ 代表当前进程的进程号(PID)。
$! 代表后台运行的最后一个进程的进程号(PID)。

5.4环境变量

  环境变量也称为全局变量,可以在创建它们的Shell及其派生出来的任意子进程Shell中使用,环境变量又可以分为自定义环境变量和**bash内置的环境变量**。

  环境变量可以在命令行中设置和创建,用户退出命令行时这些变量值就会丢失,那如何永久保存环境变量呢?可以在用户家目录下的.bash_profile(或者是.profile).bashrc(非用户登录模式特有,如:SSH)文件中,这两个文件设置后是针对特定用户的,也可以在/etc/profile文件中定义环境变量,该文件设置后将会影响所有的用户,就是说在这三个文件中进行定义的环境变量在用户登录后就会进行初始化。

例如:下图为~/.profile文件内容。

  • 常用环境变量
HOME /etc/passwd文件中列出的用户主目录
PATH 系统默认的可执行文件搜索路径
HISTSIZE 保留历史命令的数目上限
OSTYPE 系统类型
PWD 当前工作目录
TERM 终端类型,常用的有vt100,ansi,vt200,xterm等
PS1, PS2 默认提示符($)及换行提示符(>)
IFS Internal Field Separator, 默认为空格,tab及换行符

6.字符串 

  在Shell中,字符串可以用单引号,也可以用双引号,也可以不用引号。

  • 单引号

  单引号里的任何字符都会原样输出,单引号字符串中的变量是无效的。

  单引号字串中不能出现单独一个的单引号(对单引号使用转义符后也不行),但可成对出现,作为字符串拼接使用。

1
str='string'
  • 双引号

  双引号里可以有变量,也可以出现转义字符。

6.1拼接字符串

  • 使用单引号拼接
1
2
3
4
5
6
7
#!/bin/bash 
str1='World'

str2='Hello,'$str1' !'
str3='Hello, $str1 !'
echo $str2
echo $str3

输出结果为:

1
2
Hello,World !
Hello, $str1 !
  • 使用双引号拼接
1
2
3
4
5
6
7
#!/bin/bash 
str1="World"

str2="Hello, "$str1" !"
str3="Hello, $str1 !"
echo $str2
echo $str3

输出结果为:

1
2
Hello, World !
Hello, World !

6.2获取字符串长度

1
${#<string>}

string为要获取长度的目标字符串变量。

例如:

1
2
3
4
5
6
#!/bin/bash 
str1='World'
str2="World"

echo "str1's length=${#str1}"
echo "str1's length=${#str2}"

输出结果为:

1
2
str1's length=5
str1's length=5

6.3提取字符串

1
${<string>:n:len}
  • string为要获取长度的目标字符串变量。
  • n为起始字符位置,len为截取字符串的长度,提取出的字符串为n位置处字符极其之后的len-1个字符。

【注意】字符串中首个字符的索引为0

例如:

1
2
3
4
5
6
#!/bin/bash 
str1='Hello, World'
str2="Hello, World"

echo "${str1:2:3}"
echo "${str2:2:7}"

输出结果为:

1
2
llo
llo, Wo

6.4查找字符串中字符

可以通过expr实现字符串中特定字符的查找,这种查找方式会查找字符串中首次出现该字符的位置。

1
Variable=`expr index "${<string>}" <char>` 
  • string为要查找的目标字符串变量。
  • char为要查找的字符。

【注意】

  (1)在查找过程中,字符串中首个字符的索引为`1``。

  (2)以上脚本中 ` 是反引号,而不是单引号 **’**。

  (3)char可以是多个字符,例如对于acac两个字符哪个先出现,就会停止查找,返回首先出现的字符在目标字符串中的首次出现的位置。

例如:

1
2
3
4
5
6
#!/bin/bash 
str1='Hello, World'
str2="Hello, World"

echo `expr index "$str1" ' '`
echo `expr index "$str2" l`

输出结果为:

1
2
7
3

7.数组 

  bash支持一维数组(不支持多维数组),并且没有限定数组的大小。数组元素的下标由0开始。获取数组中的元素要用下标,下标可以是整数或算术表达式,其值应大于或等于0。

7.1定义一个数组

  在 Shell 中,用括号来表示数组,数组元素用<space>符号分割开。

  • 直接定义
1
数组名=(值1 值2 ... 值n)

例如:

1
version=(v1 v2 v3)

或者

1
2
3
version=(v1 
v2
v3)
  • 单独定义各个分量
1
2
3
数组名[0]=值0
数组名[1]=值1
数组名[n]=值n

【注意】可以不使用连续的下标,而且下标的范围没有限制。

7.2引用数组元素 

  • 获取数组中某个元素
1
${arrayName[index]}

【注意】index0开始

  • 获取数组中所有元素
1
2
${arrayName[@]}
${arrayName[*]}

7.3获取数组长度 

1
2
3
4

length=${#arrayName[@]} # 取得数组元素的个数
length=${#arrayName[*]} # 取得数组元素的个数
lengthn=${#arrayName[n]} # 取得数组单个元素的长度

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/bash 
a=(v1.1 v2 v3.3 )

# 取得数组元素的个数
length=${#a[@]}
echo "a's length=${length}"
length=${#a[*]}
echo "a's length=${length}"
# 取得数组单个元素的长度
lengthn=${#a[0]}
echo "a[0]'s length=${lengthn}"
lengthn=${#a[1]}
echo "a[1]'s length=${lengthn}"
lengthn=${#a[n]}
echo "a[n]'s length=${lengthn}"

运行结果如下:

1
2
3
4
5
a's length=3
a's length=3
a[0]'s length=4
a[1]'s length=2
a[n]'s length=4

三、Shell替换

  如果表达式中包含特殊字符,Shell 将会进行替换。

1.变量替换

  变量替换可以根据变量的状态(是否为空、是否定义等)来改变它的值。

${Variable} 变量本来的值
${Variable:-word} 如果变量 Variable 为空或已被删除(unset),那么返回 word,但不改变 Variable 的值。
${Variable:=word} 如果变量 Variable 为空或已被删除(unset),那么返回 word,并将 Variable 的值设置为 word。
${Variable:?message} 如果变量 Variable 为空或已被删除(unset),那么将消息 message 送到标准错误输出,可以用来检测变量 Variable 是否可以被正常赋值。
若此替换出现在Shell脚本中,那么脚本将停止运行。
${Variable:+word} 如果变量 Variable 被定义,那么返回 word,但不改变 Variable 的值。

2.命令替换

  命令替换是指Shell先执行命令,将输出结果暂时保存,在适当的地方输出。

1
`command` # 注意是反引号 ` ,按键位于 Esc下方

例如:

1
2
3
#!/bin/bash
DATE=`date`
echo "Date is $DATE"

运行结果为:

1
Date is 2022年 02月 09日 星期三 18:27:51 CST

四、Shell运算符

  Shell 支持很多运算符,包括算数运算符关系运算符布尔运算符字符串运算符文件测试运算符。原生bash不支持简单的数学运算,但是可以通过其他命令来实现,例如 awkexprexpr 最常用,它是一款表达式计算工具,使用它能完成表达式的求值操作。

1.算术运算符

运算符 说明 实例(设变量a=6, b=3)
+ 加法 `expr $a + $b` 结果为 9
- 减法 `expr $a - $b` 结果为 3
* 乘法,注意要加 \ `expr $a \* $b` 结果为 18
/ 除法 `expr $a / $b` 结果为 2
% 取余 `expr $b % $a` 结果为 0
= 赋值 a=$b 把变量 b 的值赋给 a
== 相等,比较两个数字,相同则返回 true [ $a == $b ] 返回 false
!= 不相等,比较两个数字,不相同则返回 true [ $a != $b ] 返回 true

【注意】

  (1)表达式和运算符之间要有空格,例如 $a+$b 是不对的,必须写成 $a + $b

  (2)使用expr时,完整的表达式要被 **` ** 包含。

  (3)赋值操作时,=两端不能有空格。

  (4)条件表达式要放在方括号之间,并且要有空格,例如: [$a==$b] 是错误的,必须写成 **[ $a == $b ]**。

  (5)乘号(*)前边必须加反斜杠(\)才能实现乘法运算。

2.关系运算符

  关系运算符只支持数字,不支持字符串,除非字符串的值是数字。

运算符 说明 实例(设变量a=6, b=3)
-eq 相等 == [ $a -eq $b ] 返回 false
-ne 不相等 != [ $a -ne $b ] 返回 true
-gt 大于 > [ $a -gt $b ] 返回 true
-lt 小于 < [ $a -lt $b ] 返回 false
-ge 大于等于 >= [ $a -ge $b ] 返回 true
-le 小于等于 <= [ $a -le $b ] 返回 false

3.布尔运算符

运算符 说明 实例(设变量a=6, b=3)
! ! [ ! false ] 返回 true
-o | [ $a -lt 7 -o $b -gt 10 ] 返回 true
-a & [ $a -lt 7 -o $b -gt 10 ] 返回 false

4.逻辑运算符

运算符 说明 实例(设变量a=6, b=3)
&& 逻辑的 and [[ $a -lt 10 && $b -gt 10 ]] 返回 false
|| 逻辑的 or [[ $a -lt 10 || $b -gt 10 ]] 返回 true
## 5.字符串运算符
运算符 说明 实例(设变量a="abc", b="abd")
= 检测两个字符串是否相等,相等返回 true [ $a = $b ] 返回 false
!= 两个字符串是否不相等,不相等返回 true [ $a != $b ] 返回 true
-z 字符串长度是否为0,为0返回 true [ -z $a ] 返回 false
-n 字符串长度是否不为 0,不为 0 返回 true [ -n $a ] 返回 true
$ 字符串是否为空,不为空返回 true [ $a ] 返回 true

6.文件测试运算符

  文件测试运算符用于检测 Unix 文件的各种属性

运算符 说明
-b file 检测文件是否是块设备文件,如果是,则返回 true。
-c file 检测文件是否是字符设备文件,如果是,则返回 true。
-d file 检测文件是否是目录,如果是,则返回 true。
-f file 检测文件是否是普通文件(既不是目录,也不是设备文件),如果是,则返回 true。
-g file 检测文件是否设置了 SGID 位,如果是,则返回 true。
-k file 检测文件是否设置了粘着位(Sticky Bit),如果是,则返回 true。
-p file 检测文件是否是有名管道,如果是,则返回 true。
-u file 检测文件是否设置了 SUID 位,如果是,则返回 true。
-r file 检测文件是否可读,如果是,则返回 true。
-w file 检测文件是否可写,如果是,则返回 true。
-x file 检测文件是否可执行,如果是,则返回 true。
-s file 检测文件是否为空(文件大小是否大于0),不为空返回 true。
-e file 检测文件(包括目录)是否存在,如果是,则返回 true。
-L file 检测测文件是否为符号链接,如果是,则返回 true。
file1 -nt file2 测试文件 file1 是否比文件 file2 更新。
file1 -ot file2 测试文件 file1 是否比文件 file2 更旧。

五、Shell语句

  shell 脚本程序由零或多条shell语句构成。 shell语句包括三类:说明性语句功能性语句结构性语句

1.说明性语句

  其实就是注释,以#号开始到该行结束,不被解释执行。sh里没有多行注释,只能每一行加一个#号。

1
2
3
4
5
6
#--------------------------------------------
# 这是一个注释
# author:
# file name:
# description:
#--------------------------------------------

2.功能性语句

2.1只读变量readonly

1
readonly Variable # Variable为变量名称

  使用 readonly 命令可以将变量定义为只读变量,只读变量的值不能被改变。

2.2删除变量unset

1
unset Variable # Variable为变量名称

2.3read

  read用来从标准输入中读取数据并赋值给变量。如果没有进行重定向,默认就是从键盘读取用户输入的数据;如果进行了重定向,那么可以从文件中读取数据。

1
read [option] [Variables]
[option] -a array 把读取的数据赋值给数组 array,从下标 0 开始
-d delimiter 用字符串 delimiter 指定读取结束的位置,而不是一个换行符(默认结束符号为换行符,读取到的数据不包括 delimiter)。
-n num 读取 num 个字符就结束,而不是整行字符。如果没有读满 num 个字符就按下回车或遇到换行符,则也会结束读取。
-N num 严格要求读满 num 个字符才自动结束读取,即使中途按下了回车或遇到了换行符也不结束,其中换行符或回车算一个字符。
-p prompt 显示提示信息,提示内容为 prompt。默认不支持"\n"换行,要换行需要特殊处理(例如:通过 $'string \n' 就可以实现换行输入)
-t timeout 设置超时时间,单位为秒。如果用户没有在指定时间内输入完成,那么 read 将会返回一个非 0 的退出状态,表示读取失败,即使已经输入了一部分。
-u fd 使用文件描述符 fd 作为输入源,而不是标准输入,类似于重定向。
-e 在输入的时候可以使用命令补全功能(Tab键)。
-r 原样读取(Raw mode),不把反斜杠字符解释为转义字符,这意味着"\"会变成文本的一部分。
-s 静默模式(Silent mode),不会在屏幕上显示输入的字符。可用于输入密码和其它确认信息的时候。
[Variables] 变量名称

【注意】

  (1)可以使用多个参数。

  (2)变量读取说明

1
read [-ers] [-a aname] [-d delim] [-i text] [-n num] [-N num] [-p prompt] [-t timeout] [-u fd] [var_name1 var_name2 ...]

  首先说一下IFS(internal field separator,为内部字段分隔符,是bash shell中的环境变量,它定义了bash shell用作字段分隔符的一系列字符,默认情况下,bash shell会将下列字符当做字段分隔符:空格制表符换行符

  read命令用于从标准输入中读取输入单行,并将读取的单行根据IFS环境变量分裂成多个字段,并将分割后的字段分别赋值给指定的变量列表var_name。第一个字段分配给第一个变量var_name1,第二个字段分配给第二个变量var_name2,依次到结束。如果指定的变量名少于字段数量,则多出的字段数量也同样分配给最后一个var_name,如果指定的变量命令多于字段数量,则多出的变量赋值为空。如果没有指定任何var_name,则分割后的所有字段都存储在环境变量REPLY中。

例如

1
2
3
#!/bin/bash
read -p $'Enter your name: \n'
echo $REPLY

运行结果:

1
2
3
Enter your name: 
qidaink
qidaink

  (3)给多个变量赋值时,必须在一行内输入所有的值,不能换行,否则只能给第一个变量赋值,后续变量都会赋值失败。

2.4test

  test 命令用于检查某个条件是否成立,它可以进行数值、字符和文件三个方面的测试。通常和 if 语句一起使用,并且大部分 if 语句都依赖 test

  以下只为举例说明使用格式,还有一些运算符可以查看本章《第四节 Shell运算符》。

  • 数值测试
1
test $[num1] <关系运算符> $[num2]
运算符 -eq 等于,则为真
-ne 不等于,则为真
-gt 大于,则为真
-ge 大于等于,则为真
-lt 小于,则为真
-le 小于等于,则为真
  • 字符串测试
1
test $[num1] <字符串运算符> $[num2]
运算符 = 等于,则为真
!= 不等于,则为真
-z 字符串 字符串的长度为零,则为真
-n 字符串 字符串的长度不为零,则为真
  • 文件测试
1
test <文件测试运算符> file_name
运算符 -e file 如果文件存在,则为真
-f file 如果文件存在且可读,则为真
### 2.5`echo`

2.5.1基本参数

  echo用于在屏幕上打印出指定的字符串,命令格式如下:

1
echo [option] string
-n 禁止在最后自动换行
-e 开启转义字符功能,即遇到以下字符进行处理,而不会将其当成一般字符输出
\a 警报,响铃
\b 退格(删除键)
\f 换页(FF),将当前位置移到下页开头
\n 换行
\c 不换行
\r 回车
\t 水平制表符(tab键)
\v 垂直制表符
\ooo 插入ooo(八进制)所代表的ASCII字符
\\ 反斜杠
--version 显示版本信息
--helo 显示帮助
【注意】该命令不带参数时**自带换行**。

2.5.2实例

  • 显示普通字符串
1
2
3
4
#!/bin/bash
echo "string1"
echo "string2"
echo string3

运行结果:

1
2
3
string1
string2
string3
  • 显示转义字符
1
2
3
#!/bin/bash
echo "\"string\""
echo \"string\"

运行结果:

1
2
"string"
"string"
  • 显示变量
1
2
3
4
5
#!/bin/bash
name=qidaink
echo $name
echo "$name"
echo "${name}"

运行结果:

1
2
3
qidaink
qidaink
qidaink
  • 显示换行与不换行
1
2
3
4
5
6
7
#!/bin/bash
name=qidaink
echo -e "$name\nString1" # 开启转义字符,\n 换行
echo "$name\nString1"

echo -e "$name\c" # 开启转义字符,\c不换行
echo "String2"

运行结果:

1
2
3
4
qidaink
String1
qidaink\nString1
qidainkString2
  • 显示结果定向至文件
1
2
3
#!/bin/bash
name=qidaink
echo -e "$name\nString1" > file.txt

运行结果:

  在当前Shell脚本所在目录创建一个filt.txt文件(无该文件的话),并将要显示的内容显示在filt.txt文件中。

  • 原样输出字符串,不进行转义或取变量(用单引号)
1
2
3
4
#!/bin/bash
name=qidaink
echo "$name\nString1"
echo '$name\nString1'

运行结果:

1
2
qidaink\nString1
$name\nString1
  • 显示命令执行结果
1
2
3
4
#!/bin/bash
echo `date`
echo 'date'
echo "date"

运行结果:

1
2
3
2022年 02月 14日 星期一 13:29:17 CST
date
date

2.6printf

  printfPOSIX 标准所定义,因此使用 printf 的脚本比使用 echo 移植性好。printf 使用引用文本或空格分隔的参数,外面可以在 printf中使用格式化字符串,还可以制定字符串的宽度、左右对齐方式等。默认的 printf 不会像echo 自动添加换行符。使用格式如下:

1
printf  "format-string>"  [arguments...]

其中format-string 为格式控制字符串,arguments 为参数列表。Shell脚本中的printfC语言中的printf用法基本一致,只是在写法上有些许区别。

  • printf 命令不用加括号
  • format-string 可以没有引号,但最好加上,单引号双引号均可。
  • 参数多于格式控制符(%)时,format-string 可以重用,可以将所有参数都转换。
  • arguments 使用空格分隔,不用逗号。

2.6.1 转义字符

\a 警报,响铃
\b 退格(删除键)
\c 不显示输出结果中任何结尾的换行字符,而且任何留在参数里的字符、任何接下来的参数以及任何留在格式字符串中的字符都被忽略。
\f 换页(FF),将当前位置移到下页开头
\n 换行
\r 回车
\t 水平制表符(tab键)
\v 垂直制表符
\ooo 插入ooo(八进制)所代表的ASCII字符
\\ 反斜杠字符
#### 2.6.2 格式控制字符串
  • 格式控制字符
格式字符 形式 意义
i, d %i, %d 以十进制形式输出带符号整数(正数不输出符号)
u %u 以十进制形式输出无符号整数
o %o 以八进制形式输出无符号整数(不输出前缀0)
x, X %x, %X 以十六进制形式输出无符号整数(不输出前缀0x)
f %f 以小数形式输出单、双精度实数
e, E %e, %E 以指数形式输出单、双精度实数
g, G %g, %G 以 %f 或 %e 中较短的输出宽度输出单、双精度实数
c %c 输出单个字符
s %s 输出字符串
- 标识
标识 描述
- 在给定的字段宽度内左对齐,默认是右对齐
+ 强制在结果之前显示加号或减号(+ 或 -),即正数前面会显示 + 号。默认情况下,只有负数前面会显示一个 - 号。
space
(空格)
有符号值若为正,则在值前显示前导空格(但是不显示符号);若为负,则在值前显示-。
# 与 o、x 或 X 说明符一起使用时,非零值前面会分别显示 0、0x 或 0X。
与 e、E 和 f 一起使用时,会强制输出包含一个小数点,即使后边没有数字时也会显示小数点。默认情况下,如果后边没有数字时候,不会显示显示小数点。
与 g 或 G 一起使用时,结果与使用 e 或 E 时相同,但是尾部的零不会被移除。
0 输出数值时指定左面不使用的空位置自动填0。
  • 最小输出宽度(width
宽度 描述
number 数值(十进制整数),数据长度 小于number,则左补空格;否则按实际输出。
* 星号,精度在 format 字符串中未指定,但是会作为附加整数值参数放置于要被格式化的参数之前。
  • 精度(.precision
精度 描述
.number 对于整数说明符(d、i、o、u、x、X):precision 指定了要写入的数字的最小位数。如果写入的值短于该数,结果会用前导零来填充。如果写入的值长于该数,结果不会被截断。精度为 0 意味着不写入任何字符。
对于 e、E 和 f 说明符:要在小数点后输出的小数位数。
对于 g 和 G 说明符:要输出的最大有效位数。
对于 s : 要输出的最大字符数,不足number则正常输出,超过则截断。默认情况下,所有字符都会被输出,直到遇到末尾的空字符。
对于 c 类型:没有任何影响。
当未指定任何精度时,默认为 1。如果指定时不带有一个显式值,则假定为 0。
.* 星号,精度在 format 字符串中未指定,但是会作为附加整数值参数放置于要被格式化的参数之前。
但是在尝试的时候似乎有一些数据类型不匹配的情况,不过也用的不多。

2.6.3 实例

1
2
3
4
5
#!/bin/bash
printf "%-10s %-8s %-4s\n" name sex kg
printf "%-10s %-8s %-4.1f\n" qidai 男 72.162
printf "%-10s %-8s %-4.1f\n" fanhua 男 60.9612
printf "%-10s %-8s %-4.1f\n" yunyuhai 女 48.96313141

2.7Shell中带颜色的输出

2.7.1ANSI控制码简介

  ANSI控制码用于在字符显示系统中控制光标移动和字符色彩等,常用于BBS系统中。ANSI ESCAPE SEQUENCES又称为VT100系列控制码,国内译为ANSI控制码。ANSI控制码依赖终端,不是依赖语言,所以在shellperlC里应用都是可以的。

  **ANSI控制码开始的标志都为ESC[ESC对应ASCII码表033(八进制)**,在Shell编程中,输出的命令有echoprintfecho需要用-e启用转义,两个输出命令通过\033或者\e来输入ESC,例如\033[32m即为ESC[32m

####2.7.2ANSI控制码

\033[0m 关闭所有属性
\033[1m 设置高亮度
\033[4m 下划线
\033[5m 闪烁
\033[7m 反显
\033[8m 消隐
\033[30m ~ \033[37m 设置前景色(字体色)
30:黑 31:红 32:绿 33:黄 34:蓝色 35:紫色 36:深绿 37:白色
\033[40m ~ \033[47m 设置背景色
40:黑 41:深红 42:绿 43:黄色 44:蓝色 45:紫色 46:深绿 47:白色
\033[nA 光标上移n行
\033[nB 光标下移n行
\033[nC 光标右移n行
\033[nD 光标左移n行
\033[y;xH 设置光标位置
\033[2J 清屏
\033[K 清除从光标到行尾的内容
\033[s 保存光标位置
\033[u 恢复光标位置
\033[?25l 隐藏光标
\033[?25h 显示光标

【显示字符属性控制】

1
[n1;n2;......m

  设定显示字符的属性状态。若有两个以上设定则以分号将代码隔开。除非重新设定,否则原设定之属性一直被保留,若想后边的不受影响,可以在结束后加一个\033[0m

2.7.3ANSI码应用格式

  • echo
1
echo -e "\033[颜色值m 文本" # 最后加上\033[0m可以使其不影响其他字体 
  • printf
1
printf  "\033[颜色值m 文本\n" # 最后加上\033[0m可以使其不影响其他字体 

2.7.4实例

  • echo
1
2
3
4
#!/bin/bash
echo -e "\033[31m Hello World! \033[0m"
echo -e "\e[31m Hello World! \e[0m"
echo -e "\033[31;46m Hello World! \033[0m"

输出效果:

  • printf
1
2
3
4
#!/bin/bash
printf "\033[31m Hello World! \033[0m Hello World!\n"
printf "\e[31m Hello World! \e[0m Hello World!\n"
printf "\033[31;46m Hello World! \033[0m Hello World!\n"

输出效果:

3.结构性语句

  结构性语句主要根据程序的运行状态、输入数据、变量的取值、控制信号以及运行时间等因素来控制程序的运行流程。主要包括:条件测试语句(两路分支)、多路分支语句、循环语句、循环控制语句和后台执行语句等。

3.1分支语句

3.1.1if...else...语句

  • 语法结构1
1
2
3
4
5
6
7
8
9
10
11
12
13
# if ... fi 结构
if [ expression ]
then
Statement(s) to be executed if expression is true
fi

# 或者
if [ expression ] ; then
Statement(s) to be executed if expression is true
fi

# 或者写成一行
if [ expression ] ; then Statement(s) to be executed if expression is true; fi;
  • 语法结构2
1
2
3
4
5
6
7
# if ... else ... fi 结构
if [ expression ]
then
Statement(s) to be executed if expression is true
else
Statement(s) to be executed if expression is not true
fi
  • 语法结构3
1
2
3
4
5
6
7
8
9
10
11
12
13
# if ... elif ... fi 结构
if [ expression 1 ]
then
Statement(s) to be executed if expression 1 is true
elif [ expression 2 ]
then
Statement(s) to be executed if expression 2 is true
elif [ expression 3 ]
then
Statement(s) to be executed if expression 3 is true
else
Statement(s) to be executed if no expression is true
f

【注意】

  (1)以上三种结构中,ifelif之后要跟一个then,结束要有fi,中间的执行语句不需要像C语言一样有{}

  (2)then可以与if或者elif写在同一行,但是要加一个;

  (3)if也常与test命令一起使用。

3.1.1case语句

  • 语法结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
case 值 in
模式1)
command1
command2
command3
;;
模式2)
command1
command2
command3
;;
*)
command1
command2
command3
;;
esac

【注意】

(1)取值后面必须为单词 in每一模式必须以右括号结束。取值可以为变量或常数,匹配发现取值符合某一模式后,其间所有命令开始执行直至 ;;

(2)每个 case 分支用右圆括号开始,用两个分号 ;;表示 break,即执行结束。如果没有匹配到任何一个模式,使用星号 * 捕获该值,再执行后面的命令。

(3)esac(就是 case 反过来)作为整个结构结束标记。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/bin/bash

read -p "Inter a number:" value
if [ $value -lt 0 ] || [ $value -gt 100 ]
then
echo "input error."
exit 0
else
value=`expr $value / 10`
case $value in
8 | 9 | 10)
echo "A"
;;
6 | 7)
echo "B"
;;
*)
echo "C"
;;
esac
fi

3.2循环语句

3.2.1for循环

  • 语法结构1
1
2
3
4
5
6
7
for 变量 in 列表
do
command1
command2
...
commandN
done
  • 语法结构2
1
2
3
4
5
6
7
for((表达式1; 表达式2; 表达式2))
do
command1
command2
...
commandN
done

【注意】

(1)以上两种结构都可以,但是都要注意不可省略dodone

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/bin/bash
printf "\033[31m shell方式 \033[0m\n"
for value in 1 2 3 4 5
do
printf "$value"
done
echo ""

printf "\033[31m 类似C语言方式 \033[0m\n"
for ((val = 1; val <=5; val++))
do
printf "$val"
done
echo ""

3.2.2while循环

  • 语法结构1
1
2
3
4
while [ expression ]
do
Statement(s) to be executed if expression is true
done
  • 语法结构2
1
2
3
4
while (( expression )) 
do
Statement(s) to be executed if expression is true
done

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/bin/bash
# 语法结构1例子
i=1
sum=0
while [ $i -le 10 ]
do
sum=`expr $sum + $i`
i=`expr $i + 1`
done
echo "i = $i sum = $sum"

# 语法结构2例子
num=0
while (( $num < 5))
do
echo "$num"
num=`expr $num + 1`
done
echo "num=$num"

3.2.3until循环

  until 循环执行一系列命令直至条件为 true 时停止。until 循环与 while 循环在处理方式上刚好相反。一般 while 循环优于 until 循环,但在某些时候—也只是极少数情况下,until 循环更加有用。

  • 语法结构
1
2
3
4
until [ expression ]
do
Statement(s) to be executed until command is true
done

例如:

1
2
3
4
5
6
7
#!/bin/bash
num=0
until [ ! ${num} -lt 3 ]
do
echo ${num}
num=`expr ${num} + 1`
done

3.2.4无限循环写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# for写法
for (( ; ; ))
do
command
done

# while写法1
while :
do
command
done
# while写法2
while true
do
command
done

3.2.5跳出循环

  在循环过程中,有时候需要在未达到循环结束条件时强制跳出循环,ShellC语言一致,使用两个命令来实现该功能:breakcontinue。用法与C语言一致。

【注意】

  (1)一般来说break是跳出本层循环,continue是结束本次循环,直接进入下一次循环。

  (2)在嵌套循环中,break 命令后面还可以跟一个整数,表示跳出第几层循环。continue后边也可以跟一个整数,表示继续第几层循环。

1
2
break n      # break所在循环层为第1层,然后再向外计算层数
continue n # continue所在循环层为第1层,然后再向外计算层数

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/bin/bash
for var1 in 1 2
do
echo "第1层循环 var1=${var1}"
for var2 in 3 4
do
echo -e "\t第2层循环 var2=${var2}"
for var3 in 5 6
do
if [ ${var1} == 1 ] && [ ${var2} == 4 ] && [ ${var3} == 5 ]
then
echo -e "break 跳出"
break 3
elif [ ${var1} == 1 ] && [ ${var2} == 3 ] && [ ${var3} == 5 ]
then
echo -e "continue 继续"
continue 3
fi
echo -e "\t\t第3层循环 var3=${var3}"
echo -e "\t\t一次循环结束 ${var1} ${var2} ${var3}"
done
done
done

六、Shell函数

1.函数的定义

1
2
3
4
5
[function] function_name() 
{
list of commands.
[ return int_value ]
}

【注意】

  (1)function是可选的,可以省略不写,直接就是函数名字也可以。

  (2)return int_value表示返回值,如果不加,将以最后一条命令运行结果,作为返回值。函数的返回值可以在调用该函数后通过$?来获得。

  (3)Shell中,通过return只能返回整数值,并且是0-255的范围,如果超出这个范围就会错误的结果。

  (4)如果希望直接从终端调用函数,可以将函数定义在主目录下的 .profile 文件,这样每次登录后,在命令提示符后面输入函数名字就可以立即调用。

2.函数的调用

1
2
3
4
# 方式1
function_name [arg1 arg2 ...]
# 方式2
value_name=`function_name [arg1 arg2 ... ]`

【注意】

  (1)函数必须先定义后使用,这意味着必须将函数放在脚本开始部分,直至Shell解释器首次发现它时,才可以使用。不像C语言一样可以只在前边声明函数,到最后再定义。

  (2)Shell函数的调用只需要写出函数名即可,不需要写出括号()

3.函数的删除

  像删除变量一样,函数也可以被删除。

1
$unset .f function_name

4.函数的参数传递

  在Shell中,调用函数时可以向其传递参数。在函数体内部,通过 $n (也就是位置变量)的形式来获取参数的值。除了$n还有几个特殊字符也可以用于处理参数。

${n} 位置变量
${*} 以一个单字符串显示所有向脚本传递的参数
${$} 脚本运行的当前进程ID号
${!} 后台运行的最后一个进程的ID号
${@} 与$*相同,但是使用时加引号,并在引号中返回每个参数
${?} 显示最后命令的退出状态。0表示没有错误,其他任何值表明有错误
$# 传递到脚本或函数的参数个数

【注意】 最后一个 # 特殊字符在使用时也可以加 {},只不过这里不写是因为加上后,会导致Hexo渲染后的页面发生错乱。

1
2
3
4
5
6
7
8
9
# 定义函数时使用传入的参数
[function] function_name()
{
echo "$1" # 输出第1个位置变量
echo "${10}" # 输出第10个位置变量
}

# 调用函数时传入参数
function_name [ arg1 arg2 arg3 ... ]

例如:

1
2
3
4
5
6
7
8
9
10
#!/bin/bash
function test()
{
for var in 1 2 3 4 5 6 7 8 9 10
do
echo '$'${var}' = '${var}' '
done
}

test 10 9 8 7 6 5 4 3 2 1 0

输出结果为:

1
2
3
4
5
6
7
8
9
10
$1 = 1 
$2 = 2
$3 = 3
$4 = 4
$5 = 5
$6 = 6
$7 = 7
$8 = 8
$9 = 9
$10 = 10

5.函数内部的变量

  Shell函数内部可以定义变量,有两种定义方式。两种方式代表了变量有不同的作用域。

全局作用域:在脚本的其他任何地方都能够访问该变量。

局部作用域:只能在声明变量的作用域内(函数内部)访问。

1
2
3
4
# 局部变量
Local variable_name=value
# 全局变量
variable_name=value

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/bin/bash
function test()
{
var1=10
local var2=20
echo "函数内打印在函数内部定义的变量"
echo "var1=${var1}"
echo "var2=${var2}"
}

test
echo "函数外打印在函数内部定义的变量"
echo "var1=${var1}"
echo "var2=${var2}"

输出结果为:

1
2
3
4
5
6
函数内打印在函数内部定义的变量
var1=10
var2=20
函数外打印在函数内部定义的变量
var1=10
var2=

可以看出,局部变量var2的值在函数外是访问不到的。

七、Shell文件包含

  Shell脚本与C语言一样,可以包含外部文件,Shelll可以将外部脚本的内容合并到当前脚本。这样可以很方便的封装一些公用的代码作为一个独立的文件以便于使用。

Shell 文件包含的语法格式有两种:

1
2
3
4
# 语法格式1
. file_name
# 语法格式2
source file_name

【注意】被包含的脚本文件不需要可执行权限

例如:

1
2
3
4
5
6
7
8
9
10
11
12
#------------以下分隔开的是两个文件---------
# 变量定义文件
#!/bin/bash
# filename: file1.sh
var="file1.sh"
# --------------------------------------
# 引用其他文件内容的文件
#!/bin/bash
# filename: file2.sh
. file1.sh

echo "${var}"

./file2.sh输出结果:

1
file1.sh

八、Shell重定向

  Unix 命令默认从标准输入设备(stdin)获取输入,将结果输出到标准输出设备(stdout)显示。一般情况下,标准输入设备就是键盘,标准输出设备就是终端,即显示器。

8.1输出重定向

  • 输出重定向的概念

  命令的输出不仅可以是显示器,还可以输出到某个文件,这就叫做输出重定向。

命令 说明
command > file 将输出到显示器的内容重定向到 file文件。
【注意】file文件中原有的内容会被清空
command >> file 将输出到显示器的内容以追加的方式重定向到 file文件。
【注意】file文件中原有的内容会保留,新的内容追加在结尾。

【注意】>>>两个符号都是起到重定向的作用,但是,前者会清空原有文件,但是后者则为追加。

  • 实例
1
2
3
4
# 命令行中执行
who > file_user # 将命令完整的输出重定向在用户文件中(file_user)
date >> file_user # 将日期追加到file_user文件结尾
ls ~ > file_user # 清空file_user,将家目录中所有文件的名称重定向到file_user文件

运行结果如下图:

8.2输入重定向

  • 输入重定向的概念

  Unix 命令也可以从文件获取输入,而不是默认的键盘

命令 说明
command < file 本来需要从键盘获取输入的命令会转移到文件file中读取内容。
  • 实例
1
2
3
# 命令行中执行
wc -l file_user
wc -l < file_user

运行结果如下图:

  第一个命令没有进行重定向,会输出文件名;第二个命令却不会,因为第二个命令仅仅知道从标准输入读取内容。

8.3重定向的深入理解

  文件描述符,File descriptor,简称fd,当应用程序请求内核打开/新建一个文件时,内核会返回一个文件描述符用于对应这个打开/新建的文件,其fd本质上就是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIXLinux这样的操作系统。

  文件描述符 0 通常是标准输入(STDIN),1 是标准输出(STDOUT),2 是标准错误输出(STDERR

  一般情况下,每个 Unix/Linux 命令运行时都会打开三个文件

标准输入文件(stdin) stdin的文件描述符为0,Unix程序默认从stdin读取数据。
标准输出文件(stdout) stdout 的文件描述符为1,Unix程序默认向stdout输出数据。
标准错误文件(stderr) stderr的文件描述符为2,Unix程序会向stderr流中写入错误信息。

  默认情况下,command > filestdout 重定向到 filecommand < filestdin 重定向到 file

所有可用重定向命令如下:

命令 说明
command > file 将输出到显示器的内容重定向到 file文件。
【注意】file文件中原有的内容会被清空
command >> file 将输出到显示器的内容以追加的方式重定向到 file文件。
【注意】file文件中原有的内容会保留,新的内容追加在结尾。
n > file 将文件描述符为 n 的内容重定向到 file文件。
【注意】file文件中原有的内容会被清空。
n >> file 将文件描述符为 n 的内容以追加的方式重定向到 file文件。
【注意】file文件中原有的内容会保留,新的内容追加在结尾。
n >& file 将输出文件 file 和 n 合并。
命令 说明
command < file 本来需要从键盘获取输入的命令会转移到文件file中读取内容。
n <& file 将输入文件 file 和 n 合并。
<< tag 将开始标记 tag 和结束标记 tag 之间的内容作为输入。
  • stderr 重定向到 file
1
2
command 2 > file # 清空原文件再写入
command 2 > file # 追加到原文件后面
  • stdoutstderr 合并后重定向到 file
1
2
command > file 2>&1
command >> file 2>&1

【注意】这里的 2>之间不可以有空格,2>是一体的时候才表示错误输出。

  • stdinstdout 都重定向
1
command < file1 >file2

command 命令将 stdin 重定向到 file1,将 stdout 重定向到 file2

  • Here Document

  Here DocumentShell 中的一种特殊的重定向方式,它的基本的形式如下:

1
2
3
4
# 将两个 delimiter 之间的内容(document) 作为输入传递给 command
command << delimiter
document
delimiter

【注意】

  (1)结尾的delimiter 一定要顶格写,前面不能有任何字符,后面也不能有任何字符,包括SpaceTab 缩进。

  (2)开始的delimiter前后的空格会被忽略掉。

例如:

1
2
3
4
5
#!/bin/bash
cat << tag
Hello,World!
qidaink
tag

输出结果:

1
2
Hello,World!
qidaink
  • /dev/null 文件

  如果希望执行某个命令,但又不希望在屏幕上显示输出结果,那么可以将输出重定向到 /dev/null/dev/null 是一个特殊的文件,写入到它的内容都会被丢弃;如果尝试从该文件读取内容,那么什么也读不到。但是 /dev/null 文件非常有用,将命令的输出重定向到它,会起到”禁止输出”的效果。

1
command > /dev/null 2>&1 # 屏蔽 stdout 和 stderr

九、Shell中的各种括号

9.1${}

  ${ }用于变量替换,一般情况下,$var${var}一样,但是用 ${ } 会比较精确的界定变量名称的范围。

1
${Variable}

9.2$()

$()与反引号**``**都可用做命令替换。

1
2
3
# 命令替换格式
$(command) # 并不是所有shell都支持
`command` # 基本上可在全部的 unix shell 中使用
  • 实例
1
2
3
4
5
#!/bin/bash
var1=`date`
var2=$(date)
echo '`date`= '${var1}''
echo '$(date)= '${var2}''

输出结果:

1
2
`date`= 2022年 02月 16日 星期三 16:26:49 CST
$(date)= 2022年 02月 16日 星期三 16:26:49 CST

9.3(())

  只要括号中的运算符、表达式符合C语言运算规则,都可用在$((expression))中,甚至是三目运算符。作不同进位(如二进制、八进制、十六进制)运算时,输出结果全都自动转化成十进制。

  用 (( )) 也可重定义变量值

例如:

1
2
3
4
5
6
#!/bin/bash
var=3
((var++))
echo "var=${var}"

echo "$((16#5f))"

输出结果:

1
2
var=4
95

9.4[]

  bash 的内部命令,[test是等同的。if/test结构中的左中括号[是调用test的命令标识,右中括号]是关闭条件判断的。这个命令把它的参数作为比较表达式或者作为文件测试,并且根据比较的结果来返回一个退出状态码。if/test结构中并不是必须右中括号,但是新版的Bash中要求必须这样。

  字符范围。用作正则表达式的一部分,描述一个匹配的字符范围。作为test用途的中括号内不能使用正则表达式。

9.5$[]$(())

$[]$(())是一样的,都是进行数学运算的。

例如:

1
2
3
4
5
6
#!/bin/bash
a=2
b=3
c=5
echo '$((a * b * c))='$((a * b * c))''
echo '$[a * b * c]='$[a * b * c]''

输出结果:

1
2
$((a * b * c))=30
$[a * b * c]=30

9.5[[]]

&emsp;&emsp;[[bash 程序语言的关键字。并不是一个命令,[[ ]] 结构比[ ]结构更加通用。在[[]]之间所有的字符都不会发生文件名扩展或者单词分割,但是会发生参数扩展和命令替换。

  支持字符串的模式匹配,使用=~操作符时甚至支持shell的正则表达式。字符串比较时可以把右边的作为一个模式,而不仅仅是一个字符串,比如[[ hello == hell? ]],结果为真。[[ ]] 中匹配字符串或通配符,不需要引号。

  使用[[ ... ]]条件判断结构,而不是[ ... ],能够防止脚本中的许多逻辑错误。比如,&&||<> 操作符能够正常存在于[[ ]]条件判断结构中,但是如果出现在[ ]结构中的话,会报错。比如可以直接使用if [[ $a != 1 && $a != 2 ]], 如果不适用双括号, 则为if [ $a -ne 1] && [ $a != 2 ]或者if [ $a -ne 1 -a $a != 2 ]