shell编程使用方法

编程语言及工具

105人已加入

描述

    Shell概述

  Shell是一种具备特殊功能的程序,它提供了用户与内核进行交互操作的一种接口。它接收用户输入的命令,并把它送入内核去执行。内核是Linux系统的心脏,从开机自检就驻留在计算机的内存中,直到计算机关闭为止,而用户的应用程序存储在计算机的硬盘上,仅当需要时才被调入内存。Shell是一种应用程序,当用户登录Linux系统时,Shell就会被调入内存去执行。Shell独立于内核,它是连接内核和应用程序的桥梁,并由输入设备读取命令,再将其转为计算机可以理解的机械码,Linux内核才能执行该命令。

  Shell

  优势

  Shell脚本语言的好处是简单、易学、易用,适合处理文件和目录之类的对象,以简单的方式快速完成某些复杂的事情通常是创建脚本的重要原则,脚本语言的特性可以总结为以下几个方面:

  语法和结构通常比较简单。

  学习和使用通常比较简单,

  通常以容易修改程序的“解释”作为运行方式,而不需要“编译。

  程序的开发产能优于运行效能。

  Shell脚本语言是Linux/Unix系统上一种重要的脚本语言,在Linux/Unix领域应用极为广泛,熟练掌握Shell脚本语言是一个优秀的Linux/Unix开发者和系统管理员必经之路。利用Shell脚本语言可以简洁地实现复杂的操作,而且Shell脚本程序往往可以在不同版本的Linux/Unix系统上通用。

  Shell编程

  基本格式

  Shell脚本的文件名后缀通常是.sh (当然你也可以使用其他后缀或者没有后缀,.sh是为了规范)

  程序编写格式:

  [java] view plain copy#!/bin/bash

  # 注释使用#号

  代码示例:

  [java] view plain copy//使用vi编辑器编写shell脚本(a.sh不存在则会新建)

  vi a.sh

  进入vi编辑模式后编写执行代码

  [java] view plain copy//固定格式,记住就可以了

  #!/bin/bash

  //执行的代码

  echo Hello World

  赋予权限并执行:

  [java] view plain copy//赋予可执行权限

  chmod +x a.sh

  //执行(调用/bin/bash执行a.sh脚本)

  。/a.sh

  执行结果:

  Shell

  下面是几种运行情况:

  [java] view plain copya.sh

  这样的话需要保证脚本具有执行权限并且在环境变量PATH中有(。),这样在执行的时候会先从当前目录查找。

  Shell

  [java] view plain copy./a.sh

  只要保证这个脚本具有执行权限即可

  [java] view plain copy/usr/local/a.sh

  只要保证这个脚本具有执行权限即可

  [java] view plain copybash a.sh

  直接可以执行,甚至这个脚本文件中的第一行都可以不引入/bin/bash,它是将hello.sh作为参数传给bash命令来执行的。

  [java] view plain copybash -x /path/to/aa.sh

  bash的单步执行

  [java] view plain copybash -n /path/to/aa.sh

  bash语法检查

  变量

  变量不需要声明,初始化不需要指定类型

  变量命名

  1、只能使用数字,字母和下划线,且不能以数字开头

  2、变量名区分大小写

  3、建议命令要通俗易懂

  注意:变量赋值是通过等号(=)进行赋值,在变量、等号和值之间不能出现空格。

  显示变量值使用echo命令(类似于java中的system.out) ,加上$变量名,也可以使用${变量名}

  例如:

  [java] view plain copyecho $JAVA_HOME

  echo ${JAVA_HOME}

  变量的申明和使用:

  Shell

  变量分类:

  Shell变量有这几类:本地变量、环境变量、局部变量、位置变量、特殊变量。

  本地变量:

  只对当前shell进程有效的,对当前进程的子进程和其它shell进程无效。

  定义:VAR_NAME=VALUE

  变量引用:${VAR_NAME} 或者 $VAR_NAME

  取消变量:unset VAR_NAME

  相当于java中的私有变量(private),只能当前类使用,子类和其他类都无法使用。

  比如在一个bash命令窗口下再使用bash,则变成了子进程,本地变量不会被这个子进程所访问。

  Shell

  环境变量:

  自定义的环境变量对当前shell进程及其子shell进程有效,对其它的shell进程无效

  定义:export VAR_NAME=VALUE

  对所有shell进程都有效需要配置到配置文件中

  [java] view plain copyvi /etc/profile

  source /etc/profile

  相当于java中的protected修饰符,对当前类,子孙类,以及同一个包下面可以共用。

  和windows中的环境变量比较类似

  自定义的环境变量:

  Shell

  局部变量:

  在函数中调用,函数执行结束,变量就会消失

  对shell脚本中某代码片段有效

  定义:local VAR_NAME=VALUE

  相当于java代码中某一个方法中定义的局部变量,只对这个方法有效。

  位置变量:

  比如脚本中的参数:

  $0:脚本自身

  $1:脚本的第一个参数

  $2:脚本的第二个参数

  相当于java中main函数中的args参数,可以获取外部参数。

  Shell

  特殊变量:

  $?:接收上一条命令的返回状态码

  返回状态码在0-255之间

  $#:参数个数

  $*:或者$@:所有的参数

  $$:获取当前shell的进程号(PID)(可以实现脚本自杀)(或者使用exit命令直接退出也可以使用exit [num])

  引号

  Shell编程中有三类引号:单引号、双引号、反引号。

  ‘’单引号不解析变量

  [java] view plain copyecho ‘$name’

  “”双引号会解析变量

  [java] view plain copyecho “$name”

  ``反引号是执行并引用一个命令的执行结果,类似于$(。。。)

  [java] view plain copyecho `$name`

  示例:

  Shell

  循环

  for循环

  通过使用一个变量去遍历给定列表中的每个元素,在每次变量赋值时执行一次循环体,直至赋值完成所有元素退出循环

  格式1

  [java] view plain copyfor ((i=0;i《10;i++))

  do

  。。。

  Done

  格式2

  [java] view plain copyfor i in 0 1 2 3 4 5 6 7 8 9

  do

  。。。

  Done

  格式3

  [java] view plain copyfor i in {0..9}

  do

  。。。

  done

  注意:for i in {0..9} 等于for i in {0..9..1} , 第三个参数为跨步。

  例如:

  {0..9..2} 表示 0,2,4,6,8

  while循环

  适用于循环次数未知,或不便用for直接生成较大的列表时

  格式:

  [java] view plain copywhile 测试条件

  do

  循环体

  done

  如果测试条件为“真”,则进入循环,测试条件为假,则退出循环。

  Shell

  打印结果为0~9.

  循环控制

  循环控制命令——break

  break命令是在处理过程中跳出循环的一种简单方法,可以使用break命令退出任何类型的循环,包括while循环和for循环

  循环控制命令——continue

  continue命令是一种提前停止循环内命令,而不完全终止循环的方法,这就需要在循环内设置shell不执行命令的条件

  条件

  bash条件测试

  格式:

  [java] view plain copytest EXPR

  [ EXPR ]:注意中括号和表达式之间的空格

  整型测试:

  -gt:大于:

  -lt:小于

  -ge:大于等于

  -le:小于等于

  -eq:等于

  -ne:不等于

  例如[ $num1 -gt $num2 ]或者test $num1 -gt $num2

  字符串测试:

  =:等于,例如判断变量是否为空 [ “$str” = “” ] 或者[ -z $str ]

  !=:不等于

  Shell

  判断

  if判断:

  单分支

  [java] view plain copy if 测试条件;then

  选择分支

  fi

  双分支

  [java] view plain copyif 测试条件

  then

  选择分支1

  else

  选择分支2

  fi

  多分支

  [java] view plain copyif 条件1; then

  分支1

  elif 条件2; then

  分支2

  elif 条件3; then

  分支3

  。。。

  else

  分支n

  i

  双分支示例:

  Shell

  Case判断

  有多个测试条件时,case语句会使得语法结构更清晰

  格式:

  [java] view plain copycase 变量引用 in

  PATTERN1)

  分支1

  ;;

  PATTERN2)

  分支2

  ;;

  。。。

  *)

  分支n

  ;;

  esac

  PATTERN :类同于文件名通配机制,但支持使用|表示或者

  a|b:a或者b

  *:匹配任意长度的任意字符

  ?:匹配任意单个字符

  [a-z]:指定范围内的任意单个字符

  示例:

  Shell

  算术运算

  [java] view plain copylet varName=算术表达式

  varName=$[算术表达式]

  varName=$((算术表达式))

  varName=`expr $num1 + $num2`

  使用这种格式要注意两个数字和+号中间要有空格。

  示例:

  Shell

  逻辑运算符

  if [ 条件A && 条件B ] 在shell中怎么写?

  if [ 条件A && 条件B ];then 是不对的

  解决方法:

  (1)需要用到shell中的逻辑操作符

  -a 与

  -o 或

  ! 非

  如if [ 条件A -a 条件B ]

  Shell

  (2)if [ 条件A ] && [条件B ]

  (3)if((A&&B))

  (4)if [[ A&&B ]]

  Shell

  自定义函数

  格式:

  [java] view plain copyfunction 函数名(){

  。。。

  }

  引用自定义函数文件时,使用source func.sh

  有利于代码的重用性

  函数传递参数(可以使用类似于Java中的args,args[1]代表Shell中的$1)

  函数的返回值,只能是数字

