shell高级编程笔记(第十一章 内部命令与内建)

第十一章 内部命令与内建

内建命令指的就是包含在Bash工具集中的命令。这主要是考虑到执行效率的问题(内建命令将比外部命令执行的更快,外部命令通常需要fork出一个单独的进程来执行)。另外一部分原因是特定的内建命令需要直接存取shell内核部分。

当一个命令或者是shell本身需要初始化(或创建)一个新的子进程来执行一个任务的时候,这种行为被称为forking。这个新产生的进程被叫做子进程,并且这个进程是从父进程中分离出来的。当子进程执行它的任务时,同时父进程也在运行

注意:当父进程取得子进程的PID的时候,父进程可以传递给子进程参数,而反过来则不行。这将产生不可思议的并且很难追踪的问题

Example 11.1 一个fork出多个自己实例的脚本

#!/bin/bash
#spawn.sh

PIDS=$(pidof sh $0)   #这个脚本不同实例的进程ID
P_array=($PIDS)   #把它们放到数组里(为了统计子进程的个数)
echo $PIDS   #显示父进程和子进程的进程ID
let "instances = ${#P_array[*]} -1"   #计算元素个数(这里-1是因为要除去脚本本身)

echo "$instances instance(s) of this script running"
echo "[Hit Ctl-C to exit]";echo
sleep 1
sh $0   #再来一次
exit 0   #脚本永远不会走到这里。因为(sh $0)它在不断的fork新的子进程

#在使用Ctl-C退出之后,所有产生的进程都会被kill掉,因为我们Ctl-C停止的是父进程,子进程会跟着消失

关于pidof命令的解释

注意:不要让这个脚本运行时间太长,它最后会占用大部分的系统资源

一般的脚本中的内建命令在执行时将不会fork出一个子进程。但是脚本中的外部或过滤命令通常会fork一个子进程

一个内建命令通常与一个系统命令同名,但是Bash在内部重新实现了这些命令。比如Bash的echo命令与/bin/echo就不尽相同,虽然它们的行为绝大多数情况下是一样的

#!/bin/bash
#
echo "This line uses the \"echo\" builtin"
/bin/echo "This line uses the /bin/echo system command"

关键字的意思就是保留字。对于shell来说关键字有特殊含义,并且用来构建shell的语法结构。比如:for,while,do和!都是关键字。与内建命令相同的是,关键字也是Bash的骨干部分,但是与内建命令不同的是,关键字自身并不是命令,而是一个比较大的命令结构的一部分。

I/O类

echo

打印一个表达式或变量

echo -e 打印转义字符
echo -n 不换行打印

echo命令可以用来作为一系列命令的管道输入

if echo "$VAR" |grep -q txt   #if [[ $VAR = *txt* ]]
#grep -q 不显示任何信息
then
  echo "$VAR contains the substring sequence \"txt\""
fi

注意:echo命令与命令替换相组合,可以用来设置一个变量

a=`echo "HELLO" | tr A-Z a-z`

参见Example 12.19,Example 12.3,Example 12.42,Example 12.43

注意:echo `command`将会删除任何有命令产生的换行符

$IFS(内部分隔符)一般都会将\n包含在它的空白字符集合中。Bash因此会根据参数中的换行来分离命令的输出。然后echo将以空格代替换行来打印这些参数

[root@localhost aaa]# ls -l
总用量 8
-rwxr-xr-x 1 root root 114 7月   7 11:17 1.sh
-rw-r--r-- 1 root root  20 7月   2 18:42 2.txt
[root@localhost aaa]# echo `ls -l`
总用量 8 -rwxr-xr-x 1 root root 114 7月 7 11:17 1.sh -rw-r--r-- 1 root root 20 7月 2 18:42 2.txt

所以,我们怎么才能在一个需要echo出来的字符串中嵌入换行呢?

#!/bin/bash
#嵌入一个换行
echo "Why doesn't this string \n split on teo lines?"   #不会换行

echo
echo $"A line of text containing
a linefeed"   #打印2行

echo
echo "This string splits
on two lines"   #打印2行

echo
echo -n $"Another line of text containing
a linefeed"   #打印2行,即使-n也没能阻止换行

echo
string=$"Yet another line of text containing
a linefeed (maybe)."
echo $string   #一行,换行变成了空格(因为分配到了变量中)

注意:这个命令是shell的一个内建命令。与/bin/echo不同,虽然行为相似

[root@localhost aaa]# type -a echo
echo is a shell builtin
echo is /bin/echo

printf

printf命令,格式化输出,是echo命令的增强。它是C语言printf()库函数的一个有限的变形,并且在语法上有些不同。

printf format-string... parameter...

这是Bash的内建版本,与/bin/printf或/usr/bin/printf命令不同。想更深入的了解,请查看printf的man页

Example 11.2 printf

#!/bin/bash
#
PI=3.14159265358979
DecimalConstant=31373
Message1="Greetings,"
Message2="Earthing."
echo

printf "Pi to 2 decimal places = %1.2f" $PI
echo
printf "Pi to 9 decimal places = %1.9f" $PI

printf "\n"   #打印一个换行

printf "Constant = \t%d\n" $DecimalConstant   #插入一个tab

printf "%s %s \n" $Message1 $Message2

