shell 语法

概论

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

脚本示例

新建一个test.sh文件,内容如下:

1
2
#!/bin/bash
echo "hello world"

用解释器执行:

1
2
3
4
helong@DESKTOP-C22TSTC:~/shellStudy$ vim test.sh # 编辑文档
helong@DESKTOP-C22TSTC:~/shellStudy$ chmod +x test.sh # 增加脚本可执行权限
helong@DESKTOP-C22TSTC:~/shellStudy$ ./test.sh # 当前路径下执行
hello world # 脚本输出

注意:也可以直接使用sh执行shell脚本

注释

单行注释

每行中#之后的内容均是注释

1
2
# 这是一行注释
echo "hello world" # 这也是注释

多行注释

格式:

1
2
3
4
5
:<<EOF
第一行注释
第二行注释
第三行注释
EOF

其中EOF可以换成其他任意字符串。例如:

1
2
3
4
5
:<<abc
第一行注释
第二行注释
第三行注释
abc

变量

定义变量

定义变量,不需要加$符号,例如:

1
2
3
name1='helong' # 单引号定义字符串
name2="helong" # 双引号定义字符串
name3=helong #也可以不加引号,同样表示字符串

使用变量

使用变量,需要加上$符号,或者${}符号。花括号是可选的,主要为了帮助解释器识别变量边界

1
2
3
4
name=helong
echo $name # 输出helong
echo ${name} # 输出helong
echo ${name}hello # 输出helonghello

只读变量

使用readonly或者declare可以将变量变为只读

1
2
3
4
5
name=helong
readonly name
declare -r name # 两种写法均可

name=abc # 会报错(只读变量不能进行修改)

删除变量

unset可以删除变量

1
2
3
name=helong
unset name
echo $name #输出空行

变量类型

  1. 自定义变量(局部变量)子进程不能访问的变量
  2. 环境变量(全局变量)子进程可以访问的变量

==自定义环境变量改成环境变量:==

1
2
3
helong@DESKTOP-C22TSTC:~/shellStudy$ name=helong # 定义变量
helong@DESKTOP-C22TSTC:~/shellStudy$ export name # 第一种方法
helong@DESKTOP-C22TSTC:~/shellStudy$ declare -x name # 第二种方法

==环境变量改为自定义变量:==

1
2
helong@DESKTOP-C22TSTC:~/shellStudy$ export name=helong # 定义环境变量
helong@DESKTOP-C22TSTC:~/shellStudy$ declare -x name # 改为自定义变量

字符串

字符串可以用单引号,也可以用双引号,也可以不用引号。 单引号与双引号的区别:

  1. 单引号中的内容会原样输出,不会执行,不会取变量
  2. 双引号中的内容可以执行,可以取变量
1
2
3
name=helong # 不用引号
echo 'hello, $name \"hh\" ' # 单引号字符串输出 hello, $name \"hh\"
echo "hello, $name \"hh\"" # 双引号字符串输出 hello, helong "hh"

获取字符串长度

