11.3 Shell的内建命令
所谓Shell内建命令,就是由Bash自身提供的命令,而不是文件系统中的某个可执行文件。例如,用于进入或者切换目录的cd命令,虽然我们一直在使用它,但如果不加以注意很难意识到它与普通命令的性质是不一样的:该命令并不是某个外部文件,只要在Shell中你就一定可以运行这个命令。打个比方,就像使用语言互相沟通是人类与生俱来的能力,但是我们有时却需要使用移动电话来进行远距离的沟通,那么人类本身具备的语言能力就是“内建”的能力,而移动电话却是一个外部的工具。
- #cd
命令并不是一个可执行文件
[root@localhost ~]# which cd
usrbin/which: no cd in (usrkerberos/sbin:usrkerberos/bin:usr
local/sbin:usrlocal/bin:/sbin:/bin:usrsbin:usrbin:/root/bin)
#more
命令是一个可执行文件,文件位置为binmore
[root@localhost ~]# which more
binmore
还记得系统变量$PATH吗?在第8章中介绍过,$PATH变量包含的目录中几乎聚集了系统中绝大多数的可执行命令。通常来说,内建命令会比外部命令执行得更快,执行外部命令时不但会触发磁盘I/O,还需要fork出一个单独的进程来执行,执行完成后再退出。而执行内建命令相当于调用当前Shell进程的一个函数。
Shell的内建命令众多,在3.2.25版本的Bash中有几十个,如下所示:
- bash, :, ., [, alias, bg, bind, break, builtin, cd, command,
compgen, complete, continue, declare, dirs, disown, echo, enable,
eval, exec, exit, export, fc, fg, getopts, hash, help, history,
jobs, kill, let, local, logout, popd, printf, pushd, pwd, read,
readonly, return, set, shift, shopt, source, suspend, test,
times, trap, type, typeset, ulimit, umask, unalias, unset, wait
本章将列出经常使用的内建命令并做简单的描述,在后面的章节中也会根据实际需要对部分命令做更详细的描述。
1.如何确定内建命令:type
不要试图用脑子记住所有的命令,这不可能也不需要。判断一个命令是不是内建命令只需要借助于命令type即可,如下所示:
- #cd
命令是个内建命令
[root@localhost ~]# type cd
cd is a Shell builtin
#ifconfig
命令不是内建命令,而是一个外部文件
[root@localhost ~]# type ifconfig
ifconfig is sbinifconfig
2.执行程序:“.”(点号)
点号用于执行某个脚本,甚至脚本没有可执行权限也可以运行。有时候在测试运行某个脚本时可能并不想为此修改脚本权限,这时候就可以使用“.”来运行脚本。以之前的HelloWorld.sh为例,如果没有运行权限的话,用“./”执行就会有报错,但是若在其前面使用点号来执行就不会报错,如下所示:
#
如果脚本没有可执行权限,则会报权限错误
[root@localhost ~]# ./HelloWorld.sh
-bash: ./HelloWorld.sh: Permission denied
#
使用点号执行没有加执行权限的脚本可以正常运行
[root@localhost ~]# . ./HelloWorld.sh
Hello World
与点号类似,source命令也可读取并在当前环境中执行脚本,同时还可返回脚本中最后一个命令的返回状态;如果没有返回值则返回0,代表执行成功;如果未找到指定的脚本则返回false。
- [root@localhost ~]# source HelloWorld.sh
Hello World
3.别名:alias
alias可用于创建命令的别名,若直接输入该命令且不带任何参数,则列出当前用户使用了别名的命令。现在你应该能理解类似ll这样的命令为什么与ls-l的效果是一样的吧。
- [root@localhost ~]# alias
alias cp='cp -i'
alias l.='ls -d .* --color=tty'
alias ll='ls -l --color=tty'
alias ls='ls --color=tty'
alias mv='mv -i'
alias rm='rm -i'
alias which='alias | usrbin/which --tty-only --read-alias --show-dot --show-tilde'
使用alias可以自定义别名,比如说一般的关机命令是shutdown-h now,写起来比较长,这时可以重新定义一个关机命令,以后就方便多了。使用alias定义的别名命令也是支持Tab键补全的,如下所示:
- [root@localhost ~]# alias myShutdown='shutdown -h now'
注意,这样定义alias只能在当前Shell环境中有效,换句话说,重新登录后这个别名就消失了。为了确保永远生效,可以将该条目写到用户家目录中的.bashrc文件中,如下所示:
- [root@localhost ~]# cat .bashrc
# .bashrc
# User specific aliases and functions
alias rm='rm -i'
alias cp='cp -i'
alias mv='mv -i'
#
自定义关机命令的别名
myShutdown='shutdown -h now'
# Source global definitions
if [ -f etcbashrc ]; then
. etcbashrc
fi
4.删除别名:unalias
该命令用于删除当前Shell环境中的别名。有两种使用方法,第一种用法是在命令后跟上某个命令的别名,用于删除指定的别名。第二种用法是在命令后接-a参数,删除当前Shell环境中所有的别名。同样,这两种方法都是在当前Shell环境中生效的。
删除ll
别名
[root@e-bai ~]# unalias ll
再运行该命令时,报找不到该命令的错误。说明该别名被删除了
[root@e-bai ~]# ll
-bash: ll: command not found
5.任务前后台切换:bg、fg、jobs
该命令用于将任务放置后台运行,一般会与Ctrl+z、fg、&符号联合使用。典型的使用场景是运行比较耗时的任务。比如打包某个占用较大空间的目录,若在前台执行,在任务完成前将会一直占用当前的终端,而导致无法执行其他任务,此时就应该将这类任务放置后台。
- [root@localhost ~]# tar -zcf usr.tgz /usr
tar: Removing leading `/' from member names #
开始打包
#
占用前台导致无法运行其他任务,此处用Ctrl+z
组合键暂停前台任务
[1]+ Stopped tar -zcf usr.tgz /usr
[root@localhost ~]# jobs #
查看暂停的任务,刚刚的tar
任务编号为1
[1]+ Stopped tar -zcf usr.tgz /usr
[root@localhost ~]# bg 1 #
把tar
任务放置后台继续运行
[1]+ tar -zcf usr.tgz /usr & #tar
任务继续运行了
[root@localhost ~]# fg 1 #
使用fg
把后台任务调至前台运行
tar -zcf usr.tgz /usr
#
如果预知某个任务耗时很久,可以一开始就将命令放入后台运行
[root@localhost ~]# tar -zcf usr.tgz /usr &
6.改变目录:cd
改变当前工作目录。如果不加参数,默认会进入当前用户的家目录。
7.声明变量:declare、typeset
这两个命令都是用来声明变量的,作用完全相同。很多语法严谨的语言(比如C语言)对变量的声明都是有严格要求的:变量的使用原则是必须在使用前声明、声明时必须说明变量类型,而Shell脚本中对变量声明的要求并不高,因为Shell弱化了变量的类概念,所以Shell又被称为弱类型编程语言,声明变量时并不需要指明类型。不过,若使用declare命令,可以用-i参数声明整型变量,如下所示:
#
声明变量i_num01
,其值为1
[root@localhost ~]# i_num01=1
#
声明变量f_num01
,其值为3.14
[root@localhost ~]# f_num01=3.14
#
声明变量str01
,其值为HelloWorld
[root@localhost ~]# str01="HelloWorld"
#
使用declare
声明整型变量i_num02
,其值为1
[root@localhost ~]# declare -i i_num02=1
使用-r声明变量为只读,如下所示:
- [root@localhost ~]# declare -r readonly=100 #
声明只读变量
[root@localhost ~]# readonly=200 #
试图改变变量值
-bash: readonly: readonly variable #
报错,提示尝试修改只读变量
使用-a声明变量,如下所示:
- [root@localhost ~]# declare -a arr='([0]="a" [1]="b" [2]="c")'
[root@localhost ~]# echo ${arr[0]}
a
[root@localhost ~]# echo ${arr[1]}
b
[root@localhost ~]# echo ${arr[2]}
c
使用-F、-f显示脚本中定义的函数和函数体,如下所示:
#
创建脚本fun.sh
,内容如下
[root@localhost ~]# cat fun.sh
!binbash
func_1()
{
echo "Funciotn 1"
}
func_2()
{
echo "Function 2"
}
echo "declare -F:"
declare -F
echo
echo "declare -f:"
declare -f
#
运行该脚本的输出效果如下
[root@localhost ~]# bash fun.sh
declare -F:
declare -f func_1
declare -f func_2
declare -f:
func_1 ()
{
echo "Funciotn 1"
}
func_2 ()
{
echo "Function 2"
}
8.打印字符:echo
echo用于打印字符,典型用法是使用echo命令并跟上使用双引号括起的内容(即需要打印的内容),该命令会打印出引号中的内容,并在最后默认加上换行符。使用-n参数可以不打印换行符。
- [root@localhost ~]# echo "Hello World"
Hello World
[root@localhost ~]# #
命令提示符出现在新的一行
[root@localhost ~]# echo -n "Hello World"
Hello World[root@localhost ~]# #
命令提示符在同一行
默认情况下,echo命令会隐藏-e参数(禁止解释打印反斜杠转义的字符)。比如“\n”代表新的一行,如果尝试使用echo输出新的一行,在不加参数的情况下只会将“\n”当作普通的字符,若要打印转义字符,则需要通过使用-e参数来允许。
- #echo
默认禁止打印反斜杠转义的字符
[root@localhost ~]# echo "\n"
\n #
只是把“\n
”当做普通的字符
#
为了允许打印转义字符,需要使用-e
参数
#
下面的输出有两行,第一行是输出的新行,第二行是默认的换行符
[root@localhost ~]# echo -e "\n"
[root@localhost ~]#
9.跳出循环:break
从一个循环(for、while、until或者select)中退出。break后可以跟一个数字n,代表跳出n层循环,n必须大于1,如果n比当前循环层数还要大,则跳出所有循环。下面的脚本演示了使用break和break 2的区别(运行前请将对应的注释符去掉)。
#
创建演示脚本break_01.sh
[root@localhost ~]# cat break_01.sh
!binbash
for I in A B C D
do
echo -n "$I:"
for J in seq 10
do
if [ $J -eq 5 ]; then
break
break 2
fi
echo -n " $J"
done
echo
done
echo
#
判断当J
值为5
时,break
的输出结果(循环运行了4
次)
[root@localhost ~]# bash break_01.sh
A: 1 2 3 4
B: 1 2 3 4
C: 1 2 3 4
D: 1 2 3 4
#
判断当J
值为5
时,break 2
的输出结果(仅运行了1
次循环便终止了)
[root@localhost ~]# bash break_01.sh
A: 1 2 3 4
10.循环控制:continue
停止当前循环,并执行外层循环(for、while、until或者select)的下一次循环。continue后可以跟上一个数字n,代表跳至外部第n层循环。n必须大于1,如果n比当前循环层数还要大,将跳至最外层的循环。下面的脚本演示了使用continue和continue 2的区别(运行前请将对应的注释符去掉)。
#
创建演示脚本continue_01.sh
[root@localhost ~]# cat continue_01.sh
!binbash
for I in A B C D
do
echo -n "$I:"
for J in seq 10
do
if [ $J -eq 5 ]; then
continue
continue 2
fi
echo -n " $J"
done
echo
done
echo
#
判断当J
值为5
时,continue
的输出结果
[root@localhost ~]# bash continue_01.sh
A: 1 2 3 4 6 7 8 9 10
B: 1 2 3 4 6 7 8 9 10
C: 1 2 3 4 6 7 8 9 10
D: 1 2 3 4 6 7 8 9 10
#
判断当J
值为5
时,continue 2
的输出结果
[root@localhost ~]# bash continue_01.sh
A: 1 2 3 4B: 1 2 3 4C: 1 2 3 4D: 1 2 3 4
11.将所跟的参数作为Shell的输入,并执行产生的命令:eval
- #eval
用法例一:将字符串解析成命令执行
#
定义cmd
为一个字符串,该字符串为“ls -l etcpasswd
”
[root@localhost ~]# cmd="ls -l etcpasswd"
#
如果使用eval
,则会将之前的字符串解析为命令并执行
[root@localhost ~]# eval $cmd
-rw-r--r-- 1 root root 1638 Mar 3 00:43 etcpasswd
#eval
用法例二:程序运行中根据某个变量确定实际的变量名
[root@localhost ~]# name1=john #
定义变量name1
[root@localhost ~]# name2=wang #
定义变量name2
[root@localhost ~]# num=1 #
使用该变量确定真实的变量名name$num
[root@localhost ~]# eval echo "$"name$num
John
#eval
用法例三:将某个变量的值当做另一个变量名并给其赋值
[root@localhost ~]# name1=john
[root@localhost ~]# name2=wang
[root@localhost ~]# eval $name1="$name2" #
等价于john="wang"
[root@localhost ~]# echo $john
wang
12.执行命令来取代当前的Shell:exec
内建命令exec并不启动新的Shell,而是用要被执行的命令替换当前的Shell进程,并且将老进程的环境清理掉,而且exec命令后的其他命令将不再执行。假设在一个Shell里面执行了exec echo''Hello''命令,在正常地输出一个“Hello”后Shell会退出,因为这个Shell进程已被替换为仅仅执行echo命令的一个进程,执行结束自然也就退出了。如图11-5所示,命令执行完成后,连接状态是一个红色的断开符。
图11-5 exec执行后退出Shell
想要避免出现这种情况,一般将exec命令放到一个Shell脚本里面,由主脚本调用这个脚本,主脚本在调用子脚本执行时,当执行到exec后,该子脚本进程就被替换成相应的exec的命令。注意source命令或者点号,不会为脚本新建Shell,而只是将脚本包含的命令在当前Shell执行。exec典型的用法是与find联合使用,用find找出符合匹配的文件,然后交给exec处理,如下所示:
#
列出系统中所有以.conf
结尾的文件
[root@localhost ~]# find / -name "*.conf" -exec ls -l {} \;
#
删除系统中所有临时文件
find / -name "*.tmp" -exec rm -f {} \;
13.退出Shell:exit
在当前Shell中直接运行该命令的后果是退出本次登录。在Shell脚本中使用exit代表退出当前脚本。该命令可以接受的参数是一个状态值n,代表退出的状态,下面的脚本什么都不会做,一旦运行就以状态值为5退出。如果不指定,默认状态值是0。
#
脚本exit.sh
的内容
[root@localhost ~]# cat exit.sh
!binbash
exit 5
#
[root@localhost ~]# bash exit.sh #
运行该脚本
[root@localhost ~]# #
看起来什么都没有发生,实际上并不是这样
[root@localhost ~]# echo $? #
使用$?
可以取出之前命令的退出状态值
5 #
这就是脚本中定义的退出状态值
14.使变量能被子Shell识别:export
用户登录到系统后,系统将启动一个Shell,用户可以在该Shell中声明变量,也可以创建并运行Shell脚本,通常,如果说登录时的Shell是父Shell,则在该Shell中运行的Shell是该Shell的子Shell。当子Shell运行完毕后,将返回执行该脚本的父Shell。从这种意义上来说,用户可以有许多Shell,每个Shell都是由父Shell创建的。
在父Shell中创建变量时,这些变量并不会被其子Shell进程所知,也就是说变量默认情况下是“私有”的,或称“局部变量”。使用export命令可以将变量导出,使得该Shell的子Shell都可以使用该变量,这个过程称为变量输出。
为演示export的作用,请先在当前Shell中创建文件export.sh,其内容如下所示:
#
创建export.sh
脚本
[root@localhost ~]# cat export.sh
!binbash
echo $var
#
直接执行这个脚本,由于变量var
在脚本中并没有定义,所以其值是空,脚本输出确实什么也没有
[root@localhost ~]# bash export.sh
[root@localhost ~] #
无任何输出
#
现在在当前Shell
中创建变量var
,并赋值为100
,并尝试输出该值
[root@localhost ~]# var=100
[root@localhost ~]# echo $var
100 #
确实变量var
被赋值了
#
由于这里的var
和子Shell
中的var
都是局部变量,所以如果现在再运行子Shell
,依然会打印出空值
[root@localhost ~]# bash export.sh
[root@localhost ~] #
无任何输出
#
但是如果在定义变量的时候使用了export
就不一样了,子Shell
可以读取该变量
[root@localhost ~]# export var=100
[root@localhost ~]# bash export.sh
100 #
这里读取到了父Shell
的变量var
值
要说明的是,即便子Shell确实读取到了父Shell中变量var的值,也只是值的传递,如果在子Shell中尝试改变var的值,改变的只是var在子Shell中的值,父Shell中的该值并不会因此受到影响,你可以认为父Shell和子Shell都各自拥有一个叫var的变量,它们恰巧名字相同而已。
15.发送信号给指定PID或进程:kill
Linux是一个多任务的操作系统,系统上经常同时运行着多个进程。我们需要知道如何控制这些进程。Linux操作系统包括3种不同类型的进程,第一种是交互进程,这是由一个Shell启动的进程,既可以在前台运行,也可以在后台运行;第二种是批处理进程,与终端没有联系,是一个进程序列;第三种是监控进程,也称系统守护进程,它们往往在系统启动时启动,并保持在后台运行。
kill命令用来终止进程,其工作的原理是向系统的内核发送一个系统操作信号和某个程序的进程标识号,然后系统内核就可以对进程标识号指定的进程进行操作。比如用ps命令可以看到许多进程,有时需要使用kill中止某些进程来提高系统资源。该命令可以向某个PID或进程发送信号,具体用法在前面的第7章中已做详细的描述,此处不赘述。
- [root@localhost ~]# kill [ -s signal | -p ] [ -a ] [ -- ] pid ...
[root@localhost ~]# kill -l [ signal ]
#-s:
指定要发送的信号,信号可以是信号名或是信号数值
#-p:
只打印出进程的PID
,并不真的发送信号
#-l:
指定信号的名称列表
#pid:
进程的ID
号
#signal:
信号
16.整数运算:let
let是Shell内建的整数运算命令。以下是let的具体用法:
- #let
使用范例
let I=2+2 --->I=4
let J=5-2 --->J=3
let K=2*5 --->K=10
let L=15/7 --->L=2
(整数计算,所以计算结果也是整数)
let M=15%7 --->M=1
(求余)
let N=2**3 --->N=8
(代表2
的3
次方)
#let
也支持类C
的计算方式
let i++
(i
自增1
)
let i--
(i
自减1
)
let i+=10
(i
值等于i
增加10
)
let i-=10
(i
值等于i
减少10
)
let i*=10
(i
值等于i
乘以10
)
let i/=10
(i
值等于i
除以10
)
let i%=10
(i
值等于i
模10
)
17.显示当前工作目录:pwd
pwd命令会打印当前工作目录的绝对路径名。如果使用-P选项,打印出的路径名中不会包含符号连接。如果使用了-L选项,打印出的路径中可以包含符号连接。如下所示:
#
运行pwd
可以显示当前所在目录的绝对路径
[root@localhost ~]# pwd
/root
#
变量OLDPWD
记录了上一次工作目录,如果你从登录系统之后一直没有改变工作目录,则OLDPWD
为空
[root@localhost ~]# echo $OLDPWD
#
这里为空
#
变量PWD
记录了当前的工作目录,它与pwd
命令运行结果是一致的
[root@localhost ~]# echo $PWD
/root
#
进入/tmp
目录
[root@localhost ~]# cd /tmp
#
这时OLDPWD
记录了上一次工作目录/root
,所以此处输出为/root
[root@localhost ~]# echo $OLDPWD
/root
#
下面将演示-P
和-L
参数
#
首先确定varmail
目录其实是一个软连接
[root@localhost ~]# ls -l varmail
lrwxrwxrwx 1 root root 10 Nov 27 17:54 varmail -> spool/mail
#
进入varmail
目录
[root@localhost ~]# cd varmail
#
使用-L
参数和不加参数的pwd
命令输出结果是一样的
[root@localhost mail]# pwd -L
varmail
#
使用-P
参数则显示出真实的路径,而不是软链接
[root@localhost mail]# pwd -P
varspool/mail
18.声明局部变量:local
该命令用于在脚本中声明局部变量,典型的用法是用于函数体内,其作用域也在声明该变量的函数体中。如果试图在函数外使用local声明变量,则会提示错误。
19.从标准输入读取一行到变量:read
有时候我们开发的脚本必须具有交互性,也就是在运行过程中依赖人工输入才能继续。比如说,一箱啤酒有12瓶,买n箱啤酒一共多少瓶?由于此处n是一个变量,如果脚本能在运行中询问“你想买多少箱啤酒”,然后计算出一共有多少瓶啤酒的话,脚本会显得更为友好。
#
根据输入的箱数计算一共有多少瓶啤酒
[root@localhost ~]# cat read.sh
!binbash
declare N
echo "12 bottles of beer in a box"
echo -n "How many box do you want:"
read N
echo "$((N*12)) bottle in total"
#
运行效果
[root@localhost ~]# bash read.sh
12 bottles of beer in a box
How many box do you want:10 #
这里输入数字
120 bottle in total
从上面的例子中可以看到,我们通过read从键盘输入中读取到变量N的值使用了两句代码,实际上read可以使用-p参数代替。
#
原先用于读取变量N
使用的两句代码
echo -n "How many box do you want:"
read N
read
使用-p
参数简化后的代码
read -p "How many box do you want:" N
如果不指定变量,read命令会将读取到的值放入环境变量REPLY中。另外要记住,read是按行读取的,用回车符区分一行,你可以输入任意文字,它们都会保存在变量REPLY中。
- [root@localhost ~]# read #
输入read
命令后回车
Hello world #
输入Hello world
[root@localhost ~]# echo $REPLY #
打印环境变量REPLY
Hello world #
结果与之前的输入一致
20.定义函数返回值:return
典型的用于函数中,常见用法是return n,其中n是一个指定的数字,使函数以指定值退出。如果没有指定n值,则返回状态是函数体中执行的最后一个命令的退出状态。
- [root@localhost ~]# cat return.sh
#!binbash
#
定义了一个函数fun_01
,该函数简单地返回1
function fun_01 {
return 1
}
#
调用该函数
fun_01
#
查看之前函数的返回值
echo $?
#
实际运行一下这个脚本的效果
[root@localhost ~]# bash return.sh
1
21.向左移动位置参数:shift
要想搞清楚shift的用法,首先需要了解脚本“位置参数”的概念。假设一个脚本在运行时可以接受参数,那么从左到右第一个参数被记作$1,第二个参数为$2,以此类推,第n个参数为$N。所有参数记作$@或$*,参数的总个数记作$#,而脚本本身记作$0。
#
通过阅读该脚本了解$0
、$1
、$2
、$3
、$@
、$#
等符号的含义
[root@localhost ~]# cat shift_01.sh
!binbash
echo "This script's name is:$0"
echo "The First parameter is:$1"
echo "The Second parameter is:$2"
echo "The Third parameter is:$3"
echo "All of the parameters are $@"
echo "Count of parameter is:$#"
#
运行该脚本的效果
[root@localhost ~]# bash shift_01.sh 1 2 3
This script's name is:shift_01.sh
The First parameter is:1
The Second parameter is:2
The Third parameter is:3
All of the parameters are 1 2 3
Count of parameter is:3
shift命令可以对脚本的参数做“偏移”操作。假设脚本有A、B、C这3个参数,那么$1为A,$2为B,$3为C;shift一次后,$1为B,$2为C;再次shift后$1为C,如下所示:
#
通过阅读该脚本了解shift
命令的作用
[root@localhost ~]# cat shift_02.sh
!binbash
until [ -z "$1" ]
do
echo "$@ "
shift
Done
#
运行该脚本,使用A B C
这3
个参数
[root@localhost ~]# bash shift_02.sh A B C