学习shell时,无意中看到了这么好的一篇文章,于是就copy下来了。这篇文章可以使大家很好的对shell有所了解。 此文来源于http://www.study-area.org/linux/system/linux_shell.htm

认识SHELL

或许﹐许多人都已经听过shell 或bash 这些名字﹐但不知道您是否知道它们究竟是什么东东呢﹖

先回到电脑基础常识上吧﹕所有的电脑都是由硬体和软体构成的﹐硬体就是大家能摸得着看得见的部份﹐例如﹕键盘﹑荧幕﹑CPU﹑记忆体﹑硬碟﹑等等。离开了硬体﹐所谓的电脑是不存在的﹐因为整个系统的输入和输出以及运算都离不开硬体。请问﹕如果没有键盘和荧幕您是怎样使用电脑的﹖ 但是﹐您透过键盘进行的输入﹐以及从荧幕看到的输出﹐真正发挥功能的﹐是软体的功劳。而直接负责和这些硬体进行沟通的软体﹐就是所谓的核心(kernel)﹐kernel 必须能够接管键盘的输入﹐然后交由CPU 进行处理﹐最后将执行结果输出到荧幕上。当然﹐除了键盘和荧幕外﹐所有的硬体都必须获得kernel 的支援才能使用。

那么﹐kernel 又如何知道我们键盘输入的东西是什么呢﹖ 那就是我们这里介绍的shell 所负责的事情了。因为电脑本身所处理的数据﹐都是二进位的机器码﹐和我们人类习惯使用的语言很不一样。比方说﹐输入pwd 命令﹐我们知道这是print working directory 的意思(非常简单的人类语音)﹐但作为kernel 来说﹐它并不知道pwd 是什么﹐kernel 只会看机器码﹐这时候﹐shell 就会帮我们将pwd 翻译为kernel 能理解的程式码。所以﹐我们在使用电脑的时候﹐基本上就是和shell 打交道﹐而不是直接和kernel 沟通﹐更不是直接控制硬体。

简单来看﹐我们就这样来看待它们的关系﹕光从字面来解析的话﹐shell 就是“壳”﹐kernel 就是“核”。好比一个果实一样﹐您第一眼看到的就是壳﹐把壳扒开才看的到里面的核。shell 就是使用者和kernel 之间的界面﹐将使用者下的命令翻译给kernel 处理﹐关系如下图﹕

 

 

我们在shell 输入一个命令﹐shell 会尝试搜索整个命令行﹐并对其中的一些特殊字符做出处理﹐如果遇到CR 字符( Enter ) 的时候﹐就尝试重组整行命令﹐并解释给kernel 执行。而一般的命令格式(syntax)大致如下﹕

command parameter1 patrameter2 ...

各命令都有自己的选项(options, 通常用“ - ”符号带领)﹐可输入也可以不输入﹐如果没有额外指定﹐命令通常都有自己的预设选项﹔而参数(argument)则视各程式要求而定﹐有些很严格﹐有些也有预设的参数。例如"ls -l" 这个命令﹐选项是-l (long list)﹐而预设的参数则是当前目录。在命令行中,选项和参数都被称为参项(parameter)。

我们经常谈到的Linux﹐其实是指kernel这部份﹐而在kernel之外﹐则是各种各样的程式和工具﹐整合起来才成为一个完整的Linux发行套件。无论如何﹐Linux的kernel只有一个(尽管有许多不同的版本﹐都由Linus Tovalds负责维护)﹐但kernel之外的shell却有许多种﹐例如bourne Shell﹑C Shell﹑Korn Shell﹑Zsh Shell﹑等等﹐但我们最常接触到的名叫BASH (Bourne Again SHell)﹐为GNU所加强的一个burne shell版本﹐也是大多数Linux套件的预设shell 。不同的shell都各自有其不同的优缺点﹐有兴趣您可以自行找这方面的资料来看﹐我这里就不一一介绍了。

BASH 这个优秀的shell﹐之所以会被各大Linux 套件采用为预设的shell﹐除了它本身是open source 程式之外﹐它的强大功能应该是吸引大家目光的重要因素之一。BASH 的功能很多﹐实在很难全部介绍﹐下面只列举其中一少部份而已﹕

命令补全功能﹕
当您输入命令的时候﹐您可以输入目录或档案的开首字面﹐然后按'tab'键将您的命令路径补全。比方说﹐您要ls 一下/etc/sysconfig 这个目录的内容(假设您已经在/etc 目录下了)﹐您可以只输入ls sy 然后接连按两下tab 键﹐然后就会将/etc/ 目录下所有以sy 开头的档案和目录显示出来﹐您或许可以看到sysconfig﹑sysctl.conf ﹑syslog.conf 这三个结果﹔如果您只输入ls sys 再按两下tab 的话﹐结果是是一样的﹐因为在/etc/ 目录下面﹐所有以sy 开头的档案﹐第3 个字面都是s 而没有其它字面了﹔如果您输入ls sysc 再重复这个动作﹐那么显示结果就剩下sysconfig 和sysctl.conf 而已﹐因为以sysc 开头的只有这两个档﹐如果您再按ls sysco 接一个tab﹐那就会帮您将sysconfig 这个唯一以sysco 开头的档案补全。

 

如果您所输入的路径﹐是唯一的﹐那么只要按一下tab 就能补全﹐否则﹐会听到一下beat 声﹐这时您再补一下tab ﹐就会将所有以此路径开头的档案列出来﹔假如符号条件的档案太多﹐那系统会先将符号条件的档案数目告诉您﹐例如242 possibilities﹐然后您按y 才显示﹐如果按n 则让您增加命令的输入﹐然后您可以重复这些动作﹐直到您所输入的路径只剩唯一的对应﹐才可以用一个tab 补全。

