高级Bash脚本编程指南(18):内部命令与内建命令(一)

高级Bash脚本编程指南(18):内部命令与内建命令(一)

成于坚持,败于止步

内建命令指的就是包含在Bash工具包中的命令, 从字面意思上看就是built in.这主要是考虑到执行效率的问题:内建命令将比外部命令执行的更快, 一部分原因是因为外部命令通常都需要fork出一个单独的进程来执行,另一部分原因是特定的内建命令需要直接访问shell的内核部分.

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

注意: 当父进程获得了子进程的进程ID时, 父进程可以给子进程传递参数,然而反过来却不行. 这将会产生不可思议的并且很难追踪的问题.

看一个fork出多个自身实例的脚本

 

#!/bin/bash

PIDS=$(pidof bash $0)  # 这个脚本不同实例的进程ID.
P_array=( $PIDS )    # 把它们放到数组里(为什么?).
echo $PIDS           # 显示父进程和子进程的进程ID.
let "instances = ${#P_array[*]} - 2"  # 计算元素个数, 至少为1.
                                      # 为什么减1?
echo "$instances instance(s) of this script running."
echo "[Hit Ctl-C to exit.]"; echo


sleep 1              # 等一下.
bash $0                # 再来一次, Sam.

exit 0               # 没必要; 脚本永远不会运行到这里.
                     # 为什么运行不到这里?

#  在使用Ctl-C退出之后,
#+ 是否所有产生出来的进程都会被kill掉?
#  如果是这样的话, 为什么?

# 注意:
# ----
# 小心, 不要让这个脚本运行太长时间.
# 它最后会吃掉你绝大多数的系统资源.

结果也许会让你吃惊,不过不重要,了解这种fork机制就足够了

 

 

root@ubuntu:~/resource/shell-study/0614-2013# ./test1.sh 
28641 20791 18642
1 instance(s) of this script running.
[Hit Ctl-C to exit.]

28644 28641 20791 18642
2 instance(s) of this script running.
[Hit Ctl-C to exit.]

28647 28644 28641 20791 18642
3 instance(s) of this script running.
[Hit Ctl-C to exit.]

28650 28647 28644 28641 20791 18642
4 instance(s) of this script running.
[Hit Ctl-C to exit.]

28653 28650 28647 28644 28641 20791 18642
5 instance(s) of this script running.
[Hit Ctl-C to exit.]

^C
root@ubuntu:~/resource/shell-study/0614-2013# 

常情况下, 脚本中的Bash内建命令在运行的时候是不会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."

结果:

root@ubuntu:~/resource/shell-study/0614-2013# ./test2.sh 
This line uses the "echo" builtin.
This line uses the /bin/echo system command.
root@ubuntu:~/resource/shell-study/0614-2013# 

 

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

echo

echo命令需要-e参数来打印转义字符. 通常情况下, 每个echo命令都会在终端上新起一行, 但是-n参数会阻止新起一行.echo命令可以作为输入, 通过管道传递到一系列命令中去.

if echo "$VAR" | grep -q txt   # if [[ $VAR = *txt* ]]
then
  echo "$VAR contains the substring sequence \"txt\""
fi

echo命令可以与命令替换组合起来, 这样可以用来设置一个变量.

 

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

小心echo `command`将会删除任何由command所产生的换行符.$IFS (内部域分隔符) 一搬都会将 \n (换行符) 包含在它的空白字符集合中.Bash因此会根据参数中的换行来分离command的输出, 然后echo.最后echo将以空格代替换行来输出这些参数.

 

root@ubuntu:~/resource/shell-study/0614-2013# ls -l 
total 8
-rwxr-xr-x 1 root root 860 2013-06-14 03:16 test1.sh
-rwxr-xr-x 1 root root 114 2013-06-14 03:20 test2.sh
root@ubuntu:~/resource/shell-study/0614-2013# echo `ls -l`
total 8 -rwxr-xr-x 1 root root 860 2013-06-14 03:16 test1.sh -rwxr-xr-x 1 root root 114 2013-06-14 03:20 test2.sh
root@ubuntu:~/resource/shell-study/0614-2013# 

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

 

 

