Advanced Bash-Scripting Guide学习笔记-变量、参数、引用、判断

一、前言

前面我们学习了<<The Linux Command Line>>这本书,从中学习了一些常用的linux命令以及基本的shell编程语法,比如流程、数组、变量展开、数字和字符串操作等,会写一些基本的shell脚本了,但是还有些地方非常模糊,比如[[]]和[],()和(())区别,比如如何通过选项去将脚本写的更加具有通用性和可读性等等,所以就去准备找一本更专业的书籍更系统全面的学习shell编程,这次的笔记会严格按照书中的章节顺序来写,当然对于已经在上一本书籍中学习过的知识就不再详细描述,尽量做到不重复,同时常见熟悉的知识点就不记录了,主要记录不常见易出错的知识点,我的系统环境是centos7,代码编辑使用原生的vi和vim,所以在学习这本书籍的时候会顺带着学习vi和vim这两个编辑工具

二、开始

2.1.热身

下面截取书中的第一个示例脚本,该脚本的功能是清除日志文件的内容
cat logclear.sh

#!/bin/bash

# 要清除的日志文件所处的目录
LOG_DIR=/var/log

# 根用户的UID
ROOT_UID=0

# 清除日志并不是要完全清除,最好还是保留最新的日志信息
LINES=50

# 不能修改目录 
E_XCD=66

# 非根用户以error退出
E_NOTROOT=67

# 判断脚本运行的用户是否为根用户
if [ "$UID" -ne 0 ];then
    echo "Must be root to run this script."
    exit $E_NOTROOT
fi

# 测试是否有参数
if [ -n "$1" ];then
    lines=$1
else
    # 如果没有命令行参数,默认值等于LINES的值
    lines=$LINES  
fi

# 对于是否有参数还有一种更优的写法
#E_ERRORARG=65
#case "$1" in
#"") lines=$LINES;;
#*[!0-9]*) echo "Usage: $(basename $0)";exit $E_ERRORARG;;
#*) lines=$1
#esac


# 确保切换到了日志目录
#cd $LOG_DIR
#if [ $(pwd) != "$LOG_DIR" ];then
 #   echo "Can not change to dir."
  #  exit $E_XCD
#fi

# 上面更优的写法可以这样写
 cd /var/log || { echo "Can not change to dir." >&2; exit $E_XCD;}

# 保存消息最近的一部分
tail -n $lines messages > mesg.temp
mv mesg.temp messages

exit 0

这个脚本的本身很简单,但是里面有一些写脚本的的要素我们需要掌握,脚本中有三个判断需要注意:

  • 判断当前运行脚本的用户是否是root
  • 判断是否有位置参数,位置参数是数字还是字符,不同的情况有不同的操作
  • 判断是否在日志文件所处的目录中

2.2.一些疑点

  • 最好不要使用sh来执行脚本,sh调用脚本会关闭一些bash的扩展功能
  • 为什么不能直接运行脚本呢,而必须使用./scriptname,这是因为出于安全考虑,当前目录并没有加入环境变量

三、特殊字符

