标签列表

everest33

自制力

导航

Linux命令行与shell脚本编程大全第三版 shell脚本专题 学习笔记(二)

※,平时记录 

获取当前脚本所在目录的绝对路径:·basepath=$(cd $(dirname $0); pwd)·

 

第二部分:shell脚本编程基础 

手册性教程:

http://c.biancheng.net/view/706.html

shell在线中文手册abs,shell中文教程,shell中文教程 

※,多行注释:

:'
这里是多行注释,经测试这种方式还是会被解析
'
<<! 这个!可以是任意字符(也可以是单词如LongComment),下面对应起来即可
这里是另一种多行注释
!

※,shell,exec,source执行脚本的区别:参考此文。可以在脚本中打印当前的进程号`$$`来验证。

  • 使用$ sh script.sh执行脚本时,当前shell是父进程,生成一个子shell进程,在子shell中执行脚本。脚本执行完毕,退出子shell,回到当前shell。$ ./script.sh$ sh script.sh等效。
  • 使用$ source script.sh方式,在当前上下文中执行脚本,不会生成新的进程。脚本执行完毕,回到当前shell。source方式也叫点命令,$ . script.sh$ source script.sh等效。
  • 使用exec command方式,会用command进程替换当前shell进程,并且保持PID不变。执行完毕,直接退出,不回到之前的shell环境。所以exec命令一般放在shell脚本里执行。
    • 一个例外,当exec命令来对文件描述符操作的时候,就不会替换shell,而且操作完成后,还会继续执行接下来的命令。 ·exec 3<&0·这个命令就是将操作符3也指向标准输入

※,shell脚本的追踪与debug:

  • ·bash -n myfile.sh·// -n参数:不执行脚本,仅检查语法问题。
  • ·bash -v myfile.sh·// -v参数:在执行脚本前,先将脚本内容输出到屏幕上(并不是一次性输出全部内容,而是执行一个命令输出一次)。这个参数和-x参数差不多,但是没有-x参数清晰(有个+号区分源码和执行结果)
  • ·bash -x myfile.sh· // -x参数:将使用到的 script 内容显示到屏幕上(也是执行一个命令输出一次),这是很有用的参数!
    • 输出中的+号后面的内容是脚本源码内容。不同级别的源码内容前面的+个数不同,第一级一个+号,第二级两个+号,以此类推。
    • 这个bash -x  就是通过 set -x (即在shell脚本中shebang之后添加一行代码:set -x)实现的...!!

※,`set`命令

https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html官方文档

set [-abefhkmnptuvxBCEHPT] [-o option-name] [--] [-] [argument …]
set [+abefhkmnptuvxBCEHPT] [+o option-name] [--] [-] [argument …]
  • ·+·号表示关闭功能,·-·号表示开启功能。
  • `set -o <command>` // 启用command命令,如`set -o history`表示启用命令的history记录功能,history这个命令功能在交互式shell中默认是开启的。
  • ·set +o <command>· // 关闭command命令,如·set +o history·表示关闭history日志记录功能,这在外部黑客入侵机器时经常会使用这个命令。
  • `set -e` // 等同于·set -o errexit·,脚本出错立即退出。使用方法:在shell脚本的shebang之后添加一行代码:`set -e`。对于管道set -e失效,需要使用:·set -o pipefail·
  • 官方文档中讲到,set -e对 command1 && command2这种形式不生效(除非出错的是最后一个命令...)。如果command1出错,程序还会继续运行;但最后一个命令出错程序会退出。可以把这个命令拆成两行来写就解决问题了。
  • 如果函数返回非0(false),那么set -e将使程序在调用函数处退出,如果函数返回了0(true),那么程序将继续运行。
  • set -e有一个例外情况,就是不适用于管道命令。解决方法:·set -o pipefail·
    所谓管道命令,就是多个子命令通过管道运算符(|)组合成为一个大的命令。Bash 会把最后一个子命令的返回值,作为整个命令的返回值。也就是说,只要最后一个子命令不失败,管道命令总是会执行成功,因此它后面命令依然会执行,set -e就失效了。
  • 函数调用并不会产生子shell,可以自己验证(通过ps --forest,通过 $$, $PPID等等可以验证)

※,使用脚本添加crontab任务的若干方法

crontab 是运维过程中常用的定时任务执行工具
 一般情况下在有新的定时任务要执行时,使用crontab -e ,将打开一个vi编辑界面,配置好后保存退出,但是在自动化运维的过程中往往需要使用shell脚本
 或命令自动添加定时任务。接下来结束三种(Centos)自动添加的crontab 任务的方法:

方法一:
编辑 /var/spool/cron/用户名 文件,如:
echo "* * * * * hostname >> /tmp/tmp.txt" >> /var/spool/cron/root
优点:简单
缺点:需要root权限

方法二:
编辑 /etc/crontab 文件,
echo "* * * * * root hostname >> /tmp/tmp.txt" >> /etc/crontab
需要注意的是,与常用的crontab 有点不同,/etc/crontab 需指定用名。而且该文件定义为系统级定时任务 不建议添加非系统类定时任务,编辑该文件也需要root权限
 
方法三:
 利用crontab -l 加 crontab file 两个命令实现自动添加
crontab -l > conf && echo "* * * * * hostname >> /tmp/tmp.txt" >> conf && crontab conf && rm -f conf
由于crontab file会覆盖原有定时任务,所以使用 crontab -l 先导出原有任务到临时文件 “conf” 再追加新定时任务
优点:不限用户,任何有crontab权限的用户都能执行
缺点:稍微复杂 




11章.构建基本脚本

※,shell脚本的第一行 #!/bin/bash 

在shell脚本的第一行中,必须写#!/bin/bash。如果是用其他shell,在修改相应的shell路径#!/bin/sh 表示本脚本由/bin/路径的sh程序来解释.... 跟命令行下~ #/bin/sh Scriptname效果相同如果不写也成,那就用你登陆的那个shell来解释执行. 可以不写,但应该有良好的编程习惯.“在很多情况中,如果没有设置好这一行,那么该程序很可能会无法执行,因为系统可能无法判断该程序需要使用什么shell来执行” -------鸟哥。所以,shell脚本第一行不写这一条语句,能不能执行就看人品。

但是对于非shell脚本,这第一行就大有学问了!参考此文,总结如下:

  • #! 是有名字的,叫做·shebang·或`sha-bang`
  • #! 后面可以有一个或多个空白字符,后接解释器的绝对路径,用于指明执行这个脚本文件的解释器。
  • `#!/usr/bin/python` //python脚本首行加了这个之后就可以./xx.py执行了,不用每次都执行 python xx.py了。另外,python(Linux,python2.7)的CGIHTTPServer模块调用py脚本作为cgi脚本时,首行不写这个会报错!
  • ·#!/usr/bin/env python· // env python 是在 `env | grep PATH`下的所有目录中寻找名为python的可执行文件,执行找到的第一个。而python则是指定的目录下(·which python·可查看)的python可执行文件。

