The linux command line学习笔记

一、前言

现在人们学习很多都是网上看视频,即使是看书也是看电子书了,往往我们看了就很少去做笔记,查起来也是很麻烦,这本书是一本初级学习linux命令的书籍,个人觉得很不错,给我的工作带来了很多帮助,为此将书中的一些认为比较好的知识点按照原书章节记录在此,章节顺序是想到哪写到哪。

二、章节笔记

2.1.正则表达式应用

该章节以grep命令中正则表达式的应用为例呈现其基本用法
grep语法:
grep [options] regex filename
上面的regex就表示正则表达式
下面举一些grep带上不同的选项的一些查询语句
grep -l bzip dirname 查询出具有匹配项的文件名,对匹配项本身不关心
grep -L bzip dirname 查询出不具有匹配项的文件名,对匹配项本身不关心
上面的bzip本身其实也是一种正则表达式,是最为简单的正则表达式,该字符属于原义字符,除了原义字符外,还有元字符,正则表达式中的元字符包括以下字符:

^ $ . [ ] { } - ? * + ( ) | \

注意:在命令行中传递带有元字符的正则表达式的时候为了防止shell展开最好将正则表达式用引号引起来。

2.1.1.任何字符

圆点字符匹配在其位置的任意一个字符
grep -h '.zip' filename 查找包括匹配行的文本行,不关心文件,只关心匹配项所在的文本行
注意:这里的正则表达式.zip表示四个字符,zip不符合匹配项

2.1.2.锚点

grep -h '^zip' filename 表示匹配以zip开头的文本行
grep -h 'zip$' filename 表示匹配以zip结尾的文本行
grep -h '^zip$' filename 表示匹配zip
grep -h '^$' filename 表示匹配空行

2.1.3.中括号表达式

grep -h '[bg]zip' filename 表示匹配包含bzip或者gzip的文本行
一个字符集合可能包含任意多个字符,并且元字符被放置到中括号里面后会失去了它们的特殊含义。 然而,在两种情况下,会在中括号表达式中使用元字符,并且有着不同的含义。第一个元字符 是插入字符,其被用来表示否定;第二个是连字符字符,其被用来表示一个字符范围

2.1.4.否定

grep -h '[^bg]zip' filename 表示匹配不包含bzip或者gzip的文本行,注意zip这里也不符合要求
比较一下这个例子:
grep -h '^[cd]zip' filename 表示匹配以bzip或者czip开头的文本行
grep -h '[A-Z]' filename 表示匹配含有写字母的文本
那么如何使得连字符-维持其原有的意义呢,看下面这个例子:
grep -h '[-AZ]' filename 表示匹配包含一个连字符或者大写A或者大写Z的文本行

2.1.5.交替

扩展表达式的第一个特性叫做alternation,允许从一系列表达式之间选择匹配项
echo "AAA" | grep -E 'AAA|BBB|CCC'
grep -Eh '^(ab|cd|zip)' filename 表示匹配以ab开头或者cd开头或者zip开头的文本行

2.1.6.限定符

? 匹配零个或一个元素
这里有一个判断是否是电话号码的例子:
echo "123 4567890" | grep -Eh '\(?[0-9][0-9][0-9]\)? [0-9][0-9][0-9][0-9][0-9][0-9][0-9]$'
* 匹配零个或多个元素
echo "This works." | grep -E '[[:upper:]][[:upper:][:lower:] ]*\.' filename表示匹配包含大写字母小写字母以及空格和点号的文本行
+ 匹配一个或者多个元素
与 * 非常相似,除了它要求前面的元素至少出现一次匹配。这个正则表达式只匹配 那些由一个或多个字母字符组构成的文本行,字母字符之间由单个空格分开,看以下几个例子:

echo "This that" | grep -E '^([[:alpha:]]+ ?)+$'
echo "a b c" | grep -E '^([[:alpha:]]+ ?)+$'
echo "a b 9" | grep -E '^([[:alpha:]]+ ?)+$'
echo "abc  d" | grep -E '^([[:alpha:]]+ ?)+$'

^([[:alpha:]]+ ?)+$这个正则表达式不匹配“a b 9”这一行,因为它包含了一个非字母的字符;它也不匹配 “abc d” ,因为在字符“c”和“d”之间不止一个空格
{ } - 匹配特定个数的元素

  • {m}匹配前面的元素,如果出现了m次
  • {n,m}匹配前面的元素,如果出现的次数大于等于n,小于等于m
  • {n,}匹配前面的元素,如果出现的次数大于等于n
  • {,m}匹配前面的元素,如果出现的次数小于等于n
    前面有个判断电话号码的例子
    echo "(123) 4567890" | grep -Eh '^\(?[0-9][0-9][0-9]\)? [0-9][0-9][0-9][0-9][0-9][0-9][0-9]$'
    可以写成这样
    echo "123 4567890" | grep -Eh '^\(?[0-9]{3}\)? [0-9]{7}$'
    那么在实际生活中怎么去利用正则表达式呢,下面还是以这个电话号码的正则表达式来举例子,首先创建一个有10个电话号码的电话簿,但是里面的电话号码有些是符合电话号码规则,有些不是
    for i in {1..10};do echo "(${RANDOM:0:3}) ${RANDOM:0:3}-${RANDOM:0:4}" >> phonelist.txt;done
    我们需要从文件中筛选出不符合电话号码规则的行
    grep -Ev '^\(?[0-9]{3}\)? [0-9]{3}-[0-9]{4}$' phonelist.txt

2.2.流程控制与循环

2.2.1.if分支

if分支的结构如下所示

if commands;then
    commands
elif commands;then
    commands
else
    commands
fi

