Shell脚本编程详解

前置知识

shell变量

  按照惯例,Shell变量由全大写字母加下划线组成,有两种类型的Shell变量:

环境变量

  环境变量可以从父进程传给子进程,因此Shell进程的环境变量可以从当前Shell进程传给fork出来的子进程。

$ env  # 打印系统环境变量
$ printenv  # 打印系统环境变量
$ echo $PATH  # PATH环境变量

  实际应用中比较常用的是设置(PATH)环境变量:

  • 对所有用户永久生效

    $ vim /etc/profile 
    $ export CLASSPATH=./JAVA_HOME/lib;
    $ export PATH=/usr/local/bin:$PATH
    
  • 仅对当前终端有效

    $ export PATH=/usr/local/bin:$PATH
    
  • 对某一用户永久有效

    $ vim ~/.bashrc 
    $ export PATH=/usr/local/bin:$PATH
    
本地变量

  只存在于当前Shell进程,用set命令可以显示当前Shell进程中定义的所有变量(包括本地变量和环境变量)和函数。环境变量是任何进程都有的概念,而本地变量是Shell特有的概念。

$ VAR=value  # 等号左右两边不要有空格,否则会被Shell解释成命令和命令行参数
$ echo $VAR
$ echo ${VAR}
$ $VAR # 特别注意这种方式会当作命令行来执行
>value: command not found

$ unset VAR # 删除已定义的环境变量或本地变量

  一个变量定义后仅存在于当前Shell进程,它是本地变量,用export命令可以把本地变量导出为环境变量,定义和导出环境变量通常可以一步完成。

$ export VARNAME=value

shell脚本执行方式

# assume a script like below
# test.sh
cd docker_test
ls
  • ./test.sh

    chmod a+x test.sh
    ./test.sh
    

  Shell会fork一个子进程并调用exec执行./test.sh这个程序,exec系统调用应该把子进程的代码段替换成./test.sh程序的代码段,并从它的_start开始执行。然而test.sh是个文本文件,根本没有代码段和_start函数,怎么办呢?其实exec还有另外一种机制,如果要执行的是一个文本文件,并且第一行用Shebang指定了解释器,则用解释器程序的代码段替换当前进程,并且从解释器的_start开始执行,而这个文本文件被当作命令行参数传给解释器。

  • /bin/bash ./test.sh

    这种执行方式和上一种效果相同,都不会影响交互环境,且不需要test.sh文件具有可执行权限。

  • source ./test.sh

    这种执行方式直接在交互式Shell下执行的,会改变交互式Shell的环境。

  • . ./test.sh

  这种执行方式直接在交互式Shell下执行的,会改变交互式Shell的环境。

小结: (cd ./docker_test;ls -l)相当于前两种方式,去掉括号相当于后两种方式

基本语法

通配符

  通配符(Wildcard)常用于脚本或平常的命令行之中,需要注意的就是所匹配的文件名是由Shell展开的,也就是说在参数还没传给程序之前已经展开了。

# *   匹配0个或多个任意字符
# ?   匹配一个任意字符
# [若干字符]  匹配方括号中任意一个字符的一次出现

$ ls /dev/ttyS*
$ ls ch0?.doc
$ ls ch0[0-2].doc
$ ls ch[012]   [0-9].doc

命令代换

  由' '反引号括起来的是一条命令,Shell先执行该命令,然后将输出结果立刻代换到当前命令行中。

$ DATE=`date`
$ echo $DATE

# 命令代换也可以使用$()
$ DATE=$(date)

算数代换

  算术代换是指,$(())中的Shell变量取值将转换成整数,与$[]等价:

$ VAR=45
$ echo $(($VAR+3))
$(())中只能用+-*/和()运算符,并且只能做整数运算。