echo
#模仿C函数,sprintf()
#使用一个格式化的字符串来加载一个变量
echo
Pi12=$(printf "%1.12f" $PI)
echo "Pi to 12 decimal places = $Pi12"

Msg=`printf "%s %s \n"  $Message1 $Message2`
echo $Msg;echo $Msg

exit 0

更多的关于printf的解释

使用printf的最主要的应用就是格式化错误消息

#!/bin/bash
#
E_BADDIR=65

var=/aaa

error(){
    printf "$@" >&2
    echo
   exit $E_BADDIR
}

cd $var || error $"Cant't cd to %s." "$var"

read

从stdin中读取一个变量的值,也就是与键盘交互取得变量的值。
使用-a参数可以取得数组变量(见Example 26.6)

Example 11.3 使用read变量分配

#!/bin/bash
#
echo -n "Enter the value of variable 'var1': "
read var1

echo "var1 = $var1"
echo

#一个read命令可以设置多个变量
echo -n "Enter the values of variables 'var2' and 'var3' (separated by a space or tab): "
read var2 var3
echo "var2 = $var2   var3 = $var3"
exit 0

一个不带变量参数的read命令,将把自键盘的输入存入到专用变量$REPLY中。

Example 11.4 当使用一个不带变量参数的read命令时,将会发生什么?

#!/bin/bash
#read-novar.sh
echo

#-----------------------
echo -n "Enter a value: "
read var
echo "\"var\" = "$var""
#到这里都和期望的一样
#-----------------------
echo

#----------------------------
echo -n "Enter another value: "
read   #没有变量分配给read,因此输入将分配给默认变量$REPLY
var="$REPLY"
echo "\"var\" = "$var""
#这部分代码和上边的代码等价
#----------------------------
echo
exit 0

通常情况下,在使用read命令时,输入一个\然后回车,将会阻止产生一个新行。-r选项将会让\转义

Example 11.5 read 命令的多行输入

#!/bin/bash
#
echo
echo "输入一个以\\结尾的字符串,然后按<Enter>"
echo "然后,输入第二个字符串,然后再次按<Enter>"
read var1   #\将会阻止产生新行
echo "var1 = $var1"

echo;echo

echo "输入另一个以\\结尾的字符串,然后按<Enter>"
read -r var2   #-r选项将会让\转义
echo "var2 = $var2"
#第一个<Enter>就会结束var2变量的录入
echo
exit 0

read命令有些有趣的选项,这些选项允许打印出一个提示符,然后在不输入的情况下,就可以读入你的按键字符

#!/bin/bash
#不敲回车,读取一个按键字符
read -s -n1 -p "Hit a key" keypress
echo;echo "Keypress was "\"$keypress\"""

#-s 选项意味着不打印输出
#-n N 选项意味着接受N个字符的输入
#-p 选项意味着在读取输入之前打印出后边的提示符
#使用这些选项是有技巧的,因为你需要使用正确的循环来使用它们。

read的-n选项也可以检测方向键和一些控制按键

Example 11.6 检测方向键

#!/bin/bash
#arrow-detect.sh:检测方向键和一些非打印字符的按键

#---------------------
#按键产生的字符编码
arrowup='\[A'
arrowdown='\[B'
arrowrt='\[C'
arrowletf='\[D'
insert='\[2'
delete='\[3'
#---------------------

SUCCESS=0
OTHER=65

echo -n "Press a key... "
read -n3 key
echo -n "$key" | grep "$arrowup"
if [ $? -eq $SUCCESS ];then
  echo "Up-arrow key pressed"
  exit $SUCCESS
fi

echo -n "$key" | grep "$arrowdown"
if [ "$?" -eq $SUCCESS ];then
  echo "Down-arrow key pressed." 
  exit $SUCCESS
fi

echo -n "$key" | grep "$arrowrt"
if [ "$?" -eq $SUCCESS ];then
  echo "Right-arrow key pressed."
  exit $SUCCESS
fi

echo -n "$key" | grep "$arrowleft"
if [ "$?" -eq $SUCCESS ];then
  echo "Left-arrow key pressed."
  exit $SUCCESS
fi

echo -n "$key" | grep "$insert"
if [ "$?" -eq $SUCCESS ];then
  echo "\"Insert\" key pressed."
  exit $SUCCESS
fi

echo -n "$key" | grep "$delete"
if [ "$?" -eq $SUCCESS ];then
  echo "\"Delete\" key pressed."
  exit $SUCCESS
fi


echo "按了其他一些键"

exit $OTHER 

练习:通过使用case结构来简化这个脚本

#!/bin/bash
#arrow-detect2.sh:检测方向键和一些非打印字符的按键

#---------------------
#按键产生的字符编码
arrowup='\[A'
arrowdown='\[B'
arrowrt='\[C'
arrowletf='\[D'
#---------------------

SUCCESS=0
OTHER=65

echo  "Press a key... "
read -s -n3 key
case $key in
`echo -n "$key" | grep "$arrowup"`) echo "Up-arrow key pressed";;
`echo -n "$key" | grep "$arrowdown"`) echo "Down-arrow key pressed." ;;
`echo -n "$key" | grep "$arrowrt"`) echo "Right-arrow key pressed." ;;
`echo -n "$key" | grep "$arrowletf"`) echo "Left-arrow key pressed.";;
*) echo "按了其他一些键"
esac
exit $OTHER

注意:对read命令来说,-n选项将不会检测ENTER(新行)键