同样的﹐这个功能也可以用在输入命令的时候﹐比方说﹐您要输入Xconfigurator 命令﹐那您只需输入Xc 然后按一下tab 就可以了﹗ 是否很方便呢﹖ ^_^

 

Tip﹕ 用tab来补全命令﹐不但方便迅速﹐而且也比较保险。因为﹐如果您前面的路径输入不正确﹐用tab是不能完成补全的﹐这可以避免您输入错误的路径而执行错误的程式。我强烈建议您执行每一个命令都常试用tab补全功能﹐以确保其正确性。(多敲这个tab键没什么坏处啦)

 

 

命令记录表﹕ 每次您输入一个命令﹐并按Enter执行之后﹐那您这个命令就被存放在命令记录表(command history)中﹐而每个命令都有一个记录号码﹐您可以用 history 命令来看看当前的命令历史表。这样﹐您只要用向上方向键﹐就可以依次呼叫出您最近所输入的命令﹐按下方向键则退回最新的命令﹐找到您想要重新输入的命令﹐然后再按Enter即可。

 

不过﹐也有一下更便利的办法﹕您可以输入! nnn (其中的nnn是history命令找到的命令记录号码)﹐就能执行指定的旧命令了﹔如果您输入!!再Enter的话﹐那就是重复上一个命令(和按向上方向键再Enter一样)﹔如果您输入!ls的话﹐则是最后一次的ls开头的命令﹐如果是!cd那就是上一个cd开头的命令﹐如此类推﹔如果您按着Ctrl和R两个键之后﹐然后输入您以前曾经输入过的命令﹐那它会和上面介绍的补全功能一样﹐将您以前输入过的命令补全起来。呵~~太厉害啦﹗

Bash会将您登录之后的所有命记录在记cache里面﹐然后﹐只要您成功退出这个shell之后﹐那这些记录就会存放到家目录的~/.bash_history这个档里面(小心看﹐它是以.开头的档案哦﹐也就是隐藏档是也﹐您要用ls -a才看得到。)不过﹐这个档只保持一定数量的命令记录而已﹐您可以透过$HISTFILESIZE这个变数(我们马上会介绍变数)﹐来获得或改变档案的记录数量。

 

 

alias 功能﹕ 在Linux 里面﹐您可以透过alias (别名) 的功能﹐来定义出一个命令的预设参数﹐甚至用另​​外一个名称来简化一个命令(及参数)。如果您输入alias 这个命令﹐您就会看到目前的alias 有哪些。您或许会看到其中有一个﹕ alias rm='rm -i' 这行﹐它的意思是﹕如果您执行rm 这个命令﹐那么系统实际执行的命令会带上-i 的参数﹐也就是以interactive模式进行﹐结果是在您进行删除档案的时候﹐会经过您的确认才真正删除。在某些没有这个alias 的系统中﹐那您执行rm 而不另行指定-i 的话﹐那就无声无息的将您能砍的档案给砍掉。小心哦﹐在Linux 上面﹐档案一旦删除就没办法救回了﹗ 所以﹐用心的系统﹐会帮您做这个alias。

 

在另外一种情形之下﹐当您发现某些长命令会经常使用到﹐但打字起来挺麻烦的﹐那您就可以用alias 来解决。比方说﹐您每次关机要输入的命令是shutdown -h now 这么一串﹐那您先输入which shd (目的是确定现有的命令名称)﹐如果您并没有发现这个命令出现在您的命令路径之中的话﹐那您可以输入alias shd='shutdown -h now'﹐然后再输入shd 就可以关机了﹗ 不过﹐现在不要执行它﹗ ﹗ 因为您这样真的会把机器关掉哦~~ 请您用alias 替换其它的长命令看看﹖

如果您要取消一个alias﹐可以使用unalias命令﹐如﹕unalias shd 。

一旦您满意您的新alias ﹐那您可以修改您的~/.bashrc 这个档﹐将它加在其它alias 命令之后﹔假如您想系统上所有使用者都能使获得这个alias ﹐那就将它放到/etc/bashrc 里面吧。(如果您目前还不会编辑档案﹐那就回到上一章补习vi 吧:-)

 

 

强大的script 能力 玩过DOS 的朋友﹐一定会知道batch 档案的功能﹐在BASH 本身可以帮您执行一系列根据条件判断的命令﹐其功能比DOS 的batch 强大多了。在本章的后面部份﹐会详细讨论shell script 的基本技巧。 事实上﹐bash 还有许多厉害的功能﹐恐怕很难全部介绍了﹐还是留给您自己去找寻了。

 

环境变数

还记得上一章里面﹐我曾经提到过﹕当我们登入系统的时候﹐首先就获得一shell﹐而且它也占据一个行程﹐然后再输入的命令都属于这个shell 的子程式。如果您学习够细心﹐不难发现我们的shell 都在/etc/passwd 这个档里面设定的﹐也就是帐号设定的最后一栏﹐预设是/bin/bash 。

事实上﹐当我们获得一个shell之后﹐我们才真正能和系统沟通﹐例如输入您的命令﹑执行您的程式﹑等等。您也可以在获得一个shell之后﹐再进入另外一个shell (也就是启动一个子程式)﹐然后还可以再进入更深一层的shell (再进入子程式的子程式)﹐直到您输入exit才退回到上一个shell里面(退回上一级的父程式)。假如您已经阅读过上一章所说过的子程式概念﹐应该不难理解。不过﹐您的行为也不是无限制的﹐而且﹐有许多设定都必须事先得到定义。所以﹐当您获得shell的时候﹐同时也获得一些环境设定﹐或称为“ 环境变数( Environment variables) ”。

