TS Blog

【译】 使用 Bash Shell 编写脚本的快速指南

May 04, 2017


原文链接:A quick guide to writing scripts using the bash shell

简单的 shell 脚本

一个简单的 shell 脚本只是一点点按顺序执行的命令列表。通常,一个 shell 脚本应该从如下面的一行开始:

#!/bin/bash

这表示脚本应该在 bash shell 中运行,无论用户选择了哪个交互式 shell。这是非常重要的,因为不同 shell 的语法可能有很大差异。

简单的例子

这是一个非常简单的 shell 脚本示例。 它只是运行一些简单的命令:

#!/bin/bash
echo "hello, $USER. I wish to list some files of yours"
echo "listing files in the current directory, $PWD"
ls  # list files

首先,请注意第4行的注释。在一个 bash 脚本中,任何一个 #(除了第一行的 shebang 之外)都被视为注释。 即 shell 解释器会忽略它。而对于人们阅读脚本是有益的。

$USER$PWD变量。这些是由 bash shell 本身定义的标准变量,它们不需要在脚本中定义。请注意,当变量名称在双引号内时,变量是展开的expanded)。展开(expand)是一个非常合适的词:shell 看到字符串 $USER,并用变量的值替换它,然后执行命令。

下面我们来继续讨论变量…

变量

任何编程语言都需要变量。如下定义一个变量:

X="hello"

然后引用它:

$X

更具体地说,$X 用于表示变量 X 的值。 一些要注意的语义:

  • 如果你在 = 标志的两边留下空格,bash就会变得不快乐。 例如,以下内容导致了一个错误:

    X = hello
  • 虽然在我的例子中有引号,但并不总是必需的。 当变量的值包含空格时需要引号。 例如:

    X=hello world # error
    X="hello world" # OK

这是因为 shell 本质上将命令行看作一堆由空格分隔的命令和命令参数。 foo=baris 被认为是一个命令。 foo = bar的问题是 shell 看到由空格分隔的单词 foo,并把它解释为一个命令。 同样,命令 X=hello world 的问题是 shell 将 X=hello 解释为一个命令,而 world 这个词没有任何意义(因为赋值命令不能携带参数)。

单引号与双引号

基本上,变量名称只在双引号内展开,单引号里不展开。如果不需要引用变量,单引号很好用,因为结果更可预测。

一个例子:

#!/bin/bash
echo -n '$USER=' # -n option stops echo from breaking the line
echo "$USER"
echo "\$USER=$USER"  # this does the same thing as the first two lines

输出看起来像这样(假设你的用户名是 elflord):

$USER=elflord

$USER=elflord

双引号更灵活,但是可预测性较低。如果可以在两者之间选择的话,使用单引号。

使用引号括起变量

有时,使用双引号保护变量名是个好主意。 如果您的变量值包含空格或是空字符串,则这是很重要的。 一个例子如下:

#!/bin/bash
X=""
if [ -n $X ]; then  # -n tests to see if the argument is non empty
  echo "the variable X is not the empty string"
fi

这段脚本会输出:the variable X is not the empty string。 因为 shell 将 $X 展开为空字符串。 表达式 [ -n ] 返回 true(因为它没有提供参数)。 一个更好的脚本将是:

#!/bin/bash
X=""
if [ -n "$X" ]; then  # -n tests to see if the argument is non empty
  echo "the variable X is not the empty string"
fi

在这个例子中,表达式展开为 [ -n "" ],返回 false。因为用双引号括起来的字符串显然是空的。

变量展开实战

只是为了说服你,shell 真的像我之前提到的那样在 “展开” 变量,这里是一个例子:

#!/bin/bash
LS="ls"
LS_FLAGS="-al"

$LS $LS_FLAGS $HOME

这看起来有点神秘。 最后一行会发生什么,它实际上是执行命令 ls -al /home/elflord(假设 /home/elflord 是你的主目录)。 也就是说,shell 只是用它们的值替换变量,然后执行命令。