read命令的-t选项允许时间输入(见Example 9.4)

read命令也可以从重定向的文件中读入变量的值。如果文件中的内容超过一行,那么只有第一行被分配到这个变量中。如果read命令有超过一个参数,那么每个变量都会从文件中取得以定义的空白分隔的字符串作为变量的值。小心!

Example 11.7 通过文件重定向来使用read

#!/bin/bash
#
read var1 < data-file
echo "var1 = $var1"
#var1将会把data-file的第一行的全部内容都作为它的值

read var2 var3 < data-file
echo "var2 = $var2   var3 = $var3"
#注意:这里read命令将会产生一种不直观的行为
#1)重新从文件的开头开始读入变量
#2)每个变量都设置成了以空白分割的字符串,而不是之前的以整行的内容作为变量的值
#3)最后一个变量将会取得第一行剩余的全部部分(不管是否以空白分割)
#4)如果需要赋值的变量个数比文件中第一行空白分割的字符串的个数多,那么这些变量将被赋空值
#----------例子(便于理解)-------------
#[root@localhost aaa]# cat data-file 
#123456789 qwert !@#
#
#[root@localhost aaa]# sh 1.sh 
#var1 = 123456789 qwert !@#   #read var1 < data-file
#var2 = 123456789   var3 = qwert !@#   #read var2 var3 < data-file
#------------------------------------

#如何用循环来解决上边的问题
while read line
do
  echo "$line"
done < data-file

#使用$IFS(内部域分隔变量)来将每行的输入单独的放到read中,如果你不想使用默认空白的话
echo "List of all users: "
OIFS=$IFS;IFS=:
while read name passwd uid gid fullname ignore
do
  echo "$name ($fullname)"
done < /etc/passwd
IFS=$OIFS   #恢复原始的$IFS

#在循环内部设置$IFS变量,而不用把原始的$IFS保存到临时变量中
echo "----------------------"
echo "List of all users: "
while IFS=: read name passwd uid gid fullname ignore
do
  echo "$name ($fullname)"
done < /etc/passwd
echo
echo "$IFS still $IFS"
exit 0

注意:管道输出到一个read命令中,使用管道echo输出到read会失败。然而使用管道cat输出看起来能够正常运行

cat file1 file2 |
while read line
do
  echo $line
done  

Example 11.8 管道输出到read中的问题

#!/bin/bash
#readpipe.sh
last="(null)"
cat $0 |
while read line
do
  echo "{$line}"
  last=$line
done
printf "\nAll done,last:$last\n"
exit 0
#-----------下面是脚本的输出-----------
#{#!/bin/bash}
#{#readpipe.sh}
#{last="(null)"}
#{cat $0 |}
#{while read line}
#{do}
#{echo "{$line}"}
#{last=$line}
#{done}
#{printf "nAll done,last:$lastn"}
#{exit 0}
#
#All done,last:(null)
#
#变量(last)是设置在子shell中的而没有设置在外边

在许多Linux发行版上,gendiff脚本通常在/usr/bin下,将find的输出使用管道传递一个while循环中

find $1 \(-name "*$2" -o -name ".*$2"\) -print |
while read f
do
...

文件系统类

cd

修改目录命令,在脚本中用的最多的时候就是,命令需要在指定目录下运行时,需要用cd修改当前工作目录

(cd /source/directory && tar cf - .) | (cd /dest/directory && tar xpvf -)

-P(physical)选项的作用是忽略符号链接

cd - 将把工作目录改为$OLDPWD,就是之前的工作目录

注意:当我们用两个/来作为cd命令的参数时,结果却出乎我们的意料