所谓的变数( variable ) ﹐就是用特定的名称(或标签)保存一定的设定值﹐然后供程式将来使用。例如﹐姓=chen ﹔名=kenny ﹐那么'姓'和'名'就是变数名称﹐而chen和kenny就是变数所保存的值。由shell所定义和管理的变数﹐我们称为环境变数﹐因为这些变数可以供shell所产生的所有子程式使用。环境变数名称一般都用大写字母表示﹐例如﹐我们常用的环境变数有这些﹕

变数名称代表意思
HISTCMD当前命令的记录号码。
HISTFILE命令记录表之存放档案。
HISTSIZE命令记录表体积。
HOME预设登录家目录。
IFS预设分隔符号。
LINENO当前命令在shell script 中的行数。
MAIL邮件信箱的路径。
MAILCHECK检查邮件的秒数。
OLDPWD上次进入的目录路径。
OSTYPE作业系统类型。
PATH预设命令搜索路径。
PPID父程式之PID。
PWD当前工作目录路径。
SECONDS当前shell 之持续启动时间。
SHELL当前shell 之执行路径。
TMOUT自动登出之最高闲置时间。
UID使用者之UID。
$当前shell 之PID。
最后一个命令之返回状态。

假如您想看看这些变数值是什么﹐只要在变数名称前面加上一个“ $ ”符号﹐然后用echo命令来查看就可以了﹕

echo $PWD 
/root 
echo $$ 
1206 
echo $? 
0

第一个命令就是将当前目录的路径显示出来﹐和您执行pwd 命令的结果是一样的﹔第二个命令将当前这个shell 的PID 显示出来﹐也就是1206。如果您这时候输入kill -9 1206 的话﹐会将当前的shell 砍掉﹐那您就要重新登录才能获得另外一个shell﹐而它的PID 也是新的﹔第三行命令是上一个命令的返回状态﹕如果命令顺利执行﹐并没有错误﹐那通常是0﹔如果命令遇到错误﹐那返回状态则是非0 ﹐其值视程式设计者而定(我们在后面的shell script 的时候会介绍)。关于最后一个命令﹐不妨比较一下如下结果﹕

ls mbox 
mbox 
echo $? 

ls no_mbox 
ls: no_mbox: No such file or directory 
echo $? 
1

您会发现﹕第一命令成功执行﹐所以其返回状态是0 ﹔而第二个命令执行失败﹐其返回状态是1 。假如程式设计者为不同的错误设定不同的返回状态等级﹐那您可以根据返回值推算出问题是哪种错误引起的。

 

Tips﹕ 如果您日后写程式或script﹐要养成一个习惯﹐为每一种命令结果设定返回状态。这非常重要﹐尤其在进行debug的时候。这个我们在后面学习script的时候再谈。

 

我们随时都可以用一个= (等号)来定义一个新的变数或改变一个原有变数。例如﹕

MYNAME=kenny 
echo $MYNAME 
kenny

假如您要取消一个定义好的变数﹐那么﹐您可以使用unset命令﹕

unset MYNAME

不过﹐环境变数的特性之一﹐是单向输出的。也就是说﹕一个shell的特定变数﹐只能在这个shell里面使用。如果您要分享给同一个shell里面的其它程式﹑script﹑命令使用﹐或它们的子程式使用﹐那您必须用export命令将这个变数进行输出。但无论如何﹐如果您在一个子程式中定义了一个变数﹐那么这个变数的值﹐只影响这个子程式本身以及它自己的子程式﹐而永远不会影像到父程式或父程式产生的其它子程式。

比方说﹐您在一个程式中定义一个新的变数﹐或改变一个原有变数值﹐在程式结束的时候﹐那它所设定的变数均被取消﹔如果您想将变数值分享给该程式所产生的子程式﹐您必须用export 命令才能保留这个变数值﹐除非子程式另外重新定义。但无论如何﹐当前程式所定义的变数值﹐是无法传回父程式那边的。不妨做做如下的实验﹕

MYNAME=kenny 
echo $MYNAME 
kenny 
export MYNAME
#设定一个变数。 

#当前的设定值。 
#用export输出变数值。
/bin/bash# 再开一个shell﹐也就是进入子程式中。
echo $MYNAME 
kenny

#

# 保留原有设定值。

export MYNAME=netman 
echo $MYNAME 
netman

# 重新定义设定值﹐同时也用export 输出。


#变数值被新值取代。

exit

# 退出子程式﹐返回父程式。

echo $MYNAME 
kenny

#

# 父程式的变数值并没有改变。

关于变数的另一个特性﹐是的变数值是可以继承的。也就是说﹐您可以将一个变数值来设定另外一个变数名称。比方说﹕

FIRST_NAME="Kenny" 
MYNAME=$FIRST_NAME

echo $MYNAME 
Kenny

# 定义一个变数。

# 再定义另一个变数﹐但它的值是第一个变数。


#第二个变数继承了第一个变数的值。

另外﹐在定义变数的时候您还要注意一些规则﹕

 

  • 定义变数时﹐“=”号两边没有空白键﹔
  • 作为变数的名称﹐只能是字母和数字﹐但不能以数字开头﹔如果名称太长﹐可以用“_”分隔﹔
  • 预先定义的变数均为大写﹐自定义变数可以混合大小写﹐以更好区别﹔
  • 只有Shell 本身定义的变数才能称为环境变数;
  • 如果变数中带有特殊字符﹐必须先行用“\”符号跳脱﹔
  • 如果变数中带有空白﹐必须使用引号﹐或进行跳脱。

 