这里只把不常见的容易犯错的特殊字符记录一下
#
标准的引用或者\能够对#进行转义
echo "The # here does not begin a comment."
echo 'The # here does not begin a comment.'
echo The # here does not begin a comment.
上面三者输出的结果一样,都是The # here does not begin a comment.
'string' 全引用,阻止了了全部特殊字符
"string" 部分引用,阻止了部分特殊字符
` 后置引用
: 空命令,该命令是一个shell的内置命令,返回一个0,就是shell返回一个true
!取反操作
* 通配符,注意在shell里使用来匹配文件名,但是在正则表达式中有不同的意义
$ 正则表达式中作为结束行符
$$ 运行脚本的pid
() 小括号相当于开启一个子shell,里面的变量值不能再全局使用
{} 这里有一个大括号扩展,可以用来数组初始化,也能进行文件拼接
a={a b c}等同于a=(a b c)
cat {file1,file2,file3} > file
cp file22{txt,yyy}拷贝文件file22txt到filleyyy目录中

3.1.代码块和重定向

{local a;a=123;}该代码块里的a在外面也是可以被识别的
代码块还可以重定向到文件,下面是一个脚本例子,该脚本使用来判断是否可以安装某个rpm包
cat test.sh

#!/bin/bash

SUCCESS=0
E_NGORE=65
# 首先判断是否存在
if [ -z "$1" ];then
    echo "Usage:$(basename $0) rpm-file"
    exit $E_NGORE
fi   
{
rpm -qpi $1 # 查看包的说明   
rpm -qpl $1  # 查看包的列表
rpm -i --test $1 # 查询包是否可以安装   
if [ $? -eq "$SUCCESS" ];then
    echo "The package $1 could be installed."
else
    echo "The package $1 could not be installed."
fi
} > $1.txt   

exit 0

{}不能开启新shell,()可以开启新shell
{} ; 该特殊字符串主要用于find命令的-exec选项
&> 重定向标准错误和标准输出,等同于2>&1
[] 数组元素 Array[1]=slot_1 echo ${Array[1]}
[] 字符范围 正则表达式中使用,作为字符匹配的范围
(()) 数学计算的扩展
command &>filename 重定向stdout和stderr到文件中
commadn >&2 重定向command的stdout到stderr
<> ASCII比较 例如if [[ "$a" > "$b" ]]
<> 正则表达式中的单词边界<the>
| 管道符 echo ls -l | bash 该命令等同于ls -l ;cat *.list | sort | uniq ,管道除了了可以在命令之间传递,还可以在命令与脚本之间进行传递
cat test.sh

#!/bin/bash

tr 'a-z' 'A-Z'  
exit 0

ls -l | bash test.sh 该命令结合脚本可以将小写全部替换成大写输出,但并不会改变原数据的小写形态,只是输出变为大写

>| 强制重定向

|| 或
& 后台运行,在一个脚本中循环或命令都可以在后台运行,看下面一个例子
cat 1.sh

#!/bin/bash

for i in 1 2 3 4 5
do
    echo -n $i
done&

echo 

for p in 10 9 8 7 6 
do
    echo -n $p
done

echo

exit 0

输出结果
[root@test ~]# bash 1.sh

109876
12345[root@test ~]#
这里想一想为什么第二个echo没有生效

&& 与操作
- 用于重定向stdin/stdout,-常用于管道后面的命令
ex1:
cat 2.sh

#!/bin/bash

(cd /root/source && tar cf - .) | (cd /root/directory && tar xpvf -)

该脚本等同于
cp -a /root/source/* /root/directory

ex2:备份当前目录最后一天所有被修改过的文件
cart 3.sh

#!/bin/bash

backfile=backup-$(date %Y%m%d)
archive=${1:-backfile}
tar cf - $(find ./ -mtime -1 -type f -print) > ${archive}.tar
gzip ${archive}.tar
echo "Directory $PWD backed up in archive file \"${archive.tar}\""

其实上面的写法也是有漏洞的,如果找到的文件过多或者文件名有空格就会报错,下面提供两种更优的写法:
cat 4.sh

#!/bin/bash

backfile=backup-$(date %Y%m%d)
archive=${1:-backfile}
find . -type f -mtime -1 -exec tar rvf ${archive}.tar {} \;
gzip ${archive}.tar
echo "Directory $PWD backed up in archive file \"${archive.tar}\""

cat 5.sh

#!/bin/bash

backfile=backup-$(date %Y%m%d)
archive=${1:-backfile}
find . -type f -mtime -1 -print0 | xargs -0 tar rvf ${archive}.tar
gzip ${archive}.tar
echo "Directory $PWD backed up in archive file \"${archive.tar}\""

这里我们利用了 - 的重定向功能,但是有一点需要注意,如果某个文件名是以-开头,这时候最好加上前缀来区分,比如
./-filename $PWD/-filename

- 算术减号
+ 算术加号
+ 打开选项
= 算术等号
% 算术取模运算,也用于正则
~ 当前用户home目录
~+ 当前工作目录
~- 之前工作目录
=~ 用于正则
^ 行首

控制字符
ctrl + b 光标后退
ctrl + h 删除光标前面的字符
ctrl + k 删除光标到末尾的字符
ctrl + l 清屏
ctrl + u 删除光标到行首的所有字符
ctrl + c/z 终止前台工作
ctrl + w 删除光标到前面最近一个空格之间的字符

tip:
大括号代码段无法接受管道输出,比如:
ls | { read firstline;read secondline; }执行会报错,可以改成

3.2.变量和参数

变量替换
$ 变量替换操作符
变量赋值替换
变量赋值前后不要有空格
ex1: $hello ${hello} "$hello" "${hello}"区别
hello="A B C D"
echo $hello输出
A B C D
echo "$hello"输出
A B C D
这里的区别是前者为变量替换,而后者叫部分引用,但是部分引用里也会发生替换
echo '$hello'输出
$hello,这叫全引用
echo "$hello (null value) = $hello" 输出
$hello (null value) = A B C D
如果hello=
echo "$hello (null value) = $hello"输出
$hello (null value) =

unset可以取消赋值的变量
a=20
unset a
echo $a返回空值

变量赋值
= 前后不要有空白,注意这个与-eq不一样,-eq是test
ex1:

#!/bin/bash

echo -n "value of \"a\" is:"
read a
echo -n "value of \"a\" is $a"
echo
exit 0

ex2:

#!/bin/bash

a=`echo hello!`
echo $a

上面的命令扩展里可以直接使用!,但是如果在命令行使用则需要使用!

ex3:

#!/bin/bash

a=`ls -l`
echo $a   
echo 
echo "$a" 
exit 0

结果分别如下
08bbc
其实``是可以使用()来替代的,它们均是另外一种面变量赋值的方式,或者说是命令替换

Bash的变量类型
bash的变量是无类型的,可以自由转化
ex1:

#!/bin/bash

a=1234
echo "a=$a"   整型   
b=${a/12/bb}  
echo "b=$b"   字符串型   
declare -i b   
echo "b=$b"    仍然是字符串型   

let "b += 1"   
echo "b=$b"   # 1  整型   

c=""   # 空变量   
echo "c=$c"   # 空值   
let "c += 1"  # 算数操作允许空变量   
echo "c=$c"  # 1 整型   

echo "d=$d"  # d没有声明   
let "d += 1"  
echo "d=$d"   # 1 整型   

特殊的变量类型
local variables 脚本代码里面或函数里面
environment variables 环境变量,什么叫环境变量?(当shell启动,会创建环境变量,更新或添加新的环境变量会导致shell更新自己的环境,也会影响继承这个环境的所有子进程)
positional arguments
$0~$9位置参数,大于10用${10}..
[ -n "$1" ] 判端$1是否被引用
[ -z $1 ] 判断$1长度是否为0
ex: 查看server信息

#!/bin/bash

E_NOARGS=65
[ -z "$1" ] && { echo "Usage: $(basename $0) args";exit ${E_NOARGS}; }

cp $(pwd)/wh /usr/local/bin

case $(basename $0) in
"wh")    whois $1.com;;
"wh-a")  whois $1.com;;
"wh-b")  whois $1.com;;
"wh-c")  whois $1.com;;
*)     echo "Usage:$(basename $0) args";;
esac

bash wh linuxwt

Domain Name: LINUXWT.COM
   Registry Domain ID: 2147305270_DOMAIN_COM-VRSN
   Registrar WHOIS Server: grs-whois.hichina.com
   Registrar URL: http://www.net.cn
   Updated Date: 2019-07-05T05:41:36Z
   Creation Date: 2017-07-27T05:03:49Z
   Registry Expiry Date: 2020-07-27T05:03:49Z
   Registrar: Alibaba Cloud Computing (Beijing) Co., Ltd.
   Registrar IANA ID: 420
   Registrar Abuse Contact Email: DomainAbuse@service.aliyun.com
   Registrar Abuse Contact Phone: +86.95187
   Domain Status: ok https://icann.org/epp#ok
   Name Server: DNS11.HICHINA.COM
   Name Server: DNS12.HICHINA.COM
   DNSSEC: unsigned
   URL of the ICANN Whois Inaccuracy Complaint Form: https://www.icann.org/wicf/

shift
shift可以重新分配位置参数,当你使用了超过0个位置参数你就可以使用这个来简化你的脚本
$1 < --- $2 ,$2 < --- $2等,老的$1消失,但是$0是永远不会变的
cat shift

#!/bin/bash

until [ -z "$1" ]
do
    echo -n "$1"
    shift
done

四、引用

前面说过了$a和"$a"的区别了,引号的作用就是保证变量的值中的特殊字符不被shell重新扩展,日常生活中引号中的内容是有特殊含义的,但在shell里引号里的内容是相反的,是表示其字面含义的。
转义符
以下转义特殊例子,并没有将其转义到原始意思
\n 换行
\r 回车
\t TAB
echo -e "a\tb" 输出 a b
也可以使用echo $'a\tb' 会输出同样的结果
echo hello! 输出 hello!
echo "hello!" 输出 hello!
转义字符\可以将特殊字符关闭输出其本义,但是有些时候echo sed等命令使用转义字符会起到相反的作用,比如
echo "\042"输出 \042
echo -e "\042" 输出引号"
echo $'\042'输出引号

"表示引号本身
$表示$本身,$后的变量将不能被扩展
\表示\本身

\的作用在于它被运用的情况,一般分为以下几种情况
是否被转义
echo \z 输出 z
echo \z 输出\z
echo '\z'输出\z
echo '\z'输出\z
echo "\z"输出\z
echo "\z"输出\z

是否被引号
是否在命令替换
是否在here document

\有时候作为续行符在shell脚本里使用的时候需要注意,下面几个例子作为参考
echo foo\
bar
输出foobar
echo "foo
bar"
输出foobar
echo 'foo
bar'
输出foobar
echo "foo
bar"
输出foobar
echo 'foo\
bar'
输出foo\
bar

五、退出

在Unix里执行一个脚本或脚本函数最后都会返回一个exit状态,值为0这个exit也叫退出码,一般脚本结尾会以exit结束,如果不加exit,会以脚本最后的命令来决定退出码是多少,0为成功,非0为失败,1表示失败,其他数值都有其特殊的意义,不应当随意指定退出码的值

六、Test

有一个if-grep结构
vi if-grep.sh

#!/bin/bash

word="123abc"
WORD="123"
if echo "$word" | grep -q "$WORD";then
    echo "$WORD found in $word"
else
    echo "$WORD not found in $word"
fi

-q表示阻止输出
test /usr/bin/test [] /usr/bin/[都是用来测试文件类型和比较字符串,其中test与[是内建命令,]是[的结尾,/usr/bin/[与/usr/bin/test是二进制文件,它们都可以达到同样的意思,除此以外还有一个test扩展[[]],它主要是为了照顾其他程序员设定的,是一个关键字

type test
内建命令
type /usr/bin/test
二进制文件
type '['
内建命令
type ']'
not found
type '[['
关键字
type ']]'
关键字
type /usr/bin/[
二进制文件

这几个命令是等效的,下面例子来说明
cat example1.sh

#!/bin/bash

if test -z "$1";then
    echo "no command-line argument"
else
    echo "First command-line argument is $1"
fi

cat example2.sh

#!/bin/bash

if [ -z "$1" ];then
    echo "no command-line argument"
else
    echo "First command-line argument is $1"
fi

cat example3.sh

#!/bin/bash
if [[ -z "$1" ]];then
    echo "no command-line argument"
else
    echo "First command-line argument is $1"
fi

cat example4.sh

#!/bin/bash
if /usr/bin/test -z "$1";then
    echo "no command-line argument"
else
    echo "First command-line argument is $1"
fi

cat example5.sh

#!/bin/bash
if /usr/bin/[ -z "$1" ];then
    echo "no command-line argument"
else
    echo "First command-line argument is $1"
fi 

[[]]相比较[]可以通过在里面使用&& || < >避免很多逻辑错误,其实理论上来讲,在使用if的时候不需要必须加上test []或[[]]
example6.sh

#!/bin/bash


dir="/tmp"
if cd "$dir" 2>/dev/null;then
    echo "Now in $dir"
else
    echo "can not change to $dir"
fi

exit 0

if命令返回if后面命令的退出码

(())结构扩展并计算一个算数表达式的结果,如果表达式结果为0,它将返回1作为退出码,如果结果为非0,返回0作为退出码
cat example7.sh

#!/bin/bash

(( 0 ))
echo "Exit status of \"(( 0 ))\" is $?"   //输出1

(( 1 ))   
echo "Exit status of \"(( 0 ))\" is $?" //输出0

文件测试操作
-e 文件是否存在
-f 文件是否是常规文件。非目录或设备文件
-d 文件是否是目录
-b 文件是否是块设备文件
-c 文件是否是字符设备文件
-S 文件是否是socket文件
-r 文件是否对当前用户可读
-w 文件是否对当前用户可写
-x 文件是否对当前用户可执行
-h 文件是否是链接文件
file1 -nt file2 文件1比文件2新
file1 -ot file2 文件1比文件2旧
! 反转测试的结果,如果无测试条件,返回true

cat example8.sh

#!/bin/bash

##################检查目录下的软连接文件####################
#################如果无位置参数传入,默认为当前目录#########
[ $# -eq 0 ] && directorys=$(pwd) || directorys=$@

############################################################
#函数传入目录参数,输出软链接文件,并用引号引用,如果目录下面
#存在 子目录,调用函数进行递归检查
############################################################     
linkchk () {
for dir in $1/*
do
    [ -h "$dir" -a -e "$dir" ] && echo \"$dir\"
    [ -d "$dir" ] && linkchk $dir
done
}

####################循环传入目录检查########################
for directory in $directorys
do
    [ ! -d "$directory" ] && echo "$directory is not directory.
Usage: $0 dir1 dir2." || linkchk $directory
done

比较操作-整数比较
[ "$a" -eq "$b" ] 相等
[ "$a" -ne "$b" ] 不等
[ "$a" -gt "$b" ] 大于
[ "$a" -ge "$b" ] 大于等于
[ "$a" -lt "$b" ] 小于
[ "$a" -le "$b" ] 小于等于
(( "$a" > "$b" )) 大于
(( "$a" < "$b" )) 小于
(( "$a" >= "$b" )) 大于等于
(( "$a" <= "$b" )) 小于等于

比较操作-字符串比较
[ "$a" = "$b" ] 等于
[ "$a" == "$b" ] 与=等价
[[]]与[]中==的行为是不同的
[[ $a == z* ]] 模式匹配,匹配以z开头的字符串
[[ $a == "z*" ]] 字符匹配,$a等于z*
[ "$a" != "$b" ] 不等于,但是该操作如果在[[]]中必须使用模式匹配
[ "$a" < "$b" ] 小于,ascii码比较
[ "$a" > "$b" ] 大于
[[ "$a" < "$b" ]] 小于
[[ "$a" > "$b" ]] 大于
-z 测试字符串长度为0,即为null
-n 测试字符串不为null,注意字符串必须用""引用起来避免报错

cat example9.sh

#!/bin/bash

a=1
b=2
[ "$a" -eq "$b" ] && echo 123 || echo 456

其实上面这个脚本a和b既可以是数字也可以是字符串,bash里没有强类型
所以可以用下面的方式
[ "$a" = "$b" ] && echo 123 || echo 456

cat example10.sh

#!/bin/bash

[ -n $str ] && echo "Str is not null" || echo "Str is null"

bash example10.sh输出Str is not null

cat example111.sh

#!/bin/bash

[ -n "$str" ] && echo "Str is not null" || echo "Str is null"

bash example11.sh输出Str is null
脚本example10.sh中没有初始化str,但是最后输出的结果却是非空,所以最好用""引用起来可以避免类似的错误

cat example12.sh

#!/bin/bash

[ $# -eq 0 ] && { echo "Usage:$(basename $0) filename" 2>&1; } 

filename=$1

[ ! -f "$filename" ] && { echo "not found file $filename";} 

[ ${filename##*.} != "gz" ] && echo "$filename is not gzipped file"

zcat $1 | more

exit $?