[root@localhost aaa]# cd //
[root@localhost //]# pwd
//

输出应该是/。无论在命令行下还是在脚本中,这都是个问题。

pwd

打印当前的工作目录。这将给用户(或脚本)的当前的工作目录(见Example 11.9)。使用这个命令的结果和从内建变量$PWD中读取的值是相同的。

pushd,popd,dirs

这几个命令可以使得工作目录书签化,就是可以按顺序向前或向后移动工作目录。
压栈的动作可以保存工作目录列表。选项可以允许对目录栈作不同的操作。

pushd dir-name 把路径dir-name压入目录栈,同时修改当前目录到dir-name。

popd 将目录栈中最上边的目录弹出,同时修改当前目录到弹出来的那么目录

dirs 列出所有目录栈的内容(与$DIRSTACK变量相比较)。一个成功的pushd或者popd将会自动的调用dirs命令。

Example 11.9 修改当前的工作目录

#!/bin/bash
#
dir1=/usr/local
dir2=/var/spool

pushd $dir1   #将会自动运行一个dirs(把目录栈的内容列到stdout上)
echo "Now in directory `pwd`."

pushd $dir2
echo "Now in directory `pwd`."

echo "DIRSTACK数组的顶部条目是$DIRSTACK"
popd
echo "Now back in directory `pwd`."

popd
echo "Now back in original working directory `pwd`."
exit 0
#--------下面是脚本输出的内容
#/usr/local /opt/aaa   :pushd命令的输出内容
#Now in directory /usr/local.
#/var/spool /usr/local /opt/aaa   :pushd命令的输出内容
#Now in directory /var/spool.
#DIRSTACK数组的顶部条目是/var/spool
#/usr/local /opt/aaa   :popd命令的输出内容
#Now back in directory /usr/local.
#/opt/aaa   :popd命令的输出内容
#Now back in original working directory /opt/aaa.

变量类

let

let命令将执行变量的算术操作。在许多情况下,它被看作是复杂的expr版本的一个简化版

Example 11.10 用let命令来作算术操作

#!/bin/bash
#
echo
let a=11   #与a=11相同
let a=a+5   #等价于let "a = a + 5"(双引号和空格更具可读性)
echo "11 + 5 = $a"   #16

#左移几位,代表乘2的几次方,这里左移3为就表示a*2**3
#相反右移就是除以2的几次方。
let "a <<= 3"   #等价于let "a = a << 3"
echo "\"\$a\" (=16) left-shifted 3 places = $a"   #128

let "a /= 4"   #等价于let "a = a / 4"
echo "128 / 4 = $a"   #32

let "a -= 5"   #等价于let "a = a - 5"
echo "32 - 5 = $a"   #27

let "a *= 10"   #等价于let "a = a * 10"
echo "27 * 10 = $a"   #270

let "a %= 8"   #等价于let "a = a % 8"
echo "270 modulo 8 = $a (270 / 8 = 33,remainder $a)"   #6
echo
exit 0

eval

eval arg1 [arg2] ... [argN]

将表达式中的参数或者表达式列表组合起来,并且评估它们。包含在表达式中的任何变量都将被扩展。结果将会被转化到命令中。这对于从命令行或脚本中产生代码是很有作用的

[root@localhost aaa]# process=xterm
[root@localhost aaa]# show_process="eval ps ax |grep $process"
[root@localhost aaa]# $show_process
 6502 pts/1    S+     0:00 grep xterm

Example 11.11 显示eval命令的效果

#!/bin/bash
#
y=`eval ls -l`
echo $y
echo
echo "$y"   #用""将变量引起来,换行符就不会被空格替换了
echo;echo

y=`eval df`
echo $y

#当没有换行符出现时,对于使用awk这样的工具来说,可能分析输出的结果更容易一些。
echo;echo

#现在来看一下怎么用eval命令来扩展一个变量
for i in 1 2 3 4 5
do
  eval value=$i   #value=$i将具有同样的效果。eval并不非得在这里使用
  #一个缺乏特殊含义的变量将被评估为自身,也就是说,这个变量除了能够被扩展成自身所表示的字符以外,不能扩展成任何其他的含义
  echo $value
done
echo
echo "-------------"
echo

for i in ls df
do
  value=eval $i   #value=$i在这里就有了本质的区别
  #eval将会评估命令ls和df,ls和df就具有特殊含义,因为它们被解释成命令,而不是字符串本身。
  echo $value
done
exit 0  

Example 11.12 强制登出(应该是需要Ubuntu系统)

#!/bin/bash
#结束ppp进程来强制登出log-off
#脚本应该以根用户的身份来运行

killppp="eval kill -9 `pa ax | awk '/ppp/{print $1}'`"
$killppp   #这个变量现在成为了一个命令

chmod 666 /dev/ttyS3   #恢复读写权限,否则什么?
#因为在ppp上执行一个SIGKILL将会修改串口的权限,我们把权限恢复到之前的状态

rm /var/lock/LCK...ttyS3   #删除串口锁文件。为什么?

exit 0

Example 11.13 另一个rot13的版本(tr字符替换这里没整明白)

#!/bin/bash
#使用eval的一个rot13的版本(rot13就是把26个字母从中间分为两瓣,各13个)

setvar_rot_13(){
    local varname=$1 varvalue=$2
    #local设置局部变量
    eval $varname='$(echo "$varvalue" | tr a-z n-za-m)'
}

setvar_rot_13 var "foobar"
echo $var   #sbbone
setvar_rot_13 var "$var"
echo $var   #foobar
exit 0

罗利·温斯顿捐献了下边的脚本,关于eval命令
Example 11.14 在perl脚本中使用eval命令来强制变量替换(又一个不明白的脚本)

#!/bin/bash
#在Perl脚本中"test.pl":
...
my $WEBROOT = <WEBROOT_PATH>;
...

#要强制变量替换,请尝试:
$export WEBROOT_PATH=/usr/local/webroot
$sed 's/<WEBROOT_PATH>/$WEBROOT_PATH/' <test.pl> out

#但这只是
my $WEBROOT = $WEBROOT_PATH;

#然而
$export WEBROOT_PATH=/usr/local/webroot
$eval sed 's%\<WEBROOT_PATH\>%$WEBROOT_PATH%' <test.pl> out

eval命令是有风险的,如果有更合适的方法来实现功能的话,尽量要避免使用它。

eval命令将执行命令的内容,如果命令中有rm -rf*这种东西,可能就不是你想要的了。如果在一个不熟悉的人编写的脚本中使用eval命令将是危险的。

set

set命令用来修改内部脚本变量的值。一个作用就是触发选项标志位来帮助决定脚本行为。另一个作用就是以一个命令的结果(set `command`)来重新设置的位置参数。脚本将会从命令的输出中重新分析出位置参数

Example 11.15 使用set来改变脚本的位置参数

#!/bin/bash
#set-test
#使用3个命令行参数来调用这个脚本,比如:"./set-test one teo three"

echo
echo "set \`uname -a\`之前的位置参数 :"
echo "Command-line argument #1 = $1"
echo "Command-line argument #2 = $2"
echo "Command-line argument #3 = $3"

set `uname -a`   #把`uname -a`的命令输出设置为新的位置参数

echo $_   #打印出的就是输出的最后一个单词(查看uname -a的输出)

echo "set \`uname -a\` 之后的位置参数 :"
echo "Field #1 of 'uname -a' = $1"
echo "Field #1 of 'uname -a' = $2"
echo "Field #1 of 'uname -a' = $3"
echo "----------"
echo $_
echo
exit 0
#下面为脚本输出
#[root@localhost aaa]# uname -a
#Linux localhost 2.6.32-573.el6.x86_64 #1 SMP Wed Jul 1 18:23:37 EDT 2015 x86_64 x86_64 x86_64 GNU/Linux
#[root@localhost aaa]# sh 1.sh one two three
#
#set `uname -a` 之前的位置参数  :
#Command-line argument #1 = one
#Command-line argument #2 = two
#Command-line argument #3 = three
#GNU/Linux
#set `uname -a` 之后的位置参数  :
#Field #1 of 'uname -a' = Linux
#Field #1 of 'uname -a' = localhost
#Field #1 of 'uname -a' = 2.6.32-573.el6.x86_64
#----------
#----------
#

不使用任何选项或参数来调用set命令的话,将会列出所有的环境变量和其它所有的已经初始化过的命令

[root@localhost aaa]# set
BASH=/bin/bash
...
IFS=$' \t\n'
JAVA_HOME=/usr/local/jdk
JRE_HOME=/usr/local/jdk/jre
...
PS1='[\u@\h \W]\$ '
PS2='> '
PS4='+ '
...
colors=/etc/DIR_COLORS

使用参数--来调用set命令的话,将会明确的分配位置参数。如果--选项后边没有跟变量名的话,那么结果就使所有位置参数都被unset了。

Example 11.16 重新分配位置参数

#!/bin/bash
#
variable="one two three four five"
set -- $variable   #将位置参数的内容设置为变量$variable的内容

first_param=$1
second_param=$2
shift;shift
remaining_params="$*"
echo
echo "first parameter = $first_param"   #one
echo "second parameter = $second_param"   #two
echo "remaining parameters = $remaining_params"   #three four five
echo;echo

#再来一次
set --   #如果没有指定变量,那么将会unset所有位置参数

first_param=$1
second_param=$2
echo "first parameter = $first_param"   #空
echo "second parameter = $second_param"   #空
echo
exit 0

见Example 10.2和Example 12.51

unset

unset命令用来删除一个shell变量,效果就是把这个变量设为null。
注意:这个命令对位置参数无效。

[root@localhost aaa]# unset PATH
[root@localhost aaa]# echo $PATH

Example 11.17 unset一个变量

#!/bin/bash
#unset.sh

variable=hello
echo "variable = $variable"

unset variable
echo "(unset) variable = $variable"
exit 0

export

export命令将会使得被export的变量在运行的脚本的所有的子进程中都可用。不幸的是,没有办法将变量export到父进程中。
关于export命令的一个重要的使用就是用在启动文件中,启动文件是用来初始化并且设置环境变量。让用户进程可以存取环境变量。

Example 使用export命令传递一个变量到一个内嵌awk的脚本中

#!/bin/bash
#
ARGS=2
E_WRONGARGS=65

if [ $# -ne "$ARGS" ];then
  echo "Usage:`basename $0` filename column-number"
  exit $E_WRONGARGS
fi

filename=$1
column_number=$2

export column_number   #将列号通过export出来,这样后边的进程就可用了
awkscript='{total += $ENVIRON["column_number"]} END {print total}'
#$ENVIRON["column_number"]声明column_number为环境变量,意思就是:
#+当$2=2时,$ENVIRON["column_number"]就等于$2,表示第二列

awk "$awkscript" "$filename"
exit 0

以下举例便于理解awk传递外部参数($ENVIRON[“column_number”])

#第一种:使用awk -v选项(指定在任何输入被读入前定义参数 #指定执行BEGIN前的变量赋值)
#!/bin/bash
#
ARGS=2
E_WRONGARGS=65

if [ $# -ne "$ARGS" ];then
  echo "Usage:`basename $0` filename column-number"
  exit $E_WRONGARGS
fi

filename=$1
column_number=$2

awkscript='{total += $column_number} END {print total}'

awk -v column_number=$2 "$awkscript" "$filename"
exit 0


#第二种:转义一个$符号:\$${var}
#!/bin/bash
#
ARGS=2
E_WRONGARGS=65

if [ $# -ne "$ARGS" ];then
  echo "Usage:`basename $0` filename column-number"
  exit $E_WRONGARGS
fi

filename=$1
column_number=$2

awkscript="{total += \$${column_number}} END {print total}"

awk "$awkscript" "$filename"
exit 0

注意:可以在一个操作中同时赋值和export变量,如:export var1=xxx。
然而像格雷格·科劳宁指出的,在某些情况下使用上边的这种形式将与先设置变量,然后export变量效果不同

declare,typeset

declare和typeset命令被用来指定或限制变量的属性

readonly

与declare -r作用相同,设置变量的只读属性,也可以认为是设置常量。设置了这种属性之后,如果还要修改它,那么你将得到一个错误消息。这种情况与C语言中的const常量类型的情况是相同的

getopts

可以说这是分析传递到脚本的命令行参数的最强工具。这个命令与getopt外部命令,和C语言中的库函数getopt的作用是相同的。它允许传递和连接多个选项到脚本中,并能分配多个参数到脚本中。

getopts结构使用两个隐含变量。$OPTIND是参数指针(选项索引),和$OPTARG(选项参数)(可选的)可以在选项后边附加一个参数。在声明标签中,选项名后边的冒号用来提示这个选项名已经分配了一个参数。

getopts结构通常都组成一组放在一个while循环中,循环过程中每次处理一个选项和参数,然后增加隐含变量$OPTING的值,再进行下一次的处理

注意:
1 通过命令行传递到脚本中的参数前边必须加上一个减号(-)。这是一个前缀,这样getopts命令将会认为这个参数是一个选项。事实上,getopts不会处理不带"-“前缀的参数,如果第一个参数就没有”-",那么将结束选项的处理。

2 使用getopts的while循环模板还是与标准的while循环模板有些不同。没有标准while循环中的[]判断条件

3 getopts结构将会取代getopt外部命令。

Example 11.19 使用getopts命令来读取传递给脚本的选项/参数

#!/bin/bash
#练习getopts和OPTIND
#这里我们将学习getopts如何处理脚本的命令行参数。
#参数被作为选项(标志)被解析,并且分配参数

#试一下通过如下方法来调用这个脚本
#'scriptname -mn'
#'scriptname -oq aBc1er3'   #aBcer3可以是任意字符
#'scriptname -qXXX -r'
#'scriptname -qr'   :意外的结果,r将被看成是选项q的参数
#'scriptname -q -r'   :意外的结果,同上
#'scriptname -mnop -mnop'   :意外的结果

NO_ARGS=0
E_OPTERROR=65

if [ $# -eq $NO_ARGS ];then
  echo "Usage:`basename $0` options (-mnopqrs)"
  exit $E_OPTERROR
fi

while getopts ":mnopq:rs" Option
do
  case $Option in
  m) echo "Scenario #1: option -m- [OPTIND=${OPTIND}]";;
  n|o) echo "Scenario #2: option -$Option- [OPTIND=${OPTIND}]";;
  p) echo "Scenario #3: option -p- [OPTIND=${OPTIND}]";;
  q) echo "Scenario #4: option -q- with argument \"$OPTARG\" [OPTIND=${OPTIND}]";;
  r|s) echo "Scenario #5: option -$Option-";;
  *) echo "Unimplemented option chosen.";;
  esac