※,bash shell中的 $(), ${}, $[], $(()), [], [[]], (()) 代表的含义:https://blog.csdn.net/taiyang1987912/article/details/39551385

  • $()和反引号作用一样,用于命令替换,就是在shell中fork 一个子进程去做括号里的命令然后再返回父进程。Shell命令替换一般用于将命令的输出结果赋值给某个变量,也可以配合其他命令使用。如:cd $(docker inspect ubuntu --format "{{.GraphDriver.Data.UpperDir}}"),先执行$()中的命令,将其输出作为cd的参数。注意cd命令是内置命令,不能配合涉及写管道的xargs命令使用,所以cd命令一般配合$()使用。
    • 反引号基本上可用在全部的 unix shell 中使用,若写成 shell script ,其移植性比较高,但反单引号容易打错或看错。
    • $()并不是所有shell都支持。
  • ${ }用于变量替换。一般情况下,$var 与${var} 并没有什么不一样,但是用 ${ } 会比较精确的界定变量名称的范围。
    • ${}还有个模式匹配功能,亦即字符串截取功能
    • 字符串替换
      • ${var/pattern/replacement} //将字符串变量var中第一个pattern替换成replacement。
      • ${var//pattern/replacement} //将字符串变量var中所有的pattern替换成replacement。
      • ·${var/$'\r'/''}· // 将变量var(字符串)中的\r替换为空, 使用\r无法完成替换。·$'\n'·$''这种格式很独特,$'\n'指的是换行符。$'\r'同理。
  • $[]和$(())是一样的,在$(())中可以操作的在$[]中也可以,都是进行数学运算的,支持+ - * / %(“加、减、乘、除、取模”),注意在其中只能作整数运算,对于浮点数是当作字符串处理的,会报错。bash中使用bc(bash calculator)命令计算浮点数。
    • $(()) 和 $[] 返回的是一个具体的运算后的值,而(()) 和 [] 和 [[]] 返回的是true或false,是用于if判断语句后面的
    • 在 $[]和$(())中的变量名称,可于其前面加 $ 符号来替换,也可以不用.如 $((a +b* c))和 $(($a + $b*$c))效果相同。
    • 此外,$[]和$(()) 还可作不同进位(如二进制、八进位、十六进制)运算,只是,输出结果皆为十进制而已。如 $((2#111)),输出7。 8#,16#分别表示8进制和16进制。
  • ============
  • [] 为test命令的另一种形式,[ expression ]。是用于if判断语句后面的
    • 必须在左括号的右侧和右括号的左侧各加一个空格,否则会报错
    • test命令使用标准的数学比较符号来表示字符串的比较,而用文本符号(-gt 等等)来表示数值的比较。
    • 大于符号或小于符号必须要转义,否则会被理解成重定向。
  •  
  • (()) :(()) 专门针对数学运算,对运算结果进行判断从而返回true或false。格式为 (( expression )),空格非必须,expression可以是 数学运算表达式 或 (数学)比较表达式。且不需要再将表达式里面的大小于符号转义。是用于if判断语句后面的
    • expression 是数学表达式时,如果计算结果为0,那么返回的退出状态码为1,即为false;而一个非零值的表达式所返回的退出状态码将为0,或者是"true"
    • expression 是比较表达式时,表达式结果为true,则返回退出状态码0,即true。表达式结果为false,则返回退出状态码1,即false。
    • (())中可以使用的数学运算符号有:+;-;*;/;%;val++ 后增;val-- 后减;++val 先增;--val 先减;! 逻辑求反;~ 位求反;** 幂运算;<< 左位移;>> 右位移;& 位布尔和;| 位布尔或;&& 逻辑和;|| 逻辑或
    • ·((3%3))·// 计算结果为0,返回退出状态码为1,false.
    • `((3>2))`// 表达式为true,返回退出状态码为0,true。
  • [[]]:[[]]可以视为 test命令的加强版,在[]中可以使用的在[[]]中也可以使用。[[ expression ]]。是用于if判断语句后面的,当然while之类的也能用
    • 和[]一样,必须在左括号的右侧和右括号的左侧各加一个空格,否则会报错
    • 和[]一样,字符串比较时,==,>,等符号两边需要空格。
    • 和[]一样,若使用了 ==,<,>等符号,则符号两边即使是数字也被视为字符串。数字比较只能用 -eq, -gt等。
    • 和[]一样,[[]]中的 >,<不用转义。
    • 双方括号在bash shell中工作良好。不过要小心,不是所有的shell都支持双方括号
    • 使用[[ ... ]]条件判断结构,而不是[ ... ],能够防止脚本中的许多逻辑错误。比如可以直接使用if [[ $a != 1 && $a != 2 ]], 如果不使用双括号, 则为if [ $a -ne 1] && [ $a != 2 ]或者if [ $a -ne 1 -a $a != 2 ]。
    • [[ ]]中针对字符串增加模式匹配(pattern matching)特性。在模式匹配中可以定义个正则表达式来匹配字符串值。
    • ====普通模式匹配:普通模式匹配不是正则,在此模式中,*代表任意多个字符,?代表单个字符======
    • ·user=tong;[[ $user == t* ]]·// *代表任意多个字符。是否以字母 t 开头,返回退出状态码0,true。
    • ·[[ tong == "t*" ]]· //如果 t* 用双引号或单引号括起来,那么就不是模式匹配了,而是字面匹配(literal match),返回1,false。
    • ·[[ tong == ton. ]]· //这里的.就是普通的字符.,没有任何含义。返回退出状态码1,false。
    • ·[[ tong == ton? ]]·// ?代表单个字符,返回0,true。
    • ====正则匹配模式:使用 =~ 开启正则匹配模式!====================
    • ·[[ tong =~ ton. ]]· //正则,返回0,true。
    •  

※,内联输入重定向( << )

内联输入重定向允许你直接在命令行中重定向数据。格式如下:

内联输入重定向 (英文为 Here Document ) 允许你直接在命令行中重定向数据。格式如下:

command << delimiter
document
delimiter
它的作用是将两个 delimiter 之间的内容(document) 作为输入传递给 command

注意:结尾的delimiter 一定要顶格写,前面和后面都不能有任何字符(包括空格和 tab 缩进)。开始的delimiter前后的空格会被忽略掉。

cat << EOF
line1
line2
line3 word1
EOF
# EOF 标记了内联重定向的开始和结束,可是使用任意的字符串来标记,只要相同就可以。

更常用的方式:
cat >> logFile << EOF
...
EOF
# 注意>>logFile这部分和Here Doc没有关系,是配合Here Doc命令使用的(追加文档,若是>logFile则是覆盖文档)。cat命令也可以是其他命令。

如果脚本中有shell命令($()包裹的命令),则命令会被解析。如果不想命令被解析,可以将始的delimiter用双引号引起来,如

# 如果不用双引号将EOF引起来,那么写入shell.sh中的将是USER_IP=192.168.35.36这种解析后的结果,
# 而使用了双引号则会将命令原样写入shell.sh文件中。
cat >> shell.sh << "EOF"
USER_IP=$(who -u am i 2>/dev/null| awk '{print $NF}'|sed -e 's/[()]//g')
EOF

※,"<<<" (和"<<"使用方法一样,区别就在于一个是一行,一个是多行): 在BASH文档中,称之为 "Here Strings"。Here String是Here Documents 的一个变种。它由操作符"<<<"和作为标准输入的字符串构成,作用是将一个普通字符串重定向到command命令。使用场景如:sed命令处理一个字符串。 command <<<  "WORD" // WORD也可以是执行某个命令的输出,如 command <<< $(pwd)。

  • 示例0:
    grep -q redis-server <<< $(ps -ef) //可以在if条件是判断redis-server进程是否存在。如果存在返回0(true),如果不存在返回1(false)
    示例一:
    [root@localhost scripts]$a=HelloWorld
    [root@localhost scripts]$sed -n p <<< $a
    HelloWorld
    [root@localhost scripts]$
    示例二:
    cat >> /tmp/a <<< $(ps -ef) //将ps -ef命令的输出追加到/tmp/a文件中。
    示例三:
    以下命令相同
    # mysql -u root   -e      "select user,host from mysql.user;"
    # mysql -u root   <<<    "select user,host from mysql.user;"

 

※,浮点数计算:bc计算器。

  • bash计算器实际上是一种编程语言,它允许在命令行中输入浮点表达式,然后解释并计算该表达式,最后返回结果。
  • 浮点运算是由内建变量 scale 控制的。必须将这个值设置为你希望在计算结果中保留的小数位数,否则无法得到期望的结果。

脚本中使用bc计算器: 可以用命令替换运行 bc 命令,并将输出赋给一个变量。基本格式如下:`variable=$(echo "options; expression" | bc)`。第一部分 options 允许你设置变量。如果你需要不止一个变量,可以用分号将其分开。expression 参数定义了通过 bc 执行的数学表达式。

例一:

$ cat test9
#!/bin/bash
var1=$(echo "scale=4; 3.44 / 5" | bc)
echo The answer is $var1
$

例二:

#!/bin/bash
var1=10.46
var2=43.67
var3=33.2
var4=71
var5=$(bc << EOF
scale = 4
a1 = ( $var1 * $var2)
b1 = ($var3 * $var4)
a1 + b1
EOF
)
echo The final answer for this mess is $var5
# 必须用命令替换符号标识出用来给变量赋值的命令。

※,管道:command1 | command2

  • 管道是将一个命令的输出重定向到另一个命令中。不要以为由管道串起的两个命令会依次执行。Linux系统实际上会同时运行这两个命令,在系统内部将它们连接起来。在第一个命令产生输出的同时,输出会被立即送给第二个命令。数据传输不会用到任何中间文件或缓冲区。

※,退出脚本:shell中运行的每个命令都使用退出状态码(exit status)告诉shell它已经运行完毕。退出状态码是一个0~255的整数值,在命令结束运行时由命令传给shell。可以捕获这个值并在脚本中使用。

  • 1,查看退出状态码:Linux提供了一个专门的变量 $? 来保存上个已执行命令的退出状态码。对于需要进行检查的命令,必须在其运行完毕后立刻查看或使用 $? 变量。它的值会变成由shell所执行的最后一条命令的退出状态码。按照惯例,一个成功结束的命令的退出状态码是 0 。如果一个命令结束时有错误,退出状态码就是一个正数值。
  • 1.1, exit命令:默认情况下,shell脚本会以脚本中的最后一个命令的退出状态码退出。你可以改变这种默认行为,返回自己的退出状态码。 exit 命令允许你在脚本结束时指定一个退出状态码。如exit 5;在运行完脚本后立即查看$?可以看到其值为作为参数传给 exit 命令的值,即5,exit的参数可以是变量,但其大小只能是0-255,超出的部分将会除以256然后取模,如256会被当做0,257为1。
  • 总结:exit命令有两个作用,一是中断脚本执行,二是自定义脚本退出状态码(不给exit传参数时默认为0)。

※,



12章. 使用结构化指令

※,使用if - then 语句

bash shell的if语句有两种使用方式:1是后面跟命令;2是后面跟test命令(test命令即[ ]形式),具体如下叙述

if使用方式一:bash shell的 if 语句会运行 if 后面的那个命令。如果该命令的退出状态码是 0(该命令成功运行),位于 then部分的命令就会被执行。如果该命令的退出状态码是其他值,then部分的命令就不会被执行,bash shell会继续执行脚本中的下一个命令。 fi 语句用来表示 if-then语句到此结束。

if使用方式二:if test condition 形式: test 命令是一个特殊的命令,可以用来判断某些条件是否成立。如果 test 命令中列出的条件成立,test 命令就会退出并返回退出状态码 0 ,这样if语句就会执行;如果条件不成立, test 命令就会退出并返回非零的退出状态码,这样if语句就不会执行

if语句的格式如下:

### if 语句都有两种格式,要么关键词单独放一行,要么通过分号隔开 ###
# if - then 格式1:
if command
then
commands
fi

# if -then 格式2:
if command; then
commands
fi

# if-then-else 格式1:
if command
then
commands
else
commands
fi

# if-then-else 格式2:
if command; then
commands; else
commands
fi

# if - then -elif - then - elif - then - else
if  command1
then
command set 1
elif  command2
then
command set 2
elif  command3
then
command set 3
elif  command4
then
command set 4
else
command set 5
fi
##在 elif 语句中,紧跟其后的 else 语句属于 elif 代码块。它们并不属于之前的if-then 代码块。
  • `if sh -c "exit 0"; then echo Shell exited as true; else echo Shell exited as false; fi` // exit 零值 会被shell的if判断为true
  • `if sh -c "exit 1"; then echo Shell exited as true\!; else echo Shell exited as false; fi` //  exit 非零值 会被shell的if判断为false
  • // 判断是否安装了某个程序。! 与 type之间有个空格是必须的
    if ! type node >/dev/null 2>&1; then
        echo 'node 未安装';
    else
        echo 'node 已安装';
    fi
  •  

※,case

case 命令会采用列表格式来检查单个变量的多个值。两个分号不是错误,而是语法就是这样的。

case variable in
    pattern1 | pattern2 ) commands1 ;;
    pattern3 ) commands2 ;;
    *) default commands ;;
esac

,test指令:test命令可以测试某个条件是否成立。

★,test命令在bash shell中有一个等同语法:方括号,注意前方括号后和后方括号前必须都要有个空格。$?  表示上一个命令的返回值,0表示成功(if中判断为true),其他表示各种异常(if中判断为false)。 另外:·$!· 表示Shell最后运行的后台Process的PID

  • 如果不写 test 命令的 condition 部分,它会以非零的退出状态码退出。false
  • `a="hello"; test a` //返回0。true
  • ·a=""; test a·//返回非0。false

★,test指令可以用来作:1,数值比较;2,字符串比较;3,文件比较

test命令的数值比较功能
比较(-eq等两边需要空格,并且eq前面有个短横线!) 描述
n1 -eq n2 检查 n1 是否与 n2 相等
n1 -ge n2 检查 n1 是否大于或等于 n2
n1 -gt n2 检查 n1 是否大于 n2
n1 -le n2 检查 n1 是否小于或等于 n2
n1 -lt n2 检查 n1 是否小于 n2
n1 -ne n2 检查 n1 是否不等于 n2

例子

  • =====数值比较:数值比较可以使用数字和变量========
  • ·[ 3 -gt 2 ]· //返回0,true.
  • `a=4; [ $a -gt 1 ]` //返回0,true.
  • `[ 5.5 -gt 4 ]`// 报错。bash shell只能处理整数,不能在test 命令中使用浮点值。

 

test命令的字符串比较功能
比较(==,!=, >等操作符两边需要空格!) 描述
str1 = str2(或 str1 == str2) 检查 str1 是否和 str2 相同
str1 != str2 检查 str1 是否和 str2 不同
str1 < str2 检查 str1 是否比 str2 小(在ASCII字母顺序下)
str1 > str2 检查 str1 是否比 str2 大(在ASCII字母顺序下)

大于号和小于号必须转义,否则shell会把它们当作重定向符号,把字符串值当作文件名。

大于和小于顺序和 sort 命令所采用的不同

`[ "a" > "b" ]`//这个脚本中只用了大于号,没有出现错误,但结果是错的。

脚本把大于号解释成了输出重定向,因此它创建了一个名为b的文件。由于重定向顺利完成

test命令返回了退出状态码0。p.s. "a","b"可以不用加引号

-n str1 检查 str1 的长度是否非0
-z str1 检查 str1 的长度是否为0
例子
  • `[ b \> a ]`//这里的b和a无需加引号,会被自动识别为字符串而不是变量。返回0,true.
  • `[ A \> a ]`//返回1,false.test命令中使用的是标准的ASCII顺序,根据每个字符的ASCII数值来决定排序结果小写字母大于大写字母。但是sort命令刚好相反
  • -n 和 -z 可以检查一个变量是否含有数据
  • `[ -n d ]`//d在这里是字符串d非变量d。返回0,true.
  • ·[ -n "" ]·// 返回非0,false.
  • `[ 3 == 3 ]`//返回0,true。这里使用==符号,则3是字符串而非数字
  • ·[ 3 \> 13 ]·//返回非0,false。这里使用了>符号,则3,13都是字符串。

 

 

test命令的文件比较功能
-e file 检查file是否存在(文件或目录皆可)
-d file 检查file是否存在且是一个目录
-f file 检查file是否存在且是一个文件
-s file 检查file是否存在且非空
-r file 检查file是否存在且可读
-w file 检查file是否存在且可写
-x file 检查file是否存在且可执行
-O file 检查file是否存在且属当前用户所有
-G file

检查file是否存在且文件的默认组与当前用户的默认组相同

(这里注意-G只会比较用户的默认组,而不是用户所属的所有组,这让人有点困惑)

  • /etc/passwd的第四个字段表示用户所属的默认组。/etc/group的第四个字段列出了属于某个组的所有用户(但是除了以这个组作为默认组的用户,这些用户被默认省略了)
  • 假如文件myFile默认组为groupA,若当前用户默认组为groupA,则test -G myFile返回0,表示成功,假如当前用户的默认组为groupB,即使当前用户也属于groupA,也返回1,表示失败。
