Shell语法

概述

Shell是我们通过命令行与操作系统沟通的语言

Shell是逐行解释执行的,它不需要编译,它可以直接运行。

Shell脚本可以直接在命令行运行,也可以将一套逻辑组织成一个文件,方便复用。

Linux里常见的Shell脚本,有很多,比如


  • ​Bourne Shell​​​(​​/usr/bin/sh​​​或​​/bin/sh​​)
  • ​Bourne Again Shell​​​(​​/bin/bash​​)
  • ​C Shell​​​(​​/usr/bin/csh​​)
  • ​K Shell​​​(​​/usr/bin/ksh​​)
  • zsh

Linux系统中一般默认用bash,所以接下来讲解bash中的语法

文件开头需要写上 ​​#! /bin/bash​​,指明bash为脚本解释器。

示例

创建一个Shell脚本

vim test.sh

按下​​i​​,进入编辑模式,并输入如下内容

#! /bin/bash

echo "Hello,World"

运行方式


  1. 作为可执行文件
    给文件添加可执行权限
    ​chmod +x test.sh​​执行
    ​./test.sh​
  2. 用解释器执行
    ​bash test.sh​

注释

单行注释:每行 ​​#​​ 之后的内容均是注释

# 这是注释
echo "Hello,World" # 注释

多行注释

:<<EOF
这是注释
这是注释
这是注释
EOF

其中​​EOF​​可以换成其他任意字符串,例如

:<<abc 注释 注释注释 abc 

变量

定义变量

name1="hby"
name2='hby'
name3=hby
# 这三种写法等价,都是定义字符串

使用变量

需要加上​​$​​​,或者​​${}​​​,​​{}​​​是可选的,​​{}​​是用来帮助解释器识别变量名的边界的

echo $name1  # 输出hby 
echo ${name2} # 输出hby
echo ${name2}haha # 输出hbyhaha

只读变量

使用​​readonly​​​或者​​declare​​​进行声明,类似C++的​​const​​​或者java的​​final​

name=hby
readonly name # 标记为只读
declare -r name # 两种写法都可

name=abc # 会报错

可以用​​type​​​命令来判断另一个命令是否是内置命令等,比如​​type readonly​​​,​​type ls​​​,​​type cd​​​,​​type pwd​​​,​​type top​

删除变量

使用​​unset​​即可

#! /bin/bash

name=hby
unset name

echo ${name} # 输出空字符串, 当一个变量不存在时, 其为空字符串

变量类型


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

将自定义变量改为环境变量

root@yogurt:~$ name=hby # 自定义变量
root@yogurt:~$ export name # 改为环境变量
root@yogurt:~$ declare -x name # 改为环境变量

将环境变量改为自定义变量

root@yogurt:~$ export name=hby # 定义环境变量
root@yogurt:~$ declare +x name # 改为自定义变量

如何在一个bash里开一个子进程呢?

在一个命令行下,输入​​bash​​​,会开启一个子进程,然后原先的进程会休眠,再输入​​exit​​,则会 从子进程退出到父进程。

root@yogurt:~$ echo $$ # 查看当前shell的pid
root@yogurt:~$ bash # 新开一个子进程
root@yogurt:~$ exit # 退出子进程

字符串

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

单引号和双引号的区别:


  • 单引号中的内容会原样输出,不会执行,不会读取变量
  • 双引号中的内容,可以执行,可以读取变量

name=hby
echo 'hello, $name \"hh\"'
echo "hello, $name \"hh\""
echo hello, $name \"hh\"

不加引号的效果和双引号的一致

获取字符串的长度

