希望在一个shell 循环里用for循环执行传入的参数,该如何做?

您现在的位置: &
shell脚本学习与总结
shell脚本学习与总结
  1.shell 脚本是区分小写的2.Unix特殊字符有: ( ; $ ? & * () [] ` ‘ “ + 使用其时要进行转义()3.Shell的注释以#开头4.函数的定义Function fuction_name(){Command to execute}调用时直接用function_name.5.控制结构1)If...then语句If [ test_command ]ThenCommandsif2)If...then...else语句If [ test_command ]ThenCommandsElsecommandsif3)If...then...elif...then...(else)语句If [ test_command ]ThenCommandsElif [ test_command ]ThenCommandsElseCommandsFi4)for ... In语句For loop_varible in argument_listDoCommandsdone5)while语句While test_command_is_trueDoCommandsDone6)until 语句Until test_command_is_trueDoCommandsDone7)case语句Case $variable inMatch_1)Commands_for_1Match_2)Commands_for_2...*) #option for other valuesCommands_for_no_matchesac6.break、continue、exit和return语句Break跳出整个循环体,然后执行循环体外接下来的代码;Continue 结束本次循环,继续下次循环;Exit 退出整个脚本,一般在其后加入一个整数(如exit 0),作为返回代码发送给系统;Return 用于在函数中返回数据,或返回一个结果给调用函数7.here文档用于将输入重定向到某个交互式shell脚本或程序,而不需要用户介入。Program_name && LABLEProgram_input_1Program_input_2..Program_input_#LABLE注意,程序输入行中的LABLE标记之间是没有空白的,且输入的必须是程序所期望的准确数据,否则可能会失效。8.符号命令( )  在一个子shell中运行括号所括起来的命令(( ))& 在某个shell中对变量进行求值和赋值,并进行数学运算$(( )) 对括起来的表达式进行求值[ ]&& 与test命令相同[[ ]]& 用于字符串比较$( )&& 命令替换` `&&& 命令替换9.命令行参数命令行参数$0,$1,$2,...,$9是位置参数,$0指向的是命令本身。命令shift用于位置参数向左移动,如shift命令命令$2成为$1。Shift加入一个数字来移动多个位置,如shift 3使得$4成为$1。shift是一种按照参数列出顺序来处理每个位置参数的良好方式。10.特殊参数$* 指定所有的命令行参数,与$@的意义一样。两者只有在加双引号时意义不同,如“$*”将整个参数列表作为一个参数来获取,”$@”获取整个参数列表,并将它分隔成不同的参数。$? 检查返回代码。一个成功执行完的命令返回代码为0,不成功是一个非0值。11.双引号,单引号和 `(esc下面的按键)单引号''对内容进行全引用,也就是说,对变量工命令语句使用文字正文,不进行任何替换;而双引号则进行部分引用,则允许字符替换或命令替换。`(esc下面的按键)用于执行某个命令或脚本并替换其输出结果,即命令替换,相同功能有$( )。此外,如果希望每次使用某个变量时重新读取它的值,也可使用它,如`$PWD`,则每次使用这个变量时都会重新读取它的新值。12.文件权限和粘滞位(suid,sgid)文件权限有读,写,执行三种权限。将文件操作模式设为总是作为某个特定的用户(suid),或总是作为某个特定的组成员(sgid)来执行称为设置粘滞位。可以用命令chmod进行修改文件权限。13.在远程主机上运行命令Ssh user@hostname command_to_execute如:ssh jack@192.168.1.3 “uptime”14.设置陷阱当某个程序被迫中止时,会有一个退出信号,这个信号称为一个陷阱(trap)。这样我们可以在捕捉到退出信号时执行命令,如捕获到退出信号1,2,3,15时退出:Trap `echo “nEXITTING on a TRAPPED SINGAL”; exit` 1 2 3 15注意不能捕获到kill -9.的退出信号。15.查看用户信息Who 提供每个登录用户的用户名、tty、登录时间及用户登录地(IP)W 对who的扩展,包括作业进程时间,总用户进程时间等,但没有用户登录地信息。Last 显示自wtmp文件创建开始登录过的用户名单信息,包括登录时间,退出时间,tty等。&&& 16.ps命令显示当前系统进程的信息。17.与用户通信Wall,rwall,write,talk18.大小写文本用tr或typeset命令。VALUES = “AFCDLD”Echo $VALUES | tr ‘[A-Z]' ‘[a-z]'& #将大写转换成小写;tr ‘[a-z]' ‘[A-Z]'则小写转换成大写或在VALUES前使用Typeset -l VALUES& #将大写转换成小写;typeset -u 则小写转换成大写。19.定时运行脚本cronCrontab -e进入用户cron表添加定时脚本,如在1月15日星期天0:12执行脚本/usr/bin/test.sh#分(0-59) 时(0-23)日(1-31) 月(1-12) 星期(0-6for Sunday-saturday) 12 0 15 1 0 /usr/bin/test.sh定时任务还可作用at命令。20.输出控制静默运行,即不输出任何内容到屏幕上: 2&&1 & /dev/null输出到系统指定的控制台: & /dev/console21.解析命令行参数getoptsGetopts optionstring VARIABLEOptionstring 是所需的各种参数,用冒号隔开,如果不需要参数,则冒号可以省略。如果optionstring前有一个冒号,则任何未匹配到时会在VARIBLE中加载一个?号使用getopts的作用是解析出参数,然后作用此参数做不同的操作。如:While getopts :s:m:h:d:p: TMDoCase $TM inS)Do somethingM)Do something...?)Exit 1Esac22.逐行处理文件While read LINEDoEcho “$LINE”Done &$FILENAME23.作用select命令创建菜单Select menu in Yes No QuitDoCase $menu inYes)Do somethingNo)Do somethingQuit)Break*)Do somethingEsacdoneshell脚本学习1,设置运行环境
  在脚本的顶部写入:#!/bin/bash2,SHELL中变量与赋值str=hello&& linux中变量不需定义,要用时直接赋值使用。如:str,注意等号两边不能有空格str=`ls -l /tmp/sh`& 如果要把某个命令的执行结果赋给某个变量时,=号右边要用``括起来echo "$str"&& 查看变量的值,此处结果为:hello3,从键盘输入字符或数值赋给指定的变量read name&& 如:从键盘输入lishi,则name的值为:lishi4,"",'',``双引号,单引号,倒引号之间区别echo "my name is $name" 显示字符串,但含有转义字符引用其变量的值。例中结果:my name is tomecho 'my name is $name' 把单引号中的内容原样显示出来,结果:my name is $nameecho `ls -l`&& 把倒引号中字符当作命令来执行,并把执行结果显示出来。5,加,减,乖,除,求模运算。注意要用倒引号括起来expr `5 + 4`expr `5 - 4`expr `5 \* 4`expr `5 / 4`expr `5 % 4`如果是在脚本里面进行运算时,倒引号要包括=号右边全部内容。如:sum=0sum=`expr $sum + 1`6,对文本操作的命令
  less 能上下翻more 能一屏一屏的翻head 看文本的头10行& 参数-n 5 表示只显示头5行tail 看文本的尾10行,加 -f参数,能实时看日志文件的变化。如看tomcat日志文件变化。tail -f /usr/tomcat/logs/canitsl.out
  参数-n 5 表示只显示头5行7,$?表示上一个命令是否正确执行,0表示正常,1表示错误ls /tmp/hello,如果/tmp/下没有hello这个文件或目录。则$?为1,反之为08,./test lishi wangwu$0程序名,$1第一个参数的名称,例中$0为test$*参数组成的字符串,例中$*为lishiwangwu$#传递给程序参数的总数目,例中$#为29,linux中变量分为全局环境变量和用户配置变量全局环境变量针对系统中所有用户而设置的工作环境目录,在/etc/profile中用户配置变量针对某一用户的。在用户登录的目录下,.bash_profile中10,重定向重定向就是改变原来输入输出的方向,默认都是屏幕输出设备,键盘是输入设备。“&”是输出重定向符。“&”是输入重定向符。“&”只存放正确的信息,“2&”存放错误的信息,每次存入前都会把之前文件内容清空再放入如:ls /usr & /tmp/aaa&& 把/usr目录下的所有文件和文件夹名称信息放到期/tmp/aaa文件中。如:ls /test 2& /tmp/aaa& 如果没有/test文件夹,这时就会发生错误,那么2&就会把错误信息存入aaa文件中。“&”能创建新文件,如:&hello.java“&”能清空一个文件 如hello.java文件中有内容,我再&hello.java这时,hello.java内容清空“&&”双大于号在一起就变成了追加功能,在之前的文件内容后面追加内容。如:cat /tmp/sh && /tmp/aaa&& 把/tmp/sh目录中的所有文件和目录信息追加到aaa文件中。aaa文件之前的内容不会被清除。cat & hello.java时,能在屏幕上输入很多的内容,按ctrl+D时就会退出。再cat hello.java时刚才输入的内容,全在hello.java文件中11,输入重定向如:cat & a.txt &&EEE,从屏幕上输入内容到a.txt中,直到输入EEE时才结束!!!!像这种结合方式,经常被用在自动执行记录某些日志,或者写入信息时用到。
  SYSPROFILE=/etc/profilecat && $SYSPROFILE &&EOFexport& JAVA_HOME=/usr/java/jsdkexport& JAVA_OPTS="-Xms64m -Xmx768m"export& PATH=$JAVA_HOME/bin:$JAVA_HOME/jre/bin::$PATHexport& CLASSPATH=.:$JAVA_HOME/lib:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jarEOF12,管道管道:把前一个命令的输出作为下一个命令的输入。顾名思义就是连接前后两个管道的作用。把上一个管道的尾与下一个管道的头相连接。ls -l /tmp/test | wc -l&&&& 统计/tmp/test目录中共有多少个文件和目录。把/tmp/test目录中文件和目录按列显示。并把显示出来的结果作为wc -l命令的信息源。13,条件判断语句字符串比较:=,!=,-n:判断字符串长度是否大于0,大于0则为真,-z:判断字符串长度是否等于0,等于0则为真数字比较:-eq相等, -ge大于等于,-le小于等于,-ne不等于,-gt大于,-lt小于逻辑判断:!非,&&与,||或文件判断:-d目录判断,-f文件判断,-r可读,-w可写,-x可执行test 条件1 比较符 条件2& 如:test 1 -eq 1[ 条件1 比较符 条件2 ]&& 如:[1 -eq 1 ],[ -n "" ][ `who | wc -l` -le 10 ]&& echo "YES"& 判断当前系统的登录用户数是否小于等于10,是,则输出YES
  16,循环语句:
  while 条件为真时,执行do..done例:j=1while((j&=10)) 或者while [ j -le 10 ]doecho "j=$j"j=`expr $j + 1done if语句:if&&&&&&&&&&&& then&&&&&&& else&&&&&& 此处也可elif与fi再嵌套& fi&&&&&&& 例:x=4;y=7if [ $x -eq $y ]then echo "相等"elseecho "不相等"fi case 变量 in数值1) 语句 ;;数值2) 语句 ;;*) 语句 ;;& #如果数值不在范围之中,就执行这一行例:USER=whoamicase $USER inlishi)echo "you are LISHI";echo "Welcome ";;root)echo "you are ROOT"echo "hi root ";;admin)echo "you are admin";echo "admin,hello ";;*)echo "当前用户不是lishi,root,admin";;esac for 循环例:用FOR循环显示/tmp/sh目录中的每个文件信息。变量i的取值是/tmp/sh目录中的每个文件如for i in "a" "b" "c",此时变量i每次循环的取值为a,b,cpath=/tmp/sh/for i in `ls $path`dols -l& $idone例:#用for与if相结合的手法,显示出从1到键盘输入数之间的偶数#注意if语句的双括号read xfor((i=1;i&=$x;i++))doif [ $i % 2 == 0 ]thenecho "$i"fidone14,函数# 定义一个累加的函数sum, 再从键盘上输入两个数,再调用sum函数# 注意,函数一定要放在调用该函数的前面sum(){a=$xb=$ytotal=`expr $a + $b`echo "total = $total"}echo "please enter two number:"read xread ysum $x,$y shell& start:
  shell有bsh,bash,cash等
  1,在linux中管理员用户登录进去时,提示符为:#,一般用户登录进去时的提示符为:$登录进去后,退出或要切换用户时,用:exit命令,正常退出。2,查看当前系统中的shell版本,在/etc/shell目录中查看。3,在系统中查看不同用户默认的shell版本,/etc/passwd& 查看当前用户的shell,echo shell4,直接用命令改变某个用户的shell环境:chsh 系统用户名,根据提示输入新shell路经,如:/bin/bash5,查看当前用户的环境变量和ID号,set | grep user,set | grep uid& 或,查看/etc/passwd文件6,查看某个命令在哪个位置& which 命令& 如:which ifconfig&& 当某些一般用户中有些命令提示无法找到或执行时,一般是环境变量中没有把该命令的路经加进来。用export命令设置环境变量 7,查看以前使用过的命令 history&& history -c清除之前使用过的命令8,;在shell中用来区分一个命令的结束。一行中可以多个命令9,调试shell脚本用. 脚本文件名& 或 bash 脚本文件名10,文件权限分三类:a,文件属主:创建该文件的用户b,同组用户:拥有该文件的用户组中的任何用户c,其它用户:即不属于拥有该文件的用户组的某一用户如:-rwxr-xr-x 1 root root& 217 08-10 19:51 test1.sh第一个字符表示文件的类型,是文件夹,还是普通文件 例中-表示普通文件后面的9个字符分三段,第一段是文件属主的权限第二段是同组用户的权限,第三段是其它用户的权限赋权限:g是代表同组用户,o是代表其它用户chmod go+wx ./test.sh& 给同组用户和其它用户赋写和执行的权限& chmod u+wr ./test.sh& 给自己赋读和写权限chmod o+wrx ./test.sh&& 给其它用户赋读,写和执行的权限去权限与赋权限同理,只是把“+”换成“-”就可以了chmod go-rw ./b.c& 把同组用户和其它用户读和写的权限去除
  11,一般权限也可以用数字表示:4:读,2:写,1:执行,如果用数字给某一个文件赋权限,要写3段数字,如:764,则表示用户自己是读写执行,同组用户是读写,其它用户是读的权限。12,给文件和文件夹赋权限时,两者互不干扰,除非在给文件夹赋权限时带-R参数那么,该文件夹下的所有内容就赋予了和文件夹一样的权限了,小心使用-R13,查看文件夹权限时,用:ll -d /tmp/sh&&& -d是查看文件夹的,不然只会列出该文件夹的内容了。
  14,改变文件的所属用户,chown oracle /tmp/sh/api.sh&& 改变文件的所属组,chown :oracle /tmp/sh/api.sh同时,改变文件的所属用户和组,chown oracle:dba /tmp/sh/api.sh& 那么api.sh的所属用户和组信息为:-rwxrwxr-x 1 oracle dba 264 07-28 15:57 /tmp/sh/api.sh15,id& 命令,查看当前是哪个用户,以及哪个组的相当信息16,groups 查看系统当前有多少个组,groups 用户名 如:groups oracle,查看用户所属组17,getent group 组名& 如:getent group dba& 查看dba组中有哪些用户18,创建一个用户并把它加入到指定组中 useradd wangcai -G root
  19,当一个脚本需要以拥有者或组的用户执行时,需要用到suid,guid文件设置了suid或guid时,如果该文件没有执行权限,那么设置suid或guid时就没有意思,会用大写"S"表示。4代表suid,2代表guid如:start-orcl.sh 例子,首先用chown改变文件所属用户,再用chmod改变文件suid和guid的权限-rwxr-xr-x 1 root root& 632 08-15 17:31 start-orcl.shchown oracle start-orcl.sh-rwxr-xr-x 1 oracle root& 632 08-15 17:31 start-orcl.shchmod 6751 start-orcl.sh-rwsr-s--x 1 oracle root& 632 08-15 17:31 start-orcl.sh
  20,用指定的用户身份执行一个脚本 su - oracle -c "/tmp/sh/start-orcl.sh"以oracle的身份执行start-orcl.sh这个脚本21,创建快捷方式 ln -s /tmp/sh 1& 创建快捷方式1指向/tmp/sh,访问1相当于访问/tmp/sh一样。22,定时任务:用 service crond status 查看 cron服务状态,如果没有启动则 service crond start启动它。基本用法:crontab -l列出当前的crontab任务crontab -d删除当前的crontab任务crontab -e (solaris5.8上面是 crontab -r)编辑一个crontab任务,ctrl_D结束crontab filename crontab的格式为:分 时 日 月 星期 命令(中间用空格隔开)。&&& crontab文件的条目是从左边读起的,第一列是分,以此类推,最后一列是需要执行的命令。&&& 每一列称为crontab的一个域,在这些域中,可以用-来连接一个时间范围,例如星期一到星期五,可以用1-5来表示。&&& 单个时间点可以用,号分隔,比如星期一和星期四,那么可以表示为1,4。如果某个表示时间的域没有特别限制,可以用*号表示。每一个时间条目包含5个域,用空格隔开。&&& 比如我希望每天晚上21:30运行bin目录下的cleanup.sh文件,那么该命令就应该为:&&& 30 21 * * * /app/bin/cleanup.sh (注意:由于不需要限定日期、月份、星期,所以日期、月份和星期域用*号表示)&&& 比如我希望每月得1、10、20的00:00运行backup.sh文件,那么命令应该为:&&& 00 00 1,10,20 * *& /app/bin/backup.sh (注意:由于不需要限定月份和星期,所以月份和星期域用*号表示)&&& #每两个小时&&& 0 */2 * * * date&&& crontab -e 然后在打开的文件中编辑内容,如:0 */2 * * * date,保存退出。&&& 也可以把如: 0 */2 * * * date的内容,放到一个filename文件中,再用crontab filename &&& 把内容加到crontab中,这时用crontab -l也可以看到filename文件中的内容。让配置文件生效:如果让配置文件生效,还得重新启动cron,切记,既然每个用户下的cron配置文件修改后。也要重新启动cron服务器,/etc/init.d/crond restart 。编辑/etc/crontab文件,在末尾加上一行: 30 5 * * * root init 6 这样就将系统配置为了每天早上5点30自动重新启动。需要将crond设置为系统启动后自动启动的服务,可以在/etc/rc.d/rc.local 中,在末尾加上service crond start如果还需要在系统启动十加载其他服务,可以继续加上其他服务的启动命令。比如: service mysqld start shell脚本学习小结 1.字符截断: 如果是一般路径的字符截断可以用basename 和dirname 这两个工具: basename 可以从一个文件路径中截一个文件名 例如: $ basename /home/file.tar file.tar dirname 可以从一个文件路径中截到一个目录路径 例如: $ dirname /home/file.tar /home 不使用外部工具进行字符截断 bash 有自带的功能来对变量进行字符截断,一般使用"##","#","%%","%","*" 组 合来实现。例如:   复制代码 代码如下:  $ string=hellowbashshell $ echo ${string##*sh} ell $ echo ${string#*sh} shell $ echo ${string%%sh*} hellowba