file1 -nt file2 检查file1 是否比 file2 新,注意这个命令不会判断file1和file2是否存在,使用时需要首先判断文件是否存在,否则会得到错误的结果。
file1 -ot file2 检查file1 是否比 file2 旧,同上注意事项。
 例子  
windows@Tonus:~/shellScript$ ll
总用量 20
drwxrwxr-x  2 windows windows 4096  3月 18 10:06 ./
drwxr-xr-x 24 windows windows 4096  3月 18 09:50 ../
-rwxrwxr-x  1 windows windows  133  3月 18 09:50 forShell*
-rwxrwxr-x  1 windows windows  502  3月 17 10:39 ifShell.sh*
-rw-rw-r--  1 windows windows   54  3月 18 09:50 sortFile
windows@Tonus:~/shellScript$ [ -f sortFile ];echo $?
0
windows@Tonus:~/shellScript$ test -f sortFile;echo $?
0
windows@Tonus:~/shellScript$ [ -d sortFile ];echo $?
1
windows@Tonus:~/shellScript$ test -d sortFile;echo $?
1
windows@Tonus:~/shellScript$ test -f a b;echo $? //这里参数过多,只能有一个参数
-bash: test: a:需要二元表达式
2
windows@Tonus:~/shellScript$ test -f a;echo $? //a不存在
1
windows@Tonus:~/shellScript$ 

※,符合条件测试

  • ·if [ condition1 ] && [ condition2 ]·//布尔AND
  • ·if [ condition1 ] || [ condition2 ]·//布尔OR

※,Linux多个命令执行顺序

  • 顺序执行多条命令:command1;command2;command3
    • 用;号隔开每个命令, 每个命令按照从左到右的顺序,顺序执行, 彼此之间不关心是否失败, 所有命令都会执行。
  • 有条件执行多条命令: command1 && command2 || command3; 可以使用括号改变优先级。
    • && 表示 在前一个命令执行成功后才会执行第二个命令;
    • || 表示 在前一个命令执行失败后才会执行第二个命令(第一个为真第二个就无需执行了)。
      gitt不存在,其他都存在
      # which gitt && which git || which tree
      /usr/bin/tree
      
      # which gitt && (which git || which tree)
      无输出
      
      # which which && (which git || which tree)
      /usr/bin/which
      /usr/bin/git
      
      # which which && which git || which tree
      /usr/bin/which
      /usr/bin/git
    • #### ~/.bashrc需要添加下面的指令,$-表示当前 shell 的选项标志(set命令可以设置)。
      #### 此脚本含义是,如果shell是以交互式(i)方式(tty)使用时,则脚本继续往下走,读取后续的命令;
      #### 而如果shell是以非交互式方式(non-tty)方式使用时,则return,提前退出此脚本。
      #### 比如sftp服务端就是以non-tty方式使用shell的,sftp服务端会读取~/.bashrc脚本,
      #### (实际解决的一个问题:堡垒机89.9无法上传文件的问题(sftp协议传输过程被bash中的echo语句污染导致))
      #### 如果这里的脚本有输出字符,则会导致sftp服务端将这些字符发送给sftp客户端,导致客户端无法解析(packet too long)
      #### 导致无法上传文件!可以参考此文:https://www.ittsystems.com/troubleshooting-received-message-too-long/#wbounce-modal
      # If not running interactively, don't do anything and return early
      [[ $- == *i* ]] || return
      
      #### [[ $- == *i* ]] 返回0或1
      #### 0代表true,返回0 表示是以交互方式运行,不会执行后面的return;
      #### 1代表false,返回1 表示是以非交互方式运行,会执行后面的return提前退出。
      
      另外也可以如下设置:
      # If not running interactively, don't do anything
      case $- in
          *i*) ;;
            *) return;;
      esac


13. 更多的结构化指令(for,while,until)

※,环境变量IFS(internal field separator内部字段分隔符),定义了bash shell用作字段分隔符的一系列字符。默认情况下bash shell会将 空格符、制表符、换行符 当做字段分隔符。读取文件中的内容时常常需要按照需求定义这个环境变量里。可以按需要修改IFS的值,用法如下:

  • 以下自测时的环境为:Ubuntu 20.10
  • IFS=$'\n' //将换行符视为字段分隔符,$''($加单引号)语法格式 是bash shell中的很特殊的一种用法
  • IFS=$"\n" // 将反斜杠和字母n作为字段分隔符
  • IFS='\n' // 同上,将反斜杠和字母n作为字段分隔符
  • IFS="\n" // 同上,将反斜杠和字母n作为字段分隔符
  • IFS=\n //将字母n作为字段分隔符
  • IFS=\\n  //将反斜杠和字母n作为字段分隔符
  • IFS=$'\n'$'\t' //将换行符和制表符作为字段分隔符
  • IFS=$'\n'.:;" //将换行符,点号,冒号,分号,双引号(都)作为字段分隔符
  • `echo "$IFS"|od -b`
0000000 040 011 012 012  
0000004
直接输出IFS是看不到值的,转化为二进制就可以看到了,"040"是空格,"011"是Tab,"012"是换行符"\n" 。最后一个 012 是因为 echo 默认是会换行的。

关于使用IFS将字符串拆成数组的说明:

  • 比如字符串为:·a,\n  b,\n  ·即一个字母接一个换行符再接两个空格这种形式的字符串。现在想把 a,b这两个字母放在一个数组变量里面,可以定义`IFS=\ ,$'\n'`,即空格(需转义)逗号换行符,而且这三个符号也没有顺序,但是不能把整体用引号引起来。

※,使用通配符读取目录

可以使用for命令来自动遍历目录中的文件。进行此操作时,必须在文件名或路径名中使用通配符。它会强制shell使用文件扩展匹配。文件扩展匹配是生成 匹配指定通配符 的文件或路径名的过程。

windows@Tonus:~/shellScript$ pwd
/home/windows/shellScript
windows@Tonus:~/shellScript$ ll
总用量 32
drwxrwxr-x  2 windows windows  4096  3月 18 10:48  ./
drwxr-xr-x 24 windows windows  4096  3月 18 10:48  ../
-rw-rw-r--  1 windows windows     0  3月 18 10:09 'a b'
-rwxrwxr-x  1 windows windows   510  3月 18 10:46  forShell*
-rw-r--r--  1 windows windows 12288  3月 18 10:49  .forShell.swp
-rwxrwxr-x  1 windows windows   502  3月 17 10:39  ifShell.sh*
-rw-rw-r--  1 windows windows     0  3月 18 10:10  log
-rw-rw-r--  1 windows windows    59  3月 18 10:25  sortFile
windows@Tonus:~/shellScript$ vi forShell 

