Shell 的作用是解释执行用户的命令,用户输入一条命令,shell 就行一条,这种方式成为交互式,还有另外一种方式,就是用户事先写一个 shell 脚本,包含很多命令,然后让 shell 一次性的进行执行,这种方式被称为“批处理方式”。 一般我们在UNIX中使用的 shell 就是 bash 和 sh,当然也有其他 shell,在 UNIX 环境下可以使用 /etc/shells: valid login shells 命令来显示所有的 shell,想要切换,直接输入 shell 名即可。
bash 启动脚本是 bash 启动时会自动执行的脚本,因此用户可以把一些环境变量的设置和 alias、umask 设置等放到启动脚本中,这样每次启动 shell 时都会自动生效。 但是,启动 bash 的方法不同,执行启动脚本的步骤也不同。
交互 Shell 指的是用户在提示符下输入命令的 Shell,而不是执行脚本的 shell。
这样启动 bash 会自动执行以下脚本: 1. 执行 /etc/profil,系统中的每个用户登录时都执行,只有管理员可以修改 2. 然后依次执行当前用户主目录的 ~/.bash_profile、~/.bash_login 和 ~/.profile 三个文件(如果存在的话) 3. 在Shell 退出时,会执行 ~/.bash_logout 脚本(如果存在的话)
通常在 ~/.bash_profile 中会有下面几行:
if [ -f ~/.bashrc ]; then
~/.bashrc
fi
这样,如果 ~/.bashrc 存在,则会继续调用这个脚本。
比如在图形界面下开一个终端窗口,或者在登录 Shell 提示符下再输入 bash 命令,就得到一个交互非登录的 shell。 这种 shell 在启动时自动执行 ~/.bashrc 脚本。 因此,如果要在启动脚本中做某些设置,使它在图形终端窗口和字符终端的Shell中都起作用,最好就是在 ~/.bashrc 中设置。
如果终端或远程登录,那么登录 Shell 是该用户的所有其他进程的父进程,所以环境变量在登录 Shell 的启动脚本里设置一次就可以自动带到其他非登录 Shell 里,而本地变量、函数、 alias 等设置没有办法带到子Shell里,需要每次启动非登录Shell时设置一遍,所以就需要有非登录Shell的启动脚本,所以一般来说在 ~/.bash_profile 里设置环境变量,在 ~/.bashrc 里设置本地变量、函数、 alias 等。 如果你的Linux带有图形系统则不能这样设置,由于从图形界面的窗口管理器登录并不会产生登录Shell,所以环境变量也应该在 ~/.bashrc 里设置。
为了执行脚本而 fork 出来的子 Shell 是非交互式 Shell,启动时执行的脚本文件有环境变量 BASH_ENV 定义,相当于执行下面的命令:
if [ -n "$BASH_ENV" ]; then . "$BASH_ENV"; fi
如果以 sh 命令启动 bash,bash 将模拟 sh 的行为。
如果作为交互登录 shell 启动,则会依次执行: 1. /etc/profile 2. ~/.profile
如果作为交互式 Shell 启动,相当于执行。
if [ -n "$BASH_ENV" ]; then . "$BASH_ENV"; fi
以 #! /bin/sh 开头的脚本就是这种情况,不会运行任何脚本。
凡是使用 which 命令查不到程序文件所在位置的命令都是 shell 的内建命令,这些命令相当于 Shell 的进程中的一个函数,没有单独的 man 手册,可以使用下面的命令查看: man bash-builtins。 对于非内建命令,Shell 会 fork 并 exec 该命令,创建一个新的进程,但是对于内建命令则不会,如 cd、alias、umask、exit、export、shift、if、eval、[、for、while 等命令都是内建命令。 虽然内建命令不创建新的进程,但是也会有返回值,通常也用 0 表示调用成功,这个返回值被称为 Exit Status (状态码),可以使用特殊变量 $? 读出。
shell 脚本中用 # 表示注释,相当于 C 语言中的 // 注释,但是#! 却表示该脚本使用 /bin/sh 解释执行,并不代表注释。 如下例是一个简单的 shell 脚本:
#! /bin/sh
cd ..
ls
将上面的代码保存在 .sh 文件中,即为一个 Shell 脚本。 执行脚本只需要输入命令:./script.sh,这是 sh ./script.sh 命令的简写。
执行上面的脚本的步骤为:
shell 提供了与操作系统通信的方式。此通信以交互的方式(来自键盘的输入立即操作)或作为一个 shell 脚本执行。shell 脚本是 shell 和操作系统命令的序列,它存储在文件中。
一般的,Shell 变量由全大写字母加下划线组成,有两种类型的 Shell 变量。
环境变量可以从父进程传给子进程,因此 Shell 进程的环境变量可以从当前的 Shell 进程传给 fork 出来的子进程,使用 printenv 命令可以显示当前 Shell 进程的环境变量。
只存在于当前 Shell 进程的变量,用set命令可以显示当前 Shell 进程中定义的所有变量(包括本地变量和环境变量)和函数。
下面介绍几个变量使用和显示的常用关键字:
显示当前所有变量和函数。
环境变量是任何进程都有的概念,而本地变量是 Shell 所特有的概念,在 Shell 中,环境变量和本地变量的定义和用法相似,在 Shell 中定义或赋值一个变量可以使用下面的格式:
VARNAME=value
等号两边是不能有空格的,否则就会被解释成命令或命令行参数。
任何一个变量定义后都仅存在于当前的 Shell 进程,是本地变量,用 export 命令可以把本地变量导出为环境变量,定义和导出环境变量也可以一步完成。
export VARNAME=value
当然也可以分两步:
VARNAME=value
export VARNAME
使用 unset 命令可以删除已经定义了的环境变量或本地变量。
unset VARNAME
使用 echo 命令可以显示变量的值。
一般对于 VARNAME 变量,我们使用 ${VARNAME} 表示他的值,在不引起歧义的情况下,我们也可以直接使用 VARNAME 表示他的值。
Shell 中的所有变量都是字符串,Shell中的变量也不需要先定义后使用,使用一个没有定义的变量,这个变量的值为空字符串。
Shell 中也有通配符,如下表:
通配符 | 意义 |
---|---|
* | 匹配 0 个或多个任意字符 |
? | 匹配 1 个任意字符 |
[若干字符] | 匹配方括号中的任意字符 |
如我们可以使用 ls ch0[012].doc 命令查找文件,如果当前目录下有 ch00.doc 和 ch02.doc,ls 的参数会直接转换成这两个文件名,而不是一个匹配字符串。
由反引号(键盘上 ESC 键下面的,主键盘区左上角·/~)所引起来的也是一条命令,Shell 会首先执行反引号中的命令,然后将结果代换到原来的位置进行原命令的执行,如下面的命令:
DATE=`date`
echo $DATE
反引号和 $() 是一样的:
DATE=$(date)
echo $DATE
Shell 会将 $(()) 中的 Shell 变量的取值转换成整数用于算术计算(其他情况下 Shell 都将变量视为字符串,无法进行算术计算)
VAR=45
echo $(($VAR+3))
$(()) 中只能进行 +、-、*、/ 和 () 运算,并且只能进行整数运算。
和 C 语言一样,Shell 中也需要转义字符,如 \ 、\$、\、`、\"
在 Shell 中单引号中的所有字符都被认为是普通的字符,所以不需要转义字符,如运行:
echo '$SHELL'
会显示 $SHELL。
echo 'ABC\\'
会显示 ABC\
双引号也将其中的字符串视为字面值,但是反引号、$、转义字符等等都保持原来的意义。 如:
echo "$SHELL"
会显示 /bin/bash。
echo "`date`"
会显示 Sun Apr 20 11:22:06 CEST 2003。
Shell脚本与Windows/Dos下的批处理相似,也就是用各类命令预先放入到一个文件中,方便一次性执行的一个程序文件,主要是方便管理员进行设置或者管理用的。但是它比Windows下的批处理更强大,比用其他编程程序编辑的程序效率更高,因为它具有丰富的语法,可以实现控制、循环、判断等一系列类似编程语言的操作。
命令 test 或 [] 可以测试一个条件是否城里,如果测试结果为真,则该命令的 Exit Status 为0,如果测试结果为假,则命令的 Exit Status 为1(与C语言中正好相反) 由于 [] 中的 [ 实际上是一个命令,他后面的都是这个命令的参数,因此需要用空格隔开。
如下例:
VAR=2
test $VAR -gt 1
echo $?
显示 0。
VAR=2
test $VAR -gt 3
echo $?
显示 1。
test 也可以换成 []:
VAR=2
[ $VAR -gt 3 ]
echo $?
显示 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 ] | 逻辑或 |
如:
VAR=abc
[ -d Desktop -a $VAR = 'abc' ]
echo $?
需要注意的是,如果上例中的 VAR 变量没有被预先定义,那么就会被解释器展开为空字符串,整个命令就变成了:
[ -d Desktop -a = 'abc' ]
因此,解释器会报告相应的错误。
为了避免这样的意外情况发生,一个好的 Shell 编程习惯总是把变量取值放到双引号之中:
VAR=abc
[ -d Desktop -a "$VAR" = 'abc' ]
echo $?
这样,虽然 VAR 没有被预先定义,但是命令还是被展开成了。
[ -d Desktop -a "" = 'abc' ]
和 C 语言类似,在 Shell 中使用 if、then、elif、else、fi 几个命令实现分支控制,例如:
if [ -f ~/.bashrc ]; then
.~/.bashrc
fi
: 是一个特殊的指令,称为“空命令”,该命令不做任何事,但是 Exit Status 总是真,也可以使用 /bin/true 或 /bin/false 获得总是真或假的 Exit Status。
if :; then echo "always true"; fi
与下面的例子是一样的:
if /bin/true; then echo "always true"; fi
我们也可以使用 read 命令等待用户键入一行字符串,存到一个 Shell 变量中。
#! /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
与C语言类似,Shell 也提供 && 与 ||
test "$VAR" -gt 1 -a "$VAR" -lt 3
等价于。
test "$VAR" -gt 1 && test "$VAR" -lt 3
因为 && 操作的短路求值特性,很多 Shell 脚本喜欢写成:
test "$(whoami)" != 'root' && (echo you are using a nonprivileged account; exit 1)
case 命令类似于 C 语言的 switch/case 语句,esac 用来标志 case 语句块的结束。 Shell 中的 case 语句不仅可以用来匹配数字,也可以用来匹配字符串和通配符。
如下例,每个匹配分支都可以有若干条命令,末尾必须以;;结束。
#! /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
Shell 脚本的 for 循环结构和 C 语言很不一样,他类似于某些编程语言的 foreach 循环。 如下面的例子:
#! /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~ 等。
for FILENAME in chap?; do mv $FILENAME $FILENAME~; done
while 的用法和 C 语言非常类似,比如下面是一个验证密码的脚本:
#! /bin/sh
echo "Enter password:"
read TRY
while [ "$TRY" != "secret" ]; do
echo "Sorry, try again"
read TRY
done
我们也可以像 C 语言中那样控制 while 循环的循环次数:
#! /bin/sh
COUNTER=1
while [ "$COUNTER" -lt 10 ]; do
echo "Here we go again"
COUNTER=$(($COUNTER+1))
done
有很多变量是被 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 被称为“位置参数”。
shift 命令可以令位置参数左移,比如 shift 3 表示让 4 变成 1,5 变成 2,原来的 1、2就会被丢弃掉,而
Shell 中的函数定义中没有返回值也没有参数列表。 如下面例子所示:
#! /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。
#! /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
Shell提供了一些用于调试脚本的选项:
有三种方法使用这些选项: 1. 在命令行提供参数 $ sh -x ./script.sh 2. 在脚本开头提供参数 #! /bin/sh -x 3. 在脚本中用 set 命令启用或禁用参数
#! /bin/sh
if [ -z "$1" ]; then
set -x
echo "ERROR: Insufficient Args."
exit 1
set +x
fi
#
# 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