Linux 系统编程学习笔记 - Shell脚本
Shell简介
shell用于解释执行用户的命令。
shell 2种执行方式:
- 用户输入一条,解释执行一条;
- 事先写一个shell脚本,包含多条命令,shell一次执行完毕,这种方式也叫批处理(batch);
shell的多个版本:
- sh(Bourne Shell):Steve Bourne开发,各种UNIX系统配有;
- csh(C Shell):由Bill Joy开发,随BSD UNIX发布,像C,支持Bourne Shell不支持的功能:作业控制、命令历史、命令行编辑;
- ksh(Korn Shell):由David Korn开发,向后兼容sh,添加了csh新加入功能,很多UNIX标准配置的shell,这些系统的/bin/sh通常指向符号链接/bin/ksh;
- tcsh(TENEX C Shell):csh增强版,引入命令补全功能,在FreeBSD、Mac OS X等系统上替代了csh;
- bash(Bourne Again Shell):由GNU开发,目标是与POSIX标准保持一致,同时兼顾对sh的兼容,也从csh和ksh借鉴了很多功能,各种Linux发行版标准配置;
查看已知(但不一定安装)的Shell:文件/etc/shells
$ cat /etc/shells
用户默认Shell设置位于/etc/passwd文件,打开图形终端窗口自动执行/bin/bash的配置就在该文件。
切换到其他Shell,用sh命令
Linux中默认的shell如何切换为其他类型的shell
Shell如何执行命令
执行交互式命令
用户输入命令后,一般情况下Shell会fork并exec该命令,Shell内建命令除外(如cd,alias,umask,exit)。执行内建命令相当于调用Shell进程中的一个函数,并不创建新的进程。
内建命令不穿就新进程,但会有Exit Status(状态码),0标识成功,非0表示失败,可以用特殊变量 $? 读出。
查看内建命令
$ man bash-builtins
cd命令为什么要实现成内建命令?而不用独立的程序?
因为Shell独立程序是通过fork新建子进程,然后让子进程exec转去执行命令对应的独立程序,但是子进程当前目录路径的改变并不会影响父进程,子进程结束又回到父进程shell环境了,这样无法实现当前shell路径的改变。
同样的,改变当前shell的参数和环境变量表内的都需要使用内建命令。
参考cd命令为何要实现成shell内建命令
执行脚本
编写脚本script.sh:
#! /bin/sh
cd ..
ls
# 表示注释
#! (Shebang)位于文件开头第一行,则表示该脚本使用后面指定的解释器/bin/sh解释执行
如果把该脚本加上可执行权限,然后执行
$ chmod + x script.sh
$ ./script.sh
Shell会fork一个子进程,并调用exec执行./script.sh脚本程序,exec系统调用把子进程的代码替换成./script.sh程序代码段,并从_start开始执行。
如果要执行的是一个文本文件,并且第一行用Shebang指定了解释器,则用解释器程序代码段替换当前进程,并且从解释器的_start开始执行,而该文本文件被当做命令行参数传给解释器。
执行./script.sh <=>
$ /bin/sh ./script.sh
以这种方式执行,是不需要script.sh文件具有可执行权限(x)的。
假如script以#! /bin/sed -f
开头,执行脚本相当于执行
$ /bin/sed -f ./script.sh
下面这两种执行shell脚本方法是等价的:
$ ./script.sh
$ sh ./script.sh
执行script脚本步骤:
- 交互Shell(bash)fork/exec一个子Shell(sh)用于脚本执行,父进程bash等待子进程sh终止;
- sh读取脚本中的cd ..命令,调用相应的函数执行内建命令,改变当前工作目录为上一级目录;
- sh读取脚本中的ls命令,for/exec该程序,列出当前工作目录下的文件,sh等待ls终止;
- ls终止后,sh继续执行,读到脚本文件末尾,sh终止;
- sh终止后,bash继续执行,打印提示符等待用户输入;
如果将命令行下输入的命令用过() 括起来和不括起来是不一样的,括起来的会fork出一个Shell执行括号中的命令。一行中可以输入由分号 ";"隔开的多个命令,比如
$ (cd ..;ls -l) # shell命令,等同于script脚本执行效果
$ cd ..;ls -l # shell命令,不同于上面的命令,因为是直接在交互式shell下执行的,会改变shell的pwd(当前目录)
# 等价于这样执行Shell脚本
$ source ./script.sh # source 是shell内建命令,不会创建子shell,而是直接在交互式shell下逐行执行脚本命令
# or
$ . ./script.sh
示例:
$ (exit 2) # fork子shell执行exit命令,返回状态码2
$ echo $? # 在当前shell获取上一次运行命令的结束代码
Shell的基本语法
变量
惯例:Shell变量由全大写字母 + 下划线组成。
两种类型Shell变量:
-
环境变量
环境变量可以从父进程传递给子进程,Shell进程的环境变量可以传递fork的子进程。printenv命令可以显示当前Shell进程的环境变量。 -
本地变量
只存在于当前Shell进程,set命令可以显示当前Shell进程中定义的所有变量(本地变量和环境变量)和函数。
环境变量是任何进程都有的概念,本地变量是Shell特有的概念。
Shell中,定义或赋值一个变量
$ VARNAME=value # 注意等号两边都不能有空格,否则会被Shell解释成命令和命令行参数
一个变量定义后,仅存在于当前Shell进程,是本地变量,用export命令可以把本地变量导出为环境变量,定义和导出环境变量通常可以一步完成:
$ export VARNAME=value # 定义本地变量,并导出为环境变量
# <=>
$ VARNAME=value
$ export VARNAME
unset命令删除变量
$ unset VARNAME
\({VARNAME}读取变量值,没有歧义时可以简化成\)VARNAME
# 注意它们的区别
$ echo $SHELL # 变量名SHELL
$ echo $SHELLabc # 变量名SHELLabc
$ echo $SHELL abc # 变量名SHELL,abc是参数
$ echo ${SHELL}abc # 变量名SHELLabc
注意:Shell变量的值都是字符串,无需专门指定类型;Shell变量不需要先定义,后使用,对于未定义变量取值为空字符串。
文件名替换(Globbing):*>[]
用于匹配的字符称为通配符(Wildcard)
* | 匹配0个或多个任意字符 |
? | 匹配一个任意字符 |
[若干字符] | 匹配方括号中任意一个字符的一次出现 |
$ ls /dev/ttyS* # ls参数:当前目录下以/dev/ttyS为前缀的文件名
$ ls ch0?.doc # ls参数:ch0+一个字符的doc文件名
$ ls ch0[0-2].doc # ls参数:ch0 + 0/1/2的doc文件名
$ ls ch[012][0-9].doc # ls参数:ch + 012任一字符出现一次 + 0~9任一字符出现一次的doc文件名
命令代换:`或$()
`或$()括起来的也是一条命令,shell先执行该命令,然后将输出结果立刻代换到当前命令行中。
如定义一个存放date命令的输出
# 命令代换 用` `表示
$ DATE=`date` # `date` 命令输出结果,代换到当前命令中
$ echo $DATE
#<=> 用$()表示
$ DATE=$(date)
算术代换:$(())
用于算术计算,\((())中Shell变量取值将转换为整数。\)(())只能用+-*/和()运算符,而且只能做整数运算
$ VAR=45
$ echo $(($VAR+3))
转义字符\
\ Shell中用作转义字符,用于去紧跟后面的单个字符的特殊意义(回车除外)。简单来说,就是把后面紧跟字符当做字面量。
$ echo $SHELL # 打印SHELL变量值
/bin/bash
$ echo \$SHELL # 打印字面量 $SHELL
\$SHELL
$ echo \\ # 打印字面量 \
\
# 创建名为$ $的文件
$ touch \$\ \$
特殊符号 -,即使加上\转义还是报错,会被当做命令行参数选项,不会当做文件名。解决办法:
$ touch \-hello # 会报错
# 正确的创建名为-hello的2种方法
$ touch ./-hello # 创建名为-hello的文件
# <=>
$ touch -- -hello
\ + 回车,表示续行
$ ls \ # 续行命令
>-l # > 是shell给出的续行提示符
# <=> $ ls -l
单引号
和C不同,shell中单引号、双引号都是字符串的界定符,而不是字符的界定符。
注意:
- 单引号(' ')不同于算术代换(
- 字符串中,不能出现单引号;
- 引号需要配对,如果不配对,shell会给出续行提示符(>);
$ echo '$SHELL'
$SHELL # 打印结果
$ echo 'ABC\回车
> DE'回车
ABC\
DE
双引号
双引号用于保持引号内所有字符的字面值(包括回车),但以下几种情况除外:
- 反引号仍表示命令替换
- $表示$的字面值
- ' 表示'的字母值
- " 表示"的字面值
- \ 表示\的字面值
- 除以上情况外,其他字符前面的\无特殊含义,只表示字面值
$ echo "$SHELL"
/bin/bash
$ echo "`date`"
$ echo "I'd say: \"Go for it\""
I'd say: "Go for it"
$ echo "\"回车
>"回车
"
$ echo "\\"
\
bash 启动脚本
启动脚本是bash启动时自动执行的脚本。
启动脚本有什么用?
用户可以把一些环境变量的设置和alias、umask设置放在启动脚本中,这样每次启动Shell时这些设置自动生效。
启动bash的方法不同,执行启动脚本的步骤也不同:
作为交互登录Shell启动,或者使用--login参数启动
交互Shell是指用户在提示符下输命令的Shell,而非执行脚本的Shell,登录Shell是在输入用户名和密码登录后得到的Shell,如从字符终端等了或者用telnet/ssh从远程登录,但是从图形界面的窗口管理器登录之后会显示桌面而不会产生登录Shell(也不会执行启动脚本),在图形界面下打开终端窗口得到的Shell也不是登录Shell。
启动bash时如何自动执行脚本?
- 先执行/etc/profile,系统中每个用户登录时都要执行该脚本,如果系统管理员系统设置对所有用户都生效,可以写在这个脚本里;
- 然后依次查找当前用户主目录的~/.bash_profile, /.bash_login和/.profile三个文件,找到第一个存在并且可读的文件来执行。
如果希望某个设置只对当前用户生效,可以写在这个脚本里,由于这个脚本中/tec/profile之后执行,/etc/profile设置的环境变量的值在这个脚本里可以修改,覆盖系统中的全局设置。
/.profile启动脚本是sh规定的,bash规定首先查找以/.bash开头的启动脚本,如果没有执行~/.profile,是为了和sh保持一致。 - 退出登录时,执行~/.bash_logout脚本(如果存在)。
以交互非登录Shell启动
图形界面下开一个终端窗口,或者在登录Shell提示符下在输入bash命令,得到一个交互式非登录的Shell。这种Shell在启动时自动执行~/.bashrc脚本。
为使登录Shell也能自动执行/.bashrc,通常在/.bash_profile中调用~/.bashrc
# 如果~/.bashrc文件存在则source它
if [ -f ~/.bashrc ]; then
. ~/.bashrc
fi
多数Linux发行版在创建账户时会自动创建/.bash_profile和/.bashrc脚本,~/.bash_profile中通常有上面这几行
如果要在启动脚本中做某些 ,使它在图形终端窗口和字符终端的Shell中都起作用,最好就是在~/.bashrc中设置。
实验,在~/.bashrc文件末尾添加一行:
export PATH=$PATH:/home/akaedu
然后关掉终端串口重新打开,或者从字符终端logout之后重新登录,主目录下面的程序就可以直接输入程序名,而不必输入路径了,如:
$ a.out
# 不必带路径
$ ./a.out
为什么要区分登录Shell和非登录Shell?
因为最初的设计是这样考虑的:如果从字符终端或远程登录,那么等你Shell是该用户的所有其他进程的父进程,也是其他子Shell的父进程,所以环境变量值登录Shell的启动脚本里设置一次就可以自动带到其他非登录Shell里,而本地变量、函数、alias等设置没有办法带到子Shell里,需要每次启动非登录Shell时再设置一遍,所以就需要有非登录Shell的启动脚本。
一遍建议:
~/.bash_profile 里设置环境变量;
~/.bashrc设置本地变量、函数、alias等;
如果Linux带有图形系统,则环境变量应该准~/.bashrc里设置,因为图形界面的窗口管理器登录不会产生登录Shell。
非交互启动
为执行脚本fork出来的子Shell是非交互Shell,启动时执行的脚本文件由环境变量BASH_EN定义,相当于自动执行命令:
if [ -n "$BASH_ENV" ]; then . "$BASH_ENV"; fi`
如果环境变量BASH_EN的值不是空字符串,则把它的值当作启动脚本的文件名,source这个脚本。
以sh命令启动
如果以sh命令启动bash,bas将模拟sh的行为,以~/.bash_开头的那些启动脚本就不认了。所以,如果作为交互登录Shell启动,或者使用--login参数启动,则一次执行下面的脚本:
- /etc/profile
- ~/.profile
如果作为交互Shell启动,相当于自动执行:
if [ -n "$ENV" ]; then . "$ENV"; fi`
如果作为非交互Shell启动,则不执行任何启动脚本。通常以'#! /bin/sh' 开头的Shell脚本都属于这种方式
Shell脚本语法
条件测试 test [
命令test或[ 可以测试一个条件是否成立,如果测试结果为真,则该命令的Exit Status = 0;如果为假,则Exit Status = 1(注意不是C true/false)。
例如,测试两个数的大小关系:
$ VAR=2
$ test $VAR -gt 1 # 测试 $VAR > 1
$ echo $?
0
$ test $VAR -gt 3 # 测试 $VAR > 3
$ echo $?
1
$ [ $VAR -gt 3] # 测试 $VAR > 3 # $VAR, -gt, 3, ]是[命令的4个参数
$ echo $?
1
test命令和[ 命令的区别是:test命令不需要] 参数。以[命令为例,常见测试命令:
-gt 表示进行 > (大于测试)。其他参数的含义见下表:
例子
$ VAR=abc
$ [ -d Desktop -a $VAR = 'abc' ] # 测试:如果目录Desktop存在,而且变量VAR='abc'
$ echo $?
0
如果$VAR没有事先定义,则展开为空串,会造成测试条件语法错误
验证测试:
$ unset VAR
$ [ -d Desktop -a $VAR = 'abc' ] # 由于已经取消变量VAR,shell展开为空串,[ 测试语法错误
bash: [: too many arguments
$ [ -d Desktop -a "$VAR" = 'abc' ] # 将变量写作" "内,即使是空串,也不会报错
$ echo $?
1
建议:应该总是变量取值放在双引号之中,避免展开为空串导致错误
if/then/elif/else/fi
分支控制。
例
if [ -f ~/.bashsrc ]; then
. ~/.bashsrc
fi
其实是3条命令:
第一条,if [ -f ~/.bashsrc ]
第二条,then .~/.bashsrc
第三条,fi
两条命令写在同一行需要用; (分号)隔开。then后有换行,但命令没有写完,Shell自动续行。
if命令的参数组成一条子命令,如果Exit Status = 0(真),则执行then后面子命令;如果 Exit Status ≠ 0(假),则执行elif、else、fi后面的命令。
fi 表示if语句块的结束
例,
#! /bin/sh
if [ -f /bin/bash ]
then echo "/bin/bash is a file"
else echo "/bin/bash is NOT a file"
fi
if :; then echo "always true"; fi
: 是特殊的命令,称空命令,不做任何事,但Exit Status = 0(真)。也可以执行/bin/true or /bin/false分别代表真 or 假。
#! /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
read 命令等待用户输入一个字符串,存入Shell变量
*&&和||语法
&& 相当于 "if...then...", || 相当于"if not...then..."。
注意区别:-a和-o仅用于测试表达式连接2个测试条件,-a中测试条件中表示逻辑and,-o表示逻辑or
在测试语句中,它们是等价的,如
test "$VAR" -gt 1 - a "$AVR" -lt 3
<=>
test "$VAR" -gt 1 && test "$VAR" -lt 3
case/esac
case命令可类比C的switch/case,esac表示case语句块的结束。C语言case只能匹配整型或字符型常量表达式,Shell脚本的case可以匹配字符串和Wildcard。每个匹配分支可以有若干条命令,末尾必须以 ;; (2个分号)结束。执行时,直接跳到esac之后,不需要像C一样用break跳出。
#! /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
使用case语句的例子,可以在系统服务的脚本目录/etc/init.d中找到。
启动apache2服务命令:
$ sudo /etc/init.d/apache2 start
$1 是一个特殊变量,在执行脚本时自动取值为第一个命令行参数,也就是start。同理,命令行参数指定stop, reload or restart也可以进入其他分支。
for/do/done
Shell 脚本循环结构不同于C,类似于C++的foreach。
如
#! /bin/sh
for FRUIT in apple banna pear; do # FRUIT在in后面的列表中循环取值
echo "I like $FRUIT"
done
示例,将指定目录chap0/1/2/...下的所有文件,其名称后面加上~(表示临时文件)
$ for FILENAME in chap?; do mv $FILENAME $FILENAME~; done
# <=>
$ for FILENAME in `ls charp?`; do mv $FILENAME $FILENAME~; done
while/do/done
while用法类似C。
例,验证密码的脚本
#! /bin/sh
echo "Enter password:"
read TRY
while [ "$TRY" != "scret" ]; do
echo "Soryy, try again"
read TRY
done
通过算术运算控制循环次数
#! /bin/sh
COUNTER=1
while [ "$COUNTER" -lt 10 ]; do
echo "Here we go again"
COUNTER=$(($COUNTER+!))
done
位置参数和特殊变量
Shell自动赋值的特殊变量
位置参数可以用shift命令左移。比如shift 3表示原来的$4变成$1,原来的$5现在变成$2等,原来的$1, $2, $3丢弃,$0不移动。不带参数的shift命令相当于shift 1。
#! /bin/sh
echo "The program $0 is now running"
echo "The first paramter is $1"
ehco "The second parameter is $2"
echo "The parameter list is $@"
shift # <=> shift 1
echo "The first parameter is $1"
echo "The second parameter is $2"
echo "The parameter list is $@"
函数
Shell也有函数的概念,但是函数定义中没有返回值也没有参数列表。
#! /bin/sh
foo() { echo "Function foo is called"; }
echo "-=start=-"
foo
echo "-=end=-"
注意函数体左花括号{ 和后面的命令之间必须有空格或换行,如果将最后一条命令和右花括号写在同一行,命令末尾必须有; (分号)
定义函数foo() 并不执行,只有在调用的时候才执行。
Shell 函数没有参数列表,但是可以传参,函数体用位置参数$0, $1, $2等来提取传入参数。函数中的位置参数是函数局部变量,不会影响函数外面的$0, $1, $2等变量。函数可以用return命令返回,后面如果带数字表示函数的Exit Status。
例,脚本一次创建多个目录,各目录名通过命令行参数传入,脚本逐个测试各目录是是否存在,如果目录不存在,首先打印信息然后试着创建该目录。
#! /bin/sh
# true:返回0;false:返回1
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脚本的调试方法
Shell提供一些用于调试脚本的选项,如:
-n 读一遍脚本中的命令但不执行,用于检查脚本中的语法错误
-v 一边执行脚本,一边将执行过的脚本命令打印到标准错误输出
-x 提供跟踪执行信息,将执行的每一条命令和结果依次打印出来
使用这些选项的3种方法:
- 命令行提供参数
$ sh -x ./script.sh
- 脚本开头提供参数
#! /bin/sh -x
- 脚本中用set命令启用或禁用参数
#! /bin/sh
if [ z "$1" ]; then
set -x # 启用 -x参数
echo "ERROR: Insufficient Args."
exit 1
set +x # 禁用 -x参数
fi