使用大括号来保护变量

好了,这里有一个潜在的问题。 假设要 echo 变量 X 的值,紧接着是字母 abc。 问题:你怎么做的? 我们来试一试:

#!/bin/bash
X=ABC
echo "$Xabc"

这样没有得到输出。哪里出了错?答案是,shell 认为我们引用的是未初始化的变量 Xabc。处理这个问题的方法是把大括号放在 X 上以将其与其他字符分开。以下给出了期望的结果:

#!/bin/bash
X=ABC
echo "${X}abc"

条件语句

有时需要检查某些条件。一个字符串是否有0个长度?文件 “foo” 是否存在,它是一个符号链接还是一个真实的文件?首先,我们使用 if 命令来运行测试。 语法如下:

if condition
then
  statement1
  statement2
  ..........
fi

有时您可能希望在条件失败时指定备用操作。这是如何做的:

if condition
then
  statement1
  statement2
  ..........
else
  statement3
fi

或者,如果第一个 if 失败,则可以测试另一个条件。 请注意,可以添加任何数量的 elif

if condition1
then
  statement1
  statement2
  ..........
elif condition2
then
  statement3
  statement4
  ........    
elif condition3
then
  statement5
  statement6
  ........    

fi

如果相应的条件为真,则 if/elif 和下一个 eliffi 之间的块内的语句将被执行。 实际上,任何命令都可以替代条件,并且当且仅当命令返回退出状态为0(换句话说,如果命令退出“成功”),则该块将被执行。 但是,在本文档中,我们只会使用 test[ ] 来测试条件。

测试命令和操作符

几乎所有的条件语句使用的命令都是测试命令。 测试返回 truefalse(更准确地说,退出 0 或非零状态),这取决于测试是通过还是失败。 它大概如下工作:

test operand1 operator operand2

对于某些测试,只需要一个操作数(operand2)测试命令通常以下列形式缩写:

[ operator operand2 ]

让讨论回到现实,我们举几个例子:

#!/bin/bash
X=3
Y=4
empty_string=""
if [ $X -lt $Y ]  # is $X less than $Y ?
then
  echo "\$X=${X}, which is smaller than \$Y=${Y}"
fi

if [ -n "$empty_string" ]; then
  echo "empty string is non_empty"
fi

if [ -e "${HOME}/.fvwmrc" ]; then       # test to see if ~/.fvwmrc exists
  echo "you have a .fvwmrc file"
  if [ -L "${HOME}/.fvwmrc" ]; then     # is it a symlink ?  
    echo "it's a symbolic link
  elif [ -f "${HOME}/.fvwmrc" ]; then   # is it a regular file ?
    echo "it's a regular file"
  fi
else
  echo "you have no .fvwmrc file"
fi

值得注意的一些陷阱

测试命令需要以“operand1 operator operand2”或“operator operand2”的形式,换句话说,您真的需要这些空格,因为 shell 认为第一个不包含空格的块是运算符(如果以 - 开头)或操作数。例如:

if [ 1=2 ]; then
  echo "hello"
fi

以上会给出准确的 “错误” 输出(即 echo "hello",因为 shell 看到一个操作数,但没有操作符)。

另一个潜在的陷阱来自于不保护引号中的变量。 我们已经给出了一个例子,说明为什么你必须用引号括起你想要使用在 -n 测试中的操作数。而且,在大部分时候都有很多足够好的理由来使用引号。当您在测试中展开变量时,无法执行此操作可能会导致非常严重的错误。 以下是一个例子:

#!/bin/bash
X="-n"
Y=""
if [ $X = $Y ] ; then
  echo "X=Y"
fi

这将导致错误输出,因为 shell 将我们的表达式展开为[ -n = ],字符串 “=” 具有非零长度。

测试操作符的简要总结