----------------------- Page 2----------------------- $ echo ${string%sh*} hellowbash "#"表示从字符开始部分除去,一旦匹配则立即除去 "##"表示从字符开始部分除去,会搜整个字符串最长的和的匹配来除去 "%"表示从字符结束的部分除去,一旦匹配成公则立即除去 "%%"表示从字符结束的部分开始除去,会搜寻整个字符穿中最长的匹配来除去 "*"统配符,一般与“##”或"#"联用时放在搜索字符串的左边,例如:${String#*sh}(在sh 的左 边),与"%%"或"%"联用时会放在匹配字符串的右边,例如:${String%%sh*} 常用技巧: 在路径中取文件名:${path##*/} (与basename 相同功能) 在路径中取目录路径:${path%/*}(与dirname 相同功能) 取文件的扩展名:${path##*.} 2. 自变量的接收 接收来自命令行传入的参数,第一个参数用$1 表示,第二个参数$2 表示,。。。以此类推。 注意:$0 表示脚本文件名。另外一个在shell 编程中经常用到的是“$@”这个代表所有的参 数,。你可以用一个循环来遍历这个参数。如果用java 来类比的话,可以把$@看作是man 函数中定义的那个数组 3.if 语句: 格式: if [ condition ] then action fi 注意:“if”和“[”之间需要空格,如果你不空格,shell会报告语法错 误的。我就被这个浪费了好一阵时间 ----------------------- Page 3----------------------- conditon 测试类型对照表 运算符 描述 示例 文件比较运算符 -e filename 如果filename 存在,则为真 [ -e /var/log/syslog ] -d filename 如果filename 为目录,则为真[ -d /tmp/mydir ] -f filename 如果filename 为常规文件,则[ -f /usr/bin/grep ] 为真 -L filename 如果filename 为符号链接,则[ -L /usr/bin/grep ] 为真 -r filename 如果filename 可读,则为真 [ -r /var/log/syslog ] -w filename 如果filename 可写,则为真 [ -w /var/mytmp.txt ] -x filename 如果filename 可执行,则为真[ -L /usr/bin/grep ] filename1-nt 如果filename1 比filename2 [ /tmp/install/etc/services -nt filename2 新,则为真 /etc/services ] filename1-ot 如果filename1 比filename2 [ /boot/bzImage -ot filename2 旧,则为真 arch/i386/boot/bzImage ] 字符串比较运算符 (请注意引号的使用,这是防止空格扰乱代码的好方法) -z string 如果string 长度为零,则为真 [ -z "$myvar" ] -n string 如果string 长度非零,则为真 [ -n "$myvar" ] string1= string2 如果string1 与string2 相同,[ "$myvar" = "one two three" ] 则为真 string1!= string2 如果string1 与string2 不同,[ "$myvar" != "one two three" ] 则为真 算术比较运算符 num1-eq num2 等于 [ 3 -eq $mynum ] num1-ne num2 不等于 [ 3 -ne $mynum ] num1-lt num2 小于 [ 3 -lt $mynum ] num1-le num2 小于或等于 [ 3 -le $mynum ] num1-gt num2 大于 [ 3 -gt $mynum ] num1-ge num2 大于或等于 [ 3 -ge $mynum ] 感觉bash 中的if 相比其他的一些语言智能多了,在bash 中,测试一个文件的存在跟比较 两个数字的大小没有什么两样 ;) ----------------------- Page 4----------------------- 4.for 语句 bash 里的语句总是那么的人性化,十分的接近自然语言,在for 语句中几乎可以 迭代任何类似与集合的数据类型(或许这样个说法不对,但我确实想不到更好的 词来代替)。 看一个例子: #!/bin/bash for args in $@ do echo $args done 把上面这段代码录入保存为showargs.sh 设置为可执行(chmod +x showargs.sh) 执行: $ ./showargs.sh arg1 arg2 arg3 arg4 arg1 arg2 arg3 arg4 这个例子中,我们用到了之“$@”,它代表了所有的命令行参数。在这里用for 对其进行遍历,系统迭代地从$@中取出命令行参数把他放到args 中,最后使用 echo $args 进行输出。 for 更经常用到的是遍历目录,下面的例子用于列出当前目录下的所有文件和文 件夹的名称   复制代码 代码如下:  $ for file in * & do & echo $file & done
这里用*代表当前目录,列出的是所有的文件和文件夹的名称,在这里,文件夹 和文件你是分不出来的,如果你需要,你应该用if [-d ${file}]来做一下判断。 ----------------------- Page 5----------------------- 对于文件遍历,更有趣的是,你可以在 “in” 后面接上多个表达式。也就是说, 你可以一次在遍历多个目录。 下面这段代码能把当前目录下go 文件夹和do 文件夹里的文件复制到fo 文件夹 下   复制代码 代码如下:  #!/bin/bash for args in ./go/* ./do/* do cp ${args} ./fo echo "copying ${args} to ./fo/${args}" done
&&&主编推荐
&&&热门试卷
&&&最新视频
&&&热门阅读
&&&最新问答
&&&&&&&&&&&&&&&
希赛网 版权所有 & &&&&湘教QS2-164&&增值电信业务经营许可证湘B2-Python问题:如何在shell中执行一个模块,这个模块需要传入参数?
[问题点数:0分]
Python问题:如何在shell中执行一个模块,这个模块需要传入参数?
[问题点数:0分]
不显示删除回复
显示所有回复
显示星级回复
显示得分回复
只显示楼主
相关帖子推荐:
本帖子已过去太久远了,不再提供回复功能。作者: Chet Ramey
Unix Shell提供了一个接口,支持用户通过命令与操作系统进行交互。但shell同时也算得上是一门丰富的编程语言,因为它包含了基本的流程控制结构: 替换(alternation),循环,条件判断,还有基本的数学操作,函数定义,字符串变量,以及与命令之间的双向通信。
shell可以在终端或终端模拟器(如xterm)中以交互的方式运行,也可以存储在文件中作为脚本来使用。大部分现代shell环境(包括Bash)提供命令行编辑功能,用户可以使用Emacs或Vi风格的快捷键来编辑命令行,或访问命令的历史纪录。
Bash的处理过程类似于shell的流水线(pipe):首先由终端或脚本读入数据,然后使用一系列变换过程依次进行处理,执行到最后一个shell命令后返回。
本章将讨论Bash的主要组件:输入处理,解析,单词展开(word expansion)和其他命令处理,管道(pipeline)中的命令执行。这些组件构成一个流水线(pipeline),从键盘或脚本中获取字符,然后逐步转化为命令。
图3.1 Bash组件结构
Bash是一种GNU操作系统中的shell,通常在Linux内核上实现,其他操作系统(比如著名的Mac OS X)也有一些不同的实现版本。Bash在sh的历史版本的基础上做了一些功能上的改进,使其更便于交互式使用或编程。
Bash是Bourne-Again SHell的缩写,为了纪念shell鼻祖Stephen Bourne(他是当代Unix Shell程序/bin/sh的创造者,该程序出现在贝尔实验室第七版Unix上)。Bash的原作者是自由软件基金会(Free Software Foundation)的一名雇员Brian Fox,而我是现在的开发者和维护员,同时还是俄亥俄州凯斯西储大学(Case Western Reserve University in Cleveland, Ohio)的志愿者。
和其他GNU软件一样,Bash具有很好的可移植性。Bash能运行在几乎每个版本的Unix上,它还被移植到了其他操作系统上(比如Windows上Cygwin和MingW),Bash同时还是某些类Unix操作系统(比如QNX和Minix)发行版的一部分。Bash的移植只依赖于一个Posix的编译和运行环境,这个条件容易满足,比如微软公司的Unix服务(Services For Unix, SFU)就能支持Posix。
3.2 句法单元和原语(primitive)
3.2.1 原语(primitive)
Bash中包含三种基本记号(token):保留关键字,单词,操作符。保留关键字指在shell中和编程语言中有明确含义的词语,这些关键字通常用来表达程序控制结构(比如if和while)。操作符由一个或多个元字符(metacharacter)构成,元字符指shell中具有特殊意义的字符,比如|和&。余下的shell的输入都可以视为普通的单词,但有时也会有特殊的含义,比如赋值语句和数字,这完全取决于在命令行中的位置。
3.2.2 变量和参数
和所有的编程语言一样,shell也提供变量,变量是一些用来指代数据并支持数据操作的名称。shell的变量包括用户变量和内置变量(称为参数)。一般,shell参数反映了shell的内部状态,它们的值可能是自动设置的,也可能由其他操作设置。
变量的值都是字符串。根据上下文的不同,有一些值会被特殊处理(后面会有解释)。变量使用&name=value&的形式来赋值,&value&这一项是可选的,将其省略表示赋值为空字符串。赋值时,shell将&value&展开并赋给&name&。shell能根据一个变量是否被设置(set)来执行不同的操作,但是赋值是设置变量的唯一方式。如果变量没有赋值,即使已经声明或已经设置属性,也会被视为没有设置(unset)。
以美元符($)开头的单词表示对变量或参数的引用。带$的单词将会被变量的值所替代。shell提供了大量的展开操作符,包括简单的值替换,根据模式匹配修改或删除变量值的一部分。
shell还支持局部变量和全局变量,但变量都默认是全局的。如果赋值语句置于简单命令(最常见的命令类型,可附带一些参数和重定向)前,那么这些赋值语句定义的变量将是局部的。shell中可以定义过程(即shell函数),函数中也可以定义局部变量。
为减小代码量,除了通常的变量之外,shell还支持整数和数组。整型变量表示数字,任何字符串赋值给整形变量时都会展开为算术表达式,计算结果并赋值。数组可以是索引数组(indexed array)或关联数组(associative array),前者使用数字下标,后者使用字符串下标。数组元素都是字符串,必要时也可视为整数。但数组不支持嵌套,即数组元素不能是其他数组。
Bash使用哈希表(hash table)来存储和访问shell变量,这些哈希表被链接起来管理变量的作用域。shell函数中支持多种变量作用域,比如命令前置赋值语句构成一个临时作用域。当被前置赋值语句的命令是shell内置命令时,shell就必须跟踪变量的顺序,以保证变量引用的正确性,作用域链表可实现这个功能。如果运行嵌套层次太多,需要遍历的作用域数目会相当惊人。
3.2.3 shell编程语言
简单的shell命令包含命令名称(比如echo或cd),可选数目的参数和重定向(redirection)。重定向允许shell用户控制命令的输入和输出。如前所述,用户可以为简单命令定义局部变量。
保留关键字可以实现复杂的shell命令。shell中包含了任何高级语言中都有的程序结构,比如if-then-else, while, 遍历列表的for循环,C风格的算术for循环等等。因而,shell中可构造诸如选择性和重复执行命令的更为复杂的命令。
Unix带给计算世界的一个重要贡献是管道(pipeline),管道指一系列顺序执行的命令,其中前一个命令的输出构成下一个命令的输入。任何shell结构都可以用于管道,我们甚至可以使用管道来生成其自身的输入。
Bash支持标准输入,标准输出,和标准错误(standard error)三个数据流,命令的结果可以重定向到一个文件或一个进程(Unix中,任何进程和设备都可视为文件)。shell程序员还可以在当前shell环境中使用重定向来打开或关闭文件。
Bash支持shell编程,shell脚本可以存储起来重复使用。shell函数和shell脚本都可以作为命令来执行,和单个命令的使用类似。shell函数定义为特殊的格式,可以在同一个shell上下文中存储和执行。shell脚本将命令存储在一个文件中,只能在一个新的shell进程中执行。shell函数共享了所在环境中的大部分上下文,但是shell脚本由于在新的shell进程中调用,只能共享一些进程间间的环境。
3.2.4 注意事项
继续往下读时,读者要时刻牢记shell的实现代码中只使用了少量的数据结构: 数组,树,单向链表和双向链表,以及哈希表。几乎所有shell结构都是用这些基本结构实现的。
shell在不同阶段传输信息并处理数据单元的数据结构是WORD_DESC。
typedef struct word_desc {
/* Zero terminated string. */
/* Flags associated with this word. */
} WORD_DESC;
单词被组合为简单的链表,比如参数列表。
typedef struct word_list {
struct word_list *
WORD_DESC *
} WORD_LIST;
WORD_LIST在shell中无处不在。一个简单的命令就是一个单词列表,展开结果同样是一个单词列表,内置命令的参数还是一个单词列表。
3.3. 输入处理
Bash流水线(pipeline)的第一个阶段是输入处理,即从终端或文件中读入字符,拆成行,传递给shell解析器,然后转换为命令。所谓行其实就是以换行符结尾的字符串。
3.3.1. readline和命令行编辑
在交互模式下,Bash的输入来自终端,而对于脚本的执行,输入则来自参数。在交互模式下,Bash允许用户编辑命令行,快捷键类似于Unix Emacs或Vi编辑器。
Bash使用readline软件库来实现命令行编辑,用户可以使用多种功能,比如保存命令行,调用前一条命令,执行csh风格的历史记录的展开。bash是readline库的主要用户,两者也是在一起开发,但readline的实现并不依赖于Bash。许多其他项目也采用readline来支持终端命令行编辑。
readline还支持为众多readline命令绑定不限长度的快捷键。readline的命令功能强大,支持光标在命令行中移动,插入或删除文字,获取以前的记录,补全文本等等。基于这些命令,用于可以自定义宏,按下指定的快捷键就可以插入一段定制的文字。readline的宏为用户提供简单字符串替换和速记功能。
readline 结构
readline是一个包含基本读入/分发/执行/重显示等步骤的循环结构。readline从键盘使用read或类似的函数读入字符,或者使用宏来获得输入。每个字符都是键映射表(或分发(dispatch)表)中的索引。虽然索引都是单字节字符,映射表的的值却可以表达更多的内容,因为它们可以指向新的映射表,这使得readline能够支持多字符快捷键。快捷键最后会被绑定到某个readline命令上(如beginning-of-line),并且触发这个命令。当敲击键盘时,字符会存入编辑缓冲区,这是因为这些字符本身是已经绑定到了self-insert命令上。readline还支持让一个快捷键绑定到一个命令,同时延长快捷键再绑定到另外一个命令上,映射表中有一个特殊的索引来标识这种情况(这个功能最近才开始支持)。将快捷键绑定到宏上带来了极大的灵活性,用户可以编辑任意字符,还可以定制复杂的快捷操作。readline的编辑缓冲区存储了绑定到self-insert命令的那些字符,缓冲区显示时占用一行或多行。
readline使用C语言字符来处理字符缓存和字符串,并基于这些简单字符来构造多字节字符。出于速度和存储方面的考虑,readline内部并不使用wchar_t类型,事实上,人们开始编码的年代还不怎么广泛支持多字符字符。如果本地设置(locale)支持多字节字符,readline自动读入整个多字节字符并插入编辑缓冲区。因为可以用一些简单字符序列来表达多字节字符,将多字节字符设为快捷键在理论上是行得通的,但是难于实现而且没人愿意这么用。比如,Emacs和Vi的命令中就全部是单字节字符构成的快捷键。
当快捷键触发编辑命令后,命令的执行可能会导致缓冲区中插入新的字符,或者编辑位置发生变化,或者部分乃至整行都被替换,但是readline始终会及时地将结果更新到终端上。有些编辑命令虽然可绑定到快捷键上,但并不会改变编辑缓冲区,比如有的只是修改历史文件。
更新终端显示虽然看起来简单,实际却非常复杂。readline必须跟踪3个地方:当前缓冲区内容,更新后的缓冲区内容,以及实际显示的字符。若考虑多字节字符,显示出来的字符可能与缓冲区内容并不一定完全相符,更新显示时必须要考虑到这个问题。当重新显示时,readline需要比较当前缓冲区内容与更新后缓冲区内容,找出差异再决定怎样高效地将差异更新到屏幕上。这个问题实际上已经被研究多年(即所谓的串对串校验问题, string-to-string correction problem)。readline采用的方法是定位出差异区域的边界点,然后计算出更新这一部分的代价,包括移动光标的代价(删除一个字符再插入一个字符显然不如直接覆盖)。readline随后以最小的代价来更新终端,删除行尾多余的字符,并将光标置于正确的位置。
更新显示引擎是readline中修改最为频繁的代码。大部分的修改都者为了增加新的功能,其中最为重要的功能是让提示符(prompt)保持稳定(比如提示符的颜色),以及处理多字节字符的能力。
readline将编辑缓冲区中的内容返回给调用程序,然后由调用程序负责存储到历史列表中。
readline扩展程序
readline不仅为用户提供了多种定制和扩展默认行为的方式,还支持让应用程序扩展默认功能。首先,可绑定的readline函数接受一个参数集合并返回特定类型的结果,应用程序可以很容易地使用定制的函数来扩展readline。以Bash为例,Bash绑定了30多个命令,支持Bash关键字的补全,调用shell内置命令等。
readling还允许应用程序使用回调函数来修改默认行为。应用程序可以传入函数指针替换readline内部函数,干涉其运行过程,从而执行特定的操作。
3.3.2. 非交互式输入处理
如果shell不使用readline,它会使用stdio或自己的输入缓冲函数来获得输入。如果shell是非交互式的,则Bash的缓冲输入模块更倾向于使用stdio,因为Posix标准对输入有特别的限制条件:shell在解释一条命令时只能占用必要的输入,而把其它的留给运行中的其它程序。这点非常重要,特别是shell从标准输入读入一个脚本的情况。shell允许在输入时尽可能多地缓冲输入,只要它能够在文件中回滚到解析器停止的位置。这意味着在不可随机访问(non-seekable)设备(如管道pipe)中shell一次只能读一个字符,对于文件来说shell则可以缓存任意多的内容。
不考虑这些特殊情况,非交互式输入的处理结果和readline一样:每个缓冲区以换行符(newline)结束。
3.3.3. 多字节字符
多字节字符的处理功能是在shell出现之后很久才加入进来的,因此其设计原则是尽可能小地影响已有代码。如果本地设置(locale)支持多字节字符,shell将输入存储在字节缓冲区中(简单的C语言字符),但是会将它们作为多字节字符进行处理。readline知道如何显示多字节字符(关键问题是一个多字节字符占用多少屏幕空间,以及屏幕上显示一个多字节字符需要使用多少个字节),如何在一行中移动一个字符(不是一个字节)的位置,等等。除了这些问题,多字节字符不会怎么影响输入处理。但需要注意的是,shell的其它部分在处理输入时需要考虑到多字节字符的影响(后面会有介绍)。
解析引擎的第一步工作是词法分析:将字符流分割成单词(word),然后赋予其意义。单词(word)是解析器操作的基本单元,是由元字符分隔的字符序列。简单的元字符如空格和制表符(tab),shell语言中的特殊字符也可构成元字符(如分号和&符号)。
Tom Duff曾在他的关于rc的文章(the Plan 9 shell)中说过,shell的一个历史遗留问题是没有人真正知道什么是Bash的语法。Posix的shell委员会最终还是发布一个Unix Shell的标准语法,虽然这个语法仍然存在大量的上下文依赖关系,而且还不能兼容以前的一些代码,但无疑这个标准是目前最好的,它的发布值得赞赏。
Bash解析器源于早期版本的Posix语法,而且,据我所知,它也是唯一一个使用Yacc或Bison实现的Bourne风格的shell。这带来了一些麻烦,shell语法本身并不特别适合yacc风格的语法解析,需要采用一些复杂的语法分析,而且解析器和词法分析器之间也要通力合作。
执行过程中,词法分析器从readline或其它输入来源中获取字符行,根据元字符将它们分割成记号(token),并根据上下文定位这些记号,然后将其传给解析器组合成语句和命令。这个过程中涉及了大量的上下文,比如,获得的单词可能是保留字,可能是标识符,可能是赋值语句的一部分,也可能是其它单词。下面是一个完全合法的命令:
do for= echo $for
这个命令的作用是打印&for&这个字符串。
在这里顺便简单介绍一下别名(aliasing)。Bash支持使用任意文本(称为别名alias)替换简单命令的第一个单词(word),这个替换过程完全是基于文本的。因而别名可以改变shell的语法,有的别名甚至实现了一个Bash不支持的复合命令。别名完全是在Bash解析器的词法分析阶段实现的,而解析器必须告诉分析器(analyzer)什么时候允许展开别名。
和许多编程语言一样,shell支持字符的转义,用来改变字符的原有含义,使得一些元字符(如&)可以出现在命令中。Bash中三种类型的引用,相互之间稍有不同,对包含的文本的解析方式也不尽相同。第一种是反斜划线(backslash),用来转义后面的一个字符。第二种是单引号,它禁止对包含的文本进行解析。第三种是双引号,它阻止部分解析,但是允许一些单词(word)的展开(处理反斜划线的方式也不相同)。词法分析器解译被引用的字符和字符串,防止它们被解析为保留字或元字符。但有两个例外情况,$'...'和$&...&,它们处理转义字符的方式和ANSI C一样,允许使用标准国际化函数来处理。前者使用更为广泛,后者则因为案例太少而不为人知。
从解析器(parser)到分析器(analyzer)剩下的工作就非常直接了。解析器对一些状态进行编码,然后共享给分析器以支持依赖上下文的语法分析。比如,词法分析器根据记号的类型将单词(word)分类为:保留字(依赖上下文),单词,赋值语句,等等。为实现这个功能,解析器提供解析命令的进度信息,比如是否正在处理一个多行文本(有时候称为here-document),或者一个case语句或条件命令,或者shell的扩展模式(extended shell pattern)或复合赋值语句。
在解析阶段,识别命令替换结点位置的大部分工作被封装在了一个函数中(parse_comsub),这个函数能够处理大量的shell语法,并且包含了大量的冗余的读取记号的代码。而且,parse_comsub函数必须要知道here-document,shell命令,元字符和单词(word)的边界,引用(quoting),以及什么时候允许保留字存在(比如,识别case语句中的保留字)。正确完成这些工作是比较费时的。
在单词展开过程中展开一个命令替换,Bash使用解析器来获得这个结构正确的结束位置。这和eval命令将字符串变成命令的过程有些类似,但不同的是命令的结束位置并不在字符串的结尾。为了实现这个功能,解析器必须将右括号视为一个合法的命令终止符,这又导致了大量特殊情况的出现,并且要求词法分析器(在恰当的上下文中)将右括号标记为EOF。在递归触发yyparse之前,解析器还必须能够保存和恢复解析器状态,因为在读取一条命令的中途,一个命令替换的解析和执行可能是展开一个提示字符过程的一部分。因为输入函数实施了预读(输入来源可能是字符串,文件,或使用readline的终端),这个函数必须最后能将输入指针(input pointer)恢复到正确的位置。这一点非常重要,不仅保证了输入不会丢失,而且使得命令替换的展开函数能构造出正确的用于执行的字符串。
可编程的单词展开也存在类似的问题,因为在解析一个命令时允许执行其它任意命令,解决方案是在执行前保存状态,在执行后恢复状态。
引用(quoting)是另外一个不兼容性和争论的来源。在Posix的shell标准发布二十年之后,标准工作组成员们仍然在争论引用的那些难以理解的行为是否合理。但是Bash一直都只是一个实现上的参考,对制订标准是无能为力的。
解析器返回一个C结构体来表达一个命令(对于复合命令,这个结构体中可能还包含有其它命令),然后将其传递给shell的下一个阶段:单词展开。命令结构体由一系列命令对象和单词列表组成。大部分的单词列表对应于各种变换,其意义随上下文的变化而变化(下面会有解释)。
3.5. 单词展开
在解析阶段之后,在执行阶段之前,解析阶段产生的许多单词对应于一个或多个单词展开,比如$OSTYPE可以展开为&linux-gnu&。
3.5.1. 参数和变量展开
变量展开是用户最熟悉的。shell变量几乎都没有类型(少数例外),都被视为字符串。展开就是将这些字符串展开和转变为新的单词和单词列表。
有的展开可以作用于变量的值上。编程人员可以获得变量值的子字符串,获得值的长度,删除匹配串首或串尾的子串,根据字符串匹配替换子串,或者修改值中的大小写。
还有一些展开依赖于变量的状态:变量是否已经设置会导致不同的展开或赋值行为。比如,若parameter已经设置(并且不为空),${parameter:-word}会被展开为parameter,否则展开为word。
3.5.2. 其他展开
Bash还支持其他多种展开,它们的规则都很诡异。处理过程中,最优先的展开是括号展开(brace exapansion),括号展开把
pre{one,two,three}post
preonepost pretwopost prethreepost
命令替换(command substitution)是shell执行命令与操纵变量的完美结合。shell运行一个命令,收集其输出,然后将输出作为展开的值。
命令替换的一个问题在于命令的立即执行然后等待结果,此过程中shell无法传入输入。Bash使用一个称为进程替换(process substitution)的功能来弥补这些不足,进程替换实际上是命令替换和管道的组合。和命令替换类似,Bash运行一个命令,但令其运行于后台而不再等待其完成。关键在于Bash为这条命令打开了一个用于读和写的管道,并且绑定到一个文件名,最后展开为结果。
下一个展开是波浪符展开(tilde expansion)。起初,~alan记号只是表示对Alan主目录的引用,多年后,这却渐渐变成了引用多个不同目录的一种方式。
最后是算术展开(arithmetic expansion)。$((expression))会执行expression表达式,其规则与C语言表达式一致。expression的计算结果变成展开的结果。
单引号和双引号之间最明显的差异在于变量展开(variable expansion)。单引号禁止所有展开,被包含的字符全部原封不动,而双引号则允许部分展开。单词展开(word expansion),命令展开,算术展开,进程替换都可以在双引号中进行(双引号只处理其结果),而括号展开和波浪符展开则被禁止。
3.5.3. 单词拆分
单词展开的结果会被拆分(split),拆分的限定符来自于shell变量IFS的值。shell使用这种方式将单个单词拆分成多个。每当$IFS1中的一个字符在结果中出现,Bash就会将单词一分为二。单引号和双引号中禁止单词拆分。
3.5.4. 名称替换(globbing)
结果拆分之后,shell将逐个处理之前展开得到的单词,使用一个模式来匹配已知的文件名,包括一些主要的文件目录。
3.5.5. 实现
如果shell的基本架构将流水线(pipeline)并行化,单词展开则是它自身的管道。单词展开的每个阶段处理一个单词,经过可能的转换,然后将其传递给下一个展开阶段。等到所有的单词展开都已执行,命令就可以开始执行了。
Bash的单词展开的实现基于前面所说的基本数据结构。对解析器输出的单词逐个展开,由一个单词获得一个或多个单词。WORD_DESC是一个通用的数据结构,足以封装单个单词展开所需的所有信息。其中,flags可用于编码单词展开阶段的信息,并把信息传递给下一个阶段。例如,解析器使用一个标志位记录某个单词是一个赋值语句,并通知展开阶段和命令执行阶段,而单词展开代码内部也使用标志位来禁止单词拆分或标记空字符串(比如&$x&,其中$x未定义或为空值)。使用单个字符串来表达展开后的单词,并且用某种字符编码来表达其它信息,实现起来并不容易。
对于解析器来说,单词展开的代码还需要处理多字节字符。比如,变量长度展开(${#variable})用于计算字符数目而非字节数,这些代码能正确识别展开的结束位置或多字节字符存在情况下特殊字符的展开。
3.6. 命令执行
Bash内部流水线到命令执行阶段才真正开始执行命令。大部分时候,展开后的单词集合被分解为命令名称和参数集合,然后作为文件传递给操作系统被读入和执行,而余下的单词则作为argv剩下的部分传递给操作系统。
到目前为止,描述的主要是简单命令(命令名加上一些参数)的执行,它们是最常用的类型,但是Bash还提供了更多的功能。
命令执行阶段的输入是一些解析器创建的命令结构,以及一些展开单词的集合。这是Bash编程语言真正开始起作用的时候。编程语言使用变量和前面提到的展开来实现高级语言中常见的结构:循环(looping),条件(conditionals),替换(alternation),分组(grouping),选择(selection),基于模式匹配的条件执行,表达式求值,以及其它一些shell特有的结构。
3.6.1. 重定向
shell作为操作系统界面的表现之一是能够将输入和输出重定向到它所调用的命令上。重定向的语法反映了shell早期用户的复杂操作:直到最近,用户仍然需要跟踪正在使用的文件描述符(file descriptor),并且显式地用数字来指定(除了标准输入,标准输出和标准错误)。
重定向语法最近增加的一个功能是允许用户将shell定向到一个合适的文件描述符,然后赋值给一个指定的变量,因而用户不需要再去选择文件描述符。这个功能减少了程序员跟踪文件描述符的负担,但是增加了额外的处理过程:shell必须将文件描述符复制到正确的位置,还要确保它们赋值给指定的变量。这是又一个从词法分析器到解析器再到命令执行的信息传递过程的例子:分析器找出包含变量赋值的重定向,解析器生成重定向对象并用一个标志位标记需要赋值,然后重定向代码根据这个标志位确保文件描述符数字被赋值到正确的变量上。
实现重定向最难的地方是记得如何撤销重定向。shell有意不区分文件命令和内置命令,但是,无论是哪种命令,重定向的影响范围不应该超过命令结束的时候2。因此,shell必须跟踪重定向,保证可以将其撤销,否则,内置命令的输出重定向会改变shell的标准输出。Bash知道如何撤销每一种重定向,要么关闭之前分配的文件描述符,要么先保存文件描述符之后再使用dup2恢复。这些过程使用的重定向对象和解析器创建的重定向对象是一样的,并且用同样的函数来处理。
因为多重重定向使用简单对象列表来实现,用来撤销的那些重定向也保存在一个单独的列表中。这个列表在命令完成时进行处理,但是shell必须确认处理的时机,因为绑定到shell函数或&.&内置命令上的重定向必须在它们执行完之前都一直有效。如果没有命令被执行,内置命令exec将直接丢弃这个撤销列表,因为关联到exec上的重定向在整个shell环境中都是有效的。
另外一个复杂的地方来自Bash本身。Bourne shell的历史版本只允许用户操纵文件描述符0-9,而把10和10以上保留为内部使用。后来,Bash放松了这个限制,允许用户在不超过进程文件打开数限制的条件时使用任意文件描述符。这意味着Bash必须跟踪内部的文件描述符,包括那些不是直接由shell打开而是由外部库打开的那些文件描述符,这些内部文件描述符还可能时不时被移动。因此,大量的文件描述符需要跟踪,有的使用启发式的close-on-exec标志位,有的重定向列表在整个命令执行过程中都需要维护,然后在执行完成时处理或丢弃。
3.6.2. 内置命令
Bash中包含了大量的内置命令,这些命令由shell执行,但不会创建新进程。
使用内置命令最普遍的原因是维护和修改shell内部状态。比如cd命令就是一个Unix世界中的经典案例,cd命令需要修改当前工作目录这个内部状态,因此不能用外部命令来实现。
Bash内置命令与shell其他部分使用相同的原语(primitive)。每个内置命令都是用C语言函数来实现的,以单词列表作为其参数。这些单词来自于单词展开阶段,内置命令将其视为命令名和参数。大部分情况下,内置命令使用标准的展开规则,但也有一些例外:Bash内置命令接受赋值语句作为其参数时(比如declare和export),赋值参数的展开规则与寻常变量赋值时的展开一致。WORD_DESC结构体中的flags成员在这个地方也会发挥作用,在shell内部流水线(pipeline)不同阶段之间传递信息。
3.6.3. 简单命令执行
简单命令是shell中最常遇到的命令。其过程无非是:搜索和执行文件命令,收集退出状态,简单命令覆盖了shell当前大部分功能。
shell的变量赋值(形如var=value的单词)本身也是一种简单命令。赋值语句可以置于其它命令之前,也可以单独成一行。如果置于命令前,变量将传递给该命令的局部环境(如果这个命令是内置命令或shell函数,除少数例外情况,变量赋值都将起作用)。如果单独成一行,赋值语句将修改shell状态的值。
当出现一个既不是shell函数又不内置命令的命令名称,Bash会搜索文件系统来寻找同名的可执行文件。PATH环境变量的值是一些由冒号分隔的目录列表,Bash根据这些目录来搜索文件命令。命令名称中若包含斜划线(或其它目录分隔符),Bash将直接执行此命令,而不会再搜索。
当一个命令通过PATH搜索得到,Bash将命令名称和对应的完整路径存于哈希表中,PATH搜索之前会查询这个表来提高效率。如果命令没有被搜到,Bash会执行一个特殊的函数,以命令名称和参数作为这个函数的参数。一些Linux发行版通过这个方式来提示用户安装缺失的命令。
如果Bash搜索并执行一个文件命令,它会创建一个新的执行环境并fork出一个新的进程,然后让此可执行文件在新的环境中执行。执行环境完全从shell环境复制而来,只是在信号处理和重定向的文件打开关闭等方面有少许修改。
3.6.4. 任务控制
shell在前台执行命令时,必须等待命令返回才能执行下一条命令,但shell还可以在后台执行命令,然后立刻读入下一条命令。任务控制(Job control)指在前台和后台之间切换进程(正在执行的命令),以及挂起(suspend)和恢复(resume)执行的能力。为实现这个功能,Bash引入了任务(job)的概念,任务本质上指的是被一个或多个进程执行的某个命令。比如,管道为每个管道元素创建一个进程,它们共同构成一个任务。进程组可以将多个分离的进程组合成单个任务。终端绑定了一个进程组ID,这同时也是前台进程组的ID。
shell使用一些简单的数据结构来实现任务控制。用一个结构体就能表达一个子进程,包括了进程ID,状态和终止时的返回状态。管道只不过是这个进程结构体的简单链表。任务(job)的结构更加简单,包括进程列表,一些任务状态(运行,挂起,退出,等等),以及任务进程组的ID。进程列表通常只有一个进程,只有管道才会导致一个任务包含多个进程。如果一个进程ID和任务进程组ID相同,则此ID成为这个进程组的领导(leader)。当前任务集合存于一个数组中,这种实现方式在概念上和实际的表现形式是比较接近的。任务的状态和退出状态由其成员进程的相应状态组合而成。
和shell中其他部分一样,实现任务控制的复杂性在于登记(bookkeeping)。shell必须把进程归入正确的进程组中,并保证子进程的创建也要同步进行。因为终端的进程组决定了前台任务,终端的进程组必须要设置正确(如果前台任务没有归入shell的进程组,那么shell将无法读入终端输入)。因为任务如此依赖于进程,实现复杂命令并不是那么显然,比如while和for循环可以作为一个整体来停止或启动,其他shell很少能做到这一点。
3.6.5. 复合命令
复合命令包含了一个或多个简单的命令,里面往往还带有一些关键字(比如if或while)。这保证了shell的强大编程能力。
复合命令的实现没有什么特别的地方。解析器为各个命令构造相应的对象,然后遍历这些对象。每个复合命令都使用一个C函数来实现,它的功能包括执行恰当的展开,执行特定的命令,根据命令的返回值来变更执行流程。以实现for命令的函数为例。首先它会展开in关键字后面的单词列表,该函数随后遍历这些单词,在for循环中赋给相应的变量并执行。for命令的执行不会受子命令返回值得影响,但是break和continue这两个内置命令会改变循环的执行过程。一旦列表中所有的单词都已经处理完,for命令就会返回。从这个例子可以看出来,命令的实现和语法的描述是非常相近的。
3.7. 经验教训
3.7.1. 什么是重要的
参与到Bash项目中已经有20多年,在这期间我也获益良多。最重要的一点是一定要保留详细的修改日志,其重要性怎么强调都不过份。通过阅读修改日志来回忆起当初的想法,感觉是很好的。甚至你还可以将某个修改与一个bug报告联系起来,然后编写一个重现bug的测试用例或提出一些建议。
如果条件允许,我建议在项目之初就考虑全面的回归测试。Bash拥有数千个测试用例,覆盖了差不多所有的非交互性功能。我考虑过测试交互式功能,其实Posix标准的一致性测试套件中就有交互性测试,只是并没有将这个测试框架发布出来(我认为很有必要)。
标准很重要,Bash受益于它是一个标准的实现。参与到你正在实现的软件的标准化过程中来也是非常重要的。在讨论相关功能和行为时,标准往往是最终的参考依据。当然,前提是这是一个好的标准。
外部标准重要,内部标准同样重要。我很幸运地接触到了GNU项目的诸多标准,它们包含了大量关于设计和实现方面的好且实用的建议。
好的文档同样非常关键。如果你希望别人使用你的软件,全面并清晰的文档就是必要的。一个成功的软件必须拥有大量的文档,因而开发者提供权威的版本就显得非常重要。
优秀的软件随处可见,那就充分利用起来吧。比如,gnulib中包含了大量的有用的函数,你尽可以把它们&抠&出来。BSD各个版本和Mac OS X就是这么干的。Picasso说过:好的艺术家靠的是偷,说的就是这个道理。
参与用户社区,但是准备挨骂,有时候这并不好受。活跃的用户社区好处是显然的,但是这些人可能会非常情绪化,不要太当真就好。
3.7.2. 如果可以重来
Bash拥有数百万用户,我知道后向兼容有多么地重要。在某种意义上,后向兼容意味着永远不用向用户说抱歉。但是,这个世界远不是这么简单。事实上,我不得不一直做一些破坏兼容性的修改,比如修正一个不好的决定,修改一个错误的设计,或者更正shell不同部分之间的不兼容性,这些都是情有可原的修改,但是几乎都会引起一些用户的抱怨。我早就应该对当兼容性分级处理的。
Bash的发展一直都没有特别的开放,我已经习惯于里程碑发布形式(比如 bash-4.2)并由个人提交补丁。我的理由是:我需要适应开发商们更长的发布周期(相对于自由软件和开源世界),而且我也有过beta版本传播地过于广泛的不快回忆。当然,如果一切都要重来,我还是会考虑更快的发布频率,比如可以使用一个公开的源码仓库。
不真正动手去做是完不成任何事的。有一件事我已经考虑了很久,却一直没去做,那就是将Bash的解析器重写为递归下降(recursive-descent)的方式,以取代bison。以前,我以为为了遵守Posix标准,这件事就非得做,但是后来我只需要少量修改就解决了这个问题。如果当时就从头写起,大概现在我已经实现了一个新的解析器,那么很多问题都会变得简单得多。
Bash是一个优秀和复杂的大型免费软件。经历了超过二十年的良性发展,已经变得成熟和强大。现在,Bash几乎远处不在,数百万人每天都在使用,虽然有些人可能还浑然不知。
从Stephen Bourne写出第七版Unix中的shell开始,Bash就一直受到多方的影响。其中最重要的影响来自于Posix标准,它形成了shell很重要的一部分行为。然而兼容性和标准化难以两全,实现起来是非常头痛的事。
Bash受益于GNU项目,因为GNU项目提供了一个Bash赖以生存的开发生态环境。没有GNU就不会有Bash。Bash还得益于活跃并朝气蓬勃的用户社区,用户的反馈成就了今天的Bash,这也是自由软件的精髓所在。
alternation: 某种编程语言中的结构,翻译成替换。在Perl中alternation指一个字符串集合中的任选一个。
word: 指shell中的的一个字符串(以空格等非打印字符分隔),翻译成单词。根据上下文的不同,word具有两种含义。广义上讲,所有连续的可打印字符串都可以视为word,但具体到shell中,word又可指狭义的不含特殊字符的字符串。两种含义在文中都有出现,但以后者为主。
expansion: 展开。类似于变量取值,将包含特殊字符和结构的字符序列经过一次或多次替换得到一个新的字符串
pipe/pipeline: 管道, 将一些命令串连起来(用|符号),前一个命令的输出构成后一个命令的输出。另外一种语义是:shell的内部流程,翻译成流水线。
数据与命令: 文中这两个概念容易混淆。因为所有的shell命令或脚本都是以字符串形式存在,因此都可以视为shell解释器的输入数据。但是,这些shell命令或脚本从编程语言的角度来看,又是以语句为基本单元的,这样的语句可被称为命令。而shell中最基本的命令则包括内置命令和文件命令, 从这个意义上看, 一个语句包含了命令和命令的参数,而这些参数又可视为命令的数据。
primitive: 原语
token: 记号
metacharacter: 元字符
builtin: 内置命令
parser: 解析器
lexical analysis: 词法分析
local: 本地设置
prompt: 提示符
大部分情况下, 只包含了一个这样的字符.
内置命令exec是这条规则的例外情况
图上的文字也应该翻译出来
描述了bash的设计思路,根据处理命令的流水线工作展开,读后发现像shell这类工具,用起来很方便,但内在考虑的问题并不简单。

我要回帖

更多关于 shell while循环 的文章

 

随机推荐