1. 简介

shell 是 Linux 中用于访问和控制操作系统的程序,提供了交互式的界面,也可以通过编写 shell 脚本文件,直接执行文件。

以下主要介绍 Linux 众多 shell 中的 bash(Bourne Again Shell),它的位置在 /bin/bash 或 /usr/bin/bash。

查看当前的 shell:

echo $0

如果系统登陆的默认 shell 不是 bash,可以输入 bash 命令直接切换到 bash。

2. 脚本

我们编写的脚本文件习惯扩展名 sh,如 test.sh,但不是必须的,只要它是有执行(x)权限的文本文件,就可以被当作 shell 脚本执行。

# 执行脚本
./test.sh

# 指定 shell 执行脚本
bash test.sh

shell 脚本通常习惯在开头加上以下内容,表示执行脚本选择的 shell。

#!/bin/bash

默认情况下,以上的执行方式,执行 shell 脚本会启动一个新的 shell 子进程来执行脚本的命令,脚本内无法看到当前的变量。

可以在执行的时候不启动 shell 子进程,直接在当前环境执行,就可以访问和修改当前环境的所有变量、环境、工作目录。

source test.sh
. test.sh # . 是 source 的简写

也可以通过将变量导出为环境变量的方式,使变量被当前 shell 和所有子进程可见。

export var=123

脚本调用可以传参,并在脚本中获取。

# 调用传参
./test.sh good 12

# 获取参数
$# # 参数个数
$0 # 程序名称
$1 # 第1个参数
$2 # 第2个参数,依次类推,获取不到返回空
$* # 所有参数
$@ # 所有参数,加双引号输出参数

脚本中还可以获取脚本执行的信息和状态。

$$ # 当前进程id
$! # 后台运行的最后一个进程id
$- # shell当前选项
$? # 上一条命令的退出状态,0表示成功,其他表示错误

3. 注释

单行注释以 # 开头,直到一行的结尾。

# 注释
echo $var # 注释

多行注释,开始和结束标识符相等的范围内为注释,标识符不是固定的。

:<<EOF
注释
注释
EOF

:<<'
注释
注释
'

:<<!
注释
注释
!

# 简化版本,表示符必须为单引号
: '
注释
注释
'

4. 变量

变量通过等号赋值的方式定义,等号左右不能有空格。

字符串变量:

var=good
var='good' # 支持内部空格
var="good" # 支持内部空格和引用变量,推荐使用

