Bash 老司机也可能忽视的 10 大编程细节

AI 研习社按:Bash,作为大部分 Linux 发行版的出厂预设 Shell,因其晦涩难懂的语法设置,以及需要特别留心的编程细节,几乎成为 Linux 区别于其他操作系统的代名词。针对 Bash 中一些极容易出错的细节,我们在这里总结了 10 条编程注意事项,希望对各位泛 Linux 环境的开发者有所裨益。原文来自一位名叫 Julia Evans 的开发者博客,AI 研习社编译。

作为一名 Bash 脚本编写经验超过 10 年的老程序员,我通常不用 Bash 处理复杂的编程任务。但作为一款我们在日常 Linux 使用中几乎无法避免的通用工具,Bash 的确有许多与我们习以为常的 C++ 和 Java 等高级语言非常不同的基础特性。在这里我并不打算讨论 Bash 编程的高阶应用,而是仅仅针对 Bash 中那些与众不同的基础特性做一简单梳理和汇总。希望对各位有所帮助。

当然,如果你对阅读博客不感兴趣,这里我再顺便推荐两个开源免费的小工具。一个是 Shell 语法检查工具 shellcheck,可以在运行前对脚本进行全面的语法检查;另一个是 shfmt,可以自动对写好的 Shell 脚本按照要求格式化。

shellcheck 地址:https://www.shellcheck.net/ (点击“阅读原文”跳转获取超链接)

shfmt 地址:https://github.com/mvdan/sh

1. 等号两边慎用空格

Bash 中的赋值语句通常都是这样的:

VARIABLE=2

然后我们通过 $VARIABLE 引用该变量。这里有一点非常重要,也极容易忽视的就是:千万不要在等号两边加空格。虽然加上空格也不会引起语法错误,但很可能造成意想不到的结果。例如 VARIABLE= 2 这个语句,解释器很可能会将一个空字符串赋值给 VARIABLE,然后运行一个名字叫 2 的脚本。

一般常用的 Bash 变量都是字符串,我很少见到有数组的。另外,虽然解释器也接受小写,但 Bash 中默认是将变量名全部大写的。

2. 用 ${} 限定变量名

例如我定义了一个变量 MYVAR,内容是字符串“file.txt”,然后想执行如下命令:

mv $MYVAR $MYVAR__bak # wrong!

结果一定会报错。因为解释器会搜索 MYVAR__bak 这个变量,而我们根本没有定义。因此,为了避免出现类似问题,最好的办法是每次引用时都在变量两边加上括号,就像这样:

mv ${MYVAR} ${MYVAR}__bak # right!

3. 区分全局变量、局部变量和环境变量

Bash 有三种变量:全局变量、局部变量和环境变量。其中最常用的是环境变量。

实际上每个 Linux 进程都有许多预设的环境变量(运行 env 命令可查看),Bash 中对环境的变量的应用非常简单。例如,想要查看 MYVAR 环境变量的值,可以运行下面这条命令:

echo "$MYVAR"

想要设置环境变量,可以用这条命令:

export MYVAR=2

需要注意的是,一旦在进程中设置了环境变量,那么这个环境变量会在所有与其相关的子进程中生效,例如下面这个例子:

export MYVAR=2; python test.py

$MYVAR 环境变量也会在 test.py 脚本中生效。

另一种是全局变量,如下所示这样的赋值语句实际上就是在定义全局变量:

MYVAR=2

全局变量就像其他编程语言一样,会在整个代码中生效。

最后一种是局部变量,这种变量通常只在一个循环语句或者 Bash 函数中有效。一般不常用。

█ 4. 活用命令替换

通常我会用下面这段 for 循环打印输出 1-10 这 10 个数字。

for i in `seq 1 10` # you can use {1..10} instead of `seq 1 10` do echo "$i" done

如果把这些代码写到一行里,是这样的:

for i in `seq 1 10`; do echo $i; done

这里我想强调的是,通过反引号(即键盘上Tab键上方的按键,注意不是单引号)将 seq 命令的输出结果,嵌入了 for 循环中直接使用。通过类似这种命令替换的方式,我们可以大大减少代码冗余,同时减少代码的出错几率。常见的替换方式有如下两种:

OUTPUT=`command` # or OUTPUT=$(command)

█ 5. if 的注意事项