for file in /home/windows/shellScript/*
do
    # 这里$file需要用双引号引起来,不然含有空格的文件或目录会报错,Linux下的文件或目录可以含有空格
    if [ -f "$file" ];then
        echo "$file " is a file
    elif test -d "$file";then
        echo "$file " is a directory
    elif [[ "$file" == t* ]];then
        continue;# 如果文件名称以t开头,则跳过本次循环继续下次循环。同样的 break 表示结束本次循环,后面的语句将不再进行。
    fi
done
for line in *Shell*;do echo $line;done;

windows@Tonus:~/shellScript$ ./forShell 
/home/windows/shellScript/a b  is a file
/home/windows/shellScript/forShell  is a file
/home/windows/shellScript/ifShell.sh  is a file
/home/windows/shellScript/log  is a file
/home/windows/shellScript/sortFile  is a file
=================================
forShell
ifShell.sh
windows@Tonus:~/shellScript$ 

※,C语言风格的for 命令:C语言风格的 for 命令有些部分并没有遵循bash shell标准的 for 命令

  • 变量赋值可以有空格(例子中的 i = 1);
  • 条件中的变量不以美元符开头;
  • 迭代过程的算式未用 expr 命令格式(expr i <= 10)。
for (( i = 1; i <= 10; i++ ))
do
    echo "The next number is $i"
done

# 使用多个变量
for (( a=1, b=10; a <= 10; a++, b-- ))
do
    echo "$a - $b"
done

`for ((i = 1;i < 10;i++));do echo line$i>>data.txt; done`

※,实例

1. 查找可执行文件

#!/bin/bash
# finding files in the PATH
IFS=:
for folder in $PATH
do
    echo "$folder:"
    for file in $folder/*
    do  
        if [ -x $file ]
        then
            echo " $file"
        fi  
    done
done

2. 创建多个用户

#!/bin/bash
# process new user accounts
input="users.csv"
while IFS=',' read -r userid name
do
echo "adding $userid"
useradd -c "$name" -m $userid
done < "$input"

#注释:read 命令会自动读取users.csv文本文件的下一行内容,所以不需要专门再写一个循环来处理。当read 命令返回 FALSE 时(也就是读取完整个文件时), while 命令就会退出。
#要想把数据从文件中送入 while 命令,只需在 while 命令尾部使用一个重定向符就可以了!
$ cat users.csv
rich,Richard Blum
christine,Christine Bresnahan
barbara,Barbara Blum
tim,Timothy Bresnahan

3. 读取用户输入,错了重新输入

## $'\n' 换行
while [[ $os_type -ne 1  && $os_type -ne 2 ]];do
    read -p $'1)centos7   2)ubuntu20\n请选择当前操作系统,输入1或2\n' os_type
done


14,处理用户输入

※,获取当前shell执行脚本的名字的几种方法

当我们编写shell脚本时,有时候需要获取当前执行脚本的名字,最常用的应该就是$0了,但是根据执行脚本的方式不同,这种方式是有缺陷的,我们执行脚本的可能方式有:

  • ./script.sh
  • . script.sh
  • source script.sh

这几种方式的$0不尽相同,先总结如下:

方法 描述
$0 only works when user executes “./script.sh”
$BASH_ARGV only works when user executes “. script.sh” or “source script.sh”
${BASH_SOURCE[0]} works on both cases.
readlink -f useful when symbolic link is used

※,命令行参数:添加在命令后的数据

  • 位置参数(positional paramter):$0(脚本名称), $1....$9, ${10}, ${11}....
    basename 命令会返回不包含路径的脚本名
    $ cat test5b.sh
    #!/bin/bash
    # Using basename with the $0 parameter
    #
    name=$(basename $0)
    echo
    echo The script name is: $name
    #
    $ bash /home/Christine/test5b.sh
    The script name is: test5b.sh
    $
    $ ./test5b.sh
    The script name is: test5b.sh
    $ 
  •  if [ -n "$1" ] // 使用 -n 检查参数是否存在

  • if [ $# -ne 2 ] //检查变量个数是否不等于2,特殊变量 $# 含有脚本运行时携带的命令行参数的个数。
    • 理论上讲,$#代表变量总个数,那么${$#}代表的就是最后一个参数。但实际上是${!#}代表最后一个参数,可能是因为bash中 ${}中不能使用$符号。
  • $* 和 $@ 变量可以用来轻松访问所有的参数(不包含$0,$0是脚本名称,永远不会变。$0出现在shell的函数中也是表示脚本名称,不会改变)。这两个变量都能够在单个变量中存储所有的命令行参数。

    • $* 变量会将命令行上提供的所有参数当作一个单词保存。这个单词包含了命令行中出现的每一个参数值。基本上 $* 变量会将这些参数视为一个整体,而不是多个个体。
    • $@ 变量会将命令行上提供的所有参数当作同一字符串中的多个独立的单词。这样你就能够遍历所有的参数值,得到每个参数。这通常通过 for 命令完成。
      $ cat test12.sh
      #!/bin/bash
      # testing $* and $@
      #
      echo
      count=1
      #
      for param in "$*"
      do
      echo "\$* Parameter #$count = $param"
      count=$[ $count + 1 ]
      done
      #
      echo
      count=1
      #
      for param in "$@"
      do
      echo "\$@ Parameter #$count = $param"
      count=$[ $count + 1 ]
      done
      $
      $ ./test12.sh rich barbara katie jessica
      $* Parameter #1 = rich barbara katie jessica
      $@ Parameter #1 = rich
      $@ Parameter #2 = barbara
      $@ Parameter #3 = katie
      $@ Parameter #4 = jessica
      $
  • 使用shift命令移动参数:默认情况下它会将每个参数变量向左移动一个位置。所以,变量 $3的值会移到 $2 中,变量 $2 的值会移到 $1 中,而变量 $1 的值则会被删除(注意,变量 $0 的值,也就是程序名,不会改变)。这是遍历命令行参数的另一个好方法,尤其是在你不知道到底有多少参数时。你可以只操作第一个参数,移动参数,然后继续操作第一个参数。例子如下:
    $ cat test13.sh
    #!/bin/bash
    # demonstrating the shift command
    echo
    count=1
    while [ -n "$1" ]
    do
    echo "Parameter #$count = $1"
    count=$[ $count + 1 ]
    shift
    done
    $
    $ ./test13.sh rich barbara katie jessica
    Parameter #1 = rich
    Parameter #2 = barbara
    Parameter #3 = katie
    Parameter #4 = jessica
    $
    • 使用 shift 命令的时候要小心。如果某个参数被移出,它的值就被丢弃了,无法再恢复。
    • 可以一次性移动多个位置,只需要给 shift 命令提供一个参数,指明要移动的位置数就行了。如 shift 2

  • 1

※,命令行选项:选项是跟在单破折线后面的单个字母,它能改变命令的行为。(处理命令行选项有三种方法:1像参数一样处理,2getopt命令,3getopts命令)

1. 首先,命令行选项和命令行参数地位是一样的。都会占用$1这些位置参数。如果一个脚本同时使用了命令行参数和命令行选项,Linux区分它们的标准方式是用特殊字符双破折线(--)。shell会用双破折线来表明选项列表结束,剩余的则被当做命令行参数。

2. 处理带值的选项。(脚本中兼容下即可,具体见下面代码)

#!/bin/bash
# $1必须用双引号
while [ -n "$1" ];do
    case "$1" in
        -a) echo "found -a option";;
        -b) echo "found -b option with paramter $2"
            shift;;
        -c) echo "found -c option";;
        -d) echo "found -d option";;
        --) shift
            break;;
         *) echo "$1 is not a option";;
    esac
    shift
done
echo
count=1
echo $@
for param in "$@";do
    # 必须用引号包起来
    echo "#param$count = $param"
    count=$((count + 1))
done

 

3. 将多个选项放进一个参数中,

※,使用 getopt 命令处理命令行选项和参数(注意:getopt 命令有一个更高级的版本叫作 getopts)

·getopt optstring parameters· // 在 optstring 中列出你要在脚本中用到的每个命令行选项字母。然后,在每个需要参数值的选项字母后加一个冒号。 getopt 命令会基于你定义的 optstring 解析提供的参数。

例子:

$ getopt ab:cd -a -b test1 -cd test2 test3
-a -b test1 -c -d -- test2 test3
$

optstring 定义了四个有效选项字母:a、b、c和d。冒号(:)被放在了字母b后面,因为b选项需要一个参数值。当getopt命令运行时,它会检查提供的参数列表( -a -b test1 -cd test2 test3 ),并基于提供的 optstring 进行解析。注意,它会自动将 -cd 选项分成两个单独的选项,并插入双破折线来分隔行中的额外参数。

如果指定了一个不在 optstring 中的选项,默认情况下, getopt 命令会产生一条错误消息。

$ getopt ab:cd -a -b test1 -cde test2 test3
getopt: invalid option -- e
-a -b test1 -c -d -- test2 test3
$
如果想忽略这条错误消息,可以在命令后加 -q 选项。
$ getopt -q ab:cd -a -b test1 -cde test2 test3
-a -b 'test1' -c -d -- 'test2' 'test3'
$

使用getopt命令的脚本如下:

#!/bin/bash
# 将原始脚本的命令行参数传给 getopt 命令,之后再将 getopt 命令的输出传给 set 命令,用 getopt 格式化后的命令行参数来替换原始的命令行参数set -- $(getopt -q ab:cd "$@")
# $1必须用双引号
while [ -n "$1" ];do
    case "$1" in
        -a) echo "found -a option";;
        -b) echo "found -b option with paramter $2"
            shift;;
        -c) echo "found -c option";;
        -d) echo "found -d option";;
        --) shift
            break;;
         *) echo "$1 is not a option";;
    esac
    shift
done
echo
count=1
echo $@
for param in "$@";do
    # 必须用引号包起来,否则#开头被视为注释
    echo "#param$count = $param"
    count=$((count + 1))
done

windows@Tonus:~/shellScript$ bash paramShell.sh -a -b good p1 p2 p3 -cd
found -a option
found -b option with paramter 'good'
found -c option
found -d option

'p1' 'p2' 'p3'
#param1 = 'p1'
#param2 = 'p2'
#param3 = 'p3'
windows@Tonus:~/shellScript$

在 getopt 命令中仍然隐藏着一个小问题。看看这个例子。
$ ./test18.sh -a -b test1 -cd "test2 test3" test4
Found the -a option
Found the -b option, with parameter value 'test1'
Found the -c option
Parameter #1: 'test2
Parameter #2: test3'
Parameter #3: 'test4'
$
getopt 命令并不擅长处理带空格和引号的参数值。它会将空格当作参数分隔符,而不是根据双引号将二者当作一个参数。更高级的 getopts 命令可以解决这个问题

※,使用更高级的 getopts:getopts 命令(注意是复数)内建于bash shell,扩展了 getopt 命令。 

getopts 命令的格式如下:
·getopts optstring variable· // optstring 值类似于 getopt 命令中的那个。有效的选项字母都会列在 optstring 中,如果选项字母要求有个参数值,就加一个冒号。要去掉错误消息的话,可以在 optstring 之前加一个冒号。 getopts 命令将当前参数保存在命令行中定义的 variable 中。

每次调用getopts 时,它一次只处理命令行上检测到的一个参数。处理完所有的参数后,它会退出并返回一个大于0的退出状态码。这让它非常适合用解析命令行所有参数的循环中.

getopts 命令会用到两个环境变量。如果选项需要跟一个参数值, OPTARG 环境变量就会保存这个值。 OPTIND 环境变量保存了参数列表中 getopts 正在处理的参数位置。这样你就能在处理完选项之后继续处理其他命令行参数了.

  • 将选项字母和参数值放在一起使用,而不用加空格.   ./test19.sh -abtest1
  • getopts可以在参数值中包含空格../test19.sh -b "test1 test2" -a
  • getopts 还能够将命令行上找到的所有未定义的选项统一输出成问号.

getopts 命令知道何时停止处理选项(getopt命令会格式化选项和参数,通过双破折号--来区分选项和参数,而getopts命令只处理选项。),并将参数留给你处理。在 getopts 处理每个选项时,它会将 OPTIND 环境变量值增一(OPTIND变量初始值为1)。在 getopts 完成处理时,你可以使用 shift 命令和 OPTIND 值来移动参数。

#!/bin/bash
echo $OPTIND#初始值为1
while getopts :abc: opt;do
    case "$opt" in
        a) echo "Found the -a option";;
        b) echo "Found the -b option";;
        c) echo "found the -c option with paramter $OPTARG";;
        *) echo "unknown option $opt";;
    esac
done
shift $[$OPTIND - 1] #初始值为1 所以-1
echo $@
count=1
for param in "$@";do
    echo "param#$count: $param"
    # 以下三种写法皆可
    #count=$[count+1]
    #count=$[$count +1]
    count=$((count+1))
done

windows@Tonus:~/shellScript$ bash getopts.sh -ac "good bad" bug -b bad ugly
OPTIND初始值为:1
Found the -a option
found the -c option with paramter good bad
bug -b bad ugly
param#1: bug
param#2: -b
param#3: bad
param#4: ugly
windows@Tonus:~/shellScript$

※,获取用户输入

尽管命令行选项和参数是从脚本用户处获得输入的一种重要方式,但有时还需要和用户进行交互,bash shell为此提供了read命令。read命令从标准输入(键盘)或另一个文件描述符中接收输入,收到输入后,read命令会将数据放进一个变量。

  • ·read <variable>· // 将用户输入放进变量variable中。
  • ·read -p "Please Enter Your Name: " <variable>· // -p指定提示文字
    • 产生换行符,使用·$''·语法 `read -p $'Please Enter \x0a your name\n'`// `\x0a和\n·都可以在`$''`中换行。echo命令也可以如此用
  • ·read <variable1> <variable2> ... · // read会自动将用户输入的数据分配给指定的变量,如果变量不够,则剩余的数据就全部分配给最后一个变量。
  • ·read · //不指定任何变量,则read命令会将它收到的任何数据都放进特殊环境变量 REPLY 中。
    read -p "IwillUseREPLY2saveWhatUSaid:"
    echo you just said $REPLY
  • ·read -t <seconds>· // -t参数指定了read命令等待输入的秒数,当计时器过期后,read命令会返回一个非零退出状态码。
    if read -p "IwillUseREPLY2saveWhatUSaid:" -t 3;then
    echo you just said: $REPLY
    else echo ;echo "timeout error!too slow"
    fi
  • ·read -n1· //-n参数指定一个数字,当read命令读取指定的长度的用户输入字符后就自动退出,然后将输入的数据赋值给变量。
    #!/bin/bash
    # getting just one character of input
    #
    read -n1 -p "Do you want to continue [Y/N]? " answer
    case $answer in
    Y | y) echo
    echo "fine, continue on…";;
    N | n) echo
    echo OK, goodbye
    exit;;
    esac
    echo "This is the end of the script"
  • ·read -s <variable>· // -s 选项可以避免在 read 命令中输入的数据出现在显示器上(实际上,数据会被显示,只是read 命令会将文本颜色设成跟背景色一样)。
  • ·cat <file> | read -p "read from a file" line· // 从文件中读取一行。每次调用 read 命令,它都会从文件中读取一行文本。当文件中再没有内容时, read 命令会退出并返回非零退出状态码。读取全部行使用while循环,代码如下:
    #!/bin/bash
    count=1
    cat log | while read line;do #while 循环会持续通过 read 命令处理文件中的行,直到 read 命令以非零退出状态码退出
    echo "Line $count: $line"
        count=$[$count+1]
    done
    echo "finish processing the file"


第15章:呈现数据

※,标准文件描述符

Linux系统将每个对象当作文件处理。这包括输入和输出进程。Linux用文件描述符( file descriptor )来标识每个文件对象。文件描述符是一个非负整数,可以唯一标识会话中打开的文件。每个进程一次最多可以有九个文件描述符。出于特殊目的,bash shell保留了前三个文件描述符( 0 、 1 和 2 )。0代表标准输入STDIN,1代表标准输出STDOUT,2代表标准错误STDERR。这三个特殊文件描述符会处理脚本的输入和输出。shell用它们将shell默认的输入和输出导向到相应的位置

1. stdin

STDIN 文件描述符代表shell的标准输入。对终端界面来说,标准输入是键盘。shell从 STDIN文件描述符对应的键盘获得输入,在用户输入时处理每个字符。
在使用输入重定向符号( ·<· 等同于`0<`)时,Linux会用重定向指定的文件来替换标准输入文件描述符。它会读取文件并提取数据,就如同它是键盘上键入的。

  • ·&0·代表标准输入指向的文件,默认是键盘(Linux中键盘也是文件)

2. stdout

STDOUT 文件描述符代表shell的标准输出。在终端界面上,标准输出就是终端显示器。shell的所有输出(包括shell中运行的程序和脚本)会被定向到标准输出中,也就是显示器。
默认情况下,大多数bash命令会将输出导向 STDOUT 文件描述符。你可以用输出重定向来改变。通过输出重定向符号(·>·等同于`1>`),通常会显示到显示器的所有输出会被shell重定向到指定的重定向文件。你也可以将数据追加到某个文件。这可以用 >> 符号来完成。

  • ·&1·代表标准输出指向的文件,默认是显示器(Linux中显示器也是文件)

3. stderr

shell通过特殊的 STDERR 文件描述符来处理错误消息。 STDERR 文件描述符代表shell的标准错误输出。shell或shell中运行的程序和脚本出错时生成的错误消息都会发送到这个位置。
默认情况下, STDERR 文件描述符会和 STDOUT 文件描述符指向同样的地方(尽管分配给它们的文件描述符值不同)。也就是说,默认情况下,错误消息也会输出到显示器输出中。但重定向两者是分开的。

  • ·&2·代表标准错误指向的文件,默认也是显示器(Linux中显示器也是文件)
  • 标准错误重定向符号为:`2>`
  • --------------------------------------------------------------------
  • `cat < a.txt` //输入重定向
  • `ls -al 1>data.txt 2>err.txt` //输出和错误分别重定向,1>必须在一起,不能有空格,否则会报错。2>也一样。
  • ·ls -al &> b.txt· // &> 将STDOUT 和 STDERR的输出重定向到同一个文件。错误输出优先级更高,集中在文件前面。

※,在脚本中使用重定向(重定向输入输出):

  • ·echo "This is an error message >&2· // 脚本中的这句代码会将消息内容由标准输出重定向至标准错误,如果 ./script.sh 2>errLog,则这句代码会将输出写入errLog中。&2代表标准错误指向的文件,默认即显示器。同理,&0代表标准输入指向的文件,默认即键盘。
  • ·ls testFile, badFile >&2 2>c.txt· // ·>&2· 等同于·1>&2·整体必须在一起,不能用空格,表示将标准输出重定向至 标准错误代表的文件。注意shell执行命令时从前往后依次解释,即>&2时,由于此时标准错误2指向的显示器,所以标准输出实际被重定向至显示器,然后2> c.txt表示将标准错误重定向至c.txt文件。最终结果是标准输出至显示器,标准错误输出至c.txt文件中。
  • ==========exec命令可以将不同的文件描述符(0到8)指定到不同的地方================
  • ·exec 1> d.txt· // exec命令可以告诉shell在脚本执行期间将标准输出重定向至d.txt文件中,exec 命令会启动一个新shell并将 STDOUT 文件描述符重定向到文件。脚本中发给 STDOUT 的所有输出会被重定向到文件。
  • ·exec 0< e.txt· // 将 STDIN 从键盘重定向到e.txt文件中。
    #!/bin/bash
    # redirecting file input
    exec 0< testfile
    count=1
    while read line
    do
    echo "Line #$count: $line"
    count=$[ $count + 1 ]
    done
    $ bash test12
    Line #1: This is the first line.
    Line #2: This is the second line.
    Line #3: This is the third line.
    # read 命令读取用户在键盘上输入的数据。将 STDIN 重定向到文件后,当 read 命令试图从 STDIN 读入数据时,它会到文件去取数据,而不是键盘。
    # 这是在脚本中从待处理的文件中读取数据的绝妙办法。Linux系统管理员的一项日常任务就是从日志文件中读取数据并处理。这是完成该任务最简单的办法。

在脚本中重定向输入和输出时,并不局限于这3个默认的文件描述符。在shell中最多可以有9个打开的文件描述符。其他6个从 3 ~ 8 的文件描述符均可用作输入或输出重定向。你可以将这些文件描述符中的任意一个分配给文件,然后在脚本中使用它们。

  • ·exec 3> f.txt· // 将文件描述符3指向文件f.txt
    • ·echo "hello world" >&3·//·>&3·必须是整体。这里可以看出,3以上的文件描述符实际还是要通过标准输出等来发挥作用,比如这里是把标准输出重定向到了文件描述符3所指向的文件。
    • `3>`是一个整体不能用空格,如果输错成了如下命令:`exec 3 > f.txt`,那么这个命令实际把STDOUT重定向到了f.txt中了,因为重定向符号`>`默认是`1>`
  • ·exec 3>>g.txt· //将输出追加到现有文件中。

一旦重定向了 STDOUT 或 STDERR ,就很难再将它们重定向回原来的位置。如果你需要在重定向中来回切换的话,可以分配另外一个文件描述符给标准文件描述符,这意味着你可以将 STDOUT 的原来位置重定向到另一个文件描述符,然后再利用该文件描述符重定向回 STDOUT。例子:

  • #!/bin/bash
    # storing STDOUT, then coming back to it
    exec 3>&1
    exec 1>test14out
    echo "This should store in the output file"
    echo "along with this line."
    exec 1>&3
    echo "Now things should be back to normal"
    $
    $ ./test14
    Now things should be back to normal
    $ cat test14out
    This should store in the output file
    along with this line.

同理,可以重定向输入文件描述符,例子:

  • #!/bin/bash
    # redirecting input file descriptors
    exec 6<&0
    exec 0< testfile
    count=1
    while read line
    do
    echo "Line #$count: $line"
    count=$[ $count + 1 ]
    done
    exec 0<&6
    read -p "Are you done now? " answer
    case $answer in
    Y|y) echo "Goodbye";;
    N|n) echo "Sorry, this is the end.";;
    esac
    $ ./test15
    Line #1: This is the first line.
    Line #2: This is the second line.
    Line #3: This is the third line.
    Are you done now? y
    Goodbye
    文件描述符 6 用来保存 STDIN 的位置。然后脚本将 STDIN 重定向到一个文件。read 命令的所有输入都来自重定向后的 STDIN (也就是输入文件)
    在读取了所有行之后,脚本会将 STDIN 重定向到文件描述符 6 ,从而将 STDIN 恢复到原先的位置。该脚本用了另外一个 read 命令来测试 STDIN 是否恢复正常了。这次它会等待键盘的输入。
  • 创建读写文件描述符:尽管看起来可能会很奇怪,但是你也可以打开单个文件描述符来作为输入和输出。可以用同一个文件描述符对同一个文件进行读写。不过用这种方法时,你要特别小心。由于你是对同一个文件进行数据读写,shell会维护一个内部指针,指明在文件中的当前位置。任何读或写都会从文件指针上次的位置开始。如果不够小心,它会产生一些令人瞠目的结果。例如
    $ cat test16
    #!/bin/bash
    # testing input/output file descriptor
    exec 3<> testfile
    read line <&3
    echo "Read: $line"
    echo "This is a test line" >&3
    $ cat testfile
    This is the first line.
    This is the second line.
    This is the third line.
    $ ./test16
    Read: This is the first line.
    $ cat testfile
    This is the first line.
    This is a test line
    ine.
    This is the third line.
    $

    当脚本向文件中写入数据时,它会从文件指针所处的位置开始。 read 命令读取了第一行数据,所以它使得文件指针指向了第二行数据的第一个字符。在 echo 语句将数据输出到文件时它会将数据放在文件指针的当前位置,覆盖了该位置的已有数据。

  • 关闭文件描述符:如果你创建了新的输入或输出文件描述符,shell会在脚本退出时自动关闭它们。如果需要手动关闭文件描述符,可以它重定向到特殊符号 ·&-·。如·exec 3>&-·会关闭文件描述符 3 ,不再在脚本中使用它。一旦关闭了文件描述符,就不能在脚本中向它写入任何数据,否则shell会生成错误消息。在关闭文件描述符时还要注意另一件事。如果随后你在脚本中打开了同一个输出文件(即又重定向至这个文件),shell会用一个新文件来替换已有文件。这意味着如果你输出数据,它就会覆盖已有文件,可能会造成数据丢失,谨慎。♡
  • =========================exec命令解释============================
  • 使用 exec 命令可以并不启动新的 Shell,而是使用执行命令替换当前的 Shell 进程,并且将老进程的环境清理掉,而且 exec 命令后的其他命令将不再执行
  • exec 命令通常用在 Shell 脚本程序中,可以调用其他的命令。如果在当前终端中使用命令,则当指定的命令执行完毕后会立即退出终端。
  • exec [-cl] [-a name] [command [arguments]]·
    -c  #在空环境中执行指定的命令
    -l  #在传递给command的第零个arg的开头放置一个破折号
    -a  #Shell将name作为第零个参数传递给command

※,列出打开的文件描述符:你能用的文件描述符只有9个,你可能会觉得这没什么复杂的。但有时要记住哪个文件描述符被重定向到了哪里很难。为了帮助你理清条理,bash shell提供了 `lsof` 命令。lsof 命令会列出整个Linux系统打开的所有文件描述符。这是个有争议的功能,因为它会向非系统管理员用户提供Linux系统的信息。鉴于此,许多Linux系统隐藏了该命令,这样用户就不会一不小心就发现了。

lsof(list open files)可以列出当前系统中进程打开的所有文件,在Linux环境下,我们可以理解为一切(包括网络套接口)皆文件。lsof 一般需要访问核心内存和各种文件,所以必须以 root 用户的身份运行它才能够充分地发挥其功能。

lsof 的默认输出

  • COMMAND 正在运行的命令名的前9个字符
  • PID 进程的PID
  • TID:任务 ID。
  • USER 进程属主的登录名
  • FD 文件描述符号以及访问类型( r 代表读, w 代表写, u 代表读写)
    • cwd:应用程序当前工作目录,这是该应用程序启动的目录,除非它本身对这个目录进行更改    
      txt:该类型的文件是程序代码,如应用程序二进制文件本身或共享库,如上列表中显示的 /sbin/init 程序
      lnn:库引用(AIX)
      err:FD 信息错误
      jld:监狱目录(FreeBSD)
      ltx:共享库文本(代码和数据)
      mxx:十六进制内存映射类型号 xx
      m86:DOS合并映射文件
      mem:内存映射文件
      mmap:内存映射设备
      pd:父目录
      rtd:根目录
      tr:内核跟踪文件(OpenBSD)
      v86:VP/ix 映射文件
      0:标准输出
      1:标准输入
      2:标准错误
      文件描述符后一般还跟着文件状态模式:
      r:只读模式
      w:写入模式
      u:读写模式
      空格:文件的状态模式为 unknow,且没有锁定
      -:文件的状态模式为 unknow,且被锁定

      同时在文件状态模式后面,还跟着相关的锁:
      N:对于未知类型的 Solaris NFS 锁
      r:文件部分的读锁
      R:整个文件的读锁
      w:文件的部分写锁
      W:整个文件的写锁
      u:任何长度的读写锁
      U:用于未知类型的锁
      x:用于部分文件上的 SCO OpenServer Xenix 锁
      X:用于整个文件上的 SCO OpenServer Xenix 锁
      space:无锁

  • TYPE 文件的类型,常见的文件类型有:   
    • REG:普通文件
      DIR:表示目录
      CHR:表示字符类型
      BLK:块设备类型
      UNIX:UNIX 域套接字
      FIFO:先进先出队列
      IPv4:IPv4 套接字
  • DEVICE 设备的设备号(主设备号和从设备号)
  • SIZE 如果有的话,表示文件的大小或文件偏移量(以字节为单位)
  • NODE 本地文件的节点号
  • NAME 打开文件的确切名称

·lsof· 用法见 此博文,lsof是一个很强大的命令。lsof能完成ps和netstat命令所能做的一切,并且还有其他额外的很多功能。理解一些关于lsof如何工作的关键性东西是很重要的。最重要的是,当你给它传递选项时,默认行为是对结果进行“或”运算。因此,如果你正是用-i来拉出一个端口列表,同时又用-p来拉出一个进程列表,那么默认情况下你会获得两者的结果。

  • `man lsof` //所有的用法都在这个manual手册里,比网上的资料要好的多!
  • `lsof` // 没有任何选项:lsof列出活跃进程的所有打开文件
  • ·-a· : 结果进行“与”运算(而不是“或”)
  • ·^·lsof很多命令的参数值可以使用非(^)来取反,见下文例子
  • -n, -P参数可以大大提升lsof的速度。解析host name和port名称需要耗费大量时间。
    • ·-n· -n参数不解析host name。
    • ·-P· 大写的p。不解析端口名。
  • `-l` : 小写L。在输出显示用户ID而不是用户名,比如root用户名对应用户ID为0.
  • `-t`: 仅获取进程ID
  • `lsof <fileName>` 显示与指定文件交互的所有一切
  • ·lsof <dirName>· 显示与指定目录交互的所有一切。一些使用场景如下:
    • 我们会遇到磁盘卸载报umount: /home: device is busy之类的提示,此时可以使用lsof命令找出占用磁盘的进程·lsof /home·,然后直接kill掉此进程,磁盘成功卸载
      • 当然除了kill进程外也可以使用如下方法:
      • `umount -l /home` 强行解除挂载,-l, --lazy              detach the filesystem now, and cleanup all later
      • `fuser -mv -k /home`直接杀死占用磁盘的进程
    • Lsof解决文件已删除空间未释放问题。文件已删除,但是仍有进程在占用这些文件,因此空间仍然没有释放。可以使用·lsof -n |grep deleted·查看占用文件的进程,然后直接kill掉。
    • 巧用losf恢复已删除文件:前提是文件的进程必须存在。步骤如下: 假设被删除的文件为lsof.log
      • ·lsof | grep lsof.log·查找出进程ID。
        [root@192 ~]# lsof |grep lsof.log
        tail      9933      root    3r      REG                8,2        10     391018 /root/lsof.log (deleted)
      • PID:9933 FD:3 那我们有直接进入/proc/9933/fd/3查看一下,发现文件描述符3软连接到lsof.log文件,并且已经被删除。此时文件描述符3中依然有文件中的内容,只要·cat 3 > /root/lsof.log·即可恢复
  • ·lsof -p <pid>· // 查看进程<pid>打开的文件。
    • ·lsof -p 1,2,3,4· //进程ID可以有多个,用逗号隔开。
  • `lsof -c <command>` // 列出由command命令打开的文件
    • ·lsof -c ^docker· //列出所有非docker命令打开的文件。
  • `lsof -u <userName/UID>`  列出由 用户名称或用户ID 打开的所有文件
    • ·lsof -u root·
    • `lsof -u ^root` 列出所有非root打开的文件
  • `lsof -g` 输出列表中新增一项PGID。PGID定义:进程可以组成进程组(setpgrp系统调用),进程组可以简化向所有组内进程发送信号的操作。进程组ID叫做PGID,进程组内的所有进程都有相同的PGID,等于该组组长的PID。
    • ·lsof -g <PGID>· 列出属于<PGID>的文件。
  • `lsof -d 4` 显示使用fd为4的进程
  • ·lsof +d /usr/local/· 显示目录下被进程开启的文件
  • `lsof +D /usr/local/` 同上,但是会搜索目录下的目录,时间较长
  •  

获取网络信息:lsof -i[46] [protocol][@hostname|@hostaddr][:serviceList|:portList]

  • `lsof -i`  获取所有网络连接相关信息
  • ·:service·如:ssh代表的就是端口,所有的service名称保存在文件`/etc/services`中
  • ·lsof -i tcp· 仅显示TCP连接(同理可获得UDP连接)
  • ·lsof -i :22` OR `lsof -i :ssh`显示与指定端口相关的网络信息
    • ·lsof -i:22,3306· 用逗号分割多个端口列表
  • `lsof -i @10.8.0.238` 显示指定主机的连接信息,此主机可以是源主机也可以是目的主机。
  • ·lsof -i @10.8.0.238:22·显示基于主机与端口的连接
  • ·lsof -i -s tcp:LISTEN· //找出正等候连接的端口。
    • 查看man lsof可以看到这种用法:-s [p:s],p代表protocol,s代表状态。单独的-s参数是列出文件大小,而这种用法和单独的-s没有任何关系。这种用法可已过滤出各种状态的连接
    • `lsof -i -s tcp:^listen` // 所有非LISTEN的tcp连接。
    • ·lsof -i -s udp:Idle·
    • ·lsof  -i -s TCP:ESTABLISHED·
  •  

※,阻止命令输出:如果在运行在后台的脚本出现错误消息,shell会通过电子邮件将它们发给进程的属主。要解决这个问题,可以将 STDERR 重定向到一个叫作null文件的特殊文件。null文件跟它的名字很像,文件里什么都没有。shell输出到null文件的任何数据都不会保存,全部都被丢掉了。在Linux系统上null文件的标准位置是/dev/null。你重定向到该位置的任何数据都会被丢掉,不会显示。

也可以在输入重定向中将/dev/null作为输入文件。由于/dev/null文件不含有任何内容,程序员通常用它来快速清除现有文件中的数据,而不用先删除文件再重新创建,如

$ cat testfile
This is the first line.
This is the second line.
This is the third line.
$ cat /dev/null > testfile
$ cat testfile
$

,创建临时文件:mktemp 命令可以在/tmp目录中创建一个唯一的临时文件。shell会创建这个文件,但不用默认的 umask 值。它会将文件的读和写权限分配给文件的属主,并将你设成文件的属主。一旦创建了文件,你就在脚本中有了完整的读写权限,但其他人没法访问它(当然,root用户除外)。

  • 要用 mktemp 命令在本地目录中创建一个临时文件,你只要指定一个文件名模板就行了。模板可以包含任意文本文件名,在文件名末尾加上6个 X 就行了。mktemp 命令会用6个字符码替换这6个 X ,从而保证文件名在目录中是唯一的。·mktemp Everest.XXXXXX·
  • ·mktemp` // 在 /tmp文件夹中创建一个临时文件,系统随机命名
  • ·mktemp Everest.XXXXXX· // 必须是6个X。Everest是模板前缀。 在当前文件夹下创建一个模板前缀的临时文件。
  • ·mktemp -t Everest.XXXXXX· // -t选项在/tmp下创建一个模板前缀的临时文件.
  • `mktemp -d  Everest.XXXXXX` //-d创建文件夹而非文件
  • ·mktemp -dt Everest.XXXXXX· // 在/tmp文件夹下创建一个文件夹

※,记录消息:将输出同时发送到显示器和日志文件,不用将输出重定向两次,只要用特殊的 tee 命令就行。tee 命令相当于管道的一个T型接头。它将从 STDIN 过来的数据同时发往两处。一处是STDOUT ,另一处是 tee 命令行所指定的文件名。这样既能将数据保存在文件中,也能将数据显示在屏幕上。

  • ·tee filename· 
  • ·date | tee testFile· // 由于 tee 会重定向来自 STDIN 的数据,你可以用它配合管道命令来重定向命令输出。注意,默认情况下, tee 命令会在每次使用时覆盖输出文件内容。
  • ·date | tee -a testFile· // 如果你想将数据追加到文件中,必须用 -a 选项。

※,实例:从csv文件中读取数据,然后创建SQL语句插入MySQL数据库。

$cat test23
#!/bin/bash
# read file and create INSERT statements for MySQL
outfile='members.sql'
IFS=','
while read lname fname address city state zip
do
cat >> $outfile << EOF
INSERT INTO members (lname,fname,address,city,state,zip) VALUES
('$lname', '$fname', '$address', '$city', '$state', '$zip');
EOF
done < ${1}
$

脚本中出现了三个重定向!!!运行脚本时,显示器上不会出现任何输出,但是在member.sql文件中生成了insert语句。

  • while循环的done语句后的重定向。当运行程序 test23 时, $1 代表第一个命令行参数。它指明了待读取数据的文件(csv文件)。 read 语句会使用 IFS 字符解析读入的文本,我们在这里将 IFS 指定为逗号
  • cat >> $outfile << EOF。这条语句中包含两个重定向,一个输出追加重定向(双大于号)和一个输入追加重定向(双小于号)。意思是 cat命令的输入和输出都被重定向了。cat命令的输出重定向至$outfile指定的文件(追加), cat命令的输入也不再取自标准输入,而是被重定向至一个内联输入重定向(EOF标记了内联输入的开始和结束)。


第16章:控制脚本

※,处理信号:Linux利用信号与运行在系统中的进程进行通信。在Linux中,进程之间通过信号来通信。进程的信号就是预定义好的一个消息,进程能识别它并决定忽略还是作出反应。进程如何处理信号是由开发人员通过编程来决定的。大多数编写完善的程序都能接收和处理标准Unix进程信号

默认情况下,bash shell会忽略收到的任何 SIGQUIT (3) 和 SIGTERM (5) 信号(正因为这样,交互式shell才不会被意外终止)。但是bash shell会处理收到的 SIGHUP (1) 和 SIGINT (2) 信号。如果bash shell收到了 SIGHUP 信号,比如当你要离开一个交互式shell,它就会退出。但在退出之前,它会将 SIGHUP 信号传给所有由该shell所启动的进程(包括正在运行的shell脚本)。通过 SIGINT 信号,可以中断shell。Linux内核会停止为shell分配CPU处理时间。这种情况发生时,shell会将 SIGINT 信号传给所有由它所启动的进程,以此告知出现的状况。

kill命令:根据pid杀掉单独一个进程。默认信号:SIGTERM

  • ·kill -l· //小写的L, 列出所有的信号。几个常见的信号,前面的数字是其代号
    • 1) SIGHUP : 挂起进程
    • 2) SIGINT : 终止进程
    • 9) SIGKILL : 无条件终止进程
  • ·kill pid` 默认向进程pid发送一个TERM(15)的信号,15相比9比较优雅的终止一个进程。
  • ·kill -15 <pid>`
  • `kill -term <pid>` OR `kill -TERM <pid>`
  • `kill -SIGTERM <pid>` //注意此处-SIGTERM不能小写
  • ·kill -0 <pid>`// -0参数不会向进程发送任何信号,不会杀掉进程,而是检查进程是否存在,如果存在则返回0,如果不存在则返回1。

killall命令:根据名称(精确匹配或正则(-r参数))批量杀掉进程。默认信号:SIGTERM。

  • `killall xxxx`  killall 命令非常强大,它支持通过进程名而不是PID来结束进程。 killall 命令也支持通配符,这在系统因负载过大而变得很慢时很有用。慎用!
  • `killall <p-name>`// 杀掉所有名名称精确匹配为p-name的进程,这里杀掉的是名称精确匹配为p-name的进程,比如写ssh杀不掉sshd进程。
    • 进程名称p-name指的shell命令或者是运行的二进制文件名称,文件的路径名称不属于进程名称的一部分
      [root@sungrow27 grafanaloki]$ps -ef |grep grafanaloki
      root      342485 1733488  0 17:10 pts/3    00:00:00 grep --color=auto grafanaloki
      root      374301 2766094  0 7月11 pts/2   00:18:17 /data1/tong/grafanaloki/bin/loki-linux-amd64 -config.file=/data1/tong/grafanaloki/config/loki-local-config.yaml
      root     2419533 2766094  1 7月12 pts/2   00:26:48 /data1/tong/grafanaloki/bin/promtail-linux-amd64 -config.file=/data1/tong/grafanaloki/config/promtail-local-config.yaml
      [root@sungrow27 grafanaloki]$killall -0 grafanaloki
      grafanaloki: 未找到进程
      [root@sungrow27 grafanaloki]$killall -0 -r loki
      [root@sungrow27 grafanaloki]$
    • 当直接运行shell脚本时,shell脚本的名称并不是进程的名称。当以bash <script.sh>方式运行shell脚本时,会有一个名为bash的进程。
  • `killall -i <p-name>` // -i选项,交互式进行。
  • ·killall -r <p-regular-name>`//杀掉正则匹配为p-regular-name的所有进程。这里的正则为:*代表0或多个,.代表单个字符,?代表0或1个。
  • `killall -0 <name>` //-0参数不会向进程发送任何信号,不会杀掉进程,而是检查进程是否存在,如果存在则返回0,如果不存在则返回1。
  • `killall -0 -r cr.*` //检查正则名称为cr.*的进程是否存在。

pkill命令:默认信号:SIGTERM。pkill命令和killall命令类似,都是根据名称批量处理一批进程。不同点在于pkill不加选项默认就是正则名称,killall不加参数默认是精确匹配。

pkill命令和另一个命令pgrep是同源命令,pgrep用名称(正则名称)或其他属性查找进程并打印到标准输出,pkill则对匹配到的所有进程发送指定的信号,默认是SIGTERM信号。

pgrep, pkill - look up or signal processes based on name and other attributes

  • ·pkill -15 cr` //杀掉正则匹配cr的所有进程。如cron进程。
  • ·pkill -15 -u root cr· // 杀掉正则匹配cr并且属于root用户的所有进程。
  • ·pkill -15 ^cr$· 杀掉正则匹配^cr$的所有进程,即名称精确为cr的所有进程。
  • ·pkill -0 cr· // -0参数不会向进程发送任何信号,不会杀掉进程,而是检查进程是否存在,如果存在则返回0,如果不存在则返回1。
  • `pgrep ssh` // 查找正则名称为ssh的所有进程
  • ·pgrep -u root ^ssh$· //查找正则名称为^ssh$且属于用户root的所有进程
  • `pgrep firefox -f` //-f参数即--full。此参数use full process name to match,所谓的full process name就是带着所有启动参数的名称,也就是ps -ef中cmd列的所有描述字符。
  • ·pkill -15 firefox -f· // -f参数同上

※,生成信号:bash shell允许用键盘上的组合键生成两种基本的Linux信号。这个特性在需要停止或暂停失控程序时非常方便。

  • Ctrl+C组合键会生成 SIGINT 信号。中断进程。
  • Ctrl+Z组合键会生成一个 SIGTSTP 信号,停止shell中运行的任何进程。停止(stopping)进程跟终止(terminating)进程不同:停止进程会让程序继续保留在内存中,并能从上次停止的位置继续运行。你可以在进程运行期间暂停进程,而无需终止它。尽管有时这可能会比较危险(比如,脚本打开了一个关键的系统文件的文件锁),但通常它可以在不终止进程的情况下使你能够深入脚本内部一窥究竟

※,捕获信号:也可以不忽略信号,在信号出现时捕获它们并执行其他命令。trap 命令允许你来指定shell脚本要监看并从shell中拦截的Linux信号。如果脚本收到了 trap 命令中列出的信号,该信号不再由shell处理,而是交由本地处理。

  • ·trap commands signals· // 在 trap 命令行上,你只要列出想要shell执行的命令,以及一组用格分开的待捕获的信号。你可以用数值或Linux信号名来指定信号。
  • `trap "echo ' Sorry! I have trapped Ctrl-C'" SIGINT` // 本例中用到的 trap 命令会在每次检测到 SIGINT 信号时显示一行简单的文本消息。捕获这些信号会阻止用户用bash shell组合键Ctrl+C来停止程序。

除了在shell脚本中捕获信号,你也可以在shell脚本退出时进行捕获。这是在shell完成任务时执行命令的一种简便方法。要捕获shell脚本的退出,只要在 trap 命令后加上 EXIT 信号就行,如·trap "echo Goodbye..." EXIT·。

也可以删除已设置好的捕获。只需要在 trap 命令与希望恢复默认行为的信号列表之间加上两个破折号就行了。·trap -- SIGINT·

※,以后台模式运行脚本:

  • 以后台模式运行shell脚本非常简单。只要在命令后加个 & 符就行了。当 & 符放到命令后时,它会将命令和bash shell分离开来,将命令作为系统中的一个独立的后台进程运行。注意:当后台进程运行时,它仍然会使用终端显示器来显示 STDOUT 和 STDERR 消息。
  • 在终端会话中使用后台进程时一定要小心。在 ps 命令的输出中,每一个后台进程都和其所在的终端会话(如 pts/0 )联系在一起。如果终端会话退出,那么后台进程也会随之退出。
  • 如果希望运行在后台模式的脚本在登出控制台后能够继续运行,可以使用 nohup 命令。nohup 命令运行了另外一个命令来阻断所有发送给该进程的 SIGHUP 信号。这会在退出终端会话时阻止进程退出。如·nohup ./test1.sh &·  在你使用 nohup 命令时,如果关闭该会话,脚本会忽略终端会话发过来的 SIGHUP 信号。由于 nohup 命令会解除终端与进程的关联,进程也就不再同 STDOUT 和 STDERR 联系在一起。为了保存该命令产生的输出, nohup 命令会自动将 STDOUT 和 STDERR 的消息重定向到一个名为nohup.out的文件中。

※,nohup、&、setsid 让命令在后台可靠运行

当用户注销(logout)或者网络断开时,当前终端会收到 SIGHUP(hangup)信号从而关闭其所有子进程。因此,解决办法就有两种:要么让进程忽略 HUP 信号(nohup命令),要么让进程运行在新的会话里从而成为不属于此终端的子进程(setsid命令)

nohup 和 & 测试 https://mp.weixin.qq.com/s/nyT-FPdIUdJUiUCYVGEnTg

  • 直接运行命令,程序运行,显示输出信息,按 ctrl + c 程序会受到SIGINT(signal interrupt)信号,程序停止运行。关闭session窗口,程序收到SIGHUP(signal hangup),程序停止运行。
  • 以&结尾运行命令,程序在后台运行,输出信息会显示在前台。ctrl + c 程序继续运行,关闭session,程序停止运行。
  • 以 nohup 运行命令,让命令忽略SIGHUP(hangup)信号,标准输出和标准错误缺省会被重定向到名称为nohup.out的文件中(如果无权限则会写入${HOME}/nohup.out)。ctrl + c程序停止运行,关闭session,程序继续运行。
  • nohup和&同时运行命令,程序在后台运行,输出写入nohup.out文件。ctrl +c程序继续运行,关闭session,程序继续运行。也就是说 平日线上经常使用nohup和&配合来启动程序同时免疫SIGINT和SIGHUP信号。

setsid: 在新的session中执行命令,使命令的进程不属于接收SIGHUP信号的终端会话的子进程,那么就不会受到当前终端的SIGHUP信号的影响。

  • setsid [options] <program> [arguments...]
  • ·setsid ./gogs web· //不需要在后面加&

※,作业控制

  • ·jobs· // 查看shell当前正在处理的作业
  • `jobs -l` // 小写L,查看pid
  • jobs 命令输出中的加号和减号。带加号的作业会被当做默认作业。在使用作业控制命令时,如果未在命令行指定任何作业号,该作业会被当成作业控制命令的操作对象。当前的默认作业完成处理后,带减号的作业成为下一个默认作业。任何时候都只有一个带加号的作业和一个带减号的作业,不管shell中有多少个正在运行的作业。

※,重启停止的作业:在bash作业控制中,可以将已停止的作业作为后台进程或前台进程重启。前台进程会接管你当前工作的终端,所以在使用该功能时要小心了

  • ·bg· //以后台模式重启默认作业
  • `bg <jobsId>`
  • ·fg· // 以前台模式重启默认作业
  • `fg <jobsId>`

※,调整谦让度:

在多任务操作系统中(Linux就是),内核负责将CPU时间分配给系统上运行的每个进程。调度优先级(scheduling priority)是内核分配给进程的CPU时间(相对于其他进程)。在Linux系统中,由shell启动的所有进程的调度优先级默认都是相同的。调度优先级是个整数值,从 -20(最高优先级)到+19(最低优先级)。默认情况下,bash shell以优先级0来启动所有进程。

  • ·ps -p 8088 -o pid,ppid,ni,cmd· // PS命令-o参数指定输出列名,注意用逗号分割,中间不能有空格!!!!ni代表的即是此进程优先级。
  • ·nice -n 10 ./test.sh > test.log &· // nice 命令允许你设置命令启动时的调度优先级.必须将 nice 命令和要启动的命令放在同一行中。nice 命令阻止普通系统用户来提高命令的优先级。
  • `renice -n 10 -p 8088` //  renice 命令可以改变系统上已运行命令的优先级。注意:①非root只能对属于你的进程执行 renice且只能通过 renice 降低进程的优先级;②root用户可以随意降低或提高任何进程的优先级。

※,定时任务

1, 用 at 命令来计划执行作业。atd 守护进程会检查系统上的一个特殊目录(通常位于/var/spool/at)来获取用 at 命令提交的作业在你使用 at 命令时,该作业会被提交到作业队列(job queue)。作业队列会保存通过 at 命令提交的待处理的作业。

  • ·at [-f filename] time· // 默认情况下, at 命令会将 STDIN 的输入放到队列中。你可以用 -f 参数来指定用于读取命令(脚本文件)的文件名。
  • `atq` // atq 命令可以查看系统中有哪些at作业在等待。
  • ·atrm <at作业号>· // 用 atrm 命令来删除等待中的作at业

2, 安排需要定期执行的脚本。Linux系统使用cron程序来安排要定期执行的作业。cron程序会在后台运行并检查一个特殊的表(被称作cron时间表),以获知已安排执行的作业。

  • min hour dayofmonth month dayofweek command  // cron时间表采用一种特别的格式来指定作业何时运行。

    如何设置一个在每个月的最后一天执行的命令,因为你无法设置
    dayofmonth的值来涵盖所有的月份。这个问题困扰着Linux和Unix程序员,也激发了不少解
    决办法。常用的方法是加一条使用 date 命令的 if-then 语句来检查明天的日期是不是01:
    00 12 * * * if [ ` date +%d -d tomorrow ` = 01 ] ; then ; command
    它会在每天中午12点来检查是不是当月的最后一天,如果是,cron将会运行该命令
  • 每个系统用户(包括root用户)都可以用自己的cron时间表来运行安排好的任务。Linux提供了 crontab 命令来处理cron时间表.

    • ·crontab -l· // 要列出已有的cron时间表
    • `crontab -e` // 默认情况下,用户的cron时间表文件并不存在。要为cron时间表添加条目,可以用 -e 选项。
  • 如果你创建的脚本对精确的执行时间要求不高,用预配置的cron脚本目录会更方便。有4个基本目录:hourly、daily、monthly和weekly。

    • `ls /etc/cron.*ly` // 如果脚本需要每天运行一次,只要将脚本复制到daily目录,cron就会每天执行它。
    •  
  • anacron程序: 如果某个作业在cron时间表中安排运行的时间已到,但这时候Linux系统处于关机状态,那么这个作业就不会被运行。当系统开机时,cron程序不会再去运行那些错过的作业。而anacron知道某个作业错过了执行时间,它会尽快运行该作业。anacron程序只会处理位于cron目录的程序,比如/etc/cron.monthly。。它用时间戳来决定作业是否在正确的计划间隔内运行了。每个cron目录都有个时间戳文件,该文件位于/var/spool/anacron。anacron时间表的基本格式和cron时间表略有不同:
    • period delay identifier command // period条目定义了作业多久运行一次,以天为单位。注意,anacron不会运行位于/etc/cron.hourly的脚本。这是因为anacron程序不会处理执行时间需求小于一天的脚本。

※,shell脚本获取后台运行任务的返回值

当使用·&·把进程放入后台以后,如果需要了解进程的执行情况,可以使用wait函数。默认情况下wait会等待任意子进程结束但是不会返回子进程的返回值。而以子进程的pid作为参数调用wait时,wait便能够返回该子进程的退出状态了。

例一:

#!/bin/bash
./etcdkeeper -p 9081 &
wait $! # 注意:如果上面 的命令是个死循环,那么这里也会卡住。
status=$?
if [[ status -gt 0 ]]; then
    echo "程序运行出错"
else 
    echo "程序已启动"
fi

例二:

#!/bin/bash
dir=`dirname $`
$dir/test01.sh &
$dir/test02.sh &
echo '' > $dir/tmp.log
for pid in $(jobs -p)
do
wait $pid
status=$?
if [ $status !=  ];then
echo "$pid status is $status have some error!" >> $dir/tmp.log
else
echo "$pid status is $status success!" >> $dir/tmp.log
fi
done

 



 

第17章:使用函数

※,创建函数:bash shell中有两种定义函数的方式:

·function name {      // 注意:无括号!name 属性定义了赋予函数的唯一名称。脚本中定义的每个函数都必须有一个唯一的名称
commands

OR

·name () {
commands

※,使用/调用 函数:要在脚本中使用函数,只需要像其他shell命令一样,在行中指定函数名就行了。注意函数调用前必须已经定义了此函数。

#!/bin/bash
function myFun {
    echo "good"
}
myFun # 此处调用函数myFun

※,返回值:bash shell会把函数当作一个小型脚本,运行结束时会返回一个退出状态码。有3种不同的方法来为函数生成退出状态码。

①默认退出状态码:默认情况下,函数的退出状态码是函数中最后一条命令返回的退出状态码,在函数执行结束后,可以用标准变量 $? 来确定函数的退出状态码。

#!/bin/bash
function myFun {
    echo "good"
    ls nonexistsfile
}
myFun # 此处调用函数myFun
echo "myFun exits status is: $?"

windows@Tonus:~/shellScript$ bash myfun.sh 
good
ls: 无法访问 'nonexistsfile': 没有那个文件或目录
myFun exits status is: 2
windows@Tonus:~/shellScript$ 

②return 命令来返回数值。 return 命令可以基于函数的结果,通过编程的方式将函数的退出状态码设为特定值(return返回的值只能在0-255之间)。

  • return和exit的返回值能用echo $?获取
    •  记住,函数一结束就取返回值;如果在用 $? 变量提取函数返回值之前执行了其他命令,函数的返回值就会丢失。记住, $?变量会返回执行的最后一条命令的退出状态码。
  •  记住,退出状态码必须是0~255。任何大于256的值都会被取模,即除以256的余数。如return 257 实际会被返回1.
  • return 的作用是退出当前函数,不退出整个脚本
  • 函数中return 后面的命令一概不执行。这点和Java一样,函数中return语句后面的都不会再执行
  • exit代表退出整个脚本
  •  
windows@Tonus:~/shellScript$ cat myfun.sh 
#!/bin/bash
function myFun {
    echo "good"
    ls nonexistsfile
    return 33
}
myFun # 此处调用函数myFun
echo "myFun exits status is: $?"
windows@Tonus:~/shellScript$ bash myfun.sh 
good
ls: 无法访问 'nonexistsfile': 没有那个文件或目录
myFun exits status is: 33
windows@Tonus:~/shellScript$ 

③函数中使用echo语句返回(输出):正如可以将命令的输出保存到shell变量中一样,也可以用这种技术来获得任何类型的函数输出,并将其保存到变量中(使用命令替换,即反引号或$() ,只要涉及到将函数结果保存至变量中都需要使用命令替换!)。如: result=$(myFun), 这个命令会将myFun函数的输出保存到变量$result中。

※,传参调用函数:可以在函数中使用shell变量(如$0, $1..., $@, $*, $#等等),对其赋值以及从中取值。这样你就能将任何类型的数据从主体脚本程序的脚本函数中传入传出

windows@Tonus:~/shellScript$ cat myfun.sh
#!/bin/bash
function myFun {
echo 参数为$1
}
myFun param1 #此处调用函数myFun,并传参
result=$(myFun hello) # 传参调用函数,并将函数结果保存至变量中。使用$()后,myFun函数体内的echo打印并不会打印了。
echo $result
windows@Tonus:~/shellScript$ bash myfun.sh
参数为param1
参数为hello
windows@Tonus:~/shellScript$

※,作用域

1,默认情况下,你在脚本中定义的任何变量都是全局变量。在函数外定义的变量可在函数内正常访问。函数内默认的变量也是全局变量,可以将函数内的变量定义为局部变量,只要在变量前加local关键字即可,局部变量只会在函数内有效,可以和外部某变量重名,但是两个变量是独立的。注意下面一种情况, 使用 变量替换时的一种情况:

#!/bin/bash
myfun(){
    name="Everest"
    echo "hello world"
}
myfun #调用函数myfun,执行后将会使name变量可见,而且也会打印 hello world
echo $name #这里会打印出来name的值。

# "这也是一种定义函数的语法,注意没有小括号"
function myfun1 {
    hobby="football"
    echo "hi world"
}
#注意变量替换中先执行myfun1,这是在fork的一个子进程中执行的,不会使hobby变量对当前脚本可见。
# 也不会打印hi world
myfun1Value=$(myfun1) 
echo hobby:$hobby #打印空, hobby无值
echo $myfun1Value # 打印 hi world

windows@Tonus:~/shellScript$ bash functionShell.sh  // bash + 脚本文件的方式运行脚本时,脚本可以没有x可执行权限!
hello world
Everest
hobby:
hi world

※,函数递归

阶乘示例

#!/bin/bash
factorial() {
    if [ $1 -eq 1 ];then
        echo 1
    else
        local temp=$[ $1 - 1 ]
        local result=$(factorial $temp)
        echo $[ $result * $1 ]
    fi
}

read -p "Enter the Value:" value
echo $(factorial $value)

※,创建库: bash shell允许创建函数库文件,然后在多个脚本中引用该库文件

★,source命令:source命令可以使Shell读入指定的Shell程序文件并依次执行文件中的所有语句。source命令通常用于重新执行刚修改的初始化文件,使之立即生效,而不必注销并重新登录。

  • ·source filename·:这个命令其实只是简单地读取脚本里面的语句依次在当前shell里面执行,没有建立新的子shell。那么脚本里面所有新建、改变变量的语句都会保存在当前shell里面。
  • 当shell脚本具有可执行权限时,用·sh filename·与·./filename·执行脚本是没有区别的。 两者都会重新建立一个子shell,在子shell中执行脚本里面的语句,该子shell继承父shell的环境变量,但子shell新建的、改变的变量不会被带回父shell,除非使用export。

1. 第一步是创建一个包含脚本中所需函数的公用库文件。如创建一个名为 funlib 的公共库文件。

$ cat funlib

#!/bin/bash
function addem {
    echo $[ $1 + $2 ]
}

multiem(){
    echo $(($1 * $2))
}

下一步是在用到这些函数的脚本文件中包含 funlib 库文件。从这里开始,事情就变复杂了。问题出在shell函数的作用域上。和环境变量一样,shell函数仅在定义它的shell会话内有效。如果你在shell命令行界面的提示符下运行 funlib 脚本,shell会创建一个新的shell并在其中运行这个脚本。它会为那个新shell定义这三个函数,但当你运行另外一个要用到这些函数的脚本时,它们是无法使用的。这同样适用于脚本。如果你尝试像普通脚本文件那样运行库文件,函数并不会出现在脚本中。下面是错误适用库文件的示例

$ cat badTest.sh

#!/bin/bash
# using a library file the wrong way
./myfuncs # 像普通脚本文件那样运行库文件,函数并不会出现在脚本中。错误的使用库文件!
result=$(addem 10 15)
echo "The result is $result"
$
$ ./badtest4
./badtest4: addem: command not found
The result is
$

使用函数库的关键在于 source 命令。 source 命令会在当前shell上下文中执行命令,而不是创建一个新shell。可以用 source 命令来在shell脚本中运行库文件脚本。这样脚本就可以使用库中的函数了。source 命令有个快捷的别名,称作点操作符(dot operator)。这里有个用 funlib 库文件创建脚本的例子.

$ cat test14
#!/bin/bash
# using functions defined in a library file
# source ./funlib #和下面一行等同。
. ./funlib #假设库文件和此脚本位于同一目录。source后,funlib中的函数就像在当前文件中定义的一样。
value1=10
value2=5
result1=$(addem $value1 $value2) # 如上文所述,变量替换中$()执行的命令是在fork的一个子进程shell中进行的,$()中的命令声明的变量等不会对当前shell有效。
result2=$(multem $value1 $value2)
result3=$(divem $value1 $value2)
echo "The result of adding them is: $result1"
echo "The result of multiplying them is: $result2"
echo "The result of dividing them is: $result3"
$
$ ./test14
The result of adding them is: 15
The result of multiplying them is: 50
The result of dividing them is: 2
$

※,在命令行上使用函数

1. 方法一:在命令行上创建函数。可以单行定义函数(此时每个命令后面必须添加分号以便shell知道命令的起止)。也可以采用多行方式定义函数,在定义时,bash shell会使用次提示符来提示输入更多命令。用这种方法,你不用在每条命令的末尾放一个分号,只要按下回车键就行。在函数的尾部使用花括号,shell就会知道你已经完成了函数的定义。警告:在命令行上创建函数时要特别小心。如果你给函数起了个跟内建命令或另一个命令相同的名字,函数将会覆盖原来的命令。

2. 方法二:在 ~/.bashrc文件中定义函数。bash shell在每次启动时都会在主目录下查找这个文件,不管是交互式shell还是从现有shell中启动的新shell。这是一种创建实用工具的简便方法,不管 PATH 环境变量设置成什么,都可以直接拿来使用。

  • 直接定义函数。
  • 读取函数库文件。在.bashrc中 可以用 source 命令(或者它的别名点操作符)将库文件中的函数添加到你的.bashrc脚本中。

这样在新打开的shell命令行中都可以直接使用 库文件中的函数了。如果已经打开了shell,再在 .bashrc 中添加库文件,那么需要在当前shell中 使用source命令: ·source ~/.bashrc·。这样就可以在当前shell中直接使用库文件函数了。

※,实例

函数的应用绝不仅限于创建自己的函数自娱自乐。在开源世界中,共享代码才是关键,而这一点同样适用于脚本函数。你可以下载大量各式各样的函数,并将其用于自己的应用程序中。本节介绍了如何下载、安装、使用GNU shtool shell脚本函数库。shtool库提供了一些简单的shell脚本函数,可以用来完成日常的shell功能,例如处理临时文件和目录或者格式化输出显示。

下面是GNU shtool shell脚本函数库的使用方法:

1. 下载和安装:shtool软件包的下载地址是: ftp://ftp.gnu.org/gnu/shtool/shtool-2.0.8.tar.gz 。在Linux中可以使用 wget <url> 或 curl <url> --output <file> 来下载。下载完成解压即可。

 

  •  wget是个专职的下载利器,简单,专一,极致;而curl可以下载,但是长项不在于下载,而在于模拟提交web数据,POST/GET请求,调试网页,等等。
  • 在下载上,也各有所长,wget可以递归,支持断点;而curl支持URL中加入变量,因此可以批量下载。

 

2. 构建库。shtool文件必须针对特定的Linux环境进行配置。配置工作必须使用标准的 configure 和make 命令,这两个命令常用于C编程环境。要构建库文件,只要输入:

$ ./confifgure
$ make

configure 命令会检查构建shtool库文件所必需的软件。一旦发现了所需的工具,它会使用工具路径修改配置文件。make 命令负责构建shtool库文件。最终的结果( shtool )是一个完整的库软件包。你也可以使用 make 命令测试这个库文件,如下:

$ make test
Running test suite:
echo...........ok
mdate..........ok
table..........ok
prop...........ok
move...........ok
install........ok
mkdir..........ok
mkln...........ok
mkshadow.......ok
fixperm........ok
rotate.........ok
tarball........ok
subst..........ok
platform.......ok
arx............ok
slo............ok
scpp...........ok
version........ok
path...........ok
OK: passed: 19/19
$

测试模式会测试shtool库中所有的函数。如果全部通过测试,就可以将库安装到Linux系统中的公用位置(即:使用make install命名安装到公用位置),这样所有的脚本就都能够使用这个库了。要完成安装,需要使用 make 命令的install 选项。不过你得以root用户的身份运行该命令。(可以不用su切换root用户,最近发现 ·sudo -s <command>·很好用,sudo -s make install 。可以查看sudo -h)

$ su
Password:
# make install
./shtool mkdir -f -p -m 755 /usr/local
./shtool mkdir -f -p -m 755 /usr/local/bin
./shtool mkdir -f -p -m 755 /usr/local/share/man/man1
./shtool mkdir -f -p -m 755 /usr/local/share/aclocal
./shtool mkdir -f -p -m 755 /usr/local/share/shtool
...
./shtool install -c -m 644 sh.version /usr/local/share/shtool/sh.version
./shtool install -c -m 644 sh.path /usr/local/share/shtool/sh.path
#
现在就能在自己的shell脚本中使用这些函数了。

shtool函数的使用格式:shtool [options] [function [options] [args]]。 如

  • `shtool --help`
  • `shtool platform`
  • `shtool prop -p "waiting..."`//显示一个进度条







结束标记

posted on 2021-08-25 00:21  everest33  阅读(76)  评论(0编辑  收藏  举报