1
2
3
helong@DESKTOP-C22TSTC:~$ name=helong
helong@DESKTOP-C22TSTC:~$ echo ${#name}
6 # 输出长度为6

提取子串

1
2
3
helong@DESKTOP-C22TSTC:~$ name=helong
helong@DESKTOP-C22TSTC:~$ echo ${name:0:5}
helon # 输出索引下标为0~4的子串【左闭右开】

默认变量

文件参数变量

在执行shell脚本时,可以向脚本传递参数。 $1是第一个参数,$2是第二个参数,一次类推。特殊的$0是shell文件名(包含路径)。例如: 创建文件test.sh

1
2
3
4
5
6
7
#!/bin/bash

echo "文件名:"$0
echo "第一个参数:"$1
echo "第二个参数:"$2
echo "第三个参数:"$3
echo "第四个参数:"$4

然后执行该脚本:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
helong@DESKTOP-C22TSTC:~$ vim test.sh
helong@DESKTOP-C22TSTC:~$ cat test.sh
#!/bin/bash

echo "文件名:"$0
echo "第一个参数:"$1
echo "第二个参数:"$2
echo "第三个参数:"$3
echo "第四个参数:"$4
helong@DESKTOP-C22TSTC:~$ sh test.sh 1 2 3 4
文件名:test.sh
第一个参数:1
第二个参数:2
第三个参数:3
第四个参数:4

其他参数相关变量

参数 说明
$# 代表文件传入的参数个数,如上例中值为4
$* 由所有参数构成的用空格隔开的字符串,等价上例中的$1 $2 $3 $4
$@ 每个参数分别用双引号括起来的字符串,等价上例中的"$1" "$2" "$3" "$4"
$$ 脚本当前运行的进程ID
$? 上一条命令的退出状态(exit code)。0表示正常退出,其他值表示错误
$(commod) 返回commod这条明林的stdout(可嵌套)
command 返回commod这条命令的stout(不可嵌套)

expr 命令

expr命令用于求表达式的值,格式为:

1
expr 表达式

表达式说明

  • 用空格隔开每一项
  • 用反斜杠放在shell特定的字符前面(发现 表达式运行错误时,可以试试转义)
  • expr会在stdout中输出结果。如果为逻辑关系表达式,则结果为真,stdout1, 否则为0
  • expr的exit code: 如果逻辑关系表达式,则结果为真,exit code为0,否则为1

字符串表达式

  • length STRING:返回STRING的长度
  • index STRING CHARSET : 子串CHARSET中任意单个字符在STRING中最前面的字符位置,下标从1开始。如果STRING中完全不存在CHARSET中的字符,则返回0
  • substr STRING POSITION LENGTH : 返回STRING字符串中从POSITION开始,长度最大为LENGTH的子串。如果POSITIONLENGTH为负数,0或非数值,则返回空字符串。

示例:

1
2
3
4
5
6
#!/bin/bash
str="hello world"

echo `expr length "$str"`
echo `expr index "$str" awd`
echo `expr substr "$str" 2 3`

输出结果:

1
2
3
4
helong@DESKTOP-C22TSTC:~$ sh test.sh
11 # ``不是单引号,表示执行该命令,输出11
7 # 输出7,下标从1开始
ell

整数表达式

expr支持普通的算术操作,算术表达式优先级低于字符串表达式,高于逻辑表达式。

  • + - 加减运算。两端参数会转换为整数,如果转换失败则报错。
  • * / %乘,除,取模运算。两端参数会转换为整数,如果转换失败则报错。
  • ()可以改变表达式的优先级,但需要用反斜杠转义。

示例:

1
2
3
4
5
6
7
8
a=3
b=4

echo `expr $a + $b` # 输出7
echo `expr $a - $b` # 输出-1
echo `expr $a \* $b` # 输出12, *需要转义
echo `expr $a / $b` # 输出0,整除
echo `expr \($a + 1\) \* \($b + 1\)` # 输出20,值为(a + 1) * (b + 1)

逻辑关系表达式

  • |如果第一个参数非空且非0,则返回第一个参数的值,否则返回第二个参数的值,但要求第二个参数的值也是费控或非0,否则返回0。如果第一个参数是非空或非0时,不会计算第二个参数。(短路或)
  • & 如果两个参数都是非空且非0,则返回第一个参数,否则返回0。如果第一个参数为0或为空,则不会计算第二个参数。
  • < ≤ = == ≠ ≥ > 比较两端的参数,如果为true,则返回1,否则返回0。 “==”是“=”的同义词。“expr”首先尝试将两端参数转换为整数,并做算术比较,如果转换失败,则按字符集排序规则做字符比较。
  • () 可以该表表达式优先级,但是需要用反斜杠转义

实例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
a=3
b=4

echo `expr $a \> $b` # 输出0,> 需要转义
echo `expr $a '<' $b` # 输出1,< 也可以将特殊字符用引号引起来
echo `expr $a '>=' $b` # 输出0
echo `expr $a \<\= $b` # 输出1

c=0
d=5

echo `expr $c \& $d`  # 输出0
echo `expr $a \& $b`  # 输出3
echo `expr $c \| $d`  # 输出5
echo `expr $a \| $b`  # 输出3

read 命令

read 命令用于从标准输入中读取单行数据。当读到文件结束符时,exit code1,否则为0

参数说明

  • -p : 后面可以接提示信息
  • -t : 后面跟秒数,定义输入字符的等待时间,超过等待时间后会自动忽略此命令

示例:

1
2
3
4
5
6
7
8
helong@DESKTOP-C22TSTC:~$ read name # 读入name的值
hello world! # 标准输入
helong@DESKTOP-C22TSTC:~$ echo $name # 输出name的值
hello world! # 标准输出
helong@DESKTOP-C22TSTC:~$ read -p "Please input your name:" -t 30 name # 读入name的值,等待时间30秒
Please input your name:helong # 标准输入
helong@DESKTOP-C22TSTC:~$ echo $name # 输出name的值
helong # 标准输出

echo命令

echo用于输出字符串。命令格式:

1
echo STRING

显示普通字符串

1
2
echo "Hello Shell"
echo Hell Shell # 引号可以省略

显示转义字符

1
2
echo "\"Hello World\"" # 只能使用双引号,如果使用单引号则不转义
echo \"Hello World\" # 也可以省略双引号

显示变量

1
2
name=helong
echo "My name is $name" # 输出 My name is helong

换行显示

1
2
echo -e "Hi\n" # -e 开启转义
echo "helong"

输出结果:

1
2
3
Hi

helong

显示不换行

1
2
echo -e "Hi \c" # -e 开启转义 \c 不换行
echo "helong"

输出结果:

1
Hi helong

显示结果定向至文件

1
echo "hello world" > output.txt

原样输出字符串

1
2
3
helong@DESKTOP-C22TSTC:~$ name="helong"
helong@DESKTOP-C22TSTC:~$ echo '$name\"'
$name\"

显示命令执行结果

1
2
helong@DESKTOP-C22TSTC:~$ echo `date`
Sun Aug 14 17:23:02 CST 2022

printf命令

printf 命令用语格式化输出,类似于c/c++中的printf()函数。

默认不会在字符号串末尾添加换行符。

命令格式

1
printf format-string [arguments...]

用法示例

脚本内容:

1
2
3
4
printf "%10d.\n" 123  # 占10位,右对齐
printf "%-10.2f.\n" 123.123321  # 占10位,保留2位小数,左对齐
printf "My name is %s\n" "helong"  # 格式化输出字符串
printf "%d * %d = %d\n"  2 3 `expr 2 \* 3` # 表达式的值作为参数

输出结果:

1
2
3
4
       123.
123.12    .
My name is helong
2 * 3 = 6

test 命令与判断符号[]

逻辑运算符&& ||

  • && 表示与,|| 表示或

  • 二者具有短路原则:

    • expr1 && expr2 :当 expr1 为假时,直接忽略 expr2
    • expr1 || expr2 :当 expr1 为真时,直接忽略 expr2
  • 表达式的 exit code 0 ,表示真;为 非零,表示假。(C/C++中的定义相反

test命令

  • 在命令行中输入man test,可以查看test命令的用法。

  • test 命令用于判断文件类型,以及对变量做比较。

  • test 命令用 exit code 返回结果,而不是使用 stdout0 表示 非0 表示

例如:

1
2
test 2 -lt 3 # 为真,返回值为0
echo $? # 输出上个命令的返回值,输出0

文件类型判断

命令格式:

1
test -e filename # 判断文件是否存在
测试参数 代表意义
-e 文件是否存在
-f 是否为文件
-d 是否为目录

==判断文件存在否==:

1
2
3
4
5
6
helong@DESKTOP-C22TSTC:~$ ls # 列出当前目录下的文件
ethereum  go  myChain  myDevChain  nodejs  project  shellStudy  test.sh
helong@DESKTOP-C22TSTC:~$ test -e test.sh && echo echo "exist" || echo "Not exist"
echo exist # 输出文件存在
helong@DESKTOP-C22TSTC:~$ test -e test2.sh && echo echo "exist" || echo "Not exist"
Not exist # 输出文件不存在

文件权限判断

命令格式:

1
test -r filename # 判断文件是否可读
测试参数 代表意义
-r 文件是否可读
-w 文件是否可写
-x 文件是否可执行
-s 是否为非空文件

整数间的比较

命令格式:

1
test $a -eq $b # a是否等于b
测试参数 代表意义
-eq a是否等于b
-ne a是否不等于b
-gt a是否大于b
-lt a是否小于b
-ge a是否大于等于b
-le a是否小于等于b

字符串比较

测试参数 代表意义
test -z STRING 判断STRING是否为空,如果为空,则返回true
test -n STRING 判断STRING是否为非空,如果为非空,则返回true(-n 可以省略)
test str1 == str2 判断str1是否等于str2
test str1 ≠ str2 判断str1是否不等于str2

多重条件判断

命令格式:

1
test -r filename -a -x filename
测试参数 代表意义
-a 两个条件是否同时成立
-o 两个条件是否至少一个成立
! 取反。如果test ! -x file, 当file不可执行时,返回true

判断符号[]

[]test 用法几乎一模一样,更常用于if语句中。另外[[]][]的加强版,支持特性更多。 例如:

1
2
3
4
5
6
7
8
9
helong@DESKTOP-C22TSTC:~$ [ 2 -lt 3 ] # 为真,返回值为0
helong@DESKTOP-C22TSTC:~$ echo $? # 输出上个命令的返回值,输出0
0
helong@DESKTOP-C22TSTC:~$ ls # 列出当前目录下的所有文件
ethereum  go  myChain  myDevChain  nodejs  project  shellStudy  test.sh
helong@DESKTOP-C22TSTC:~$ [ -e test.sh ] && echo "exist" || echo "Not exist"
exist # test.sh 文件存在
helong@DESKTOP-C22TSTC:~$ [ -e test2.sh ] && echo "exist" || echo "Not exist"
Not exist # test2.sh文件不存在

注意

  • [] 内的每一项都要用空格隔开
  • 中括号内的变量,最好用双引号括起来
  • 中括号内的常数,最好用单或双引号括起来

例如:

1
2
3
4
5
6
helong@DESKTOP-C22TSTC:~$ name="he long"
helong@DESKTOP-C22TSTC:~$ [ $name == "he long" ]
-bash: [: too many arguments # 错误,等价于[ he long = "he long"],参数太多
helong@DESKTOP-C22TSTC:~$ [ "$name" == "he long" ] # 正确
helong@DESKTOP-C22TSTC:~$ echo $?
0 # 输出为0,代表真

判断语句

if…then形式

类似于C/C++中的if-else语句。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
if condition
then
    语句1
    语句2
    ...
elif condition
then
    语句1
    语句2
    ...
elif condition
then
    语句1
    语句2
else
    语句1
    语句2
    ...
fi

case…esac 形式

类似于C/C++中的switch语句。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
case $变量名称 in
    值1)
        语句1
        语句2
        ...
        ;;  # 类似于C/C++中的break
    值2)
        语句1
        语句2
        ...
        ;;
    *)  # 类似于C/C++中的default
        语句1
        语句2
        ...
        ;;
