1. 简介
Shell 的作用是解释执行用户的命令,用户输入一条命令,shell 就行一条,这种方式成为交互式,还有另外一种方式,就是用户事先写一个 shell 脚本,包含很多命令,然后让 shell 一次性的进行执行,这种方式被称为“批处理方式”。 一般我们在UNIX中使用的 shell 就是 bash 和 sh,当然也有其他 shell,在 UNIX 环境下可以使用 /etc/shells: valid login shells 命令来显示所有的 shell,想要切换,直接输入 shell 名即可。
2. bash 启动
bash 启动脚本是 bash 启动时会自动执行的脚本,因此用户可以把一些环境变量的设置和 alias、umask 设置等放到启动脚本中,这样每次启动 shell 时都会自动生效。 但是,启动 bash 的方法不同,执行启动脚本的步骤也不同。
2.1. 作为交互登录 Shell 启动,或者使用 —login 参数启动
交互 Shell 指的是用户在提示符下输入命令的 Shell,而不是执行脚本的 shell。
这样启动 bash 会自动执行以下脚本: 1. 执行 /etc/profil,系统中的每个用户登录时都执行,只有管理员可以修改 2. 然后依次执行当前用户主目录的 ~/.bash_profile、~/.bash_login 和 ~/.profile 三个文件(如果存在的话) 3. 在Shell 退出时,会执行 ~/.bash_logout 脚本(如果存在的话)
通常在 ~/.bash_profile 中会有下面几行:
代码语言:javascript复制if [ -f ~/.bashrc ]; then
~/.bashrc
fi
这样,如果 ~/.bashrc 存在,则会继续调用这个脚本。
2.2. 以交互式非登陆 shell 启动
比如在图形界面下开一个终端窗口,或者在登录 Shell 提示符下再输入 bash 命令,就得到一个交互非登录的 shell。 这种 shell 在启动时自动执行 ~/.bashrc 脚本。 因此,如果要在启动脚本中做某些设置,使它在图形终端窗口和字符终端的Shell中都起作用,最好就是在 ~/.bashrc 中设置。
如果终端或远程登录,那么登录 Shell 是该用户的所有其他进程的父进程,所以环境变量在登录 Shell 的启动脚本里设置一次就可以自动带到其他非登录 Shell 里,而本地变量、函数、 alias 等设置没有办法带到子Shell里,需要每次启动非登录Shell时设置一遍,所以就需要有非登录Shell的启动脚本,所以一般来说在 ~/.bash_profile 里设置环境变量,在 ~/.bashrc 里设置本地变量、函数、 alias 等。 如果你的Linux带有图形系统则不能这样设置,由于从图形界面的窗口管理器登录并不会产生登录Shell,所以环境变量也应该在 ~/.bashrc 里设置。
2.3. 非交互式启动
为了执行脚本而 fork 出来的子 Shell 是非交互式 Shell,启动时执行的脚本文件有环境变量 BASH_ENV 定义,相当于执行下面的命令:
代码语言:javascript复制if [ -n "$BASH_ENV" ]; then . "$BASH_ENV"; fi
2.4. 以 sh 命令启动
如果以 sh 命令启动 bash,bash 将模拟 sh 的行为。
如果作为交互登录 shell 启动,则会依次执行: 1. /etc/profile 2. ~/.profile
如果作为交互式 Shell 启动,相当于执行。
代码语言:javascript复制if [ -n "$BASH_ENV" ]; then . "$BASH_ENV"; fi
以 #! /bin/sh 开头的脚本就是这种情况,不会运行任何脚本。
3. Shell 如何执行命令
3.1. 执行交互式命令
凡是使用 which 命令查不到程序文件所在位置的命令都是 shell 的内建命令,这些命令相当于 Shell 的进程中的一个函数,没有单独的 man 手册,可以使用下面的命令查看: man bash-builtins。 对于非内建命令,Shell 会 fork 并 exec 该命令,创建一个新的进程,但是对于内建命令则不会,如 cd、alias、umask、exit、export、shift、if、eval、[、for、while 等命令都是内建命令。 虽然内建命令不创建新的进程,但是也会有返回值,通常也用 0 表示调用成功,这个返回值被称为 Exit Status (状态码),可以使用特殊变量 $? 读出。
3.2. 执行脚本
shell 脚本中用 # 表示注释,相当于 C 语言中的 // 注释,但是#! 却表示该脚本使用 /bin/sh 解释执行,并不代表注释。 如下例是一个简单的 shell 脚本:
代码语言:javascript复制#! /bin/sh
cd ..
ls
将上面的代码保存在 .sh 文件中,即为一个 Shell 脚本。 执行脚本只需要输入命令:./script.sh,这是 sh ./script.sh 命令的简写。
执行上面的脚本的步骤为:
4. Shell 的基本语法
shell 提供了与操作系统通信的方式。此通信以交互的方式(来自键盘的输入立即操作)或作为一个 shell 脚本执行。shell 脚本是 shell 和操作系统命令的序列,它存储在文件中。
5. 变量
一般的,Shell 变量由全大写字母加下划线组成,有两种类型的 Shell 变量。
5.1. 环境变量
环境变量可以从父进程传给子进程,因此 Shell 进程的环境变量可以从当前的 Shell 进程传给 fork 出来的子进程,使用 printenv 命令可以显示当前 Shell 进程的环境变量。
5.2. 本地变量
只存在于当前 Shell 进程的变量,用set命令可以显示当前 Shell 进程中定义的所有变量(包括本地变量和环境变量)和函数。
下面介绍几个变量使用和显示的常用关键字:
5.3. set
显示当前所有变量和函数。
5.4. 变量的定义
环境变量是任何进程都有的概念,而本地变量是 Shell 所特有的概念,在 Shell 中,环境变量和本地变量的定义和用法相似,在 Shell 中定义或赋值一个变量可以使用下面的格式:
代码语言:javascript复制VARNAME=value
等号两边是不能有空格的,否则就会被解释成命令或命令行参数。
5.5. export — 将变量导出为环境变量
任何一个变量定义后都仅存在于当前的 Shell 进程,是本地变量,用 export 命令可以把本地变量导出为环境变量,定义和导出环境变量也可以一步完成。
代码语言:javascript复制export VARNAME=value
当然也可以分两步:
代码语言:javascript复制VARNAME=value
export VARNAME
5.6. unset — 删除变量
使用 unset 命令可以删除已经定义了的环境变量或本地变量。
代码语言:javascript复制unset VARNAME
5.7. echo — 显示变量的值
使用 echo 命令可以显示变量的值。
一般对于 VARNAME 变量,我们使用 ${VARNAME} 表示他的值,在不引起歧义的情况下,我们也可以直接使用 VARNAME 表示他的值。
Shell 中的所有变量都是字符串,Shell中的变量也不需要先定义后使用,使用一个没有定义的变量,这个变量的值为空字符串。
6. 通配符 — *、?、[]
Shell 中也有通配符,如下表:
shell 中的通配符
通配符 | 意义 |
---|---|
* | 匹配 0 个或多个任意字符 |
? | 匹配 1 个任意字符 |
[若干字符] | 匹配方括号中的任意字符 |
如我们可以使用 ls ch0[012].doc 命令查找文件,如果当前目录下有 ch00.doc 和 ch02.doc,ls 的参数会直接转换成这两个文件名,而不是一个匹配字符串。
7. 命令代换 — ` 或 $()
由反引号(键盘上 ESC 键下面的,主键盘区左上角·/~)所引起来的也是一条命令,Shell 会首先执行反引号中的命令,然后将结果代换到原来的位置进行原命令的执行,如下面的命令:
代码语言:javascript复制DATE=`date`
echo $DATE
反引号和 $() 是一样的:
代码语言:javascript复制DATE=$(date)
echo $DATE
8. 算术代换 — $(())
Shell 会将 $(()) 中的 Shell 变量的取值转换成整数用于算术计算(其他情况下 Shell 都将变量视为字符串,无法进行算术计算)
代码语言:javascript复制VAR=45
echo $(($VAR 3))
$(()) 中只能进行 、-、*、/ 和 () 运算,并且只能进行整数运算。
9. 转义字符 —
和 C 语言一样,Shell 中也需要转义字符,如 、$、、`、"
10. 字符串 — ’、"
在 Shell 中单引号中的所有字符都被认为是普通的字符,所以不需要转义字符,如运行:
代码语言:javascript复制echo '$SHELL'
会显示 $SHELL。
代码语言:javascript复制echo 'ABC\'
会显示 ABC
双引号也将其中的字符串视为字面值,但是反引号、$、转义字符等等都保持原来的意义。 如:
代码语言:javascript复制echo "$SHELL"
会显示 /bin/bash。
代码语言:javascript复制echo "`date`"
会显示 Sun Apr 20 11:22:06 CEST 2003。
11. Shell 脚本语法
Shell脚本与Windows/Dos下的批处理相似,也就是用各类命令预先放入到一个文件中,方便一次性执行的一个程序文件,主要是方便管理员进行设置或者管理用的。但是它比Windows下的批处理更强大,比用其他编程程序编辑的程序效率更高,因为它具有丰富的语法,可以实现控制、循环、判断等一系列类似编程语言的操作。
12. 条件测试 — test、[]
命令 test 或 [] 可以测试一个条件是否城里,如果测试结果为真,则该命令的 Exit Status 为0,如果测试结果为假,则命令的 Exit Status 为1(与C语言中正好相反) 由于 [] 中的 [ 实际上是一个命令,他后面的都是这个命令的参数,因此需要用空格隔开。
如下例:
代码语言:javascript复制VAR=2
test $VAR -gt 1
echo $?
显示 0。
代码语言:javascript复制VAR=2
test $VAR -gt 3
echo $?
显示 1。
test 也可以换成 []:
代码语言:javascript复制VAR=2
[ $VAR -gt 3 ]
echo $?
显示 1。
12.1. 常见测试命令
常见测试命令
命令 | 意义 |
---|---|
[ -d DIR ] | 如果 DIR 存在并且是一个目录,则为真 |
[ -f FILE ] | 如果 FILE 存在并且是一个文件,则为真 |
[ -z STRING ] | 如果 STRING 长度为 0,则为真(如果 STRING 不存在,则同样为真) |
[ -n STRING ] | 如果 STRING 长度非 0,则为真 |
[ STRING1 = STRING2 ] | 如果两个字符串完全相同,则为真 |
[ ARG1 OP ARG2 ] | ARG1 与 ARG2 均为整数时,OP 取 -eq(等于),-ne(不等于),-lt(小于),-le(小于等于),-gt(大于),-ge(大于等于)中的一个 |
与 C 语言类似,测试条件之间还可以做与、或、非逻辑运算。
常见逻辑运算
运算 | 意义 |
---|---|
[ ! EXPR ] | 逻辑反 |
[ EXPR1 -a EXPR2 ] | 逻辑与 |
[ EXPR1 -o EXPR2 ] | 逻辑或 |
如:
代码语言:javascript复制VAR=abc
[ -d Desktop -a $VAR = 'abc' ]
echo $?
需要注意的是,如果上例中的 VAR 变量没有被预先定义,那么就会被解释器展开为空字符串,整个命令就变成了:
代码语言:javascript复制[ -d Desktop -a = 'abc' ]
因此,解释器会报告相应的错误。
为了避免这样的意外情况发生,一个好的 Shell 编程习惯总是把变量取值放到双引号之中:
代码语言:javascript复制VAR=abc
[ -d Desktop -a "$VAR" = 'abc' ]
echo $?
这样,虽然 VAR 没有被预先定义,但是命令还是被展开成了。
代码语言:javascript复制[ -d Desktop -a "" = 'abc' ]
13. 分支控制 — if、then、elif、else、fi
和 C 语言类似,在 Shell 中使用 if、then、elif、else、fi 几个命令实现分支控制,例如:
代码语言:javascript复制if [ -f ~/.bashrc ]; then
.~/.bashrc
fi
13.1. :
: 是一个特殊的指令,称为“空命令”,该命令不做任何事,但是 Exit Status 总是真,也可以使用 /bin/true 或 /bin/false 获得总是真或假的 Exit Status。
代码语言:javascript复制if :; then echo "always true"; fi
与下面的例子是一样的:
代码语言:javascript复制if /bin/true; then echo "always true"; fi
13.2. read
我们也可以使用 read 命令等待用户键入一行字符串,存到一个 Shell 变量中。
代码语言:javascript复制#! /bin/sh
echo "Is it morning? Please answer yes or no."
read YES_OR_NO
if [ "$YES_OR_NO" = "yes" ]; then echo "Good morning!"
elif [ "$YES_OR_NO" = "no" ]; then
echo "Good afternoon!"
else
echo "Sorry, $YES_OR_NO not recognized. Enter yes or no."
exit 1
fi
exit 0
13.3. &&、||
与C语言类似,Shell 也提供 && 与 ||
代码语言:javascript复制test "$VAR" -gt 1 -a "$VAR" -lt 3
等价于。
代码语言:javascript复制test "$VAR" -gt 1 && test "$VAR" -lt 3
因为 && 操作的短路求值特性,很多 Shell 脚本喜欢写成:
代码语言:javascript复制test "$(whoami)" != 'root' && (echo you are using a nonprivileged account; exit 1)
13.4. case、esac
case 命令类似于 C 语言的 switch/case 语句,esac 用来标志 case 语句块的结束。 Shell 中的 case 语句不仅可以用来匹配数字,也可以用来匹配字符串和通配符。
如下例,每个匹配分支都可以有若干条命令,末尾必须以;;结束。
代码语言:javascript复制#! /bin/sh
echo "Is it morning? Please answer yes or no."
read YES_OR_NO
case "$YES_OR_NO" in
yes|y|Yes|YES)
echo "Good Morning!";;
[nN]*)
echo "Good Afternoon!";;
*)
echo "Sorry, $YES_OR_NO not recognized. Enter yes or no."
exit 1;;
esac
exit 0
14. for、do、done
Shell 脚本的 for 循环结构和 C 语言很不一样,他类似于某些编程语言的 foreach 循环。 如下面的例子:
代码语言:javascript复制#! /bin/sh
for FRUIT in apple banana pear; do
echo "I like $FRUIT"
done
例子中,FRUIT 是一个变量,让这个变量依次取值为 apple、banana、pear 做循环,done 用来标志循环结束。
如果目录下有 chap0、chap1、chap2 等文件,下面的循环将他们重命名为 chap0~ 、 chap1~ 、 chap2~ 等。
代码语言:javascript复制for FILENAME in chap?; do mv $FILENAME $FILENAME~; done
15. while、do、done
while 的用法和 C 语言非常类似,比如下面是一个验证密码的脚本:
代码语言:javascript复制#! /bin/sh
echo "Enter password:"
read TRY
while [ "$TRY" != "secret" ]; do
echo "Sorry, try again"
read TRY
done
我们也可以像 C 语言中那样控制 while 循环的循环次数:
代码语言:javascript复制#! /bin/sh
COUNTER=1
while [ "$COUNTER" -lt 10 ]; do
echo "Here we go again"
COUNTER=$(($COUNTER 1))
done
16. 一些特殊的变量
有很多变量是被 Shell 自动赋值的,如下表。
shell 中一些特殊的变量
变量 | 意义 |
---|---|
$0 | 相当于C语言 main 函数的 argv[0] |
$1 、 $2 … | 这些称为位置参数(Positional Parameter),相当于C语言 main 函数的 argv[1] 、 argv[2] … |
$# | 相当于C语言 main 函数的 argc - 1 ,注意这里的 # 后面不表示注释 |
$@ | 表示参数列表 "$1" "$2" … ,例如可以用在 for 循环中的 in 后面。 |
$? | 上一条命令的Exit Status |
$$ | 当前Shell的进程号 |
参数 $n 被称为“位置参数”。
16.1. shift
shift 命令可以令位置参数左移,比如 shift 3 表示让 4 变成 1,5 变成 2,原来的 1、2就会被丢弃掉,而
17. 函数
Shell 中的函数定义中没有返回值也没有参数列表。 如下面例子所示:
代码语言:javascript复制#! /bin/sh
foo(){ echo "Function foo is called";}
echo "-=start=-"
foo
echo "-=end=-"
注意函数体的左花括号{和后面的命令之间必须有空格或换行,如果将最后一条命令和右花括号 } 写在同一行,命令末尾必须有;号。 Shell脚本中的函数必须先定义后调用,一般把函数定义都写在脚本的前面,把函数调用和其它命令写在脚本的最后(类似C语言中的 main 函数,这才是整个脚本实际开始执行命令的地方)。
Shell函数没有参数列表并不表示不能传参数,事实上,函数就像是迷你脚本,调用函数时可以传任意个参数,在函数内同样是用 0 、 1 、 2 等变量来提取参数,函数中的位置参数相当于函数的局部变量,改变这些变量并不会影响函数外面的 0 、 1 、 2 等变量。函数中可以用 return 命令返回,如果 return 后面跟一个数字则表示函数的Exit Status。
代码语言:javascript复制#! /bin/sh
is_directory()
{
DIR_NAME=$1
if [ ! -d $DIR_NAME ]; then
return 1
else
return 0
fi
}
for DIR in "$@"; do
if is_directory "$DIR"
then :
else
echo "$DIR doesn't exist. Creating it now..."
mkdir $DIR > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo "Cannot create directory $DIR"
exit 1
fi
fi
done
18. Shell 脚本的调试方法
Shell提供了一些用于调试脚本的选项:
- -n — 读一遍脚本中的命令但是不执行,用于检查脚本中的语法错误
- -v — 一边执行脚本,一边将执行过的脚本命令打印到标准错误输出
- -x — 提供跟踪执行信息,将执行的每一条命令和结果依次打印出来
有三种方法使用这些选项: 1. 在命令行提供参数 $ sh -x ./script.sh 2. 在脚本开头提供参数 #! /bin/sh -x 3. 在脚本中用 set 命令启用或禁用参数
代码语言:javascript复制#! /bin/sh
if [ -z "$1" ]; then
set -x
echo "ERROR: Insufficient Args."
exit 1
set x
fi
19. 示例 — 九九乘法表
代码语言:javascript复制#
# file: nine.sh
# author: 龙泉居士
# date: 2013-05-06 22:58
#
VARI=1
while [ "$VARI" -lt 10 ]; do
VARJ=1
while [ "$VARJ" -lt "$VARI" ]; do
echo -n " "
VARJ=$(($VARJ 1))
done
VARJ="$VARI"
while [ "$VARJ" -lt 10 ]; do
VARM=$(($VARI*$VARJ))
if [ "$VARM" -lt 10 ]; then
echo -n " "
fi
echo -n "$VARM "
VARJ=$(($VARJ 1))
done
echo ""
VARI=$(($VARI 1))
done