Shell脚本编程

版权声明:本文为博主原创文章,转载请注明出处。 https://blog.csdn.net/gongxifacai_believe/article/details/83539872

1、Shell脚本是什么?

Shell脚本语言为过程式语言,解释运行,依赖于外部程序文件来运行。Shell脚本是命令的堆积,但很多命令不具有幂等性,需要用程序逻辑来判断运行条件是否满足,以避免其运行中发生错误。但并不是所有命令执行失败,都会导致脚本运行终止,命令执行失败,如果产生严重错误,该严重错误指,脚本会exit,任何时候shell脚本或shell解释器遇到exit命令就会终止,或我们使用某种判断机制将其强行终止,或者脚本运行过程中出现语法错误,也可能终止。 编译过程:高级语言–>编译器–>目标代码,如C、C++、Java。 解释过程:高级语言–>解释器–>机器代码,如shell、perl、python。

2、Shell脚本编辑器

文本编辑器:nano 行编辑器:sed (一次只处理一行,不会占用整个屏幕) 全屏幕编辑器:nano, vi, vim (打开后全屏幕都是编辑器)

[root@localhost exercise]# nano fstab
其中,^o表示保存,^x表示退出,^为Ctrl键;

3、如何编写shell脚本?

脚本文件的第一行顶格,给出shebang,即解释器路径,用于指明解释执行当前脚本的解释器程序文件。常见的解释器如下:

#!/bin/bash
#!/usr/bin/python
#!/usr/bin/perl

4、运行脚本

(1)赋予执行权限,并直接运行此程序文件。

chmod +x /PATH/TO/SCRIPT_FILE
/PATH/TO/SCRIPT_FILE

(2)直接运行解释器,将脚本以命令行参数传递给解释器程序,脚本不需要执行权限。

bash /PATH/TO/SCRIPT_FILE

脚本中的空白行会被解释器忽略。脚本中,除了shebang,余下所有以#开头的行都会被视作注释行而被忽略,shell脚本的运行是通过运行一个子shell进程实现的。

5、一个简单的Shell脚本示例

写一个脚本,实现如下功能: 1)显示/etc目录下所有以大写p或小写p开头的文件或目录本身; 2)显示/var目录下的所有文件或目录本身,并将显示结果中的小写字母转换为大写后显示; 3)创建临时文件/tmp/myfile.XXXX。

#!/bin/bash
echo "Show some directories start with p or P in /etc:"
ls -d /etc/{p*,P*}       或者 ls -d /etc/[pP]*
echo                     显示一个空白行         
echo -e "\n"             显示两个空白行
									
