Shell 入门指南
本页目录
Shell 使得 Linux/Unix 程序员能够通过脚本语言与操作系统的核心功能进行交互,配合各类不同的应用可以组合出各种神奇的套路。这里会以 Bash 为例,用系列文章跟大家分享 Shell 的洪荒之力。
更新记录
- 2016.08.02: 初稿
本指南需要读者有一定 Linux 基础,会尽可能把各个概念都说明白,但是说到底编程是一门手艺,手艺就需要去探索和练习,所以遇到问题先自己想想怎么办,不行再寻求帮助。
Hello World
我们先来看一个最简单的 Shell 程序:
#!/bin/bash
echo "Hello World! This is wdxtub."
# 显示时间
echo "Today is: $(date)"
把上面的代码保存到 hw.sh 中,然后在终端中进入包含 hw.sh 的文件夹中,用 chmod u+x hw.sh 添加执行权限,最后用 ./hw.sh 执行。应该能看到如下输出:
$ ./hw.sh # < 这句表示执行该 Shell 脚本
Hello World! This is wdxtub. # < 之后是执行的结果
Today is: Tue Aug 2 11:57:24 CST 2016
可能第一行的 #!/bin/bash 就让人迷糊了,没事,我现在就来解释。第一行叫 shebang,用来告知系统如何执行该脚本,这句话的意思就是说要用 /bin/bash 这个程序来跑这段代码。有意思的是,这一句也可以带参数,如果我们想开启调试功能,可以把第一句改为 #!/bin/bash -xv 即可
我们还能学到的东西是:
- 用
#表示这个符号之后同一行是注释 - 每一句指令用换行或分号隔开
- 可以用
echo命令来输出字符串,默认添加一个换行符 - 可以用
date命令来获取当前的时间 - 如果想在一条指令里插入另一条指令,可以把另一条指令用
$()包住(就像date那样)
很简单对不对,没错,实际上 Shell 程序就是把脚本放到一起来执行而已,如果对 Linux 命令有比较清晰的了解的话,简直易如反掌。不过,这只是基础而已,还有很多 shell 的『魔法』需要我们去掌握。
如果是纯新手怎么办呢?没事,我也有准备,可以在命令行中输入 man bash 或者访问这里查看英文/中文版的使用说明。至于为什么这里只介绍 bash 呢?因为大部分 Linux 环境默认是用 bash 的,比方说各类云服务提供商开出来的虚拟机,如果不想每台机都重新去配置的话,直接学好 bash 是完全够用的。同理,了解 vim 和 tmux 的默认配置也很重要,毕竟不可能用一台机就先去配一堆自定义环境和快捷键吧(别人也需要用呢)
Bash 基本操作
作为执行 shell 脚本的程序,bash 的交互式版本有的时候甚至比 shell 本身还更常用,毕竟登录到远程服务器,面对的就是一个闪动的光标,也就是 bash 了。我们就先来了解一下 bash 基本的操作,这样在测试命令的时候就会方便许多。
所谓操作,其实就是各类快捷键,这里挑些常用的介绍一下,一句话:熟能生巧(这里为了显示的时候好看,字母都用大写,实际上操作的时候,用小写即可)
Tab能够自动补全,从命令名称到文件夹到路径,多按两下还能出个列表方便我们输入,是居家旅行必备操作,如果只能记住一个快捷键,那么记住Tab即可Ctrl+R用来搜索,按下之后就可以输入搜索的关键词,再按一次Ctrl+R可以切换到下一个匹配的结果,如果找到合适的,按下Enter就可以执行,按下→会把查询结果放到当前行,我们可以进行编辑Ctrl+W删除该行最后一个单词,至于怎么定义一个单词呢?常见的分隔符有空格,_,,,:,-等等(基本上不是字母和数字就可以认为是分隔符)Ctrl+U删除整行Ctrl+A将光标移到行首Ctrl+E将光标移到行尾Ctrl+K删除从光标处到行尾的所有内容Ctrl+L清屏- 输入
history可以查看命令行的历史 !$会展开上一条命令的最后一个参数,回车后会作为命令的一部分执行!!会展开上一条命令,直接回车会再次执行;只查看历史请用history- 输入
cd -可以回到上一个工作路径 - 如果想要设定 bash 的提示文本,需要修改
~/.bashrc中的PS1环境变量
好!了解了这些我们就可以继续旅程了,我们就从前面出现的 echo 命令说起吧!
热身:打印文本
echo
前面我们输出字符串时,用的命令是 echo "Hello World! This is wdxtub.",其中字符串是用双引号包住的,但其实不带双引号,或者用单引号也可以完成这样的效果,比如:
$ echo Hello World
Hello World
$ echo 'Hello World'
Hello World
那么问题来了,这三种用什么区别呢?
- 不带引号的
echo没办法显示;,因为分号是用来分隔命令的 - 使用单引号
echo不会对命令中的变量求值,只按照原样显示(变量会在下一节说明,这里简单有个概念即可) - 使用双引号
echo有些值需要进行转义(使用\),比方说感叹号
printf
除了 echo 命令,我们其实还可以用 printf 命令来进行格式化输出,不过需要注意的是这里我们需要自己添加换行符,我们在前面的脚本文件中添加如下三行:
printf "%-5s %-10s %4s\n" No. Name Score
printf "%-5s %-10s %4.2f\n" 1 wdxtub 99.9999
printf "%-5s %-10s %4.2f\n" 43 dawang 66.6566
然后我们再执行一下,结果是:
$ ./hw.sh
Hello World! This is wdxtub.
Today is: Tue Aug 2 16:58:16 CST 2016
No. Name Score
1 wdxtub 100.00
43 dawang 66.66
这里能学到的套路是什么呢?
%s,%c,%d,%f是格式替代符,就是和 C 语言一样的方式%-5s指明了一个左对齐且宽度为 5 的字符串,如果内容不足 5 个字符,则会以空格填充%4.2f表示保留两位小数可以看到是默认四舍五入的- 最后要加上
\n才能正确换行
cat, head, tail, uniq, cut
打印文本常用的命令还有 cat, head, tail, uniq, cut,这里主要针对文件的输出,我们先创建一个文本文件 sample.txt,其内容为
1 hohoho .yo
1 hohoho .max
2 ohohoh .I
2 ohohoh .I
3 hahaha .want
4 ahahah .to
5 wowwow .see
6 mummum .you
接下来给出几个简单的实例,具体命令的用法,可以使用 man command 来进行查询,或者用 TLDR 命令来查询(我更推荐后者)
- 打印文件内容
cat sample.txt - 打印后 4 行
tail -n 4 sample.txt - 打印头 4 行
head -n 4 sample.txt - 合并相邻重复的行
uniq sample.txt - 显示相邻重复的行
uniq -d sample.txt - 打印每行
.之前的内容cut -d '.' -f 1 sample.txt
基础:表达式
如果只是执行单行命令,其实很难完全发挥『脚本』的威力,毕竟直接在命令行里输入也是可以的嘛,接下来我们就看看如何像编程一样写脚本。
变量
首先要了解的是环境变量,我们可以通过 env 命令查看系统当前的环境变量,也就是那些不需要在脚本中显式声明就可以直接使用的变量。和之前一样,我们先来看一个例子,在之前的脚本中加入下面几行:
#export 设置环境变量,在该 shell 脚本执行阶段有效
export MY_WEB=http://www.wdxtub.com
echo "My home is $HOME"
web="wdxtub.com"
echo "My website is ${web}"
echo 'This is not $MY_WEB'
echo ${web/com/cn}
echo "上一个程序的返回值: $?"
echo "脚本的 PID: $$"
echo "参数个数: $#"
echo "脚本参数: $@"
echo "分隔好的脚本参数 第一个:$1 第二个:$2"
我们执行的时候带两个参数试试看 ./hw.sh hello world,结果为
$ ./hw.sh hello world
#前面部分省略
#..........
My home is /Users/dawang
My website is wdxtub.com
This is not $MY_WEB
wdxtub.cn
上一个程序的返回值: 0
脚本的 PID: 9782
参数个数: 2
脚本参数: hello world
分隔好的脚本参数 第一个:hello 第二个:world
- 变量声明
web="wdxtub.com"即可 - 使用变量的时候需要加上
$或者${},我更推荐后者,看起来更清晰 - 单引号不会展开变量,参考
echo 'This is not $MY_WEB' - 在变量内部进行字符串替换
echo ${web/com/cn}就可以把原来的wdxtub.com中的com换成cn - 内置的参数很好用,具体可以参考脚本中的内容(已经很详细)
- 环境变量中比较常用的有
HOME,PWD,USER,UID,SHELL
下面再给出一些常见的用法
- 获得字符串长度可以在变量前加
# $SHELL通常表示登录 shell,不保证是当前解释器;$0是当前 shell 或脚本的调用名- 检测是否是超级用户可以通过
$UID的值判断,如果是超级用户,UID 应为 0
数组
Bash 支持普通数组(下标是整数)和关联数组(下标是字符串,类似 Map),我们直接上脚本代码
# 定义数组
iarray=(1 2 3 4 5 6 7)
iiarray=("one" "two" "three")
echo ${iarray[0]}
echo ${iiarray[2]}
# 打印所有值
echo ${iiarray[*]}
# 还是打印所有值
echo ${iiarray[@]}
打印数组长度只需要在数组变量前加 # 符号。关联数组因为需要 Bash 4.0 以上,这里暂时略过。
遍历数组时使用 "${array[@]}",可保留每个元素中的空格;读取长度使用 ${#array[@]}。普通变量展开也应优先加双引号,避免意外分词和通配符扩展。
数学运算
通常我们使用 let、(( )) 和 $(( )) 来执行基本的算术操作,使用 expr 和 bc 来进行高级操作。老规矩,先看脚本代码,在 hw.sh 中添加下面几行
class1=33
class2=44
let total=class1+class2
echo $total
let class1++
let class2--
total=$((class1 + class2))
echo $total
echo "4 * 3.45" | bc
执行结果为
$ ./hw.sh hello world
# 前面部分省略
# ..........
77
77
13.80
这里我们可以简单了解
let之后变量名不需要加$- 算术表达式中的变量可以加
$,也可以不加 let及expr仅支持整数bc支持浮点数,可以通过stdin来传给bc(就是|符号)
重定向
这部分我们需要记住三个数字
- 0 - stdin 标准输入
- 1 - stdout 标准输出
- 2 - stderr 标准错误
重定向输出所用符号是 > 和 >>,其中 > 会清空文件,而 >> 则是追加。那前面的三个数字在哪里呢?其实 > 相当于 1>,>> 相当于 1>>
然后我们来看看标准错误输出,一般来说有两种处理方式,一种是和标准输出重定向到一起,那么通过 command > output.txt 2>&1 或 cmd &> output.txt 即可;另一种则是分开两个文件,那么使用 command 2>stderr.txt 1>stdout.txt 即可;如果不想保存任何标准错误输出(甚至都不想在终端中见到),可以 command 2>/dev/null,这里的 /dev/null 是一个特殊的设备文件,接收到的任何数据都会被丢弃。
标准输入的用法和输出类似,这里不再赘述。另外如果需要把标准输出通过管道运到两个不同的地方时(| 只能重定向到一个地方),可以使用 tee 命令,不过这里暂时略过,留到之后的中级教程中介绍。
条件流程
比较常用的就是 if 的套路,格式为
if condition;
then
commands;
elif condition;
then
commands;
else
commands
fi
如果觉得这样写太长,可以利用逻辑操作与短路原理来进行编写
# 如果 condition 为真,则执行 action
[ condition ] && action;
# 如果 condition 为假,则执行 action
[ condition ] || action;
算术比较
条件通常被放置在封闭的中括号内,一定要注意 [ 和 ] 两边有空格!如果我们需要进行算术比较,就需要使用如下的比较符号
-gt大于-lt小于-ge大于等于-le小于等于-eq等于-ne不等于- 逻辑与:
[ "$var" -ne 0 ] && [ "$var2" -gt 2 ] - 逻辑或:
[ "$var" -ne 0 ] || [ "$var2" -gt 2 ]
一个实际的例子:检测是否是超级用户
if [ $UID -ne 0 ]; then
echo "你不是超级用户(root),请以 root 身份运行"
else
echo "当前是超级用户"
fi
文件系统测试
有的时候我们可能需要对不同的文件进行一定的判断,下面是可以使用的比较方法:
[ -f "$file_var" ]如果给定的变量包含正常的文件路径或文件名,则返回真[ -x "$var" ]如果给定的变量包含的文件可执行,则返回真[ -d "$var" ]如果给定的变量包含的是目录,则返回真[ -e "$var" ]如果给定的变量包含的文件存在,则返回真[ -c "$var" ]如果给定的变量包含的是一个字符设备文件,则返回真[ -b "$var" ]如果给定的变量包含的是一个块设备文件的路径,则返回真[ -w "$var" ]如果给定的变量包含的文件可写,则返回真[ -r "$var" ]如果给定的变量包含的文件可读,则返回真[ -L "$var" ]如果给定的变量包含的是一个符号链接,则返回真
字符串比较
比较字符串的时候最好使用双中括号,比较的语法为:
[[ $str1 = "$str2" ]]文本一样时返回真,注意=前后都必须有一个空格[[ $str1 == "$str2" ]]文本一样时返回真,和上一个一样,只是写法不同[[ $str1 != "$str2" ]]文本不一样时返回真[[ $str1 > $str2 ]]如果 str1 的字母序比 str2 大,则返回真[[ $str1 < $str2 ]]如果 str1 的字母序比 str2 小,则返回真[[ -z $str1 ]]如果 str1 是空字符串,则返回真[[ -n $str1 ]]如果 str1 是非空字符串,则返回真
可以通过 && 和 || 来组合多个条件。如果不想写太多的方括号,那么可以用 test 命令,比如 [ "$var" -eq 0 ] 等价于 test "$var" -eq 0
循环
在 read 命令前临时设定 IFS,可以指定本次读取的字段分隔符,避免影响后面的脚本,这里的 IFS 表示 Internal Field Separator。我们来看看下面一段脚本
data="class1,class2,class3,class4"
IFS=, read -r -a items <<< "$data"
for item in "${items[@]}";
do
printf 'Item: %s\n' "$item"
done
输出为
Item: class1
Item: class2
Item: class3
Item: class4
这里因为指定了分隔符,所以会把字符串用 ,,分隔,然后就可以利用循环来输出了。前面的例子中我们看到了 for 循环,这里我们再来看一段 for 循环的用法:
for i in {a..z}
do
echo $i
done
这样就可以输出 a-z 了,当然也可以换成其他的,比如 {1..43}, {a..h}, {A..K} 等等
还可以使用类似 C 语言的 for 循环,比如
for ((i=0; i<10; i++))
{
echo $i
}
另外的循环有 while 和 until,其实是类似的,这里只给出格式
# while 循环
while condition # 如果 condition 为 true 则是无限循环
do
commands
done
# until 循环
until condition
do
action
done
函数
函数的定义和非常简单,比如
function foo()
{
echo "Arguments work just like script arguments: $@"
echo "And: $1 $2..."
echo "This is a function"
return 0
}
也可以省略 function 关键词,比如
bar ()
{
echo "Another way to declare functions!"
return 0
}
执行的话,直接输入函数名即可,比如
foo
bar
foo hello world
进阶:快捷操作
别名
简单来说,别名类似于网上很流行的短链接,只不过我们可以具体设定,比方说我每次写博客都需要进入到某个文件夹,那么我可以这么写 alias blog="cd ~/Documents/Blog",我想要快速查看目录时,可以写 alias ll="ls -al"。这之后我就可以直接用 blog 和 ll 命令了,非常方便。
如果想要在每次打开新的 shell 进程时都能够使用这两个『新』命令,那么可以把前面两句写在 ~/.bashrc 或 ~/.bash_profile 中。
管道
Shell 脚本最强大的地方就是能够通过管道把不同的应用程序串起来,这样每个都完成一小部分工作,最终完成一个看起来很复杂的任务。大概的样子是这样的 $ command1 | command2 | command3
大家可以尝试一下这条命令 ls | cat -n
小结
本文我们从打印文本开始,见到了解了最基本的 Shell 脚本语法和 Bash 的基本操作。之后的系列文章会继续深入下去,用更多例子来进行讲解。
参考链接与资源
语法与边界行为参考 GNU Bash 手册。本文使用 Bash 扩展,不应直接改用 sh 执行。
补充:参数边界与失败状态
传递全部脚本参数时使用 "$@",它将每个参数分别保留;"$*" 会把参数合成一个字符串。参数中可能有空格时,这个区别尤其重要。
if 判断的是命令退出状态,0 通常表示成功,非零的具体含义由程序决定。$? 应立即读取,下一条命令会覆盖它。管道默认使用最后一个命令的状态,Bash 的 set -o pipefail 可以让非末尾命令的失败反映到管道状态中;关键步骤仍应明确处理失败。
脚本使用相对路径时,默认相对于调用时的工作目录,不自动相对于脚本文件。部署前应写清楚解释器、工作目录和外部命令依赖。完整规则见 GNU Bash 手册。
相关文章
- Linux 实战:归档、磁盘与文本命令2016-09-28
- Crontab 指南:周期任务与执行环境2016-07-26
- Linux 服务器检查指南2016-09-28
- Tmux 与 Byobu:终端中的窗口管理2016-03-30