在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
今天木有冷笑话,只有一个噩耗。噩耗是:今天木有冷笑话!!! 第七章输入输出、文件与命令执行 学C的应该了解标准输入输出和错误输出吧?感觉总打很多字进度太慢,所以一直在省略类似C的东西,也方便以后看这篇文章的人能够快速学完shell脚本(或者是快速看完这本书)。 读取行read命令是重要方式之一,它可以自标准输入读取行后,通过shell字段切割的功能(使用$IFS)进行切分,第一部分给第一个变量,第二部分给第二个,类推。如果切割单词多余变量,则剩下所有的给最后一个变量。如果输入行以反斜杠结尾,则read会丢弃反斜杠与换行符继续读取下行数据。它有一个选项 -r,它将忽视最后反斜杠当读入数据。使用read可能的一个错误是通过循环让read读取一个文件如: 再介绍一个用来改变shell本身I/O设置的exec命令。如果只有I/O重定向而没有任何参数时,exec会改变shell的文件描述符: 复制代码 代码如下: exec 2> /tmp/$0.log #重定向shell本身的标准错误输出 exec 3< /some/file #打开新文件描述符3 ... read name rank serno <&3 #从该文件读取 exec 3>&- #关闭文件描述符3 上例展示了如何关闭文件描述符。 书中对printf做了完整的介绍,这里就不再介绍了,就是C里边的那些东西。 shell中有两种与文件名相关的展开:第一个是波浪号展开(~ tilde expansion),另一个叫法较多如通配符展开式(wildcard expansion)、全局展开(globbing)或路径展开(pathname expansion)。 如果命令行字符第一个字符为波浪号或者变量指定的值里任何未被引号括起来的冒号之后的第一个字符为波浪号时,shell便会执行波浪号展开。波浪号展开的目的,是要将用户根目录的符号型表示方式,改为实际的目录路径。 shell环境下的通配符展开,有几个基本的通配符:? , * , [set] [!set] ,前俩略过,第三个是匹配出现中括号集合中的字符,第四个取反义。比如可以查找 *.html 就知道处所有类似的文件。另外有一点注意的,在linux下文件名里的.号没有任何特殊意义,匹配所有文件时只需用一个*即可,不用像windows下那样*.*。 命令替换,书上写的概念很绕口,其实就是一个命令的用法或者写法,例如: 书中教了一个expr命令,从提到这个命令,到接下来的两段都在说这个命令不好用,并且可以是由$(( )) ,test替代。但是可以了解一下,作用就是计算之后跟着的一个表达式比如:expr 1 + 1 。这里注意加号两边的空格,是必要的,书里貌似没说,郁闷半天才发现必须添加空格。。- -! 这里又提了引用,就是说用来防止shell将某些东西解释成你不想要的意义,比如你就是有就想要*,而不是需要一个通配符,这时候你需要转义(\) ,或者是单引号引起来(单引号引起来的内容转义符号也无效),或是双引号。混用的时候请小心。 书中详细说了一下命令的执行顺与,感觉很有必要细看一下,全都摘录一下。 shell程序碰到一句命令,都会执行上边的一次流程。比如: 复制代码 代码如下: $ mkdir /tmp/x $ cd /tmp/x $ touch f1 f2 $ f=f y="a b" $ echo ~+/${f}[12] $y $(echo cmd subst) $((3+2)) > out 命令一开始会根据shell语法分割token,最重要一点是I/O重定向 > out 在这里是被识别的,并存储供稍后使用。最后这句echo被分为5个token,分别是: echo ,~+/${f}[12] , $y , $(echo cmd subst) ,$((3 + 2))这5个部分。 eval语句是再告知shell取出eval的参数,并再执行它们一次,是他们经过整个命令行的处理步骤。看一个例子: 复制代码 代码如下: listpage="ls | more" $listpage 运行之后你会发现shell把|与more看作ls的参数,而不是直接产生一页页的文件列表。这是由于在shell执行变量时,管道字符出现在步骤5,也就是在它确实寻找管道字符之后(在步骤1).变量的展开一直要到步骤8才进行解析。结果,shell把 | 与more看作ls的参数,使得ls会试图在当前目录下寻找名为|与more的文件。 现在,想想eval $listpage吧,在shell到达最后一步时,会执行带有ls、|与more参数的eval,这会让shell回到步骤1,具有一行包括了这些参数的命令。在步骤1发现|后,将该行分割为两个命令:ls 和more。每个要被处理的命令都以一般方式执行,最后的结果是在当前目录下分页的文件列表。 还有两个其他的结构,有时也很有用,subShell与代码块。 代码块概念上与subShell雷同,只不过它不会建立新进程。代码块用花括号括起。且会对主脚本造成影响(比如当前目录)。一般花括号被视为关键字,即它们只有出现在命令的第一个符号时被识别。实际上,这表示你必须将结束花括号放置在换行符或分号之后。 shell有很多命令,之前说过,特殊内建命令与一般内建命令的差别在于shell查找要执行的命令时,会先查找特殊内建命令再找shell函数,接下来才是一般内建命令。最后是$PATH路径内的外部命令。这种查找顺序让定义shell函数以扩展或覆盖一般shell内建命令成为可能。举例说你希望shell的提示号能包含当前目录路径的最后一个组成部分。最简单的实现方式,就是在每次改变目录时,都让shell改变PS1.你可以写一个自己专用的函数如下: 复制代码 代码如下: # chdir ---改变目录时更新PS1的个人函数 chdir () { cd "$@" #实际更改目录 x=$(pwd) #取得当前目录的名称 PS1="${x##*/}\$ " #截断前面的组成部分后,指定给PS1 } 这么做有个问题,你必须在shell下输入chdir而不是cd,这样你可以自己写一个名为cd的函数,然后shell会先找到你的cd函数,而不是一般内建函数cd。但是这样又会有问题,shell函数如何真正访问cd命令,这里函数内cd会再此调用你写的cd函数导致递归出现。这时候我们需要转义策略,使用内建命令command来告诉shell要避开函数的查找直接访问真正的命令。 #cd --改变目录时更新PS1的私人版 第八章产生脚本
这一章详解了两个好用脚本的实现过程,这两个脚本详解内容揉杂在注释里给出。 脚本一:功能是在给出的路径下查找目标路径 复制代码 代码如下: #! /bin/sh - # # 标准输出所产生的结果,通常是查找路径下找到的每一个文件之第一个实体的完成路径, # 或是“filename: not found ”的标准错误输出。 # # 如果所有文件都找到,则退出码为0, # 否则,即为找不到的文件个数(非0) # shell的退出码限制为125 # 语法: # pathfind [--all] [--?] [--help] [--version] envvar pattern(s) # # 使用--all选项时,在路径下的每一个目录都会被查找, # 而非停在第一个找到者。 # 所有脚本的头部说明脚本功能是必不可少的,对人阅读很有用。 #在网络的环境下,安全性一直是必须慎重考虑的问题。其中有一种攻击shell脚本 #的方式,是利用输入字段分隔字符:IFS,它会影响shell接下来对输入数据解释的 #方式。为避免此类的攻击,部分shell仅在脚本执行前,将IFS重设为标准值;其他 #则导入该变量的一个外部设置。很难在屏幕上看出来,单引号内包含一个换行一 #个空格和一个制表符,这是IFS的默认值。也可以使用转义\040\t\n,但bourne #shell不支持这一的转义。重新定义IFS时有一点要特别留意,当"$*"展开以回复命 #令行时,IFS值的第一个字符,会被当成字段分隔符。这里不使用$*,不受影响 IFS=' ' #另一种常见的安全性攻击,则是欺骗软件,它执行非我们所预期的命令。为了阻断 #这种攻击,我们希望调用的程序是可信任的版本,而非潜伏在用户提供的查找路径 #下的欺骗程序,因此我们将PATH设最小值,存储初始值供以后使用。exprot语句 #是这里的关键么,它可以确保所有子进程继承我们的安全查找路径。 OLDPATH="$PATH" PATH=/bin:/usr/bin export PATH #错误输出函数 error(){ echo "$@" 1>&2 usage_and_exit 1 } #简短的一个信息提示函数,$PROGRAM稍后会赋值为命令名 usage(){ echo "Usage: $PROGRAM [--all] [--?] [--help] [--version] envvar pattern(s)" } #提供信息和状态码退出 usage_and_exit(){ usage exit $1 } #提供用户版本号 version(){ echo "$PROGRAM version $VERSION" } #给出警告信息并在状态码上加1,记录警告次数 warning(){ echo "$@" 1>&2 EXITCODE=`expr $EXITCODE + 1` } #按大写全局,小写局部命名规则,初始化需要用的变量 all=no envvar= EXITCODE=0 #basename会截去参数最后一个斜杠之前字串,返回剩下的部分 PROGRAM=`basename $0` VERSION=1.0 #接下来这块就是linux经典的命令行参数解析部分了,不多解释,需要注意的是?号 #是通配符,所以筛选选项的时候防止展开加上单引号。 while test $# -gt 0 do case $1 in --all | --al | --a | -all | -al | -a ) all=yes --help | --hel | --he | --h | '--?' | -help | -hel | -he | -h | '-?' ) usage_and_exit 0 --version | --versio | --versi | --vers | --ver | --ve | --v | \ -version | -versio | -versi | -vers | -ver | -ve | -v ) version exit 0 -* ) error "Unrecognized option: $1" * ) break #这里我小纠结了一下esac命令,没搞清楚在这干嘛的,也没这个命令说明,仔细看 #看才发现是case逆序写法,是case的结束标志,就像if结束标志fi一样 esac shift done #下面我们要处理除选项外的参数了,我们可以用"$@"来取得,但是避免将他们存 #储在变量内如files="$@",因为文件名中如果有空格将无法正确被处理。 envvar="$1" test $# -gt 0 && shift #因为有可能用户提供的环境变量是PATH,为安全性考虑会重设,这是我们检测该变 #量,并适当更新envvar,开头的x是为了避免开展当成test的选项。 test "x$envvar" = "xPATH" && envvar=OLDPATH #下边这句虽然很段,但是最棘手部分:使用shell的eval语句。我们envvar里已经拥 #有了环境变量的名称,可以"$envvar"取得,但我们现在要的是它的展开,我们也 #想要冒号分隔符转换成一般空白分隔符。如果MYPATH为用户所提供的名称,我们 #便会构建参数字符串'${'"$envvar"'}',也是shell展开为'${MYPATH}'的等同物。两边 #的单引号是为了避免它更进一步展开,该字串传给eval,它会将其视为两个参数: #echo与${MYPATH}。eval在环境下寻找MYPATH,假设找到就执行展开命令,并输 #出,通过管道传给tr命令将冒号转换为空格,最后将转化值给dirpath,错误信心隐藏 #输给/dev/null dirpath=`eval echo '${'"$envvar"'}' 2>/dev/null | tr : ' ' ` #为错误情况进行健全检测 if test -z "$envvar" then error Environment variable missing or empty elif test "x$dirpath" = "x$envvar" then error "Broken sh on this platform: cannot expand $envvar" elif test -z "$dirpath" then error Empty directory search path elif test $# -eq 0 then exit 0 fi #接下来三重循环,外层处理参数文件或模式,中层循环处理查找路径下的目录,内 #层循环匹配单一目录下的文件。 for pattern in "$@" do result= for dir in $dirpath do for file in $dir/$pattern do if test -f "$file" then result="$file" echo $result test "$all" = "no" && break 2 fi done done test -z "$result" && warning "$pattern: not found" done #限制退出状态是一般linux实现上的限制 test $EXITCODE -gt 125 && EXITCODE=125 exit $EXITCODE 这里作者给留了课后作业:增添一个功能,不单单只能匹配文件,也能匹配其他东西比如:符号性连接文件,可读取文件或者可执行文件之类的,需要test -x选项来进行匹配,本人完成的如下: 复制代码 代码如下: #变量初始化的地方增添一个test选项变量,默认为f testopt=f #选项解析的位置增添test选项并检测合法性 --test | --tes | --te | --t | -test | -tes | -te | -t ) echo $2 if echo $2 | grep -e "^[bcdefgGhkLOPrSstuwx]\{1\}$" then testopt=$2 shift else error "Unrecognized --test option: $2" fi #最后循环test匹配位置改为: if test -"$testopt" "$file" 欧了,课后作业完成,安全性还没经验,有老师批改作业木有? 下边给了第二个脚本,是软件构建自动化,代码灰长长。。。这个打代码都码的头疼了,跳过吧,有兴趣童鞋自己搞定。我是不求甚解的先赶进度了,回来再搞。 复制代码 代码如下: #! /bin/sh - # 在一台或多台构建主机上,并行构建一个或多个包 # # 语法: # build-all [ --? ] # [ --all "..." ] # [ --check "..." ] # [ --configure "..." ] # [ --environment "..." ] # [ --help ] # [ --logdirectory dir ] # [ --on "[user@]host[:dir][,envfile] ..." ] # [ --source "dir..." ] # [ --userhosts "file(s)" ] # [ --version ] # package(s) # # 可选用的初始化文件: # $HOME/.build/directories list of source directories # $HOME/.build/userhosts list of [user@]host[:dir][,envfile] IFS=' ' PATH=/usr/local/bin:/bin:/usr/bin export PATH UMASK=002 umask $UMASK build_one(){ #语法: # build_one [user@]host[:build-directory][,envfile] arg="`eval echo $1`" userhost="`echo $arg | sed -e 's/:.*$//'`" user="`echo $userhost | sed -e s'/@.*$//'`" test "$user" = "$userhost" && user=$USER host="`echo $userhost | sed -e s'/^[^@]@//'`" envfile="`echo $arg | sed -e 's/^[^,]*,//'`" test "$envfile" = "$arg" && envfile=/dev/null builddir="`echo $arg | sed -e s'/^[^,]*,//'`" test "$builddir" = "$arg" && builddir=/tmp parbase=`basename $PARFILE` #NB:如果这些模式被更换过,则更新find_package() package="`echo $parbase | \ sed -e 's/[.]jar$//' \ -e 's/[.]tar[.]bz2$//' \ -e 's/[.]tar[.]gz$//' \ -e 's/[.]tar[.]Z$//' \ -e 's/[.]tar$//' \ -e 's/[.]taz$//' \ -e 's/[.]zip$//'`" #如果我们在远程主机上看不到包文件,则复制过去 echo $SSH $SSHFLAGS $userhost "test -f $PARFILE" if $SSH $SSHFLAGS $userhost "test -f $PARFILE" then parbaselocal=$PARFLE else parbaselocal=$parbase echo $SCP $PARFILE $userhost:$builddir $SCP $PARFILE $userhost:$builddir fi #在远程主机上解开存档文件、构建,以及后台执行方式检查它 sleep 1 #为了保证唯一的日志文件名 now="`date $DATEFLAGS`" logfile="$package.$host.$now.log" nice $SSH $SSHFLAGS $userhost " echo '==================================================' ; test -f $BUILDBEGIN && . $BUILDBEGIN || \ test -f $BUILDBEGIN && source $BUILDBEGIN || \ true ; echo 'Package: $package' ; echo 'Archive: $PARFILE' ; echo 'Date: $now' ; echo 'Local user: $USER' ; echo 'Local host: `hostname`' ; echo 'Local log directory: $LOGDIR' ; echo 'Local log file: $logfile' ; echo 'Remote user: $user' ; echo 'Remote host: $host' ; echo 'Remote directory: $builddir' ; printf 'Remote date: ' ; date $DATEFLAGS ; printf 'Remote uname: ' ; uname -a || true ; printf 'Remote gcc version: ' ; gcc --version | head -n 1 || echo ; printf 'Remote g++ version: ' ; g++ --version | head -n 1 || echo ; echo 'Configure environment: `$STRIPCOMMENTS $envfile | \ $JOINLINES`' ; echo 'Extra environment: $EXTRAENVIRONMENT' ; echo 'Configure directory: $CONFIGUREDIR' ; echo 'Configure flags: $CONFIGUREFLAGS' ; echo 'Make all targets: $ALLTARGETS' ; echo 'Make check targets: $CHECKTARGETS' ; echo 'Disk free report for $builddir/$package:' ; df $builddir | $INDENT ; echo 'Environment:' ; env | env LC_ALL=C sort | $INDENT ; echo '==============================================' ; umask $UMASK ; cd $builddir || exit 1 ; /bin/rm -rf $builddir/$package ; $PAR $parbaselocal ; test "$parbase" = "$parbaselocal" && /bin/rm -f $parbase ; cd $package/$CONFIGUREDIR || exit 1 ; test -f configure && \ chmod a+x configure && \ env `$STRIPCOMMENTS $envfile | $JOINLINES` \ $EXTRAENVIRONMENT \ nice time ./configure $CONFIGUREFLAGS ; nice time make $ALLTARGETS && nice time make $CHECKTARGETS ; echo '===============================================' ; echo 'Disk free report for $builddir/$package:' ; df $builddir | $INDENT ; printf 'Remote date: ' ; date $DATEFLAGS ; cd ; test -f $BUILDEND && . $BUILDEND || \ test -f $BUILDEND && source $BUILDEND || \ true; echo '===============================================' ; " < /dev/null > "$LOGDIR/$logfile" 2>&1 & } error(){ echo "$@" 1>&2 usage_and_exit 1 } find_file(){ #语法: # find_file file program-and-args #如果找到,返回0,如果找不到返回1 if test -r "$1" then PAR="$2" PARFILE="$1" return 0 else return 1 fi } find_package(){ #语法: # find_package package-x.y.z base=`echo "$1" | sed -e 's/[-_][.]*[0-9].*$//'` PAR= PARFILE= for srcdir in $SRCDIRS do test "$srcdir" = "." && srcdir="`pwd`" for subdir in "$base" "" do #如果此列表有改变,则更新build_one()内的包设置 find_file $srcdir/$subdir/$1.tar.gz "tar xfz" && return find_file $srcdir/$subdir/$1.tar.Z "tar xfz" && return find_file $srcdir/$subdir/$1.tar "tar xf" && return find_file $srcdir/$subdir/$1.tar.bz2 "tar xfj" && return find_file $srcdir/$subdir/$1.tar.tgz "tar xfz" && return find_file $srcdir/$subdir/$1.tar.zip "unzip -q" && return find_file $srcdir/$subdir/$1.jar "jar xf" && return done done } set_userhosts(){ #语法: # set_userhosts file(s) for u in "$@" do if test -r "$u" then ALTUSERHOSTS="$ALTUSERHOSTS $u" elif test -r "$BUILDHOME/$u" then ALTUSERHOSTS="$ALTUSERHOSTS $BUILDHOME/$u" else error "File not found: $u" fi done } usage(){ cat <<EOF Usage: $PROGRAM [ --? ] [ --all "..." ] [ --check "..." ] [ --configure "..." ] [ --environment "..." ] [ --help ] [ --logdirectory dir ] [ --on "[user@]host[:dir][,envfile] ..." ] [ --source "dir ..." ] [ --userhosts "file(s)" ] [ --version ] package(s) EOF } usage_and_exit(){ usage exit $1 } version(){ echo "$PROGRAM version $VERSION" } warning(){ echo "$@" 1>&2 EXITCODE=`expr $EXITCODE + 1 ` } ALLTARGETS= altlogdir= altsrcdirs= ALTUSERHOSTS= BUILDBEGIN=./.build/begin BUILDEND=./.build/end BUILDHOME=$HOME/.build CHECKTARGETS=check CONFIGUREDIR=. CONFIGUREFLAGS= DATEFLAGS="+%Y.%m.%d.%H.%M.%S" EXITCODE=0 EXTRAENVIRONMENT= INDENT="awk '{ print \"\t\t\t\" \$0 }'" JOINLINES="tr '\n' '\040'" LOGDIR= PROGRAM=`basename $0` SCP=scp SSH=ssh SSHFLAGS=${SSHFLAGS--x} STRIPCOMMENTS='sed -e s/#.*$//' userhosts= VERSION=1.0 #默认的初始化文件 defaultdirectories=$BUILDHOME/directories defaultuserhosts=$BUILDHOME/userhosts #要寻找包分发的位置列表,如果用户未提供个人化列表,则使用默认列表: SRCDIRS="`$STRIPCOMMENTS $defaultdirectories 2> /dev/null`" test -z "$SRCDIRS" && \ SRCDIRS=". /usr/local/src /usr/local/gnu/src $HOME/src $HOME/gnu/src /tmp /usr/tmp /var/tmp" while test $# -gt 0 do case $1 in --all | --al | --a | -all | -al | -a ) shift ALLTARGETS="$1" --cd | -cd ) shift CONFIGUREDIR="$1" --check | --chec | --che | --ch | -check | -chec | -che | -ch ) shift CHECKTARGETS="$1" --configure | --conf | --co | -configure | -conf | -co ) shift CONFIGUREFLAGS="$1" --environment | --environ | -- envir | --e | -environment | \ -environ | -envir | -e ) shift EXTRAENVIRONMENT="$1" --help | --h | '--?' | -help | -h | '-?' ) usage_and_exit 0 --logdirectory | --log | --l | -logdirectory | -log | -l ) shift altlogdir="$1" --on | --o | -on | -o ) shift userhosts="$userhosts $1" --source | --s | -source | -s ) shift altsrcdirs="$altsrcdirs $1" --userhosts | --u | -userhosts | -u ) shift set_userhosts $1 --version | --v | -version | -v ) version exit 0 -* ) error "Unrecognized option: $1" * ) break esac shift done #寻找适当的邮件客户端程序 for MAIL in /bin/mailx /usr/bin/mailx /usr/sbin/mailx /usr/ucb/mailx \ /bin/mail /usr/bin/mail do test -x $MAIL && break done test -x $MAIL || error "Cannot find mail client" #命令行来源目录优先于默认值 SRCDIRS="$altsrcdirs $SRCDIRS" if test -n "$userhosts" then test -n "$ALTUSERHOSTS" && userhosts="$userhosts `$STRIPCOMMENTS $ALTUSERHOSTS 2> /dev/null`" else test -z "$ALTUSERHOSTS" && ALTUSERHOSTS="$defaultuserhosts" userhosts="`$STRIPCOMMENTS $ALTUSERHOSTS 2> /dev/null`" fi #检查是否要执行某些操作 test -z "$userhosts" && usage_and_exit 1 for p in "$@" do find_package "$p" if test -z "$PARFILE" then warning "Cannot find package file $p" fi LOGDIR="$altlogdir" if test -z "$LOGDIR" -o ! -d "$LOGDIR" -o ! -w "$LOGDIR" then for LOGDIR in "`dirname $PARFILE`/logs/$p" \ $BUILDHOME/logs/$p /usr/tmp /var/tmp /tmp do test -d "$LOGDIR" || mkdir -p "LOGDIR" 2> /dev/null test -d "$LOGDIR" -a -w "$LOGDIR" && break done fi msg="Check build logs for $p in `hostname`:$LOGDIR" echo "$msg" echo "$msg" | $MAIL -s "$msg" $USER 2> /dev/null for u in $userhosts do build_one $u done done #将退出状态限制为一般unix实际做法 test $EXITCODE -gt 125 && EXITCODE=125 exit $EXITCODE 个人原创,转载请注明:三江小渡 |
请发表评论