Shell编程
脚本
shell脚本工作原理:
- loader程序加载脚本,发现不是elf可执行文件,返回错误
- bash收到错误,然后打开文件头部,发现是脚本
- 调用脚本执行
几乎所有编程语言的教程都是从使用著名的“Hello World”开始的,我们的第一个 Shell 脚本也输出“Hello World”。
使用vim创建一个新的文本文件,并输入
#!/bin/sh
echo "Hello World !" #这是一条语句
第 1 行的#!是一个约定的标记,它告诉系统这个脚本需要什么解释器来执行,即使用哪一种 Shell;后面的/bin/sh就是指明了解释器的具体位置。然后将文件修改为可执行文件 chmod 777 filename
可以为所有用户赋予读写执行的权限,如果有其他需求修改即可。
shell的注释以#开头,#后面的内容即为注释
脚本调试
在编写较长并且复杂的脚本文件容易出现错误,且脚本文件是非编译程序,不配置的话是不会有编译器差错的。因为只能靠运行程序并尝试解释屏幕显示错误。
set命令选项
选项 | 功能 |
---|---|
-n | 读取命令但不执行,用来检查脚本中的语法错误 |
-v | 输出参数替换前的命令 |
-x | 输出的是带参数展开后的命令 |
如果想要关闭上述命令,将-换为+即可。
变量
关于变量赋值已经在八中讲过了,现在我们来学习一下怎么进行变量替换操作,既只有某种条件发生时才进行替换,这里举出几个最常用的:
替换操作 | 功能 |
---|---|
${value:-word} | 如果变量不存在(或为空),返回word,否则返回变量的值 |
${value:=word} | 如果变量不存在(或为空),设置VAR变量为word,返回word |
${value:?word} | 如果变量不存在(或为空),打印message,退出,但交互shell不会退出 |
${value:+word} | 如果变量存在,返回word;否则返回null |
删除变量则使用unset VARNAME即可
命令行参数
位置参数
运行 Shell 脚本文件时我们可以给它传递一些参数,这些参数在脚本文件内部可以使用$n的形式来接收,例如,$1 表示第一个参数,$2 表示第二个参数,依次类推,最多可以有10个命令行参数,如果参数个数太多,达到或者超过了 10 个,那么就得用${n}的形式来接收了,例如 ${10}、${23}。{ }的作用是为了帮助解释器识别参数的边界,这跟使用变量时加{ }是一样的效果。
同样,在调用函数时也可以传递参数。Shell 函数参数的传递和其它编程语言不同,没有所谓的形参和实参, 在定义函数时也不用指明参数的名字和数目。 换句话说,定义 Shell 函数时不能带参数,但是在调用函数时却可以传递参数,这些传递进来的参数,在函数内部就也使用$n的形式接收,例如,$1 表示第一个参数,$2 表示第二个参数,依次类推。
这种通过$n的形式来接收的参数,在 Shell 中称为位置参数,除了 $n,Shell 中还有 $#、$*、$@、$?、$$ 几个特殊参数。
给脚本文件传递位置参数
#!/bin/sh
echo "Language: $1"
echo "URL: $2"
运行并附带参数:
给函数传递位置参数
#!/bin/sh
#定义函数
function func(){
echo "Language: $1"
echo "URL: $2"
}
#调用函数
func shell https://github.com