那么shell是如何判断命令的成功和失败的呢?这里就要说到退出状态,退出状态是一个值,其范围是0-255,0表示成功,其他值表示失败,shell提供了两个内部命令true和false,执行后分别会返回1和0
来看几个例子:
if true;then echo "it is true";fi 该结果是it is true
if false;then echo "it is true";fi 无结果,因为false为1,程序直接就退出了
再看几个例子:
if false;true;then echo "It's true.";fi 该结果是It's true.
if true; false; then echo "It's true."; fi 无结果返回
当if后跟很多命令的时候,最后一条命令其决定作用,以上的true和false直接理解成退出状态即可
if在shell里常与文件表达式配合来判断文件的状态,下面我们用一个脚本来简单介绍一下几个常用的文件表达式

#!/bin/bash
# test-file: Evaluate the status of a file
FILE=~/.bashrc
if [ -e "$FILE" ];then //-e判断是否存在这个文件
    if [ -f "$FILE" ];then
        echo "$FILE is a regular file"
    fi
    
    if [ -d "$FILE" ];then
        echo "$FILE is a directory"
    fi
    
    if [ -r "$FILE" ];then
        echo "$FILE is readable"
    fi
    
    if [ -w "$FILE" ];then
        echo "$FILE is writable"
    fi
    
    if [ -x "$FILE" ];then
        echo "$FILE is executable"
    fi
    
else
    echo "$FILE does not exist"
    exit 1
fi  
exit  

着重说一下最后的exit,exit后面不带参数,默认退出状态为0,如果这个命令出现在脚本的最后一行,默认退出状态以0终止,但是上面这个脚本如果判断出文件不存在,直接就以1退出了,不会运行最后一行的exit
如果我们把上面的shell脚本里的内容变成一个函数,同时要求当目标文件不存在的时候返回状态值1

test_file () {
if ...
...
...
else
    echo "$FILE does not exist"
    return 1
fi
}

除了判断文件表达式以外,shell里也经常涉及到字符串表达式的判断

#!/bin.bash

# test string : evaluate the value  of a string
ANSWER="maybe"
if [ -z ${ANSWER} ];then
    echo "there is no answer." >&2
    exit 1
fi
if [ ${ANSWER} == "yes" ];then
    echo "the answer is yes."
elif [ ${ANSWER} == "no" ];then
    echo "the answer is no."
elif [ ${ANSWER} == "maybe" ];then
    echo "the answer is maybe."
else
    echo "the answer is unknown."
fi

下面再说一说shell里的整型表达式,同样我们以一个例子来说明一下

#!/bin/bash

INT=-5
if [ -z ${INT} ];then
    echo "the INT is empty."
    exit 1
fi
if [ ${INT} -eq 0 ];then
    echo "the INT is zero."
else 
    if [ ${INT} -lt 0 ];then
        echo "the INT is negative."
    else
        echo "the INT is positive."
    fi
    if [ $((INT % 2)) -eq 0 ];then
        echo "the INT is even."
    else
        echo "the INT is odd."
    fi
fi

上面的脚本有一定的局限性,比如INT如果使非数字的字符串就会导致脚本出错,下面给出修改后的脚本

#!/bin/bash

INT=-5
if [[ ${INT} =~ ^-?[0-9]+$ ]];then
    if [ ${INT} -eq 0 ];then
        echo "the INT is zero."
    else
        if [ ${INT} -lt 0 ];then
            echo "the INT is negative."
        else 
            echo "the INT is positive."
        fi
        if [ $((INT % 2)) -eq 0 ];then
            echo "the INT is even."
        else
            echo "the INT is odd."
        fi
    fi
else
    echo "the INT is not an integer."
    exit 1
fi

[[]]也支持操作符==的类型匹配

#!/bin/bash

FILE=foo.bar
if [[ ${FILE} == foo.* ]];then
    echo "the FILE mathes patern foo.*"
fi

shell里除了提供了[[]]复合命令,也提供了(())符合命令,前面判断INT是否是偶数的时候已经使用过了,其实(())更方便用来操作整数,上面的脚本可以写成下面这样

#!/bin/bash

INT=-5
if [[ ${INT} =~ ^-?[0-9]+$ ]];then
    if ((INT == 0));then
        echo "INT is zero"
    else
        if ((INT < 0));then
            echo "INT is negative"
        else
            echo "INT is postive"
        fi
        if (( ((INT % 2)) == 0));then
            echo "INT is even"
        else
            echo "INT is odd"
        fi
    fi
else
    echo "INT is not aninteger"
    exit 1
fi

有一点要注意,shell里(())只能处理整数,所以它能够识别出INT而不需要去展开
上面说了文件表达式、字符串表达式和整型表达式,还有一种更为复杂的结合表达式
所用的操作符有三种:

  • &&
  • ||

  • 下面给一个&&的例子
#!/bin/bash

MIN_VAL=1
MAX_VAL=100
INT=50
if [[ ${INT} =~ ^-?[0-9]+$ ]];then
    if [[ INT -ge MIN_VAL && INT -LE MAX_VAL ]];then
        echo "$INT is within $MIN_VAL to $MAX_VAL"
    else
        echo "$INT is out of range"
    fi
else
    echo "INT is not an integer" >&2
    exit 1
fi

也可以写成这样

#!/bin/bash

MIN_VAL=1
MAX_VAL=100
INT=50
if [[ ${INT} =~ ^-?[0-9]+$ ]];then
    if (( ((INT > MIN_VAL)) && ((INT < MAX_VAL)) ));then
        echo "$INT is within $MIN_VAL to $MAX_VAL"
    else
        echo "$INT is out of range"
    fi
else
    echo "INT is not an integer" >&2
    exit 1
fi

也可以这样写

