shell 脚本编程实例

视频教程

0. 一些不方便分类的知识点

  1. “$#” 表示运行脚本或者调用函数时,输入的参数个数,不包括脚本文件名以及函数名;
  2. “$0” 不管在函数外部还是函数内部,都表示当前脚本文件名;
  3. “$1” 在函数内部,表示传递给函数的第一个参数;在函数外部表示传递给脚本的第一个参数;“$2” “$3” 意义也一样;
  4. “$$” 表示当前脚本的 pid;可以通过 “kill -9 pid” 指令退出指定进程;
  5. 调试 “bash -x scriptName”,通过这个调试命令,可以查看整个脚本的运行过程;
cat /etc/shells		# 查看当前系统支持的shell
echo $SHELL			# 查看当前系统默认的shell

echo -e "Enter the info: \c"	# -e使能转义字符,\c不换行
echo $(ls)		# 执行Linux命令
(( n++ )) 		# n自增1
n=$(( n+1 ))	# n自增1
# 以下这句命令利用逻辑运算符的短路运算,可以判断文件是否存在
# [ -f fileName ] : 如果 fileName 存在,其返回值为 0,但是这个 0 是被当作 true 理解的。(返回值可以通过 echo $? 指令查看)
# 当 [ -f fileName ] 为 true 时,因为后面是 &&,就会继续判断 && 后面的条件(这里是执行 echo 语句),这里必须要保证 && 后面的语句执行状态为 true,否则将会继续执行 || 后面的语句;
# 当 [ -f fileName ] 为 false 时,将会直接跳过 && 后面的语句,直接执行 || 后面的语句;
[ -f fileName ] && echo "file exsit" || echo "file not found"

1. 输出系统变量和自定义变量

#! /bin/bash
echo current work dir is $PWD
echo $SHELL
echo $BASH
echo $BASH_VERSION

pi=3.14		# 系统变量一般大写,自定义变量小写,且不能以数字开头
echo $pi

2. 获取用户输入

echo enter your name:
read name
echo your name is $name

echo enter three string:
read str1 str2 str3		# 一次获取多个输入值,以空格间隔(中间有空格需加引号),回车结束输入
echo your input $str1 $str2 $str3

read -p "enter your name: " name	# -p 可以添加输入提示
echo your name is $name

read -sp "enter your password: " password	# 静默输入(输入时不显示); -sp 不能写成 -ps;
echo your possword is $password

read -p "enter names: " -a names	# -a 表示输入一个数组,空格间隔,回车结束输入
echo ${names[0]} ${names[1]}		# 输出数组中项的值

echo $REPLY
read		# 当read后没有带变量名时,默认将输入存储在系统变量 $REPLY 中
echo $REPLY

3. 获取命令参数

echo totally $# params	# $#表示参数个数(不包括文件名)
echo $0 $1 $2	# $0表示文件名;$1表示第一个参数;$2表示第二个参数
args=("$@")		# $@表示所有参数; "args=$@"好像也可以
echo $@			# 输出所有参数(不包含文件名)
echo ${args[0]} ${args[1]}	# args[0]表示第一个参数,而不是文件名!

4. if 语句

4.1 整型数比较

符号 意义 举例
-eq equal if [ “$a” -eq “$b” ]
-ne not equal if [ “$a” -ne “$b” ]
-gt greater than if [ “$a” -gt “$b” ]
-ge greater than or equal if [ “$a” -ge “$b” ]
-lt less than if [ “$a” -lt “$b” ]
-le less than or equal if [ “$a” -le “$b” ]
< less than ((“$a” < “$b”))
<= less than or equal ((“$a” <= “$b”))
> greater than ((“$a” > “$b”))
>= greater than or equal ((“$a” >= “$b”))

4.2 字符串比较

符号 意义 举例
= equal if [ “$a” = “$b” ]
== equal if [ “$a” == “$b” ]
!= NOT equal if [ “$a” != “$b” ]
< less than, in ASCII order if [[ “$a” < “$b” ]]
> greater than, in ASCII order if [[ “$a” > “$b” ]]
-z string is NULL(len=0) if [[ -z “$a” ]]