关于后两项﹐或许我们再找些例子来体会一下﹕

TOPIC='Q & A'

# 用单引号保留特殊符号和空白

 

Q1=What\'s\ your\ \"topic\"\?

echo $Q1 
What's your "topic"?

 

# 用\ 将特殊符号(含引号)和空白跳脱出来

#

# 跳脱后﹐特殊符号和空白都保留下来。

 

ANS="It is $TOPIC."

echo $ANS 
It is Q & A.

 

# 用双引号保留变数值($)

#

# 用双引号﹐显示出变数值。

 

WRONG_ANS='It is "$TOPIC".'

echo $WRONG_ANS 
It is "$TOPIC".

 

 

# 用单引号保留特殊符号和空白(同第一行)


#用单引号﹐全部保留﹔同时﹕

# $ 也当成一般符号保留﹐而非变数值。

 

ALT_ANS='the $TOPIC'\ is\ "'$TOPIC'"\.

echo $ALT_ANS 
The $TOPIC is 'Q & A'.

 

# 同时混合单引号﹑双引号﹑和跳脱字符 \

#

#单引号保留全部﹔双引号保留变数值﹔ 
# \将特殊符号跳脱出来。

我这里解释一下最后面的例子好了﹕ 'the $TOPIC is '"$TOPIC"\.。首先用单引号将'the $TOPIC is '这段文字括好﹐其中用3个空白键和一个$符号﹔然后用双引号保留$TOPIC的变数值﹔最后用\跳脱小数点。

在引用" " 和' ' 符号的时候﹐基本上﹐ ' ' 所包括的内容﹐会变成单一的字串﹐任何特殊字符都失去其特殊的功能﹐而变成一般字符而已﹐但其中不能再使用'符号﹐而在" " 中间﹐则没有' ' 那么严格﹐某些特殊字符﹐例如$ 号﹐仍然保留着它特殊的功能。您不妨实作一下﹐比较看看echo ' "$HOME" ' 和echo " '$HOME' " 的差别。

 

Tips﹕ 在shell命令行的跳脱字符“ \ ”其实我们会经常用到的。例如﹐您的一个命令太长﹐一直打下去可能超过一行﹐或是想要整洁的输入命令行﹐您或许想按Enter键敲到下一行继续输入。但是﹐当您敲Enter键的时候﹐事实上是输入一个CR (Carriage-Return)字符﹐一但shell读到CR字符﹐就会尝试执行这个命令。这时﹐您就可以在输入Enter之前先输入\符号﹐就能将CR字符也跳脱出来﹐这样shell就不会马上执行命令了。这样的命令行﹐我们在script中经常看到﹐但您必须知道那代表什么意思。

 

