1、Shell脚本是什么?
Shell脚本语言为过程式语言,解释运行,依赖于外部程序文件来运行。Shell脚本是命令的堆积,但很多命令不具有幂等性,需要用程序逻辑来判断运行条件是否满足,以避免其运行中发生错误。但并不是所有命令执行失败,都会导致脚本运行终止,命令执行失败,如果产生严重错误,该严重错误指,脚本会exit,任何时候shell脚本或shell解释器遇到exit命令就会终止,或我们使用某种判断机制将其强行终止,或者脚本运行过程中出现语法错误,也可能终止。
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 $?
存在性及类型测试:
-b FILE:是否存在并且为 块设备 文件;
-c FILE:是否存在并且为 字符设备 文件;
-d FILE:是否存在并且为 目录文件;
-f FILE:是否存在并且为 普通文件;
-h FILE 或 -L FILE:是否存在并且为 符号链接文件;
-p FILE:是否存在且为 命名管道文件;
-S FILE:是否存在且为 套接字文件;
[root@localhost ~]# [ -b /dev/sda ]
[root@localhost ~]# echo $?
文件权限测试:
-r FILE:是否存在并且 对当前用户可读;
-w FILE:是否存在并且 对当前用户可写;
-x FILE:是否存在并且 对当前用户可执行;
特殊权限测试:
-u FILE:是否存在并且 拥有suid权限;
-g FILE:是否存在并且 拥有sgid权限;
-k FILE:是否存在并且 拥有sticky权限;
[root@localhost ~]# [ -u /usr/bin/passwd ]
[root@localhost ~]# echo $?
文件是否有内容:
-s FILE:是否有内容;
[root@localhost ~]# touch /exercise/hello
[root@localhost ~]# [ -s /exercise/hello ]
[root@localhost ~]# echo $?
时间戳:
-N FILE:文件自从上一次读操作后是否被修改过;
从属关系测试:
-O FILE:当前用户是否为文件的属主;
-G FILE:当前用户是否属于文件的属组;
双目测试:
FILE1 -ef FILE2:FILE1与FILE2是否指向同一个文件系统的相同inode的硬链接;
FILE1 -nt FILE2:FILE1是否新于FILE2;
FILE1 -ot FILE2:FILE1是否旧于FILE2;
4)组合测试条件:
第一种方式:
COMMAND1 && COMMAND2
COMMAND1 || COMMAND2
! COMMAND
[ -O FILE ] && [ -r FILE ]
第二种方式:
EXPRESSION1 -a EXPRESSION2
EXPRESSION1 -o EXPRESSION2
! 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
1
2
3
8、脚本参数
脚本的状态返回值,默认是脚本中执行的最后一条命令的状态返回值。
自定义状态退出状态码:exit [n]:n为自己指定的状态码;0表示成功,非0表示失败。注意:shell进程遇到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}
(b) seq [start [incremtal]] last
3 返回列表的命令:ls, cat
ls列表中一行只有一个元素,有多行;
4 glob:ls -d /etc/p*
5 变量引用:$@, $*
2)while循环:
while CONDITION; do
循环体
循环控制变量修正表达式
done
进入条件:CONDITION测试为”真“;
退出条件:CONDITION测试为”假“。
3)until循环:
until CONDITION; do
循环体
循环控制变量修正表达式
done
进入条件:CONDITION测试为”假“
退出条件:CONDITION测试为”真“
循环示例:
1. 求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
1
2
3
退出方式:某个测试条件满足时,让循环体执行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脚本程序文件。
函数递归:函数直接或间接调用自身。 (https://blog.csdn.net/gongxifacai_believe/article/details/83539872)