1.在终端显示输出
1.1 username@hostname$
$表示普通用户,#表示管理员用户root
1.2 脚本以#!/bin/bash开始
1.3 脚本执行方式
(1) 将脚本名作为命令行参数:
bash myScript.sh
(2) 授予脚本执行权限,将其变为可执行文件:
chmod 755 myScript.sh 或者chmod a+x sample.sh
./myScript.sh.
1.4 启动shell的背后
会执行一组命令来初始化提示文本、颜色等设置。这组命令来自用户主目录中的脚本文件/.bashrc(对于登录shell则是/.bash_profile)。Bash shell还维护了一个历史记录文件~/.bash_history,用于保存用户运行过的命令。
1.5 echo 与 printf
双引号允许shell解释字符串中出现的特殊字符。单引号不会对其做任何解释。
#!/bin/bash
#文件名: printf.sh
printf "%-5s %-10s %-4s\n" No Name Mark
printf "%-5s %-10s %-4.2f\n" 1 Sarath 80.3456
printf "%-5s %-10s %-4.2f\n" 2 James 90.9989
printf "%-5s %-10s %-4.2f\n" 3 Jeff 77.564
执行结果如下:
No Name Mark
1 Sarath 80.35
2 James 91.00
3 Jeff 77.56
。%-5s指明了一个格式为左对齐且宽度为5的字符串替换(-表示左对齐)。如果不指明-,字符串就采用右对齐形式。
默认情况下,echo会在输出文本的尾部追加一个换行符。可以使用选项-n来禁止这种行为。
在使用转义序列时,需要使用echo -e"包含转义序列的字符串"这种形式。
2.使用变量与环境变量
2.1 环境变量
常用的惯例是在脚本中使用大写字母命名环境变量,使用驼峰命名法或小写字母命名其他变量。
可以使用env或printenv命令查看当前shell中所定义的全部环境变量。
要查看其他进程的环境变量,可以使用如下命令。一个变量以name=value的形式来描述,彼此之间由null字符(\0)分隔。
cat /proc/$PID/environ |tr '\0' '\n'
注意,var = value不同于var=value。把var=value写成var = value是一个常见的错误。两边没有空格的等号是赋值操作符,加上空格的等号表示的是等量关系测试。
环境变量是从父进程中继承而来的变量。export命令声明了将由子进程所继承的一个或多个变量。这些变量被导出后,当前shell脚本所执行的任何应用程序都会获得这个变量。shell创建并用到了很多标准环境变量,我们也可以导出自己的环境变量。
2.2 shell内建特性
2.2.1 获得字符串的长度
$ var=12345678901234567890
$ echo ${#var}
20
2.2.2 识别当前所使用的shell
echo $SHELL
echo $0
2.2.3 检查是否为超级用户
$UID为0就是超级用户。
2.2.4 修改Bash的提示字符串(username@hostname:~$)
$ cat ~/.bashrc | grep PS1
3.使用函数添加环境变量
环境变量通常保存了可用于搜索可执行文件、库文件等的路径列表。例如LD_LIBRARY_PATH。
当你使用源代码构建并安装程序时,通常需要为新的可执行文件和库文件添加特定的路径。假设我们要将myapp安装到/opt/myapp,它的二进制文件在/opt/myapp/bin目录中,库文件在/opt/myapp /lib目录中。
缺省的路径:/lib和/usr/lib
/etc/ld.so.conf中指明的路径。
prepend() { [ -d "$2" ] && eval $1=\"$2\$\{$1:+':'\$$1\}\" && export $1 ; }
该函数用法如下:
prepend PATH /opt/myapp/bin
prepend LD_LIBRARY_PATH /opt/myapp/lib
测试方法:
export text=love
./prepend.sh
prepend.sh脚本如下
#!/bin/bash
echo $text
prepend() { [ -d "$2" ] && eval $1=\"$2\$\{$1:+':'\$$1\}\" && export $1 ; }
prepend text /home/work
echo $text
结果:
love
/home/work:love
3.1 一种shell参数扩展的形式
${parameter:+expression}
如果parameter有值且不为空,则使用expression的值。
通过这次修改,在向环境变量中添加新路径时,当且仅当旧值存在,才会增加:。
4.使用shell进行数学运算
- 使用let、(( ))和[]执行基本的算术操作。
- 工具expr和bc可以用来执行高级操作。
#!/bin/bash
a=4;
b=5;
let result=a+b;
echo result: $result;
let result+=1;
echo result: $result;
echo '===using [] operator==='
result=$[ a + b ]
echo result: $result;
result=$[ $a + b ]
echo result: $result;
echo '===using (()) operator==='
result=$((a + b))
echo result: $result;
result=$(($a + $b))
echo result: $result;
echo '===using expr operator==='
result=`expr 3 + 4`
echo result: $result;
result=$(expr $a + 4)
echo result: $result;
结果:
result: 9
result: 10
===using [] operator===
result: 9
result: 9
===using (()) operator===
result: 9
result: 9
===using expr operator===
result: 7
result: 8
当使用let时,变量名之前不需要再添加$。
#!/bin/bash
echo "4 * 0.56" | bc
no=54;
result=`echo "$no * 1.5" | bc`
echo $result
echo "scale=2;22/7" | bc
no=100
echo "obase=2;$no" | bc
no=1100100
echo "obase=10;ibase=2;$no" | bc
echo "sqrt(100)" | bc
echo "10^2" | bc
结果:
2.24
81.0
3.14
1100100
100
10
100
5.玩转文件描述符与重定向
5.1 预备知识
文件描述符是与某个打开的文件或数据流相关联的整数。文件描述符0、1以及2是系统预留的。
- 0 —— stdin (标准输入)。
1 —— stdout(标准输出)。
2 —— stderr(标准错误)。
5.2 实战
- > 保存到文件中,会清空文件中先前的内容
- >> 追加到文件中
- 命令错误时,返回一个非0的退出状态;命令成功后,返回0的退出状态。退出状态可从$?中获得。
- 使用2>将stderr重定向到其他文件。
ls + 2> out.txt
- 如下cmd为任意的shell指令
//将stderr和stdout分别重定向到不同的文件中
cmd 2>stderr.txt 1>stdout.txt
//使得stderr和stdout都被重定向到同一个文件中
cmd &> output.txt
- 将数据重定向到文件,并提供一份重定向数据的副本作为管道中后续命令的stdin。
tee命令从stdin中读取,然后将输入数据重定向到stdout以及一个或多个文件中。
注意tee只能从stdin中读取,不能从stderr中读取。
tee -a用于追加内容。
cmd | tee FILE1 FILE2 | otherCommand
- 要发送输入内容的两份副本给stdout,使用-作为命令的文件名参数即可:cmd1 | cmd2 | cmd -
[root@jkz ~]# echo who is this | tee -
who is this
who is this
5.3 补充
- 将文件重定向到命令: <
cmd < file
- 重定向脚本内部的文本块
#!/bin/bash
cat<<EOF>log.txt
This is a generated file. Do not edit. Changes will be overwritten.
EOF
出现在cat <<EOF>log.txt与下一个EOF行之间的所有文本行都会被当作stdin数据。
$ cat log.txt
This is a generated file. Do not edit. Changes will be overwritten.
- 自定义文件描述符
exec命令创建全新的文件描述符。
常用的打开模式有3种:
1)只读模式
2)追加写入模式
3)截断写入模式
<操作符可以将文件读入stdin;
>操作符用于截断模式的文件写入(数据在目标文件内容被截断之后写入);
>>操作符用于追加模式的文件写入。
[root@jkz ~]# exec 3<log.txt
[root@jkz ~]# cat <&3
This is a generated file. Do not edit. Changes will be overwritten.
[root@jkz ~]# cat <&3
[root@jkz ~]#
使用文件描述符3打开并读取文件,只能使用一次。
[root@jkz ~]# cat log.txt
This is a generated file. Do not edit. Changes will be overwritten.
[root@jkz ~]# exec 5>log.txt
[root@jkz ~]# echo newline >&5
[root@jkz ~]# cat log.txt
newline
[root@jkz ~]# exec 5>>log.txt
[root@jkz ~]# echo append line >&5
[root@jkz ~]# cat log.txt
newline
append line
6.数组与关联数组
Bash支持普通数组和关联数组,前者使用整数作为数组索引,后者使用字符串作为数组索引。
6.1 普通数组
[root@jkz ~]# array_var=(test1 test2 test3)
[root@jkz ~]# echo ${array_var[0]}
test1
[root@jkz ~]# echo ${array_var[1]}
test2
[work@jkz ~]$ array_var[0]="test1"
[work@jkz ~]$ array_var[1]="test2"
[work@jkz ~]$ array_var[2]="test3"
[work@jkz ~]$ echo ${array_var[0]}
test1
[work@jkz ~]$ echo ${array_var[1]}
test2
[work@jkz ~]$ echo ${array_var[*]}
test1 test2 test3
[work@jkz ~]$ echo ${array_var[@]}
test1 test2 test3
[work@jkz ~]$ echo ${#array_var[*]}
3
6.2 关联数组
- step1.声明关联数组
step2.将元素添加到关联数组
step3.取出指定索引对应的值
step4.列出所有值或所有值
[work@jkz ~]$ declare -A ass_array
[work@jkz ~]$ ass_array=([index1]=val1 [index2]=val2)
[work@jkz ~]$ ass_array[index3]=val3
[work@jkz ~]$ ass_array[index4]=val4
[work@jkz ~]$ echo ${ass_array[index4]}
val4
[work@jkz ~]$ echo ${ass_array[index1]}
val1
[work@jkz ~]$ echo ${ass_array[*]}
val4 val1 val2 val3
[work@jkz ~]$ echo ${ass_array[@]}
val4 val1 val2 val3
[work@jkz ~]$ echo ${!ass_array[*]}
index4 index1 index2 index3
7.别名
- 创建别名
alias new_command='command sequence'
- 将其定义放入~/.bashrc文件中。每当一个新的交互式shell进程生成时,都会执行 ~/.bashrc中的命令。
echo 'alias new_command="command sequence" ' >> ~/.bashrc
- 删除别名
从~/.bashrc中删除 - 可以创建一个别名rm,它能够删除原始文件,同时在backup目录中保留副本。
alias rm='cp $@ ~/backup && rm $@'
如果身份为特权用户,别名也会造成安全问题。为了避免对系统造成危害,应该将命令转义。
字符\可以转义命令,从而执行原本的命令。在不可信环境下执行特权命令时,在命令前加上\来忽略可能存在的别名总是一种良好的安全实践。这是因为攻击者可能已经将一些别有用心的命令利用别名伪装成了特权命令,借此来盗取用户输入的重要信息。alias列出当前定义的所有别名
8.采集终端信息
编写命令行shell脚本时,总是免不了处理当前终端的相关信息,比如行数、列数、光标位置、遮盖的密码字段等。
获取终端的行数、列数
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输入密码时,脚本不应该显示输入内容。
#!/bin/sh
#Filename: password.sh
echo -e "Enter password: "
# 在读取密码前禁止回显
stty -echo
read password
# 重新允许回显
stty echo
echo
echo Password read.
9.获取并设置日期及延时
在系统内部,日期被存储成一个整数,其取值为自1970年1月1日0时0分0秒起所流逝的秒数。这种计时方式称为纪元时或Unix时间。
- 读取日期
date - 打印纪元时
date +%s
将日期转换成纪元时
date --date "Tue Dec 11 08:08:02 CST 2018" +%s
根据指定的日期找出这一天是星期几
date --date "Dec 11 2018" +%A - 用带有前缀+的格式化字符串作为date命令的参数
date "+%d %B %Y" - 设置日期和时间
date -s "10 Dec 2018 11:01:22"
设置为互联网时间:
ntpdate -u ntp.api.bz - date命令可以用于计算一组命令所花费的执行时间
#!/bin/bash
#文件名: time_take.sh
start=$(date +%s)
commands;
statements;
end=$(date +%s)
difference=$(( end - start))
echo Time taken to execute commands is $difference seconds.
date命令的最小精度是秒。对命令计时的另一种更好的方式是使用time命令:
time commandOrScriptName.
[work@jkz ~]$ time ./arith.sh
result: 9
result: 10
===using [] operator===
result: 9
result: 9
===using (()) operator===
result: 9
result: 9
===using expr operator===
result: 7
result: 8
real 0m0.005s
user 0m0.004s
sys 0m0.000s
- 在脚本中生成延时
#!/bin/bash
#文件名: sleep.sh
echo Count:
tput sc
# 循环40秒
for count in `seq 0 40`
do
tput rc
tput ed
echo -n $count
sleep 1
done
tput rc : 恢复光标到上一次保存的位置
10.调试脚本
- 使用选项-x,启用shell脚本的跟踪调试功能
bash -x script.sh
//或者
sh -x script
运行带有-x选项的脚本可以打印出所执行的每一行命令以及当前状态。
- 使用set -x和set +x对脚本进行部分调试。
#!/bin/bash
#文件名: debug.sh
for i in {1..6};
do
set -x
echo $i
set +x
done
echo "Script executed"
用{start..end}来迭代从start到end之间的值。这个语言构件(construct)在执行速度上要比seq命令略快。
- 通过定义 _DEBUG环境变量来启用或禁止调试及生成特定形式的信息。
#!/bin/bash
function DEBUG()
{
[ "$_DEBUG" == "on" ] && $@ || :
}
for i in {1..10}
do
DEBUG echo "I is $i"
done
[work@jkz ~]$ _DEBUG=on ./xdebug.sh
I is 1
I is 2
I is 3
I is 4
I is 5
I is 6
I is 7
I is 8
I is 9
I is 10
- 工作原理
set -x:在执行时显示参数和命令。
set +x:禁止调试。
set -v:当命令进行读取时显示输入。
set +v:禁止打印输入。
11.函数和参数
- 函数定义
function fname()
{
statements;
}
fname()
{
statements;
}
fname() { statement; }
- 函数调用
fname ;
- 参数传递与访问
fname arg1 arg2 ;
fname()
{
echo $1, $2; #访问参数1和参数2
echo "$@"; #以列表的方式一次性打印所有参数
echo "$*"; #类似于$@,但是所有参数被视为单个实体
return 0; #返回值
}
$0是脚本名称。
$1是第一个参数。
$2是第二个参数。
$n是第n个参数。
"$@"被扩展成"$1" "$2" "$3"等。
"$*"被扩展成"$1c$2c$3",其中c是IFS的第一个字符。
"$@"要比"$*"用得多。由于"$*"将所有的参数当作单个字符串,因此它很少被使用。
递归函数与Fork炸弹
修改配置文件/etc/security/limits.conf中的nproc来限制可生成的最大进程数。导出函数
函数也能像环境变量一样用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
- 读取命令返回值(状态)
cmd;
echo $?;
- 向命令传递参数
$ cat showArgs.sh
for i in `seq 1 $#`
do
echo $i is $1
shift
done
$ sh showArgs.sh a b c
1 is a
2 is b
3 is c
shift命令可以将参数依次向左移动一个位置,让脚本能够使用$1来访问到每一个参数。
12.将一个命令的输出发送给另一个命令
Unix shell脚本最棒的特性之一就是可以轻松地将多个命令组合起来生成输出。
当我们组合多个命令时,通常将stdin用于输入,stdout用于输出。在这种情况下,这些命令被称为过滤器(filter)。我们使用管道(pipe)连接每个过滤器,管道操作符是|。
cmd1 | cmd2 | cmd3
- 将命令序列的输出赋给变量
cmd_output=$(ls | cat -n)
echo $cmd_output
//或者
cmd_output=`ls | cat -n`
echo $cmd_output
12.1 其它命令分组的方法
- 利用子shell生成一个独立的进程,使用()操作符来定义一个子shell。
$> pwd
/
$> (cd /bin; ls)
awk bash cat...
$> pwd
/
- 通过引用子shell的方式保留空格和换行符
假设我们使用子shell或反引用的方法将命令的输出保存到变量中,为了保留输出的空格和换行符(\n),必须使用双引号。
$ cat text.txt
1
2
3
$ out=$(cat text.txt)
$ echo $out
1 2 3 # 丢失了1、2、3中的\n
$ echo "$(cat text.txt)"
1
2
3
13.在不按下回车键的情况下读入n 个字符
使用read以交互的形式读取用户输入。
- 从输入中读取n个字符并存入变量variable_name
[work@jkz ~]$ read -n 2 var
ee[work@jkz ~]$ echo $var
ee
[work@jkz ~]$ read -n 3 var
aaa[work@jkz ~]$ echo $var
aaa
- 用无回显的方式读取密码
[work@jkz ~]$ read -s var
[work@jkz ~]$ echo $var
123
- 使用read显示提示信息
[work@jkz ~]$ read -p "Enter input:" var
Enter input:34
[work@jkz ~]$ echo $var
34
- 在给定时限内读取输入
#在2秒内将键入的字符串读入变量var
read -t 2 var
- 用特定的定界符作为输入行的结束
[work@jkz ~]$ read -d ":" var
hello:[work@jkz ~]$ echo $var
hello
14.持续运行命令直至执行成功
repeat()
{
while true
do
$@ && return
done
}
函数repeat()中包含了一个无限while循环,该循环执行以函数参数形式(通过$@访问)传入的命令。如果命令执行成功,则返回,进而退出循环。
- 一种更快的做法
在大多数现代系统中,true是作为/bin中的一个二进制文件来实现的。这就意味着每执行一次之前提到的while循环,shell就不得不生成一个进程。为了避免这种情况,可以使用shell的内建命令:,该命令的退出状态总是为0。
repeat() { while :; do $@ && return; done }
- 加入延时
repeat() { while :; do $@ && return; sleep 30; done }
- 使用方法
repeat wget -c http://www.example.com/software-0.1.tar.gz
15.字段分隔符与迭代器
内部字段分隔符(Internal Field Separator,IFS)是shell脚本编程中的一个重要概念。在处理文本数据时,它的作用可不小。
IFS是一个环境变量,其中保存了用于分隔的字符。它是当前shell环境使用的默认定界字符串。
#!/bin/bash
oldIFS=$IFS
echo $oldIFS
data="name,gender,rollno,location"
IFS=,
for item in $data;
do
echo Item:$item
done
IFS=$oldIFS
[work@jkz ~]$ ./ifs.sh
Item:name
Item:gender
Item:rollno
Item:location
15.1 循环
- 面向列表的for循环
list可以是字符串,也可以是值序列。
for var in list;
do
commands; #使用变量$var
done
for i in {a..z}; do actions; done;
- 迭代指定范围数字
for((i=0;i<10;i++))
{
commands; #使用变量$i
}
- 条件为真,继续循环
while condition
do
commands;
done
- until循环:一直循环,直到给定的条件为真
x=0;
until [ $x -eq 9 ]; #条件是[$x -eq 9 ]
do
let x++; echo $x;
done
16.比较与测试
if condition;
then
commands;
fi
if condition;
then
commands;
else if condition; then
commands;
else
commands;
fi
可以使用等价的逻辑运算符:
- [ condition ] && action;
如果condition为真,则执行action - [ condition ] || action;
如果condition为假,则执行action
算术比较
比较条件通常被放置在封闭的中括号内。一定要注意在[或]与操作数之间有一个空格。
[ $var -eq 0 ] #当$var等于0时,返回真
[ $var -ne 0 ] #当$var不为0时,返回真
-gt:大于。
-lt:小于。
-ge:大于或等于。
-le:小于或等于。-a是逻辑与操作符
-o是逻辑或操作符。
[ $var1 -ne 0 -a $var2 -gt 2 ] #使用逻辑与-a
[ $var1 -ne 0 -o $var2 -gt 2 ] #逻辑或-o
文件系统相关测试
可以使用不同的条件标志测试各种文件系统相关的属性。
- [ -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
字符串比较
进行字符串比较时,最好用双中括号。
- [[ $str1 == $str2 ]] 也可以使用=(前后各有一个空格,否则就变成了赋值)
- [[ $str1 != $str2 ]]
- [[ $str1 > $str2 ]] 根据字符的ascii值进行比较
- [[ -z $str1 ]] str1为空串
- [[ -n $str1 ]] str1不为空串
test
test命令可以用来测试条件。用test可以避免使用过多的括号,增强代码的可读性。之前讲过的[]中的测试条件同样可以用于test命令。
if [ $var -eq 0 ]; then echo "True"; fi
等价于
if test $var -eq 0 ; then echo "True"; fi
注意,test是一个外部程序,需要衍生出对应的进程,而 [ 是Bash的一个内部函数,因此后者的执行效率更高。
17.使用配置文件定制bash
你在命令行中输入的绝大部分命令都可以放置在一个特殊的文件中,留待登录或启动新的bash会话时执行。将函数定义、别名以及环境变量设置放置在这种特殊文件中,是一种定制shell的常用方法。
Linux和Unix中能够放置定制脚本的文件不止一个。这些配置文件分为3类:
- 登录时执行的
/etc/profile
$HOME/.profile
$HOME/.bash_login
$HOME/.bash_profile
如果.bash_profile或.bash_login文件存在,则不会去读取.profile文件。 - 启动交互式shell时执行的
/etc/bash.bashrc
$HOME/.bashrc - 调用shell处理脚本文件时执行的
不会执行任何配置文件,除非定义了环境变量BASH_ENV
export BASH_ENV=~/.bashrc
注意,如果你是通过图形化登录管理器登入的话,是不会执行/etc/profile、$HOME/.profile和$HOME/.bash_profile这3个文件的。这是因为图形化窗口管理器并不会启动shell。当你打开终端窗口时才会创建shell,但这个shell也不是登录shell。