特殊参数
变量 | 含义 |
---|---|
$0 | 当前脚本的文件名 |
$n(n≥1) | 传递给脚本或函数的参数。n 是一个数字,表示第几个参数 |
$# | 传递给脚本或函数的参数个数 |
$* | 传递给脚本或函数的所有参数,将所有参数使用$IFS隔开,形成一个字符串 |
$@ | 传递给脚本或函数的所有参数。当被双引号” “包含时,$@ 与 $* 稍有不同 |
$? | 上个命令的退出状态,或函数的返回值 |
$$ | 表示当前进程id |
$* 和 $@ 都表示传递给函数或脚本的所有参数,这里重点区分一下当,$*和 $@ 不被双引号” “包围时,它们之间没有任何区别,都是将接收到的每个参数看做一份数据,彼此之间以空格来分隔。
但是当它们被双引号” “包含时,就会有区别了:
"$*"
会将所有的参数从整体上看做一份数据,而不是把每个参数都看做一份数据。"$@"
仍然将每个参数都看作一份数据,彼此之间是独立的。
比如传递了 5 个参数,那么对于”$*”来说,这 5 个参数会合并到一起形成一份数据,它们之间是无法分割的;而对于”$@”来说,这 5 个参数是相互独立的,它们是 5 份数据。
如果使用 echo 直接输出”$*”和”$@”做对比,是看不出区别的;但如果使用 for 循环来逐个输出数据,立即就能看出区别来,这里先提前使用for循环,如果不了解的话请在学习后再来回顾。
#!/bin/sh
echo "print each param from \"\$*\""
for var in "$*"
do
echo "$var"
done
echo "print each param from \"\$@\""
for var in "$@"
do
echo "$var"
done
$? 获取上一个命令的退出状态,所谓退出状态,就是上一个命令执行后的返回结果。退出状态是一个数字,一般情况下,大部分命令执行成功会返回 0,失败返回 1,这和C语言的 main() 函数是类似的,除了用于获取上一个命令退出状态外,还可以用来获取函数的返回值。这里仅讲获取函数的返回值,前一种情况类比即可。
#!/bin/bash
#得到两个数相加的和
function add(){
return $(expr $1 + $2)
}
add 23 50 #调用函数
echo $? #获取函数返回值
算术表达式
Shell中变量都是字符串,计算算术的时候使用$(())作为算术表达式并获取值(注意,双小括号运算仅在Bash shell中运行),否则将视作两个字符串操作。
echo $((3+2))
除了算术运算外还可以进行逻辑运算,规则与其他编程语言相同,不再累述了(值得注意的是对于字符串而言=也可以表示等于)。
逻辑连接
使用&&和||连接两条命令,&&表示and,||表示or,相似的,在shell中也有逻辑短路。
- Command1 && command2,当command1正确执行,才会执行command2
- Command1 || command2,当command1执行错误,才会执行command2
eval
eval命令将会首先扫描命令行进行所有的替换,然后再执行命令。该命令使用于那些一次扫描无法实现其功能的变量。该命令对变量进行两次扫描。这些需要进行两次扫描的变量有时候被称为复杂变量,eval告诉shell再跑一遍命令解释流程。
eval echo \$$# 取得最后一个参数
cat last
eval echo \$$#
./last one two three four
four
第一遍扫描后,shell把反斜杠去掉了。当shell再次扫描该行时,它替换了$4的值,并执行echo命令。
shift
位置参数可以用shift命令左移。比如shift 3表示原来的$4现在变成$1,原来的$5现在变成$2等等,原来的$1、$2、$3丢弃,$0不移动。不带参数的shift命令相当于shift 1。
我们知道,对于位置变量或命令行参数,其个数必须是确定的,或者当 Shell 程序不知道其个数时,可以把所有参数一起赋值给变量$*。若用户要求 Shell 在不知道位置变量个数的情况下,还能逐个的把参数一一处理,也就是在 $1 后为 $2,在 $2 后面为 $3 等。在 shift 命令执行前变量 $1 的值在 shift 命令执行后就不可用了。
until [ $# -eq 0 ]
do
echo "第一个参数为: $1 参数个数为: $#"
shift
done
执行以上程序x_shift.sh:
./x_shift.sh 1 2 3 4
结果显示如下:
第一个参数为: 1 参数个数为: 4
第一个参数为: 2 参数个数为: 3
第一个参数为: 3 参数个数为: 2
第一个参数为: 4 参数个数为: 1
条件分支
语法格式为:
if 条件; then
command lines
[elif 条件
command lines]
[else
command lines]
fi
条件是一段命令,测试的是$?是否是0,为0满足条件,条件外面的括号是必须的,且条件必须用空格包围起来。
测试条件
测试字符串表达式
[]和test是等价的
逻辑运算符有:
- -a与运算符
- -o或运算符
- -!非运算符
操作符 | 示例 | 说明 |
---|---|---|
= | “$STR1” = “STR2” | 两个字符串是否相同 |
!= | “$STR1” != “STR2” | 两个字符串是否不同 |
-n | -n “$STR” | 字符串不是null |
-z | -z “$STR” | 字符串是null |
在引用变量做字符串测试时,一定要加引号(保证正确的检验),操作符两侧必须有空格,[ … ]中括号必须有空格
文件测试表达式
操作符 | 示例 | 说明 |
---|---|---|
-r | -r filename | 文件是否可读 |
-w | -w filename | 文件是否可写 |
-s | -s filename | 文件是否存在并且长度非0 |
-f | -f filename | 文件是否为普通文件 |
-d | -d filename | 文件是否为目录文件 |
例子:[ -e /var/log/syslog ] 判断是否存在
算术表达式测试
利用$(())计算算算术表达式
即[ $((2+3)) = 5 ]
例
#!/bin/sh
printf "Are you ok?\n"
printf "Input y for yes and no for no: "
read ANSWER
if [ "$ANSWER" = y ]; then
printf "Glad to hear that!\n"
else
printf "Go home!\n"
fi
case语句
case $1 in
-f)
… ;;
-d | --directory)
… ;;
*)
… ;;
esac
case、in 和 esac 都是 Shell 关键字,;;和*)就相当于其它编程语言中的 break 和 default。
例子
#! /bin/sh
printf "Are you ok?\n"
printf "Input y for yes and no for no: "
read ANSWER
case $ANSWER in
[Yy]) printf "Glad to hear that!\n";;
[Nn]) printf "Go home!\n";;
*) printf "No such option\n";;
esac
case in 的 pattern 部分支持简单的正则表达式,具体来说,可以使用以下几种格式:
格式 | 说明 |
---|---|
* | 表示任意字符串。 |
[abc] | 表示 a、b、c 三个字符中的任意一个。比如,[15ZH] 表示 1、5、Z、H 四个字符中的任意一个 |
[m-n] | 表示从 m 到 n 的任意一个字符。比如,[0-9] 表示任意一个数字,[0-9a-zA-Z] 表示字母或数字 |
| | 表示多重选择,类似逻辑运算中的或运算。比如,abc |
循环
for循环
for循环有C风格的循环也有Python风格的for in循环,这里介绍后者。
for variable in list-of-values; do
…
done
list-of-values用$IFS隔开,缺省是空格、tab、回车,例子如下
#! /bin/sh
for count in 1 2 3; do
echo “In the loop for $count times”
done
while循环
while循环格式如:
while [ condition ]; do
…
done
[ condition ]等价于前述的test命令,例子如下
#! /bin/sh
count=1
while [ $((count < 10)) = “1” ]; do
echo $count
count=$((count+1))
done
until循环
Until循环的格式如下,Until表示条件为假时,执行循环体
until [ condition ]; do
…
done
例子如下:
#! /bin/sh
count=1
until [ $((count < 10)) = “0” ]; do
echo $count
count=$((count+1))
done
函数
函数已经在前面中出现过了,函数引用类似于普通命令,但是函数执行类似于内置命令,不会新fork一个shell。
Shell 函数定义的语法格式如下:
function name() {
statements
[return value]
}
调用 Shell 函数时可以给它传递参数,也可以不传递。如果不传递参数,直接给出函数名字即可,如果传递参数,那么多个参数之间以空格分隔,不管是哪种形式,函数名字后面都不需要带括号,和其它编程语言不同的是,Shell 函数在定义时不能指明参数,但是在调用时却可以传递参数,并且给它传递什么参数它就接收什么参数。
Shell 也不限制定义和调用的顺序,你可以将定义放在调用的前面,也可以反过来,将定义放在调用的后面。