注意到,以上比较中,不管是数值比较还是字符串比较,只要使用了 “<”, “>” 相关的符号,就必须有双重括号,这是为了覆盖 “<”, “>” 这两个符号本身的重定向功能。

count=9		# 中间不能有空格
if [ $count -ne 10 ]
then
    if (( $count > 10 ))	# (()) 不能用 [[]] 代替,不然将当作字符串比较
    then
        echo \$count is greater than 10
    else
        echo \$count is less than 10
    fi
else
    echo \$count is 10
fi
val=0			# val="" 将会导致下面的判断为假
if [ $val ]		# 这里将会判断为真,因为默认 val 为字符串
then 
	echo true, \$val is $val
else 
	echo false, \$val is $val
fi
num=0
if ( $val )		# 这里判断为真,() 内部为数值判断
then
    echo true, \$num is $num
else
    echo false,\$num is $num
fi
num=2
if (( $num <= 5 ))
then
    echo \$num\($num\) is less than  or equal to 5
elif (( $num < 10 ))
then
    echo \$num\($num\) is less than 10
else
    echo \$num\($num\) is greater than or equal to 10
fi

5. file check

符号 意义 举例
-e 文件是否存在 if [ -e $file_name ]
-f 文件是否存在以及是否常规文件 if [ -f $file_name ]
-d 文件夹是否存在 if [ -d $dir_name]
-c 字符文件(文本文件) if [ -c $file_name ]
-b block文件(二进制,图片,视频文件) if [ -b $file_name ]
-s 文件是否为空 if [ -s $file_name ]
-r, -w, -x 文件或文件夹是否有相应权限 if [ -x $file_name ]
echo -e "Enter the file name: \c"	# -e使能转义功能,\c不换行
read name

if [ -f $name ]		# 文件是否存在
then
    if [ -w $name ]		# 文件是否有写入权限
    then
        echo "append some text to file, Ctrl-D to quit: "
        cat >> $name	# 追加输入的内容到指定文件
    else 
        echo "$name has no write permissions"
    fi
else
    echo "$name is not exist"
fi

6. 逻辑操作(&&(-a), ||(-o))

age=25
if [ $age -gt 18 ] && [ $age -lt 30 ]  # 这个写法和一些两种都一样
# if [ $age -gt 18 -a $age -lt 30 ]
# if [[ $age -gt 18 && $age -lt 30 ]]
then
	echo "valid age"
else
	echo "invalid age"
fi

7. 整形 算术操作(+, -, *, /, %)

所有的算术操作都需要在双括号 (()) 里面完成! 或者使用 expr 命令行计算器工具
但是以上两个方式都只能针对整数进行运算

echo $((1+2))			# 3
echo $(expr 1 + 2) 		# 3, 加号两边的空格一定要有
echo $(( 2 * 3 ))		# 6
echo $(expr 2 \* 3) 	# 6, *前面需要转义

8. 小数 算术运算(+, -, *, /, %)

bc: basic calculator

echo 12.5+1.2 | bc
echo "scale=3; 2/3" | bc	# scale指定保留的小数位数,且只能在除法、求平方根等特定计算中使用中使用;一定需要双引号;
echo "scale=3;sqrt(27)" | bc -l		# 平方根计算; -l是为了调用数学库用于sqrt计算
echo "3^3" | bc

9. case 语句

case $1 in		# 根据第一个参数判断
    "car" )
        echo car
        echo second line;;	# 可以执行多个语句,直到 ;; 结束
    "van" )
        echo van
        echo second line;;
    * )					# default 情况
        echo nothing
        echo second line;;
esac
# 如果输入 ? 和 * 将出现奇怪现象,暂时不知道怎么解释
read -p "Enter a character: " ch
case $ch in
    [a-z] )		# 这个条件可以是一个范围
        echo user input $ch between a to z;;
    [A-Z] )
        echo user input $ch between A to Z;;
    [0-9] )
        echo user input $ch between 0 to 9;;
    ? )			# ?只匹配一个字符
        echo user input others character $ch;;
    * )			# *匹配所有字符
        echo user input not only one character;;
