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"
运行方式
-
作为可执行文件
给文件添加可执行权限
chmod +x test.sh
执行
./test.sh
-
用解释器执行
bash test.sh
单行注释:每行 # 之后的内容均是注释
# 这是注释 echo "Hello,World" # 注释
多行注释
:<其中EOF可以换成其他任意字符串,例如
:<变量 定义变量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} # 输出空字符串, 当一个变量不存在时, 其为空字符串变量类型
自定义变量(局部变量)
子进程不能访问的变量
环境变量(全局变量)
子进程可以访问的变量
将自定义变量改为环境变量
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[@]} # 数组长度(数组中实际有多少元素), 输出3expr命令不是一个shell内建的命令,而是一个第三方命令
expr命令可以用来求表达式的值,格式为
expr 表达式
字符串表达式
由于expr是一个命令,所以后面表达式的每一项(参数),都要用空格隔开
某一些特殊字符,需要用反斜杠来进行转义
特殊字符,如空格,要用引号括起来
expr会在stdout中输出结果
整数表达式
length STRING
返回 STRING 的长度
比如
#! /bin/bash str="Hello World" echo $(expr length "$str") # 输出11index 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) # 输出elloexpr支持算术操作,算术表达式优先级低于字符串表达式,高于逻辑关系表达式
- + - 加减,两端参数会转换为整数,转换失败时会报错
- * / % 乘除,取模
- () 可以改变优先级,但需要用反斜杠转义
例如
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` # 输出3read命令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 "HellonWorld" # -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 %sn" "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-elseif condition then 语句一 ... else 语句二 ... fi多层if-elif-elif-elseif condition then 语句一 ... elif condition then 语句一 ... elif condition then 语句一 ... else 语句一 ... ficase…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 donefor 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 donewhile循环和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 donebreak命令跳出当前这一层循环。(注意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死循环的处理方式:
函数
- 使用top命令找到进程的PID
- 输入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自带的一些语法。



