shell 是英语“壳,外壳”的意思。你可以把它想象成嵌入在 Linux 这样的操作系统中的一个“微型编程语言”。
写一个 shell 脚本:
- 创建脚本文件
vim test.sh
.sh,这已经成为一种约定俗成的命名惯例了 ,其实 Shell 脚本文件和普通的文本文件并没有什么区别。我们给它加上 .sh 以强调这是一个 Shell 脚本文件。我们大可以给这个文件起名叫 test (不带 .sh 后缀)。
- 指定脚本要使用的Shell
#!/bin/bash
上面这句代码中, /bin/bash 是 bash 程序在大多数 Linux 系统中的存放路径,而最前面的 #! 被称作 Sha-bang,或者 Shebang。
在计算机科学中,Shebang(也称为 Hashbang )是一个由井号和叹号构成的字符串行 #! ,其出现在文本文档的第一行的前两个字符。
在文档中存在 Shebang 的情况下,类 Unix 操作系统的进程载入器会分析 Shebang 后的内容,将这些内容作为解释器指令,并调用该指令,并将载有 Shebang 的文档路径作为该解释器的参数。
这一行( #!/bin/bash )其实并不是必不可少的,但是它可以保证此脚本会被我们指定的 Shell 执行。
如果你没有写这一行,那么此脚本文件会被用户当前的 Shell 所执行。这就可能产生问题:假如你的脚本是用 bash 的语法来写的,而运行这个脚本的用户的 Shell 是 ksh,那么这个脚本就应该不能正常运行了。
- 运行命令
#!/bin/bash
ls
- 注释
Shell 脚本的注释以 # (井号)开头。例如:
#!/bin/bash
# 列出目录的文件
ls
- 运行 shell 脚本
给脚本文件添加可执行的权限
chmod +x test.sh
运行:
./test.sh
以调试模式运行
我们需要学习如何调试一个脚本程序。用法如下:
bash -x test.sh
我们直接调用 bash 这个 Shell 程序,并且给它一个参数 -x (表示以调试模式运行),后面再跟上要调试运行的脚本文件。
如此一来,Shell 就会把我们的脚本文件运行时的细节打印出来了,在出现错误时可以帮助我们排查问题所在。
创建属于自己的命令
目前,我们的 Shell 脚本文件须要这样运行:
./test.sh
而且我们需要位于正确的目录中。
那么其他的一些程序(命令其实都是程序),比如 git,pwd,ls,等等,为什么可以直接从不论哪个目录执行(不需要在前面加上 ./ 这样的路径)呢?
秘密就在于这些程序存放的目录是在 PATH 这个环境变量中的。
PATH 是 Linux 的一个系统变量。这个变量包含了你系统里所有可以被直接执行的程序的路径。
如果我们在终端输入
echo $PATH
我们就可以看到目前自己系统里的那些“特殊”的目录了。
例如我的情况:
每一个路径之间是用冒号( : )来分隔的。
因此,只要你把 test.sh 这个文件拷贝到上述路径列表的任意一个目录(例如 /usr/local/bin ,/usr/bin,等等)中,你就可以在随便什么目录中运行你的 Shell 脚本了。
test.sh
那么,现在就开始正式学习 shell。
变量
就让我们来定义一个变量吧。所有的变量都有一个名字和一个值。
message='Hello World'
注意:在等号两边不要加空格。
这样我们就定义了一个变量,但他只是存在内存中,如果我们想要他显示则需要:
echo $message
如果想要使用转义字符,则需要家 -e 参数:
echo -e what \n $message
而且还需要注意引号的使用:
我们可以用引号来界定包含空格的字符串。
引号一共有三种:
类型 | 表示 |
---|---|
单引号 | ' |
双引号 | " |
反引号 | ` |
根据引号类型不同,bash 的处理方式也会不同。
- 单引号忽略被它括起来的所有特殊字符。
比如:
echo 'The message is $message'
则,输出
The message is $message
- 双引号忽略大多数特殊字符,但不包括:美元符号( $ )、反引号( ` )、反斜杠( \ ),这3种特殊字符将不被忽略。 不忽略美元符号意味着 Shell 在双引号内部可进行变量名替换。
echo "The message is $message"
输出:
The message is Hello World
- 反引号,会执行变量(前提时变量时命令),如:
message=`pwd`
echo "You are in the directory $message"
输出:
You are in the directory /home/exe
read : 请求输入
我们可以请求用户输入文本,这就要用到 read 命令了。
read 命令读取到的文本会立即被储存在一个变量里。
read name
echo "Hello $name !"
read 可以同时给几个变量赋值:
read firstname lastname
echo "Hello $firstname $lastname !"
-p :显示提示信息,如:
read -p 'Please enter your name : ' name
echo "Hello $name !"
运行以上脚本:
这下我们的程序就比较友好了,因为用户知道要做什么。
-n :限制字符数目
read -p 'Please enter (5 characters max) : ' -n 5 name
echo "Hello $name !"
运行这个脚本,我们发现一旦输入的字符数达到了我们限定的 5 个,那么 bash 会立即显示 「 Hello + 我们输入的字符 !」,都不需要我们按下回车键。
-t :限制输入时间
用 -t 参数,我们可以限定用户的输入时间(以秒为单位),也就是说超过这个时间,就不读取输入了。
-s :隐藏输入内容
如果你想要用户输入的是一个密码,那 -s 参数还是有用的。
数学运算
在 bash 中,所有的变量都是字符串,因此它也不会做运算。但我们可以用 let 命令来让他做计算:
let "a = 5"
let "b = 2"
let "c = a + b"
可用的运算符是以下几种:
运算 | 符号 |
---|---|
加法 | + |
减法 | - |
乘法 | * |
除法 | / |
幂( 乘方) | ** |
余( 整数除法的余数) | % |
和其他大多数主流编程语言一样,bash 也支持运算的「连写」:
let "a = a * 3"
环境变量
到目前为止,我们在脚本文件中创建的变量只存在于脚本中。换言之,在 A 脚本程序中定义的变量不能被 B 脚本程序使用。
我们来学习一个被称为「环境变量」的特殊变量。顾名思义,Shell 的环境变量可以被此种 Shell 的任意脚本程序使用。我们有时也把环境变量称为「全局变量」。
我们可以用 env 命令来显示你目前所有的环境变量:
env
其中比较重要的几个环境变量是:
SHELL :指明目前你使用的是哪种 Shell。我目前用的是 zsh。
PATH :是一系列路径的集合。只要有可执行程序位于任意一个存在于 PATH 中的路径,那么我们就可以直接输入可执行程序的名字来执行,而不需要加上所在路径前缀或进入到可执行程序所在目录去执行。上一课我们已经学习过 PATH 了。
HOME :你的家目录所在的路径。
PWD :目前所在的目录
OLDPWD :你上次所在的目录
可以看到,这些环境变量的名字都约定俗称是大写的。
如何使用这些环境变量呢?很简单,就和平时使用变量一样:
#!/bin/bash
echo "Your default Shell is $SHELL"
有时,我们需要自己定义环境变量。你可以用 export 命令来完成。在 .bashrc 或 .zshrc 这样的 Shell 配置文件里可以找到这样的命令:
如上图,我就定义了一些环境变量,比如 NDK_CCACHE 的值是 ccache,CCACHE_DIR 的值是 ~/.ccache。
参数变量
就跟我们之前学过的各种 Linux 命令一样,你的 Shell 脚本也可以接收参数。
假设,我们可以这样调用我们的脚本文件:
./variable.sh 参数1 参数2 参数3
这些个 参数1,参数2,参数3 被称为「参数变量」。
但问题是我们还不知道如何接收这些参数到我们的脚本中。其实不难,因为这些变量是被自动创建的。
$# :包含参数的数目。
$0 :包含被运行的脚本的名称 (我们的示例中就是 variable.sh )。
$1:包含第一个参数。
$2:包含第二个参数。
...
$8 :包含第八个参数。
...
依次类推。
echo "You have executed $0, there are $# parameters"
echo "The first parameter is $1"
如果我们有很多很多参数怎么办呢?可以用 shift 命令来「挪移」参数,以便依次处理。
#!/bin/bash
echo "The first parameter is $1"
shift
echo "The first parameter is now $1"
当然了,shift 命令常被用在循环中,使得参数一个接一个地被处理。
数组
定义数组:
array=('value0' 'value1' 'value2')
访问:
${array[2]}
我们也可以单独给数组的元素赋值,例如:
array[3]='value3'
我们可以一次性显示数组中所有的元素值,需要用到通配符 *(星号)。
#!/bin/bash
array=('value0' 'value1' 'value2')
array[5]='value5'
echo ${array[*]}
条件语句
if
if [ 条件测试 ]
then
做这个
fi
注意:方括号 [] 中的「条件测试」两边必须要空一格。不能写成 [test],而要写成[ test ]。
else,elif
if [ 条件测试 ]
then
做这个
elif [ 条件测试 2 ]
then
做 2 的事情
else
做那个
fi
不同的测试类型
在 bash 中我们可以做三种测试:
- 测试字符串
- 测试数字
- 测试文件
测试字符串
条件 | 意义 |
---|---|
$string1 = $string2 | 两个字符串是否相等。Shell 大小写敏感,因此 A 和 a 是不一样的。 |
$string1 != $string2 | 两个字符串是否不同。 |
-z $string | 字符串 string 是否为空。z是 zero 的首字母,是英语「零」的意思。 |
-n $string | 字符串 string 是否不为空。n 是英语 not 的首字母,是英语「不」的意思。 |
测试数字
条件 | 意义 |
---|---|
$num1 -eq $num2 | 两个数字是否相等。和判断字符串所用的符号( = )不一样。eq 是 equal 的缩写,是英语「等于」的意思。 |
$num1 -ne $num2 | 两个数字是否不同。ne 是 not equal 的缩写,是英语「不等于」的意思。 |
$num1 -lt $num2 | 数字 num1 是否小于 num2。lt 是 lower than 的缩写,是英语「小于」的意思。 |
$num1 -le $num2 | 数字 num1 是否小于或等于 num2。le 是 lower or equal 的缩写,是英语「小于或等于」的意思。 |
$num1 -gt $num2 | 数字 num1 是否大于 num2。gt 是 greater than 的缩写,是英语「大于」的意思。 |
$num1 -ge $num2 | 数字 num1 是否大于或等于 num2。ge 是 greater or equal 的缩写,是英语「大于或等于」的意思。 |
测试文件
相比于主流编程语言,Shell 的一大优势就是可以非常方便地测试文件:文件存在吗?我们可以写入文件吗?这个文件比那个文件修改时间更早还是更晚?
条件 | 意义 |
---|---|
-e $file | 文件是否存在。e 是 exist 的首字母,表示「存在」。 |
-d $file | 文件是否是一个目录。因为 Linux 中所有都是文件,目录也是文件的一种。d 是 directory 的首字母,表示「目录」。 |
-f $file | 文件是否是一个文件。f 是 file 的首字母,表示「文件」。 |
-L $file | 文件是否是一个符号链接文件。L 是 link 的首字母,表示「链接」。 |
-r $file | 文件是否可读。r 是 readable 的首字母,表示「可读的」。 |
-w $file | 文件是否可写。w 是 writable 的首字母,表示「可写的」。 |
-x $file | 文件是否可执行。x 是 executable 的首字母,表示「可执行的」。 |
$file1 -nt $file2 | 文件 file1 是否比 file2 更新。nt 是 newer than 的缩写,表示「更新的」。 |
$file1 -ot $file2 | 文件 file1 是否比 file2 更旧。ot 是 older than 的缩写,表示「更旧的」。 |
一次测试多个条件
在一个条件测试中,我们可以同时测试多个条件。需要用到两种符号:
符号 | 意义 |
---|---|
&& | 两个&。表示「逻辑与」。此符号两端的条件必须全为真,整个条件测试才为真;只要有一个不为真,整个条件测试为假。 |
II | 两个竖线。表示「逻辑或」。此符号两端的条件只要有一个为真,整个条件测试就为真;只有两个都为假,整个条件测试才为假。 |
反转测试
我们可以用「否定」来反转测试条件,要用到感叹号( ! )。
来看一个例子:
#!/bin/bash
read -p 'Enter a file : ' file
if [ ! -e $file ]
then
echo "$file does not exist"
else
echo "$file exists"
fi
条件测试中我们写了 「 ! -e $file 」,表示「如果文件 file 不存在」。
case : 测试多个条件
相当与其他语言的 switch:
#!/bin/bash
case $1 in
"Matthew")
echo "Hello Matthew !"
;;
"Mark")
echo "Hello Mark !"
;;
"Luke")
echo "Hello Luke !"
;;
"John")
echo "Hello John !"
;;
*)
echo "Sorry, I do not know you."
;;
esac
来分析一下上面的程序。因为有很多新的内容:
case $1 in :$1 表示我们要测试的变量是输入的第一个参数。in 是英语「在...之中」的意思。
"Matthew") :测试其中一个 case,也就是 $1 是否等于 "Matthew"。当然,我们也可以用星号来做通配符来匹配多个字符,例如 "M*") 可以匹配所有以 M 开头的字符串。
;; :类似于主流编程语言中的 「 break; 」,表示结束 case 的读取,程序跳转到 esac 后面执行。
*) :相当于 if 条件语句的 else,表示「否则」,就是「假如不等于上面任何一种情况」。
esac :是 case 的反写,表示 case 语句的结束。
循环
Shell 中,主要的循环语句有三种:while 循环,until 循环 和 for 循环。
while 循环
while 循环的逻辑是这样的:
while [ 条件测试 ]
do
做某些事
done
until 循环
与 while 这个关键字相反的有一个 until 关键字,until 在英语中是 「直到」的意思。
它也可以实现循环,只不过逻辑和 while 循环正好相反。
#!/bin/bash
until [ "$response" = 'yes' ]
do
read -p 'Say yes : ' response
done
for 循环
Shell 的 for 循环和主流的程序语言的循环略有不同。
- 遍历列表
for 循环可以遍历一个「取值列表」,基本的逻辑如下:
for 变量 in '值1' '值2' '值3' ... '值n'
do
做某些事
done
如:
#!/bin/bash
for animal in 'dog' 'cat' 'pig'
do
echo "Animal being analyzed : $animal"
done
for 循环的取值列表不一定要在代码中定义好,我们也可以用一个变量,如下例:
#!/bin/bash
listfile=`ls`
for file in $listfile
do
echo "File found : $file"
done
我们还可以简化上面的程序,不需要用到 listfile 这个变量:
#!/bin/bash
for file in `ls`
do
echo "File found : $file"
done
我们可以再改进这个程序,让它复制当前目录下的文件,并且把每个副本的名字修改为「现有名字 + -copy」 (copy 是英语「拷贝」的意思):
#!/bin/bash
for file in `ls`
do
cp $file $file-copy
done
常规点的 for 循环:
#!/bin/bash
for i in `seq 1 10`
do
echo $i
done
以上程序中,「 seq 1 10 」会返回一个取值列表,是从 1 到 10 的整数值。因此,echo 就会遍历输出 1 到 10 这 10 个整数。
Shell 函数
定义(或创建) Shell 函数是非常容易的。有两种方式:
函数名 () {
函数体
}
或
function 函数名 {
函数体
}
注意的地方:
函数名后面跟着的圆括号里不加任何参数:这一点与主流编程语言很不相同。
函数的完整定义必须置于函数的调用之前。
传递参数
在 Shell 函数中,我们给它传递参数的方式其实很像给 Shell 脚本传递命令行参数。我们把参数直接置于函数名字后面,然后就像我们之前 Shell 脚本的参数那样:$1, $2, $3, 等等。
#!/bin/bash
print_something () {
echo Hello $1
}
# 这里调用函数时传递了参数
print_something Luke
print_something John
返回值
大多数主流编程语言都有函数返回值的概念,可以让函数回传一些数据。
Shell 的函数却没办法做到。但是 Shell 的函数可以返回一个状态,有点类似一个程序或命令退出时会有一个退出状态,表明是否成功。
Shell 函数要返回状态,也用 return 这个关键字。
#!/bin/bash
print_something () {
echo Hello $1
return 1
}
print_something Luke
print_something John
echo Return value of previous function is $?
返回的状态不一定要是被硬编码的(比如上例中的 1 ),也可以是一个变量。
变量 $?
包含前一次被运行的命令或函数的返回状态。
运行结果:
一般来说,返回状态 0 表示一切顺利;一个非零值表示有错误。
变量作用范围
变量的作用范围意味着一个 Shell 脚本的哪些部分可以访问到这个变量。
默认来说,一个变量是全局的(global),意味着在脚本的任何地方都可以访问它。
我们也可以创建局部(local)变量。当我们在函数中创建局部变量时,这个变量就只能在这个函数中被访问。
要定义一个局部变量,我们只要在第一次给这个变量赋值时在变量名前加上关键字 local 即可( local 是英语「 本地的」的意思)。
定义局部变量有一个好处,就是可以防止被脚本的其他地方的代码意外改变数值。
重载命令
我们可以用函数来实现命令的重载,也就是说把函数的名字取成与我们通常在命令行用的命令相同的名字。
例如,也许我们每次在脚本中调用 ls 命令时,其实是想要实现 ls -lh 的效果。那么我们可以这么做:
#!/bin/bash
ls () {
command ls -lh
}
ls
如果没有 command 这个关键字,那么程序会陷入无限循环。
如果你不小心忘了 command 关键字而陷入无限循环,可以用 Ctrl + c 的组合快捷键来停止程序。