esac

10. 数组

  1. bash 中的数组索引可以是不连续的,可以在随意索引处添加元素,而删除元素后,这个元素之后的元素索引也并不会发生变化。
  2. 而数组长度和索引值无关,其真实表现了数组的真实个数;比如一个数组在索引 0,4,6处有值,其长度为 3;
  3. 任何一个变量都可以认为是数组,变量值存储在索引为0的位置;
os=('linux' 'windows' 'MacOS')
echo ${os[@]}	# 输出所有元素
echo ${os[0]}	# linux
echo ${!os[@]}	# 输出所有的索引,0 1 2
echo ${#os[@]}	# 输出数组长度,3
os[${#os[@]}]='Android'		# 添加项
echo ${os[@]}
unset os[2]		# 删除索引为2的元素
echo ${!os[@]}	# 查看删除元素后索引值的变化: 0 1 3; 可以看到删除2后没有将后面的index前移

11. while 循环

n=1
while [ $n -le 5 ]
# while (( $n <= 5 ))	# 这句和上面的语句等效
do
    echo \$n is $n
    n=$((n+1))			# 可以用 (( n++ )) 代替
    sleep 1				# delay 1s
done
# 以下脚本功能:将脚本本身打印到终端
while read p	# 读一行
do
echo $p
done < $0		# 以参数0(即脚本名)为输入,重定向到 while 循环
# 功能和上面的脚本一样
cat $0 | while read p	# 通过管道将脚本内容输入到 while 循环
do
echo $p
done
# 功能和上面的脚本一样
while IFS=' ' read -r line		# IFS指定字分隔符为空格;-r表示在读取文件中防止反斜杠的转义解释
do
echo $line
done < $0

12. until 循环

  1. until循环和while循环唯一的不同就是:while循环是在条件为真时执行,而until循环是在条件为假时执行;
  2. 在各种循环中,也可以使用 break 跳出循环,以及使用 continue 跳过本次循环;
n=1
until (( n > 5 ))
do
	echo \$n is $n
	(( n++ ))
done

13. for循环

# 以下将会输出:1 2 .. 5		# 都当作字符串处理了
for n in 1 2 .. 5
do 
    echo $n
done
# 以下将输出:1 2 3 4 5
for n in {1..5}		# 需要 $BASH_VERSION 大于 3.0
do
	echo $n
done
# 输出:1 3 5 7 9
for n in {1..10..2}		# {starValue..endValue..step}	# 需要 $BASH_VERSION 大于 4.0
do 
    echo $n
done
# 输出:1 2 3 4 5
for (( i=0; i<5; i+=1 ))	# 操作符前后可以加空格,也可以用 i++
do 
    echo $i
done
for n in $(ls)
do 
    echo $n		# 依次输出 ls 命令的输出
done
# 执行命令
for cmd in ls pwd date
do
    $cmd	# 依次执行以上命令
done
# 输出当前路径下所有文件夹的名称(当文件夹名称中有空格时输出还有点问题)
for item in *	# *将会枚举当前路径下的所有文件名和文件夹名
do
	if [ -d ${item} ]
	then
		echo $item
	fi
done

14. select 循环

select 循环适合做和用户进行交互的菜单,给用户提供一些选择

select name in jack mark denis eleen
do
    echo "$name selected"
done
select name in jack mark denis eleen
do
    case $name in		# select循环和case语句结合,可以方便得处理各种情况
        jack) 
            echo jack selected
            break;;		# 通过break可以跳出select循环,后面的两个 ;; 是必不可少的,不然报语法错误
        mark)
            echo mark selected
            break;;
        denis) 
            echo denis selected
            break;;
        eleen) 
            echo eleen selected
            break;;
        *) 
            echo input error;;		# 输入错误,不跳出循环,继续读取用户输入 
    esac
done