#!/bin/bash

# 嵌入一个换行?
echo "Why doesn't this string \n split on two lines?"
# 上边这句的\n将被打印出来. 达不到换行的目的.

# 让我们再试试其他方法.

echo
	
echo $"A line of text containing
a linefeed."
# 打印出两个独立的行(嵌入换行成功了).
# 但是, 是否必须有"$"作为变量前缀?

echo

echo "This string splits
on two lines."   # 并不是非有"$"不可.

echo
echo "---------------"
echo

echo -n $"Another line of text containing
a linefeed."
# 打印出两个独立的行(嵌入换行成功了).
# 即使使用了-n选项, 也没能阻止换行. (译者注: -n 阻止了第2个换行)


echo
echo "---------------"
echo

# 然而, 下边的代码就没能像期望的那样运行.
# 为什么失败? 提示: 因为分配到了变量.
string1=$"Yet another line of text containing
a linefeed (maybe)."

echo $string1
# 换行变成了空格.only one line
exit 0

结果:

root@ubuntu:~/resource/shell-study/0614-2013# ./test3.sh 
Why doesn't this string \n split on two lines?

A line of text containing
a linefeed.

This string splits
on two lines.

---------------

Another line of text containing
a linefeed.
---------------

Yet another line of text containing a linefeed (maybe).
root@ubuntu:~/resource/shell-study/0614-2013# 

 

所以这里echo命令是shell的一个内建命令, 与/bin/echo不同, 虽然行为相似,同样你可以用下面语句方法得到验证:

root@ubuntu:~/resource/shell-study/0614-2013# type -a echo
echo is a shell builtin
echo is /bin/echo
root@ubuntu:~/resource/shell-study/0614-2013# 

printf

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

printf format-string... parameter...

这是Bash的内建版本, 与/bin/printf或者/usr/bin/printf命令不同. 如果想更深入的了解, 请察看printf(系统命令)的man页.老版本的Bash可能不支持printf.同样你可以用以下方法查究:

root@ubuntu:~/resource/shell-study/0614-2013# type -a printf
printf is a shell builtin
printf is /usr/bin/printf
root@ubuntu:~/resource/shell-study/0614-2013# 

接下来看一个使用printf的例子

 

#!/bin/bash

PI=3.14159265358979
DecimalConstant=31373
Message1="Greetings,"
Message2="Earthling."

echo
printf "Pi to 2 decimal places = %1.2f" $PI
echo
printf "Pi to 9 decimal places = %1.9f" $PI  # 都能够正确的结束.

printf "\n"                                  # 打印一个换行,
                                             # 等价于 'echo' . . .

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

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

exit 0

结果:

root@ubuntu:~/resource/shell-study/0614-2013# ./test4.sh 

Pi to 2 decimal places = 3.14
Pi to 9 decimal places = 3.141592654
Constant = 	31373
Greetings, Earthling. 


Pi to 12 decimal places = 3.141592653590
Greetings, Earthling.

root@ubuntu:~/resource/shell-study/0614-2013#

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

 

 

#!/bin/bash

E_BADDIR=65

var=nonexistent_directory

error()
{
  printf "$@" >&2
  # 格式化传递进来的位置参数, 并把它们送到stderr.
  echo
  exit $E_BADDIR
}

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

结果:

root@ubuntu:~/resource/shell-study/0614-2013# ./test5.sh 
./test5.sh: line 15: cd: nonexistent_directory: No such file or directory
Can't cd to nonexistent_directory.
root@ubuntu:~/resource/shell-study/0614-2013# 

这里有个$@,也许你看这有点面熟,不过就是不知道是什么意思,这里多花点时间来说明一下$*,$@,$#这三兄弟到底有什么区别,看实例吧,这是最好的办法

 

 

#!/bin/bash 
my_fun() { 
    echo "$#" 
	echo "$@"
	echo "$*"
} 
echo "result:"
my_fun "1" "2" "3" 
echo 
my_fun "1 2" "3"