Shell

  read

  read命令接收标准输入(键盘)的输入,或者其他文件描述符的输入。得到输入后,read命令将数据放入一个标准变量中。

  格式

  [java] view plain copyread VAR_NAME

  read如果后面不指定变量,那么read命令会将接收到的数据放置在环境变量REPLY中

  [java] view plain copy#表示输入时的提示字符串:

  read -p “Enter your name:” VAR_NAME

  Shell

  [java] view plain copy# -t表示输入等待的时间

  read -t 5 -p “enter your name:” VAR_NAME

  [java] view plain copy# -s 表示安全输入,键入密码时不会显示

  read -s -p “Enter your password: ” pass

  declare

  用来限定变量的属性

  -r 只读

  -i 整数:某些算术计算允许在被声明为整数的变量中完成,而不需要特别使用expr或let来完成。

  -a 数组

  示例:

  Shell

  字符串操作

  获取长度:

  [java] view plain copy${#VAR_NAME}

  字符串截取

  [java] view plain copy${variable:offset:length}或者${variable:offset}

  取尾部的指定个数的字符

  [java] view plain copy${variable: -length}:注意冒号后面有空格

  大小写转换

  小--》大:

  [java] view plain copy${variable^^}

  大--》小:

  [java] view plain copy${variable,,}

  示例:

  Shell

  数组

  定义:declare -a:表示定义普通数组

  特点

  支持稀疏格式

  仅支持一维数组

  数组赋值方式

  一次对一个元素赋值a[0]=$RANDOM

  一次对多个元素赋值a=(a b c d)

  按索引进行赋值a=([0]=a [3]=b [1]=c)

  使用read命令read -a ARRAY_NAME查看元素

  [java] view plain copy${ARRAY[index]}:查看数组指定角标的元素

  ${ARRAY}:查看数组的第一个元素

  ${ARRAY[*]}或者${ARRAY[@]}:查看数组的所有元素

  获取数组的长度

  [java] view plain copy${#ARRAY[*]}

  ${#ARRAY[@]}

  获取数组内元素的长度

  [java] view plain copy${#ARRAY[0]}

  注意:${#ARRAY[0]}表示获取数组中的第一个元素的长度,等于${#ARRAY}

  从数组中获取某一片段之内的元素(操作类似于字符串操作)

  格式:

  [java] view plain copy${ARRAY[@]:offset:length}

  offset:偏移的元素个数

  length:取出的元素的个数

  ${ARRAY[@]:offset:length}:取出偏移量后的指定个数的元素

  ${ARRAY[@]:offset}:取出数组中偏移量后的所有元素

  数组删除元素:

  [java] view plain copyunset ARRAY[index]

  示例:

  Shell

  其他命令

  date

  显示当前时间

  格式化输出 +%Y-%m-%d

  格式%s表示自1970-01-01 00:00:00以来的秒数

  指定时间输出 --date=‘2009-01-01 11:11:11’

  指定时间输出 --date=‘3 days ago’ (3天之前,3天之后可以用-3)

  示例:

  Shell

  后台运行脚本

  在脚本后面加一个&

  [java] view plain copytest.sh &

  这样的话虽然可以在后台运行,但是当用户注销(logout)或者网络断开时,终端会收到Linux HUP信号(hangup)信号从而关闭其所有子进程

  nohup命令

  不挂断的运行命令,忽略所有挂断(hangup)信号

  [java] view plain copynohup test.sh &

  nohup会忽略进程的hangup挂断信号,所以关闭当前会话窗口不会停止这个进程的执行。

  nohup会在当前执行的目录生成一个nohup.out日志文件

  标准输入、输出、错误、重定向

  标准输入、输出、错误可以使用文件描述符0、1、2引用

  使用重定向可以把信息重定向到其他位置

  ls 》file 或者 ls 1》file(ls 》》file)

  lk 2》file(lk是一个错误命令)

  ls 》file 2》&1

  ls 》 /dev/null(把输出信息重定向到无底洞)

  例子:

  [java] view plain copycommand 》/dev/null 2》&1

  Crontab定时器

  linux下的定时任务

  编辑使用crontab -e

  一共6列,分别是:分 时 日 月 周 命令

  Shell

  查看crontab执行日志

  [java] view plain copytail -f /var/log/cron

  必须打开rsyslog服务cron文件中才会有执行日志(service rsyslog status)

  [java] view plain copytail -f /var/spool/mail/root(查看crontab最近的执行情况)

  查看cron服务状态

  [java] view plain copyservice crond status

  启动cron服务

  [java] view plain copyservice crond start

  小结及示例:

  基本格式 :

  *  *  *  *  *  command

  分 时 日 月 周 命令

  第1列表示分钟1~59 每分钟用*或者 */1表示

  第2列表示小时1~23(0表示0点)

  第3列表示日期1~31

  第4列表示月份1~12

  第5列标识号星期0~6(0表示星期天)

  第6列要运行的命令

  crontab文件的一些例子:

  30 21 * * * /usr/local/etc/rc.d/lighttpd restart

  上面的例子表示每晚的21:30重启apache。

  45 4 1,10,22 * * /usr/local/etc/rc.d/lighttpd restart

  上面的例子表示每月1、10、22日的4 : 45重启apache。

  10 1 * * 6,0 /usr/local/etc/rc.d/lighttpd restart

  上面的例子表示每周六、周日的1 : 10重启apache。

  0,30 18-23 * * * /usr/local/etc/rc.d/lighttpd restart

  上面的例子表示在每天18 : 00至23 : 00之间每隔30分钟重启apache。

  0 23 * * 6 /usr/local/etc/rc.d/lighttpd restart

  上面的例子表示每星期六的11 : 00 pm重启apache。

  * */1 * * * /usr/local/etc/rc.d/lighttpd restart

  每一小时重启apache

  * 23-7/1 * * * /usr/local/etc/rc.d/lighttpd restart

  晚上11点到早上7点之间,每隔一小时重启apache

  0 11 4 * mon-wed /usr/local/etc/rc.d/lighttpd restart

  每月的4号与每周一到周三的11点重启apache

  0 4 1 jan * /usr/local/etc/rc.d/lighttpd restart

  一月一号的4点重启apache

  ps和jps

  ps:用来显示进程的相关信息

  ps显示当前shell启动的所有进程

  ps -e显示系统中所有进程

  ps -ef|grep java

  jps:类似linux的ps命令,不同的是ps是用来显示所有进程,而jps只显示java进程,准确的说是显示当前用户已启动的部分java进程信息,信息包括进程号和简短的进程command。

  问题:某个java进程已经启动,用jps却显示不了该进程进程号,使用ps -ef|grep java却可以看到?

  java程序启动后,默认(请注意是默认)会在/tmp/hsperfdata_userName目录下以该进程的id为文件名新建文件,并在该文件中存储jvm运行的相关信息,其中的userName为当前的用户名,/tmp/hsperfdata_userName目录会存放该用户所有已经启动的java进程信息。而jps、jconsole、jvisualvm等工具的数据来源就是这个文件(/tmp/hsperfdata_userName/pid)。所以当该文件不存在或是无法读取时就会出现jps无法查看该进程号。

  原因:1,磁盘读写、目录权限问题。2,临时文件丢失,被删除或是定期清理。3,java进程信息文件存储地址被设置,不在/tmp目录下

  登录Shell和交互shell

  交互式的:顾名思义,这种shell中的命令时由用户从键盘交互式地输入的,运行的结果也能够输出到终端显示给用户看。

  非交互式的:这种shell可能由某些自动化过程启动,不能直接从请求用户的输入,也不能直接输出结果给终端用户看。输出最好写到文件。比如使用Shell脚本。

  登录式:意思是这种是在某用户由/bin/login登陆进系统后启动的shell,跟这个用户绑定。这个shell是用户登陆后启动的第一个进程。login进程在启动shell时传递第0个参数指明shell的名字,该参数第一个字符为“-”,指明这是一个login shell。比如对bash而言,启动参数为“-bash”。

  非登录式:不需login而由某些程序启动的shell。传递给shell的参数,是没有‘-’前缀的。还以Bash为例,当以非login方式启动时,它会调用~/.bashrc,随后~/.bashrc中调用/etc/bashrc,最后/etc/bashrc调用所有/etc/profile.d目录下的脚本。

  一旦打开一个交互式login shell,或者以--login选项登录的非交互式shell,都会首先加载并执行/etc/profile中的命令,然后再依次加载~/.bash_profile, ~/.bash_login, 和~/.profile中的命令。

  当bash以login shell启动时,它会执行/etc/profile中的命令,然后/etc/profile调用/etc/profile.d目录下的所有脚本;然后执行~/.bash_profile,~/.bash_profile调用~/.bashrc,最后~/.bashrc又调用/etc/bashrc。要识别一个shell是否为login shell,只需在该shell下执行echo $0。

  注意: /etc/profile中的设置只对Login Shell生效,而crontab运行脚本的shell环境是non-login的,不会加载/etc/profile的设置。

  Shell应用示例

  根据时间创建文件夹

  需求:创建10个目录,目录名称以当天时间开头,后面拼上目录编码

  例如:1970-01-01_1

  Shell

  编写脚本monitor.sh

  持续观察服务器每天的运行状态,需要结合shell脚本程序和计划任务,定期跟踪记录不同时段服务器的cpu负载,内存,交换空间,磁盘使用量等信息

  [java] view plain copy#!/bin/bash

  #this is the second script!

  day_time=`date+“%F %R”`

  cpu_test=`uptime`

  mem_test=`free -m | grep “mem” | awk ‘{print $2}’`

  swap_test=`free -m | grep “mem” | awk ‘{print $4}’`

  disk_test=`df -hT`

  user_test=`last -n 10`

  echo “now is $day_time”

  echo “%cpu is $cpu_test”

  echo “Numbet of Mem size(MB) is $mem_test”

  echo “Number of swap size(MB) is $swap_test”

  echo “the disk shiyong qingkuang is $disk_test”

  echo “the users login qingkuang is $user_test”

  设置cron任务

  [java] view plain copy*/15 * * * * bash /monitor.sh

  55 23 * * * tar cxf /var/log/runrec /var/log/running.today && --remove-files

  SHELL编程之常用技巧

  /dev和/proc目录

  dev目录是系统中集中用来存放设备文件的目录。除了设备文件以外,系统中也有不少特殊的功能通过设备的形式表现出来。设备文件是一种特殊的文件,它们实际上是驱动程序的接口。在Linux操作系统中,很多设备都是通过设备文件的方式为进程提供了输入、输出的调用标准,这也符合UNIX的“一切皆文件”的设计原则。所以,对于设备文件来说,文件名和路径其实都不重要,最重要的使其主设备号和辅助设备号,就是用ls -l命令显示出来的原本应该出现在文件大小位置上的两个数字,比如下面命令显示的8和0:

  [zorro@zorrozou-pc0 bash]$ ls -l /dev/sda

  brw-rw---- 1 root disk 8, 0 5月 12 10:47 /dev/sda12

  设备文件的主设备号对应了这种设备所使用的驱动是哪个,而辅助设备号则表示使用同一种驱动的设备编号。我们可以使用mknod命令手动创建一个设备文件:

  [zorro@zorrozou-pc0 bash]$ sudo mknod harddisk b 8 0

  [zorro@zorrozou-pc0 bash]$ ls -l harddisk

  brw-r--r-- 1 root root 8, 0 5月 18 09:49 harddisk123

  这样我们就创建了一个设备文件叫harddisk,实际上它跟/dev/sda是同一个设备,因为它们对应的设备驱动和编号都一样。所以这个设备实际上是跟sda相同功能的设备。

  系统还给我们提供了几个有特殊功能的设备文件,在bash编程的时候可能会经常用到:

  /dev/null:黑洞文件。可以对它重定向如何输出。

  /dev/zero:0发生器。可以产生二进制的0,产生多少根使用时间长度有关。我们经常用这个文件来产生大文件进行某些测试,如:

  [zorro@zorrozou-pc0 bash]$ dd if=/dev/zero of=。/bigfile bs=1M count=1024

  1024+0 records in

  1024+0 records out

  1073741824 bytes (1.1 GB, 1.0 GiB) copied, 0.3501 s, 3.1 GB/s1234

  dd命令也是我们在bash编程中可能会经常使用到的命令。

  /dev/random:Linux下的random文件是一个根据计算机背景噪声而产生随机数的真随机数发生器。所以,如果容纳噪声数据的熵池空了,那么对文件的读取会出现阻塞。

  /dev/urandom:是一个伪随机数发生器。实际上在Linux的视线中,urandom产生随机数的方法根random一样,只是它可以重复使用熵池中的数据。这两个文件在不同的类unix系统中可能实现方法不同,请注意它们的区别。

  /dev/tcp & /dev/udp:这两个神奇的目录为bash编程提供了一种可以进行网络编程的功能。在bash程序中使用/dev/tcp/ip/port的方式就可以创建一个scoket作为客户端去连接服务端的ip:port。我们用一个检查http协议的80端口是否打开的例子来说明它的使用方法:

  [zorro@zorrozou-pc0 bash]$ cat tcp.sh

  #!/bin/bash

  ipaddr=127.0.0.1

  port=80

  if ! exec 5《》 /dev/tcp/$ipaddr/$port

  then

  exit 1

  fi

  echo -e “GET / HTTP/1.0\n” 》&5

  cat 《&51234567891011121314

  ipaddr的部分还可以写一个主机名。大家可以用此脚本分别在本机打开web服务和不打开的情况下分别执行观察是什么效果。

  /proc是另一个我们经常使用的目录。这个目录完全是内核虚拟的。内核将一些系统信息都放在/proc目录下一文件和文本的方式显示出来,如:/proc/cpuinfo、/proc/meminfo。我们可以使用man 5 proc来查询这个目录下文件的作用。

  函数和递归

  我们已经接触过函数的概念了,在bash编程中,函数无非是将一串命令起了个名字,后续想要调用这一串命令就可以直接写函数的名字了。在语法上定义一个函数的方法是:

  name () compound-command [redirection]

  function name [()] compound-command [redirection]12

  我们可以加function关键字显式的定义一个函数,也可以不加。函数在定义的时候可以直接在后面加上重定向的处理。这里还需要特殊说明的是函数的参数处理和局部变量,请看下面脚本:

  [zorro@zorrozou-pc0 bash]$ cat function.sh |awk ‘{print “\t”$0}’

  #!/bin/bash

  aaa=1000

  arg_proc () {

  echo “Function begin:”

  local aaa=2000

  echo $1

  echo $2

  echo $3

  echo $*

  echo $@

  echo $aaa

  echo “Function end!”

  }

  echo “Script bugin:”

  echo $1

  echo $2

  echo $3

  echo $*

  echo $@

  echo $aaa

  arg_proc aaa bbb ccc ddd eee fff

  echo $1

  echo $2

  echo $3

  echo $*

  echo $@

  echo $aaa

  echo “Script end!”12345678910111213141516171819202122232425262728293031323334

  我们带-x参数执行一下:

  + aaa=1000

  + echo ‘Script bugin:’

  Script bugin:

  + echo 111

  111

  + echo 222

  222

  + echo 333

  333

  + echo 111 222 333 444 555

  111 222 333 444 555

  + echo 111 222 333 444 555

  111 222 333 444 555

  + echo 1000

  1000

  + arg_proc aaa bbb ccc ddd eee fff

  + echo ‘Function begin:’

  Function begin:

  + local aaa=2000

  + echo aaa

  aaa

  + echo bbb

  bbb

  + echo ccc

  ccc

  + echo aaa bbb ccc ddd eee fff

  aaa bbb ccc ddd eee fff

  + echo aaa bbb ccc ddd eee fff

  aaa bbb ccc ddd eee fff

  + echo 2000

  2000

  + echo ‘Function end!’

  Function end!

  + echo 111

  111

  + echo 222

  222

  + echo 333

  333

  + echo 111 222 333 444 555

  111 222 333 444 555

  + echo 111 222 333 444 555

  111 222 333 444 555

  + echo 1000

  1000

  + echo ‘Script end!’

  Script end!1234567891011121314151617181920212223242526272829303132333435363738394041424344454647

  观察整个执行过程可以发现,函数的参数适用方法跟脚本一样,都可以使用n、*、$@这些符号来处理。而且函数参数跟函数内部使用local定义的局部变量效果一样,都是只在函数内部能看到。函数外部看不到函数里定义的局部变量,当函数内部的局部变量和外部的全局变量名字相同时,函数内只能取到局部变量的值。当函数内部没有定义跟外部同名的局部变量的时候,函数内部也可以看到全局变量。

  bash编程支持递归调用函数,跟其他编程语言不同的地方是,bash还可以递归的调用自身,这在某些编程场景下非常有用。我们先来看一个递归的简单例子:

  [zorro@zorrozou-pc0 bash]$ cat recurse.sh

  #!/bin/bash

  read_dir () {

  for i in $1/*

  do

  if [ -d $i ]

  then

  read_dir $i

  else

  echo $i

  fi

  done

  }

  read_dir $11234567891011121314151617

  这个脚本可以遍历一个目录下所有子目录中的非目录文件。关于递归,还有一个经典的例子,fork炸弹:

  。(){ 。|.& };.1

  这一堆符号看上去很令人费解,我们来解释一下每个符号的含义:根据函数的定义语法,我们知道。(){}的意思是,定义一个函数名子叫“。”。虽然系统中又个内建命令也叫。,就是source命令,但是我们也知道,当函数和内建命令名字冲突的时候,bash首先会将名字当成是函数来解释。在{}包含的函数体中,使用了一个管道连接了两个点,这里的第一个。就是函数的递归调用,我们也知道了使用管道的时候会打开一个subshell的子进程,所以在这里面就递归的打开了子进程。{}后面的分号只表示函数定义完毕的结束符,在之后就是调用函数名执行的。,之后函数开始递归的打开自己,去产生子进程,直到系统崩溃为止。

  bash并发编程和flock

  在shell编程中,需要使用并发编程的场景并不多。我们倒是经常会想要某个脚本不要同时出现多次同时执行,比如放在crond中的某个周期任务,如果执行时间较长以至于下次再调度的时间间隔,那么上一个还没执行完就可能又打开一个,这时我们会希望本次不用执行。本质上讲,无论是只保证任何时候系统中只出现一个进程还是多个进程并发,我们需要对进程进行类似的控制。因为并发的时候也会有可能产生竞争条件,导致程序出问题。

  我们先来看如何写一个并发的bash程序。在前文讲到作业控制和wait命令使用的时候,我们就已经写了一个简单的并发程序了,我们这次让它变得复杂一点。我们写一个bash脚本,创建一个计数文件,并将里面的值写为0。然后打开100个子进程,每个进程都去读取这个计数文件的当前值,并加1写回去。如果程序执行正确,最后里面的值应该是100,因为每个子进程都会累加一个1写入文件,我们来试试:

  [zorro@zorrozou-pc0 bash]$ cat racing.sh

  #!/bin/bash

  countfile=/tmp/count

  if ! [ -f $countfile ]

  then

  echo 0 》 $countfile

  fi

  do_count () {

  read count 《 $countfile

  echo $((++count)) 》 $countfile

  }

  for i in `seq 1 100`

  do

  do_count &

  done

  wait

  cat $countfile

  rm $countfile12345678910111213141516171819202122232425

  我们再来看看这个程序的执行结果:

  [zorro@zorrozou-pc0 bash]$ 。/racing.sh

  26

  [zorro@zorrozou-pc0 bash]$ 。/racing.sh

  13

  [zorro@zorrozou-pc0 bash]$ 。/racing.sh

  34

  [zorro@zorrozou-pc0 bash]$ 。/racing.sh

  25

  [zorro@zorrozou-pc0 bash]$ 。/racing.sh

  45

  [zorro@zorrozou-pc0 bash]$ 。/racing.sh

  5123456789101112

  多次执行之后,每次得到的结果都不一样,也没有一次是正确的结果。这就是典型的竞争条件引起的问题。当多个进程并发的时候,如果使用的共享的资源,就有可能会造成这样的问题。这里的竞争调教就是:当某一个进程读出文件值为0,并加1,还没写回去的时候,如果有别的进程读了文件,读到的还是0。于是多个进程会写1,以及其它的数字。解决共享文件的竞争问题的办法是使用文件锁。每个子进程在读取文件之前先给文件加锁,写入之后解锁,这样临界区代码就可以互斥执行了:

  [zorro@zorrozou-pc0 bash]$ cat flock.sh

  #!/bin/bash

  countfile=/tmp/count

  if ! [ -f $countfile ]

  then

  echo 0 》 $countfile

  fi

  do_count () {

  exec 3《 $countfile

  #对三号描述符加互斥锁

  flock -x 3

  read -u 3 count

  echo $((++count)) 》 $countfile

  #解锁

  flock -u 3

  #关闭描述符也会解锁

  exec 3》&-

  }

  for i in `seq 1 100`

  do

  do_count &

  done

  wait

  cat $countfile

  rm $countfile

  [zorro@zorrozou-pc0 bash]$ 。/flock.sh

  10012345678910111213141516171819202122232425262728293031323334

  对临界区代码进行加锁处理之后,程序执行结果正确了。仔细思考一下程序之后就会发现,这里所谓的临界区代码由加锁前的并行,变成了加锁后的串行。flock的默认行为是,如果文件之前没被加锁,则加锁成功返回,如果已经有人持有锁,则加锁行为会阻塞,直到成功加锁。所以,我们也可以利用互斥锁的这个特征,让bash脚本不会重复执行。

  [zorro@zorrozou-pc0 bash]$ cat repeat.sh

  #!/bin/bash

  exec 3》 /tmp/.lock

  if ! flock -xn 3

  then

  echo “already running!”

  exit 1

  fi

  echo “running!”

  sleep 30

  echo “ending”

  flock -u 3

  exec 3》&-

  rm /tmp/.lock

  exit 01234567891011121314151617181920

  -n参数可以让flock命令以非阻塞方式探测一个文件是否已经被加锁,所以可以使用互斥锁的特点保证脚本运行的唯一性。脚本退出的时候锁会被释放,所以这里可以不用显式的使用flock解锁。flock除了-u参数指定文件描述符锁文件以外,还可以作为执行命令的前缀使用。这种方式非常适合直接在crond中方式所要执行的脚本重复执行。如:

  */1 * * * * /usr/bin/flock -xn /tmp/script.lock -c ‘/home/bash/script.sh’1

  关于flock的其它参数,可以man flock找到说明。

  受限bash

  以受限模式执行bash程序,有时候是很有必要的。这种模式可以保护我们的很多系统环境不受bash程序的误操作影响。启动受限模式的bash的方法是使用-r参数,或者也可以rbash的进程名方式执行bash。受限模式的bash和正常bash时间的差别是:

  不能使用cd命令改变当前工作目录。

  不能改变SHELL、PATH、ENV和BASH_ENV环境变量。

  不能调用含有/的命令路径。

  不能使用。执行带有/字符的命令路径。

  不能使用hash命令的-p参数指定一个带斜杠\的参数。

  不能在shell环境启动的时候加载函数的定义。

  不能检查SHELLOPTS变量的内容。

  不能使用》, 》|, 《》, 》&, &》和 》》重定向操作符。

  不能使用exec命令使用一个新程序替换当前执行的bash进程。

  enable内建命令不能使用-f、-d参数。

  不可以使用enable命令打开或者关闭内建命令。

  command命令不可以使用-p参数。

  不能使用set +r或者set +o restricted命令关闭受限模式。

  测试一个简单的受限模式:

  [zorro@zorrozou-pc0 bash]$ cat restricted.sh

  #!/bin/bash

  set -r

  cd /tmp

  [zorro@zorrozou-pc0 bash]$ 。/restricted.sh

  。/restricted.sh: line 5: cd: restricted12345678

  subshell

  我们前面接触过subshell的概念,我们之前说的是,当一个命令放在()中的时候,bash会打开一个子进程去执行相关命令,这个子进程实际上是另一个bash环境,叫做subshell。当然包括放在()中执行的命令,bash会在以下情况下打开一个subshell执行命令:

  使用&作为命令结束提交了作业控制任务时。

  使用|连接的命令会在subshell中打开。

  使用()封装的命令。

  使用coproc(bash 4.0版本之后支持)作为前缀执行的命令。

  要执行的文件不存在或者文件存在但不具备可执行权限的时候,这个执行过程会打开一个subshell执行。

  在subshell中,有些事情需要注意。subshell中的$$取到的仍然是父进程bash的pid,如果想要取到subshell的pid,可以使用BASHPID变量:

  [zorro@zorrozou-pc0 bash]$ echo $$ ;echo $BASHPID && (echo $$;echo $BASHPID)

  5484

  5484

  5484

  2458412345

  可以使用BASH_SUBSHELL变量的值来检查当前环境是不是在subshell中,这个值在非subshell中是0;每进入一层subshell就加1。

  [zorro@zorrozou-pc0 bash]$ echo $BASH_SUBSHELL;(echo $BASH_SUBSHELL;(echo $BASH_SUBSHELL))

  0

  1

  21234

  在subshell中做的任何操作都不会影响父进程的bash执行环境。subshell除了PID和trap相关设置外,其他的环境都跟父进程是一样的。subshell的trap设置跟父进程刚启动的时候还没做trap设置之前一样。

  协进程coprocess

  在bash 4.0版本之后,为我们提供了一个coproc关键字可以支持协进程。协进程提供了一种可以上bash移步执行另一个进程的工作模式,实际上跟作业控制类似。严格来说,bash的协进程就是使用作业控制作为实现手段来做的。它跟作业控制的区别仅仅在于,协进程的标准输入和标准输出都在调用协进程的bash中可以取到文件描述符,而作业控制进程的标准输入和输出都是直接指向终端的。我们来看看使用协进程的语法:

  coproc [NAME] command [redirections]1

  使用coproc作为前缀,后面加执行的命令,可以将命令放到作业控制里执行。并且在bash中可以通过一些方法查看到协进程的pid和使用它的输入和输出。例子:

  zorro@zorrozou-pc0 bash]$ cat coproc.sh

  #!/bin/bash

  #例一:简单命令使用

  #简单命令使用不能通过NAME指定协进程的名字,此时进程的名字统一为:COPROC。

  coproc tail -3 /etc/passwd

  echo $COPROC_PID

  exec 0《&${COPROC[0]}-

  cat

  #例二:复杂命令使用

  #此时可以使用NAME参数指定协进程名称,并根据名称产生的相关变量获得协进程pid和描述符。

  coproc _cat { tail -3 /etc/passwd; }

  echo $_cat_PID

  exec 0《&${_cat[0]}-

  cat

  #例三:更复杂的命令以及输入输出使用

  #协进程的标准输入描述符为:NAME[1],标准输出描述符为:NAME[0]。

  coproc print_username {

  while read string

  do

  [ “$string” = “END” ] && break

  echo $string | awk -F: ‘{print $1}’

  done

  }

  echo “aaa:bbb:ccc” 1》&${print_username[1]}

  echo ok

  read -u ${print_username[0]} username

  echo $username

  cat /etc/passwd 》&${print_username[1]}

  echo END 》&${print_username[1]}

  while read -u ${print_username[0]} username

  do

  echo $username

  done123456789101112131415161718192021222324252627282930313233343536373839404142

  执行结果:

  [zorro@zorrozou-pc0 bash]$ 。/coproc.sh

  31953

  jerry:x:1001:1001::/home/jerry:/bin/bash

  systemd-coredump:x:994:994:systemd Core Dumper:/:/sbin/nologin

  netdata:x:134:134::/var/cache/netdata:/bin/nologin

  31955

  jerry:x:1001:1001::/home/jerry:/bin/bash

  systemd-coredump:x:994:994:systemd Core Dumper:/:/sbin/nologin

  netdata:x:134:134::/var/cache/netdata:/bin/nologin

  ok

  aaa

  root

  bin

  daemon

  mail

  ftp

  http

  uuidd

  dbus

  nobody

  systemd-journal-gateway

  systemd-timesync

  systemd-network

  systemd-bus-proxy

  systemd-resolve

  systemd-journal-remote

  systemd-journal-upload

  polkitd

  avahi

  colord

  rtkit

  gdm

  usbmux

  git

  gnome-initial-setup

  zorro

  nvidia-persistenced

  ntp

  jerry

  systemd-coredump

  netdata1234567891011121314151617181920212223242526272829303132333435363738394041

  最后

  本文主要介绍了一些bash编程的常用技巧,主要包括的知识点为:

  /dev/和/proc目录的使用。

  函数和递归。

  并发编程和flock。

  受限bash。

  subshell。

  协进程。

  至此,我们的bash编程系列就算结束了。当然,shell其实到现在才刚刚开始。毕竟我们要真正实现有用的bash程序,还需要积累大量命令的使用。

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分