name="hby"
echo ${#name} # 输出3

提取子串

name="hello,hby"
echo ${name:0:5} # 提取从0开始的5个字符
echo ${name:5} # 提取从5到字符串结尾的子串

默认变量

文件参数变量

在执行Shell脚本时,可以向脚本传递参数,​​$1​​​是第一个参数,​​$2​​​是第二个参数,以此类推,特殊的,​​$0​​是文件名(命令的第一段)

例,创建一个​​test.sh​

#! /bin/bash
echo "文件名: "$0
echo "第1个参数: "$1
echo "第2个参数: "$2
echo "第3个参数: "$3
echo "第4个参数: "$4

比如执行

​./test.sh​

则​​$0​​​就是​​./test.sh​

如果用绝对路径执行,比如

​/home/yogurt/test.sh​

则​​$0​​​就是 ​​/home/yogurt/test.sh​

其他相关变量


  • ​$#​​ 文件传入的参数个数
  • ​$*​​​ 由所有参数构成的,由空格隔开的字符串, ​​"$1 $2 $3 $4"​
  • ​$@​​​ 每个参数分别用双引号括起来的字符串, ​​"$1" "$2" "$3" "$4"​
  • ​$$​​ 脚本当前运行的进程ID
  • ​$?​​​ 上一条命令的退出状态,​​exit code​​,0表示正常退出,其他值表示错误
  • ​$(command)​​​ 返回​​command​​这条命令的输出结果(stdout)(注意区分输出结果和返回值)
  • `command` 返回 command 这条命令的stdout

数组

数组中可以存放多个不同类型的值,只支持一维数组,初始化时不需要指明数组大小,数组下标从0开始

定义

数组用小括号表示,元素之间用空格隔开,如

array=(1 abc "def" yogurt)

注意,数组元素都是字符串,无论是用双引号,单引号,或者不加引号

也可以直接用下标定义数组中某个元素的值

array[0]=1
array[1]=abc
array[2]="def"
array[3]=yogurt

数组下标定义比较灵活,中间位置的下标都可以不用定义,比如

array[0]=1
array[1]=abc
array[2]="def"
array[1000]=yogurt

读取

读取数组中某个元素的值

${array[index]}

array=(1 abc "def" yogurt)
echo ${array[0]}
echo ${array[1]}

读取整个数组

${array[@]}  #第一种写法
${array[*]} #第二种写法

例如

#! /bin/bash
array[0]=1
array[1]=abc
array[1000]=yogurt
echo ${array[@]} # 输出的时候会直接跳国中间没有值的部分
echo ${#array[@]} # 数组长度(数组中实际有多少元素), 输出3

expr命令

不是一个shell内建的命令,而是一个第三方命令

​expr​​命令可以用来求表达式的值,格式为

​expr 表达式​


  • 由于​​expr​​是一个命令,所以后面表达式的每一项(参数),都要用空格隔开
  • 某一些特殊字符,需要用反斜杠来进行转义
  • 特殊字符,如空格,要用引号括起来
  • ​expr​​会在​​stdout​​中输出结果

字符串表达式


  • ​length STRING​​返回 ​​STRING​​ 的长度
    比如
    ​#! /bin/bash str="Hello World" echo $(expr length "$str") # 输出11 ​
  • ​index STRING CHARSET​​​​CHARSET中​​,任意单个字符在​​STRING​​中最前面的字符位置,下标从1开始,如果在​​STRING​​中完全不存在​​CHARSET中​​的字符,则返回0
    比如
    ​#! /bin/bash str="Hello World" echo $(expr index "$str" aWd) # 在Hello World字符串中, 查找aWd中哪个字符最先出现 # 最先出现的是W, 下标为7(下标从1开始) ​
  • ​substr STRING POSITION LENGTH​​返回​​STRING​​字符串中从​​POSITION​​开始(下标也从1开始),长度最大为​​LENGTH​​的子串。如果​​POSITION​​或​​LENGTH​​为负数,0,或非数值,则返回空字符串

    ​#! /bin/bash str="Hello World" echo $(expr substr "$str" 2 4) # 输出ello ​

整数表达式

​expr​​支持算术操作,算术表达式优先级低于字符串表达式,高于逻辑关系表达式


  • ​+ -​​ 加减,两端参数会转换为整数,转换失败时会报错
  • ​* / %​​ 乘除,取模
  • ​()​​ 可以改变优先级,但需要用反斜杠转义

例如

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 % $b` # 输出3
echo `expr \( $a + 1 \) \* \( $b + 1 \)` # 输出20,值为(a + 1) * (b + 1)

逻辑关系表达式


  • ​|​​或,遵循短路原则。如果第一个参数非空或非零,则返回第一个参数的值,否则返回第二个参数的值,但要求第二个参数的值也是非空或非零,否则返回0。如果第一个参数是非空或非零,则不会计算第二个参数
  • ​&​​与,遵循短路原则。如果2个参数都非空非0,则返回第一个参数,否则返回0。
  • ​< <= = == != >= >​​比较大小。​​=​​和​​==​​是同义词。比较两端的参数,如果为​​true​​,则返回1,否则返回0。​​expr​​会首先尝试将两端参数转换为整数,并作算术比较,如果转换失败,则按字符集排序规则做字符串比较。

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命令用于从标准输入中读取单行数据

参数说明


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

例如

#! /bin/bash

read -p "What's your name? " name
echo Hello, $name

当​​read​​​命令正常读入字符串时,其​​exit code​​为0,表示正常退出。

当读入了文件结束符EOF(按下​​Ctrl + d​​​) 时,​​read​​​命令的​​exit code​​为1。

echo命令

用于输出字符串,会自动在末尾加上换行符

​echo STRING​

显示普通字符串

echo "Hello World"
echo Hello World # 引号可以省略

显示转义字符串

echo "\"Hello,World\"" # 双引号或者不加引号
# 单引号会原样输出, 不会转义

显示变量

name=yogurt
echo "my name is $name"

显示换行

echo -e "Hello\nWorld" # -e开启转义, esacpe

显示不换行

echo -e "Hello \c" # \c取消换行
echo "World"
# 由于 echo 会自动在输出完后换行, 如果想要2个echo输出在同一行, 可以用 \c 取消换行

显示的结果定向至文件

echo "Hello,World" > output.txt

原样输出字符串,用单引号即可

显示命令执行结果

echo `date`

printf命令

该命令用于格式化输出,类似于​​C/C++​​​中的​​printf​​函数

默认不会在结尾加上换行符

命令格式

printf format-string [argument]

printf "%10d!\n" 123 # 占10位, 右对齐
printf "%-10.2f!\n" 123.123123 #占10位, 保留2位小数, 左对齐
printf "My name is %s\n" "yogurt"
printf "%d * %d = %d \n" 2 3 `expr 2 \* 3`

test命令与判断符号[]

用于判断一个表达式是否为真

test命令是一个shell内置的命令,其与[]的作用几乎一模一样

逻辑运算符 ​​&&​​​ 和 ​​||​​,这个是属于shell的。二者也遵循短路原则


  • ​exp1 && exp2​​​:当​​exp1​​​为假时,会直接忽略​​exp2​
  • ​exp1 || exp2​​​:当​​exp1​​​为真时,会直接忽略​​exp2​

当表达式的​​exit code​​​为0时,表示真;非0表示假。(与​​C/C++​​中的定义相反)

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

​test​​​命令用​​exit code​​​返回结果,而不是用​​stdout​​。

​0​​表示真,非0表示假

test 2 -lt 3
echo $? # 得到0, 表示 2 < 3 的结果是真

test 3 -lt 2
echo $? # 得到1, 表示 3 < 2 的结果是假

文件类型判断

如​​test -e filename​​,判断文件是否存在

参数


  • ​-e​​ 文件是否存在
  • ​-f​​ 是否为文件
  • ​-d​​ 是否为目录

test -e test.sh && echo "exists" || echo "Not exists"

文件权限判断

​test -r filename​​,判断文件是否可读

参数


  • ​-r​​ 文件是否可读
  • ​-w​​ 文件是否可写
  • ​-x​​ 文件是否可执行
  • ​-s​​ 文件是否非空

整数间比较

​test $a -eq $b​

参数


  • ​-eq​​ 是否相等
  • ​-ne​
  • ​-gt​
  • ​-ge​
  • ​-lt​
  • ​-le​

字符串比较

参数


  • ​test -z STRING​​ 判断STRING是否为空
  • ​test -n STRING​​​ 判断STRING是否非空(​​-n​​可以省略)
  • ​test str1==str2​​ 判断是否相等
  • ​test str1!=str2​

多重条件判定

​test -r filename -a -x filename​

参数


  • ​-a​​​ 两个条件是否同时成立(​​a for and​​)
  • ​-o​​​ 两个条件是否至少一个成立(​​o for or​​)
  • ​!​​​ 取反。如​​test ! -x filename​

判断符号​​[]​

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

例如

[ 2 -lt 3 ] # 为真, 返回值为0
echo $? # 输出0
[ -e test.sh ] && echo "exists" || echo "Not exists"

注意


  • ​[]​​​中的每一项之间都要用空格隔开,比如​​[ 2 -lt 3 ]​
  • ​[]​​括号内的变量,最好用双引号括起来
  • ​[]​​括号内的常数,最好用单/双引号括起来

name="acwing yxc"
[ $name == "acwing yxc" ] # 错误, 等价于[ acwing yxc == "acwing yxc" ] 会报错参数过多
[ "$name" == "acwing yxc" ] # 正确

​type [​​​ 会发现​​[​​是一个命令

判断语句

if…then 形式

单层if

命令格式

if condition
then
语句1
语句2
...
fi

判断的是condition的退出状态(​​exit code​​)是否为0

所以​​condition​​​应该是一个可执行的命令,​​if​​​会根据这个命令执行的返回值(​​exit code​​)来判断条件是否成立

示例

a=3
b=4
if [ "$a" -lt "$b" ] && [ "$a" -gt 2 ]
then
echo ${a}在范围内
fi

单层if-else

if condition
then
语句一
...
else
语句二
...
fi

多层if-elif-elif-else

if condition
then
语句一
...
elif condition
then
语句一
...
elif condition
then
语句一
...
else
语句一
...
fi

case…esac形式

类似于java或C++代码中的​​swich​

case $变量名称 in
值1)
语句1
语句2
...
;;
值2)
语句1
语句2
...
;;
*)
语句1
语句2
...
;;
esac

示例

a=4

case $a in
1)
echo ${a}等于1
;;
2)
echo ${a}等于2
;;
3)
echo ${a}等于3
;;
*)
echo ${a}等于其他
;;
esac

循环语句

for循环

第一种for循环

命令格式

for var in val1 val2 val3
do
语句1
语句2
...
done

示例:输出​​a b c​​ 每个元素一行

for i in a b c
do
echo $i
done

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

for file in `ls`
do
echo $file
done

示例:输出1-10

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

或者使用​​{1..10}​​​,​​{a..z}​

for i in {1..10}
do
echo $i
done
for i in {a..z}
do
echo $i
done

第二种for循环

类似于C++或者Java中的for循环

命令格式

for ((expression; condition; expresion))
do
语句1
语句2
...
done

示例,输出1-10

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

while循环

和C++与Java类似

命令格式

while condition
do
语句1
语句2
...
done

示例,从键盘读入并输出数据,直到按下文件结束符(​​Ctrl + d​​​)(当输入文件结束符EOF后,​​read​​命令返回false,此时循环结束)

while read name
do
echo $name
done

另一种类似do-while循环的循环

命令格式

until condition
do
语句1
语句2
...
done

只要condition为假,就一直执行,直到condition为真,结束。

示例

until [ "${word}" == "yes" ] || [ "${word}" == "YES" ]
do
read -p "Please input yes/YES to stop this program: " word
done

break命令

跳出当前这一层循环。(注意shell中的break不能跳出case,与C++有点不一样)

continue命令

跳出当前这一次循环。

示例(输出1-10中的全部奇数):

for ((i=1; i<= 10; i++))
do
if [ `expr $i % 2` -eq 0 ]
then
continue
fi
echo $i
done

死循环的处理方式:


  1. 使用top命令找到进程的PID
  2. 输入​​kill -9 PID​​ 杀掉进程

函数

shell中的函数类似于C/C++的函数,但是​​return​​​的返回值是​​exit code​​​,取值为​​0-255​​,其中0表示正常结束。

如果想获取函数的输出结果,可以通过​​echo​​​输出到​​stdout​​​中,然后通过​​${function_name}​​​来获取​​stdout​​中的结果。

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

命令格式

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

示例

# 先定义一个函数
func() {
name=hby
echo "Hello $name"
# 不写return语句,默认return 0
}
# 调用这个函数
func # 无需加小括号, 像命令一样直接调用

获取函数的执行结果和返回值

# 先定义一个函数
func() {
name=hby
echo "Hello, $name"
return 123
}

output=$(func) #调用函数并获取stdout
ret=$? #获取返回值

echo "output=$output" # output=Hello, hby
echo "ret=$ret" # ret=123

函数参数传递

在函数内,​​$1​​​表示第一个参数,​​$2​​表示第二个参数,…

​$0​​仍然是文件名,注意

示例(计算1到n的和)

func() {
if [ $1 -le 0 ]
then
echo 0
return 0
fi

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

echo $(func 10) # 55

函数内的局部变量

可以在函数内部定义局部变量,作用范围仅在当前函数内。可以在递归函数中定义局部变量,以防止进行递归调用函数时,变量之间相互影响。

命令格式:

local 变量名=变量值

例如:

#! /bin/bash

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

func # 执行函数
echo $name # 该变量不存在,输出空串

其实shell执行的很慢,可以将上面的函数调用改为100,用​​time​​命令查看一下执行时间

​time ./test.sh​

会发现都需要0.5s,可见非常慢了

exit命令

​exit​​命令用来退出当前的shell进程,并返回一个退出状态

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

示例

#! /bin/bash
if [ $# -ne 1 ] # 如果传入的参数个数等于1
then
echo "arguments not valid"
exit 1
else
echo "arguments valid"
exit 0
fi

文件重定向

每个进程都会默认打开3个文件


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

可以用文件重定向,将上面3个文件重定向到其他文件中

重定向命令列表


  • ​command > file​​​ :将​​stdout​​​重定向到​​file​​中
  • ​command < file​​​:将​​stdin​​​重定向到​​file​​中
  • ​command >> file​​​:将​​stdout​​​以追加方式重定向到​​file​
  • ​command n> file​​​:将文件描述符​​n​​​重定向到​​file​
  • ​command n>> file​​​:将文件描述符​​n​​​以追加方式重定向到​​file​

示例

#! /bin/bash

echo -e "Hello \c" > output.txt
echo "World" >> output.txt

read str < output.txt # 将这条命令的stdin重定向到output.txt,而read命令是从stdin中读取数据,所以就变成了read从output.txt中读取数据
echo $str

可以同时重定向​​stdin​​​和​​stdout​

先写一个脚本​​test.sh​​​,从​​stdin​​​中读入​​a​​​和​​b​​​,并输出两数之和到​​stdout​

#! /bin/bash

read a
read b

echo `expr $a + $b`

在编写一个​​input.txt​

11 22 

然后运行​​test.sh​​​,将其输入重定向为​​input.txt​

./test.sh < input.txt # 结果 33
./test.sh < input.txt > output.txt # 同时重定向输入和输出
# 等价于
./test.sh 0< input.txt > 1output.txt # 同时重定向输入和输出

引入外部脚本

C++中可以用include引入外部文件,java可以用import引入

shell中也可以引入其他文件中的代码

语法格式如下

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

# 或者

source filename

示例

先创建一个文件​​test1.sh​​,内容如下

name=hby

再创建一个文件​​test2.sh​​,内容如下

#! /bin/bash
. test1.sh # 这里其实相当于把 test1.sh里面的内容全部拷贝到这里了
echo $name

甚至​​test1.sh​​​都不需要是一个​​.sh​​文件,任意文件都可。

比如其文件名为​​data​​(不带任何扩展名)

然后在​​test2.sh​​​中直接引入这个​​data​​文件

#! /bin/bash
source data
echo $name

并且引入文件,可以加路径,用相对路径或者绝对路径都可

注:

因为我们的命令行终端,也是一个​​bash​​程序,可以将其理解为一个大的文件。

这个​​bash​​​程序执行之前,会先执行一次​​.bashrc​​​里面的所有内容。这个​​.bashrc​​里面会设置一些环境变量,命令别名什么的。

我们修改完​​.bashrc​​​这个文件后,一般都会​​source​​一下

​source .bashrc​

这个​​source .bashrc​​​的命令,就是将​​.bashrc​​里的内容全部执行一遍

但是像vim有个​​.vimrc​​​,这个文件用​​source​​的话会报错。

因为​​.vimrc​​它不是一个shell脚本,而是vim自带的一些语法。