如果﹐您想对一些变数值进行过滤﹐例如﹕MY_FILE=' ~/tmp/test.sh' ﹐而您想将变数值换成test.sh (也就是将前面的路径去掉)﹐那您可以将$MY_FILE换成${MY_FILE##*/}。这是一个变数值字串过滤﹕##是用来比对变数前端部份﹐然后*/是比对的色样(也就是任何字母到/之间)﹐然后将最长的部份删除掉。您可以参考如下范例﹕

当FNAME="/home/kenny/tmp/test.1.sh" 的时候﹕

变数名称代表意思结果
${FNAME}显示变数值的全部。

${FNAME##/*/}比对变数值开端﹐如果以/*/ 开头的话﹐砍掉最长的部份。

${FNAME#/*/}比对变数值开端﹐如果以/*/ 开头的话﹐砍掉最短的部份。

${FNAME%.*}比对变数值末端﹐如果以.* 结尾的话﹐砍掉最短的部份。

${FNAME%%.*}比对变数值末端﹐如果以.* 结尾的话﹐砍掉最长的部份。

${FNAME/sh/bash}如果在变数值中找到sh 的话﹐将第一个sh 换成bash。

${FNAME//sh/bash}如果在变数值中找到sh 的话﹐将全部sh 换成bash。

您除了能够对变数进行过滤之外﹐您也能对变数做出限制﹑和改变其变数值﹕

 字串没设定空字串非空字串
使用预设值
var=${str-expr}var=exprvar=var=$str
var=${str:-expr}var=exprvar=exprvar=$str
使用其它值
var=${str+expr}var=exprvar=exprvar=expr
var=${str:+expr}var=exprvar=var=expr
设定预设值
var=${str=expr}str=expr

var=expr

str 不变

var=

str 不变

var=$str

var=${str:=expr}str=expr

var=expr

str=expr

var=expr

str 不变

var=$str

输出错误
var=${str?expr}expr 输出至stderr var=var=str
var=${str:?expr}expr 输出至stderr expr 输出至stderrvar=str

一开始或许比较难理解上面的两个表格说明的意思﹐真的很混乱~~ 但只要多做一些练习﹐那您就知道怎么使用了。比方说﹕

# expr=EXPR 
# unset str 
# var=${str=expr}; echo var=$var str=$str expr=$expr 
var=expr str=expr expr=EXPR 
# var=${str:=expr}; echo var=$var str=$str expr=$expr 
var=expr str=expr expr=EXPR 
# str= 
# var=${str=expr}; echo var=$var str=$str expr=$expr 
var= str= expr=EXPR 
# var=${str:=expr}; echo var=$var str=$str expr=$expr 
var=expr str=expr expr=EXPR 
# str=STR 
# var=${str=expr }; echo var=$var str=$str expr=$expr 
var=STR str=STR expr=EXPR 
# var=${str:=expr}; echo var=$var str=$str expr=$expr 
var= STR str=STR expr=EXPR # MYSTRING=test # echo ${MYSTRING?string not set\!} test # MYSTRING= # echo ${MYSTRING?string not set\!} # unset MYSTRING # echo ${MYSTRING?string not set\!} bash: MYSTRING: string not set! 






  

 

请记住这些变数的习性﹐日后您要写shell script的时候就不会将变数搞混乱了。假如您想看看当前shell的环境变数有哪些﹐您可以输入set命令﹔如果只想检查export出来的变数﹐可以输入exportenv (前者是shell预设的输出变数)。

Bash 设定

到这里﹐您或许会问﹕shell 的环境变数在哪里定义呢﹖ 可以调整吗﹖

嗯﹐第一个问题我不大了解﹐我猜那是shell 设计者预设定义好的﹐我们一登录获得shell 之后就有了。不过﹐第二个问题﹐我却可以肯定答复您﹕您可以随时调整您的环境变数。您可以在进入shell 之后用在命令行里面重新定义﹐也可以透过一些shell 设定档来设定。

先让我们看看﹐当您在进行登录的时候﹐系统会检查哪些档案吧﹕

  1. /etc/profile﹕首先﹐系统会检查这个档﹐以定义如下这些变数﹕PATH﹑USER﹑LOGNAME﹑MAIL﹑HOSTNAME﹑HISTSIZE﹑INPUTRC。如果您会shell script (我们后面再讨论)﹐那您应该看得出这些变数是如何定义的。另外﹐还指定了umask和ulimit的设定﹕umask大家应该知道了﹐而ulmimit呢﹖ 它是用来限制一个shell做能建立的行程数目﹐以避免系统资源被无限制的消耗。最后﹐它还会检查并执行/etc/profile.d/*.sh那些script﹐有兴趣您可以追踪看看。

     

     

  2. ~/.bash_profile﹕这里会定义好USERNAME﹑BASH_ENV﹑PATH。其中的PATH除了现有的$PATH之外﹐还会再加入使用者相关的路径﹐您会发现root和普通帐号的路径是不一样的﹔而BASH_ENV呢﹐仔细点看﹐是下一个要检查的档案﹕

     

     

  3. ~/.bashrc﹕在这个档里面﹐您可以发现一些alias设定(哦~~原来在这里﹗)。然后﹐您会发现有一行﹕ . /etc/bashrc。在shell script中﹐用一个小数点然后然后一个空白键再指向另外一个script﹐意思是同时执行那个script并采用那里的变数设定。

     

     

  4. /etc/bashrc﹕基本上﹐这里的设定﹐是所有使用者在获得shell的时候都会采用的。这里指定了一些terminal设定﹐以及shell提示字符等等。

     

     

  5. ~/.bash_login﹕如果~/.bash_profile不存在﹐则使用这个档。

     

     

  6. ~/.profile﹕如果~/.bash_profile和~/.bash_login都不存在﹐则使用这个档。

     

     

  7. ~/.bash_logout﹕这个档通常只有一个命令﹕ clear ﹐也就是把荧幕显示的内容清掉。如果您想要在登出shell的时候﹐会执行一些动作﹐例如﹕清空临时档(假如您有使用到临时档)﹑还原某些设定﹑或是执行某些备份之类的。

 

您可以透过修改上面提到的档案﹐来调整您进入shell之后的变数值。一般使用者可以修改其家目录( ~/ )中的档案﹐以进行个人化的设定﹔而作为root﹐您可以修改/etc/下面的档案﹐设定大家共用的变数值。至于bash的变数值如何设定﹖ 有哪些变数﹖ 各变数的功能如何﹖ 您打可以执行man bash参考手册资料。

 

Tips﹕ 一旦您修改了/etc/profile或~/.bash_profile档案﹐其新设定要在下次登录的时候才生效。如果您不想退出﹐又想使用新设定﹐那可以用 source 命令来抓取﹕
source ~/.bash_profile

 

命令重导向

好了﹐相信您已经对您的shell有一定的了解了。然后﹐让我们看看shell上面的一些命令功能吧﹐这些技巧都是作为一个系统管理员基本要素。其中之一就是﹕ 命令重导向(command redirection)和命令管线 (command pipe) 。

在深入讲解这两个技巧之前﹐先让我们了解一下shell 命令的基本概念﹕

名称代号代表意思设备
STDIN0标准输入键盘
STDOUT1标准输出荧幕
STDERR2标准错误荧幕

表格中分别是我们在shell 中一个命令的标准I/O (输出与输入)。当我们执行一个命令的时候﹐先读入输入(STDIN)﹐然后进行处理﹐最后将结果进行输出(STDOUT)﹔如果处理过程中遇到错误﹐那么命令也会显示错误(STDERR)。我们可以很容易发现﹕一般的标准输入﹐都是从我们的键盘读取﹔而标准输出和标准错误﹐都从我们的银幕显示​​。

同时﹐在系统上﹐我们通常用号码来代表各不同的I/O﹕STDIN 是0﹑STDOUT 是1﹑STDERR 是2。

当您了解各个I/O 的意思和所代表号码之后﹐让我们看比较如下命令的结果﹕

ls mbox 
mbox 
ls mbox 1> file.stdout 

请小心看第二个命令﹕在命令的后面多了一个1 ﹐而紧接着(没有空白﹗)是一个大于符号(> )﹐然后是另外一个档案名称。但是﹐荧幕上却没有显示命令的执行结果﹐也就是说﹕ STDOUT不见了﹗ 那到底发生什么事情了呢﹖

呵﹐相信您不会这么快忘记了STDOUT 的代号是1 吧﹗ 没错了﹐因为我们这里将1 用一个> 符号重导到一个档案中了。结果过是﹕我们将标准输出从荧幕改变到档案中﹐所以我们在银幕就看不到STDOUT﹐而原先的STDOUT 结果则保存在大于符号右边的档中了。不信﹐您看看这个档案的内容就知道了﹕

cat file.stdout 
mbox

当我们用一个>将命令的STDOUT导向到一个档案的时候﹐如果档案不存在﹐则会建立一个新档﹔如果档案已经存在﹐那么﹐这个档案的内容就换成STDOUT的结果。有时候﹐您或许想保留原有档案的内容﹐而将结果增加在档案末端而已。那您可以多加一个>﹐也就是使用>>就是了。您可以自己玩玩看哦~~﹐通常﹐我们要将一些命令或错误记录下来﹐都用这个方法。

 

Tips﹕ 如果您不希望>意外的盖掉一个原有档﹐那您可以执行这个命令﹕
set -o noclobber

 

不过﹐仍可以用>| 来强迫写入。

 

上前面的例子中﹐我们指定了I/O 1 (STDOUT) 进行重导向﹐这也是预设值﹐如果您没有指定代号﹐那么就是进行STDOUT 的重导向﹐所以1> 和> 是一样的﹔1 >> 和>> 也是一样的。但如果您使用了数字﹐那么数字和> 之间一定不能有空白存在。

好了﹐下面再比较两个命令﹕

ls no_mbox 
ls: no_mbox: No such file or directory 
ls no_mbox 2>> file.stderr 
 

嗯﹐相信不用我多解释了吧﹖ (如果档案不存在﹐>> 和> 都会建立新的。)

事实上﹐在我们的日常管理中﹐重导向的应用是非常普遍的。我只举下面这个例子就好了﹕

当我们进行核心编译的时候(我们下一章再介绍)﹐荧幕会飞快的显示出成千上万行信息﹔其中有大部份是STDOUT﹐但也有些是STDERR。除非您的眼睛真的那么厉害﹐否则您很难分辩出哪些是正常信息﹐哪些是错误信息。当您要编译失败﹐尝试找错误的时候﹐如果已经将STDERR 重导出来﹐就非常方便了﹕

make dep clean bzImage modules 1>/dev/null 2>/tmp/kernel.err &

这里﹐我一共有三个打算﹕(1) 将标准输出送到一个叫nu​​ll 的设备上﹐如果您记性够好﹐我在前面的文章中曾比喻它为黑洞﹕所有东西进去之后都会消失掉。凭我个人的习惯﹐我会觉得编译核心时跑出来的信息﹐如果您不感兴趣的话﹐那都是垃圾﹐所以我将STDOUT 给重导到null 去﹐眼不见为干净﹔ (2) 然后﹐我将STDERR 重导到/tmp/kernel.err 这个档去﹐等命令结束后﹐我就可以到那里看看究竟有部份有问题。有些问题可能​​不是很重要﹐有些则可能需要重新再编核心﹐看您经验啦。(3) 最后﹐我将命令送到background 中执行(呵~~ 相信您还没忘记吧﹗)。因为﹐编译核心都比较花时间﹐所以我将之送到背景去﹐这样我可以继续做其它事情。

 

Tips﹕ 这时﹐因为系统太忙了﹐可能反应速度上会比较慢些﹐如果您真的很在意﹐不妨考虑把make的nice level提高。(忘记怎么做了﹖那翻看前一章吧)

 

前面的例子﹐我们是分开将STDOUT 和STDERR 重导到不同的档案去﹐那么﹐我们能否把两者都重导到同一个档呢﹖ 当然是可以的﹐请比较下面三行﹕

make dep clean bzImage modules >/tmp/kernel.result 2>/tmp/kernel.result 
make dep clean bzImage modules >/tmp/kernel.result 2>&1 
make dep clean bzImage modules &>/tmp/kernel. resultt

我这里告诉您﹕第一行的命令不怎么正确﹐因为这样会造成这两个输出同时在'抢'一个档案﹐写入的顺序很难控制。而第2 行和第3 行的结果都是一样的﹐看您喜欢用哪个格式了。不过﹐要小心的是﹕& 符号后面不能有空白键﹐否则会当成将命令送到背景执行﹐而不是将STDOUT 和STDERR 整合。

好了﹐前面我们都在谈STDOUT 和STDERR 的重导向﹐那么﹐我们是否能重导STDIN 呢﹖

当然可以啦~~~

有些命令﹐当我们执行之后﹐它会停在那里等待键盘的STDIN输入﹐直到遇到EOF (Ctrl+D)标签才会真正结束命令。比方说﹐在同一个系统上﹐如果有多位使用者同时登入的话﹐您可以用write命令向特的使用者送出短讯。而短讯的内容就是键盘敲入的文字﹐这时候命令会进入输入模式﹐您每输入一行并按Enter之后﹐那么讯息就会在另外一端﹐直到您按Ctrl+D键才离开并结束命令。

write user1 
Hello! 
It is me... ^_^ 
How ru! 
(Ctrl+D)

这样通常都需要花一些时间输入﹐假如对方在写什么东西和查看某些资料的时候﹐就很混乱。这时候﹐您或许可以先将短讯的内容写在一个档案里面﹐例如greeting.msg﹐然后这样输入就可以了﹕

write user1 < greeting.msg

就这样﹐这里我们用小于符号( < )来重导STDIN 。简单吧﹖ ^_^

不过﹐我们用cat 命令建立简单的档案的时候﹐却是使用> 符号的﹕

cat > file.tmp

 

等您按Ctrl+D 之后﹐从键盘输入的STDIN﹐就保存在file.tmp 中了。请想想看为什么会如此﹖ (我在LPI 的考试中碰到过这道题目哦~~~)

pipe

查字典﹐pipe 这个英文是水管﹑管道﹑管线的意思。那么﹐它和命令又有什么牵连呢﹖ 简单的说﹐一个命令管线﹐就是将一个命令的STDOUT 作为另一个命令的STDIN 。

其实﹐这样的例子我们前面已经碰到多次了﹐例如上一章介绍tr 命令的时候﹕

cat /path/to/old_file | tr -d '\r' > /path/to/new_file

上面这个命令行﹐事实上有两个命令﹕cat 和tr ﹐在这两个命令之间﹐我们用一个“ | ”符号作为这两个命令的管线﹐也就是将cat 命令的STDOUT 作为tr 命令的STDIN ﹔然后﹐tr 命令的STDOUT 用> 重导到另外一个档案去。

上面只是一个非常简单的例子而已﹐事实上﹐我们可以用多个管线连接多个程式﹐最终获得我们确切想要的结果。比方说﹕我想知道目前有多少人登录在系统上面﹕

w | tail +3 | wc -l

我们不妨解读一下这个命令行﹕(1) w 命令会显示出当前登录者的资源使用情况﹐并且每一个登录者占一行﹔(2) 再用tail 命令抓取第3 行开始的字行﹔( 3) 然后用wc -l 计算出行数。这样﹐就可以知道当前的登录人数了。

许多朋友目前都采用拨接ADSL 上网﹐每次连线的IP 都未必一样﹐只要透过简单的命令管线﹐您就可以将当前的IP 抓出来了﹕

 

  1. 我们不妨观察ifconfig ppp0 这个命令的输出结果﹕
    
    

     

     

  2. 不难发现IP 位址所在的句子中有着其它句子所没有的字眼﹕inet addr 。然后﹐我们就可用grep 把​​这行抓出来﹕
    
    

     

     

  3. 再来﹐我们先用相同的分隔符号将句子分成数列﹐然后抓出IP 位址所在的那列。

     

    嗯﹐这里﹐我们可以用“ : ”来分出4 列﹔也可以用空白键来分出5 列(空因为句子开首就是一个空白键)。如果用空白键来分的话﹐由于有些间隔有多个空白键的原因﹐那么﹐我们可以用tr 命令﹐将多个空白键集合成一个空白键﹕

    
    
    (注意﹕在' ' 之间是一个空白键﹗)

     

  4. 然后用cut 命令抓出IP 所在的列﹐细心数一数﹐应该是第3 列﹕
    
    

     

     

  5. 然后我们用“ : ”再分两列﹐抓第2 列就是IP 了﹕
    
    

 

这里﹐我们一共用5 个pipe 将4 个命令连接起来﹐就抓出机器当前的IP 位址了。是否很好用呢﹖

在同一个命令行里面出现多个命令的情形﹐除了“ | ”之外﹐或许您会看到" ` ` " 符号﹐也就是和~ 键同一个键的符号(不用按Shift )。它必须是一对使用的﹐其中可以包括单一命令﹐或命令管线。那它的效果和命令管线又有什么分别呢﹖

我们使用pipe 将一个命令的STDOUT 传给下一个命令的STDIN﹐但使用`` 的时候﹐它所产生的STDOUT 或STDERR 仅作为命令行中的一个参数而已。嗯﹐不如看看下面命令好了﹕

TODAY=`date +%D` 
echo Today is $TODAY. 
Today is 08/17/01.

从结果我们可以看出﹐我们用`` 将date 这个命令括起来(可含参数)﹐那么它的执行结果可以作为TODAY 的变数值。我们甚至还可以将一串命令管线直接用在命令行上面﹕

echo My IP is `ifconfig ppp0 | grep "inet addr" \ 
    | tr -s ' ' ' ' | cut -d ' ' -f3 | cut -d ':' -f2`
 
My IP is 211.74.48.254.

注意﹕第一行的CR 被\ 跳脱了﹐所以这个命令行'看起来'有两行。我之所以弄这么复杂﹐是告诉您这对`` 符号可以适用的范围。

 

Tips﹕ 在变数中使用``可以将命令的执行结果当成变数值的部份。事实上﹐除了用``之外﹐您也可以用这样的格式﹕  VAR_NAME=$(command)  ﹐那是和VAR_NAME=`command`的结果是一样的。

 

除了这对`` 和| 之外﹐还有另外一个符号“ ; ”来分隔命令的。不过﹐这个比较简单﹕就是当第一命令结束之后﹐再执行第二个命令﹐如此类推﹕

./configure; make; make install

呵~​​~ 如果您对您的安装程式有绝对信心﹐用上面一行命令就够了﹗

Shell Script

当我们对shell 变数和命令行有一定认识之后﹐那么﹐我们就可以尝试写自己的shell script 啰~~ 这可是非常好玩而又有成就感的事情呢﹗ ^_^

在linux 里面的shell script 可真是无处不在﹕我们开机执行的run level 基本上都是一些script ﹔登录之后的环境设定﹐也是些script ﹔甚至工作排程和记录维护也都是script 。您不妨随便到/etc/rc.d/init.d 里面抓两个程式回来看看﹐不难发现它们都有一个共同的之处﹕第一行一定是如下这样的﹕

 

其实﹐这里的#! 后面要定义的就是命令的解释器(command interpreter)﹐如果是/bin/bash 的话﹐那下面的句子就都用bash 来解释﹔如果是/usr/bin/perl 的话﹐那就用perl 来解释。不同的解释器所使用的句子语法都不一样﹐非常严格﹐就算同是用shell 来解释﹐不同的shell 之间的格式也不仅相同。所以﹐如果您看到script 的解释器是/bin/sh 的话﹐那就要小心了﹕如果您仔细看这个档案﹐事实上它仅是一个link 而已﹐有些系统或许会将它link 到其它shell 去。假如您的script 句子使用的语法是bash 的话﹐而这个sh 却link 到csh ﹐那执行起来可能会有问题。所以﹐最好还是直接指定shell 的路径比较安全一些﹕在这里的范例都使用/bin/bash 来作为script 的解释器。

在真正开始写script 之前﹐先让我们认识script 的一些基本概念﹕

简单来说﹐shell script 里面就是一连串命令行而已﹐再加上条件判断﹑流程控制﹑回圈﹑等技巧﹐聪明地执行正确的命令和使用正确的参数选项。和我们在shell 里面输入命令一样﹐shell script 也有这样的特性﹕

 

  • 当读到一个CR 字符的时候﹐就尝试执行该行命令﹔
  • 它会忽略空白行﹔句子前面的空白和tab 也不理会﹔
  • CR 字符也同样可以用“ \ ”符号跳脱﹔
  • 另外﹐“ # ”符号是注解符号﹐从这个符号至句子末端的内容全被忽略﹐程式本身不会读入这部份﹐但我们经常用来给使用者阅读﹐因而名为注解﹔
  • 等等。

 

一个良好的script 作者﹐在程式开头的时候﹐都会用注解说明script 的名称﹑用途﹑作者﹑日期﹑版本﹑等信息。如果您有这个机会写自己的script﹐也应该有这个良好习惯。

shell script档的命名没一定规则﹐可以使用任何档案名称(参考档案系统)﹐但如果您喜欢的话﹐可以用.sh来做它的副档名﹐不过这不是硬性规定的。不过﹐要执行一个shell script﹐使用者必须对它有执行权限( x )﹐用文件编辑器新建立的档案都是没有x permission的﹐请用chmod命令加上。执行的时候﹐除非该script已经至于PATH环境变数之内的路径内﹐否则您必须指定路径。例如﹐您写了一个叫test.sh的shell script﹐放在家目录内﹐假设这也是您的当前工作目录﹐您必须加上路径才能执行﹕./test.sh或~/test.sh 。所以﹐建议您在script测试无误之后﹐放在~/bin目录里面﹐那就可以在任何地方执行自己的script了﹐当然﹐您要确定~/bin已经出现在您的PATH变数里面。

script之所以聪明﹐在于它能够对一些条件进行测试( test )。您可以直接用test命令﹐也可以用if叙述﹐例如﹕test -f ~/test.sh 。它的意思是测试一下~/test.sh这个档案是否存在﹐这个-f通常用在档案上面的测试﹐除了它﹐还有很多﹕

标签代表意思
-G存在﹐并且由GID 所执行的行程所拥有。
-L存在﹐并且是symbolic link 。
-O存在﹐并且由UID 所执行的行程所拥有。
-S存在﹐并且是一个socke 。
-b存在﹐并且是block 档案﹐例如磁碟等。
-c存在﹐并且是character 档案﹐例如终端或磁带机。
-d存在﹐并且是一个目录。
-e存在。
-f存在﹐并且是一个档案。
-g存在﹐并且有SGID 属性。
-k存在﹐并且有sticky bit 属性。
-p存在﹐并且是用于行程间传送资讯的name pipe 或是FIFO。
-r存在﹐并且是可读的。
-s存在﹐并且体积大于0 (非空档)。
-u存在﹐并且有SUID 属性。
-w存在﹐并且可写入。
-x存在﹐并且可执行。

事实上﹐关于这些测试项目还有很多很多﹐您可以man bash 然后参考CONDITIONAL EXPRESSIONS 那部份。另外﹐我们还可以同时对两个档案进行测试﹐例如﹕test file1 -nt file2 就是测试file1 是否比file2 要新。这种测试使用的标签是﹕

标签代表意思
-ntNewer Than﹕第一个档案比第二个档案要新。
-otOlder Than﹕第一个档案比第二个档案要旧。
-efEqual File﹕第一个档案和第二个档案其实都是同一个档案(如link)。

我们这里所说的这些测试﹐不单只用来测试档案﹐而且还常会用来比对' 字串(string)'或数字(整数)。那什么是字串呢﹖ 字面来介绍就是一串文字嘛。在一个测试中﹐~/test.sh本身是一个档案﹔但'~/test.sh' ﹐则是在引号里面(单引号或双引号)﹐那就是字串了。

在数字和字串上面的比对(或测试)﹐所使用的标签大约有﹕

标签代表意思
=等于
!=不等于
<小于
>大于
-eq等于
-ne不等于
-lt小于
-gt大于
-le小于或等于
-ge大于或等于
-a双方都成立
-o单方成立
-z空字串
-n非空字串

在上面提到的比对中﹐虽然有些意思一样﹐但使用场合却不尽相同。例如= 和-eq 都是'等于'的意思﹐但= 只能比对字串﹐而-eq 则可以用来比对字串﹐也能用来比对表示色样(我们在regular expression 会碰到)。

 后续!!!!