以下是测试运算符的快速列表。 这不是全面的,但它可能是所有你需要记住的(如果你需要任何其他的,你可以随时检查 bash 手册页…)

operator produces true if… number of operands
-n operand non zero length 1
-z operand has zero length 1
-d there exists a directory whose name is operand 1
-f there exists a file whose name is operand 1
-eq the operands are integers and they are equal 2
-neq the opposite of -eq 2
= the operands are equal (as strings) 2
!= opposite of = 2
-lt operand1 is strictly less than operand2 (both operands should be integers) 2
-gt operand1 is strictly greater than operand2 (both operands should be integers) 2
-ge operand1 is greater than or equal to operand2 (both operands should be integers) 2
-le operand1 is less than or equal to operand2 (both operands should be integers) 2

循环

循环是使得人们可以重复一个过程或对几个不同的项目执行相同的过程的结构。 在bash中有以下种类的循环可用:

  • for 循环
  • while 循环

for 循环

for 循环的语法最适合通过示例描述:

#!/bin/bash
for X in red green blue
do
  echo $X
done

for 循环遍历空白分隔的项。 请注意,如果某些项具有嵌入空白,则需要使用引号保护它们。 以下是一个例子:

#!/bin/bash
colour1="red"
colour2="light blue"
colour3="dark green"
for X in "$colour1" $colour2" $colour3"
do
  echo $X
done

你能猜测如果我们在 for 语句中省略引号,会发生什么? 这表明变量名应该用引号保护,除非你确定它们不包含任何空格。

for 循环中的 glob

shell 将包含 * 的字符串扩展为“匹配”的所有文件名。当且仅当与匹配字符串相同时,文件名匹配,用任意字符串替换星号 * 后。 例如,字符 * 本身扩展到工作目录中所有文件的空格分隔列表(不包括以点开头的 .)。所以:

  • echo * 列出当前目录中的所有文件和目录
  • echo *.jpg 列出所有 jpeg 文件
  • echo ${HOME}/public_html/*.jpg 列出您的 public_html 目录中的所有 jpeg 文件

正因为如此,这对于对目录中的文件执行操作非常有用,特别是与for循环一起使用。 例如:

#!/bin/bash
for X in *.html
do
    grep -L '<UL>' "$X"
done

while 循环

当一个给定的条件为真 while 循环进行迭代。 一个例子:

#!/bin/bash
X=0
while [ $X -le 20 ]
do
  echo $X
  X=$((X+1))
done

这提出了一个自然的问题:为什么 bash 不允许 C 语言式的 for 循环 for(X = 1,X <10; X ++)

事实上,for 循环不被鼓励使用,因为:bash 是一种解释性语言,而且它的循环是一个相当缓慢的事情。 因此,不鼓励重复迭代。

命令替换

命令替换是 bash shell 非常方便的功能。 它使您可以获取命令的输出,并将其视为在命令行中写入。 例如,如果要将变量 X 设置为命令的输出,则通过命令替换来执行此操作。

有两种形式的命令替换:括号展开和反向展开。

括号扩展工作如下:$(commands) 展开到命令的输出,允许嵌套。因此命令可以包括括号扩展:

反向扩展将 commands 展开到命令的输出:

给出一个例子:

#!/bin/bash
files="$(ls)"
web_files=`ls public_html`
echo "$files"      # we need the quotes to preserve embedded newlines in $files
echo "$web_files"  # we need the quotes to preserve newlines
X=`expr 3 \* 2 + 4` # expr evaluate arithmatic expressions. man expr for details.
echo "$X"

$() 替代方法的优点是几乎不言而喻:嵌套很容易。 大部分的 bourne shell 可以支持(或 POSIX shell)。但是,反向替换稍微可读性更好,甚至最基本的 shell 也支持(任何 #!/bin/sh 都很好)。

请注意,如果字符串在上述 echo 语句中没有引号保护,换行符将被输出中的空格替换。