Shell Scripting with Bash

执行date命令并输出所有参数到 notes.txt

1
echo $(date): $* >> ~/notes.txt

echo 的三种引用标识(quote mark)

1.不带标识

greeting="hello"

echo $greeting, word (planet)!

#以上语句会直接报错,如果要带特殊字符,需要带上反引号 \

echo $greeting, word \(planet\)!

2.单引号

直接原样输出

echo '$greeting, world (planet)!'

3.双引号

可以输出变量,如果要原样输出$前加\

echo "$greeting, world (planet)!"

变量

赋予两个变量的值

1
usergreeting="$greeting, $user"

使用变量用花括号

1
2
3
# get the topic
topic=$1
echo "text" >> ~/${topic}notes.txt

从终端读入数据

1
2
# Ask user for input
read -p "Your note: " note

特殊变量

$#脚本的参数数量

获得一个变量的长度

${#var}

调试模式

全局调试,第一行加上 -x即可

1
2
#!bing/bash -x
........

设置某个段落的Debug,再前后加上set -x , set +x

1
2
3
4
5
6
7
# get the topic
topic="$1"
set -x
# filename to write to
filename="${HOME}/${topic}notes.txt"
set +x
# Ask user for input

条件语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

# check number of arguments
if [[ $# -ne 2 ]]; then
echo "Need exactly two arguments"
exit 1
fi

# Nested if
# Which one has most files?
if [[ $length_1 -gt $length_2 ]]; then
echo "$1 is longest"
elif [[ $length_1 -eq $length_2 ]]; then
echo "length is equal"
else
echo "$2 is longest"
fi

if mkdir a; then echo "ok"; else echo "error"; fi

if [[ $note ]]; then
echo "$date: $note" >> "$filename"
echo "Note '$note' saved to $filename"
else
echo "No input; note wasn't saved."
fi

# Is there an argument?
if [[ ! $1 ]]; then
echo "Missing argument"
exit 1
fi

# Check the $filename exists
if [[ =e $filename ]]; then
echo "File ${filename} alreday exists"
exit 1
fi

# Check bin directory exists
if [[ ! -d $bindir ]]; then
# if not: create bin directory
if mkdir "$bindir"; then
echo "create ${bindir}"
else
echo "Could not create ${bindir}."
exit 1
fi
fi

if type "$scriptname"; then
echo "There is already a command with name ${scriptname}"
exit 1
fi

判断

简写技巧

[ condition ] && action #如果condition为真,则执行action
[ condition ] || action #如果condition为假,则执行action

算数对比

[[ arg1 OP arg2 ]]

-eq:等于
-ne:不等于
-lt:小于
-gt:大于
-ge:大于或等于
-le:小于或等于

[ $var1 -ne 0 -a $var2 -gt 2 ] #使用逻辑与-a
[ $var1 -ne 0 -o $var2 -gt 2 ] #逻辑或-o

字符串比较

进行字符串比较时,最好用双中括号,因为有时候采用单个中括号会产生错误。

[[ $str ]] str不为空,返回真
[[ -z $str1 ]] 如果str1为空串,则返回真
[[ -n $str1 ]] 如果str1不为空串,则返回真
[[ $str = “something” ]] str等于字符串”something”时,返回真
[[ $str == “something” ]] 同上
[[ $str=”something” ]] 永远返回真!注意等号前后少了空格.
[[ $str1 != $str2 ]] 如果str1和str2不相同,则返回真。
[[ $str1 > $str2 ]] 如果str1的字母序比str2大,则返回真。

使用逻辑运算符 && 和 || 能够很容易地将多个条件组合起来:

str1="Not empty "
str2=""
if [[ -n $str1 ]] && [[ -z $str2 ]];
then
    echo str1 is nonempty and str2 is empty string.
fi

文件系统相关测试

  • [ -f $file_var ]􏲮􏲃􏲄􏶲􏳯􏱁􏴊􏴋􏴣􏴤􏵙􏱿􏱁:如果给定的变量包含正常的文件路径或文件名,则返回真。
  • [ -x $var ]􏲮􏲃􏲄􏶲􏳯􏱁􏴊􏴋􏴣􏴤􏱁:如果给定的变量包含的文件可执行,则返回真。
  • [ -d $var ]􏲮􏲃􏲄􏶲􏳯􏱁􏴊􏴋􏴣􏴤􏱁:如果给定的变量包含的是目录,则返回真。
  • [ -e $var ]􏲮􏲃􏲄􏶲􏳯􏱁􏴊􏴋􏴣􏴤􏱁:如果给定的变量包含的文件存在,则返回真。
  • [ -c $var ]􏲮􏲃􏲄􏶲􏳯􏱁􏴊􏴋􏴣􏴤􏱁􏲀􏲵􏱀􏲮􏲃􏲄􏶲􏳯􏱁􏴊􏴋􏴣􏴤􏱁:如果给定的变量包含的是一个字符设备文件的路径,则返回真。
  • [ -b $var ]􏲮􏲃􏲄􏶲􏳯􏱁􏴊􏴋􏴣􏴤􏱁:如果给定的变量包含的是一个块设备文件的路径,则返回真。
  • [ -w $var ]􏲮􏲃􏲄􏶲􏳯􏱁􏴊􏴋􏴣􏴤􏱁􏲮􏲃􏲄􏶲􏳯􏱁􏴊􏴋􏴣􏴤􏱁:如果给定的变量包含的文件可写,则返回真。
  • [ -r $var ]􏲮􏲃􏲄􏶲􏳯􏱁􏴊􏴋􏴣􏴤􏱁􏲮􏲃􏲄􏶲􏳯􏱁􏴊􏴋􏴣􏴤􏱁:如果给定的变量包含的文件可读,则返回真。
  • [ -L $var ]􏲮􏲃􏲄􏶲􏳯􏱁􏴊􏴋􏴣􏴤􏱁:如果给定的变量包含的是一个符号链接,则返回真。

    fpath=”/etc/passwd”
    if [ -e $fpath ]; then

    echo File exists;
    

    else

    echo Does not exist;
    

    fi

两个Expression帮助

1
2
help [[
help test

And,Or,Not

1
2
3
4
5
6
# and
[[ $# -eq 1 && $1="foo" ]]
# or
[[ $a || $b ]]
# not
[[ ! -e $file ]]

输入输出

printf

printf要比echo更强大

1
2
3
4
5
printf "hello %s, how are you?" $USER
printf "p%st\n" a e i o u
printf "%ss home is in %s" $USER $HOME
printf "|%20s |%20s |%20s |\n" $(ls)
printf -v greeting "hello %s, how are you?\n" $USER

input

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/bash

echo -n "Are you sure (Y/N)? "

answered=
while [[ ! $answered ]]; do
read -r -n 1 -s answer
if [[ $answer = [Yy] ]]; then
answered="yes"
elif [[ $answer = [Nn] ]]; then
answered="no"
fi
done

printf "\n%s\n" $answered

指定分割符

IFS=: read a b

标准流

0 – Standard Input (stdin) /dev/stdin
1 – Standard Output (stdout) /dev/stdout
2 – Standard Error (stderr) /dev/stderr

如果不像看到错误信息可以将stderr输出重定向到/dev/null

将stderr重定向到out.txt

ls + 2> out.txt

将stderr和stdout分别定向到不同的文件中

cmd 2>stderr.txt 1>stdout.txt

将stderr转换成stdout,使得stderr和stdout都被重定向到同一个文件中

cmd 2>&1 alloutput.txt

或者这样

cmd &> output.txt

/dev/null

1
2
3
4
5
6
7
8
9
10
11
if type "$scriptname" > /dev/null 2>&1; then
echo "There is already a command with name ${scriptname}"
exit 1
fi

if [[ $note ]]; then
echo "$date: $note" >> "$filename"
echo "Note '$note' saved to $filename"
else
echo "No input; note wasn't saved." 1>&2
fi

其他

数组

定义数组

array_var=(test1 test2 test3 test4)
#以0为起始索引

array_var[0]="test1"
array_var[1]="test2"
array_var[2]="test3"
array_var[3]="test4"
array_var[4]="test5"
array_var[5]="test6"

打印特定索引的数组元素内容

echo ${array_var[0]}
echo ${array_var[$index]}

打印数组中的所有值

$ echo ${array_var[*]}

􏳤 也可以􏲙􏳥􏲗􏰃􏲮这样使用
$ echo ${array_var[@]}

打印数组长度

echo ${#array_var[*]}

关联数组

定义关联数组

在关联数组中,我们可以用任何的文本作为数组索引。首先需要使用声明语句将一个变量定义为关联数组:

declare -A ass_array

使用行类“索引-值”列表

$ ass_array=([index1]=val1 [index2]=val2)

使用独立“索引-值” 进行赋值

$ ass_array[index1]=val1
$ ass_array[index2]=val2

关联数组栗子

$ declare -A fruits_value
$ fruits_value=([apple]='100 dollars' [orange]='150 dollars')
$ echo "Apple costs ${fruits_value[apple]}"

列出数组索引

$ echo ${!array_var[*]}

也可以这样

$ echo ${!array_var[@]}

以前面的fruits_value为例

$ echo ${!fruits_value[*]}

使用shell进行数学运算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#!/bin/bash
no1=4;
no2=5;
# let􏰳􏱓可以􏲶􏰇􏱔􏰅􏳍直接执行基本的算术操作。当使用let􏰲时,变量名之前􏴊􏴋􏰴􏳾􏳿不需要再添加$
let result=no1+no2
echo $result
# 操作符[]的使用方法喝let命令一样
result=$[ no1 + no2 ]
# 在[]中也可以使用$前缀
result=$[ $no1 + 5 ]
# 也可以使用(())操作符
result=$(( no1 + 50 ))
# expr同样可以用户与基本算术操作
result=`expr 3 + 4`
result=$(expr $no1 + 5)

# 以上方法不支持浮点数,只能用于整数运算,我们可以借助bc命令执行浮点数运算

echo "4 * 0.56" | bc
result=`echo "$no * 1.5" | bc`
echo $result
#设置小数精度。参数scale=2将小数位个数设置为2.
echo "scale=2;22/7" | bc
#转换进制
no=100
echo "obase=2;$no" | bc # 二进制
echo "obase=10;ibase=2;$no" | bc # 十进制
#平方根
echo "sqrt(100)" | bc #Square root
echo "10^10" | bc #Square

采集终端信息并操作

􏵛􏲟􏰓􏰔􏱁􏰅􏰷􏰨􏰡􏰷􏲮􏵛􏲟􏰓􏰔􏱁􏰅􏰷􏰨􏰡􏰷􏲮获取终端的行数和列数

tput cols
tput lines

打印当前的终端名

tput longname

将光标移动到座标(100,100)处

tput cup 100 100

设置终端背景色

tput setb n  # n可以在0到7之间取值􏳾􏳁􏲟􏴾。

设置终端前景色

tput setf n # n可以在0到7之间取值􏳾􏳁􏲟􏴾。

设置文本样式为粗体

tput bold

设置下划线的起止

tput smul
tput rmul

删除从当前光标位置到行为的所有内容:

tput ed

使用stty实现输入密码时不显示输入内容。

1
2
3
4
5
6
7
8
9
10
#!/bin/sh
#Filename: password.sh
echo -e "Enter password: "
# 在读取密码前禁止回显
stty -echo
read password
# 重新允许回显
stty echo
echo
echo Password read.

输入密码时,脚本不应该显示输入内容。

时间处理

日期能够以多种格式呈现。在系统内部,日期被存储成一个整数,其取值为自1970年1月1日0时0分0秒起所流逝的秒数。这种计时方式成为纪元时或Unix时间。

打印纪元时

$ date +%s
$ date --date "Wed mar 15 08:09:16 EDT 2017" +%s
date --date "Jan 20 2001" +%A
用带有前缀+的格式化字符串作为date命令的参数,可以按照你的选择打印出相应格式的日期。
$ date "+%d %B %Y"

使用date命令计算一组命令所花费的执行时间

1
2
3
4
5
6
7
#!/bin/bash
start=$(date +%s)
commands;
statements;
end=$(date +%s)
difference=$(( end - start))
echo Time taken to execute commands is $difference seconds.

对命令计时的另一种更好的方式是使用time命令time commandOrScriptName

计算两个日期之间相隔了多少秒:

secs1=`date -d "Jan 2 1970"
secs2=`date -d "Jan 3 1970"
echo "There are `expr $secs2 - $secs1` seconds between Jan 2 and Jan 3"
There are 86400 seconds between Jan 2 and Jan 3

计时脚本

变量一次使用了由seq命令生成的一系列数字。我们用tput sc存储光标位置。在每次循环中,通过tput rc恢复之前存储的光标位置,在终端中打印出新count值,然后使用tputs ed 清除从当前光标位置到行为之间的所有内容。行被清空之后,脚本就可以显示出新的值。

1
2
3
4
5
6
7
8
9
10
11
#!/bin/bash
echo Count :
tput sc
# 循环40秒
for count in `seq 0 40`
do
tput rc
tput ed
echo -n $count
sleep 1
done

函数和参数

函数定义

function fname()
{
    statements;
}

或者

fname()
{
    statements;
}

或者

fname() { statement; }

调用函数

使用函数名即可调用函数

$ fname ; #执行函数

访问函数参数

fname arg1 arg2 ; #传递参数

以下是函数fname的定义。在函数fname中,包含了各种访问函数参数的方法。

fname()
{
    echo $1, $2; #访问参数1和参数2
    echo "$@"; #以列表的方式一次性打印所有参数
    echo "$*";  #类似于$@,但是所有参数被视为单个实体 
    return 0;
}
  • $0是脚本名称
  • $1是第一个参数
  • $2是第二个参数
  • $n是第n个参数
  • ”$@”被扩展成”$1””$2””$3”等。
  • “$*”被扩展成”$1 $2 $3”

递归函数

F() { echo $1; F hello;sleep 1; }

Fork炸弹

以下函数会一直生成新的进程,最终形成拒绝服务攻击。代码的理解和细节请查看维基百科

:(){ :|:& };:

可以修改配置文件/etc/security/limits.conf中的nproc来限制可生成的最大进程数,进而阻止这种攻击。

hard nproc 100 #将所有用户看生成的进行数限制为100

导出函数

函数也能像环境变量一样用export导出,如此一来,函数的作用域就可以扩展到子进程中:

export -f fname
$> function getIP() { /sbin/ifconfig $1 | grep 'inet '; }
$> echo "getIP eth0" >test.sh
$> sh test.sh
  sh: getIP: No such file or directory
$> export -f getIP
$> sh test.sh
inet addr: 192.168.1.2 Bcast: 192.168.255.255 Mask:255.255.0.0

处理参数

一般处理方式为迭代所有的命令行参数,下面语句中shift命令可以依次向左移动一个位置,让脚本能够使用$1来访问到每一个参数。

1
2
3
4
5
for i in `seq 1 $#`
do
echo $i is $1
shift
done

从键盘或标准输入中读取文本

从输入中读取n个字符并存如变量

read -n number_of_chars variable_name

用无回显的方式读取密码

read -s var

使用read显示提示信息

read -p "Enter input:" var

在给定实现内读取输入

read -t 2 var #在两秒钟内键入的字符串读入变量var

用特定的定界符作为输入行的结束

read -d ":" var  #输入hello:  变量var会被复制为hello

一些小技巧

获得变量值的长度

length=${#var}

识别当前所使用的shell

echo $SHELL 或 echo $0

检查是否为超级用户

If [ $UID -ne 0 ]; then
    echo Non root user. Please run as root.
else
    echo Root user
fi

[实际上是一个命令,必须将其与剩余的字符串用空格隔开。上面的脚本也可以写成:

If test $UID -ne 0:1
  then
    echo Non root user. Please run as root.
  else
    echo Root user
fi

修改Bash的提示字符串(username@hostname:~$)

修改变量PS1即可,默认在~/.bashrc中的某一行

使用函数添加环境变量

prepend() { [ -d "$2" ] && eval $1=\"$2':'\$$1\" && export $1; }
prepend PATH /opt/myapp/bin
prepend LD_LIBRARY_PATH /opt/myapp/lib

函数prepend()首先确认该函数第二个参数所指定的目录是否存在。如果存在,eval表达式将第一个参数指定的变量设置成第二个参数的值加上:(路径分隔符),随后再跟上第一个参数的原始值。
在进行添加时,如果变量为空,则会在末尾留下一个:。要解决这个问题,可以对该函数再做一些修改:

prepend() { [ -d "$2" ] && eval $1=\"$2\$\{$1:+':'\$$1\}\" && export $1 ; }

在这个函数中,我们引入了一种shell参数扩展的形式:${parmeter:+expression},如果parameter有值且不为空,则使用expression的值。通过这次修改,在向环境变量中添加新路径时,当且仅当旧值存在,才会增加。

创建别名rm,删除原始文件并且在backup目录中保留副本。

alias rm='cp $@ ~/backup && rm $@'

将命令序列的输出赋给变量

cmd_output=$(ls | cat -n)

或者

cmd_output=`ls | cat -n`

通过子shell的方式保留空格和换行副

$ cat text.txt
1
2
3
$ out=$(cat text.txt)
$ echo $out
1 2 3 # 丢失了1、2、3中的\n
$ out="$(cat text.txt)"
$ echo $out
1
2
3

持续运行命令直至执行成功

有时候命令只有在满足某些条件时才能够成功执行。这种情况下,你可能希望重复执行命令。以下函数包含了一个无线while循环,该循环执行以函数参数形式(通过$@访问)传入的命令。如果命令执行成功,则返回,进而退出循环。

repeat()
{
    while true
    do
        $@ && return
    done
}

repeat() { while true; do $@ && return; done }

在大多数系统中true是作为/bin中一个二进制文件实现的。这就意味着每次执行依次之前提到的while循环,shell就不得不生成一个进程。为了避免这种情况,可以使用shell的内建命令:,该命令的退出状态总是为0

repeat() { while :; do $@ && return; done }

字符分隔符与迭代器

当IFS被设置为逗号时,shell将视逗号为一个定界符,因此变量$item在每次迭代中读取由逗号分割的子串作为变量值。

data="name, gender,rollno,location"

oldIFS=$IFS
IFS=, #IFS现在被设置为,
for item in $data;
do
    echo Item: $item
done
IFS=$oldIFS
打赏支持:如果你觉得我的文章对你有所帮助,可以打赏我哟。