esac

循环语句

for…in…do…done

1
2
3
4
5
6
for var in val1 val2 val3
do
    语句1
    语句2
    ...
done

示例1,输出a 2 cc,每个元素一行:

1
2
3
4
for i in a 2 cc
do
    echo $i
done

示例2,输出当前路径下的所有文件名,每个文件名一行:

1
2
3
4
for file in `ls`
do
    echo $file
done

示例3,输出 1-10

1
2
3
4
for i in $(seq 1 10)
do
    echo $i
done

示例4,使用 {1..10} 或者 {a..z}

1
2
3
4
for i in {a..z}
do
    echo $i
done

for ((…;…;…)) do…done

命令格式

1
2
3
4
5
for ((expression; condition; expression))
do
    语句1
    语句2
done

示例,输出 1-10 ,每个数占一行:

1
2
3
4
for ((i=1; i<=10; i++))
do
    echo $i
done

while…do…done循环

命令格式

1
2
3
4
5
6
while condition
do
    语句1
    语句2
    ...
done

示例,文件结束符为 Ctrl+d ,输入文件结束符后, read 指令返回false

1
2
3
4
while read name
do
    echo $name
done

死循环的处理方式

  • 在控制台输入Ctrl+C即可。
  • 否则可以直接关闭进程:
    • 使用top命令找到进程PID
    • 输入kill -9 PID即可关掉此进程

