什么是shell脚本
- 提前设计可执行语句,用来完成特定任务的文件
- 解释型程序顺序,批量执行
规范shell脚本的一般组成
-#!环境声明
-#注释文本
-可执行代码
一:编写一个批量处理shell脚本
- 任务需求:依次输出以下系统信息
-红帽系统版本,内核版本,当前的主机名
- 转换成代码
- cat /etc/redhat-release , uname -r ,hostname
[root@server0 ~]# vim /root/sysinfo
#!/bin/bash
cat /etc/redhat-release
uname -r
hostname
添加执行权限。独立运行
[root@server0 ~]# chmod +x /root/sysinfo
[root@server0 ~]# /root/sysinfo
Red Hat Enterprise Linux Server release 7.0 (Maipo)
3.10.0-123.el7.x86_64
server0.example.com
变量
基本概念
- Shell变量的名称与C语言一样,由数字、字母、下划线组成,其中只能以字母或下划线开头
- 变量可以为空值,null
- 赋值时,=两边没有空格
str="abc" a=1
- 1
- 2
- 取变量的值时,在变量前面加$
a=1 echo $a => 1
- 1
- 2
- 变量的值如果含空格,赋值时用双引号括起来
str="Hello World!"
- 1
- 算数展开 $( (…) )
i=5 j=6 echo $((i+j)) => 11 echo $(((i*j)-(i+j))) => 19 echo $((3>1)) => 1 echo $((1>3)) => 0
- 1
- 2
- 3
- 4
- 5
相关命令
export:将shell变量输出为环境变量,或者将shell函数输出为环境变量
- 打印当前环境变量
export => 打印当前进程的所有环境变量 export -p => 同export
- 1
- 2
- 修改当前进程的环境变量
export DAVID="david" => 设置环境变量DAVID,值为david export PATH=$PATH:/home/david => 在PATH后面追加/home/david路径
- 1
- 2
readonly:定义只读shell变量和shell函数
- 打印当前只读变量
readonly => 打印当前进程的所有只读变量 readonly -p => 同readonly
- 1
- 2
- 设置当前进程的只读变量
readonly DAVID="david" => 设置只读变量DAVID,值为david
- 1
env:为将要运行的程序设置环境变量
-i
env -i A=1 B=2 awk 'xxx' file # 用新的环境变量重新初始化环境变量,然后运行命令 # 新环境变量只对将要运行的命令生效
- 1
- 2
-u
env -u xxx awk 'xxx' file # 删除某个环境变量,然后用新的环境变量运行命令 # 新环境变量只对将要运行的命令生效
- 1
- 2
set:更改shell特性
- -x 显示执行的语句
- +x 不显示执行的语句
- unset:删除已经定义的变量或函数
- -f 删除函数
- -v 删除变量
参数展开
- :如果变量前后需要连接其他字符串,则用
- {}展开
str="Hello World!"
echo __$str__ => __
echo __${str}__ => __Hello World!__
- 1
- 2
- 3
- 未定义的变量会展开为null字符串
# 非常危险!
rm -fr /$UnDeclareVar => rm -fr /
- 1
- 2
- 3
- 4
替换运算符
${varname:-word}
实现:如果varname存在且非null,则返回其值;否则返回word
用途:如果变量未定义,则返回一个默认值str1="Hello Shell!" echo ${str1:-Default} => Hello Shell! echo ${str2:-Default} => Default
- 1
- 2
- 3
${varname:=word}
实现:如果varname存在且非null,则返回其值;否则设置varname=word,并返回varname
用途:如果变量未定义,则将变量设为默认值并返回该变量str1="Hello Shell!" echo ${str1:=Default} => Hello Shell! echo ${str2:=Default} => Default echo $str2 => Default
- 1
- 2
- 3
- 4
${varname:?message}
实现:如果varname存在且非null,则返回其值;否则显示varname:message,并退出脚本 如果省略message,显示默认信息"parameter null or not set" 注:交互式Shell下不需要退出,不同Shell有不同行为
- 1
- 2
- 3
- 4
用途:捕捉由于变量未定义而导致的错误
str1="Hello Shell!" echo ${str1:?str1 is not set} => Hello Shell! echo ${str2:?str2 is not set} => line N: str2: str2 is not set echo ${str2:?} => line N: str2: parameter null or not set
- 1
- 2
- 3
- 4
${varname:+word}
实现:如果varname存在且非null,则返回word;否则返回null
用途:测试变量是否存在str1="Hello Shell!" echo ${str1:+1} => 1 echo ${str2:+1} => (null)
- 1
- 2
- 3
-
- 位置参数
- N:某个命令行参数,从1开始算,如果N>9,则用花括号括起来
- {10}
- $#:命令行参数的个数
- ∗,
- @:一次表示所有参数
- “$*”:将所有参数当做一个字符串
- “$@”:将所有参数视为单独的个体
echo "\$1 = ${1:-/dev/null}" ./test.sh => $1 = /dev/null ./test.sh 1 2 3 => $1 = 1 echo "arg num = $#" echo "\$* = $*" echo "\$@ = $@" printf "\"\$*\" = %s\n" "$*" printf "\"\$@\" = %s\n" "$@" ./test.sh 1 2 3 ==> arg num = 3 $* = 1 2 3 $@ = 1 2 3 "$*" = 1 2 3 "$@" = 1 "$@" = 2 "$@" = 3
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
特殊变量
- @:一次表示所有参数
变量 意义 0 Shell程序名称 # 参数个数 @ 命令行参数,如果在”“内,则展开为一系列单独的参数 * 命令行参数,如果在”“内,则把所有参数合并为一个字符串 $ 当前进程ID PPID 父进程ID ? 前一个命令的退出状态 ! 最近一个后台命令的进程编号 HOME 用户根目录 PATH 命令的查找路径 PWD 当前工作目录 LINENO 行号 - 传给Shell的选项 PS1 主要的命令提示字符串;默认为$ PS2 行继续的提示字符串;默认为> PS4 以set -x设置的执行跟踪的提示字符串;默认为+ LANG 当前local默认名称;其他LC_*变量会覆盖其值 LC_ALL 当前local名称;会覆盖LANG和其他LC_*变量 LC_COLLATE 用来排序字符的当前local名称 LC_CTYPE 在模式匹配期间,用来确定字符类别的当前local名称 LC_MESSAGES 输出信息的当前语言的名称 ENV 仅用于交互式Shell;要读取和在启动时要执行的一个文件的完整路径名;XSI必须的变量 IFS 内部的字段分隔器 NLSPATH 在$LC_MESSAGES(XSI)所给定的信息语言里,信息目录的位置 条件
- 退出状态
- 每一条命令,不管是内置的还是外部的,退出时均返回一个整数值
- 0表示成功,非0表示失败,用$?访问
- exit命令:传递一个退出值给调用者;如果没有提供退出值,默认为最后一个命令的退出状态
if语句
if [[ condition ]]; then #statements elif [[ condition ]]; then #statements else #statements fi
- 1
- 2
- 3
- 4
- 5
- 6
- 7
test命令
两种形式
str1="abc" str2="abc" if test $str1 = $str2; then echo "str1 == str2" else echo "str1 != str2" fi # 下面的形式等同于上面的形式 if [[ $str1 = $str2 ]]; then echo "str1 == str2" else echo "str1 != str2" fi
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
运算符
运算符 如果…则为真 -e file file存在 -s file file不为空 -f file file是一般文件 -d file file是目录 -b file file是块设备文件 -c file file是字符设备文件 -p file file是命名管道(FIFO) -S file file是socket -h file file是符号链接 -L file file是符号链接,同-h -r file file是可读的 -w file file是可写入的 -x file file是可执行的,或file是可被查找的目录 -g file file有设置setgid位 -u file file有设置setuid位 string string不是null -n string string非null -z string string是null s1 = s2 字符串s1等于s2 s1 != s2 字符串s1不等于s2 n1 -eq n2 整数n1等于n2 n1 -ne n2 整数n1不等于n2 n1 -lt n2 整数n1小于n2 n1 -gt n2 整数n1大于于n2 n1 -le n2 整数n1小于等于n2 n1 -ge n2 整数n1大于等于n2 -t n 文件描述符n指向以终端 # 在test中,所有变量展开都需要用引号括起来 if[ -f "$file" ] ==> 正确 if[ -f $file ] ==> 如果$file为空,shell的行为无法预料
- 1
- 2
- 3
- 4
- 5
case语句
case word in pattern ) ;; # 执行到;;结束 * ) # 默认动作,相当于C语言中的default ;; esac
- 1
- 2
- 3
- 4
- 5
- 6
循环
for语句
for i in words; do #statements done for i; do #相当于for i in "$@" #statements done for (( i = 0; i < 10; i++ )); do #statements done
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
while与until语句
while [[ condition ]]; do # condition为真时执行 #statements done until [[ condition ]]; do # condition为假时执行 #statements done
- 1
- 2
- 3
- 4
- 5
- 6
- 7
break与continue
- break:跳出循环
- continue 继续执行下一个循环
- break与continue,都可以接受数值参数,控制语句跳出或继续执行多少个循环
while [[ condition ]]; do while [[ condition ]]; do #statements break 2 # 跳出最外面的循环 done done
- 1
- 2
- 3
- 4
- 5
- 6
shift
- 依次处理命令行参数
- 每次执行后,原来的1消失,用
-
- 3的旧值取代…,$#也会依次减1
- 有一个可选参数,指定每一次移动几位,默认为1
file= verbose= quiet= long= while [[ $# -gt 0 ]]; do # 依次处理命令行参数,没处理依次,$#都减1 case $1 in -f ) file=$2 echo "file = $file" shift # 因为-f选项后面要跟文件名,所以还得再shift一遍 ;; -v ) verbose=true quiet=false echo "verbose = $verbose" echo "quiet = $quiet" ;; -q ) quiet=true verbose=false echo "verbose = $verbose" echo "quiet = $quiet" ;; -l ) long=true echo "long = $long" ;; esac shift done
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
getopts
- 简化命令行参数处理
- 第一个参数是所有合法参数的字符串;如果选项字母后面跟冒号,说明该选项后面需要一个参数,且是必填的;如果遇到需要参数的选项,将参数值放置到变量OPTARG中
- OPTIND包含下一个要处理的参数的索引值,初始值为1
- 第二个参数为变量名称,每次getopts时,该变量会被更新,值等于找到的选项字母;遇到不合法的选项时,该变量被置为?
file= verbose= quiet= long= a= while getopts f:vqa:l opt do case $opt in f ) file=$OPTARG echo "file = $file" ;; a ) a=$OPTARG echo "a = $a" ;; v ) verbose=true quiet=false echo "verbose = $verbose" echo "quiet = $quiet" ;; q ) verbose=false quiet=true echo "verbose = $verbose" echo "quiet = $quiet" ;; l ) long=true echo "long = $long" ;; esac done
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
函数
- 函数使用前必须先定义
- 在脚本的起始处定义
在一个独立文件里定义,在脚本中用”.”符号获取
wait_for_user(){ XXX }
- 1
- 2
- 3
return
- 从函数里返回值
- 如果为指定参数,使用默认退出状态
return $? # 严谨的写法
- 1
函数参数
- 在函数中,位置参数代表函数参数;当函数完成时,原来的命令行参数会恢复
涉及到的命令
- export
- readonly
- env
- unset
- exit
- return
- test
- shift
- getopts
1 案例1:Shell脚本的编写及测试
1.1 问题
本例要求两个简单的Shell脚本程序,任务目标如下:
- 编写一个面世问候 /root/helloworld.sh 脚本,执行后显示出一段话“Hello World!!”
- 编写一个能输出系统信息的 /root/sysinfo 脚本,执行后依次输出当前红帽系统的版本信息、当前使用的内核版本、当前系统的主机名
1.2 方案
规范Shell脚本的一般组成:
- #! 环境声明(Sha-Bang)
- # 注释文本
- 可执行代码
1.3 步骤
实现此案例需要按照如下步骤进行。
步骤一:编写helloworld.sh问候脚本
1)编写脚本代码
[root@server0 ~]# vim /root/helloworld.sh
#!/bin/bash
echo "Hello World !!"
2)添加x执行权限
- [root@server0 ~]# chmod +x /root/helloworld.sh
3)运行脚本测试
[root@server0 ~]# /root/helloworld.sh
Hello World !!
步骤二:编写sysinfo系统信息报告脚本
1)编写脚本代码
- [root@server0 ~]# vim /root/sysinfo
- #!/bin/bash
- cat /etc/redhat-release
- uname -r
- hostname
2)添加x执行权限
[root@server0 ~]# chmod +x /root/sysinfo
3)运行脚本测试
- [root@server0 ~]# /root/sysinfo
- Red Hat Enterprise Linux Server release 7.0 (Maipo)
- 3.10.0-123.el7.x86_64
- server0.example.com
2 案例2:重定向输出的应用
2.1 问题
本例要求编写一个脚本 /root/out.sh,功能特性如下:
- 执行此脚本显示 I love study !!
- 执行 /root/out.sh 2> err.log 应该没有显示,但是查看 err.log 文件的内容为 I love study !!
2.2 方案
屏幕输出文本的类别:
- 标准输出(1):命令行执行正常的显示结果
- 标准错误(2):命令行执行出错或异常时的显示结果
将屏幕显示信息保存到文件:
- cmd > file 、 cmd >> file
- cmd 2> file 、 cmd 2>> file
- cmd &> file 、cmd 2> file 1>&2
使用1>&2或>&2操作,可以将命令行的标准输出编程标准错误。
2.3 步骤
实现此案例需要按照如下步骤进行。
步骤:编写out.sh输出测试脚本
1)编写脚本代码
[root@server0 ~]# vim /root/out.sh
#!/bin/bash
echo "I love study !!" >&2
2)添加x执行权限
- [root@server0 ~]# chmod +x /root/out.sh
3)运行脚本测试
[root@server0 ~]# /root/out.sh
I love study !!
[root@server0 ~]# /root/out.sh 2> err.log
[root@server0 ~]# cat err.log
I love study !!
3 案例3:使用特殊变量
3.1 问题
本例要求编写一个脚本 /root/myuseradd,功能特性如下:
1)此脚本可接收2个位置参数,能够按照下列格式执行:
- /root/myuseradd 用户名 密码
2)此脚本执行后,能显示“一共提供了 $# 个参数”,然后在下一行显示“用户名是 $1,密码是 $2 ”,紧跟下一行开始输出对应文件的前几行内容。
3.2 方案
使用位置变量可以取得在执行脚本时提供的命令行参数:
- 表示为 $n,n为序号
- $1、$2、.. .. ${10}、${11}、.. ..
使用预定义变量$#可以统计执行脚本时提供的位置变量个数。
3.3 步骤
实现此案例需要按照如下步骤进行。
步骤一:编写 /root/myuseradd 添加用户的脚本
1)编写脚本代码
[root@server0 ~]# vim /root/myuseradd
#!/bin/bash
echo "一共提供了 $# 个参数"
echo "用户名是 $1,密码是 $2 "
useradd $1
echo "$2" | passwd --stdin $1
2)添加x执行权限
- [root@server0 ~]# chmod +x /root/myuseradd.sh
步骤二:测试 /root/myuseradd 脚本
1)测试添加用户 bob,密码设为 1234567
[root@server0 ~]# /root/myuseradd bob 1234567
一共提供了 2 个参数
用户名是 bob,密码是 1234567
更改用户 bob 的密码 。
passwd:所有的身份验证令牌已经成功更新。
[root@server0 ~]# id bob
uid=1002(bob) gid=1002(bob) 组=1002(bob)
2)测试添加用户 jerry,密码设为 1234567
- [root@server0 ~]# /root/myuseradd jerry 1234567
- 一共提供了 2 个参数
- 用户名是 jerry,密码是 1234567
- 更改用户 jerry 的密码 。
- passwd:所有的身份验证令牌已经成功更新。
- [root@server0 ~]# id jerry
- uid=1003(jerry) gid=1003(jerry) 组=1003(jerry)
4 案例4:编写一个判断脚本
4.1 问题
本例要求在虚拟机 server0 上创建 /root/foo.sh 脚本,任务目标如下:
- 当运行/root/foo.sh redhat,输出为fedora
- 当运行/root/foo.sh fedora,输出为redhat
- 当没有任何参数或者参数不是 redhat 或者 fedora时,其错误输出产生以下信息: /root/foo.sh redhat|fedora
4.2 方案
Shell脚本中执行条件测试的方式:
- 任何一条命令行
- test 测试表达式
- [ 测试表达式 ]
常用的test测试选项:
- 文件状态检测 -f、-d、-e、-r、-w、-x
- 整数值比较 -gt、-ge、-eq、-ne、-lt、-le
- 字符串比较 ==、!=
- 取反操作 !
多分支if选择结构:
if 条件测试操作1;then
命令序列1....
elif 条件测试操作2;then
命令序列2....
else
命令序列3....
fi
4.3 步骤
实现此案例需要按照如下步骤进行。
步骤一:编写foo.sh判断脚本
1)编写脚本代码
- [root@server0 ~]# vim /root/foo.sh
- #!/bin/bash
- if [ $# -eq 0 ];then
- echo "/root/foo.sh redhat|fedora" >&2
- elif [ $1 = "redhat" ];then
- echo "fedora"
- elif [ $1 = "fedora" ];then
- echo "redhat"
- else
- echo "/root/foo.sh redhat|fedora" >&2
- fi
2)添加x执行权限
[root@server0 ~]# chmod +x /root/foo.sh
步骤二:测试foo.sh判断脚本
1)测试提供正确参数的情况
- [root@server0 ~]# /root/foo.sh redhat
- fedora
- [root@server0 ~]# /root/foo.sh fedora
- Redhat
2)测试提供非预期参数的情况
[root@server0 ~]# /root/foo.sh ubuntu
/root/foo.sh redhat|fedora
3)测试不提供参数的情况
- [root@server0 ~]# /root/foo.sh
- /root/foo.sh redhat|fedora
5 案例5:编写一个批量添加用户脚本
5.1 问题
本例要求在虚拟机 server0 上创建 /root/batchusers 脚本,任务目标如下:
- 此脚本要求提供用户名列表文件作为参数
- 如果没有提供参数,此脚本应该给出提示 Usage: /root/batchusers,退出并返回相应值
- 如果提供一个不存在的文件,此脚本应该给出提示 Input file not found,退出并返回相应值
- 新用户的登录Shell为 /bin/false,无需设置密码
- 列表测试文件:http://classroom/pub/materials/userlist
5.2 方案
单分支if选择结构:
if 条件测试操作
then
命令序列....
fi
脚本的退出状态:取决于退出前最后一条命令的 $? 值,或者“exit 整数值”指定。
列表式for循环结构:
- for 变量名 in 值1 值2 值3 .. ..
- do
- 命令序列($变量名)
- done
使用命令替换来获取命令结果:$(命令行)
5.3 步骤
实现此案例需要按照如下步骤进行。
步骤一:编写batchusers批量添加用户脚本
1)编写脚本代码
[root@server0 ~]# vim /root/batchusers
#!/bin/bash
if [ $# -eq 0 ] ; then
echo "Usage: /root/batchusers <userfile>" >&2
exit 1
fi
if [ ! -f $1 ] ; then
echo "Input file not found" >&2
exit 2
fi
for name in $(cat $1)
do
useradd -s /bin/false $name
done
2)添加x执行权限
- [root@server0 ~]# chmod +x /root/batchusers
步骤二:测试batchusers批量添加用户脚本
1)下载用户列表测试文件:
[root@server0 ~]# wget http://classroom/pub/materials/userlist -O /root/userlist
.. ..
2016-11-27 17:23:32 (2.83 MB/s) - ‘/root/userlist’ saved [27/27]
[root@server0 ~]# cat /root/userlist //检查下载文件
duanwu
zhongqiu
zhsan
lisi
2)实现批量添加用户:
- [root@server0 ~]# /root/batchusers /root/userlist
- [root@server0 ~]# id duanwu
- uid=1006(duanwu) gid=1006(duanwu) groups=1006(duanwu)
3)测试其他异常处理:
- [root@server0 ~]# /root/batchusers //未提供列表文件
- Usage: /root/batchusers <userfile>
- [root@server0 ~]# echo $?
- 1
- [root@server0 ~]# /root/batchusers /root/userlist.txt //提供的列表文件找不到
- Input file not found
- [root@server0 ~]# echo $?
- 2