15. 函数

  1. 函数必须先声明,然后调用;
  2. 如果函数外部变量和函数内部变量同名,在调用函数之后,会修改外部变量的值;
  3. 可以通过关键字 local 解决上面的问题;local 只能在函数中使用;
function Hello(){	# 通过关键字 function 定义名为 Hello 的函数
    echo Hello
}
quit(){				# 省略 function 关键字定义名为 quit 的函数
    exit			# 执行 exit 指令
}
Hello				# 调用 Hello 函数
quit				# 调用 quit 函数
echo hello again	# 由于上面的 quit 函数已经退出了,所以这里的 echo 语句不会执行
function print(){
	echo $1			# 通过 $1 访问函数的第一个参数
}
print Hello			# 调用 print 函数并传入一个参数
print World			# 调用 print 函数并传入一个参数
# 变量同名的问题
function print(){
    var=$1
    echo $var
}
var=Hello
echo $var		# 输出 Hello
print World		# 输出 World
echo $var		# 输出 World (var的值已经在 print 函数中被改变了)
# 通过 local 解决变量同名问题
function print(){
    local var=$1	# 通过 local 定义局部变量
    echo $var
}
var=Hello
echo $var		# 输出 Hello
print World		# 输出 World
echo $var		# 输出 Hello

16. 应用实例

#!/bin/bash
usage() {
    echo "you should input a file name as a params"
    echo "usage: $1 fileName"		# 这里的 $1 表示调用这个函数的第一个参数
    exit
}

is_file_exsit() {
    local file=$1
    [ -f $1 ] && return 0 || return 1		# 详细解释见本文最前面,简单说就是利用了逻辑运算符的短路原则
}