函数

  • bash 中的函数类似于 C/C++ 中的函数

  • return 的返回值与 C/C++ 不同,返回的是 exit code,取值为0-2550 表示正常结束。

  • 如果想获取函数的输出结果,可以通过 echo 输出到 stdout

  • 然后通过 $(function_name)来获取stdout 中的结果。

  • 函数的return值可以通过 $? 来获取。

命令格式

1
2
3
4
5
[function] func_name() {  # function关键字可以省略
    语句1
    语句2
    ...
}

不获取return值和stdout值

示例:

1
2
3
4
5
6
func() {
    name=helong
    echo "Hello $name"
}

func

输出结果:

1
Hello helong

获取return的值和stdout的值

不写return时,默认return 0

示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
func() {
    name=helong
    echo "Hello $name"

    return 123
}

output=$(func)
ret=$?

echo "output = $output"
echo "return = $ret"

输出结果:

1
2
output = Hello helong
return = 123

函数的输入参数

在函数内,$1 表示第一个输入参数,$2 表示第二个输入参数,依此类推。

注意:函数内的 $0 仍然是文件名,而不是函数名。

示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func() {  # 递归计算 $1 + ($1 - 1) + ($1 - 2) + ... + 0
    word=""
    while [ "${word}" != 'y' ] && [ "${word}" != 'n' ]
    do
        read -p "要进入func($1)函数吗?请输入y/n:" word
    done

    if [ "$word" == 'n' ]
    then
        echo 0
        return 0
    fi  

    if [ $1 -le 0 ] 
    then
        echo 0
        return 0
    fi  

    sum=$(func $(expr $1 - 1))
    echo $(expr $sum + $1)
}

