Fork me on GitHub

:has() 选择器

:has()选择器允许我们根据子元素来为父元素设置样式。

1
2
div { background: white; }
div:has(img) { background: grey; }

如果div元素内有img,则将其背景更改为灰色。

自定义滚动条

  • div::-webkit-scrollbar滚动条整体部分
  • div::-webkit-scrollbar-thumb滚动条里面的小方块,能向上向下移动或往左往右移动,取决于是垂直滚动条还是水平滚动条
  • div::-webkit-scrollbar-track滚动条的轨道(里面装有 Thumb
  • div::-webkit-scrollbar-button滚动条的轨道的两端按钮,允许通过点击微调小方块的位置
  • div::-webkit-scrollbar-track-piece内层轨道,滚动条中间部分(除去
  • div::-webkit-scrollbar-corner边角,即两个滚动条的交汇处
  • div::-webkit-resizer两个滚动条的交汇处上用于通过拖动调整元素大小的小控件注意此方案有兼容性问题,一般需要隐藏滚动条时我都是用一个色块通过定位盖上去,或者将子级元素调大,父级元素使用overflow-hidden截掉滚动条部分。暴力且直接。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* 设置滚动条的宽度 */

::-webkit-scrollbar{
width: 10px;
}
/* 将轨道改为蓝色并设置圆角边框 */
::-webkit-scrollbar-track{
background-color: blue;
border-radius: 10px;
}
/* 将滑块(显示滚动量)改为灰色并设置圆角 */
::-webkit-scrollbar-thumb{
background: gray;
border-radius: 10px;
}
/* 悬停时显示为深灰色 */
::-webkit-scrollbar-thumb:hover{
background: darkgray;
}

Scroll behavior

滚动行为可以实现平滑滚动,使页面在不同部分之间的过渡更加平滑。

1
2
3
html{
scroll-behavior:smooth;
}

accent-color

改变用户界面的颜色,例如表单控件和复选框。

1
2
3
input{
accent-color: blue;
}

Aspect Ratio

在构建响应式组件时,经常检查高度和宽度可能会令人头疼,因为你必须保持纵横比。有时候视频和图片可能会显得拉伸。

这就是为什么我们可以使用纵横比属性。一旦设置了纵横比值,然后再设置宽度,高度就会自动设置。或者反之亦然。

1
2
3
4
5
6
7
8
9
/* class为example的元素 */
.example{
/* 设置纵横比 */
aspect-ratio: 1 / .25;
/* 设置宽度后,高度会自动设置 */
width: 200px;
/* 边框不是必需的,但这里只是为了看效果而添加的 */
border: solid black 1px;
}

现在,我们设置了宽度,高度将自动设置为50px,以保持纵横比。

position: sticky

当视口到达定义的位置时,元素会粘在那里。

1
2
3
4
.some-component{
position: sticky;
top: 0;
}

sticky定位有两个主要部分:

  • 粘性元素——是我们使用position: sticky样式定义的元素。当视口位置与位置定义匹配时,元素将浮动,例如:top: 0px
  • 粘性容器——是包裹粘性项目的 HTML 元素。这是粘性项目可以浮动的最大区域。

当你使用position: sticky定义一个元素时,自动定义了父元素为粘性容器。容器是粘性项目的作用域,项目无法离开其粘性容器。

当一个具有sticky定位样式的元素被包裹起来,并且它是包裹元素内唯一的元素时,这个被定义为sticky定位的元素并不会”粘”住。原因是,当一个元素被赋予sticky定位样式时,粘性元素的容器是粘性元素可以粘住的唯一区域。这个元素没有其他元素可以浮动,因为它只能浮动在兄弟元素上,而作为唯一的子元素,它没有兄弟元素。

set 命令

Bash 执行脚本时,会创建一个子 Shell。

1
$ bash script.sh

上面代码中,script.sh是在一个子 Shell 里面执行。这个子 Shell 就是脚本的执行环境,Bash 默认给定了这个环境的各种参数。

set命令用来修改子 Shell 环境的运行参数,即定制环境。

顺便提一下,如果命令行下不带任何参数,直接运行set,会显示所有的环境变量和 Shell 函数。

1
2
$ set
set -u

执行脚本时,如果遇到不存在的变量,Bash 默认忽略它。

1
2
3
4
#!/usr/bin/env bash

echo $a
echo bar

上面代码中,$a是一个不存在的变量。执行结果如下。

1
2
3
$ bash script.sh

bar

可以看到,echo $a输出了一个空行,Bash 忽略了不存在的$a,然后继续执行echo bar。大多数情况下,这不是开发者想要的行为,遇到变量不存在,脚本应该报错,而不是一声不响地往下执行。

set -u就用来改变这种行为。脚本在头部加上它,遇到不存在的变量就会报错,并停止执行。

1
2
3
4
5
#!/usr/bin/env bash
set -u

echo $a
echo bar

运行结果如下。

1
2
$ bash script.sh
bash: script.sh:行4: a: 未绑定的变量

可以看到,脚本报错了,并且不再执行后面的语句。

-u还有另一种写法-o nounset,两者是等价的。

1
2
set -o nounset
set -x

默认情况下,脚本执行后,只输出运行结果,没有其他内容。如果多个命令连续执行,它们的运行结果就会连续输出。有时会分不清,某一段内容是什么命令产生的。

set -x用来在运行结果之前,先输出执行的那一行命令。

1
2
3
4
#!/usr/bin/env bash
set -x

echo bar

执行上面的脚本,结果如下。

1
2
3
$ bash script.sh
+ echo bar
bar

可以看到,执行echo bar之前,该命令会先打印出来,行首以+表示。这对于调试复杂的脚本是很有用的。

-x还有另一种写法-o xtrace。

1
set -o xtrace

脚本当中如果要关闭命令输出,可以使用set +x。

1
2
3
4
5
6
7
8
9
10
11
#!/bin/bash

number=1

set -x
if [ $number = "1" ]; then
echo "Number equals 1"
else
echo "Number does not equal 1"
fi
set +x

上面的例子中,只对特定的代码段打开命令输出。

Bash 的错误处理
如果脚本里面有运行失败的命令(返回值非0),Bash 默认会继续执行后面的命令。

1
2
3
4
#!/usr/bin/env bash

foo
echo bar

上面脚本中,foo是一个不存在的命令,执行时会报错。但是,Bash 会忽略这个错误,继续往下执行。

1
2
3
$ bash script.sh
script.sh:行3: foo: 未找到命令
bar

可以看到,Bash 只是显示有错误,并没有终止执行。

这种行为很不利于脚本安全和除错。实际开发中,如果某个命令失败,往往需要脚本停止执行,防止错误累积。这时,一般采用下面的写法。

command || exit 1
上面的写法表示只要command有非零返回值,脚本就会停止执行。

如果停止执行之前需要完成多个操作,就要采用下面三种写法。

1
2
3
4
5
6
7
8
9
# 写法一
command || { echo "command failed"; exit 1; }

# 写法二
if ! command; then echo "command failed"; exit 1; fi

# 写法三
command
if [ "$?" -ne 0 ]; then echo "command failed"; exit 1; fi

另外,除了停止执行,还有一种情况。如果两个命令有继承关系,只有第一个命令成功了,才能继续执行第二个命令,那么就要采用下面的写法。

1
2
command1 && command2
set -e

上面这些写法多少有些麻烦,容易疏忽。set -e从根本上解决了这个问题,它使得脚本只要发生错误,就终止执行。

1
2
3
4
5
#!/usr/bin/env bash
set -e

foo
echo bar

执行结果如下。

1
2
$ bash script.sh
script.sh:行4: foo: 未找到命令

可以看到,第4行执行失败以后,脚本就终止执行了。

set -e根据返回值来判断,一个命令是否运行失败。但是,某些命令的非零返回值可能不表示失败,或者开发者希望在命令失败的情况下,脚本继续执行下去。这时可以暂时关闭set -e,该命令执行结束后,再重新打开set -e。

1
2
3
4
set +e
command1
command2
set -e

上面代码中,set +e表示关闭-e选项,set -e表示重新打开-e选项。

还有一种方法是使用command || true,使得该命令即使执行失败,脚本也不会终止执行。

1
2
3
4
5
#!/bin/bash
set -e

foo || true
echo bar

上面代码中,true使得这一行语句总是会执行成功,后面的echo bar会执行。

-e还有另一种写法-o errexit。

1
2
set -o errexit
set -o pipefail

set -e有一个例外情况,就是不适用于管道命令。

所谓管道命令,就是多个子命令通过管道运算符(|)组合成为一个大的命令。Bash 会把最后一个子命令的返回值,作为整个命令的返回值。也就是说,只要最后一个子命令不失败,管道命令总是会执行成功,因此它后面命令依然会执行,set -e就失效了。

请看下面这个例子。

1
2
3
4
5
#!/usr/bin/env bash
set -e

foo | echo a
echo bar

执行结果如下。

1
2
3
4
$ bash script.sh
a
script.sh:行4: foo: 未找到命令
bar

上面代码中,foo是一个不存在的命令,但是foo | echo a这个管道命令会执行成功,导致后面的echo bar会继续执行。

set -o pipefail用来解决这种情况,只要一个子命令失败,整个管道命令就失败,脚本就会终止执行。

1
2
3
4
5
#!/usr/bin/env bash
set -eo pipefail

foo | echo a
echo bar

运行后,结果如下。

1
2
3
$ bash script.sh
a
script.sh:行4: foo: 未找到命令

可以看到,echo bar没有执行。

set -E
一旦设置了-e参数,会导致函数内的错误不会被trap命令捕获(参考《trap 命令》一章)。-E参数可以纠正这个行为,使得函数也能继承trap命令。

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash
set -e

trap "echo ERR trap fired!" ERR

myfunc()
{
# 'foo' 是一个不存在的命令
foo
}

myfunc

上面示例中,myfunc函数内部调用了一个不存在的命令foo,导致执行这个函数会报错。

1
2
$ bash test.sh
test.sh:行9: foo:未找到命令

但是,由于设置了set -e,函数内部的报错并没有被trap命令捕获,需要加上-E参数才可以。

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash
set -Eeuo pipefail

trap "echo ERR trap fired!" ERR

myfunc()
{
# 'foo' 是一个不存在的命令
foo
}

myfunc

执行上面这个脚本,就可以看到trap命令生效了。

1
2
3
$ bash test.sh
test.sh:行9: foo:未找到命令
ERR trap fired!

其他参数
set命令还有一些其他参数。

set -n:等同于set -o noexec,不运行命令,只检查语法是否正确。
set -f:等同于set -o noglob,表示不对通配符进行文件名扩展。
set -v:等同于set -o verbose,表示打印 Shell 接收到的每一行输入。
set -o noclobber:防止使用重定向运算符>覆盖已经存在的文件。
上面的-f和-v参数,可以分别使用set +f、set +v关闭。

set 命令总结
上面重点介绍的set命令的几个参数,一般都放在一起使用。

1
2
3
4
5
6
# 写法一
set -Eeuxo pipefail

# 写法二
set -Eeux
set -o pipefail

这两种写法建议放在所有 Bash 脚本的头部。

另一种办法是在执行 Bash 脚本的时候,从命令行传入这些参数。

1
$ bash -euxo pipefail script.sh

shopt 命令
shopt命令用来调整 Shell 的参数,跟set命令的作用很类似。之所以会有这两个类似命令的主要原因是,set是从 Ksh 继承的,属于 POSIX 规范的一部分,而shopt是 Bash 特有的。

直接输入shopt可以查看所有参数,以及它们各自打开和关闭的状态。

1
$ shopt

shopt命令后面跟着参数名,可以查询该参数是否打开。

1
2
$ shopt globstar
globstar off

上面例子表示globstar参数默认是关闭的。

(1)-s

-s用来打开某个参数。

1
$ shopt -s optionNameHere

(2)-u

-u用来关闭某个参数。

1
$ shopt -u optionNameHere

举例来说,histappend这个参数表示退出当前 Shell 时,将操作历史追加到历史文件中。这个参数默认是打开的,如果使用下面的命令将其关闭,那么当前 Shell 的操作历史将替换掉整个历史文件。

1
$ shopt -u histappend

(3)-q

-q的作用也是查询某个参数是否打开,但不是直接输出查询结果,而是通过命令的执行状态($?)表示查询结果。如果状态为0,表示该参数打开;如果为1,表示该参数关闭。

1
2
3
$ shopt -q globstar
$ echo $?
1

上面命令查询globstar参数是否打开。返回状态为1,表示该参数是关闭的。

这个用法主要用于脚本,供if条件结构使用。下面例子是如果打开了这个参数,就执行if结构内部的语句。

1
2
3
if (shopt -q globstar); then
...
fi

创建数组

数组可以采用逐个赋值的方法创建。

1
ARRAY[INDEX]=value

上面语法中,ARRAY是数组的名字,可以是任意合法的变量名。INDEX是一个大于或等于零的整数,也可以是算术表达式。注意数组第一个元素的下标是 0, 而不是 1。

下面创建一个三个成员的数组。

1
2
3
$ array[0]=val
$ array[1]=val
$ array[2]=val

数组也可以采用一次性赋值的方式创建。

1
2
3
4
5
6
7
8
ARRAY=(value1 value2 ... valueN)

# 等同于
ARRAY=(
value1
value2
value3
)

采用上面方式创建数组时,可以按照默认顺序赋值,也可以在每个值前面指定位置。

1
2
3
4
5
$ array=(a b c)
$ array=([2]=c [0]=a [1]=b)

$ days=(Sun Mon Tue Wed Thu Fri Sat)
$ days=([0]=Sun [1]=Mon [2]=Tue [3]=Wed [4]=Thu [5]=Fri [6]=Sat)

只为某些值指定位置,也是可以的。

1
names=(hatter [5]=duchess alice)

上面例子中,hatter是数组的 0 号位置,duchess是 5 号位置,alice是 6 号位置。

没有赋值的数组元素的默认值是空字符串。

定义数组的时候,可以使用通配符。

1
$ mp3s=( *.mp3 )

上面例子中,将当前目录的所有 MP3 文件,放进一个数组。

先用declare -a命令声明一个数组,也是可以的。

1
$ declare -a ARRAYNAME

read -a命令则是将用户的命令行输入,存入一个数组。

1
$ read -a dice

上面命令将用户的命令行输入,存入数组dice

读取数组

读取单个元素

读取数组指定位置的成员,要使用下面的语法。

1
$ echo ${array[i]}     # i 是索引

上面语法里面的大括号是必不可少的,否则 Bash 会把索引部分[i]按照原样输出。

1
2
3
4
5
6
7
$ array[0]=a

$ echo ${array[0]}
a

$ echo $array[0]
a[0]

上面例子中,数组的第一个元素是a。如果不加大括号,Bash 会直接读取$array首成员的值,然后将[0]按照原样输出。

读取所有成员

@*是数组的特殊索引,表示返回数组的所有成员。

1
2
3
$ foo=(a b c d e f)
$ echo ${foo[@]}
a b c d e f

这两个特殊索引配合for循环,就可以用来遍历数组。

1
2
3
for i in "${names[@]}"; do
echo $i
done

@*放不放在双引号之中,是有差别的。

1
2
3
4
5
6
7
8
9
10
11
12
13
$ activities=( swimming "water skiing" canoeing "white-water rafting" surfing )
$ for act in ${activities[@]}; \
do \
echo "Activity: $act"; \
done

Activity: swimming
Activity: water
Activity: skiing
Activity: canoeing
Activity: white-water
Activity: rafting
Activity: surfing

上面的例子中,数组activities实际包含 5 个成员,但是for...in循环直接遍历${activities[@]},导致返回 7 个结果。为了避免这种情况,一般把${activities[@]}放在双引号之中。

1
2
3
4
5
6
7
8
9
10
$ for act in "${activities[@]}"; \
do \
echo "Activity: $act"; \
done

Activity: swimming
Activity: water skiing
Activity: canoeing
Activity: white-water rafting
Activity: surfing

上面例子中,${activities[@]}放在双引号之中,遍历就会返回正确的结果。

${activities[*]}不放在双引号之中,跟${activities[@]}不放在双引号之中是一样的。

1
2
3
4
5
6
7
8
9
10
11
12
$ for act in ${activities[*]}; \
do \
echo "Activity: $act"; \
done

Activity: swimming
Activity: water
Activity: skiing
Activity: canoeing
Activity: white-water
Activity: rafting
Activity: surfing

${activities[*]}放在双引号之中,所有成员就会变成单个字符串返回。

1
2
3
4
5
6
$ for act in "${activities[*]}"; \
do \
echo "Activity: $act"; \
done

Activity: swimming water skiing canoeing white-water rafting surfing

所以,拷贝一个数组的最方便方法,就是写成下面这样。

1
$ hobbies=( "${activities[@]}" )

上面例子中,数组activities被拷贝给了另一个数组hobbies

这种写法也可以用来为新数组添加成员。

1
$ hobbies=( "${activities[@]}" diving )

上面例子中,新数组hobbies在数组activities的所有成员之后,又添加了一个成员。

默认位置

如果读取数组成员时,没有读取指定哪一个位置的成员,默认使用 0 号位置。

1
2
3
4
$ declare -a foo
$ foo=A
$ echo ${foo[0]}
A

上面例子中,foo是一个数组,赋值的时候不指定位置,实际上是给foo[0]赋值。

引用一个不带下标的数组变量,则引用的是 0 号位置的数组元素。

1
2
3
4
5
$ foo=(a b c d e f)
$ echo ${foo}
a
$ echo $foo
a

上面例子中,引用数组元素的时候,没有指定位置,结果返回的是 0 号位置。

数组的长度

要想知道数组的长度(即一共包含多少成员),可以使用下面两种语法。

1
2
${#array[*]}
${#array[@]}

下面是一个例子。

1
2
3
4
5
6
7
$ a[100]=foo

$ echo ${#a[*]}
1

$ echo ${#a[@]}
1

上面例子中,把字符串赋值给 100 位置的数组元素,这时的数组只有一个元素。

注意,如果用这种语法去读取具体的数组成员,就会返回该成员的字符串长度。这一点必须小心。

1
2
3
$ a[100]=foo
$ echo ${#a[100]}
3

上面例子中,`$

函数总是在当前 Shell 执行,这是跟脚本的一个重大区别,Bash 会新建一个子 Shell 执行脚本。如果函数与脚本同名,函数会优先执行。但是,函数的优先级不如别名,即如果函数与别名同名,那么别名优先执行。

Bash 函数定义的语法有两种。

1
2
3
4
5
6
7
8
9
# 第一种
fn() {
# codes
}

# 第二种
function fn() {
# codes
}

上面代码中,fn是自定义的函数名,函数代码就写在大括号之中。这两种写法是等价的。

1
2
3
hello() {
echo "Hello $1"
}

上面代码中,函数体里面的$1表示函数调用时的第一个参数。

调用时,就直接写函数名,参数跟在函数名后面。

1
2
$ hello world
Hello world

下面是一个多行函数的例子,显示当前日期时间。

1
2
3
4
today() {
echo -n "Today's date is: "
date +"%A, %B %-d, %Y"
}

删除一个函数,可以使用unset命令。

1
unset -f functionName

查看当前 Shell 已经定义的所有函数,可以使用declare命令。

1
$ declare -f

上面的declare命令不仅会输出函数名,还会输出所有定义。输出顺序是按照函数名的字母表顺序。由于会输出很多内容,最好通过管道命令配合moreless使用。

declare命令还支持查看单个函数的定义。

1
$ declare -f functionName

declare -F可以输出所有已经定义的函数名,不含函数体。

1
$ declare -F

参数变量

函数体内可以使用参数变量,获取函数参数。函数的参数变量,与脚本参数变量是一致的。

  • $1~$9:函数的第一个到第 9 个的参数。
  • $0:函数所在的脚本名。
  • $#:函数的参数总数。
  • $@:函数的全部参数,参数之间使用空格分隔。
  • $*:函数的全部参数,参数之间使用变量$IFS值的第一个字符分隔,默认为空格,但是可以自定义。

如果函数的参数多于 9 个,那么第 10 个参数可以用${10}的形式引用,以此类推。

1
2
3
4
5
6
7
8
9
10
11
#!/bin/bash
# test.sh

function alice {
echo "alice: $@"
echo "$0: $1 $2 $3 $4"
echo "$# arguments"

}

alice in wonderland

运行该脚本,结果如下。

1
2
3
4
$ bash test.sh
alice: in wonderland
test.sh: in wonderland
2 arguments

上面例子中,由于函数alice只有第一个和第二个参数,所以第三个和第四个参数为空。

下面是一个日志函数的例子。

1
2
3
function log_msg {
echo "[`date '+ %F %T'` ]: $@"
}

使用方法如下。

1
2
$ log_msg "This is sample log message"
[ 2018-08-16 19:56:34 ]: This is sample log message

return 命令

return命令用于从函数返回一个值。函数执行到这条命令,就不再往下执行了,直接返回了。

1
2
3
function func_return_value {
return 10
}

函数将返回值返回给调用者。如果命令行直接执行函数,下一个命令可以用$?拿到返回值。

1
2
3
$ func_return_value
$ echo "Value returned by function is: $?"
Value returned by function is: 10

return后面不跟参数,只用于返回也是可以的。

1
2
3
4
function name {
commands
return
}

全局变量和局部变量,local 命令

Bash 函数体内直接声明的变量,属于全局变量,整个脚本都可以读取。这一点需要特别小心。

1
2
3
4
5
6
7
8
# 脚本 test.sh
fn () {
foo=1
echo "fn: foo = $foo"
}

fn
echo "global: foo = $foo"

上面脚本的运行结果如下。

1
2
3
$ bash test.sh
fn: foo = 1
global: foo = 1

上面例子中,变量$foo是在函数fn内部声明的,函数体外也可以读取。

函数体内不仅可以声明全局变量,还可以修改全局变量。

1
2
3
4
5
6
7
8
9
10
#! /bin/bash
foo=1

fn () {
foo=2
}

fn

echo $foo

上面代码执行后,输出的变量$foo值为 2。

函数里面可以用local命令声明局部变量。

1
2
3
4
5
6
7
8
9
10
#! /bin/bash
# 脚本 test.sh
fn () {
local foo
foo=1
echo "fn: foo = $foo"
}

fn
echo "global: foo = $foo"

上面脚本的运行结果如下。

1
2
3
$ bash test.sh
fn: foo = 1
global: foo =

上面例子中,local命令声明的$foo变量,只在函数体内有效,函数体外没有定义。

Bash 提供三种循环语法forwhileuntil

while 循环

while循环有一个判断条件,只要符合条件,就不断循环执行指定的语句。

1
2
3
while condition; do
commands
done

上面代码中,只要满足条件condition,就会执行命令commands。然后,再次判断是否满足条件condition,只要满足,就会一直执行下去。只有不满足条件,才会退出循环。

循环条件condition可以使用test命令,跟if结构的判断条件写法一致。

1
2
3
4
5
6
7
#!/bin/bash

number=0
while [ "$number" -lt 10 ]; do
echo "Number = $number"
number=$((number + 1))
done

上面例子中,只要变量$number小于 10,就会不断加 1,直到$number等于 10,然后退出循环。

关键字do可以跟while不在同一行,这时两者之间不需要使用分号分隔。

1
2
3
4
while true
do
echo 'Hi, while looping ...';
done

上面的例子会无限循环,可以按下Ctrl + c停止。

while循环写成一行,也是可以的。

1
$ while true; do echo 'Hi, while looping ...'; done

while的条件部分也可以是执行一个命令。

1
$ while echo 'ECHO'; do echo 'Hi, while looping ...'; done

上面例子中,判断条件是echo 'ECHO'。由于这个命令总是执行成功,所以上面命令会产生无限循环。

while的条件部分可以执行任意数量的命令,但是执行结果的真伪只看最后一个命令的执行结果。

1
$ while true; false; do echo 'Hi, looping ...'; done

上面代码运行后,不会有任何输出,因为while的最后一个命令是false

until 循环

until循环与while循环恰好相反,只要不符合判断条件(判断条件失败),就不断循环执行指定的语句。一旦符合判断条件,就退出循环。

1
2
3
until condition; do
commands
done

关键字do可以与until不写在同一行,这时两者之间不需要分号分隔。

1
2
3
4
until condition
do
commands
done
1
2
3
4
5
$ until false; do echo 'Hi, until looping ...'; done
Hi, until looping ...
Hi, until looping ...
Hi, until looping ...
^C

上面代码中,until的部分一直为false,导致命令无限运行,必须按下Ctrl + c终止。

1
2
3
4
5
6
7
#!/bin/bash

number=0
until [ "$number" -ge 10 ]; do
echo "Number = $number"
number=$((number + 1))
done

上面例子中,只要变量number小于 10,就会不断加 1,直到number大于等于 10,就退出循环。

until的条件部分也可以是一个命令,表示在这个命令执行成功之前,不断重复尝试。

1
2
3
4
until cp $1 $2; do
echo 'Attempt to copy failed. waiting...'
sleep 5
done

上面例子表示,只要cp $1 $2这个命令执行不成功,就5秒钟后再尝试一次,直到成功为止。

until循环都可以转为while循环,只要把条件设为否定即可。上面这个例子可以改写如下。

1
2
3
4
while ! cp $1 $2; do
echo 'Attempt to copy failed. waiting...'
sleep 5
done

一般来说,until用得比较少,完全可以统一都使用while

for…in 循环

for...in循环用于遍历列表的每一项。

1
2
3
4
for variable in list
do
commands
done

上面语法中,for循环会依次从list列表中取出一项,作为变量variable,然后在循环体中进行处理。

关键词do可以跟for写在同一行,两者使用分号分隔。

1
2
3
for variable in list; do
commands
done

下面是一个例子。

1
2
3
4
5
#!/bin/bash

for i in word1 word2 word3; do
echo $i
done

上面例子中,word1 word2 word3是一个包含三个单词的列表,变量i依次等于word1、word2、word3,命令echo $i则会相应地执行三次。

列表可以由通配符产生。

1
2
3
for i in *.png; do
ls -l $i
done

上面例子中,*.png会替换成当前目录中所有 PNG 图片文件,变量i会依次等于每一个文件。

列表也可以通过子命令产生。

1
2
3
4
5
6
7
#!/bin/bash

count=0
for i in $(cat ~/.bash_profile); do
count=$((count + 1))
echo "Word $count ($i) contains $(echo -n $i | wc -c) characters"
done

上面例子中,cat ~/.bash_profile命令会输出~/.bash_profile文件的内容,然后通过遍历每一个词,计算该文件一共包含多少个词,以及每个词有多少个字符。

in list的部分可以省略,这时list默认等于脚本的所有参数$@。但是,为了可读性,最好还是不要省略,参考下面的例子。

1
2
3
4
5
6
7
8
9
for filename; do
echo "$filename"
done

# 等同于

for filename in "$@" ; do
echo "$filename"
done

在函数体中也是一样的,for...in循环省略in list的部分,则list默认等于函数的所有参数。

for 循环

for循环还支持 C 语言的循环语法。

1
2
3
for (( expression1; expression2; expression3 )); do
commands
done

上面代码中,expression1用来初始化循环条件,expression2用来决定循环结束的条件,expression3在每次循环迭代的末尾执行,用于更新值。

注意,循环条件放在双重圆括号之中。另外,圆括号之中使用变量,不必加上美元符号$

它等同于下面的while循环。

1
2
3
4
5
(( expression1 ))
while (( expression2 )); do
commands
(( expression3 ))
done

下面是一个例子。

1
2
3
for (( i=0; i<5; i=i+1 )); do
echo $i
done

for条件部分的三个语句,都可以省略。

1
2
3
4
5
6
7
for ((;;))
do
read var
if [ "$var" = "." ]; then
break
fi
done

上面脚本会反复读取命令行输入,直到用户输入了一个点(.)为止,才会跳出循环。

break,continue

Bash 提供了两个内部命令breakcontinue,用来在循环内部跳出循环。

break命令立即终止循环,程序继续执行循环块之后的语句,即不再执行剩下的循环。

1
2
3
4
5
6
7
8
9
#!/bin/bash

for number in 1 2 3 4 5 6
do
echo "number is $number"
if [ "$number" = "3" ]; then
break
fi
done

continue命令立即终止本轮循环,开始执行下一轮循环。

1
2
3
4
5
6
7
8
9
10
11
#!/bin/bash

while read -p "What file do you want to test?" filename
do
if [ ! -e "$filename" ]; then
echo "The file does not exist."
continue
fi

echo "You entered a valid file.."
done

上面例子中,只要用户输入的文件不存在,continue命令就会生效,直接进入下一轮循环(让用户重新输入文件名),不再执行后面的打印语句。

select 结构

select结构主要用来生成简单的菜单。它的语法与for...in循环基本一致。

1
2
3
4
5
select name
[in list]
do
commands
done

Bash 会对select依次进行下面的处理。

  1. select生成一个菜单,内容是列表list的每一项,并且每一项前面还有一个数字编号。
  2. Bash 提示用户选择一项,输入它的编号。
  3. 用户输入以后,Bash 会将该项的内容存在变量name,该项的编号存入环境变量REPLY。如果用户没有输入,就按回车键,Bash 会重新输出菜单,让用户选择。
  4. 执行命令体commands
  5. 执行结束后,回到第一步,重复这个过程。

下面是一个例子。

1
2
3
4
5
6
7
#!/bin/bash
# select.sh

select brand in Samsung Sony iphone symphony Walton
do
echo "You have chosen $brand"
done

执行上面的脚本,Bash 会输出一个品牌的列表,让用户选择。

1
2
3
4
5
6
7
$ ./select.sh
1) Samsung
2) Sony
3) iphone
4) symphony
5) Walton
#?

如果用户没有输入编号,直接按回车键。Bash 就会重新输出一遍这个菜单,直到用户按下Ctrl + c,退出执行。

select可以与case结合,针对不同项,执行不同的命令。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/bin/bash

echo "Which Operating System do you like?"

select os in Ubuntu LinuxMint Windows8 Windows10 WindowsXP
do
case $os in
"Ubuntu"|"LinuxMint")
echo "I also use $os."
;;
"Windows8" | "Windows10" | "WindowsXP")
echo "Why don't you try Linux?"
;;
*)
echo "Invalid entry."
break
;;
esac
done

上面例子中,case针对用户选择的不同项,执行不同的命令。

Bash 条件判断

条件判断

if 结构

1
2
3
4
5
6
7
if commands; then
commands
[elif commands; then
commands...]
[else
commands]
fi

这个命令分成三个部分:if、elifelse。其中,后两个部分是可选的。

1
2
3
4
5
if test $USER = "foo"; then
echo "Hello foo."
else
echo "You are not foo."
fi

上面的例子中,判断条件是环境变量$USER是否等于foo,如果等于就输出Hello foo.,否则输出其他内容。

ifthen写在同一行时,需要分号分隔。分号是 Bash 的命令分隔符。它们也可以写成两行,这时不需要分号。

1
2
3
4
if true
then
echo 'hello world'
fi

除了多行的写法,if结构也可以写成单行。

1
2
$ if true; then echo 'hello world'; fi
hello world

注意,if关键字后面也可以是一条命令,该条命令执行成功(返回值 0),就意味着判断条件成立。

1
2
3
$ if echo 'hi'; then echo 'hello world'; fi
hi
hello world

if后面可以跟任意数量的命令。这时,所有命令都会执行,但是判断真伪只看最后一个命令,即使前面所有命令都失败,只要最后一个命令返回 0,就会执行then的部分。

1
2
$ if false; true; then echo 'hello world'; fi
hello world

elif部分可以有多个。

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash

echo -n "输入一个1到3之间的数字(包含两端)> "
read character
if [ "$character" = "1" ]; then
echo 1
elif [ "$character" = "2" ]; then
echo 2
elif [ "$character" = "3" ]; then
echo 3
else
echo 输入不符合要求
fi

上面例子中,如果用户输入 3,就会连续判断 3 次。

test 命令

if结构的判断条件,一般使用test命令,有三种形式。

1
2
3
4
5
6
7
8
# 写法一
test expression

# 写法二
[ expression ]

# 写法三
[[ expression ]]

上面三种形式是等价的,但是第三种形式还支持正则判断,前两种不支持。

上面的expression是一个表达式。这个表达式为真,test命令执行成功(返回值为 0);表达式为伪,test命令执行失败(返回值为 1)。注意,第二种和第三种写法,[]与内部的表达式之间必须有空格。

1
2
3
4
5
6
7
$ test -f /etc/hosts
$ echo $?
0

$ [ -f /etc/hosts ]
$ echo $?
0

上面的例子中,test命令采用两种写法,判断/etc/hosts文件是否存在,这两种写法是等价的。命令执行后,返回值为 0,表示该文件确实存在。

实际上,[这个字符是test命令的一种简写形式,可以看作是一个独立的命令,这解释了为什么它后面必须有空格。

下面把test命令的三种形式,用在if结构中,判断一个文件是否存在。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 写法一
if test -e /tmp/foo.txt ; then
echo "Found foo.txt"
fi

# 写法二
if [ -e /tmp/foo.txt ] ; then
echo "Found foo.txt"
fi

# 写法三
if [[ -e /tmp/foo.txt ]] ; then
echo "Found foo.txt"
fi

判断表达式

if关键字后面,跟的是一个命令。这个命令可以是test命令,也可以是其他命令。命令的返回值为 0 表示判断成立,否则表示不成立。因为这些命令主要是为了得到返回值,所以可以视为表达式。

常用的判断表达式有下面这些。

文件判断

以下表达式用来判断文件状态。

  • [ -a file ]:如果file存在,则为true
  • [ -b file ]:如果file存在并且是一个块(设备)文件,则为true
  • [ -c file ]:如果file存在并且是一个字符(设备)文件,则为true
  • [ -d file ]:如果file存在并且是一个目录,则为true
  • [ -e file ]:如果file存在,则为true
  • [ -f file ]:如果file存在并且是一个普通文件,则为true
  • [ -g file ]:如果file存在并且设置了组 ID,则为true
  • [ -G file ]:如果file存在并且属于有效的组 ID,则为true
  • [ -h file ]:如果file存在并且是符号链接,则为true
  • [ -k file ]:如果file存在并且设置了它的sticky bit,则为true
  • [ -L file ]:如果file存在并且是一个符号链接,则为true
  • [ -N file ]:如果file存在并且自上次读取后已被修改,则为true
  • [ -O file ]:如果file存在并且属于有效的用户 ID,则为true
  • [ -p file ]:如果file存在并且是一个命名管道,则为true
  • [ -r file ]:如果file存在并且可读(当前用户有可读权限),则为true
  • [ -s file ]:如果file存在且其长度大于零,则为true
  • [ -S file ]:如果file存在且是一个网络 socket,则为true
  • [ -t fd ]:如果fd是一个文件描述符,并且重定向到终端,则为true。这可以用来判断是否重定向了标准输入/输出/错误。
  • [ -u file ]:如果file存在并且设置了setuid位,则为true
  • [ -w file ]:如果file存在并且可写(当前用户拥有可写权限),则为true
  • [ -x file ]:如果file存在并且可执行(有效用户有执行/搜索权限),则为true
  • [ file1 -nt file2 ]:如果FILE1FILE2的更新时间最近,或者FILE1存在而FILE2不存在,则为true
  • [ file1 -ot file2 ]:如果FILE1FILE2的更新时间更旧,或者FILE2存在而FILE1不存在,则为true
  • [ FILE1 -ef FILE2 ]:如果FILE1FILE2引用相同的设备和inode编号,则为true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/bin/bash

FILE=~/.bashrc

if [ -e "$FILE" ]; then
if [ -f "$FILE" ]; then
echo "$FILE is a regular file."
fi
if [ -d "$FILE" ]; then
echo "$FILE is a directory."
fi
if [ -r "$FILE" ]; then
echo "$FILE is readable."
fi
if [ -w "$FILE" ]; then
echo "$FILE is writable."
fi
if [ -x "$FILE" ]; then
echo "$FILE is executable/searchable."
fi
else
echo "$FILE does not exist"
exit 1
fi

上面代码中,$FILE要放在双引号之中,这样可以防止变量$FILE为空,从而出错。因为$FILE如果为空,这时[ -e $FILE ]就变成[ -e ],这会被判断为真。而$FILE放在双引号之中,[ -e "$FILE" ]就变成[ -e "" ],这会被判断为伪。

字符串判断

以下表达式用来判断字符串。

  • [ string ]:如果string不为空(长度大于0),则判断为真。
  • [ -n string ]:如果字符串string的长度大于零,则判断为真。
  • [ -z string ]:如果字符串string的长度为零,则判断为真。
  • [ string1 = string2 ]:如果string1string2相同,则判断为真。
  • [ string1 == string2 ] 等同于[ string1 = string2 ]
  • [ string1 != string2 ]:如果string1string2不相同,则判断为真。
  • [ string1 '>' string2 ]:如果按照字典顺序string1排列在string2之后,则判断为真。
  • [ string1 '<' string2 ]:如果按照字典顺序string1排列在string2之前,则判断为真。

注意,test命令内部的><,必须用引号引起来(或者是用反斜杠转义)。否则,它们会被 shell 解释为重定向操作符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/bin/bash

ANSWER=maybe

if [ -z "$ANSWER" ]; then
echo "There is no answer." >&2
exit 1
fi
if [ "$ANSWER" = "yes" ]; then
echo "The answer is YES."
elif [ "$ANSWER" = "no" ]; then
echo "The answer is NO."
elif [ "$ANSWER" = "maybe" ]; then
echo "The answer is MAYBE."
else
echo "The answer is UNKNOWN."
fi

上面代码中,首先确定$ANSWER字符串是否为空。如果为空,就终止脚本,并把退出状态设为 1。注意,这里的echo命令把错误信息There is no answer.重定向到标准错误,这是处理错误信息的常用方法。如果$ANSWER字符串不为空,就判断它的值是否等于yes、no或者maybe

注意,字符串判断时,变量要放在双引号之中,比如[ -n "$COUNT" ],否则变量替换成字符串以后,test命令可能会报错,提示参数过多。另外,如果不放在双引号之中,变量为空时,命令会变成[ -n ],这时会判断为真。如果放在双引号之中,[ -n "" ]就判断为伪。

整数判断

下面的表达式用于判断整数。

  • [ integer1 -eq integer2 ]:如果integer1等于integer2,则为true
  • [ integer1 -ne integer2 ]:如果integer1不等于integer2,则为true
  • [ integer1 -le integer2 ]:如果integer1小于或等于integer2,则为true
  • [ integer1 -lt integer2 ]:如果integer1小于integer2,则为true
  • [ integer1 -ge integer2 ]:如果integer1大于或等于integer2,则为true
  • [ integer1 -gt integer2 ]:如果integer1大于integer2,则为true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/bin/bash

INT=-5

if [ -z "$INT" ]; then
echo "INT is empty." >&2
exit 1
fi
if [ $INT -eq 0 ]; then
echo "INT is zero."
else
if [ $INT -lt 0 ]; then
echo "INT is negative."
else
echo "INT is positive."
fi
if [ $((INT % 2)) -eq 0 ]; then
echo "INT is even."
else
echo "INT is odd."
fi
fi

上面例子中,先判断变量$INT是否为空,然后判断是否为 0,接着判断正负,最后通过求余数判断奇偶。

正则判断

[[ expression ]]这种判断形式,支持正则表达式。

1
[[ string1 =~ regex ]]

上面的语法中,regex是一个正则表示式,=~是正则比较运算符。

1
2
3
4
5
6
7
8
9
10
11
#!/bin/bash

INT=-5

if [[ "$INT" =~ ^-?[0-9]+$ ]]; then
echo "INT is an integer."
exit 0
else
echo "INT is not an integer." >&2
exit 1
fi

上面代码中,先判断变量INT的字符串形式,是否满足^-?[0-9]+$的正则模式,如果满足就表明它是一个整数。

test 判断的逻辑运算

通过逻辑运算,可以把多个test判断表达式结合起来,创造更复杂的判断。三种逻辑运算AND,ORNOT,都有自己的专用符号。

  • AND运算:符号&&,也可使用参数-a
  • OR运算:符号||,也可使用参数-o
  • NOT运算:符号!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/bin/bash

MIN_VAL=1
MAX_VAL=100

INT=50

if [[ "$INT" =~ ^-?[0-9]+$ ]]; then
if [[ $INT -ge $MIN_VAL && $INT -le $MAX_VAL ]]; then
echo "$INT is within $MIN_VAL to $MAX_VAL."
else
echo "$INT is out of range."
fi
else
echo "INT is not an integer." >&2
exit 1
fi

上面例子中,&&用来连接两个判断条件:大于等于$MIN_VAL,并且小于等于$MAX_VAL

使用否定操作符!时,最好用圆括号确定转义的范围。

1
2
3
4
5
if [ ! \( $INT -ge $MIN_VAL -a $INT -le $MAX_VAL \) ]; then
echo "$INT is outside $MIN_VAL to $MAX_VAL."
else
echo "$INT is in range."
fi

上面例子中,test命令内部使用的圆括号,必须使用引号或者转义,否则会被 Bash 解释。

算术判断

Bash 还提供了((...))作为算术条件,进行算术运算的判断。

1
2
3
if ((3 > 2)); then
echo "true"
fi

上面代码执行后,会打印出true

注意,算术判断不需要使用test命令,而是直接使用((...))结构。这个结构的返回值,决定了判断的真伪。

如果算术计算的结果是非零值,则表示判断成立。这一点跟命令的返回值正好相反,需要小心。

1
2
3
4
$ if ((1)); then echo "It is true."; fi
It is true.
$ if ((0)); then echo "It is true."; else echo "it is false."; fi
It is false.

上面例子中,((1))表示判断成立,((0))表示判断不成立。

算术条件((...))也可以用于变量赋值。

1
2
$ if (( foo = 5 ));then echo "foo is $foo"; fi
foo is 5

上面例子中,(( foo = 5 ))完成了两件事情。首先把5赋值给变量foo,然后根据返回值 5,判断条件为真。

注意,赋值语句返回等号右边的值,如果返回的是 0,则判断为假。

1
2
$ if (( foo = 0 ));then echo "It is true.";else echo "It is false."; fi
It is false.

下面是用算术条件改写的数值判断脚本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/bin/bash

INT=-5

if [[ "$INT" =~ ^-?[0-9]+$ ]]; then
if ((INT == 0)); then
echo "INT is zero."
else
if ((INT < 0)); then
echo "INT is negative."
else
echo "INT is positive."
fi
if (( ((INT % 2)) == 0)); then
echo "INT is even."
else
echo "INT is odd."
fi
fi
else
echo "INT is not an integer." >&2
exit 1
fi

只要是算术表达式,都能用于((...))语法。

普通命令的逻辑运算

如果if结构使用的不是test命令,而是普通命令,比如((...))算术运算,或者test命令与普通命令混用,那么可以使用 Bash 的命令控制操作符&&(AND)||(OR),进行多个命令的逻辑运算。

1
2
$ command1 && command2
$ command1 || command2

对于&&操作符,先执行command1,只有command1执行成功后,才会执行command2。对于||操作符,先执行command1,只有command1执行失败后, 才会执行command2

1
$ mkdir temp && cd temp

上面的命令会创建一个名为temp的目录,执行成功后,才会执行第二个命令,进入这个目录。

1
$ [ -d temp ] || mkdir temp

上面的命令会测试目录temp是否存在,如果不存在,就会执行第二个命令,创建这个目录。这种写法非常有助于在脚本中处理错误。

1
[ ! -d temp ] && exit 1

上面的命令中,如果temp子目录不存在,脚本会终止,并且返回值为 1。

下面就是if&&结合使用的写法。

1
2
3
if [ condition ] && [ condition ]; then
command
fi

下面是一个示例。

1
2
3
4
5
6
7
8
9
10
#! /bin/bash

filename=$1
word1=$2
word2=$3

if grep $word1 $filename && grep $word2 $filename
then
echo "$word1 and $word2 are both in $filename."
fi

上面的例子只有在指定文件里面,同时存在搜索词word1word2,就会执行if的命令部分。

下面的示例演示如何将一个&&判断表达式,改写成对应的if结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[[ -d "$dir_name" ]] && cd "$dir_name" && rm *

# 等同于
if [[ ! -d "$dir_name" ]]; then
echo "No such directory: '$dir_name'" >&2
exit 1
fi
if ! cd "$dir_name"; then
echo "Cannot cd to '$dir_name'" >&2
exit 1
fi
if ! rm *; then
echo "File deletion failed. Check results" >&2
exit 1
fi

case 结构

case结构用于多值判断,可以为每个值指定对应的命令,跟包含多个elifif结构等价,但是语义更好。它的语法如下。

1
2
3
4
5
6
7
case expression in
pattern )
commands ;;
pattern )
commands ;;
...
esac

上面代码中,expression是一个表达式,pattern是表达式的值或者一个模式,可以有多条,用来匹配多个值,每条以两个分号(;)结尾。

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash

echo -n "输入一个1到3之间的数字(包含两端)> "
read character
case $character in
1 ) echo 1
;;
2 ) echo 2
;;
3 ) echo 3
;;
* ) echo 输入不符合要求
esac

上面例子中,最后一条匹配语句的模式是*,这个通配符可以匹配其他字符和没有输入字符的情况,类似ifelse部分。

下面是另一个例子。

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash

OS=$(uname -s)

case "$OS" in
FreeBSD) echo "This is FreeBSD" ;;
Darwin) echo "This is Mac OSX" ;;
AIX) echo "This is AIX" ;;
Minix) echo "This is Minix" ;;
Linux) echo "This is Linux" ;;
*) echo "Failed to identify this OS" ;;
esac

上面的例子判断当前是什么操作系统。

case的匹配模式可以使用各种通配符,下面是一些例子。

  • a):匹配a
  • a|b):匹配ab
  • [[:alpha:]]):匹配单个字母。
  • ???):匹配3个字符的单词。
  • *.txt):匹配.txt结尾。
  • *):匹配任意输入,通过作为case结构的最后一个模式。
1
2
3
4
5
6
7
8
9
10
11
#!/bin/bash

echo -n "输入一个字母或数字 > "
read character
case $character in
[[:lower:]] | [[:upper:]] ) echo "输入了字母 $character"
;;
[0-9] ) echo "输入了数字 $character"
;;
* ) echo "输入不符合要求"
esac

上面例子中,使用通配符[[:lower:]] | [[:upper:]]匹配字母,[0-9]匹配数字。

Bash 4.0 之前,case结构只能匹配一个条件,然后就会退出case结构。Bash 4.0 之后,允许匹配多个条件,这时可以用;;&终止每个条件块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/bash
# test.sh

read -n 1 -p "Type a character > "
echo
case $REPLY in
[[:upper:]]) echo "'$REPLY' is upper case." ;;&
[[:lower:]]) echo "'$REPLY' is lower case." ;;&
[[:alpha:]]) echo "'$REPLY' is alphabetic." ;;&
[[:digit:]]) echo "'$REPLY' is a digit." ;;&
[[:graph:]]) echo "'$REPLY' is a visible character." ;;&
[[:punct:]]) echo "'$REPLY' is a punctuation symbol." ;;&
[[:space:]]) echo "'$REPLY' is a whitespace character." ;;&
[[:xdigit:]]) echo "'$REPLY' is a hexadecimal digit." ;;&
esac

执行上面的脚本,会得到下面的结果。

1
2
3
4
5
6
$ test.sh
Type a character > a
'a' is lower case.
'a' is alphabetic.
'a' is a visible character.
'a' is a hexadecimal digit.

可以看到条件语句结尾添加了;;&以后,在匹配一个条件之后,并没有退出case结构,而是继续判断下一个条件。

Bash 脚本入门

Bash 脚本入门

脚本(script)就是包含一系列命令的一个文本文件。Shell 读取这个文件,依次执行里面的所有命令,就好像这些命令直接输入到命令行一样。所有能够在命令行完成的任务,都能够用脚本完成。

脚本的好处是可以重复使用,也可以指定在特定场合自动调用,比如系统启动或关闭时自动执行脚本。

Shebang 行

脚本的第一行通常是指定解释器,即这个脚本必须通过什么解释器执行。这一行以#!字符开头,这个字符称为Shebang,所以这一行就叫做Shebang行。

#!后面就是脚本解释器的位置,Bash 脚本的解释器一般是/bin/sh/bin/bash

1
2
3
#!/bin/sh
# 或者
#!/bin/bash

#!与脚本解释器之间有没有空格,都是可以的。

如果 Bash 解释器不放在目录/bin,脚本就无法执行了。为了保险,可以写成下面这样。

1
#!/usr/bin/env bash

上面命令使用env命令(这个命令总是在/usr/bin目录),返回 Bash 可执行文件的位置。

Shebang行不是必需的,但是建议加上这行。如果缺少该行,就需要手动将脚本传给解释器。举例来说,脚本是script.sh,有Shebang行的时候,可以直接调用执行。

1
$ ./script.sh

上面例子中,script.sh是脚本文件名。脚本通常使用.sh后缀名,不过这不是必需的。

如果没有Shebang行,就只能手动将脚本传给解释器来执行。

1
2
3
$ /bin/sh ./script.sh
# 或者
$ bash ./script.sh

执行权限和路径

只要指定了Shebang行的脚本,可以直接执行。这有一个前提条件,就是脚本需要有执行权限。可以使用下面的命令,赋予脚本执行权限。

1
2
3
4
5
6
7
8
9
10
# 给所有用户执行权限
$ chmod +x script.sh

# 给所有用户读权限和执行权限
$ chmod +rx script.sh
# 或者
$ chmod 755 script.sh

# 只给脚本拥有者读权限和执行权限
$ chmod u+rx script.sh

脚本的权限通常设为 755(拥有者有所有权限,其他人有读和执行权限)或者 700(只有拥有者可以执行)。

除了执行权限,脚本调用时,一般需要指定脚本的路径(比如path/script.sh)。如果将脚本放在环境变量$PATH指定的目录中,就不需要指定路径了。因为 Bash 会自动到这些目录中,寻找是否存在同名的可执行文件。

建议在主目录新建一个~/bin子目录,专门存放可执行脚本,然后把~/bin加入$PATH

1
export PATH=$PATH:~/bin

上面命令改变环境变量$PATH,将~/bin添加到$PATH的末尾。可以将这一行加到~/.bashrc文件里面,然后重新加载一次.bashrc,这个配置就可以生效了。

1
$ source ~/.bashrc

以后不管在什么目录,直接输入脚本文件名,脚本就会执行。

1
$ script.sh

上面命令没有指定脚本路径,因为script.sh$PATH指定的目录中。

env 命令

env命令总是指向/usr/bin/env文件,或者说,这个二进制文件总是在目录/usr/bin

#!/usr/bin/env NAME这个语法的意思是,让 Shell 查找$PATH环境变量里面第一个匹配的NAME。如果你不知道某个命令的具体路径,或者希望兼容其他用户的机器,这样的写法就很有用。

/usr/bin/env bash的意思就是,返回bash可执行文件的位置,前提是bash的路径是在$PATH里面。其他脚本文件也可以使用这个命令。比如 Node.js 脚本的Shebang行,可以写成下面这样。

1
#!/usr/bin/env node

env命令的参数如下。

  • -i, --ignore-environment:不带环境变量启动。
  • -u, --unset=NAME:从环境变量中删除一个变量。
  • --help:显示帮助。
  • --version:输出版本信息。

下面是一个例子,新建一个不带任何环境变量的 Shell。

1
$ env -i /bin/sh

注释

Bash 脚本中,#表示注释,可以放在行首,也可以放在行尾。

1
2
3
4
# 本行是注释
echo 'Hello World!'

echo 'Hello World!' # 井号后面的部分也是注释

建议在脚本开头,使用注释说明当前脚本的作用,这样有利于日后的维护。

脚本参数

调用脚本的时候,脚本文件名后面可以带有参数。

1
$ script.sh word1 word2 word3

脚本文件内部,可以使用特殊变量,引用这些参数。

  • $0:脚本文件名,即script.sh
  • $1~$9:对应脚本的第一个参数到第九个参数。
  • $#:参数的总数。
  • $@:全部的参数,参数之间使用空格分隔。
  • $*:全部的参数,参数之间使用变量$IFS值的第一个字符分隔,默认为空格,但是可以自定义。

如果脚本的参数多于 9 个,那么第 10 个参数可以用${10}的形式引用,以此类推。

注意,如果命令是command -o foo bar,那么-o$1foo$2bar$3

下面是一个脚本内部读取命令行参数的例子。

1
2
3
4
5
6
7
8
9
#!/bin/bash
# script.sh

echo "全部参数:" $@
echo "命令行参数数量:" $#
echo '$0 = ' $0
echo '$1 = ' $1
echo '$2 = ' $2
echo '$3 = ' $3

执行结果如下。

1
2
3
4
5
6
7
$ ./script.sh a b c
全部参数:a b c
命令行参数数量:3
$0 = script.sh
$1 = a
$2 = b
$3 = c

用户可以输入任意数量的参数,利用for循环,可以读取每一个参数。

1
2
3
4
5
#!/bin/bash

for i in "$@"; do
echo $i
done

如果多个参数放在双引号里面,视为一个参数。

1
$ ./script.sh "a b"

上面例子中,Bash 会认为"a b"是一个参数,$1会返回a b。注意,返回时不包括双引号。

shift 命令

shift命令可以改变脚本参数,每次执行都会移除脚本当前的第一个参数($1),使得后面的参数向前一位,即$2变成$1、$3变成$2、$4变成$3,以此类推。

while循环结合shift命令,也可以读取每一个参数。

1
2
3
4
5
6
7
8
9
#!/bin/bash

echo "一共输入了 $# 个参数"

while [ "$1" != "" ]; do
echo "剩下 $# 个参数"
echo "参数:$1"
shift
done

上面例子中,shift命令每次移除当前第一个参数,从而通过while循环遍历所有参数。

shift命令可以接受一个整数作为参数,指定所要移除的参数个数,默认为 1。

1
shift 3

上面的命令移除前三个参数,原来的$4变成$1

getopts 命令

getopts命令用在脚本内部,可以解析复杂的脚本命令行参数,通常与while循环一起使用,取出脚本所有的带有前置连词线(-)的参数。

1
getopts optstring name

它带有两个参数。第一个参数optstring是字符串,给出脚本所有的连词线参数。比如,某个脚本可以有三个配置项参数-l、-h、-a,其中只有-a可以带有参数值,而-l-h是开关参数,那么getopts的第一个参数写成lha:,顺序不重要。注意,a后面有一个冒号,表示该参数带有参数值,getopts规定带有参数值的配置项参数,后面必须带有一个冒号(:)。getopts的第二个参数name是一个变量名,用来保存当前取到的配置项参数,即l、ha

下面是一个例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
while getopts 'lha:' OPTION; do
case "$OPTION" in
l)
echo "linuxconfig"
;;

h)
echo "h stands for h"
;;

a)
avalue="$OPTARG"
echo "The value provided is $OPTARG"
;;
?)
echo "script usage: $(basename $0) [-l] [-h] [-a somevalue]" >&2
exit 1
;;
esac
done
shift "$(($OPTIND - 1))"

上面例子中,while循环不断执行getopts 'lha:' OPTION命令,每次执行就会读取一个连词线参数(以及对应的参数值),然后进入循环体。变量OPTION保存的是,当前处理的那一个连词线参数(即l、ha)。如果用户输入了没有指定的参数(比如-x),那么OPTION等于?。循环体内使用case判断,处理这四种不同的情况。

如果某个连词线参数带有参数值,比如-a foo,那么处理a参数的时候,环境变量$OPTARG保存的就是参数值。

注意,只要遇到不带连词线的参数,getopts就会执行失败,从而退出while循环。比如,getopts可以解析command -l foo,但不可以解析command foo -l。另外,多个连词线参数写在一起的形式,比如command -lhgetopts也可以正确处理。

变量$OPTINDgetopts开始执行前是 1,然后每次执行就会加 1。等到退出while循环,就意味着连词线参数全部处理完毕。这时,$OPTIND - 1就是已经处理的连词线参数个数,使用shift命令将这些参数移除,保证后面的代码可以用$1、$2等处理命令的主参数。

配置项参数终止符 –

-和–开头的参数,会被 Bash 当作配置项解释。但是,有时它们不是配置项,而是实体参数的一部分,比如文件名叫做-f--file

1
2
$ cat -f
$ cat --file

上面命令的原意是输出文件-f--file的内容,但是会被 Bash 当作配置项解释。

这时就可以使用配置项参数终止符–,它的作用是告诉 Bash,在它后面的参数开头的-和–不是配置项,只能当作实体参数解释。

1
2
$ cat -- -f
$ cat -- --file

上面命令可以正确展示文件-f--file的内容,因为它们放在–的后面,开头的-和–就不再当作配置项解释了。

如果要确保某个变量不会被当作配置项解释,就要在它前面放上参数终止符–。

1
$ ls -- $myPath

上面示例中,–强制变量$myPath只能当作实体参数(即路径名)解释。如果变量不是路径名,就会报错。

1
2
3
$ myPath="-l"
$ ls -- $myPath
ls: 无法访问'-l': 没有那个文件或目录

上面例子中,变量myPath的值为 -l,不是路径。但是,–强制$myPath只能作为路径解释,导致报错“不存在该路径”。

下面是另一个实际的例子,如果想在文件里面搜索--hello,这时也要使用参数终止符–。

1
$ grep -- "--hello" example.txt

上面命令在example.txt文件里面,搜索字符串–hello。这个字符串是–开头,如果不用参数终止符,grep命令就会把–hello当作配置项参数,从而报错。

exit 命令

exit命令用于终止当前脚本的执行,并向 Shell 返回一个退出值。

1
$ exit

上面命令中止当前脚本,将最后一条命令的退出状态,作为整个脚本的退出状态。

exit命令后面可以跟参数,该参数就是退出状态。

1
2
3
4
5
# 退出值为0(成功)
$ exit 0

# 退出值为1(失败)
$ exit 1

退出时,脚本会返回一个退出值。脚本的退出值,0表示正常,1表示发生错误,2表示用法不对,126表示不是可执行脚本,127表示命令没有发现。如果脚本被信号N终止,则退出值为128 + N。简单来说,只要退出值非0,就认为执行出错。

下面是一个例子。

1
2
3
4
if [ $(id -u) != "0" ]; then
echo "根用户才能执行当前脚本"
exit 1
fi

上面的例子中,id -u命令返回用户的 ID,一旦用户的 ID 不等于 0(根用户的 ID),脚本就会退出,并且退出码为 1,表示运行失败。

exitreturn命令的差别是,return命令是函数的退出,并返回一个值给调用者,脚本依然执行。exit是整个脚本的退出,如果在函数之中调用exit,则退出函数,并终止脚本执行。

命令执行结果

命令执行结束后,会有一个返回值。0表示执行成功,非0(通常是1)表示执行失败。环境变量$?可以读取前一个命令的返回值。

利用这一点,可以在脚本中对命令执行结果进行判断。

1
2
3
4
5
6
7
cd /path/to/somewhere
if [ "$?" = "0" ]; then
rm *
else
echo "无法切换目录!" 1>&2
exit 1
fi

上面例子中,cd /path/to/somewhere这个命令如果执行成功(返回值等于0),就删除该目录里面的文件,否则退出脚本,整个脚本的返回值变为 1,表示执行失败。

由于if可以直接判断命令的执行结果,执行相应的操作,上面的脚本可以改写成下面的样子。

1
2
3
4
5
6
if cd /path/to/somewhere; then
rm *
else
echo "Could not change directory! Aborting." 1>&2
exit 1
fi

更简洁的写法是利用两个逻辑运算符&&(且)和||(或)。

1
2
3
4
5
# 第一步执行成功,才会执行第二步
cd /path/to/somewhere && rm *

# 第一步执行失败,才会执行第二步
cd /path/to/somewhere || exit 1

source 命令

source命令用于执行一个脚本,通常用于重新加载一个配置文件。

1
$ source .bashrc

source命令最大的特点是在当前 Shell 执行脚本,不像直接执行脚本时,会新建一个子 Shell。所以,source命令执行脚本时,不需要export变量。

1
2
3
#!/bin/bash
# test.sh
echo $foo

上面脚本输出$foo变量的值。

1
2
3
4
5
6
7
8
9
# 当前 Shell 新建一个变量 foo
$ foo=1

# 打印输出 1
$ source test.sh
1

# 打印输出空字符串
$ bash test.sh

上面例子中,当前 Shell 的变量foo并没有export,所以直接执行无法读取,但是source执行可以读取。

source命令的另一个用途,是在脚本内部加载外部库。

1
2
3
4
#!/bin/bash

source ./lib.sh
function_from_lib

上面脚本在内部使用source命令加载了一个外部库,然后就可以在脚本里面,使用这个外部库定义的函数。

source有一个简写形式,可以使用一个点(.)来表示。

1
$ . .bashrc

别名,alias 命令

alias命令用来为一个命令指定别名,这样更便于记忆。

1
alias NAME=DEFINITION

上面命令中,NAME是别名的名称,DEFINITION是别名对应的原始命令。注意,等号两侧不能有空格,否则会报错。

一个常见的例子是为grep命令起一个search的别名。

1
alias search=grep

alias也可以用来为长命令指定一个更短的别名。下面是通过别名定义一个today的命令。

1
2
3
$ alias today='date +"%A, %B %-d, %Y"'
$ today
星期一, 一月 6, 2020

alias定义的别名也可以接受参数,参数会直接传入原始命令。

1
2
3
$ alias echo='echo It says: '
$ echo hello world
It says: hello world

上面例子中,别名定义了echo命令的前两个参数,等同于修改了echo命令的默认行为。

指定别名以后,就可以像使用其他命令一样使用别名。一般来说,都会把常用的别名写在~/.bashrc的末尾。另外,只能为命令定义别名,为其他部分(比如很长的路径)定义别名是无效的。

直接调用alias命令,可以显示所有别名。

1
$ alias

unalias命令可以解除别名。

1
$ unalias lt

Bash 行操作

Bash 内置了 Readline 库,具有这个库提供的很多“行操作”功能,比如命令的自动补全,可以大大加快操作速度。

这个库默认采用 Emacs 快捷键,也可以改成 Vi 快捷键。

1
$ set -o vi

下面的命令可以改回 Emacs 快捷键。

1
$ set -o emacs

如果想永久性更改编辑模式(Emacs / Vi),可以将命令写在~/.inputrc文件,这个文件是 Readline 的配置文件。

1
set editing-mode vi

Bash 默认开启这个库,但是允许关闭。

1
$ bash --noediting

上面命令中,--noediting参数关闭了 Readline 库,启动的 Bash 就不带有行操作功能。

光标移动

Readline 提供快速移动光标的快捷键。

  • Ctrl + a:移到行首。
  • Ctrl + b:向行首移动一个字符,与左箭头作用相同。
  • Ctrl + e:移到行尾。
  • Ctrl + f:向行尾移动一个字符,与右箭头作用相同。
  • Alt + f:移动到当前单词的词尾。
  • Alt + b:移动到当前单词的词首。

上面快捷键的 Alt 键,也可以用 ESC 键代替。

清除屏幕

Ctrl + l快捷键可以清除屏幕,即将当前行移到屏幕的第一行,与clear命令作用相同。

编辑操作

下面的快捷键可以编辑命令行内容。

  • Ctrl + d:删除光标位置的字符(delete)。
  • Ctrl + w:删除光标前面的单词。
  • Ctrl + t:光标位置的字符与它前面一位的字符交换位置(transpose)。
  • Alt + t:光标位置的词与它前面一位的词交换位置(transpose)。
  • Alt + l:将光标位置至词尾转为小写(lowercase)。
  • Alt + u:将光标位置至词尾转为大写(uppercase)。

使用Ctrl + d的时候,如果当前行没有任何字符,会导致退出当前 Shell,所以要小心。

剪切和粘贴快捷键如下。

  • Ctrl + k:剪切光标位置到行尾的文本。
  • Ctrl + u:剪切光标位置到行首的文本。
  • Alt + d:剪切光标位置到词尾的文本。
  • Alt + Backspace:剪切光标位置到词首的文本。
  • Ctrl + y:在光标位置粘贴文本。

同样地,Alt 键可以用 Esc 键代替。

自动补全

命令输入到一半的时候,可以按一下 Tab 键,Readline 会自动补全命令或路径。比如,输入cle,再按下 Tab 键,Bash 会自动将这个命令补全为clear

如果符合条件的命令或路径有多个,就需要连续按两次 Tab 键,Bash 会提示所有符合条件的命令或路径。

除了命令或路径,Tab 还可以补全其他值。如果一个值以$开头,则按下 Tab 键会补全变量;如果以~开头,则补全用户名;如果以@开头,则补全主机名(hostname),主机名以列在/etc/hosts文件里面的主机为准。

自动补全相关的快捷键如下。

  • Tab:完成自动补全。
  • Alt + ?:列出可能的补全,与连按两次 Tab 键作用相同。
  • Alt + /:尝试文件路径补全。
  • Ctrl + x /:先按Ctrl + x,再按/,等同于Alt + ?,列出可能的文件路径补全。
  • Alt + !:命令补全。
  • Ctrl + x !:先按Ctrl + x,再按!,等同于Alt + !,命令补全。
  • Alt + ~:用户名补全。
  • Ctrl + x ~:先按Ctrl + x,再按~,等同于Alt + ~,用户名补全。
  • Alt + $:变量名补全。
  • Ctrl + x $:先按Ctrl + x,再按$,等同于Alt + $,变量名补全。
  • Alt + @:主机名补全。
  • Ctrl + x @:先按Ctrl + x,再按@,等同于Alt + @,主机名补全。
  • Alt + *:在命令行一次性插入所有可能的补全。
  • Alt + Tab:尝试用.bash_history里面以前执行命令,进行补全。

上面的Alt键也可以用 ESC 键代替。

操作历史

基本用法

Bash 会保留用户的操作历史,即用户输入的每一条命令都会记录。有了操作历史以后,就可以使用方向键的↑和↓,快速浏览上一条和下一条命令。

退出当前 Shell 的时候,Bash 会将用户在当前 Shell 的操作历史写入~/.bash_history文件,该文件默认储存 500 个操作。

环境变量HISTFILE总是指向这个文件。

1
2
$ echo $HISTFILE
/home/me/.bash_history

history命令会输出这个文件的全部内容。用户可以看到最近执行过的所有命令,每条命令之前都有行号。越近的命令,排在越后面。

1
2
3
4
5
$ history
...
498 echo Goodbye
499 ls ~
500 cd

输入命令时,按下Ctrl + r快捷键,就可以搜索操作历史,选择以前执行过的命令。这时键入命令的开头部分,Shell 就会自动在历史文件中,查询并显示最近一条匹配的结果,这时按下回车键,就会执行那条命令。

下面的方法可以快速执行以前执行过的命令。

1
2
3
4
5
6
7
8
9
$ echo Hello World
Hello World

$ echo Goodbye
Goodbye

$ !e
echo Goodbye
Goodbye

上面例子中,!e表示找出操作历史之中,最近的那一条以e开头的命令并执行。Bash 会先输出那一条命令echo Goodbye,然后直接执行。

同理,!echo也会执行最近一条以echo开头的命令。

1
2
3
4
5
6
7
8
9
10
11
$ !echo
echo Goodbye
Goodbye

$ !echo H
echo Goodbye H
Goodbye H

$ !echo H G
echo Goodbye H G
Goodbye H G

注意,!string语法只会匹配命令,不会匹配参数。所以!echo H不会执行echo Hello World,而是会执行echo Goodbye,并把参数H附加在这条命令之后。同理,!echo H G也是等同于echo Goodbye命令之后附加H G。

由于!string语法会扩展成以前执行过的命令,所以含有!的字符串放在双引号里面,必须非常小心,如果它后面有非空格的字符,就很有可能报错。

1
2
$ echo "I say:\"hello!\""
bash: !\: event not found

上面的命令会报错,原因是感叹号后面是一个反斜杠,Bash 会尝试寻找,以前是否执行过反斜杠开头的命令,一旦找不到就会报错。解决方法就是在感叹号前面,也加上反斜杠。

1
2
$ echo "I say:\"hello\!\""
I say:"hello\!"

history 命令

history命令能显示操作历史,即.bash_history文件的内容。

1
$ history

使用该命令,而不是直接读取.bash_history文件的好处是,它会在所有的操作前加上行号,最近的操作在最后面,行号最大。

通过定制环境变量HISTTIMEFORMAT,可以显示每个操作的时间。

1
2
3
4
$ export HISTTIMEFORMAT='%F %T  '
$ history
1 2013-06-09 10:40:12 cat /etc/issue
2 2013-06-09 10:40:12 clear

上面代码中,%F相当于%Y - %m - %d%T相当于%H : %M : %S

只要设置HISTTIMEFORMAT这个环境变量,就会在.bash_history文件保存命令的执行时间戳。如果不设置,就不会保存时间戳。

环境变量HISTSIZE设置保存历史操作的数量。

1
$ export HISTSIZE=10000

上面命令设置保存过去 10000 条操作历史。

如果不希望保存本次操作的历史,可以设置HISTSIZE等于 0。

1
export HISTSIZE=0

如果HISTSIZE=0写入用户主目录的~/.bashrc文件,那么就不会保留该用户的操作历史。如果写入/etc/profile,整个系统都不会保留操作历史。

环境变量HISTIGNORE可以设置哪些命令不写入操作历史。

1
export HISTIGNORE='pwd:ls:exit'

上面示例设置,pwd、ls、exit这三个命令不写入操作历史。

如果想搜索某个以前执行的命令,可以配合grep命令搜索操作历史。

1
$ history | grep /usr/bin

上面命令返回.bash_history文件里面,那些包含/usr/bin的命令。

操作历史的每一条记录都有编号。知道了命令的编号以后,可以用感叹号 + 编号执行该命令。如果想要执行.bash_history里面的第 8 条命令,可以像下面这样操作。

1
$ !8

history命令的-c参数可以清除操作历史。

1
$ history -c

相关快捷键

下面是一些与操作历史相关的快捷键。

  • Ctrl + p:显示上一个命令,与向上箭头效果相同(previous)。
  • Ctrl + n:显示下一个命令,与向下箭头效果相同(next)。
  • Alt + <:显示第一个命令。
  • Alt + >:显示最后一个命令,即当前的命令。
  • Ctrl + o:执行历史文件里面的当前条目,并自动显示下一条命令。这对重复执行某个序列的命令很有帮助。

感叹号!的快捷键如下。

  • !!:执行上一个命令。
  • !nn为数字,执行历史文件里面行号为n的命令。
  • !-n:执行当前命令之前n条的命令。
  • !string:执行最近一个以指定字符串string开头的命令。
  • !?string:执行最近一条包含字符串string的命令。
  • !$:代表上一个命令的最后一个参数。
  • !*:代表上一个命令的所有参数,即除了命令以外的所有部分。
  • ^string1^string2:执行最近一条包含string1的命令,将其替换成string2

下面是!$和!*的例子。

1
2
3
4
5
6
7
$ cp a.txt b.txt
$ echo !$
b.txt

$ cp a.txt b.txt
$ echo !*
a.txt b.txt

上面示例中,!$代表上一个命令的最后一个参数(b.txt),!*代表上一个命令的所有参数(a.txt b.txt)。

下面是^string1^string2的例子。

1
2
3
$ rm /var/log/httpd/error.log
$ ^error^access
rm /var/log/httpd/access.log

上面示例中,^error^access将最近一条含有error的命令里面的error,替换成access

如果希望确定是什么命令,然后再执行,可以打开histverify选项。这样的话,使用!快捷键所产生的命令,会先打印出来,等到用户按下回车键后再执行。

1
$ shopt -s histverify

其他快捷键

  • Ctrl + j:等同于回车键(LINEFEED)。
  • Ctrl + m:等同于回车键(CARRIAGE RETURN)。
  • Ctrl + o:等同于回车键,并展示操作历史的下一个命令。
  • Ctrl + v:将下一个输入的特殊字符变成字面量,比如回车变成^M。
  • Ctrl + [:等同于 ESC。
  • Alt + .:插入上一个命令的最后一个词。
  • Alt + _:等同于Alt + .

上面的Alt + .快捷键,对于很长的文件路径,有时会非常方便。因为 Unix 命令的最后一个参数通常是文件路径。

1
2
$ mkdir foo_bar
$ cd #按下 Alt + .

上面例子中,在cd命令后按下Alt + .,就会自动插入foo_bar

Bash 算术运算

算术表达式

((...))语法可以进行整数的算术运算。

1
2
3
$ ((foo = 5 + 5))
$ echo $foo
10

((...))会自动忽略内部的空格,所以下面的写法都正确,得到同样的结果。

1
2
3
$ ((2+2))
$ (( 2+2 ))
$ (( 2 + 2 ))

这个语法不返回值,命令执行的结果根据算术运算的结果而定。只要算术结果不是 0,命令就算执行成功。

1
2
3
$ (( 3 + 2 ))
$ echo $?
0

上面例子中,3 + 2的结果是 5,命令就算执行成功,环境变量$?为 0。

如果算术结果为 0,命令就算执行失败。

1
2
3
$ (( 3 - 3 ))
$ echo $?
1

上面例子中,3 - 3的结果是 0,环境变量$?为 1,表示命令执行失败。

如果要读取算术运算的结果,需要在((...))前面加上美元符号$((...)),使其变成算术表达式,返回算术运算的值。

1
2
$ echo $((2 + 2))
4

((...))语法支持的算术运算符:+ - * / % ** ++ --

注意,除法运算符的返回结果总是整数,比如 5 除以 2,得到的结果是 2,而不是 2.5。

1
2
$ echo $((5 / 2))
2

++--这两个运算符有前缀和后缀的区别。作为前缀是先运算后返回值,作为后缀是先返回值后运算。

1
2
3
4
5
6
7
8
9
10
11
$ i=0
$ echo $i
0
$ echo $((i++))
0
$ echo $i
1
$ echo $((++i))
2
$ echo $i
2

上面例子中,++作为后缀是先返回值,执行echo命令,再进行自增运算;作为前缀则是先进行自增运算,再返回值执行echo命令。

$((...))内部可以用圆括号改变运算顺序。

1
2
$ echo $(( (2 + 3) * 4 ))
20

$((...))结构可以嵌套。

1
2
3
4
5
$ echo $(((5**2) * 3))
75
# 等同于
$ echo $(($((5**2)) * 3))
75

这个语法只能计算整数,否则会报错。

1
2
3
# 报错
$ echo $((1.5 + 1))
bash: 语法错误

$((...))的圆括号之中,不需要在变量名之前加上$,不过加上也不报错。

1
2
3
$ number=2
$ echo $(($number + 1))
3

上面例子中,变量number前面有没有美元符号,结果都是一样的。

如果在$((...))里面使用字符串,Bash 会认为那是一个变量名。如果不存在同名变量,Bash 就会将其作为空值,因此不会报错。

1
2
3
4
$ echo $(( "hello" + 2))
2
$ echo $(( "hello" * 2))
0

上面例子中,"hello"会被当作变量名,返回空值,而$((...))会将空值当作 0,所以乘法的运算结果就是 0。同理,如果$((...))里面使用不存在的变量,也会当作 0 处理。

如果一个变量的值为字符串,跟上面的处理逻辑是一样的。即该字符串如果不对应已存在的变量,在$((...))里面会被当作空值。

1
2
3
$ foo=hello
$ echo $(( foo + 2))
2

上面例子中,变量foo的值是hello,而hello也会被看作变量名。这使得有可能写出动态替换的代码。

1
2
3
4
$ foo=hello
$ hello=3
$ echo $(( foo + 2 ))
5

上面代码中,foo + 2取决于变量hello的值。

最后,$[...]是以前的语法,也可以做整数运算,不建议使用。

1
2
$ echo $[2+2]
4

数值的进制

Bash 的数值默认都是十进制,但是在算术表达式中,也可以使用其他进制。

  • number:没有任何特殊表示法的数字是十进制数(以 10 为底)。
  • 0number:八进制数。
  • 0xnumber:十六进制数。
  • base#numberbase进制的数。
1
2
3
4
$ echo $((0xff))
255
$ echo $((2#11111111))
255

上面例子中,0xff是十六进制数,2#11111111是二进制数。

位运算

$((...))支持以下的二进制位运算符。

  • <<:位左移运算,把一个数字的所有位向左移动指定的位。
  • >>:位右移运算,把一个数字的所有位向右移动指定的位。
  • &:位的“与”运算,对两个数字的所有位执行一个AND操作。
  • |:位的“或”运算,对两个数字的所有位执行一个OR操作。
  • ~:位的“否”运算,对一个数字的所有位取反。
  • ^:位的异或运算(exclusive or),对两个数字的所有位执行一个异或操作。
1
2
$ echo $((16>>2))
4
1
2
$ echo $((16<<2))
64
1
2
3
4
5
6
$ echo $((17&3))
1
$ echo $((17|3))
19
$ echo $((17^3))
18

逻辑运算

$((...))支持以下的逻辑运算符。

  • <:小于
  • >:大于
  • <=:小于或相等
  • >=:大于或相等
  • ==:相等
  • !=:不相等
  • &&:逻辑与
  • ||:逻辑或
  • !:逻辑否
  • expr1?expr2:expr3:三元条件运算符。若表达式expr1的计算结果为非零值(算术真),则执行表达式expr2,否则执行表达式expr3

如果逻辑表达式为真,返回 1,否则返回 0。

1
2
3
4
$ echo $((3 > 2))
1
$ echo $(( (3 > 2) || (4 <= 1) ))
1

三元运算符执行一个单独的逻辑测试。它用起来类似于if/then/else语句。

1
2
3
4
5
$ a=0
$ echo $((a<1 ? 1 : 0))
1
$ echo $((a>1 ? 1 : 0))
0

上面例子中,第一个表达式为真时,返回第二个表达式的值,否则返回第三个表达式的值。

赋值运算

算术表达式$((...))可以执行赋值运算。

1
2
3
4
$ echo $((a=1))
1
$ echo $a
1

上面例子中,a=1对变量a进行赋值。这个式子本身也是一个表达式,返回值就是等号右边的值。

$((...))支持的赋值运算符,有以下这些。

  • parameter = value:简单赋值。
  • parameter += value:等价于parameter = parameter + value
  • parameter -= value:等价于parameter = parameter – value
  • parameter *= value:等价于parameter = parameter * value
  • parameter /= value:等价于parameter = parameter / value
  • parameter %= value:等价于parameter = parameter % value
  • parameter <<= value:等价于parameter = parameter << value
  • parameter >>= value:等价于parameter = parameter >> value
  • parameter &= value:等价于parameter = parameter & value
  • parameter |= value:等价于parameter = parameter | value
  • parameter ^= value:等价于parameter = parameter ^ value
1
2
3
$ foo=5
$ echo $((foo*=2))
10

如果在表达式内部赋值,可以放在圆括号中,否则会报错。

1
$ echo $(( a<1 ? (a+=1) : (a-=1) ))

求值运算

逗号,$((...))内部是求值运算符,执行前后两个表达式,并返回后一个表达式的值。

1
2
3
4
$ echo $((foo = 1 + 2, 3 * 4))
12
$ echo $foo
3

上面例子中,逗号前后两个表达式都会执行,然后返回后一个表达式的值 12。

expr 命令

expr命令支持算术运算,可以不使用((...))语法。

1
2
$ expr 3 + 2
5

expr命令支持变量替换。

1
2
3
$ foo=3
$ expr $foo + 2
5

expr命令也不支持非整数参数。

1
2
$ expr 3.5 + 2
expr: 非整数参数

上面例子中,如果有非整数的运算,expr命令就报错了。

let 命令

let命令用于将算术运算的结果,赋予一个变量。

1
2
3
$ let x=2+3
$ echo $x
5

注意,x=2+3这个式子里面不能有空格,否则会报错。

Bash 字符串操作

字符串的长度

获取字符串长度的语法如下。

1
${#varname}
1
2
3
$ myPath=/home/cam/book/long.file.name
$ echo ${#myPath}
29

大括号{}是必需的,否则 Bash 会将$#理解成脚本的参数个数,将变量名理解成文本。

1
2
$ echo $#myvar
0myvar

上面例子中,Bash 将$#myvar分开解释了。

子字符串

字符串提取子串的语法如下。

1
${varname:offset:length}

上面语法的含义是返回变量$varname的子字符串,从位置offset开始(从 0 开始计算),长度为length

1
2
3
$ count=frogfootman
$ echo ${count:4:4}
foot

上面例子返回字符串frogfootman从 4 号位置开始的长度为 4 的子字符串foot

这种语法不能直接操作字符串,只能通过变量来读取字符串,并且不会改变原始字符串。

1
2
# 报错
$ echo ${"hello":2:3}

上面例子中,"hello"不是变量名,导致 Bash 报错。

如果省略length,则从位置offset开始,一直返回到字符串的结尾。

1
2
3
$ count=frogfootman
$ echo ${count:4}
footman

如果offset为负值,表示从字符串的末尾开始算起。注意,负数前面必须有一个空格, 以防止与${variable:-word}的变量的设置默认值语法混淆。这时还可以指定lengthlength可以是正值,也可以是负值(负值不能超过offset的长度)。

1
2
3
4
5
6
7
$ foo="This string is long."
$ echo ${foo: -5}
long.
$ echo ${foo: -5:2}
lo
$ echo ${foo: -5:-2}
lon

上面例子中,offset为 -5,表示从倒数第 5 个字符开始截取,所以返回long.。如果指定长度length为 2,则返回lo;如果length为 -2,表示要排除从字符串末尾开始的 2 个字符,所以返回lon

搜索和替换

Bash 提供字符串搜索和替换的多种方法。

字符串头部的模式匹配。

以下两种语法可以检查字符串开头,是否匹配给定的模式。如果匹配成功,就删除匹配的部分,返回剩下的部分。原始变量不会发生变化。

1
2
3
4
5
6
7
# 如果 pattern 匹配变量 variable 的开头,
# 删除最短匹配(非贪婪匹配)的部分,返回剩余部分
${variable#pattern}

# 如果 pattern 匹配变量 variable 的开头,
# 删除最长匹配(贪婪匹配)的部分,返回剩余部分
${variable##pattern}

上面两种语法会删除变量字符串开头的匹配部分(将其替换为空),返回剩下的部分。区别是一个是最短匹配(又称非贪婪匹配),另一个是最长匹配(又称贪婪匹配)。

匹配模式pattern可以使用*、?、[]等通配符。

1
2
3
4
5
6
7
$ myPath=/home/cam/book/long.file.name

$ echo ${myPath#/*/}
cam/book/long.file.name

$ echo ${myPath##/*/}
long.file.name

上面例子中,匹配的模式是/*/,其中*可以匹配任意数量的字符,所以最短匹配是/home/,最长匹配是/home/cam/book/

下面写法可以删除文件路径的目录部分,只留下文件名。

1
2
3
4
$ path=/home/cam/book/long.file.name

$ echo ${path##*/}
long.file.name

上面例子中,模式*/匹配目录部分,所以只返回文件名。

1
2
3
4
5
$ phone="555-456-1414"
$ echo ${phone#*-}
456-1414
$ echo ${phone##*-}
1414

如果匹配不成功,则返回原始字符串。

1
2
3
$ phone="555-456-1414"
$ echo ${phone#444}
555-456-1414

上面例子中,原始字符串里面无法匹配模式 444,所以原样返回。

如果要将头部匹配的部分,替换成其他内容,采用下面的写法。

1
2
3
4
5
6
7
# 模式必须出现在字符串的开头
${variable/#pattern/string}

# 示例
$ foo=JPG.JPG
$ echo ${foo/#JPG/jpg}
jpg.JPG

上面例子中,被替换的JPG必须出现在字符串头部,所以返回jpg.JPG

字符串尾部的模式匹配。

以下两种语法可以检查字符串结尾,是否匹配给定的模式。如果匹配成功,就删除匹配的部分,返回剩下的部分。原始变量不会发生变化。

1
2
3
4
5
6
7
# 如果 pattern 匹配变量 variable 的结尾,
# 删除最短匹配(非贪婪匹配)的部分,返回剩余部分
${variable%pattern}

# 如果 pattern 匹配变量 variable 的结尾,
# 删除最长匹配(贪婪匹配)的部分,返回剩余部分
${variable%%pattern}

上面两种语法会删除变量字符串结尾的匹配部分(将其替换为空),返回剩下的部分。区别是一个是最短匹配(又称非贪婪匹配),另一个是最长匹配(又称贪婪匹配)。

1
2
3
4
5
6
7
$ path=/home/cam/book/long.file.name

$ echo ${path%.*}
/home/cam/book/long.file

$ echo ${path%%.*}
/home/cam/book/long

上面例子中,匹配模式是.*,其中*可以匹配任意数量的字符,所以最短匹配是.name,最长匹配是.file.name

下面写法可以删除路径的文件名部分,只留下目录部分。

1
2
3
4
$ path=/home/cam/book/long.file.name

$ echo ${path%/*}
/home/cam/book

上面例子中,模式/*匹配文件名部分,所以只返回目录部分。

下面的写法可以替换文件的后缀名。

1
2
3
$ file=foo.png
$ echo ${file%.png}.jpg
foo.jpg

上面的例子将文件的后缀名,从.png改成了.jpg

下面再看一个例子。

1
2
3
4
5
$ phone="555-456-1414"
$ echo ${phone%-*}
555-456
$ echo ${phone%%-*}
555

如果匹配不成功,则返回原始字符串。

如果要将尾部匹配的部分,替换成其他内容,采用下面的写法。

1
2
3
4
5
6
7
# 模式必须出现在字符串的结尾
${variable/%pattern/string}

# 示例
$ foo=JPG.JPG
$ echo ${foo/%JPG/jpg}
JPG.jpg

上面例子中,被替换的JPG必须出现在字符串尾部,所以返回JPG.jpg

任意位置的模式匹配。

以下两种语法可以检查字符串内部,是否匹配给定的模式。如果匹配成功,就删除匹配的部分,换成其他的字符串返回。原始变量不会发生变化。

1
2
3
4
5
6
7
# 如果 pattern 匹配变量 variable 的一部分,
# 最长匹配(贪婪匹配)的那部分被 string 替换,但仅替换第一个匹配
${variable/pattern/string}

# 如果 pattern 匹配变量 variable 的一部分,
# 最长匹配(贪婪匹配)的那部分被 string 替换,所有匹配都替换
${variable//pattern/string}

上面两种语法都是最长匹配(贪婪匹配)下的替换,区别是前一个语法仅仅替换第一个匹配,后一个语法替换所有匹配。

1
2
3
4
5
6
7
$ path=/home/cam/foo/foo.name

$ echo ${path/foo/bar}
/home/cam/bar/foo.name

$ echo ${path//foo/bar}
/home/cam/bar/bar.name

上面例子中,前一个命令只替换了第一个foo,后一个命令将两个foo都替换了。

下面的例子将分隔符从:换成换行符。

1
2
3
4
5
$ echo -e ${PATH//:/'\n'}
/usr/local/bin
/usr/bin
/bin
...

上面例子中,echo命令的-e参数,表示将替换后的字符串的\n字符,解释为换行符。

模式部分可以使用通配符。

1
2
3
$ phone="555-456-1414"
$ echo ${phone/5?4/-}
55-56-1414

上面的例子将5-4替换成-

如果省略了string部分,那么就相当于匹配的部分替换成空字符串,即删除匹配的部分。

1
2
3
4
$ path=/home/cam/foo/foo.name

$ echo ${path/.*/}
/home/cam/foo/foo

上面例子中,第二个斜杠后面的string部分省略了,所以模式.*匹配的部分.name被删除后返回。

前面提到过,这个语法还有两种扩展形式。

1
2
3
4
5
# 模式必须出现在字符串的开头
${variable/#pattern/string}

# 模式必须出现在字符串的结尾
${variable/%pattern/string}

改变大小写

下面的语法可以改变变量的大小写。

1
2
3
4
5
# 转为大写
${varname^^}

# 转为小写
${varname,,}
1
2
3
4
5
$ foo=heLLo
$ echo ${foo^^}
HELLO
$ echo ${foo,,}
hello
  • Copyrights © 2017-2023 WSQ
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信