结果:

root@ubuntu:~/resource/shell-study/0614-2013# ./test6.sh 
result:
3
1 2 3
1 2 3

2
1 2 3
1 2 3
root@ubuntu:~/resource/shell-study/0614-2013# 

其中:

 

$*表示所有这些参数都被双引号引住。若一个脚本接收两个参数,$*等于$1$2

$@表示所有这些参数都分别被双引号引住,若一个脚本接收到两个参数,$@等价于$1$2

$#表示提供给脚本的参数号

对于这里第一条语句:

$*为"1 2 3"(一起被引号包住)

$@为"1" "2" "3"(分别被包住)

$#为3(参数数量)

所以上面error传入参数为"Can't cd to %s."  和 “nonexistent_directory”,所以printf “$@” 就相当于printf "Can't cd to %s." “nonexistent_directory”

read

从stdin中"读取"一个变量的值, 也就是, 和键盘进行交互, 来取得变量的值. 使用-a参数可以read数组变量

使用read来进行变量分配的实例:

 

#!/bin/bash

echo -n "Enter the value of variable 'var1': "
# -n 选项, 阻止换行.

read var1
# 注意: 在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"
# 如果你只输入了一个值, 那么其他的变量还是处于未设置状态(null).

exit 0

结果:

root@ubuntu:~/resource/shell-study/0614-2013# ./test7.sh 
Enter the value of variable 'var1': df sdf df
var1 = df sdf df

Enter the values of variables 'var2' and 'var3'\未换行
 (separated by a space or tab): dsf dfgd dfa sfdsa
var2 = dsf      var3 = dfgd dfa sfdsa
root@ubuntu:~/resource/shell-study/0614-2013#

很简单的实例,没有必要多做说明,不过, 一个不带变量参数的read命令, 将会把来自键盘的输入存入到专用变量$REPLY中,这一点倒是值得我们关注的

 

 

#!/bin/bash

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

结果:

root@ubuntu:~/resource/shell-study/0614-2013# ./test8.sh 

Enter a value: df
"var" = df

Enter another value: fg fg
"var" = fg fg

root@ubuntu:~/resource/shell-study/0614-2013# 

一般的, 当输入给read时, 输入一个\, 然后回车,将会阻止产生一个新行. -r选项将会让 \ 转义.

 

 

#!/bin/bash

echo

echo "Enter a string terminated by a \\, then press <ENTER>."
echo "Then, enter a second string, and again press <ENTER>."
read var1     # 当 read $var1 时, "\" 将会阻止产生新行.
              #     first line \
              #     second line

echo "var1 = $var1"
#     var1 = first line second line

#  对于每个以 "\" 结尾的行,
#+ 你都会看到一个下一行的提示符, 让你继续向var1输入内容.

echo; echo

echo "Enter another string terminated by a \\ , then press <ENTER>."
read -r var2  # -r 选项会让 "\" 转义.
              #     first line \

echo "var2 = $var2"
#     var2 = first line \

# 第一个 <ENTER> 就会结束var2变量的录入.

echo

exit 0

结果:

root@ubuntu:~/resource/shell-study/0614-2013# chmod +x test9.sh 
root@ubuntu:~/resource/shell-study/0614-2013# ./test9.sh 

Enter a string terminated by a \, then press <ENTER>.
Then, enter a second string, and again press <ENTER>.
first line \
second line
var1 = first line second line


Enter another string terminated by a \ , then press <ENTER>.
first line \
var2 = first line \

root@ubuntu:~/resource/shell-study/0614-2013# 

read命令有些有趣的选项, 这些选项允许打印出一个提示符, 然后在不输入ENTER的情况下, 可以读入你所按下的字符的内容.不过这个功能也许并不怎么常用,知道就好

 

 

#!/bin/bash
# 不敲回车, 读取一个按键字符.

read -s -n5 -p "Hit a key " keypress
echo; echo "Keypress was "\"$keypress\""."

# -s 选项意味着不打印输入.
# -n N 选项意味着只接受N个字符的输入.
# -p 选项意味着在读取输入之前打印出后边的提示符.