echo $(func 10)

输出:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
要进入func(10)函数吗?请输入y/n:y
test.sh: 8: [: y: unexpected operator
要进入func(9)函数吗?请输入y/n:y
test.sh: 8: [: y: unexpected operator
要进入func(8)函数吗?请输入y/n:y
test.sh: 8: [: y: unexpected operator
要进入func(7)函数吗?请输入y/n:y
test.sh: 8: [: y: unexpected operator
要进入func(6)函数吗?请输入y/n:y
test.sh: 8: [: y: unexpected operator
要进入func(5)函数吗?请输入y/n:y
test.sh: 8: [: y: unexpected operator
要进入func(4)函数吗?请输入y/n:y
test.sh: 8: [: y: unexpected operator
要进入func(3)函数吗?请输入y/n:y
test.sh: 8: [: y: unexpected operator
要进入func(2)函数吗?请输入y/n:y
test.sh: 8: [: y: unexpected operator
要进入func(1)函数吗?请输入y/n:y
test.sh: 8: [: y: unexpected operator
要进入func(0)函数吗?请输入y/n:y
test.sh: 8: [: y: unexpected operator
55

函数内的局部变量

命令格式:

1
local 变量名=变量值

例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#! /bin/bash

func() {
    local name=helong
    echo $name
}

func    # 第一次在函数内调用(有)

echo $name  # 第二次在函数外调用(无)

输出结果:

1
2
helong@DESKTOP-C22TSTC:~$ sh test.sh
helong

第一行为函数内的 name 变量,第二行为函数外调用 name 变量,会发现此时该变量不存在。

exit命令

exit 命令用来退出当前 shell 进程,并返回一个退出状态;使用 $? 可以接收这个退出状态。

exit 命令可以接受一个整数值作为参数,代表退出状态。如果不指定,默认状态值是 0

exit 退出状态只能是一个介于 0~255 之间的整数,其中只有 0 表示成功,其它值都表示失败。

returnexit 的==共同==之处都是返回 exit code;

​ ==区别==是 return 结束当前函数,exit 结束整个shell 脚本

示例:

创建脚本 test.sh ,内容如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#! /bin/bash

if [ $# -ne 1 ]  # 如果传入参数个数等于1,则正常退出;否则非正常退出。
then
    echo "arguments not valid"
    exit 1
else
    echo "arguments valid"
    exit 0
fi

执行脚本:

1
2
3
4
5
6
7
8
helong@DESKTOP-C22TSTC:~$ sh test.sh helong
arguments valid
helong@DESKTOP-C22TSTC:~$ echo $?
0
helong@DESKTOP-C22TSTC:~$ sh test.sh
arguments not valid
helong@DESKTOP-C22TSTC:~$ echo $?
1

文件重定向

每个进程默认打开3个文件描述符:

  • stdin 标准输入,从命令行读取数据,文件描述符为 0
  • stdout 标准输出,向命令行输出数据,文件描述符为 1
  • stderr 标准错误输出,向命令行输出数据,文件描述符为 2

可以用文件重定向将这三个文件重定向到其他文件中。

重定向命令列表

命令 说明
commod > file stdout重定向到file
commod < file stdin重定向到file
commod >> file stdout以追加方式重定向到file
commod n> file 将文件描述符n重定向到file
commod n>> file 将文件描述符n重定向追加到file

输入和输出重定向

1
2
3
4
5
6
echo -e "Hello \c" > output.txt  # 将stdout重定向到output.txt中
echo "World" >> output.txt  # 将字符串追加到output.txt中

read str < output.txt  # 从output.txt中读取字符串

echo $str  # 输出结果:Hello World

同时重定向stdin和stdout

创建bash脚本:

1
2
3
4
5
6
#! /bin/bash

read a
read b

echo $(expr "$a" + "$b")

创建input.txt,里面的内容为:

1
2
3
4

执行命令:

1
2
3
helong@DESKTOP-C22TSTC:~$ sh test.sh < input.txt  > output.txt # 执行后输出结果存于output.txt
helong@DESKTOP-C22TSTC:~$ cat output.txt 
7 # 输出结果

引入外部脚本

类似于 C/C++ 中的 include 操作,bash 也可以引入其他文件中的代码。

语法格式:

1
. filename  # 注意点和文件名之间有一个空格

1
source filename

示例 创建 test1.sh ,内容为:

1
2
3
#!/bin/bash

name=helong  # 定义变量name

然后创建 test2.sh,内容为:

1
2
3
4
5
#!/bin/bash

source test1.sh # 或 . test1.sh

echo My name is: $name  # 可以使用test1.sh中的变量

执行命令:

1
2
3
helong@DESKTOP-C22TSTC:~/shellStudy$ chmod +x test2.sh
helong@DESKTOP-C22TSTC:~/shellStudy$ ./test2.sh # 使用sh执行会报错:test2.sh: 2: source: not found
My name is: helong