if 语句的判定条件同时支持单中括号([])和双中括号([[]]),他们都可以用来隔离表达式和 if 关键词。但这里推荐使用双中括号,因为它的容错率更高,而且支持更多功能。另外,在 Linux 中单中括号 [ 实际与 test 命令是等价的,因此用双括号显然能避免更多的麻烦。

例如下面这段代码:

If [[ -e /tmp/awesome.txt ]]; then echo "awesome" fi

可以判断 awesome.txt 文件是否存在。

再比如下面的场景:

$ [ 3 < 4 ] && echo "true" bash: 4: No such file or directory $ [[ 3 < 4 ]] && echo "true" true

使用单中括号会报错,但双中括号就没问题。

除了使用双中括号之外,还可以用 test 命令的运行结果作为 if 语句的判断条件,例如:

test -e /tmp/awesome.txt

如果 awesome.txt 文件存在,则命令返回 0,否则返回错误码。

实际上,除了常见的 test 命令,所有返回固定数值的命令都可以作为 if 语句的判断条件。例如下面的代码:

if grep peanuts food-list.txt then echo "allergy allert!" fi

利用 grep 搜索关键词,然后根据结果打印警告信息。

█ 6. 使用函数

在 Bash 中定义和使用函数非常简单(特别是无参函数)。例如:

my_function () { echo "This is a function"; } my_function # calls the function

代码中定义了一个 my_function 函数,调用时也只需要写函数名。

█ 7. 用双引号引用变量

前面第 2 条提到要用 ${} 限定变量名的范围,这里要说的是利用引号限定变量值的范围。

例如下面代码:

X="i am awesome" Y="i are awesome" if [ $X = $Y ]; then echo awesome fi

实际上会报错,因为解释器会将 if 语句的判定条件理解为:

if [ i am awesome == i are awesome ]

为了避免这种错误,就必须用双引号限定变量值的范围。

X="i am awesome" Y="i are awesome" if [ "$X" = "$Y" ]; then # i put quotes because i know bash will betray me otherwise echo awesome fi

这样写就没问题了。当然,如果变量值不包括空格,那不带引号也能得到同样的结果,但毕竟带上双引号会让程序更可靠。

█ 8. 关于返回值

每一个 Linux 程序都有返回值,按照规范,这个返回值在 0-127 之间,0 表示成功,其他值是含义各不相同的错误码。在 Bash 中充分利用这一点可以增加程序的灵活性。例如:

create_user && make_home_directory

这条语句,只有 create_user 返回 0 时,才会执行 make_home_directory。

create_user; make_home_directory

则表示无论 create_user 的返回值是什么,都会执行 make_home_directory。

类似的,你也可以通过:

create_user || make_home_directory

表示只有当 create_user 返回非 0 值时,才会执行 make_home_directory。

█ 9. 使用后台任务

在 Bash 中,可以通过在命令后添加 & 符号实现后台多任务。例如:

long_running_command &

把进程放入后台后,还可以通过 fg 命令将其切换到前台。如果后台命令过多,可以先通过 jobs 命令查看进程的 job ID,然后用 fg+job ID 的方式将指定的后台进程切换到前台。

另外,还可以通过 wait 命令控制多任务的执行顺序。例如:

long_running_command1 wait long_running_command2

表示在命令 1 执行结束后才执行命令 2。

10. 活用 set 命令

在其他语言中,通常遇到错误的语句时,编译器就会报错并停止运行,但 Bash 不会。例如下面的代码:

python non_existant_file.py echo "done"

无论 non_existant_file.py 脚本是否存在,Bash 都会打印输出 done。因此为了保证代码的安全性和正确性,我们可以在代码中用

set -e

对 Bash 环境进行一些额外设置,-e 表示出现错误就停止。

类似的,在其他语言中,使用没初始化的变量也会报错,但 Bash 不会。例如下面的代码:

rm -rf "$DIRECTORY/*"

如果 $DIRECTORY 没有提前初始化,Bash 也并不会停下来,而是直接以空字符串对待,那么这句命令的含义就变成了:尝试删除根目录下的所有文件,结果将非常严重。

这时就可以用

set -u

表示 Bash 不执行未定义的变量。

除了 -e 和 -u 之外,还有

set -x

表示每条命令执行之前必须先打印命令内容。此外还可以通过 set -o 显示所有可以设置的选项。

这也是为什么许多 shell 脚本都以 set -eu 或者 set -eux 等做为开头的原因,因为这样就可以让脚本运行在更安全的环境下。

来源:jvns.ca

原文发布于微信公众号 - AI研习社(okweiwu)

原文发表时间:2017-03-29

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏天天

$.ajax()

80550
来自专栏Linux Python 加油站

Linux学习----文本三剑客——sed(马哥教育原创)

我以前的文章介绍过grep了,今天我就来说一下第二个sed,它是stream editor的缩写。在Linux的文本文件中文本存储都是一行,显示时表现的多行其实...

17230
来自专栏Micro_awake web

Less学习

1st less介绍: 什么是less? less是一个CSS预处理器,可以为网站启用可自定义、可管理、可重用的样式表。它是一种动态样式表语言(第一个版本是ru...

22580
来自专栏轻扬小栈

Vim 常用快捷键及键盘图

23330
来自专栏专注 Java 基础分享

Java --- JSP2新特性

     自从03年发布了jsp2.0之后,新增了一些额外的特性,这些特性使得动态网页设计变得更加容易。jsp2.0以后的版本统称jsp2。主要的新增特性有如下...

22390
来自专栏阮一峰的网络日志

关于URL编码

一般来说,URL只能使用英文字母、阿拉伯数字和某些标点符号,不能使用其他文字和符号。比如,世界上有英文字母的网址"http://www.abc.com",但是没...

19630
来自专栏Python

Django——模板层(template)(模板语法、自定义模板过滤器及标签、模板继承)

前言:当我们想在页面上给客户端返回一个当前时间,一些初学者可能会很自然的想到用占位符,字符串拼接来达到我们想要的效果,但是这样做会有一个问题,HTML被直接硬编...

55080
来自专栏前端说吧

vuex - 常用命令学习及用法整理

31890
来自专栏前端迷

前端面试之JavaScript(总结)

基本数据类型:number、string、null、undefined、boolean、symbol -- 栈 引用数据类型:object、array、func...

14320
来自专栏阿炬.NET

Less学习笔记

30070

扫码关注云+社区

领取腾讯云代金券