# 使用这些选项是有技巧的, 因为你需要用正确的顺序来使用它们.
exit 0

结果:

root@ubuntu:~/resource/shell-study/0614-2013# ./test10.sh 
Hit a key 
Keypress was "HELLO".
root@ubuntu:~/resource/shell-study/0614-2013# 

read命令的-n选项还有一个特殊的用途,可以检测方向键, 和一些控制按键,当然上面的实例已经说了,-n还可以指定接收的字符数,就像上面做到的那样

 

下面看一个-n选项作为检测方向键,或者控制按键的实例

 

#!/bin/bash

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

SUCCESS=0
OTHER=65

echo -n "Press a key...  "
# 如果不是上边列表所列出的按键,
# 可能还是需要按回车. (译者注: 因为一般按键是一个字符)
read -n3 key                      # 读取3个字符.

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 " Some other key pressed."

exit $OTHER

结果:

root@ubuntu:~/resource/shell-study/0614-2013# ./test11.sh 
Press a key...  DFD Some other key pressed.
root@ubuntu:~/resource/shell-study/0614-2013# ./test11.sh 
Up-arrow key pressed.
root@ubuntu:~/resource/shell-study/0614-2013# 
root@ubuntu:~/resource/shell-study/0614-2013# ./test11.sh 
Press a key...  ^[[B
Down-arrow key pressed.
root@ubuntu:~/resource/shell-study/0614-2013# 
root@ubuntu:~/resource/shell-study/0614-2013# ./test11.sh 
Press a key...  ^[[D
Left-arrow key pressed.
root@ubuntu:~/resource/shell-study/0614-2013# 

 

对于read命令来说, -n选项不会检测ENTER(新行)键.read命令的-t选项允许时间输入

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

看一个实例,通过文件重定向来使用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) 如果需要赋值的变量个数比文件中第一行以空白分割的字符串个数还多的话,
#    那么这些变量将会被赋空值.

echo "------------------------------------------------"

# 如何用循环来解决上边所提到的问题:
while read line
do
  echo "$line"
done <data-file
# 感谢, Heiner Steven 指出了这点.

echo "------------------------------------------------"

# 使用$IFS(内部域分隔变量)来将每行的输入单独的放到"read"中,
# 前提是如果你不想使用默认空白的话.

echo "List of all users:"
OIFS=$IFS; IFS=:       # /etc/passwd 使用 ":" 作为域分隔符.
while read name passwd uid gid fullname ignore
do
  echo "$name ($fullname)"
done </etc/passwd   # I/O 重定向.
IFS=$OIFS              # 恢复原始的$IFS.
# 这段代码也是Heiner Steven编写的.



#  在循环内部设置$IFS变量,
#+ 而不用把原始的$IFS
#+ 保存到临时变量中.
#  感谢, Dim Segebart, 指出了这点.
echo "------------------------------------------------"
echo "List of all users:"

while IFS=: read name passwd uid gid fullname ignore
do
  echo "$name ($fullname)"
done </etc/passwd   # I/O 重定向.

echo
echo "\$IFS still $IFS"

exit 0

查看结果:

root@ubuntu:~/resource/shell-study/0614-2013# chmod +x test12.sh 
root@ubuntu:~/resource/shell-study/0614-2013# ls
data-file  test11.sh  test1.sh  test3.sh  test5.sh  test7.sh  test9.sh
test10.sh  test12.sh  test2.sh  test4.sh  test6.sh  test8.sh
root@ubuntu:~/resource/shell-study/0614-2013# ./test12.sh 
var1 = 123 456 678 45 23 5678
var2 = 123   var3 = 456 678 45 23 5678
------------------------------------------------
123 456 678 45 23 5678
sdf sdg sdf ghds
------------------------------------------------
List of all users:
root (root)
daemon (daemon)
bin (bin)
sys (sys)
sync (sync)
games (games)
man (man)
lp (lp)
mail (mail)
news (news)
uucp (uucp)
proxy (proxy)
www-data (www-data)
backup (backup)
list (Mailing List Manager)
irc (ircd)
gnats (Gnats Bug-Reporting System (admin))
nobody (nobody)
libuuid ()
syslog ()
messagebus ()
avahi-autoipd (Avahi autoip daemon,,,)
avahi (Avahi mDNS daemon,,,)
couchdb (CouchDB Administrator,,,)
speech-dispatcher (Speech Dispatcher,,,)
usbmux (usbmux daemon,,,)
haldaemon (Hardware abstraction layer,,,)
kernoops (Kernel Oops Tracking Daemon,,,)
pulse (PulseAudio daemon,,,)
rtkit (RealtimeKit,,,)
saned ()
hplip (HPLIP system user,,,)
gdm (Gnome Display Manager)
hai-tao (ubuntu 10.04,,,)
statd ()
------------------------------------------------
List of all users:
root (root)
daemon (daemon)
bin (bin)
sys (sys)
sync (sync)
games (games)
man (man)
lp (lp)
mail (mail)
news (news)
uucp (uucp)
proxy (proxy)
www-data (www-data)
backup (backup)
list (Mailing List Manager)
irc (ircd)
gnats (Gnats Bug-Reporting System (admin))
nobody (nobody)
libuuid ()
syslog ()
messagebus ()
avahi-autoipd (Avahi autoip daemon,,,)
avahi (Avahi mDNS daemon,,,)
couchdb (CouchDB Administrator,,,)
speech-dispatcher (Speech Dispatcher,,,)
usbmux (usbmux daemon,,,)
haldaemon (Hardware abstraction layer,,,)
kernoops (Kernel Oops Tracking Daemon,,,)
pulse (PulseAudio daemon,,,)
rtkit (RealtimeKit,,,)
saned ()
hplip (HPLIP system user,,,)
gdm (Gnome Display Manager)
hai-tao (ubuntu 10.04,,,)
statd ()

$IFS still  	

root@ubuntu:~/resource/shell-study/0614-2013# 

管道输出到read命令中, 使用管道echo输出来设置变量将会失败.然而, 使用管道cat输出看起来能够正常运行.

 

#!/bin/bash

cat data-file |
while read line
do
	echo $line
done

exit 0

结果:

root@ubuntu:~/resource/shell-study/0614-2013# ./test13.sh 
123 456 678 45 23 5678
sdf sdg sdf ghds
root@ubuntu:~/resource/shell-study/0614-2013# 

不过我在使用echo测试时也是可以的

 

 

#!/bin/bash

echo "dfdfsdf";echo "1234" |
while read line
do
	echo $line
done

exit 0

结果:

root@ubuntu:~/resource/shell-study/0614-2013# ./test13.sh 
dfdfsdf
1234
root@ubuntu:~/resource/shell-study/0614-2013# 

 

管道输出到read中的问题

#!/bin/sh
# readpipe.sh
# 这个例子是由Bjon Eriksson所编写的.

last="(null)"
cat $0 |
while read line
do
    echo "{$line}"
    last=$line
done
printf "\nAll done, last:$last\n"

exit 0  # 代码结束.
        # 下边是脚本的(部分)输出.
        # 'echo'出了多余的大括号.

结果:

root@ubuntu:~/resource/shell-study/0614-2013# chmod +x test14.sh 
root@ubuntu:~/resource/shell-study/0614-2013# ./test14.sh 
{#!/bin/sh}
{# readpipe.sh}
{# 这个例子是由Bjon Eriksson所编写的.}
{}
{last="(null)"}
{cat $0 |}
{while read line}
{do}
{echo "{$line}"}
{last=$line}
{done}
{printf "nAll done, last:$lastn"}
{}
{exit 0  # 代码结束.}
{# 下边是脚本的(部分)输出.}
{# 'echo'出了多余的大括号.}

All done, last:(null)
root@ubuntu:~/resource/shell-study/0614-2013# 

待续。。。。。

posted @ 2013-06-15 15:40  爱生活,爱编程  阅读(334)  评论(0编辑  收藏  举报