echo "Translate lower charactor to upper charactor:"
ls -d /var/* | tr 'a-z' 'A-Z'
echo
									
echo "Create a temp file:"
mktemp /tmp/myfile.XXXX

6、算术运算

(1)算术运算符:+,-,*,/, **(次方), %(取模) (2)算术运算格式: 1)let VAR=算术运算表达式 2)VAR=$ [算术运算表达式] (可以直接echo引用,不用变量保存) 3)VAR=$ ((算术运算表达式)) (可以直接echo引用,不用变量保存) 4)VAR=$(expr $ARG1 $OP $ARG2)(可以直接echo引用,不用变量保存) 注意:乘法符号在有些场景中需要使用转义符。 (3)bash中默认都为字符串格式,要进行整型运算时,需要显示声明为整型:

[root@localhost ~]# num1=2
[root@localhost ~]# num2=9
[root@localhost ~]# echo "$num1+$num2"
2+9
[root@localhost ~]# declare -i num3=5     
[root@localhost ~]# declare -i num4=11
[root@localhost ~]# echo "$num3+$num4"
5+11                                            即使声明为整型,仍为字符串输出
[root@localhost ~]# let sum=$num1+$num2
[root@localhost ~]# echo $sum
11
[root@localhost ~]# echo "$[$num3+$num4]"
16				
[root@localhost ~]# echo "$(($num2+$num3))"
14
[root@localhost ~]# expr $num2 + $num4          三个参数彼此之间要有空格
20
[root@localhost ~]# expr $num2+$num4            参数之间没有空格
9+11
[root@localhost ~]# sum=$(expr $num2 + $num4)   变量保存
[root@localhost ~]# echo $sum
20
写一个脚本,完成如下功能:
添加三个用户;
求此三个用户的UID之和;

Answer: 
#!/bin/bash
id $user1 || useradd user1 &> /dev/null
id $user2 || useradd user2 &> /dev/null
id $user3 || useradd user3 &> /dev/null
num1=$(id -u user1)
num2=$(id -u user2)
num3=$(id -u user3)
sum=$(($num1+$num2+$num3))
echo "num1="$num1",num2="$num2",num3="$num3",sum="$sum

Result:
[root@localhost exercise]# bash testshell.sh
uid=0(root) gid=0(root) groups=0(root) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
uid=0(root) gid=0(root) groups=0(root) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
uid=0(root) gid=0(root) groups=0(root) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
num1=1001,num2=1002,num3=1003,sum=3006

(4)增强型赋值 变量做某种算术运算后回存至此变量中; let i=$i+# let i+=# 相当于i=$[$i+1] +=,-=,*=,/=,%=,**=:需要使用let描述; 自增:

VAR=$[$VAR+1]
let  VAR+=1
let  VAR++

自减:

VAR=$[$VAR-1]
let  VAR-=1
let  VAR--

7、条件测试

判断某需求是否满足,需要由测试机制来实现。 如何编写测试表达式以实现所需的测试: (1)执行命令,并利用命令状态返回值来判断,返回值为0表示成功,返回值为1-255表示失败。 (2)测试表达式 test EXPRESSION 测试命令 [ EXPRESSION ] 测试命令 [[ EXPRESSION ]] 测试关键字 注意:EXPRESSION两端必须有空白字符,否则为语法错误。 (3)bash的测试类型有:数值测试、字符串测试和文件测试。 1)数值测试:数值比较 -eq:是否等于,[ $num1 -eq $num2 ]; -ne:是否不等于; -gt:是否大于; -ge:是否大于等于; -lt:是否小于; -le:是否小于等于; 2)字符串测试: ==:是否等于; >:是否大于; <:是否小于; !=:是否不等于; =~:左侧字符串是否能够被右侧的PATTERN所匹配; -z “STRING”:判断指定的字串是否为空,空则为真,不空则假; -n “STRING”:判断指定的字符串是否不空,不空则真,空则为假; 注意:此表达式一般用于[[ ]]中,测试表达式中的字符串要加引号; 3)文件测试: 存在性测试:文件的存在性测试,存在则为真,否则则为假。 -a FILE -e FILE

[root@localhost ~]# [ -e /etc/rc.d/rc.sysinit ]
[root@localhost ~]# echo $?
1

存在性及类型测试: -b FILE:是否存在并且为块设备文件; -c FILE:是否存在并且为字符设备文件; -d FILE:是否存在并且为目录文件; -f FILE:是否存在并且为普通文件; -h FILE 或 -L FILE:是否存在并且为符号链接文件; -p FILE:是否存在且为命名管道文件; -S FILE:是否存在且为套接字文件;

[root@localhost ~]# [ -b /dev/sda ]
[root@localhost ~]# echo $?
0

文件权限测试: -r FILE:是否存在并且对当前用户可读; -w FILE:是否存在并且对当前用户可写; -x FILE:是否存在并且对当前用户可执行; 特殊权限测试: -u FILE:是否存在并且拥有suid权限; -g FILE:是否存在并且拥有sgid权限; -k FILE:是否存在并且拥有sticky权限;

[root@localhost ~]# [ -u /usr/bin/passwd ]
[root@localhost ~]# echo $?
0

文件是否有内容: -s FILE:是否存在且非空;

[root@localhost ~]# touch /exercise/hello
[root@localhost ~]# [ -s /exercise/hello ]
[root@localhost ~]# echo $?
1

时间戳: -t fd:fd表示文件描述符是否已经打开并且与某终端相关; -N FILE:文件自从上一次读操作后是否被修改过; 从属关系测试: -O FILE:当前用户是否为文件的属主; -G FILE:当前用户是否属于文件的属组; 双目测试: FILE1 -ef FILE2:FILE1与FILE2是否指向同一个文件系统的相同inode; FILE1 -nt FILE2:FILE1是否新于FILE2,FILE1的mtime新于FILE2则为真,否则为假; FILE1 -ot FILE2:FILE1是否旧于FILE2,FILE1的mtime旧于FILE2则为真,否则为假; 4)组合测试条件: 第一种方式: COMMAND1 && COMMAND2 COMMAND1 || COMMAND2 ! COMMAND [ -O FILE ] && [ -r FILE ] 第二种方式:(此方式必须使用测试命令进行,即test或者[ ]) [ EXPRESSION1 -a EXPRESSION2 ] [ EXPRESSION1 -o EXPRESSION2 ] [ -not EXPRESSION ] [ -O FILE -a -x FILE ]

例:将当前主机名称保存至hostName变量中,主机名如果为空,或者为localhost.localdomain,则将其设置为www.magedu.com。
hostName=$(hostname)
[ -z "$hostName" -o "$hostName" == "localhost.localdomain" -o "$hostName" == "localhost" ] && hostname www.magedu.com

8、脚本参数

脚本的状态返回值,默认是脚本中执行的最后一条命令的状态返回值。 自定义状态退出状态码:exit [n]:n为自己指定的状态码;0表示成功,非0表示失败。注意:shell进程遇到exit时,即会终止,因此,整个脚本执行即为结束,终止退出状态取决于exit命令后面的数字。如果为该脚本指定退出状态码,整个脚本的退出状态码取决于脚本中执行的最后一条命令的状态码。 [root@localhost exercise]# id user3 &> /dev/null && exit 0 || useradd user3 向脚本传递参数:位置参数变量 myscript.sh argu1 argu2 引用方式:$1, $2, …, ${10}, ${11}, … 轮替:shift [n]:位置参数轮替;

[root@localhost exercise]# vim first.sh

#!/bin/bash
#
echo "First and second pos argu: $1, $2"
shift 2                                      一脚踢两个;
echo "Third pos argu:$1"

[root@localhost exercise]# bash first.sh first second third
First and second pos argu: first, second
Third pos argu:third
例:写一脚本,通过命令行传递两个文本文件路径给脚本,计算其空白行数之和。
			
#!/bin/bash
#
[ $# -lt 2 ] && echo "At least two files" && exit 1
file1_lines=$(grep "^$" $1 | wc -l)
file2_lines=$(grep "^$" $2 | wc -l)
echo "Total blank lines: $[$file1_lines+$file2_lines]"	
			
[root@localhost exercise]# bash blank.sh /etc/fstab /etc/rc.d/init.d/functions
space line is 71

特殊变量: $0:脚本文件路径本身; $#:脚本参数的个数; $*:所有参数 “hello” “hi” “toyou”; $@:所有参数 “hello hi toyou”。

9、代码执行顺序

(1)选择执行 单分支的if语句:

if  测试条件
then
       代码分支
fi

if  测试条件; then
       代码分支
fi

双分支的if语句:

if  测试条件; then
        条件为真时执行的分支
else
       条件为假时执行的分支
fi

多分支的if语句:

if  CONDITION1; then
          条件1为真分支
elif  CONDITION2; then
          条件2为真分支
elif  CONDITION3; then
          条件3为真分支
...
elif  CONDITIONn; then
          条件n为真分支
else
          所有条件均不满足时的分支
fi

case语句:

case  $VARAIBLE  in  
PAT1)
	分支1
	;;
PAT2)
	分支2
	;;
	...
*)
	分支n
	;;
esac

case支持glob风格的通配符: *:任意长度的任意字符; ?:任意单个字符; []:范围内任意单个字符; a|b:a或b; (2)循环执行 1)for循环: 两种格式:遍历列表、控制变量; 遍历列表:

for  VARAIBLE  in  LIST; do
     循环体
done

进入条件:只要列表有元素,即可进入循环; 退出条件:列表中的元素遍历完成; LIST的生成方式:

  1. 直接给出;
  2. 整数列表; (a) {start…end},例如:{1…10}; (b) seq [start [incremtal]] last,例如:$ (seq 1 2 10);
  3. 返回列表的命令的执行结果:ls, cat,例如,$ (ls /var); ls列表中一行只有一个元素,有多行;
  4. glob:ls -d /etc/p*
  5. 变量引用:$@, $*

2)while循环:

while  CONDITION; do
	 循环体
	循环控制变量修正表达式
done

进入条件:CONDITION测试为”真“; 退出条件:CONDITION测试为”假“。 3)until循环:

until  CONDITION; do
	循环体
	循环控制变量修正表达式
done

进入条件:CONDITION测试为”假“ 退出条件:CONDITION测试为”真“

循环示例:
7. 求100以内所有正整数之和。
#!/bin/bash
#
declare -i sum=0
declare -i i=1

until [ $i -gt 100 ]; do
		let sum+=$i
		let i++
done
echo $sum			

#!/bin/bash
#
declare -i sum=0
declare -i i=1

while [ $i -le 100 ]; do
		let sum+=$i
		let i++
done
echo $sum			

2. 打印九九乘法表;
外循环控制乘数,内循环控制被乘数。
#!/bin/bash
#
for j in {1..9}; do
		for i in $(seq 1 $j); do
				echo -n -e "${i}X${j}=$[${i}*${j}]\t"
		done
		echo 
done	

循环控制语句: continue:提前结束本轮循环,而直接进入下一轮循环判断;

while  CONDITION1; do
	CMD1
	...
	if  CONDITION2; then
		continue
	fi
	CMDn
	...
done

break:提前跳出循环;

while  CONDITION1; do
	CMD1
	...
	if  CONDITION2; then
		break
	fi
done

创建死循环:

while true; do
	循环体
done

退出方式:某个测试条件满足时,让循环体执行break命令。 sleep命令:sleep NUMBER while循环的特殊用法(遍历文件的行):

while  read  VARIABLE; do
	循环体;
done  <  /PATH/FROM/SOMEFILE

依次读取/PATH/FROM/SOMEFILE文件中的每一行,且将基赋值给VARIABLE变量。 for循环的特殊用法:

for  ((控制变量初始化;条件判断表达式;控制变量的修正语句)); do
	循环体
done

控制变量初始化:仅在循环代码开始运行时执行一次; 控制变量的修正语句:每轮循环结束会先进行控制变量修正运算,而后再做条件判断。

10、用户交互

(1)read命令输入数据 read [option]... [name ...]:通过键盘输入数据,从而完成变量赋值操作; -p ‘PROMPT’ -t TIMEOUT

[root@localhost exercise]# echo -n "enter a username:"; read name
enter a username:tom
[root@localhost exercise]# echo $name
tom
[root@localhost exercise]# read -p "Enter a username:" name
Enter a username:tom
[root@localhost exercise]# read -t 5 -p "Enter a username:" name
Enter a username:[root@localhost exercise]# 
[root@localhost exercise]# echo $name
[root@localhost exercise]# [ -z "$name" ] && name="obama"
[root@localhost exercise]# echo $name
obama

(2)bash命令调试脚本 bash -n /path/to/some_script:检测脚本中的语法错误; bash -x /path/to/some_script:调试执行;

11、函数

语法一:
	function  f_name  {
		...函数体...
	}
语法二:
	f_name()  {
		...函数体...
	}

定义函数的代码段不会自动执行,在调用时执行;所谓调用函数,在代码中给定函数名即可,函数名出现的任何位置,在代码执行时,都会被自动替换为函数代码。 (1)函数的生命周期:每次被调用时创建,返回时终止。其状态返回结果为函数体中运行的最后一条命令的状态结果,自定义状态返回值,需要使用:return。return [0-255],0表示成功,1-255表示失败。 函数返回值: 1)函数的执行结果返回值: 1 使用echo或printf命令进行输出; 2 函数体中调用的命令的执行结果; 2)函数的退出状态码: 1 默认取决于函数体中执行的最后一条命令的退出状态码; 2 自定义:return; (2)函数可以接受参数: 在函数体中当中,可以使用$ 1,$ 2, …引用传递给函数的参数;还可以函数中使用$ *或$ @引用所有参数,$#引用传递的参数的个数,在调用函数时,在函数名后面以空白符分隔给定参数列表即可,例如,testfunc arg1 arg2 arg3 … (3)变量作用域: 1)局部变量:作用域是函数的生命周期;在函数结束时被自动销毁,定义局部变量的方法,local VARIABLE=VALUE; 2)本地变量:作用域是运行脚本的shell进程的生命周期,因此,其作用范围为当前shell脚本程序文件。 函数递归:函数直接或间接调用自身。

12、数组

数组:存储多个元素的连续的内存空间; 数组名:整个数组只有一个名字; 数组索引:编号从0开始: $ {ARRAY_NAME[INDEX]} 注意:bash-4及之后的版本,支持自定义索引格式,而不仅仅是0,1,2,…数字格式,此类数组称之为“关联数组”。 声明数组: declare -a NAME:声明索引数组; declare -A NAME:声明关联数组; 数组中元素的赋值方式: (1)一次只赋值一个元素:ARRAY_NAME[INDEX]=value (2)一次赋值全部元素:ARRAY_NAME=("VAL1" "VAL2" "VAL3" ...) (3)只赋值特定元素:ARRAY_NAME=([0]="VAL1" [3]="VAL4" ...) 注意:bash支持稀疏格式的数组; (4)read -a ARRAY_NAME 引用数组中的元素:${ARRAY_NAME[INDEX]},引用时,只给数组名,表示引用下标为0的元素。 数组的长度(数组中元素的个数):${#ARRAY_NAME[*]}${#ARRAY_NAME[@]} 引用数组中的所有元素:${ARRAY_NAME[*]}${ARRAY_NAME[@]} 数组元素切片:${ARRAY_NAME[@]:offset:number} offset:要路过的元素个数;number:要取出的元素个数;省略number时,表示取偏移量之后的所有元素。 向非稀疏格式数组中追加元素:ARRAY_NAME[${#ARRAY_NAME[*]}]= 删除数组中的某元素:unset ARRAY[INDEX] 关联数组:declare -A ARRAY_NAME ARRAY_NAME=([index_name1]="value1" [index_name2]="value2" ...)

13、字符串

bash的内置字符串处理工具: (1)字符串切片:${var:offset:number} 取字符串的子串;取字符串的最右侧的几个字符:${var: -length} 注意:冒号后必须有一个空白字符。 (2)基于模式取子串 ${var#*word}:其中word是指定的分隔符,功能:自左而右,查找var变量所存储的字符串中,第一次出现的word分隔符,删除字符串开头至此分隔符之间的所有字符。 ${var##*word}:其中word是指定的分隔符。功能:自左而右,查找var变量所存储的字符串中,最后一次出现的word分隔符,删除字符串开头至此分隔符之间的所有字符。 ${var%word*}:其中word是指定的分隔符。功能:自右而左,查找var变量所存储的字符串中,第一次出现的word分隔符,删除此分隔符至字符串尾部之间的所有字符; ${var%%word*}:其中word是指定的分隔符。功能:自右而左,查找var变量所存储的字符串中,最后一次出现的word分隔符,删除此分隔符至字符串尾部之间的所有字符。 (3)查找替换 ${var/PATTERN/SUBSTI}:查找var所表示的字符串中,第一次被PATTERN所匹配到的字符串,将其替换为SUBSTI所表示的字符串。 ${var//PATTERN/SUBSTI}:查找var所表示的字符串中,所有被PATTERN所匹配到的字符串,并将其全部替换为SUBSTI所表示的字符串。 ${var/#PATTERN/SUBSTI}:查找var所表示的字符串中,行首被PATTERN所匹配到的字符串,将其替换为SUBSTI所表示的字符串。 ${var/%PATTERN/SUBSTI}:查找var所表示的字符串中,行尾被PATTERN所匹配到的字符串,将其替换为SUBSTI所表示的字符串。 注意:PATTERN中使用glob风格和通配符。 (4)查找删除 ${var/PATTERN}:以PATTERN为模式查找var字符串中第一次的匹配,并删除之。 ${var//PATERN}:以PATTERN为模式查找var字符串中所有的匹配,并删除之。 ${var/#PATTERN}:查找var所表示的字符串中,行首被PATTERN所匹配到的字符串,并删除之。 ${var/%PATTERN}:查找var所表示的字符串中,行尾被PATTERN所匹配到的字符串,并删除之。 (5)字符大小写转换 ${var^^}:把var中的所有小写字符转换为大写。 ${var,,}:把var中的所有大写字符转换为小写。 (6)变量赋值 ${var:-VALUE}:如果var变量为空,或未设置,那么返回VALUE,否则,则返回var变量的值。 ${var:=VALUE}:如果var变量为空,或未设置,那么返回VALUE,并将VALUE赋值给var变量,否则,则返回var变量的值。 ${var:+VALUE}:如果var变量不空,则返回VALUE。 ${var:?ERROR_INFO}:如果var为空,或未设置,那么返回ERROR_INFO为错误提示,否则,返回var值。

14、信号捕捉

(1)列出信号 trap -l kill -l man 7 signal trap 'COMMAND' SIGNALS 常可以进行捕捉的信号:HUP, INT (2)在bash中使用ACSII颜色 \033[31m hello \033[0m ##m: 左侧#: 3:前景色 4:背景色 右侧#:颜色种类:1, 2, 3, 4, 5, 6, 7 #m:加粗、闪烁等功能; 多种控制符,可组合使用,彼此间用分号隔开。 (3)dialog命令可实现窗口化编程

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券