$[base#n],其中base表示进制,n按照base进制解释,后面再有运算数,按十进制解释。

echo $[2#10+11]
echo $[8#10+11]
echo $[10#10+11]

转义字符

  和C语言类似,\在Shell中被用作转义字符,用于去除紧跟其后的单个字符的特殊意义(回车除外),换句话说,紧跟其后的字符取字面值。例如:

$ echo \\

# 在\之后敲回车表示续行
$ cat \
>a.txt

字符串符号

  在shell中,字符串既可以用单引号‘’也可以使用双引号“”表示,但它们之间存在一定的差别。

  • 单引号

      单引号用于保持引号内所有字符的字面值,即使引号内的\和回车也不例外,但是字符串中不能出现单引号。如果引号没有配对就输入回车,Shell会给出续行提示符,要求用户把引号配上对。

    # 对比
    $ echo '$PATH'
    $ echo $PATH
    
  • 双引号

      被双引号用括住的内容,将被视为单一字串。它防止通配符扩展,但允许变量扩展。作为一种好的Shell编程习惯,应该总是把变量取值放在双引号之中。

    # 对比
    $ echo '$PATH'
    $ echo $PATH
    $ echo "$PATH"
    

逻辑运算

  Shell提供了!,&&,||语法,和C语言类似,具有Short-circuit特性。注意和!-a -o(在下面条件测试的内容中会提到)进行区分。

# 如果不是root用户则输出提示语
$ test "$(whoami)" != 'root' && (echo you are using a non-privileged account; exit 1)

# 区分-a -o
# 以下写法等价,注意体会
test "$VAR" -gt 1 -a "$VAR" -lt 3  # 连接两个测试条件
test "$VAR" -gt 1 && test "$VAR" -lt 3  # 连接两条语句

脚本语法

条件测试

  条件测试即测试一个条件是否成立,如果测试结果为真,则该命令的Exit Status为0,如果测试结果为假,则命令的Exit Status为1

$ var=2
$ test $var -gt 1
$ echo $?
0

$ [ $var -gt 3 ]
$ echo $?
1

  这里需要强调的是,左方括号[是一个命令的名字,传给命令的各参数之间应该用空格隔开,比如,$VAR-gt3][命令的四个参数,它们之间必须用空格隔开。命令test[的参数形式是相同的,只不过test命令不需要]参数。常见测试命令如下:

[ -d DIR ]              如果DIR存在并且是一个目录则为真
[ -f FILE ]             如果FILE存在且是一个普通文件则为真
[ -z STRING ]           如果STRING的长度为零则为真
[ -n STRING ]           如果STRING的长度非零则为真
[ STRING1 = STRING2 ]   如果两个字符串相同则为真
[ STRING1 != STRING2 ]  如果字符串不相同则为真
[ ARG1 OP ARG2 ]        ARG1和ARG2应该是整数或者取值为整数的变量,OP是-eq(等于)-ne(不等于)-lt(小于)-le(小于等于)-gt(大于)-ge(大于等于)之中的一个

# 带与、或、非的测试命令
[ ! EXPR ]          EXPR可以是上表中的任意一种测试条件,!表示逻辑反
[ EXPR1 -a EXPR2 ]  EXPR1和EXPR2可以是上表中的任意一种测试条件,-a表示逻辑与
[ EXPR1 -o EXPR2 ]  EXPR1和EXPR2可以是上表中的任意一种测试条件,-o表示逻辑或

流程控制

  需要记住的一点是,如果两条命令写在同一行则需要用;号隔开,一行只写一条命令就不需要写;号。

  • if/then/elif/else/fi

      if命令的参数组成一条子命令,如果该子命令的Exit Status为0(表示真),则执行then后面的子命令,如果Exit Status非0(表示假),则执行elif、else或者fi后面的子命令。if后面的子命令通常是测试命令,但也可以是其它命令。

    #! /bin/bash
    echo "Is it morning? Please answer yes or no."
    read YES_OR_NO
    if [ "$YES_OR_NO" = "yes" ]; then
    echo "Good morning!"
    elif [ "$YES_OR_NO" = "no" ]; then
    echo "Good afternoon!"
    else
    echo "Sorry, $YES_OR_NO not recognized. Enter yes or no."
    exit 1
    fi
    exit 0
    
    
    # 特殊情况
    if :; then echo "always true"; fi  # :表示空操作,Exit Status always True
    
  • case/esac

      case命令可类比C语言的switch/case语句,esac表示case语句块的结束。C语言的case只能匹配整型或字符型常量表达式,而Shell脚本的case可以匹配字符串和Wildcard,每个匹配分支可以有若干条命令,末尾必须以;;结束,执行时找到第一个匹配的分支并执行相应的命令,然后直接跳到esac之后,不需要像C语言一样用break跳出。

    #! /bin/bash
    echo "Is it morning? Please answer yes or no."
    read YES_OR_NO
    case "$YES_OR_NO" in
    yes|y|Yes|YES)
    echo "Good Morning!";;
    [nN]*)
    echo "Good Afternoon!";;
    *)
    echo "Sorry, $YES_OR_NO not recognized. Enter yes or no."
    exit 1;;
    esac
    exit 0
    
  • for/do/done

    #! /bin/bash
    for FILENAME in `ls chap?`
    do
    mv $FILENAME $FILENAME~;
    done
    
  • while/do/done

    #! /bin/bash
    COUNTER=1
    while [ "$COUNTER" -lt 10 ]; do
    echo "Here we go again"
    COUNTER=$(($COUNTER+1))
    done
    
  • break/continue

  break跳出循环,continue跳过本次循环步,与C语言不同的是break[n]可以指定跳出几层循环。

#! /bin/bash
COUNTER=1
while :  # 记住:是空命令
do
  if test $COUNTER -gt 5;then
    break;
  fi
  COUNTER=$(($COUNTER+1))
done
echo $COUNTER;

位置参数

$0  相当于C语言main函数的argv[0]
$1$2...    这些称为位置参数(Positional Parameter),相当于C语言main函数的argv[1]、argv[2]...
$#  相当于C语言main函数的argc - 1,注意这里的#后面不表示注释
$@  表示参数列表"$1" "$2" ...,例如可以用在for循环中的in后面。
$*  表示参数列表"$1" "$2" ...,同上
$?  上一条命令的Exit Status
$$  当前进程号

  位置参数可以用shift命令左移。比如shift 3表示原来的$4现在变成$1,原来的$5现在变成$2等等,原来的$1、$2、$3丢弃,$0不移动。不带参数的shift命令相当于shift 1。

#! /bin/bash

    echo "The program $0 is now running"
    echo "The first parameter is $1"
    echo "The second parameter is $2"
    echo "The parameter list is $@"
    shift
    echo "The first parameter is $1"
    echo "The second parameter is $2"
    echo "The parameter list is $@"

函数

  和C语言类似,Shell中也有函数的概念,但是函数定义中没有返回值也没有参数列表。注意函数体的左花括号’{‘和后面的命令之间必须有空格或换行,如果将最后一条命令和右花括号’}'写在同一行,命令末尾必须有;号。

# 举个例子
is_directory()
{
DIR_NAME=$1
if [ ! -d $DIR_NAME ]; then
	return 1
else
	return 0
fi
}

for DIR in "$@"; do
if is_directory "$DIR"
	then :
	else
		echo "$DIR doesn't exist. Creating it now..."
		mkdir $DIR > /dev/null 2>&1
		if [ $? -ne 0 ]; then
			echo "Cannot create directory $DIR"
			exit 1
        fi
fi
done

脚本调试

  执行脚本时可以使用相关参数进行相应的调试工作:

-n :读一遍脚本中的命令但不执行,用于检查脚本中的语法错误

-v :一边执行脚本,一边将执行过的脚本命令打印到标准错误输出

-x :提供跟踪执行信息,将执行的每一条命令和结果依次打印出来

  具体使用方法包括以下三种:

  • 命令行参数

    /bin/bash -x ./script.sh
    
  • shebang提供

    #! /bin/bash -x
    
  • set命令启用或禁止调试

    #! /bin/sh
    if [ -z "$1" ]; then
        set -x
        echo "ERROR: Insufficient Args."
        exit 1
        set +x
    fi
    

实例

开机自动脚本

  • 你启动脚本复制到 /etc/init.d目录下,假设脚本文件名为 test.sh
  • 修改权限,sudo chmod 755 /etc/init.d/test.sh
  • cd /etc/init.d
  • sudo update-rc.d test.sh defaults 95 (95表示启动顺序,如需用到网络请用较大数字,如99)

登陆自动脚本

  • 修改/etc/motd文件
  • /etc/profile.d文件中加入脚本文件

字符串处理

基本操作

${file-$DEFAULT} # 若file没有声明,则使用$DEFAULT作传回值(空值及非空值时不作处理)
${file:-$DEFAULT} # 若file没有声明或为空值,则使用$DEFAULT作传回值(非空值时不作处理)

${file+$DEFAULT} # 若变量已经声明,那么其值是$DEFAULT, 否则是null字符串
${file:+$DEFAULT} # 若变量已经声明且非空,那么其值是$DEFAULT, 否则是null字符串

${file=$DEFAULT} # 如果变量没有声明,则$DEFAULT值赋值给变量
${file:=$DEFAULT} # 如果变量没有声明,则$DEFAULT值赋值给变量

${file?$DEFAULT} # 如果变量没有声明,则$DEFAULT值输出到STDERR (空值及非空值时不作处理)
${file:?$DEFAULT} # 如果变量没有声明,则$DEFAULT值输出到STDERR(非空值时不作处理)

字符串截取

字符截取
`#`表示去掉左边的字符(单一匹配)
`%`表示去掉右边的字符(单一匹配)
`##`表示去掉左边的字符(贪婪匹配)
`%%`表示去掉右边的字符(贪婪匹配)


${file#*/}  # home/jeffery/readme.txt
${file##*/}:# readme.txt
${file#*.}:# md.txt
${file##*.}:# txt
${file%/*}# /home/jeffery
${file%%/*}# ''
${file%.*}# /home/jeffery/readme.md
${file%%.*}# /home/jeffery/readme
子串截取
# 字符串位置从0开始计数

${#file} # 获取字符串的长度
${file:0:5} # 从第0个字符起截取5个字符,/home
${file:5:5} # 从第5个字符起截取5个字符,/jeff

# 以下三个均表示截取最后四个字符
${file: -4}
${file:0:4}
${file:(-4)}

字符串替换

${file/str1/str2}  # 将字符串file中的第一个str1替换为str2 
${file//dir/path} # 将字符串file中的str1替换为str2

大小写转换

echo "$file"
echo ${file^}
echo ${file^^}
echo ${file,}
echo ${file,,}
echo ${file~}
echo ${file~~}

# ^大写,,小写, ~大小写切换, 重复一次只修改首字母,重复两次则应用于所有字母

数组

普通数组

my_array=(A B "C" D) # 定义数组
declare -a my_array2  # 定义数组方法2
my_array2[0]=B

echo ${my_array[0]}  # 获取数组元素
echo ${my_array[*]} # 获取所有元素
echo ${my_array[@]} # 获取所有元素

echo ${#my_array[*]} # 获取数组长度
echo ${#my_array[@]} # 获取数组长度

unset my_array[0]  # 删除第一个元素

echo ${my_array[@]:0:2}  # 切片操作,从0开始取值,取两个

for v in ${my_array[@]}; do  # 数组遍历
	echo $v;
done

my_array+=(a b c)  # 往数组中添加元素

关联数组

  关联数组类似于python,C#等语言中的DICTIONARY类型

 declare -A dict  # 定义关联数组
 dict=([c]=3 [d]=4)  # 赋值
 dict+=([a]=1 [b]=2)  # 向关联数组添加键值对
 echo ${!dict[@]} # 获取所有索引
 echo ${dict[a]}  # 通过key获取value

注意事项

  注意区分以下符号的使用,具体可见上文:

  • 引用变量-----> $DATE${DATE}
  • 命令代换----> `date` 或 $(date)
  • 算术代换---->$(())$[]
  • 逻辑运算符 !-a -o!&& ||
  • 条件测试 [][[]]
       如果a为空,那么[$a -eq 0]会报错,但是[[$a -eq 0]]不会,所以一般都会使用[[]]或者是["$a" -eq 0]

猜你喜欢

转载自blog.csdn.net/jeffery0207/article/details/86554754