#!/bin/bash
# test-integer3: determine if an integer is within a
# specified range of values.
MIN_VAL=1
MAX_VAL=100
INT=50
if [[ "$INT" =~ ^-?[0-9]+$ ]]; then
    if [ $INT -gt ${MIN_VAL} -a $INT -lt ${MAX_VAL} ]; then
        echo "$INT is within $MIN_VAL to $MAX_VAL."
    else
        echo "$INT is out of range."
    fi
else
    echo "INT is not an integer." >&2
    exit 1
fi

我么也可以对表达式进行(),如下面的例子

#!/bin/bash
# test-integer3: determine if an integer is within a
# specified range of values.
MIN_VAL=1
MAX_VAL=100
INT=50
if [[ "$INT" =~ ^-?[0-9]+$ ]]; then
    if [ ! \( $INT -gt ${MIN_VAL} -a $INT -lt ${MAX_VAL} \) ]; then
        echo "$INT is not  within $MIN_VAL to $MAX_VAL."
    else
        echo "$INT is out of range."
    fi
else
    echo "INT is not an integer." >&2
    exit 1
fi

注意\(后面和前面必须有空格, 因为test 使用的所有的表达式和操作符都被 shell 看作是命令参数(不像 [[ ]] 和 (( )) ), 对于 bash 有特殊含义的字符,比如说 <,>,(,和 ),必须引起来或者是转义
分支的另一种方法
command1 && command2 该语句的意思是只有命令1执行成功才会执行后面的语句
commadn1 || commadn2 该语句的意思是只有命令1执行失败才会执行后面的语句
举两个例子
mkdir /temp && cd /temp
[ -d /temp ] || mkdir /temp
[ -d /temp ] || exit 1
构造类型非常有助于在脚本中处理错误

2.2.2.for循环

for循环与while循环不一样,它提供了处理序列的方式,这在shell编程里非常实用
在bash里有两种for循环格式
传统shel格式
语法格式:

for variable [in words]
do
    commands
done

for命令的强大之处在于可以通过多种方式来来创建words列表,下面简单介绍一下
花括号创建
for i in {A..D}
路径名展开
for in in dir*.txt
命令替换
for i in $(seq 1 5)

找出文件中长度最长的单词

#!/bin/bash

# find the longest words
while [[ -n $1 ]]
do
    if [[ -r $1 ]];then
        max_word=
        max_len=0
        for i in $(strings $1)
        do
            len=$(echo $i | wc -c)
            if ((len > max_len));then
                max_len=$len
                max_word=$i
            fi
        done
        echo "$1:'$max_word' ($max_len characters)"
    fi
    shift
done

strings 程序(其包含在 GNU binutils 包中),为每一个文件产生一个可读的文本格式的 “words” 列表。 然后这个 for 循环依次处理每个单词,判断当前这个单词是否为目前为止找到的最长的一个。当循环结束的时候,显示出最长的单词,注意[[ -n $1 ]]是测试$1是否为空
如果省掉for命令的words部分,for命令会默认去处理位置参数

#!/bin/bash

for i
do
    if [[ -r $i ]];then
        max_word=
        max_len=0
        for j in $(strings $i)
        do
            len=$(echo $j | wc -c)
            if ((len > max_len));then
                max_len=$len
                max_word=$j
            fi
        done
        echo "$i:'$max_word' ($max_len characters)"
    fi
done

C语言格式
语法格式:

for (( expression1; expression2; expression3 ))
do
    commands
done

可以理解成下面这种形式

(( expression1 ))
while (( expression2 ))
do
    commands
    (( expression3 ))
done

下面是一个例子

for (( i=0; i<5; i=i+1 ))
do
    echo $i
done

expression1 初始化变量 i 的值为0,expression2 允许循环继续执行只要变量 i 的值小于5, 还有每次循环迭代时,expression3 会把变量 i 的值加1

2.2.3.while/until循环

while语法格式:

while commands
do
    commands
done

看一个简单的例子

#!/bin/bash

count=1
while [ $count -lt 5 ]
do
    echo $count
    count=$((count + 1))
done

也可以写成

#!/bin/bash

count=1
while [ $count -lt 5 ]
do
    echo $count
    count=$(expr $count + 1)
done

和if一样,while也会计算一系列命令的退出状态,只要退出状态为0,它就执行循环体内的内容
下面我们来学习一个经典的shell脚本

#!/bin/bash
# while-menu: a menu driven system information program
DELAY=3 # Number of seconds to display results
while [[ $REPLY != 0 ]]; do
    clear
cat <<EOF
Please Select:
1. Display System Information
2. Display Disk Space
3. Display Home Space Utilization
0. Quit
EOF
    read -p "Enter selection [0-3] > "
    if [[ $REPLY =~ ^[0-3]$ ]]; then
        if [[ $REPLY == 1 ]]; then
            echo "Hostname: $HOSTNAME"
            uptime
            sleep $DELAY
        fi
        if [[ $REPLY == 2 ]]; then
            df -h
            sleep $DELAY
        fi
        if [[ $REPLY == 3 ]]; then
            if [[ $(id -u) -eq 0 ]]; then
                echo "Home Space Utilization (All Users)"
                du -sh /home/*
            else
                echo "Home Space Utilization ($USER)"
                du -sh $HOME
            fi
            sleep $DELAY
        fi
    else
        echo "Invalid entry."
        sleep $DELAY
    fi
done
echo "Program terminated."

上面的脚本有一点要特别注意,here-document语法里的内容必须顶格写,否则会报错

warning: here-document at line 14 delimited by end-of-file (wanted `EOF')

菜单包含在 while 循环中,每次用户选择之后,我们能够让程序重复显示菜单。只要 REPLY 不 等于”0”,循环就会继续,菜单就能显示,从而用户有机会重新选择。每次动作完成之后,会执行一个 sleep 命令,所以在清空屏幕和重新显示菜单之前,程序将会停顿几秒钟,为的是能够看到选项输出结果。 一旦 REPLY 等于“0”,则表示选择了“退出”选项,循环就会终止,程序继续执行 done 语句之后的代码

跳出循环break和跳过本次循环continue
将上面的脚本做修改

#!/bin/bash

DELAY=3
while true
do
clear
cat <<EOF
Please Select:
1. Display System Information
2. Display Disk Space
3. Display Home Space Utilization
0. Quit
EOF 
    read -p "please select [0-3] >"
    if [[ $REPLY =~ ^[0-3]$ ]];then
        if [[ $REPLY == 1 ]];then
            echo "HOSTNAME: $HOSTNAME"
            uptime
            sleep $DELAY
            continue
        fi
        if [[ $REPLY == 2 ]];then
            df -h
            sleep $REPLY
            continue
        fi
        if [[ $REPLY == 3 ]];then
            if [[ $(id -u) -eq 0 ]];then
                echo "Home space Utilization (ALL Users)"
                du -sh /home/*
            else
                echo "Home space utilization ($USER)"
                du -sh $HOME
            fi
            sleep $DELAY
            continue
        fi
        if [[ $REPLY == 0 ]];then
            break
        fi
    else
        echo "invalid entry"
        sleep $DELAY
    fi  
done
echo "Program terminated."

该脚本是一个无限循环, true 的退出状态总是为零,所以循环永远不会终止,但是当选择”0”选项的时候,break 命令被用来退出循环。continue 命令被包含在其它选择动作的末尾, 来提高程序执行的效率。通过使用 continue 命令,当一个选项确定后,程序会跳过不需执行的其他代码
until循环与while循环相反,它遇到非0退出状态是不退出循环,反而执行循环里的内容,
语法格式为:

until commands
do
    commands
done

举个例子

#!/bin/bash

count=1
until [ $count -gt 5 ]
do
    echo $count
    count=$((count + 1))
done

用循环读取文件
while和until能够处理标准输入,这就可以使用while和until处理文件

#!/bin/bash

while read a b c
do
    printf "a: %s\tb: %s\tc:%s\n" \
    $a \
    $b \
    $c
done < test.txt

2.2.4.case分支

当if语句过多的时候,往往考虑使用case来代替if语句
我们修改前面的while-read脚本

#!/bin/bash

clear
echo "
please select:
1、display disk space
2、display system information
3、display home space utilization
0、quit
"
read -p "enter selection [0-3] >"
case $REPLY in
0) echo "program terminated."
   ;;
1) df -h
   ;;
2) echo "HOSTNAME:$HOSTNAME"
   ;;
3) if [ $(id -u) -eq 0 ];then
       du -sh /home/*
   else
       du -sh $HOME
   fi
   ;;
*) echo "invalid entry."
   exit 1
   ;;
esac

case结构有一个模式的概念,以)为终止符
常见的模式实例

  • a)
  • *.txt)
  • [[:alpha:]])
  • ???)
  • *) 默认执行的选项,表示前面的都无法匹配时执行该命令

模式里面可以引入竖线|,起一个的作用,这对于处理大小写字符特别方便,模式的书写类似于q|Q)

bash在4.0以前是无法匹配多个测试条件的,但是bash4.0以后就可以了,但是必须将结尾符;;修改为;;&

2.3.位置参数

2.3.1.$1-${n}

一个脚本如果想写的具有扩展性,其传参的功能是必须的,shell提供了一个位置参数的集合,通俗的说就是执行脚本后面带上一系列的位置参数,这些参数可以在脚本里起作用,分别用$1 $2..$9表示第一个至第九个位置参数,如果超过10了,写法为${10}之类的,这里还有两个比较特殊的参数$0 $#,它表示脚本本身和位置参数的个数,下面是一个脚本
cat ex.sh

#!/bin/bash

# example script
echo "
\$0 = $0
\$1 = $1
\$2 = $2
\$3 = $3
\$4 = $4
\$5 = $5
\$6 = $6
\$7 = $7
\$8 = $8
\$9 = $9
number of args is $#
"

如果该脚本路径为/root/ex.sh
执行bash ex.sh,输出
$0 = ex.sh
$1 =
$2 =
$3 =
$4 =
$5 =
$6 =
$7 =
$8 =
$9 =
number of args is 0
从上面看出维执行脚本时的位置参数个数为0个
如果执行/root/ex.sh,输出
$0 = /root/ex.sh
$1 =
$2 =
$3 =
$4 =
$5 =
$6 =
$7 =
$8 =
$9 =
number of args is 0
唯一的区别就是$0的值为脚本全路径
bash ex.sh ab c d,输出
$0 = ex.sh
$1 = ab
$2 = c
$3 = d
$4 =
$5 =
$6 =
$7 =
$8 =
$9 =
number of args is 3
位置参数个数为3
但是如果我们的位置参数非常多,那么怎么输出所有的参数呢,像上面这种写法就太臃肿了,不过shell提供了一个shift命令,每次执行它可以把$2参数的值传到$1上,$3的值传到$2上,当然$#的值也会减1,这样我们就可以通过一个循环来输出所有的位置参数,最终循环中止

#!/bin/bash

count=1
while [[ $# -gt 0 ]]
do
    echo "ags $count = $1"
    count=$((count + 1))
    shift
done

位置参数除了能在shell脚本中传参,也可以在shell函数中传参,什么意思呢,就是说如果某个脚本调用了某个函数,该脚本使用的位置参数在该函数中也是有效的
上面说的是单一的处理位置参数,位置参数也可以作为一个整体来进行处理,那么如何得到位置参数的列表呢,下面的脚本展示了四种方式获取位置参数列表,首先来看两个概念

  • $* 展开成一个从1开始的位置参数列表。当它被用双引号引起来的时候,展开成一个由双引号引起来 的字符串,包含了所有的位置参数,每个位置参数由 shell 变量 IFS 的第一个字符(默认为一个空格)分隔开
  • $@ 展开成一个从1开始的位置参数列表。当它被用双引号引起来的时候, 它把每一个位置参数展开成一个由双引号引起来的分开的字符串
#!/bin/bash

test1 () {
echo "\$1 = $1"
echo "\$2 = $2" 
echo "\$3 = $3"
echo "\$4 = $4"
}
test2 () {
echo -e "\n" '$* :'
test1 $*
echo -e "\n" '$* :' 
test1 "$*"
echo -e "\n" '$@ :' 
test1 $@
echo -e "\n" '$@ :'
test1 "$@"
}
test2 "wo" "shi wang teng"

执行脚本,输出
$* :
$1 = wo
$2 = shi
$3 = wang
$4 = teng

$* :
$1 = wo shi wang teng
$2 =
$3 =
$4 =

$@ :
$1 = wo
$2 = shi
$3 = wang
$4 = teng

$@ :
$1 = wo
$2 = shi wang teng
$3 =
$4 =
仔细观察,发现上面四种方式中只有"$@"可以完全复现真实的传参,保留了每一个位置参数的完整性

2.3.2.命令行选项添加

位置参数如果和命令行选项结合起来会使得我们的脚本更加具有可读性可用性,贴一段代码作为处理命令行选项的参考,反正具体的应用不一定这样写

#!/bin/bash

usage () {
    echo "$progname: usage: $progname [ -f file | -i ]"
    return
}

interactive=
filename=
while [[ -n $1 ]]
do
    case $1 in
    -f | --file)           shift
                           filename=$1
                           ;;
    -i | --interactive)    interactive=1
                           ;;
    -h | --help)           usage
                           exit
                           ;;
    *)                     usage >&2
                           exit 1
                           ;;
    esac
    shift
done

最后一个shift会使参数个数逐渐减少最终结束循环
上面的的代码里面涉及到交互选项-i,实现交互的代码为(只做参考)

#!/bin/bash

if [[ -n $interactive ]];then
    while true
    do
        read -p "enter name of output file:" filename
        if [[ -e $filename ]];then
            read -p "'$filename' already exists. overwrite ? [y/n/q] > "
            case $REPLY in
            Y | y)         break
                           ;;
            Q | q)         echo "program terminated"
                           exit 
                           ;;
            *)             continue
                           ;;
            esac
        elif [[ -z $filename ]];then
            continue
        else
            break
        fi           
    done
fi

下面用一个完整的脚本来展示命令行选项如何在脚本中起到交互作用的

#!/bin/bash
# sys_info_page: program to output a system information page
PROGNAME=$(basename $0)
TITLE="System Information Report For $HOSTNAME"
CURRENT_TIME=$(date +"%x %r %Z")
TIMESTAMP="Generated $CURRENT_TIME, by $USER"
report_uptime () {
    cat <<- _EOF_
        <H2>System Uptime</H2>
        <PRE>$(uptime)</PRE>
    _EOF_
    return
}
report_disk_space () {
    cat <<- _EOF_
        <H2>Disk Space Utilization</H2>
        <PRE>$(df -h)</PRE>
    _EOF_
    return
}
report_home_space () {
    if [[ $(id -u) -eq 0 ]]; then
        cat <<- _EOF_
            <H2>Home Space Utilization (All Users)</H2>
            <PRE>$(du -sh /home/*)</PRE>
        _EOF_
    else
        cat <<- _EOF_
            <H2>Home Space Utilization ($USER)</H2>
            <PRE>$(du -sh $HOME)</PRE>
        _EOF_
    fi
    return
}
usage () {
    echo "$PROGNAME: usage: $PROGNAME [-f file | -i]"
    return
}
write_html_page () {
    cat <<- _EOF_
        <HTML>
            <HEAD>
                <TITLE>$TITLE</TITLE>
            </HEAD>
            <BODY>
                <H1>$TITLE</H1>
                <P>$TIMESTAMP</P>
                $(report_uptime)
                $(report_disk_space)
                $(report_home_space)
            </BODY>
        </HTML>
    _EOF_
    return
}
# process command line options
interactive=
filename=
while [[ -n $1 ]]; do
    case $1 in
        -f | --file)          shift
                              filename=$1
                              ;;
        -i | --interactive)   interactive=1
                              ;;
        -h | --help)          usage
                              exit
                              ;;
        *)                    usage >&2
                              exit 1
                              ;;
    esac
    shift
done
# interactive mode
if [[ -n $interactive ]]; then
    while true; do
        read -p "Enter name of output file: " filename
        if [[ -e $filename ]]; then
            read -p "'$filename' exists. Overwrite? [y/n/q] > "
            case $REPLY in
                Y|y)    break
                        ;;
                Q|q)    echo "Program terminated."
                        exit
                        ;;
                *)      continue
                        ;;
            esac
        fi
    done
fi
# output html page
if [[ -n $filename ]]; then
    if touch $filename && [[ -f $filename ]]; then
        write_html_page > $filename
    else
        echo "$PROGNAME: Cannot write file '$filename'" >&2
        exit 1
    fi
else
    write_html_page
fi

2.4.字符与数字

前面我们主要讨论的是文件的处理,但编程往往会涉及到更小数据的处理,比如字符和数字,这一节我们就来学习几个操作字符和数字的shell功能

2.4.1.基本参数展开

  • $a ${a}使表较常见的参数展开,但如何处理人际关系不存在或为空的变量呢?shell提供了以下几种方案

2.4.2.空变量展开

  • ${parameter:-word}
    parameter 没有设置(例如,不存在)或者为空,展开结果是 word 的值。若 parameter 不为空,则展开结果是 parameter 的值,还是用例子来说明吧
    如果foo=
    echo ${foo:-"hahaha"}输出hahaha
    echo $foo输出为空
    如果foo=tengwang
    echo ${foo:-"hahaha}输出tengwang
    echo $foo输出tengwang
  • ${parameter:=word}
    若 parameter 没有设置或为空,展开结果是 word 的值。另外,word 的值会赋值给 parameter。 若 parameter 不为空,展开结果是 parameter 的值(位置参数或其它的特殊参数不能以这种方式赋值
  • ${parameter:?word}
    若 parameter 没有设置或为空,这种展开导致脚本带有错误退出,并且 word 的内容会发送到标准错误。若 parameter 不为空, 展开结果是 parameter 的值
  • ${parameter:+word}
    若 parameter 没有设置或为空,展开结果为空。若 parameter 不为空, 展开结果是 word 的值会替换掉 parameter 的值;然而,parameter 的值不会改变

2.4.3.返回变量名的参数展开

  • ${!prefix*}
  • ${!prefix@}
    上面这两种形式会返回以 prefix 开头的已有变量名,返回的结果相同
    比如返回所有的以BASH开头的环境变量名
    echo ${!BASH@}

2.4.4.字符串展开

  • ${#parameter}
    展开的结果是字符串parmeter的长度

  • ${parameter:m}
    展开一个字符串,结果从parameter的第m个字符到末尾字符
    foo="wo shi wang teng."
    echo ${foo:5}输出i wang teng. 这里注意说是从第5个字符开始,其实并不包含第5个字符,而是从其后面一个字符开始

  • ${parameter:m:n}
    展开一个字符串,结果从parameter的第m个字符开始,长度是n
    echo ${foo:5:3}输出i w
    如果m为负数,则表示展开是从字符串末尾开始
    echo ${foo: -5:3}输出ten,发现没有从后面算起的话就真的是从第5个开始,还有就是为了和参数展开${parameter:-word}区别,m前面必须要有一个空格,若n出现,必须大于0

  • ${parameter#para} ${parameter##para}
    两者parameter所包含的字符串中清除开头一部分,区别在于前者清除最短的字符,后者清除最长的字符
    foo="file.txt.zip"
    echo ${foo#*.}输出txt.zip
    echo ${foo##*.}输出zip
    如果是echo ${foo##.*}输出file.txt.zip

  • ${parameter%para} ${parameter%%para}
    两者parameter所包含的字符串中清除末尾一部分,区别在于前者清除最短的字符,后者清除最长的字符
    foo="file.txt.zip"
    echo ${foo%.*}输出file.txt
    echo ${foo%%.*}输出file

  • ${parameter/para/string} ${parameter//para/string} ${parameter/#para/string}
    ${parameter/%para/string}
    以上四种形式都是对字符串parameter进行查找并替换
    ${parameter/para/string}表示匹配到文本para的文本就用string替换它,但是只替换第一个
    ${parameter//para/string}表示匹配到文本para的文本就用string替换它,替换所有
    ${parameter/#para/string}表示匹配到文本para的文本必须出现在字符串开头
    ${parameter/%para/string}表示匹配到文本para的文本必须出现在字符串末尾
    foo="JPGJPG"
    echo ${foo/JPG/jpg}输出jpgJPG
    echo ${foo//JPG/jpg}输出jpgjpg
    echo ${foo/#JPG/jpg}输出jpgJPG
    echo ${foo/%JPG/jpg}输出JPGjpg
    字符串展开可以提高脚本执行的效率,,前面我们有一个脚本是用来查找文件中最长字符串,里面有一句echo $j | wc -c 这里如果用字符串展开${#j}可以大大提高脚本的性能,下面我们来具体做个测试
    先写查找最长字符串脚本

#!/bin/bash

for i 
do
    if [[ -r $i ]];then
        max_word=
        max_len=0
        for j in $(strings $i)
        do
            len=$(echo $j| wc -c)
            if (( len > max_len ));then
                max_len=$len
                max_word=$j
            fi
        done
        echo "${max_word}: ${max_len}"
    fi
    shift
done

替换后的脚本

#!/bin/bash

for i 
do
    if [[ -r $i ]];then
        max_word=
        max_len=0
        for j in $(strings $i)
        do
            len=${#j}
            if (( len > max_len ));then
                max_len=$len
                max_word=$j
            fi
        done
        echo "${max_word}: ${max_len}"
    fi
    shift
done

查询同一个文件
time ./ceshi.sh filename会发现使用了字符串展开的基本的执行时间远远小于使用了外部命令的脚本

2.4.5.大小写转换

在shell里实现大小写转换可以使用外部命令declare
cat 1.sh

#!/bin/bash

declare -u upper  
declare -l lower
if [[ $1 ]];then
    upper=$1
    lower=$1
    echo "$upper"
    echo "$lower"
fi

bash 1.sh aBc
输出
ABC
abc
除了用declare命令来强制转换,还可以通过参数展开的方式来实现大小写转换,有以下四种方式

  • ${parameter,,}将字符串所有字母变为小写
  • ${parameter,}将字符串的首字母变为小写字母
  • ${parameter^^}将字符串的所有字母变为大写
  • ${parameter^}将字符串的第一个字母变为大写
    再次给出一个例子
    cat 2.sh
#!/bin/bash

if [[ $1 ]];then
    echo ${1,,}
    echo ${1,}
    echo ${1^^}
    echo ${1^}
    "
fi

bash 2.sh aBC
输出
abc
aBC
ABC
ABC
注意上面这个脚本虽说参数使用的是第一个位置参数,但是其实参数是可以使用变量以及任意字符串的

2.5.算数求值和展开

算术表达式中shell支持任意进制的整型常量

2.5.1.数基

数基的意思就是一个是什么进制的数,比如2进制、10进制、8进制和16进制
下面的两个例子打印出了十六进制和二进制最大的数
echo $((0xff))
echo $((2#11111111))

2.5.2.一元运算符

+ -分别被用来表示正负
简单算术
加(+)减(-)乘(*)整除(/)求余(%) 乘方(**)
这里着重说一下整除和求余,shell里的运算只针对整数,所以整除和求余结果总是整数
比如echo $(( 5 / 2 ))结果为2,echo $(( 5 % 2 ))结果为1,下面举一个余数的例子

#!/bin/bash

for (( i=0; i<=20; i++ ))
do
    remainder=$(( i % 5 ))
    if (( remainder == 0 ));then
        printf "<%d> " $i
    else
        printf "%d " $i
    fi
done  
echo ""

注意这里的for循环里的i=0,小于20,此时i的值在循环体内是1,执行for循环里的内容后自增1,
赋值运算符
直接看例子
foo=
echo $foo
if (( foo = 5 ));then echo "it is true.";fi
it is true.
上面的foo = 5 表示将数值5赋值给foo,因为((foo = 5 ))非零为真,随机输出后面的内容,这里一定要弄清楚=与的区别,=是指将值赋予给变量,而是判断foo的值是否是5,但是旧的测试方法test是支持=的,为了区分所以就有了更为现代化的测试方式[[]] (())
除了=还有许多赋值运算符,这里作一个总结
parameter+=value
parameter-=value
parameter*=value
parameter/=value
parameter%=value
parameter++
parameter--
++parameter
--parameter
对于shell来说前缀运算符比较有用,常常用在for循环里

#!/bin/bash

for (( i=0; i<=20; ++i ))
do
    if (((i % 5) == 0 ));then
        printf "<%d>" $i
    else
        printf "%d" $i
done
printf "\n"

这里i=0,判断其小于20,马上自增1,但是在循环内使用的i还是0,这一点非常要注意

2.5.3.位运算符

位运算符是一类以特别的方式操作数字的运算符,在shell里位运算符和c里的语法语义一模一样
~按位取反,对一个数字的所有位取反,0变1,1变0
<<位左移,对一个数字的所有位左移动 二进制位全部左移N位,右补0
>>位右移,对一个数字的所有位右移动 右移N位,移到右端的低位舍弃,高位补0
&位与,对两个数字的所有位执行AND操作 都为1则结果为1,否则为0
|位或,对两个数字的所有位执行OR操作 只要有1个数为1则结果为1
^位异或,对两个数字 相同为0不同为1
关于位运算的详细解释可以参看这篇文章位运算
15<<2,表示15的二进制数左移2位,结果为60,这里有个规律,左移一位表示原来的数乘以2的1次方,左移2位表示原来的数乘以2的2次方。。。以此类推,这里我们可以利用这个规律来实现2的幂级运算

#!/bin/bash

for (( i=0; i<=7; ++i ))
do
    echo $((1<<i))
done   

输出
1
2
4
8
16
32
64
128
256
同时所有的位运算符也可以与=结合成为赋值运算符

2.5.4.逻辑运算符

逻辑运算符一般也等同于比较运算符,这里做一个总结
>
<
>=
<=
==
!=
&&
||
expr?expr1:expr2 三元表达式子,expr非零,执行expr1,否则执行expr2
这里着重用一个例子来说明一下三元表达式
a=9
b=$((a<10?++a;--a))
echo $b
10
上面是在(())里放入了逻辑运算表达式,现在如果想在里面放入赋值表达式,则需要将赋值表达式用括号括起来,否则会报错
上面的可以改成b=$((a<10?(a+=1):(a-=1)))
下面我们来看一下使用了逻辑运算符、赋值运算符和算术运算符的例子

#!/bin/bash

finished=0
a=0
printf "a\ta**2\ta**3\n"
printf "=\t====\t====\n"
until ((finished)); do
    b=$((a**2))
    c=$((a**3))
    printf "%d\t%d\t%d\n" $a $b $c
    ((a<10?++a:(finished=1)))
done

该脚本会打印出一个建议表格,其实改脚本使用for循环应该更简洁

#!/bin/bash

echo -e "a\ta**2\ta**3"
echo -e "=\t====\t===="
for (( a=0; a<=10; ++a ))
do
    b=$((a**2))
    c=$((a**3))
    echo -e "$a\t$b\t$c"
done

bc
编写一个bc脚本
vi foo.bc

2 + 2

chmod +x foo.bc
bc -q foo.bc输出4,quit退出
也可以直接bc < foo.bc
bc这个命令在实际生活中也是非常有用处的,我们可以用它来计算每个月的贷款

#!/bin/bash

PROGNAME=$(basename $0)
usage () {
    cat <<EOF
Usage:$PROGNAME PRINCIPAL INTEREST MONTHS
WHERE:
PRINCIPAL is the amount of the loan
INTEREST is the APR as a number
MONTHS is the length of the loan's term
EOF
}
if (( $# != 3 ));then
    usage
    exit
fi
principal=$1
interest=$2
months=$3
bc <<EOF
scale = 10
i = $principal / 12
p = $principal
n = $months
a = p * ((i * ((1+i) ^ n)) / (((1 + i) ^ n) - 1))
print a, "\n"
EOF

2.6.重定向

在shell里理解标准输入、标准输出和标准错误三个概念非常重要,执行一个程序也可以说是命令,会有输出,输出分两种:一种是实现程序功能的输出;一种是程序状态和错误信息,反映了程序的进展。默认标准输出和标准错误连接到屏幕,标准输入默认连接键盘

2.6.1.标准输出重定向

ls -l /usr/bin > ls-out.txt 这样我们就把输出定向到了文件ls-out.txt中,屏幕就不在显示输出了
>filename可以清空一个文件的内容或者创建一个空文件
如果想把输出结果增加到文件后面可以使用>>
标准错误重定向
标准错误没有专门的重定向符,但是shell里可以在文件流上产生输出,标准输入、标准输出、标准错误就是前三个文件流,其在内部的文件描述里分别为0、1、2,我们可以借助该描述来完成对标准错误的重定向
ls -l /bin/usr 2> ls-out.txt

2.6.2.重定向标准输出和标准输入到同一个文件

ls -l /bin/usr > ls-out.txt 2>&1
这里标准错误的重定向必须在标准输出的后面,否则标准错误重定向将失效
还有一种方法可以达到将标准输入和标准输出定向到同一个文件
ls -l /bin/usr &> ls-out.txt

2.6.3.处理不需要的输出

ls -l /bin/usr 2 > /dev/null

2.6.4.标准输入重定向

在介绍标准输入重定向前,我们先来看一个平时非常常用的命令cat
cat可以读取一个或者多个文件,然后复制文件内容到标准输出
比如有3个文件1.html 2.html 3.html。内容分别为1、2、3
cat *.html > html
cat html会输出
1
2
3
cat 后不接任何参数,那么它会从标准输入读取数据并将其复制到标准输出
cat > test.txt会将标准输入的数据复制后重定向到文件test.txt中
cat < test.txt会将标准输入重定向到读取文件test.txt的内容然后输出到标准输出

2.6.5.管道

一个命令的标准输出会通过管道线"|"送至另外一个命令的标准输入
ls -/usr/bin | less less会接收管道过来的输入,然后将结果输送到标准输出
我们通常把可以接受管道过来的输出的命令工具成为过滤器,常见的有sort grep uniq wc等
简单介绍下几个命令的常用使用方法
wc filename
同时显示文件的行数、字数、字节数
uniq是去除重复行,加上选项-c在前面显示每一行的重复行数,-d不显示
grep打印匹配行
head打印文件开头
tail打印文件末尾
tee将标准输入的数据输送到标准输出或者文件中

2.7.数组

前面我们在shell里接触到的数据类型都属于标量变量,不论是数字还是字符串,每一个变量只能存放一个值,而数组可以存放多个变量
创建数组
大多数的编程语言支持多维数组,但是bash里仅为一维数组,创建数组变量就和前面创建变量一样
a[1]=foo
echo ${a[1]}输出foo
也可以利用declare创建declare -a a
a=(1 2 3 4),数组的元素或者下标是从0开始
还介意指定下标把值赋给特定的元素
a=([0]=1 [2]=2 [4]=0)
数组的使用
这里有一个经典的例子,利用数组的特性来写一个shell脚本实现一天内每一个小时某个特定目录下文件的修改次数,这样可以发现哪一个小时系统比较活跃,先不论该脚本的实用性,我们主要是利用该脚本去体会数组在脚本当中的使用
cat hour.sh

#!/bin/bash

# hours : scripts to count files by modyfication time
usage () {
    echo "usage:$(basename $0) directory" >&2
}
if [[ ! -d $1 ]];then
    usage
    exit 1
fi
# initialize array
for i in {0..23}
do
    hour[i]=0
done
# collect data
for i in $(stat -c %y "$1"/* | cut 12-13) # 过滤出所有文件最后修改在哪一个小时
do
    j=${i/#0} # 去掉小时前面的0,比如01
    ((++hour[j])) 
    ((+=count)) # 注意这种方式的写法,这样写count默认开始为0
done
# display data
echo -e "Hour\tFile\tHour\tFile"
echo -e "----\t----\t----\t----"
for i in {0..11};
do
    j=$((i + 12))
    printf "%02d\t%d\t%02d\t%d\n" $i ${hour[i]} $j ${hour[j]}
done
printf "\nTotal files = %d\n" $count

输出整个数组的元素

array=(a b c d)   
for i in ${array[@]}
do
    echo $i
done   
或者    
for i in ${array[\*]}
do
    echo $i
done   

数组元素的个数
前面我们讲到可以通过字符展开来计算一个字符串的长度${#parameter},数组也可以类似处理
a[100]=foo
echo ${#a[100]}输出3
echo ${#a[@]}输出1
这里输出整个数组的长度是1,这和其他编程语言不一样会把其他未使用的元素初始化为空值并计入长度

获取数组元素的下标
a=([1]=1 [3]=3 [4]=7)
for i in ${a[@]};do echo $i;done
输出
1
3
7
for i in ${!a[@]};do echo $i;done
输出
1
3
4
echo ${a[@]}输出
1 3 7
echo ${!a[@]}输出
1 3 4
末尾添加元素
foo=(a b c)
foo+=(d e f)
echo ${foo[@]}输出
a b c d e f
数组元素排序
脚本实现
cat array_sort.sh

#!/bin/bash

array=(f e d c b a)
echo "normal array: ${array[@]}"
sort_array=($(for i in ${array[@]};do echo ${i};done | sort))
echo "sorted array: ${sort_array[@]}"

删除数组
foo=(1 2 4)
unset foo
echo ${foo[@]}输出为空
unset foo[1]输出 1 4
上面介绍了如何去删除整个数组的元素和某一个元素,那么我们如果作如下操作呢?
foo=
echo ${foo[@]}输出2 4
事实上,任何一个不带下标对数组变量进行引用都会默认指向数组元素0
关联数组
关联数组的索引必须是字符串,同时数组必须用declare定义
declare -A color
color["red"]="#000ff"