小土刀

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 777 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 可以查看命令行的历史
  • 输入 !$ 查看最后输入的参数
  • 输入 !! 查看上一条命令
  • 输入 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 可以用 echo $SHELLecho $0
  • 检测是否是超级用户可以通过 $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 以上,这里暂时略过。

数学运算

通常我们使用 let, (( ))[ ] 来执行基本的算术操作,使用 exprbr 来进行高级操作。老规矩,先看脚本代码,在 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 之后变量名不需要加 $
  • [] 中的变量可以加 $,也可以不加
  • letexpr 仅支持整数
  • bc 支持浮点数,可以通过 stdin 来传给 bc(就是 | 符号)

重定向

这部分我们需要记住三个数字

  • 0 - stdin 标准输入
  • 1 - stdout 标准输出
  • 2 - stderr 标准错误

重定向输出所用符号是 >>>,其中 > 会清空文件,而 >> 则是追加。那前面的三个数字在哪里呢?其实 > 相当于 1>>> 相当于 1>>

然后我们来看看标准错误输出,一般来说有两种处理方式,一种是和标准输出重定向到一起,那么通过 command > output.txt 2>&1cmd &> 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 不等于
  • -a 逻辑与,例如 [ $var -ne 0 -a $var2 -gt 2 ]
  • -o 逻辑或,例如 [ $var -ne 0 -o $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

循环

设定 IFS 变量即告诉 shell 脚本要以什么为分隔符,这里的 IFS 表示 Internal Field Separator。我们来看看下面一段脚本

data="class1,class2,class3,class4"
IFS=,
for item in $data;
do
echo Item: $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
}

另外的循环有 whileuntil,其实是类似的,这里只给出格式

# 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 post="hexo g -d"。这之后我就可以直接用 blogpost 命令了,非常方便。

如果想要在每次打开新的 shell 进程时都能够使用这两个『新』命令,那么可以把前面两句写在 ~/.bashrc~/.bash_profile 中。

管道

Shell 脚本最强大的地方就是能够通过管道把不同的应用程序串起来,这样每个都完成一小部分工作,最终完成一个看起来很复杂的任务。大概的样子是这样的 $ command1 | command2 | command3

大家可以尝试一下这条命令 ls | cat -n

小结

本文我们从打印文本开始,见到了解了最基本的 Shell 脚本语法和 Bash 的基本操作。之后的系列文章会继续深入下去,用更多例子来进行讲解。

参考链接与资源

捧个钱场?

热评文章