# 获取字符串长度
${#var}

# 获取字符串子串,指定offset和limit
${var:1:4}

整数变量:

declare -i var=123

数组变量:

# 一起定义
var=(1 2 3 4 5)

# 一个个下标定义
var[0]=43
var[1]=3453
var[2]=100

# 读取元素
${var[1]}
${var[@]} # 获取所有元素

# 从a到b的整数组成的数组
{a..b}
$(seq a b)

关联数组变量:

# 定义
declare -A site=(["google"]="www.google.com" ["taobao"]="www.taobao.com")

# 设置键值
site["baidu"]="www.baidu.com"

# 读取元素
${site["baidu"]}

使用变量时,在变量名前加上 $,花括号是可选的。

$var
${var} # 可选,可以和后面的内容区分

变量默认为本地变量,本地变量只能在当前 shell 使用。

环境变量是导出的变量,对当前 shell 和所有子进程可见,但子进程中的修改不会影响父 shell。

# 赋值并导出
export var=123

# 先定义后导出
var=123
export var

环境变量可以取消导出。

# 彻底删除变量
unset var

# 取消导出,但仍是本地变量
export -n var

将变量定义为只读变量后,无法更改它的值,也无法删除。

# 直接定义
readonly var=123

# 先定义后设为只读
var=123
readonly var

declare 命令用于声明变量和设置变量属性。

# 整数
declare -i num=100

# 索引数组
declare -a arr=("a" "b")

# 关联数组
declare -A dict=([key1]="val1")

# 只读
declare -r const=12345

# 环境变量
declare -x var="global"

# 在函数内声明全局变量
declare -g var="global"

# 变量值被赋值时自动转为小写
declare -l var="hello"

# 变量值被赋值时自动转为大写
declare -u var="hello"

# 声明为引用,修改ref会同时修改var
declare -n ref=var

# 为变量添加trace属性,调试用
declare -t var

# 显示变量的定义
declare -p var

5. 运算符

通过 expr 计算表达式并求值,表达式可以是算数运算、字符串操作、正则匹配。以下出现的部份运算符需要加 \ 转义。

成功时输出结果到标准输出,失败时返回非零状态码,通过 $? 检查 0 或 1 表示逻辑真假。

expr <expression>

将 expr 表达式计算的值赋值给变量:

var=$(expr 1 + 2)

算数运算,仅支持整数运算,运算符和数字之间要用空格隔开。

# 加法
expr 5 + 3

# 减法
expr 10 - 2

# 乘法
expr 2 \* 3

# 除法
expr 10 / 2

# 取模
expr 10 % 3

字符串操作。

# 计算长度
expr length $var
expr $var : '.*'

# 提取子串,指定offset和limit,下标从1开始
expr substr $var 2 10

# 查找子串位置,下标从1开始
expr index $var "substr"

逻辑比较,返回 0 或 1。

# 字符串相等
expr "a" = "a"

# 字符串不等
expr "a" != "b"

# 字符串大小比较
expr "a" \< "b"
expr "a" \> "b"

# 逻辑与
expr 1 \& 1

# 逻辑或
expr 0 | 1

正则匹配,仅用于简单场景。

# 计算从开头匹配的长度
expr "abc123" : '[a-z]*'

# 提取匹配的子串,两个$为固定前后包围
expr "abc123" : '$[a-z]*$'

expr 适用于兼容较旧的环境,现代新版本系统建议使用替换方案:

# 整数运算
let "var = 3 + 5"
(( var = 3 + 5 )) // let的简化版
var=$((3 + 5))

# 字符串操作
${#var} # 计算长度
${var:1:4} # 获取子串

数字运算符。

# 相等
if [ $a == $b ]
if [ $a -eq $b ]

# 不等
if [ $a != $b ]
if [ $a -ne $b ]

# 大于
if [ $a -gt $b ]
(( $a > $b ))

# 小于
if [ $a -lt $b ]
(( $a < $b ))

# 大于等于
if [ $a -ge $b ]

# 小于等于
if [ $a -le $b ]

# 自增
let num++
((num++))

# 自减
let num--
((num--))

字符串运算符。

# 相等
if [ $a = $b ]

# 不等
if [ $a != $b ]

# 是否为空
if [ -z $a ]

# 是否非空
if [ -n $a ]
if [ $a ]

布尔运算符,返回 true 或 false。

# 非运算
if [ ! false ]

# 与运算
if [ $a -gt 50 -a $b -lt 100 ]
if [ $a -gt 50 && $b -lt 100 ]

# 或运算
if [ $a -lt 50 -o $b -gt 100 ]
if [ $a -lt 50 || $b -gt 100 ]

文件运算符。

# 文件存在
if [ -e $file ]

# 普通文件
if [ -f $file ]

# 目录
if [ -d $file ]

# 文件不为空
if [ -s $file ]

# 文件是socket
if [ -S $file ]

# 文件是符号链接
if [ -L $file ]

# 文件可读
if [ -r $file ]

# 文件可写
if [ -w $file ]

# 文件可执行
if [ -x $file ]

6. 语句

if 条件判断语句,可以根据情况搭配 elif、else,最后必须要用 fi 结束。

if condition
then
  command
fi

if condition
then
  command1
else
  command2
fi

if condition1
then
  command1
elif condition2 
then 
  command2
else
  command3
fi

# 可以写成一行,原本每一行的结尾加上;
if [ condition ]; then command; fi

for 循环语句遍历数组或条件并执行命令。

for var in array
do
  command
done

# 遍历某一范围的整数
for i in $(seq 0 99)
for i in {0..99}
for ((i=0; i<100; i++))

# 无限循环
for (( ; ; ))

while 循环语句重复执行命令,直到条件不成立。

while condition
do
  command
done

# 无限循环
while :
while true

until 循环语句执行命令直至条件为真。

until condition
do
  command
done

判断语句有几种写法,if、for、while、until 同理。

if [ $a -gt $b ]
if (( $a > $b ))
if test $a -gt $b

break 语句可以跳出以上各个循环语句,continue 语句可以跳过当前循环。

case 多选择语句,匹配变量对应值的条件并执行,;; 表示跳出语句。

case $var in
  1)
  command1
  ;;
  2)
  command2
  ;;
esac

7. 函数

定义函数:

function func()
{
  command
}

# function可忽略
func()
{
  command
}

函数内通过 $1 $2 等获取参数。

函数可以通过 return 返回值,如果不返回,则以最后一条命令的运行结果作为返回值。

函数调用:

# 调用无参数函数
func

# 调用函数并带参数
func "good" "morning"

8. 重定向

文件描述符中,0表示标准输入(STDIN),1表示标准输出(STDOUT),2表示错误输出(STDERR)。

命令的输入可以重定向从文件输入,输出可以重定向到文件。

# 输入重定向到文件
command < file

# 输出重定向到文件,将覆盖文件原有内容
command > file

# 输出重定向到文件,追加到末尾
command >> file

# 重定向输入和输出
command < file1 > file2

# 只将标准错误重定向到文件
command 2>> file

# 将标准输出和标准错误一起重定向到文件
command 2>&1 >> file

# 将输出丢弃
command 2>&1 > /dev/null