[ $# -eq 0 ] && usage		# 判断运行脚本时是否有输入参数,如果没有,则会调用 usage 函数告诉用户需要输入一个参数并退出脚本

if ( is_file_exsit $1 )		# 判断文件是否存在,这里的 $1 表示运行这个脚本时输入的第一个参数
then
    echo file found
else
    echo file NOT found
fi

17. readonly 关键字

  1. 直接运行 readonly 而不指定只读对象时,将会默认设置 bash 内建的变量都变成只读变量!和 “readonly -p” 指令一样;
  2. "readonly -f " 可以输出所有的只读函数;
readonly var=31		# 定义 readonly 变量,readonly 也可以作用在前面已经定义的变量上
echo $var			# 输出 31
var=43				# 执行错误
echo $var			# 输出 31
var=12
readonly var=34		# 执行正常
echo $var			# 输出34
var=12
readonly var
var=34				# 执行错误
echo $var			# 输出12
hello() {			# 定义 hello 函数
    echo hello1
}
hello				# 调用 hello 函数

hello() {			# 重定义 hello 函数
    echo hello2
}
hello				# 调用重定义之后的 hello 函数

readonly -f hello	# 将函数声明为 readonly,需要添加 -f 参数;函数不能在定义时就声明为 readonly,必须定义后再声明;

hello() {			# 再次重定义 hello 函数,但是执行错误
    echo hello3
}
hello				# 这里输出的还是 hello2

18. 关于变量初始值

while (( COUNT < 5 ))
do
    echo $COUNT			# 第一次输出为空,之后依次输出 1 2 3 4
    (( COUNT++ ))
done
COUNT=0
while (( COUNT < 5 ))
do
    echo $COUNT			# 依次输出 0 1 2 3 4
    (( COUNT++ ))
done

19. 信号以及信号跟踪

  1. 通过 “man 7 signal” 可以查看都有哪些信号;
    EXIT -------> (Unknow) ------> 0
    SIGINT -------> CTRL+C ------> 2
    SIGTSTP -------> CTRL+Z ------> 20
    SIGKILL -------> kill -9 pid ------> 9
  2. trap “code to be executed” sig_number # 这个 trap 语句必须写在出现这个信号之前的地方,不然无效
  3. <> 和 <> 完全等价;
  4. 触发 trap 语句后,这个信号原有的功能就失效了,只执行用户在双引号中自定义的功能;然后从触发此信号时正在执行的语句的下一条处开始执行;
  5. trap 不能捕获 SIGKILL信号 和 SIGSTOP信号;通过 “kill -9 pid”(等价于"kill -SIGKILL pid")可以发送SIGKILL信号,通过 “kill -SIGSTOP pid” 可以发送 SIGSTOP信号;经过SIGSTOP信号暂停的脚本,可通过SIGCONT信号使其继续执行;
  6. 疑问:可以通过 “kill -SIGINT pid” 代替 CTRL-C 吗?
  7. 好像可以通过在另一个终端运行 “trap” 指令查看被跟踪的信号,还可以通过 “trap - sig_num” 去掉对指定信号的跟踪?但是我没成功。。。
trap "echo trap signal 2" 2		# 跟踪信号2,即 SIGINT 信号;
echo Hello
sleep 5		# 在这期间,按 CTRL-C 后触发 trap 中的语句
exit 2		# 按正常流程执行完后并没有触发 trap 中的语句执行,不知道为啥
trap "echo trap signal 0 " 0	# 跟踪信号0
echo Hello	
sleep 5		# 在这期间,按 CTRL-C 可以触发 trap 语句,但是按 CTRL-Z 不能触发 trap 语句
exit 0		# 按正常流程执行完,也触发了 trap 语句
trap "echo trap signal 0 " 0	# 可以触发
echo Hello
trap "echo trap signal 0 " 0	# 可以触发
echo Hello
exit 0
trap "echo trap signal 0 " 0	# 可以触发
echo Hello
exit 1		# 不管这里退出码是啥,好像都可以触发(因为只要执行没有出错就会产生信号0?)
# 以下代码中,正常执行完后会触发信号0
# 当代码结束前按 CTRL-C,会先触发信号2,再触发信号0
# 当代码结束前按 CTRL-Z, 两个信号都不会触发
trap "echo trap signal 0" 0
trap "echo trap signal 2" 2
echo Hello
sleep 5
# 试一试在这个代码执行时按 CTRL-C,每按依次,就会触发一次信号2,然后执行下一条语句,直到最后代码执行完成,还会触发一次信号0
trap "echo trap signal 0" 0
trap "echo trap signal 2" 2
cnt=0
while (( cnt < 10 ))
do
    sleep 5
    echo $cnt
    (( cnt++ ))
done
# 在另外一个终端通过 kill -9 pid 命令依旧可以直接关闭以下脚本;
# 因为 trap 不能捕获到 SIGKILL信号 和 SIGSTOP信号;
trap "echo trap signal SIGKILL" SIGKILL SIGSTOP		# 可以通过同一条 trap 语句同时接收多个信号
echo $$		# 输出当前脚本的 pid
cnt=0
while (( cnt < 10 ))
do
    sleep 5
    echo $cnt
    (( cnt++ ))
done
# 按正常流程执行完以下代码,会在当前目录下新建一个 sig0.txt 文件;
# 在以下代码执行结束前按下 CTRL-C,会在当前目录下新建两个文件:sigint.txt 和 sig0.txt;
trap "touch sigint.txt; exit 1" SIGINT
trap "touch sig0.txt; exit" 0
sleep 5
# 正常流程执行完以下代码,会输出 get some signal
# 在以下代码执行结束前按下 CTRL-C,会输出两遍 get some signal
trap "echo get some signal" 0 SIGINT		# 可以通过同一条 trap 语句同时接收多个信号
sleep 5

20. 调试

方式一:bash -x scriptName
方式二:在脚本的第一行(必须首行)写上 “#!/bin/bash -x” (从这里也可以看出,第一行不仅仅是写给程序员看的,也是有实际功能的)
在脚本的任意位置写上 “set -x”,代码执行到这里开始将进入调试状态;直到文件结束或者遇到 “set +x” 语句使得调试功能关闭;

#!/bin/bash -x
sleep 2
echo Hello
#!/bin/bash
set -x
sleep 2
set +x
echo Hello

你可能感兴趣的:(shell,linux,服务器,运维,shell,script)