当贝尔实验室为全新的Unix操作系统创建了交互式用户界面之后,计算机便拥有了一项独有的特性。它可以从文本文件(称为shell脚本)中读取并执行命令,好像这些命令是在终端中输入的一样。
程序员再也不用输入一堆命令来执行一系列操作,只需要把这些命令保存在文件中,然后运行这个文件就可以了。
用户是通过终端会话同shell环境打交道的。如果你使用的是基于图形用户界面的系统,这指的就是终端窗口。如果没有图形用户界面(生产服务器或SSH会话),那么登录后你看到的就是shell提示符。
打开终端时会出现一个提示符。其形式通常如下:
username@hostname$
或者也可以配置成root@hostname #
,或者简单的显示为$
或#
。
$
表示普通用户,#
表示管理员用户root。root是Linux系统中权限最高的用户。
以root用户(管理员)的身份直接使用shell来执行任务是不好的。因为如果shell具备较高的权限,命令中出现的输入错误有可能造成更严重的破坏,所以推荐使用普通用户(shell会在提示符中以$来表明这种身份)登录系统,然后借助sudo这类工具来运行特权命令。使用sudo 执行命令的效果和root一样。
shell脚本通常以shebang起始:
#!/bin/bash
shebang是一个文本行,其中#!
位于解释器路径之前。/bin/bash是Bash的解释器命令路径。bash将以#
符号开头的行视为注释。脚本中只有第一行可以使用shebang来定义解释该脚本所使用的解释器。
脚本的执行方式有两种。
(1) 将脚本名作为命令行参数:
bash mySript.sh
(2) 授予脚本执行权限,将其变为可执行文件:
chmod 755 mySript.sh
./myScirpt.sh
如果将脚本作为bash的命令行参数来运行,那么就用不着使用shebang了。可以利用shebang来实现脚本的独立运行。可执行脚本使用shebang之后的解释器路径来解释脚本。
使用chmod命令赋予脚本可执行权限:
$ chmod a+x sample.sh
该命令使得所有用户可以按照下列方式执行该脚本:
$ ./sample.sh # ./表示当前目录
或者
$ /home/path/sample.sh #使用脚本的完整路径
内核会读取脚本的首行并注意到shebang为#!/bin/bash
。它会识别出/bin/bash
并执行该脚本:
$ /bin/bash sample.sh
当启动一个交互式shell时,它(内核)会执行一组命令来初始化提示文本、颜色等设置。这组命令来自用户主目录中的脚本文件~/.bashrc
(对于登录shell则是~/.bash_profile
,这里的.
表示的是隐藏文件,可使用ls -a
查看)。Bash shell还维护了一个历史记录文件~/.bash_history
,用于保存用户运行过的命令。
~表示主目录,它通常是
/home/usr
,其中user是用户名,如果是root用户,则为/root
~
表示主目录,它通常是/home/user
,其中user是用户名,如果是root用户,则为/root
。但登录图形化环境(比如GNOME、KDE等)后所创建的终端会话并不是登录shell。使用GNOME或KDE这类显示管理器登录后并不会读取.profile
或.bash_profile
(绝大部分情况下不会),而使用ssh登录远程系统时则会读取.profile
。
shell使用分号或换行符来分隔单个命令或命令序列。
$ cmd1 ; cmd2
这等同于:
$ cmd1
$ cmd2
注释部分以#
为起始,一直延续到行尾。注释行通常用于描述代码或是在调试期间禁止直行行代码:
# sample.sh - echoes "hello world"
echo "hello world"
echo是用于终端打印的最基本命令。
默认情况下,echo在每次调用后会添加一个换行符:
$ echo "Welcome to Bash"
只需要将文本放入双引号中,echo命令就可以将其中的文本在终端中打印出来。类似地,不适用双引号也可以得到同样的输出结果:
$ echo Welcome to Bash
实现相同效果的另一种方式是使用单引号:
$ echo 'text in quotes'
双引号允许shell解释字符串中出现的特殊字符。单引号不会对其做任何解释
思考下面的命令:
echo "cannot include exclamation - ! within double quotes"
理论上命令应该输出如下:
bash: !: event not found error
但是实际上,却正常的输出了,可能是乌班图18.04做的改进。
如果需要打印像!
这样的特殊字符,那就不要将其放入双引号中,或是在特殊字符之前加上一个反斜线(\
):
echo Hello world !
或者
echo 'Hello world !'
或者
echo "Hello world \!" #将转义字符放在前面
实际上我这里将\
一起输出了
如果不适用引号,我们无法在echo中使用分号,因为分号在Bash shell中做命令间的分隔符:
echo hello; hello
对于上面的命令,Bash将echo hello作为一个命令,将hello作为另一个命令。
在下一条攻略中将讨论到的变量替换不会在单引号中执行。
另一个可用于终端打印的命令是printf。该命令使用的参数和C语言中的printf函数一样。
printf "Hello world"
printf命令接受引用文本或由空格分隔的参数。
#!/usr/bash
#filename: printf.sh
printf "%-5s %-10s %-4s\n" No Name Mark
printf "%-5s %-10s %-4.2f\n" 1 Sarath 80.3456
printf "%-5s %-10s %-4.2f\n" 2 James 90.9989
printf "%-5s %-10s %-4.2f\n" 3 Jeff 77.564
%s
、%c
、%d
和%f
都是格式替换符(format subsititution character),它们定义了该如何打印后续参数。%-5s
指明了一个格式为左对齐且宽度为5的字符串替换(-
表示左对齐)。如果不知名-
,字符串中就采用右对齐形式。宽度指定了保留给某个字符串的字符数量。对Name而言,其保留宽度是10.因此任何Name字段内的内容都被显示在10字符宽的保留区域内,如果内容不足10个字符,余下的则以空格填充。
对于浮点数,可以使用其他参数对小数部分进行舍入(round off)。
对于Mark字段,我们将其格式化为%-4.2f
,其中.2
指定保留两位小数。注意,按在每行的格式字符串后都有一个换行符(\n
)。
使用echo和printf的命令选项时,要确保选项出现在命令中的所有字符串之前,否则Bash会将其视为另外一个字符串。
在echo中转义换行符
默认情况下,echo会在输出文本的尾部追加一个换行符。可以使用-n
来禁止这种行为。echo同样接受双包含转义序列的双引号字符串作为参数。在使用转义序列时,需要使用echo -e "包含转义序列的字符串"
这种形式。例如:
echo -e '1\t2\t3'
打印彩色输出
脚本可以使用转义序列在终端中生成彩色文本。
文本颜色是由对应的色彩代码来描述的。其中包括:重置=0, 黑色=30, 红色=31, 绿色=32, 黄色=33, 蓝色=34, 洋红=35, 青色=36, 白色=37.
要打印彩色文本,可以输入如下命令:
echo -e '\e[1;31m This is red text \e[0m'
其中\e[1;31m
就是一个转义字符串,可以将颜色设置为红色,\e[0m
将颜色重新置回。只需要将31替换成想要的色彩代码就可以了。
对于彩色背景,经常使用的颜色代码是:重置=0, 黑色=40, 红色=42, 黄色=43, 蓝色=44, 洋红=45, 青色=46, 白色=47
要设置彩色背景,可以输入如下命令:
echo -e "\e[1;42m Green Backgroud \e[0m"
区别在于色彩的范围!
这些例子中包含了一些转义序列。可以使用man console_codes
来查看相关文档。
和编译型语言不同,大多数脚本语言不要求在创建变量之前声明其类型。用到什么类型就是什么类型。在变量名前面加上一个美元符号就可以访问到变量的值。shell定义了一些变量,用于保存用到的配置信息,比如可用的打印机、搜索路径等。这些变量叫做环境变量。
变量名不包含空白字符。常用的管理是在脚本中使用大写字母来命名环境变量,使用驼峰命名法或小写字母命名其它变量。
所有的应用程序和脚本都可以访问环境变量。可以使用env
或printenv
命令查看当前shell中所定义的全部环境变量:
$> env
PWD=/home/clif/ShellCookBook
HOME=/home/clif
SHELL=/bin/bash
要查看其它进程的环境变量,可以使用如下命令:
cat /proc/$PID/environ
其中,PID是相关进程的进程ID(PID是一个整数)。
补充:
ps命令可以列出正在运行的进程。
ps | less
top命令是一个常用的查看系统资源使用情况和查看占用系统资源最多的进程的命令。
假设有一个叫gedit的应用程序正在运行。我们可以使用pgrep命令获得gedit的进程ID:
$ pgrep gedit
3702
查看与该进程相关的环境变量:
cat /proc/3702/environ
特殊文件
/proc/PID/environ
是一个包含环境变量以及对应变量值的列表。每一个变量以name=value的形式来描述,彼此之间有null字符(\0)分隔。形式上确实不太易读。
向生成一份易读的报表,可以将cat命令的输出通过管道传给tr,将其中的\0
替换成\n
:
cat /proc/3702/environ | tr '\0' '\n'
可以使用等号操作符为变量赋值:
varName=value
varName是变量名,value是赋给变量的值。如果value不包含任何空白字符(例如空格),那么就不需要将其放入引号中,否则必须使用单引号或双引号。
注意,
var = value
不同于var=value
。把var=value
写成var = value
是一个常见的错误。两边没有空格的等号是赋值操作符,加上空格的等号表示的是等量关系测试。
在变量名之前加上美元符号($
)可以访问变量的内容。
var="value" # 将"value"赋值给var
echo $var
也可以这样写:
echo ${var}
输出:
value
我们可以在printf
、echo
或其他命令的双引号中引用变量的值:
#!/bin/bash
#filename:variables.sh
fruit=apple
count=5
echo "We have $count ${fruit}(s)"
输出:
We have 5 apple(s)
因为shell使用空白字符来分隔单词,所以我们需要加上一堆花括号来告诉shell这里的变量名是fruit,而不是fruit(s)。
环境变量是从父进程中继承而来的变量。例如环境变量HTTP_PROXY
,它定义了Internet连接应该使用哪个代理服务器。
该环境变量通常被设置为:
HTTP_PROXY=192.168.1.23:3128
export HTTP_PROXY
export命令声明了将由子进程所继承的一个或多个变量。这些变量被导出后,当前shell脚本所执行的任何应用程序都会获得这个变量。shell创建并用到了很多标准环境变量,我们也可以导出自己的环境变量:
例如:PATH变量列出了一系列可供shell搜索特定应用程序的目录。一个典型的PATH变量包含如下内容:
echo $PATH
各目录路径之间以:
分隔。$PATH
通常定义在/etc/environment
、\etc\profile
或~/.bashrc
如果需要再PATH中添加一条新路径,可以使用如下命令:
export PATH="$PATH:/home/bin"
也可以使用
PATH="$PATH:/home/usr/bin"
export PATH
echo $PATH
这样,我们就将/home/user/bin
添加到了PATH中。
另外,还有一些众所周知的环境变量:HOME、PWD、USER、UID、SHELL
等。
使用单引号时,变量就不会被扩展(expand),仍依照原样显示。这意味着
echo '$var'
会显示$var
。
但如果变量$var
已经定义过,那么echo "$var"
会显示出该变量的值;如果没有定义过,则什么都不显示。
shell还有很多内建特性。下面就是其中一些。
lengh=${#var}
var=12345678901234567890
echo ${#var}
20
lengh就是字符串所包含的字符数。
echo $SHELL
echo $0
echo $SHELL
/bin/bash
执行echo $0可以得到同样的输出:
echo $0
bash
if [ $UID -ne 0 ]; then
echo Non root user. Please run as root.
else
echo Root user
fi
注意,[]
实际上是一个命令,必须将其与剩余的字符串用空格隔开。上面脚本也可以写成:
if test $UID -ne 0
then
echo Non root user. Please run as root.
else
echo Root user
fi
root用户的UID是0.
补充:使用if-then
语句和test
命令
① 基本结构化命令if-then语句格式:
if command
then
command
fi
bash shell的if
语句会运行if
后面的那个命令。如果该命令的退出状态码是0(该命令成功运行),位于then
部分的命令就会被执行。如果该命令退出状态码是其他值,then
部分的命令就不会被执行。
fi
语句用来表示if-then
语句到此结束。
② if-then-else
语句
if command
then
command
else
command
fi
当if语句中的命令返回非零退出状态码时,会执行else部分中的命令。
shell脚本:使用if-then语句和test命令
Shell中的条件判断语句if~then~fi
(username@hostname:~$)
user@hostname:/home/$
的提示字符串。不同的GNU/Linux发布版中的提示字符串及颜色各不相同。我们可以利用PS1
环境变量来定义主提示字符串。默认的提示字符串是在文件~/.bashrc
中的某一行设置的。查看设置变量PS1的那一行:
cat ~/.bashrc | grep PS1
如果要修改提示字符串,可以输入:
PS1="PROMPT" # 提示字符串已经改变
PROMOT> Type commands here.
我们可以利用类似于\e[1;31
的特定转义序列来设置彩色的提示字符串(参考1.2节内容)。
还有一些特殊的字符串可以扩展成系统参数。例如:\u
可以扩展为用户名,\h
可以扩展为主机名,而\w
可以扩展为当前工作目录。
环境变量通常保存了可用于搜索可执行文件,库文件的路径列表。例如$PATH
和$LD_LIBRARY_PATH
,它们通常看起来像这样:
PATH=/usr/bin; /bin
LD_LIBRARY_PATH=/usr/lib; /lib
在我的Ubuntu18.04中,是这样:
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
这意味着只要shell执行应用程序(二进制文件或脚本)时,它就会首先查找/usr/bin
,然后查找/bin
。
当你使用源代码构建并安装程序时,通常需要为新的可执行文件和库文件添加特定的路径。假设我们要将myapp安装到/opt/myapp
,它的二进制文件在/opt/myapp/bin
目录中,库文件在/opt/myapp/lib
目录中。
这个例子展示了如何将新的路径添加到环境变量的起始部分。第一个例子利用我们目前所讲过的知识来实现。第二个例子创建了一个函数来简化修改操作。
export PATH=/opt/myapp/bin:$PATH
PATH和LD_LIBRARY_PATH现在看起来像这样:
PATH=/opt/myapp/bin:/usr/bin:/bin
LD_LIBRARY_PATH=/opt/myapp/lib:/usr/lib; /lib
我们可以在.bashrc文件中定义如下函数,简化路径添加操作:
prepend() { [ -d "$2" ] && eval $1=\"$2':'\$$1\" && export $1; }
该函数用法如下:
prepend PATH /opt/myapp/bin
prepend LD_LIBRARY_PATH /opt/myapp/lib
函数prepend()
首先确认该函数第二个参数所指定的目录是否存在。如果存在,eval
表达式将第一个参数所指定的变量值设置成第二个参数的值加上:
(路径分隔符)。随后再跟上第一个数的原始值。
在进行添加时,如果变量为空,则会在末尾留下一个:
。要解决这个问题,可以对该函数再进行一些修改:
prepend() { [ -d "$2" ] && eval $1=\"$2\$\{$1:+':'\$$1}\" && export $1 ; }
在这个函数中,我们引入了一种shell参数扩展形式:
${parameter:+expression}
如果parameter有值且不为空,则使用expression的值。
通过这次修改,在向环境变量中添加新路径时,当且仅当旧值存在,才会增加:
。