done

shift $(($OPTIND - 1))

exit 0

没有理解清楚的可以看我的getopts笔记

脚本行为

source,.(点命令)

这个命令在命令行上执行的时候,将会执行一个脚本。在一个文件内一个source file-name将会加载file-name文件。source一个文件(或点命令)将会在脚本中引入代码,并附加到脚本中(与C语言中的#include指令的效果相同)。最终的结果就像是在使用sourced行上插入了相应文件的内容。这在多个脚本需要引用相同的数据或函数库是非常有用。

Example 11.20 Including一个数据文件
提前准备一个要加载的文件

[root@localhost shell]# cat testfile 
variable1=1
variable2=2
variable3=3
variable4=4
message1=MESSAGE1
#!/bin/bash
#test.sh
. testfile   #加载一个数据文件。与source testfile效果相同,但是更具可移植性。
#文件testfile必须存在于当前工作目录,因为这个文件是使用basename来引用的

#现在引用这个数据文件中的一些数据
echo "variable1 (from data-file) = $variable1"
echo "variable3 (from data-file) = $variable3"

let "sum = $variable2 + $variable4"
echo "Sum of variable2+variable4 (from data-file) = $sum"
echo "message1 (from data-file) is \"$message1\""

#注意这里使用sh script.sh和./script.sh执行脚本的方法会找不到testfile文件
#我的理解是(不一定准确):sh script.sh和./script.sh执行脚本的方法会在新的子shell中运行,读取不到testfile文件

如果引入的文件本身就是一个可执行的脚本的话,那么它将运行起来,当它return的时候,控制权又重新回到了引用它的脚本中。一个用source引入的脚本可以使用return命令来达到这个目的

也可以向需要source的脚本中传递参数。这些参数在source脚本中被认为是位置参数

source $filename $arg1 arg2

你甚至可以在脚本文件中source脚本文件自身,虽然看不出什么实际的应用价值

Example 11.21 一个没什么用的source自身的脚本

#!/bin/bash
#self-source.sh:一个脚本递归的source自身

MAXPASSCNT=10   #source自身的最大数量

echo -n "$pass_count  "   #在第一次运行的时候,这句只不过echo出2个空格,因为$oass_count还没有被初始化
#这里不能将pass_count初始化为0,因为在后边source自身的时候又会将pass_count初始化为0,导致出现死循环。
let "pass_count += 1"

while [ "$pass_count" -le "$MAXPASSCNT" ]
do
  . $0   #脚本source自身,而不是调用自己
         #./$0(应该能够正常递归)不能在这正常运行,为什么?(建议不要去试)
done
#这里发生的动作并不是真正的递归,因为脚本成功的展开了自己,换句话说,在每次循环的过程中,在每个source行上都产生了新的代码
#当然,脚本会把每个新source进来的文件"#!"行都解释为注释,而不会把它看成是一个新的脚本。
echo
exit 0

exit

绝对的停止一个脚本的运行。exit命令可以随便找一个整数变量作为退出脚本返回shell时的退出码。使用exit 0对于退出一个简单脚本来说是种好习惯,表明运行成功。

注意:如果不带参数的使用exit来退出,那么退出码将是脚本中最后一个命令的退出码。等价于exit $?

exec

这个shell内建命令将使用一个特定的命令来取代当前进程。一般的当shell遇到一个命令,它会fork off一个子进程来真正的运行命令。使用exec内建命令,shell就不会fork了,并且命令的执行将会替换掉当前shell。因此,当我们在脚本中使用它时,命令实行完毕,它就会强制退出脚本。

Example 11.22 exec的效果

#!/bin/bash
#
exec echo "Exiting \"$0\"."   #脚本将在此退出

echo "This echo will never echo"
exit 99

Example 11.23 一个exec自身的脚本

#!/bin/bash
#self-exec.sh
echo
echo "This line appears ONCE in the script, yet it keeps echoing"
echo "The PID of this instance of the script is still $$"   #这句用来确认没有产生新的子进程

echo "----------------Hit Ctl-C to exit-----------------"
sleep 1
exec $0   #产生了本脚本的另一个实例,并且这个实例代替了之前的那个

echo "This line will never echo"   #这条永远不会echo
exit 0

exec命令还能用于重新分配文件描述符。比如:

exec < xxx-file将会用xxx-file来代替stdin。

注意:find命令的-exec选项与shell内建的exec命令是不同的。

shopt

这个命令允许shell在空闲时修改shell选项(见Example 24.1和Example 24.2)。它经常出现在启动脚本中,但是在一般脚本中也可以用。

shopt -s cdspell
#使用cd命令时,允许产生少量的拼写错误
[root@localhost shell]# shopt -s cdspell
[root@localhost shell]# cd /hpme
/home
[root@localhost home]# pwd
/home

caller

将caller命令放到函数中,将会在stdout上打印出函数调用者的信息

#!/bin/bash
#
function1 (){
    caller 0   #在function1 ()内部
}

function1

caller 0   #没效果,因为这个命令不在函数中。
#下面是脚本执行输出
#[root@localhost shell]# sh test.sh 
#7 main test.sh
#7:函数调用者所在行号;main:从脚本的main部分调用的;test.sh:脚本的名字

caller命令也可以返回在一个脚本中被source的另一个脚本的信息。像函数一样,这是一个"子例程调用",你会发现这个命令在调试的时候特别有用。

命令类

ture

一个返回成功(就是返回0)退出码的命令,但是除此之外什么事也不做。

#一个死循环
while true   #这里的true可以用:替换
do
  operation1
  ...
done  

flase

一个返回失败(非0)退出码的命令,但是除此之外什么事也不做

#测试false
if false
then
  echo "false evaluates \"true\""
else
  echo "false evaluates \"false\""
fi    #返回false

while "false"   #空循环
do
  operation1
  ...
done  

type[cmd]

与which扩展命令很像,type cmd将给出cmd的完整路径。与which命令不同的是,type命令是Bash内建命令。一个很有用的选项是-a选项,使用这个选项可以鉴别所识别的参数时关键字还是内建命令,也可以定位同名的系统命令。

[root@localhost ~]# type [
[ is a shell builtin
[root@localhost ~]# type -a [
[ is a shell builtin
[ is /usr/bin/[

hash[cmds]

在shell的hash表中,记录指定命令的路径名,所以在shell或脚本中调用这个命令的话,shell或脚本将不需要再在$PATH中重新搜索这个命令了。
如果不带参数的调用hash命令,它将列出所有已被hash的命令。-r会重新设置hash表

bind

bind内建命令用来显示或修改readline的键绑定

help

获得shell内建命令的一个小的使用总结。这与whatis命令比较像,但是help市内建命令。

[root@localhost ~]# help exit
exit: exit [n]
    Exit the shell.
    
    Exits the shell with a status of N.  If N is omitted, the exit status
    is that of the last command executed.

11.1 作业控制命令

下边的作业控制命令需要一个"作业标识符"作为参数。见这章结尾的表。

jobs

在后台列出所有正在运行的作业,给出作业号。

注意:进程和作业的概念太容易混淆了。特定的内建命令,比如kill,disown和wait既可以接受一个作业号作为参数,也可以接受一个进程号作为参数。但是fg,bg和jobs命令只能接受作业号作为参数。

[root@localhost ~]# sleep 100 &
[1] 16403
[root@localhost ~]# jobs 
[1]+  Running                 sleep 100 &

注意:"1"是作业号(作业号被当前shell所维护),而"16403"是进程号(进程号系统维护)。为了kill掉作业/进程,使用kill %1或者kill 16403这两个命令都可以

disown

从shell的当前作业表中删除作业

fg,bg

fg命令可以把一个在后台运行的作用放到前台来运行。
bg命令将会重新启动一个挂起的作业,并且在后台运行它。
如果使用fg或bg命令的时候没指定作业号,那么默认将对当前正在运行的作业做操作。

wait

停止脚本的运行,直到后台运行的所有作业都结束为止,或者直接指定作业号或进程号为选项的作业结束为止。

你可以使用wait命令来防止在后台作业没完成(这会产生一个孤儿进程)之前退出脚本。

Example 11.24 在继续处理之前,等待一个进程的结束

#!/bin/bash
#
ROOT_UID=0
E_NOTROOT=65
E_NOPARAMS=66

if [ "$UID" -ne "$ROOT_UID" ];then
  echo "Must be root to run this script"
  exit $E_NOTROOT
fi

if [ -z "$1" ];then
  echo "Usage:`basename $0` find-string"
  exit $E_NOPARAMS
fi

echo "Updating 'locate' database..."
echo "This may take a while"
updatedb /usr &   #必须使用root身份来运行

wait   #将不会继续向下运行,除非updatedb命令执行完成

locate $1
#如果没有wait命令的话,而且在比较糟的情况下,脚本可能在updatedb命令还在运行的时候退出
#+这将会导致updatedb成为一个孤儿进程
exit 0

关于updatedb和locate的简单介绍

当然,wait也可以接受一个作业标识符作为参数,比如,wait %1或wait $PPID。见"作业标识符表"

注意:在一个脚本中使用一个后台运行的命令(使用&)可能会是这个脚本挂起,直到敲回车,挂起才会被恢复。看起来只有这个命令的结果需要输出到stdout的时候才会发生这种现象。这会是一个很烦人的现象

#!/bin/bash
#1.sh
ls -l &
echo "Done"
#下面是运行结果的输出,并且不敲回车的话,不会回到命令行
#[root@localhost aaa]# ./1.sh 
#Done
#[root@localhost aaa]# 总用量 12
#-rwxr-xr-x 1 root root 34 7月  13 11:28 1.sh
#-rw-r--r-- 1 root root 18 7月   9 18:41 2.txt
#-rw-r--r-- 1 root root 20 7月   8 09:56 data-file

看起来在这个后台运行命令的后边放上一个wait命令可能会解决这个问题

#!/bin/bash
#1.sh
ls -l &
echo "Done"
wait
#下面是运行结果的输出,会直接跳到命令行
[root@localhost aaa]# ./1.sh 
Done
总用量 12
-rwxr-xr-x 1 root root 39 7月  13 11:31 1.sh
-rw-r--r-- 1 root root 18 7月   9 18:41 2.txt
-rw-r--r-- 1 root root 20 7月   8 09:56 data-file

如果把这个后台运行命令的输出重定向到文件中或者是/dev/null中,也能解决这个问题

suspend

这个命令的效果与Control-Z很相像,但是它挂起的是这个shell(这个shell的父进程应该在合适的时候重新恢复它)

logout

退出一个登陆的shell,也可以指定一个退出码

times

给出执行命令所占的时间,使用如下形式输出:

0m0.020s 0m0.020s

这是一种很有限的能力,因为这不常出现在shell脚本中

kill

通过发送一个适当的结束信号,来强制结束一个进程(见Example 13.6)

Example 11.25 一个结束自身的脚本

#!/bin/bash
#
kill $$   #"$$"就是脚本的PID

echo "这里不会输出"
exit 0
#shell将会发送一个"Terminated"消息到stdout

#在脚本结束自身进程之后,它返回的退出码是143,为什么?
#143 = 128 + 15(结束信号)

注意:kill -l将列出所有信号。kill -9是"必杀"命令,这个命令将会结束那些顽固的不想被kill掉的进程。有时候kill -15也可以干这个活。
一个僵尸进程不能被登录的用户kill掉,因为你不能杀掉一些已经死了的东西。但是init进程迟早会把它清除干净。
僵尸进程就是子进程已经结束掉,而父进程却没有kill掉这个子进程,那么这个子进程就是僵尸进程

command

command命令会禁用别名和函数的查找。它只查找内部命令以及搜索路径中找到脚本或可执行程序。(只在要执行的命令与函数或者别名同名时使用,因为函数的优先级比内见命令的优先级高)

注意:bash执行命令的优先级
别名
关键字
函数
内置命令
脚本或可执行程序($PATH)

注意:当运行的命令或函数与内建命令同名时,由于内建命令比外部命令的优先级高,而函数比内建命令优先级高,所以bash将总会执行优先级比较高的命令。这样你就没有选择的余地了。所以bash提供了3个命令来让你有选择的机会,分别是command,builtin,enable。

builtin

在builtin后边的命令将只调用内建命令。暂时的禁用同名函数或是同名扩展命令

enable

这个命令禁用或者恢复内建命令。

enable -n kill   #禁用kill内建命令,所以当我们调用kill时,使用的将是/bin/kill外部命令

enable -a kill   #恢复相应的内建命令,如果不带参数的话会恢复所有内建命令

enable -f filename   #将会从适当的编辑过的目标文件中以共享(DLL)的形式来加载一个内建命令

autoload

这是从ksh的autoloader命令移植过来的。一个带有autoload声明的函数,在它第一次被调用的时候才会被加载。这样做会节省系统资源

注意:autoload命令并不是bash安装时的核心命令的一部分。这个命令需要使用enable -f来加载

Table 11.1 作业标识符

记法含义
%N作业号[N]
%S以字符串S开头的被(命令行)调用的作业
$?S包含字符串S的被(命令行)调用的作业
%%当前作业(前台最后结束的作业,或后台最后启动的作业)
%+当前作业(前台最后结束的作业,或后台最后启动的作业)
%-最后的作业
$!最后的后台进程
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值