39 KiB
Shell 快速指南
概述
Bash 是一个 Unix Shell,作为 Bourne shell 的 free software 替代品,由 Brian Fox 为GNU项目编写。它发布于1989年,在很长一段时间,Linux 系统和 macOS 系统都把 Bash 作为默认的 shell。
那么,我们学习这个有着30多年历史的东西意义何在呢?答案很简单:这是当今最强大、可移植性最好的,为所有基于 Unix 的系统编写高效率脚本的工具之一。这就是你需要学习 bash 的原因。
Shells与模式
bash shell有交互和非交互两种模式。
交互模式
Ubuntu用户都知道,在Ubuntu中有7个虚拟终端。 桌面环境接管了第7个虚拟终端,于是按下Ctrl-Alt-F7
,可以进入一个操作友好的图形用户界面。
即便如此,还是可以通过Ctrl-Alt-F1
,来打开shell。打开后,熟悉的图形用户界面消失了,一个虚拟终端展现出来。
看到形如下面的东西,说明shell处于交互模式下:
user@host:~$
接着,便可以输入一系列Unix命令,比如ls
,grep
,cd
,mkdir
,rm
,来看它们的执行结果。
之所以把这种模式叫做交互式,是因为shell直接与用户交互。
直接使用虚拟终端其实并不是很方便。设想一下,当你想编辑一个文档,与此同时又想执行另一个命令,这种情况下,下面的虚拟终端模拟器可能更适合:
非交互模式
在非交互模式下,shell从文件或者管道中读取命令并执行。当shell解释器执行完文件中的最后一个命令,shell进程终止,并回到父进程。
可以使用下面的命令让shell以非交互模式运行:
sh /path/to/script.sh
bash /path/to/script.sh
上面的例子中,script.sh
是一个包含shell解释器可以识别并执行的命令的普通文本文件,sh
和bash
是shell解释器程序。你可以使用任何喜欢的编辑器创建script.sh
(vim,nano,Sublime Text, Atom等等)。
除此之外,你还可以通过chmod
命令给文件添加可执行的权限,来直接执行脚本文件:
chmod +x /path/to/script.sh
这种方式要求脚本文件的第一行必须指明运行该脚本的程序,比如:
#!/bin/bash
echo "Hello, world!"
如果你更喜欢用sh
,把#!/bin/bash
改成#!/bin/sh
即可。#!
被称作shebang。之后,就能这样来运行脚本了:
/path/to/script.sh
上面的例子中,我们使用了一个很有用的命令echo
来输出字符串到屏幕上。
我们还可以这样来使用shebang:
#!/usr/bin/env bash
echo "Hello, world!"
这样做的好处是,系统会自动在PATH
环境变量中查找你指定的程序(本例中的bash
)。相比第一种写法,你应该尽量用这种写法,因为程序的路径是不确定的。这样写还有一个好处,操作系统的PATH
变量有可能被配置为指向程序的另一个版本。比如,安装完新版本的bash
,我们可能将其路径添加到PATH
中,来“隐藏”老版本。如果直接用#!/bin/bash
,那么系统会选择老版本的bash
来执行脚本,如果用#!/usr/bin/env bash
,则会使用新版本。
返回值
每个命令都有一个返回值(返回状态或者退出状态)。命令执行成功的返回值总是0
(零值),执行失败的命令,返回一个非0值(错误码)。错误码必须是一个1到255之间的整数。
在编写脚本时,另一个很有用的命令是exit
。这个命令被用来终止当前的执行,并把返回值交给shell。当exit
不带任何参数时,它会终止当前脚本的执行并返回在它之前最后一个执行的命令的返回值。
一个程序运行结束后,shell将其返回值赋值给$?
环境变量。因此$?
变量通常被用来检测一个脚本执行成功与否。
与使用exit
来结束一个脚本的执行类似,我们可以使用return
命令来结束一个函数的执行并将返回值返回给调用者。当然,也可以在函数内部用exit
,这 不但 会中止函数的继续执行,而且 会终止整个程序的执行。
Shell 编程
在 bash 脚本文件中的第一行被叫做 shebang
。这一行决定了脚本可以像一个独立的可执行文件一样执行,而不用在终端之前输入sh
, bash
, python
, php
等。
#!/bin/bash
注释
脚本中可以包含 注释。注释是特殊的语句,会被 shell 解释器忽略。它们以 #
开头,到行尾结束。
示例:
#!/bin/bash
### This script will print your username.
whoami
Tip: 用注释来说明你的脚本是干什么的,以及为什么这样写。
变量
跟许多程序设计语言一样,你可以在 bash 中创建变量。
Bash 中没有数据类型,bash 中的变量可以保存一个数字、一个字符、一个字符串等等。同时无需提前声明变量,给变量赋值会直接创建变量。
你可以创建三种变量:局部变量,环境变量以及作为位置参数的变量。
局部变量
局部变量是仅在某个脚本内部有效的变量。它们不能被其他的程序和脚本访问。
局部变量可以用 =
声明(作为一种约定,变量名、=
、变量的值之间不应该有空格),其值可以用$
访问到。举个例子:
username="zhangpeng" ### 声明变量
echo $username ### 输出变量的值
unset username ### 删除变量
我们可以用 local
关键字声明属于某个函数的局部变量。这样声明的变量会在函数结束时消失。
local local_var="I'm a local value"
环境变量
环境变量是对当前 shell 会话内所有的程序或脚本都可见的变量。创建它们跟创建局部变量类似,但使用的是 export
关键字。
export GLOBAL_VAR="I'm a global value"
bash 中有非常多的环境变量。你会非常频繁地遇到它们,这里有一张速查表,记录了在实践中最常见的环境变量。
变量 | 描述 |
---|---|
$HOME |
当前用户的用户目录 |
$PATH |
用分号分隔的目录列表,shell会到这些目录中查找命令 |
$PWD |
当前工作目录 |
$RANDOM |
0到32767之间的整数 |
$UID |
数值类型,当前用户的用户ID |
$PS1 |
主要系统输入提示符 |
$PS2 |
次要系统输入提示符 |
这里 有一张更全面的 Bash 环境变量列表。
位置参数
位置参数是在调用一个函数并传给它参数时创建的变量。下表列出了在函数中,位置参数变量和一些其它的特殊变量以及它们的意义。
变量 | 描述 |
---|---|
$0 |
脚本名称 |
$1 … $9 |
第1个到第9个参数列表 |
${10} … ${N} |
第10个到N个参数列表 |
$* or $@ |
除了$0 外的所有位置参数 |
$# |
不包括$0 在内的位置参数的个数 |
$FUNCNAME |
函数名称(仅在函数内部有值) |
在下面的例子中,位置参数为:$0='./script.sh'
,$1='foo'
,$2='bar'
:
$ ./script.sh foo bar
变量可以有默认值。我们可以用如下语法来指定默认值:
### 如果变量为空,赋给他们默认值
: ${VAR:='default'}
: ${1:='first'}
echo "\$1 : " $1
: ${2:='second'}
echo "\$2 : " $2
### 或者
FOO=${FOO:-'default'}
Shell扩展
扩展 发生在一行命令被分成一个个的 记号(tokens) 之后。换言之,扩展是一种执行数学运算的机制,还可以用来保存命令的执行结果,等等。
感兴趣的话可以阅读关于shell扩展的更多细节。
大括号扩展
大括号扩展让生成任意的字符串成为可能。它跟 文件名扩展 很类似,举个例子:
echo beg{i,a,u}n ### begin began begun
大括号扩展还可以用来创建一个可被循环迭代的区间。
echo {0..5} ### 0 1 2 3 4 5
echo {00..8..2} ### 00 02 04 06 08
命令置换
命令置换允许我们对一个命令求值,并将其值置换到另一个命令或者变量赋值表达式中。当一个命令被````或$()
包围时,命令置换将会执行。举个例子:
now=`date +%T`
### or
now=$(date +%T)
echo $now ### 19:08:26
算数扩展
在bash中,执行算数运算是非常方便的。算数表达式必须包在$(( ))
中。算数扩展的格式为:
result=$(( ((10 + 5*3) - 7) / 2 ))
echo $result ### 9
在算数表达式中,使用变量无需带上$
前缀:
x=4
y=7
echo $(( x + y )) ### 11
echo $(( ++x + y++ )) ### 12
echo $(( x + y )) ### 13
单引号和双引号
单引号和双引号之间有很重要的区别。在双引号中,变量引用或者命令置换是会被展开的。在单引号中是不会的。举个例子:
echo "Your home: $HOME" ### Your home: /Users/<username>
echo 'Your home: $HOME' ### Your home: $HOME
当局部变量和环境变量包含空格时,它们在引号中的扩展要格外注意。随便举个例子,假如我们用echo
来输出用户的输入:
INPUT="A string with strange whitespace."
echo $INPUT ### A string with strange whitespace.
echo "$INPUT" ### A string with strange whitespace.
调用第一个echo
时给了它5个单独的参数 —— $INPUT被分成了单独的词,echo
在每个词之间打印了一个空格。第二种情况,调用echo
时只给了它一个参数(整个$INPUT的值,包括其中的空格)。
来看一个更严肃的例子:
FILE="Favorite Things.txt"
cat $FILE ### 尝试输出两个文件: `Favorite` 和 `Things.txt`
cat "$FILE" ### 输出一个文件: `Favorite Things.txt`
尽管这个问题可以通过把FILE重命名成Favorite-Things.txt
来解决,但是,假如这个值来自某个环境变量,来自一个位置参数,或者来自其它命令(find
, cat
, 等等)呢。因此,如果输入 可能 包含空格,务必要用引号把表达式包起来。
数组
跟其它程序设计语言一样,bash中的数组变量给了你引用多个值的能力。在bash中,数组下标也是从0开始,也就是说,第一个元素的下标是0。
跟数组打交道时,要注意一个特殊的环境变量IFS
。IFS,全称 Input Field Separator,保存了数组中元素的分隔符。它的默认值是一个空格IFS=' '
。
创建数组
在 bash 中有好几种方法创建一个数组
array[0] = val
array[1] = val
array[2] = val
array=([2]=val [0]=val [1]=val)
array=(val val val)
数组扩展
单个数组元素的扩展跟普通变量的扩展类似:
echo ${colors[1]} ### Green
整个数组可以通过把数字下标换成*
或@
来扩展:
echo ${colors[*]} ### Red Green Blue
echo ${colors[@]} ### Red Green Blue
上面两行有很重要(也很微妙)的区别,假设某数组元素中包含空格:
colors[0]=Red
colors[1]="Dark Green"
colors[2]=Blue
为了将数组中每个元素单独一行输出,我们用内建的printf
命令:
printf "+ %s\n" ${colors[*]}
# 输出:
# + Red
# + Dark
# + Green
# + Blue
为什么Desert
和fig
各占了一行?尝试用引号包起来:
printf "+ %s\n" "${colors[*]}"
# 输出:
# + Red Dark Green Blue
现在所有的元素都跑去了一行 —— 这不是我们想要的!为了解决这个痛点,${colors[@]}
闪亮登场:
printf "+ %s\n" "${colors[@]}"
# 输出:
+ Red
+ Dark Green
+ Blue
在引号内,${colors[@]}
将数组中的每个元素扩展为一个单独的参数;数组元素中的空格得以保留。
数组切片
除此之外,可以通过 切片 运算符来取出数组中的某一片元素:
echo ${colors[@]:0:2} ### Red Dark Green
在上面的例子中,${colors[@]}
扩展为整个数组,:0:2
取出了数组中从0开始,长度为2的元素。
向数组中添加元素
向数组中添加元素也非常简单。复合赋值在这里显得格外有用。我们可以这样做:
colors=(Yellow "${colors[@]}" Pink Black)
echo ${colors[@]}
# 输出:
# Yellow Red Dark Green Blue Pink Black
上面的例子中,${colors[@]}
扩展为整个数组,并被置换到复合赋值语句中,接着,对数组colors
的赋值覆盖了它原来的值。
从数组中删除元素
用unset
命令来从数组中删除一个元素:
unset colors[0]
echo ${colors[@]}
# 输出:
# Red Dark Green Blue Pink Black
运算符
算术运算符
下表列出了常用的算术运算符,假定变量 a 为 10,变量 b 为 20:
运算符 | 说明 | 举例 |
---|---|---|
+ | 加法 | expr $a + $b 结果为 30。 |
- | 减法 | expr $a - $b 结果为 -10。 |
* | 乘法 | expr $a \* $b 结果为 200。 |
/ | 除法 | expr $b / $a 结果为 2。 |
% | 取余 | expr $b % $a 结果为 0。 |
= | 赋值 | a=$b 将把变量 b 的值赋给 a。 |
== | 相等。用于比较两个数字,相同则返回 true。 | [ $a == $b ] 返回 false。 |
!= | 不相等。用于比较两个数字,不相同则返回 true。 | [ $a != $b ] 返回 true。 |
**注意:**条件表达式要放在方括号之间,并且要有空格,例如: [$a==$b] 是错误的,必须写成 [ $a == $b ]。
示例:
a=10
b=20
echo "a=$a, b=$b"
val=`expr $a + $b`
echo "a + b : $val"
val=`expr $a - $b`
echo "a - b : $val"
val=`expr $a \* $b`
echo "a * b : $val"
val=`expr $b / $a`
echo "b / a : $val"
val=`expr $b % $a`
echo "b % a : $val"
if [ $a == $b ]
then
echo "a 等于 b"
fi
if [ $a != $b ]
then
echo "a 不等于 b"
fi
关系运算符
关系运算符只支持数字,不支持字符串,除非字符串的值是数字。
下表列出了常用的关系运算符,假定变量 a 为 10,变量 b 为 20:
运算符 | 说明 | 举例 |
---|---|---|
-eq | 检测两个数是否相等,相等返回 true。 | [ $a -eq $b ] 返回 false。 |
-ne | 检测两个数是否相等,不相等返回 true。 | [ $a -ne $b ] 返回 true。 |
-gt | 检测左边的数是否大于右边的,如果是,则返回 true。 | [ $a -gt $b ] 返回 false。 |
-lt | 检测左边的数是否小于右边的,如果是,则返回 true。 | [ $a -lt $b ] 返回 true。 |
-ge | 检测左边的数是否大于等于右边的,如果是,则返回 true。 | [ $a -ge $b ] 返回 false。 |
-le | 检测左边的数是否小于等于右边的,如果是,则返回 true。 | [ $a -le $b ] 返回 true。 |
示例:
a=10
b=20
if [ $a -eq $b ]
then
echo "$a -eq $b : a 等于 b"
else
echo "$a -eq $b: a 不等于 b"
fi
if [ $a -ne $b ]
then
echo "$a -ne $b: a 不等于 b"
else
echo "$a -ne $b : a 等于 b"
fi
if [ $a -gt $b ]
then
echo "$a -gt $b: a 大于 b"
else
echo "$a -gt $b: a 不大于 b"
fi
if [ $a -lt $b ]
then
echo "$a -lt $b: a 小于 b"
else
echo "$a -lt $b: a 不小于 b"
fi
if [ $a -ge $b ]
then
echo "$a -ge $b: a 大于或等于 b"
else
echo "$a -ge $b: a 小于 b"
fi
if [ $a -le $b ]
then
echo "$a -le $b: a 小于或等于 b"
else
echo "$a -le $b: a 大于 b"
fi
布尔运算符
下表列出了常用的布尔运算符,假定变量 a 为 10,变量 b 为 20:
运算符 | 说明 | 举例 |
---|---|---|
! | 非运算,表达式为 true 则返回 false,否则返回 true。 | [ ! false ] 返回 true。 |
-o | 或运算,有一个表达式为 true 则返回 true。 | [ $a -lt 20 -o $b -gt 100 ] 返回 true。 |
-a | 与运算,两个表达式都为 true 才返回 true。 | [ $a -lt 20 -a $b -gt 100 ] 返回 false。 |
示例:
a=10
b=20
echo "a=$a, b=$b"
if [ $a != $b ]
then
echo "$a != $b : a 不等于 b"
else
echo "$a != $b: a 等于 b"
fi
if [ $a -lt 100 -a $b -gt 15 ]
then
echo "$a 小于 100 且 $b 大于 15 : 返回 true"
else
echo "$a 小于 100 且 $b 大于 15 : 返回 false"
fi
if [ $a -lt 100 -o $b -gt 100 ]
then
echo "$a 小于 100 或 $b 大于 100 : 返回 true"
else
echo "$a 小于 100 或 $b 大于 100 : 返回 false"
fi
if [ $a -lt 5 -o $b -gt 100 ]
then
echo "$a 小于 5 或 $b 大于 100 : 返回 true"
else
echo "$a 小于 5 或 $b 大于 100 : 返回 false"
fi
逻辑运算符
以下介绍 Shell 的逻辑运算符,假定变量 a 为 10,变量 b 为 20:
运算符 | 说明 | 举例 |
---|---|---|
&& | 逻辑的 AND | $a -lt 100 && $b -gt 100 返回 false |
|| | 逻辑的 OR | [[ $a -lt 100 |
示例:
a=10
b=20
if [[ $a -lt 100 && $b -gt 100 ]]
then
echo "返回 true"
else
echo "返回 false"
fi
if [[ $a -lt 100 || $b -gt 100 ]]
then
echo "返回 true"
else
echo "返回 false"
fi
字符串运算符
下表列出了常用的字符串运算符,假定变量 a 为 "abc",变量 b 为 "efg":
运算符 | 说明 | 举例 |
---|---|---|
= | 检测两个字符串是否相等,相等返回 true。 | [ $a = $b ] 返回 false。 |
!= | 检测两个字符串是否相等,不相等返回 true。 | [ $a != $b ] 返回 true。 |
-z | 检测字符串长度是否为0,为0返回 true。 | [ -z $a ] 返回 false。 |
-n | 检测字符串长度是否为0,不为0返回 true。 | [ -n $a ] 返回 true。 |
str | 检测字符串是否为空,不为空返回 true。 | [ $a ] 返回 true。 |
示例:
a="abc"
b="efg"
if [ $a = $b ]
then
echo "$a = $b : a 等于 b"
else
echo "$a = $b: a 不等于 b"
fi
if [ $a != $b ]
then
echo "$a != $b : a 不等于 b"
else
echo "$a != $b: a 等于 b"
fi
if [ -z $a ]
then
echo "-z $a : 字符串长度为 0"
else
echo "-z $a : 字符串长度不为 0"
fi
if [ -n $a ]
then
echo "-n $a : 字符串长度不为 0"
else
echo "-n $a : 字符串长度为 0"
fi
if [ $a ]
then
echo "$a : 字符串不为空"
else
echo "$a : 字符串为空"
fi
文件测试运算符
文件测试运算符用于检测 Unix 文件的各种属性。
属性检测描述如下:
操作符 | 说明 | 举例 |
---|---|---|
-b file | 检测文件是否是块设备文件,如果是,则返回 true。 | [ -b $file ] 返回 false。 |
-c file | 检测文件是否是字符设备文件,如果是,则返回 true。 | [ -c $file ] 返回 false。 |
-d file | 检测文件是否是目录,如果是,则返回 true。 | [ -d $file ] 返回 false。 |
-f file | 检测文件是否是普通文件(既不是目录,也不是设备文件),如果是,则返回 true。 | [ -f $file ] 返回 true。 |
-g file | 检测文件是否设置了 SGID 位,如果是,则返回 true。 | [ -g $file ] 返回 false。 |
-k file | 检测文件是否设置了粘着位(Sticky Bit),如果是,则返回 true。 | [ -k $file ] 返回 false。 |
-p file | 检测文件是否是有名管道,如果是,则返回 true。 | [ -p $file ] 返回 false。 |
-u file | 检测文件是否设置了 SUID 位,如果是,则返回 true。 | [ -u $file ] 返回 false。 |
-r file | 检测文件是否可读,如果是,则返回 true。 | [ -r $file ] 返回 true。 |
-w file | 检测文件是否可写,如果是,则返回 true。 | [ -w $file ] 返回 true。 |
-x file | 检测文件是否可执行,如果是,则返回 true。 | [ -x $file ] 返回 true。 |
-s file | 检测文件是否为空(文件大小是否大于0),不为空返回 true。 | [ -s $file ] 返回 true。 |
-e file | 检测文件(包括目录)是否存在,如果是,则返回 true。 | [ -e $file ] 返回 true。 |
示例:
变量 file 表示文件"/var/www/runoob/test.sh",它的大小为100字节,具有 rwx 权限。下面的代码,将检测该文件的各种属性:
file="./operatorDemo.sh"
if [ -r $file ]
then
echo "文件可读"
else
echo "文件不可读"
fi
if [ -w $file ]
then
echo "文件可写"
else
echo "文件不可写"
fi
if [ -x $file ]
then
echo "文件可执行"
else
echo "文件不可执行"
fi
if [ -f $file ]
then
echo "文件为普通文件"
else
echo "文件为特殊文件"
fi
if [ -d $file ]
then
echo "文件是个目录"
else
echo "文件不是个目录"
fi
if [ -s $file ]
then
echo "文件不为空"
else
echo "文件为空"
fi
if [ -e $file ]
then
echo "文件存在"
else
echo "文件不存在"
fi
语句
条件语句
跟其它程序设计语言一样,Bash中的条件语句让我们可以决定一个操作是否被执行。结果取决于一个包在[[ ]]
里的表达式。
条件表达式可以包含&&
和||
运算符,分别对应 与 和 或 。除此之外还有很多有用的表达式。
共有两个不同的条件表达式:if
和case
。
基元和组合表达式
由[[ ]]
(sh
中是[ ]
)包起来的表达式被称作 检测命令 或 基元。这些表达式帮助我们检测一个条件的结果。在下面的表里,为了兼容sh
,我们用的是[ ]
。这里可以找到有关bash中单双中括号区别的答案。
跟文件系统相关:
基元 | 含义 |
---|---|
[ -e FILE ] |
如果FILE 存在 (exists),为真 |
[ -f FILE ] |
如果FILE 存在且为一个普通文件(file),为真 |
[ -d FILE ] |
如果FILE 存在且为一个目录(directory),为真 |
[ -s FILE ] |
如果FILE 存在且非空(size 大于0),为真 |
[ -r FILE ] |
如果FILE 存在且有读权限(readable),为真 |
[ -w FILE ] |
如果FILE 存在且有写权限(writable),为真 |
[ -x FILE ] |
如果FILE 存在且有可执行权限(executable),为真 |
[ -L FILE ] |
如果FILE 存在且为一个符号链接(link),为真 |
[ FILE1 -nt FILE2 ] |
FILE1 比FILE2 新(newer than) |
[ FILE1 -ot FILE2 ] |
FILE1 比FILE2 旧(older than) |
跟字符串相关:
基元 | 含义 |
---|---|
[ -z STR ] |
STR 为空(长度为0,zero) |
[ -n STR ] |
STR 非空(长度非0,non-zero) |
[ STR1 == STR2 ] |
STR1 和STR2 相等 |
[ STR1 != STR2 ] |
STR1 和STR2 不等 |
算数二元运算符:
基元 | 含义 |
---|---|
[ ARG1 -eq ARG2 ] |
ARG1 和ARG2 相等(equal) |
[ ARG1 -ne ARG2 ] |
ARG1 和ARG2 不等(not equal) |
[ ARG1 -lt ARG2 ] |
ARG1 小于ARG2 (less than) |
[ ARG1 -le ARG2 ] |
ARG1 小于等于ARG2 (less than or equal) |
[ ARG1 -gt ARG2 ] |
ARG1 大于ARG2 (greater than) |
[ ARG1 -ge ARG2 ] |
ARG1 大于等于ARG2 (greater than or equal) |
条件语句可以跟 组合表达式 配合使用:
Operation | Effect |
---|---|
[ ! EXPR ] |
如果EXPR 为假,为真 |
[ (EXPR) ] |
返回EXPR 的值 |
[ EXPR1 -a EXPR2 ] |
逻辑 与, 如果EXPR1 和(and)EXPR2 都为真,为真 |
[ EXPR1 -o EXPR2 ] |
逻辑 或, 如果EXPR1 或(or)EXPR2 为真,为真 |
当然,还有很多有用的基元,在Bash的man页面能很容易找到它们。
使用if
if
在使用上跟其它语言相同。如果中括号里的表达式为真,那么then
和fi
之间的代码会被执行。fi
标志着条件代码块的结束。
### 写成一行
if [[ 1 -eq 1 ]]; then echo "true"; fi
### 写成多行
if [[ 1 -eq 1 ]]; then
echo "true"
fi
同样,我们可以使用if..else
语句,例如:
### 写成一行
if [[ 2 -ne 1 ]]; then echo "true"; else echo "false"; fi
### 写成多行
if [[ 2 -ne 1 ]]; then
echo "true"
else
echo "false"
fi
有些时候,if..else
不能满足我们的要求。别忘了if..elif..else
,使用起来也很方便。
看下面的例子:
if [[ `uname` == "Adam" ]]; then
echo "Do not eat an apple!"
elif [[ `uname` == "Eva" ]]; then
echo "Do not take an apple!"
else
echo "Apples are delicious!"
fi
使用case
如果你需要面对很多情况,分别要采取不同的措施,那么使用case
会比嵌套的if
更有用。使用case
来解决复杂的条件判断,看起来像下面这样:
echo "input param: " $1
case $1 in
"jpg" | "jpeg")
echo "It's image with jpeg extension."
;;
"png")
echo "It's image with png extension."
;;
"gif")
echo "Oh, it's a giphy!"
;;
*)
echo "Woops! It's not image!"
;;
esac
每种情况都是匹配了某个模式的表达式。|
用来分割多个模式,)
用来结束一个模式序列。第一个匹配上的模式对应的命令将会被执行。*
代表任何不匹配以上给定模式的模式。命令块儿之间要用;;
分隔。
循环语句
循环其实不足为奇。跟其它程序设计语言一样,bash中的循环也是只要控制条件为真就一直迭代执行的代码块。
Bash中有四种循环:for
,while
,until
和select
。
for
循环
for
与它在C语言中的姊妹非常像。看起来是这样:
for arg in elem1 elem2 ... elemN
do
### 语句
done
在每次循环的过程中,arg
依次被赋值为从elem1
到elemN
。这些值还可以是通配符或者大括号扩展。
当然,我们还可以把for
循环写在一行,但这要求do
之前要有一个分号,就像下面这样:
for i in {1..5}; do echo $i; done
还有,如果你觉得for..in..do
对你来说有点奇怪,那么你也可以像C语言那样使用for
,比如:
for (( i = 0; i < 10; i++ )); do
echo $i
done
当我们想对一个目录下的所有文件做同样的操作时,for
就很方便了。举个例子,如果我们想把所有的.bash
文件移动到script
文件夹中,并给它们可执行权限,我们的脚本可以这样写:
#!/bin/bash
for FILE in $HOME/*.bash; do
mv "$FILE" "${HOME}/scripts"
chmod +x "${HOME}/scripts/${FILE}"
done
while
循环
while
循环检测一个条件,只要这个条件为 真,就执行一段命令。被检测的条件跟if..then
中使用的基元并无二异。因此一个while
循环看起来会是这样:
while [[ condition ]]
do
### 语句
done
跟for
循环一样,如果我们把do
和被检测的条件写到一行,那么必须要在do
之前加一个分号。
比如下面这个例子:
#!/bin/bash
### 0到9之间每个数的平方
x=0
while [[ $x -lt 10 ]]; do ### x小于10
echo $(( x * x ))
x=$(( x + 1 )) ### x加1
done
until
循环
until
循环跟while
循环正好相反。它跟while
一样也需要检测一个测试条件,但不同的是,只要该条件为 假 就一直执行循环:
until [[ condition ]]; do
### 语句
done
select
循环
select
循环帮助我们组织一个用户菜单。它的语法几乎跟for
循环一致:
select answer in elem1 elem2 ... elemN
do
### 语句
done
select
会打印elem1..elemN
以及它们的序列号到屏幕上,之后会提示用户输入。通常看到的是$?
(PS3
变量)。用户的选择结果会被保存到answer
中。如果answer
是一个在1..N
之间的数字,那么语句
会被执行,紧接着会进行下一次迭代 —— 如果不想这样的话我们可以使用break
语句。
一个可能的实例可能会是这样:
#!/bin/bash
PS3="Choose the package manager: "
select ITEM in bower npm gem pip
do
echo -n "Enter the package name: " && read PACKAGE
case $ITEM in
bower) bower install $PACKAGE ;;
npm) npm install $PACKAGE ;;
gem) gem install $PACKAGE ;;
pip) pip install $PACKAGE ;;
esac
break ### 避免无限循环
done
这个例子,先询问用户他想使用什么包管理器。接着,又询问了想安装什么包,最后执行安装操作。
运行这个脚本,会得到如下输出:
$ ./my_script
1) bower
2) npm
3) gem
4) pip
Choose the package manager: 2
Enter the package name: bash-handbook
<installing bash-handbook>
循环控制
我们会遇到想提前结束一个循环或跳过某次循环执行的情况。这些可以使用shell内建的break
和continue
语句来实现。它们可以在任何循环中使用。
break
语句用来提前结束当前循环。我们之前已经见过它了。
continue
语句用来跳过某次迭代。我们可以这么来用它:
for (( i = 0; i < 10; i++ )); do
if [[ $(( i % 2 )) -eq 0 ]]; then continue; fi
echo $i
done
运行上面的例子,会打印出所有0到9之间的奇数。
函数
在脚本中,我们可以定义并调用函数。跟其它程序设计语言类似,函数是一个代码块,但有所不同。
bash 中,函数是一个命令序列,这个命令序列组织在某个名字下面,即 函数名 。调用函数跟其它语言一样,写下函数名字,函数就会被 调用 。
我们可以这样声明函数:
my_func () {
### 语句
}
my_func ### 调用 my_func
我们必须在调用前声明函数。
函数可以接收参数并返回结果 —— 返回值。参数,在函数内部,跟非交互式下的脚本参数处理方式相同 —— 使用位置参数。返回值可以使用return
命令 返回 。
下面这个函数接收一个名字参数,返回0
,表示成功执行。
### 带参数的函数
greeting () {
if [[ -n $1 ]]; then
echo "Hello, $1!"
else
echo "Hello, unknown!"
fi
return 0
}
greeting Denys ### Hello, Denys!
greeting ### Hello, stranger!
我们之前已经介绍过返回值。不带任何参数的return
会返回最后一个执行的命令的返回值。上面的例子,return 0
会返回一个成功表示执行的值,0
。
流,管道以及序列
Bash有很强大的工具来处理程序之间的协同工作。使用流,我们能将一个程序的输出发送到另一个程序或文件,因此,我们能方便地记录日志或做一些其它我们想做的事。
管道给了我们创建传送带的机会,控制程序的执行成为可能。
学习如何使用这些强大的、高级的工具是非常非常重要的。
流
Bash接收输入,并以字符序列或 字符流 的形式产生输出。这些流能被重定向到文件或另一个流中。
有三个文件描述符:
代码 | 描述符 | 描述 |
---|---|---|
0 |
stdin |
标准输入 |
1 |
stdout |
标准输出 |
2 |
stderr |
标准错误输出 |
重定向让我们可以控制一个命令的输入来自哪里,输出结果到什么地方。这些运算符在控制流的重定向时会被用到:
Operator | Description |
---|---|
> |
重定向输出 |
&> |
重定向输出和错误输出 |
&>> |
以附加的形式重定向输出和错误输出 |
< |
重定向输入 |
<< |
Here文档 语法 |
<<< |
Here字符串 |
以下是一些使用重定向的例子:
### ls的结果将会被写到list.txt中
ls -l > list.txt
### 将输出附加到list.txt中
ls -a >> list.txt
### 所有的错误信息会被写到errors.txt中
grep da * 2> errors.txt
### 从errors.txt中读取输入
less < errors.txt
管道
我们不仅能将流重定向到文件中,还能重定向到其它程序中。管道 允许我们把一个程序的输出当做另一个程序的输入。
在下面的例子中,command1
把它的输出发送给了command2
,然后输出被传递到command3
:
command1 | command2 | command3
这样的结构被称作 管道。
在实际操作中,这可以用来在多个程序间依次处理数据。在下面的例子中,ls -l
的输出被发送给了grep
,来打印出扩展名是.md
的文件,它的输出最终发送给了less
:
ls -l | grep .md$ | less
管道的返回值通常是管道中最后一个命令的返回值。shell会等到管道中所有的命令都结束后,才会返回一个值。如果你想让管道中任意一个命令失败后,管道就宣告失败,那么需要用下面的命令设置pipefail选项:
set -o pipefail
命令序列
命令序列是由;
,&
,&&
或者||
运算符分隔的一个或多个管道序列。
如果一个命令以&
结尾,shell将会在一个子shell中异步执行这个命令。换句话说,这个命令将会在后台执行。
以;
分隔的命令将会依次执行:一个接着一个。shell会等待直到每个命令执行完。
### command2 会在 command1 之后执行
command1 ; command2
### 等同于这种写法
command1
command2
以&&
和||
分隔的命令分别叫做 与 和 或 序列。
与序列 看起来是这样的:
### 当且仅当command1执行成功(返回0值)时,command2才会执行
command1 && command2
或序列 是下面这种形式:
### 当且仅当command1执行失败(返回错误码)时,command2才会执行
command1 || command2
与 或 或 序列的返回值是序列中最后一个执行的命令的返回值。
Debugging
shell提供了用于debugging脚本的工具。如果我们想以debug模式运行某脚本,可以在其shebang中使用一个特殊的选项:
#!/bin/bash options
options是一些可以改变shell行为的选项。下表是一些可能对你有用的选项:
Short | Name | Description |
---|---|---|
-f |
noglob | 禁止文件名展开(globbing) |
-i |
interactive | 让脚本以 交互 模式运行 |
-n |
noexec | 读取命令,但不执行(语法检查) |
-t |
— | 执行完第一条命令后退出 |
-v |
verbose | 在执行每条命令前,向stderr 输出该命令 |
-x |
xtrace | 在执行每条命令前,向stderr 输出该命令以及该命令的扩展参数 |
举个例子,如果我们在脚本中指定了-x
例如:
#!/bin/bash -x
for (( i = 0; i < 3; i++ )); do
echo $i
done
这会向stdout
打印出变量的值和一些其它有用的信息:
$ ./my_script
+ (( i = 0 ))
+ (( i < 3 ))
+ echo 0
0
+ (( i++ ))
+ (( i < 3 ))
+ echo 1
1
+ (( i++ ))
+ (( i < 3 ))
+ echo 2
2
+ (( i++ ))
+ (( i < 3 ))
有时我们需要debug脚本的一部分。这种情况下,使用set
命令会很方便。这个命令可以启用或禁用选项。使用-
启用选项,+
禁用选项:
#!/bin/bash
echo "xtrace is turned off"
set -x
echo "xtrace is enabled"
set +x
echo "xtrace is turned off again"
资料
- awesome-shell,shell 资源列表
- awesome-bash,bash 资源列表
- bash-handbook
- bash-guide ,bash 基本用法指南
- bash-it,为你日常使用,开发以及维护 shell 脚本和自定义命令提供了一个可靠的框架
- dotfiles.github.io,上面有bash和其它shell的各种dotfiles集合以及shell框架的链接
- Runoob Shell 教程
最后,Stack Overflow上 bash 标签下有很多你可以学习的问题,当你遇到问题时,也是一个提问的好地方。