Shell脚本编程

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)

你可能感兴趣的:(Shell项目实战代码,shell脚本)