2

[TOC]

认识 Shell

什么是 Shell

Shell 是命令解释器, 将命令解释给内核来执行, 它是用户和内核之间的中间层.

用户
👇
Shell
👇
内核

查看所有的shell类型

cat /etc/shells
/bin/sh
/bin/bash            # 基于 bsh 的加强重构版, 是 CentOS 7 和 Ubuntu 的默认 Shell
/usr/bin/sh
/usr/bin/bash
/bin/tcsh
/bin/csh

Linux 的启动过程

启动顺序从上至下

        BIOS                基本的输入输出系统
        |
        MBR                    硬盘的主引导部分(前446字节是主引导记录, 前512除了主引导记录还包括磁盘分区表)
        |
        BootLoader(grub)    启动和引导内核的工具, 目前使用的是 /boot/grub2
        |
        kernel                内核
CentOS 7 |   CentOS 6   
        /  \       
systemd        init                  1号进程(在CentOS 6中是 /usr/sbin/init 进程, CentOS 7则是 /usr/lib/systemd/systemd )
      |         |
系统初始化    由shell脚本完成引导      CentOS 7 中有一部分是由systemd配置, 应用程序引导. 系统初始化仍是由shell脚本完成.
      |
   shell

CentOS 6 在 init 后面的引导会和CentOS 7有略微的差异.

在CentOS 6 中, init 的引导步骤

  1. /etc/rc.d/rc.sysinit 系统初始化工作
  2. 等待用户终端接入

在CentOS 7中, systemd 的步骤

  1. /etc/systemd/system 读取启动级别
  2. /usr/lib/systemd/system 读取各个service
# 导出主引导记录
dd if=/dev/sda of=mbr.bin bs=446 count=1
hexdump -C mbr.bin                            # 查看主引导记录

# 导出主引导记录和磁盘所有分区表
dd if=/dev/sda of=mbr2.bin bs=512 count=1
hexdump -C mbr2.bin                            # 最后面以 55aa 结尾表示可引导

如何编写shell脚本

标准的 Shell 脚本要包含的元素

  • Sha-Bang

    首行的 #! 开头的部分

    文本文件首行添加 #!/bin/bash 可以在以 ./脚本.sh 这种方式执行脚本时声明当前是 bash 脚本, 系统会自行选择对应的 shell 来执行, 若是以 bash 脚本.sh 则会被视为注释.

  • # 开头的视为注释
  • 脚本执行权限, 若是二进制可执行文件只需要 x, 若是文本文件则需要 rx
  • 通常约定bash脚本的扩展名为 .sh
  • 在一行中可使用 ; 分隔多条命令, 会依次按顺序逐个执行命令, 只有在前一个命令执行完才会执行后一个命令.

确保脚本执行错误时马上退出

可在脚本开头设置: set -e , 从而告知 bash, 若有任何语句执行失败, 就直接退出脚本, 防止错误像滚雪球般变大.

此时 $? 无法使用

若在 set -e 模式下为了确保某些语句失败不退出脚本, 可采用如下方式

# 方法1
command || { echo "command failed"; exit 1; }

# 方法2
if ! command; then
    echo "command failed"
    exit 1
fi

关于 set 更多参见 set 命令

shell 脚本执行方式

执行命令的4种方式

  • bash file.sh

    会在当前终端下产生一个 bash 子进程, 再由该子进程去执行该脚本.

    这种方式无需赋予脚本执行权限
  • ./file.sh

    同样会在当前终端下产生一个子进程, 会根据脚本的 Sha-Bang(即第一行的 #!/path/to/bash) 来解释该脚本.

    比如python脚本第一行是 #!/usr/bin/python

    需要 赋予脚本执行权限

  • source file.sh

    在当前进程执行该脚本, 脚本中的操作会影响当前环境.

    这种方式同样无需赋予脚本执行权限.

    由于是在当前进程执行, 因此操作会影响当前进程, eg 脚本中的 cd /tmp 同样会改变当前环境的工作目录.

  • . file.sh

    .source 的缩写, 等价于 source file.sh

补充

  • exec <command>

    使用 command 进程替换当前进程, PID 不变, command 执行完后直接退出.

内建命令和外部命令的区别

内建命令

  • 不需要创建子进程
  • 对当前 Shell 生效
比如 cd 是内建命令, 执行时会切到当前 Shell 的工作目录

shell 选项

shopt 命令

查看/设置shell选项

shopt [选项] [<选项名>]

示例
    shopt              # 查看所有选项及其值
    shopt <选项名>        # 查看指定选项的值

选项
    -s        set, 设置值为 on
    -u        unset, 设置值为 off
    
部分重要选项
    login_shell            # 指示当前是 login shell 还是 non-login shell

实用示例

获取当前脚本所在目录

# 将当前目录保存到变量 PWD 中(注意命令)
PWD="$(cd $(dirname ${BASH_SOURCE[0]}) && pwd -P)"

BASH_SOURCE 变量是一个数组, 其第一个参数是当前脚本名

若使用 source 来执行脚本时, 参数 $0 的值是父脚本的名字, 而不是当前脚本的名字.

打印消息时带日期时间

log() {
  echo $(date "+%Y-%m-%d %H:%M:%S")" "$*
}

log "lala"

管道与重定向

一个进程默认会打开标准输入、标准输出、标准错误三个文件描述符.

  • 标准输入默认是由终端输入
  • 标准输出和标准错误默认是输出到终端

管道与管道符 |

管道和信号都是进程通信的方式之一.

匿名管道(管道符 | )是 Shell变成经常用到的通信工具.

管道符 | , 将前一个命令执行的结果传递给后面的命令.

  • 管道实际上是将不同的进程的标准输出和标准输入做一个连接.
  • 将前一个命令的 标准输出 连接到下一个命令的 标准输入
  • 管道符是通过创建子进程的方式来运行的

    子进程如果是一个 shell, 则称之为 子shell.

    在有管道符的命令中运行内建命令其实是在新的子shell中执行的, 不会影响当前shell环境, 这个需要理解好.

    因此一般避免在管道符中使用内建命令

  • 如果连接的是外部命令, 则会按顺序同时建立多个子进程来分别执行外部命令, 同时按顺序连接各个标准输出和标准输入

示例

ps | cat
echo 123 | ps

示例

image-20200227112936228

cat 子进程的标准输出(1)重定向至匿名管道(463978), 而 less 的标准输入(0)重定向至同一个匿名管道(463978), 也就是 cat 的标准输出通过匿名管道连接重定向至 less 的标准输入.

注意管道符和分号的区别:

  • 分号隔开的多条命令是没有任何关系的, 且每次只会执行一条, 执行完后才会继续下一条.
  • 管道符隔开的多条命令是具有输入输出重定向关系的, 会几乎同时启动(实际顺序是从做到右), 其中执行的命令(包括内建命令)都是在新的子进程中执行, 而不是在当前shell中执行.

返回码

正常可以使用 $? 获取上一条命令的执行结果状态码(0 正常, 非0异常)

但是若是在执行一条管道后使用 $? 获取的只是管道最后一个指令执行返回的状态码

$PIPESTATUS 变量类似 $?, 但它保存的是管道中每个命令的返回码

  • ${PIPESTATUS[0]} 表示管道中第一个命令的返回码
  • 若上一条命令不是管道, 同样会更新 `$PIPESTATUS

参考: https://www.cnblogs.com/suane...

重定向符号

重定向符号实际上是将进程的标准输入和标准输出与文件建立连接.

  • 利用文件代替终端输入
  • 利用文件代替终端输出

所有重定向符号包括

  • 输入重定向

    • <
    • <<EOF
    • <<"EOF" 不转义特殊字符
    • <<'EOF' 不转义特殊字符
  • 输出重定向

    语法
        [<文件描述符=1>]<重定向符号>
    
    参数解释
        <文件描述符>
            1        标准输出(不写则默认)
            2        标准错误
            &        标准输出和标准输出
            
        <重定向符号>
            >        清空并写入
            >>        追加写入
            
    示例
        &>>                        # 将标准输出和标准错误重定向至文件, 并追加写入
        1>1.txt 2>>2.txt        # 将标准输出重定向至 1.txt 文件并清空该文件后写入. 同时将标准错误重定向至 2.txt 文件并追加写入.
        # 组合使用输入和输出重定向(转义内容, 其中的变量会被替换, `whoami` 命令会被执行并替换)
        cat > /path/to/file <<EOF
        i am $USER
        `whoami`
        EOF
        # 组合使用输入和输出重定向(不转义内容)
        cat >> /var/spool/cron/root <<'EOF'
  EOF





### 使用输入重定向来代替标准输入

输入重定向

read <变量名> <<EOF
123
EOF

从文件输入重定向

echo 123 > tmp.txt
read <变量名> < /tmp.txt


> 不能使用管道, 因为管道里的命令是在新的进程中执行, 读取的变量无法影响当前环境
>
> ```sh
> # 无效
> echo "123" | read <变量名>
> ```
>
> 



## xargs 命令

从标准输入构建并执行命令行

- 适用于待执行命令只能从参数中而不是标准输入读取值的情况

xargs [选项] <command=echo>

选项

分隔符
-d                        # 定义输入分隔符, 默认是空白和换行
--null, -0                # 以 null 作为分隔符, 常搭配使用(文件名可能有空格, 反斜杠等) `find -print0 | xargs -0`

分割成多个命令并分别执行
-n <n>                    # 将最多 <n> 个参数用于构建一个命令行
-L <n>                    # 如果标准输入包含多行,-L参数指定多少行作为一个命令行参数(分别执行多次命令). 一般更常用 -n

-I <替换符>              # 使用-I指定一个替换字符串,这个字符串在xargs扩展时会被替换掉,当-I与xargs结合使用,每一个参数命令都会被执行一次

--interactive, -p        # 逐个命令确认是否执行, 只有回复  y 或 Y 开头的才会执行, 否则略过
--verbose, -t            # 在执行之前在标准错误输出显示待执行的命令

--max-procs <n>            # 最多同时运行多少个进程, 默认是 1, 如果是 0 则表示不限制. 可与 -n 配合, 避免只执行一次 exec

用法

<前一个命令> | xargs            # 通过管道符重新构建待执行命令
xargs                        # 由用户手动输入(Ctrl+D 结束输入), 并构建待执行命令

示例: echo, rm, mkdir, ls 等命令

# 简单的 echo 示例
echo 123 | xargs echo
# 使用每2个参数执行一次命令
echo {0..9} | xargs -n 2 echo
# echo 执行了3次(以下几种等效)
echo -e "a\nb\nc" | xargs -L 1 echo
echo -e "a\nb\nc" | xargs -n 1 echo
echo "a b c" | xargs -n 1 echo
# 找出所有 TXT 文件以后,对每个文件搜索一次是否包含字符串abc。
find . -name "*.txt" -print0 | xargs -0 grep "abc"





# 变量

## 变量定义

Shell 的变量不区分类型



命名规则

- 字母、数字、下划线
- 不以数字开头



## 变量赋值

### read 命令

交互方式

通过标准输入读取变量值

read [选项] 变量名

选项

-a             # array  assign the words read to sequential indices of the array
            # variable ARRAY, starting at zero
-p <prompt>    # 在读取变量前, 打印一个提示文本(不换行)
-r            # 不解析反斜杠, 即读入原始值(正常使用时推荐)

> 写Shell脚本时一边会避免用交互式来给变量复制, 除非是有必要.





*持续读取管道中的数据*

seq 1 10 | while read line; do echo $line; done;






### 非交互方式

注意: `=` 相邻的左右两侧不允许出现空格.



 **字符串赋值**

变量名=变量值

eg.

a=123


> 等号的左右不允许出现空格
>
> 变量值包含空格时, 需要用 `""` 或 `''` 包含起来
>
> 单引号, 双引号的区别:
>
> - 单引号: 不会对变量值中的引用命令、引用变量、转义字符等进行解析。
> - 双引号: 会解析变量中的引用命令、引用变量、转义字符, 再将解析后的值赋值给变量名



**数学表达式赋值**

let 变量名=变量值

示例

let a=10+20            # a 的值是 30

> 变量值只能是数学表达式
>
> 由于 Shell 的计算性能较差, 一般不怎么用来计算





**将命令赋值给变量**

变量名="命令"

如果变量的值是可执行命令, 则可直接使用 $<变量名> 来执行命令.
但如果要将运行结果赋值给另一个变量, 则需配合 $ 或 `

示例

直接执行变量值
l="ls -hl";     $l                # 此时等价执行了 ls -hl

将变量值执行结果赋值给另一个变量
cmd="uptime";    result=$($cmd)    # 将 uptime 运行后的结果赋值给 result 变量
cmd="uptime";    result=`$cmd`    # 同上

> 常用于拼接命令后再执行



**将命令结果赋值给变量**

变量名=$(命令)
变量名=命令

示例

current=`pwd`
current=$(pwd)





## 变量的引用

- `${变量名}` 表示对变量的引用

- `echo ${变量名}` 查看变量的值

- `${变量名}` 在部分情况下可以省略为 `$变量名`

  > 部分清空指: 在变量名后面紧跟其他字符时
  
- `${!变量名}` 对变量的引用的引用

t1=t2
t2="i am t2"
echo $t1 # 输出 "t2"
echo ${!t1} # 输出 "i am t2"






## 变量的作用范围

变量的作用范围默认只在当前Shell中, 其父、子、平行 Shell 都是不可见的.



> 如果想要某个脚本中定义的变量在当前Shell生效, 则有两种方法:
>
> - 在当前shell中执行 `source 脚本.sh` 或 `. 脚本.sh`
> - 



**变量的导出 export**

在父进程中执行 export, 从而让子进程能够获取父进程中的变量.

export 变量名[=变量值]


> 在子进程中对父进程的变量名修改在父进程是不感知的(无效), 但是在子孙进程是有效的.



**删除变量 unset**

删除变量

unset 变量名




## 系统环境变量

### 临时设置环境变量

环境变量指的是: 每个 Shell 打开都可以获得到的变量.

> 环境变量都是经过 export 的, 因此对其修改会影响到子进程.



若只是想临时修改某个环境变量来执行某个程序, 那么可以方便地类似如下所示:

<环境变量名>=<环境变量值> <命令>

示例

LANG=c man iptables        # 查看英文版本的man帮助





### 部分变量解释

#### 环境变量

**很重要**

PATH # 当前命令的搜索路径, 用 : 分隔. 可以用如下方式新增命令搜索路径

        # PATH=$PATH:/path/to/bin
        

PS1 # 当前终端提示文本




**知道即可**

USER # 当前用户名
UID # 当前用户id




#### 预定义变量

$? # 上一条命令是否正确执行, 0 表示正确, 1 表示有出错.

$$ # 当前进程号

$0 # 所属进程的进程名, 而不是脚本名!!! 这个概念不一样

        # 如果用 bash 方式来执行脚本则该值是脚本名, 如果用 source的方式则该值是父进程的进程名.
        # 这个联系之前的脚本执行方式很容易理解

$LINENO # shell脚本当前的行号


> `$?` 常用于判定上一条命令是否正确执行, 从而实现脚本自动化处理异常.



#### 位置变量

$* # 脚本执行的所有参数
$@ # 脚本执行的所有参数
$# # 参数个数

$1 # 第1个参数
...
$9
${10} # 第10个参数, 此时不能省略大括号




`$*` 与 `$@` 不同之处在于用双引号括起来时行为不一样

当传入参数为 a b c 时

"$*" # "a b c"
"$@" # 'a' 'b' 'c










### env 命令

查看当前的所有变量(包括环境变量)

env




### set 命令

修改shell环境运行参数

set # 显示所有环境变量和Shell环境参数
set [参数] [-o option-name] [arg ...] # 设置shell环境参数

选项

-u                # 使用到不存在的变量时报错(unbound variable)并终止脚本(默认忽略), 等价 -o nounset
-x                # 打开回显, 每个命令执行的时候会输出所执行的命令, 方便调试复杂脚本(默认不打开), 等价 -o xtrace

-e                # 命令运行失败时退出脚本, 防止错误累计, 实际开发建议打开(默认忽略), 等价 -o xtrace
                # 注意不适用于管道(除非是在管道的最后一个子命令)
+e                # 适用于临时关闭 `-e`

-o pipefail        # 管道中任意一个子命令失败都退出脚本(默认不会, 即使打开 -e)

常用写法

set -euxo pipefail
set -eux -o pipefail

也可以在执行 bash 脚本时从命令行传入:

bash -euxo pipefail script.sh



`set` 部分参考: http://www.ruanyifeng.com/blog/2017/11/bash-set.html



#### set -e

开启 `set -e` 后若部分语句允许失败(或失败后需要执行其他逻辑), 则可采用如下写法

写法1

command || true
command || { echo "fail"; }

写法2

if !command; then

:

fi

写法3

set +e # 临时取消 -e

do something

set -e # 恢复 -e








### 环境变量配置文件

配置文件

- `/etc/profile`
- `/etc/profile.d/*`
- `~/.bash_profile`
- `~/.bashrc`
- `/etc/bashrc`





#### 从存储位置划分:

- `/etc/` 下的配置是所有用户通用

- `~/` 下的配置是仅个人有效





#### 从文件类型划分:

- `profile`

  配置环境变量

- `bashrc`

  别名及函数定义





#### 根据 login 和 no-longin shell 划分

用户在登录时分为以下两种 Shell 



可以通过如下命令查看当前属于哪种

shopt login_shell # on 表示 login shell, off 表示 non-login shell






**login shell**

- 包括: `su - `

- 会加载 `profile` 和 `bashrc` 类文件.

- 加载顺序如下

su - root

loading /etc/profile
loaded /etc/profile

loading ~/.bash_profile
loading ~/.bash_rc
loading /etc/bashrc
loaded /etc/bashrc
loaded ~/.bash_rc
loaded ~/.bash_profile

graph TB

1(/etc/profile) --步骤 1--> 1
2(/root/.bash-profile) --步骤 2--> 3
3(/root/.bash-rc) --步骤 3--> 4
4(/etc/bashrc) --步骤4--> 3
3 --步骤5--> 2

> 上述图中
>
> 由于 `~` 会被转义, 因此用具体的 `/root/` 替代
>
> 由于 `_` 显示不出来, 因此用 `-` 替代





**no-login shell**

- 包括: `su` 不加减号

- 仅 `bashrc` 类的文件会被加载到.

- 何时开始加载: 当运行 `bash`时

- 加载顺序如下:

su root

loading ~/.bash_rc
loading /etc/bashrc
loaded /etc/bashrc
loaded ~/.bashrc


- 这种方式配置加载是不完全, 和正常登录环境不i一样, 因此一般不建议使用.



### /etc/profile

系统启动和终端启动时的系统环境初始化



### /etc/bashrc

函数和命令别名



## 字符串处理

变量默认值相关

不改变原变量值

${变量名-默认值} # 变量未定义, 使用默认值
${变量名:-默认值} # 变量为空时, 使用默认值. 为空包括未定义或空(只包含一个空格的不失为空)
${变量名:+默认值} # 变量不为空时, 使用默认值

改变原变量

${变量名=默认值} # 变量未定义, 使用默认值, 同时修改原变量
${变量名:=默认值} # 变量为空时, 使用默认值, 同时修改原变量

直接报错

${变量名:?提示文本} # 变量为空时提示报错




字符串操作

${#变量名} # 字符串长度

${变量名:pos:length} # 从位置 pos(下标从0开始)开始提取字串 length 个字符.

                      #     pos 可省略, 默认为0
                      #     length 可省略, 默认为到字符串结尾

${变量名#substring} # 前缀匹配, 删除匹配的字串(非贪婪模式)

                      #     substring 要删除的字串, 支持"通配符"

${变量名##substring} # 贪婪模式

${变量名%substring} # 后缀匹配, 删除匹配的字串(非贪婪模式)
${变量名%%substring} # 贪婪模式

${变量名/substring/replace} # 匹配所有, 并替换第一个匹配
${变量名//substring/replace} # 匹配所有, 并替换所有

${变量名/#substring/replace} # 前缀匹配, 并替换

${变量名/%substring/replace} # 后缀匹配, 并替换


> 上述的匹配是 "通配符" 匹配模式





更多可参考: https://linuxeye.com/390.html



## 数组

**定义数组**

数组名=( 元素1 元素2 元素3)

注意

元素之间用空格间隔开, 若元素本身含有空格, 则需要使用引号包含.
() 内的相邻位置不限制是否有空格



显示数组

打印第一个元素

echo $数组名

打印数组所有元素

echo ${数组名[@]}

显示数组元素个数

echo ${#数组名[@]}

显示数组第一个元素

echo $数组名

显示数组某个元素

echo ${数组名[n]} # 此处n表示元素下标, 从0开始




示例

cmdList=(
A1
A2
A3
)

for i in "${cmdList[@]}"; do

if $i; then
    echo "success"
else
    echo "error with code: $?"
fi

done






# 特殊符号

## 其他字符

- `#` 注释符

- `;` 命令分隔符

  - `;;` case 语句使用的分隔符

  > 在一行中连接多条命令, 每个命令在前一个命令执行完后执行. 
  >
  > 前一个任务的执行结果不会影响后续任务.

- `:` 空指令(什么都不做)

  可用于循环中作为一个占位符, `:` 永远返回真(即 0).

  > 因为在循环中都没有执行语句是会报错的.

- `,` 分隔目录

  > `cp 123{txt,log}` 执行效果 `cp 123.txt 123.log`

- `?` 条件测试

- `$` 取值符号

  > ```sh
  > echo $(命令)             # 取运行结果的值
  > 
  > echo ${变量名}            # 取变量的值
  > echo ${#变量名}        # 取变量长度
  > 
  > echo ${变量名[@]}        # 取数组的值
  > echo ${#变量名[@]}        # 取数组的长度
  > 
  > echo ${!变量名}        # 取变量名的值所对应的变量值. 即间接取值.
  >                       # x=y; y=z; echo ${!x}                 # 结果是 z
  > ```

- `|` 管道符

- `&` 后台运行

- shell 下专用

  - `.` 等价于 source 命令

  - `~` home 目录

  - `-` 上一次目录

    `cd -`

  - `*` 通配符(任意字符)

  - `?` 通配符(1个任意字符)

  - ` ` 空格



## 转义

- 普通字符转义赋予不同功能

  `\n`, `\t`, `\r` 单个字母的转义

- 特殊字符转义成普通字符用

  `\$`, `\"`, `\'` `\\` 单个非字母的转义(即不转义)





## 引用

- `"` 双引号: **不完全引用**

  不完全引用, 会解释双引号其中的变量.

a="$SHELL" # 值为 /bin/bash


- `'` 单引号: **完全引用**

完全引用(RAW), 不解释其中的变量.

a='$SHELL' # 值为 $SHELL


- `` ` 反引号: **执行命令**

等价 `$()`

a=whoami # 值为 当前用户名




## 括号

单独使用和非单独使用的意义通常是不一样的.



- `()`, [`(())`](#双圆括号 - let 命令的简化), `$()` 圆括号

# 单独使用, 会产生一个子shell
() # eg. (a=123) 执行完这个命令时, 由于是在子进程(子Shell)中执行, 因此不会影响当前shell环境. (👈 想一下管道)

# 数组初始化
变量名=(数组元素)

# 算数运算符
(( )) # 等价 let 命令的简写, eg. ((i++))

# 执行命令并将结果赋值给变量
变量名=$(命令) # 等价于 变量名=命令


- `[]`, `[[]]` 方括号(test 测试)

# 单独使用, 测试(test)
[ ] # 等价 test 命令, 使用 -gt, -lt 等. 方括号与内容需保持间隔. 可通过 $? 查看test结果

# 测试表达式, [ ] 的加强, 支持扩展语法: &&, ||, <, > 等
[[ ]] # 方括号与内容需保持间隔. 可通过 $? 查看test结果


- `<`, `>` 尖括号(重定向))

重定向符号

- `{}` 花括号 (范围, 枚举)

# 输出范围
echo {0..9} # 0 1 2 3 4 5 6 7 8 9

echo a{1,3,5} # a1 a3 a5

# 文件复制的快捷操作等
cp /etc/passwd{,.bak} # 实际执行的是 cp /etc/passwd /etc/passwd.bak

# 范围
for i in {1..9}; do echo $i; done;




## 运算符和逻辑符号

- `+`, `-`, `*`, `/`, `%` 算术运算符
- `>`, `<`, `=` 比较运算符
- `&&`, `||`, `!` 逻辑运算符





## 算术运算

### expr 运算

使用 `expr` 运算

expr <运算部分>

示例

expr 4 + 5
a=`expr 4 + 5`        # 将结果赋值给变量

注意

只支持整数, 不支持浮点数
数值和运算符之间"必须"有空格分隔





### let 命令

let 变量名=变量值

示例

let a=10+20            # a 的值是 30

注意

变量值可以是数学表达式, 包括: + - ++ -- += -= 等
变量值不支持浮点数.
变量值 `0` 开头为八进制.
变量值 `0x`开头为十六进制. 

> 实际很少用 `let`, 而是使用更简便的 双圆括号.
>
> 数值和运算符之间无所谓有没有空格



### 双圆括号 - let 命令的简化

[点击查看其他括号](#括号)

双圆括号是 let 命令的简化

语法

(())        # 赋值/运算
$(())        # 引用计算结果

示例

((a=10))
((a++))
((a--))
((a+=5))
((a=b=c=1))
echo $((10+20))            # 打印结果
b=$((1+2+3))            # 引用结果



# 测试与判断

## 退出与退出状态

程序

- `exit` 退出程序, 返回状态以 `exit` 的上一条命令执行结果为准
- `exit <返回值>` 退出程序, 返回状态以此处填写的 返回值(只能是数字)为准.





函数

- `return` 返回状态以 `return` 的上一条命令执行结果为准
- `return <返回值>` 退出程序, 返回状态以此处填写的 返回值(只能是数字)为准



一般约定, 返回值 `0` 表示正常, 其他都是不正常退出.



使用 `$?` 可以查看返回值, 用于确定 **当前Shell** 的上一个执行语句(进程, 脚本, 函数)是不是正常退出.





## 测试命令 test

`[ ]` 等价于 test 命令, 更推荐 `[ ]` 写法.



`[[ ]]` 是 `[ ]` 的扩展写法, 支持 `&&`、`||`、`<`、`>`

> 若要使用 `&&`、`||`、`<`、`>` 这些字符, 则**必须**用 `[[ ]]`

test 命令用于检测文件或比较值

  • 文件测试
  • 数值比较测试
  • 字符串测试

返回值说明

真(True) 返回 0
假(False) 返回 1

语法

test 表达式
test
[ 表达式 ]
[ ]

表达式

逻辑表达式
    表达式1 -a 表示2            # 逻辑与
    表达式1 -o 表达式2       # 逻辑或
    ! 表达式                 # 逻辑非
    
字符串表达式
    -z STR                # zero, 字符串长度为0
    -n STR                # non-zero, 字符串长度不为0
    STR1 = STR2            # 字符串相等测试(大小写敏感)
    STR1 != STR2        # 字符串不相等测试(大小写敏感)

数值表达式
    -eq        # =
    -ge        # >=
    -gt        # >
    -le        # <=
    -lt        # <
    -ne        # !=

文件
    不同文件比较
        FILE1 -ef FILE2        # 相同文件(同个设备, 相同inode)
        FILE1 -nt FILE2        # FILE1 修改时间比 FILE2 更新(也就是最近修改)
        FILE1 -ot FILE2        # 与 -nt 相反
    
    文件
        -e FILE                # 文件存在
        -s FILE                # 长度大于0的文件
    
    类型            
        -f FILE                # 普通文件(非下述几种类型)
        -b FILE                # block 块设备
        -c FILE                # char 字符设备
        -d FILE                # dir 目录
        -h FILE                # 软连接文件, 等同 -L
        -L FILE                # link 软连接文件, 等同 -h
        -p FILE                # 命名管道文件
        -S FILE                # socket 文件
    
    归属
        -O FILE                # 文件有有效的属主            
        -G FILE                # 文件有有效的属组
    
    权限
        -r FILE                # 已设置读权限
        -w FILE                # 已设置写权限
        -x FILE                # 已设置执行权限
        
        -u FILE                # 已设置 SUID(set-user-ID)
        -g FILE                # 已设置 SGID(set-group-ID)
        -k FILE                # 已设置 SBIT(sticky bit set) 
        
    其他            
        -t FD                  # 文件描述符在终端上打开



### `[[]]` 扩展用法

此处记录的是与 `[]` 不同的

表达式

==        # 通配符匹配, 支持: * ? 
=~        # 正则匹配    
&&
||
<
>

> 不支持 `-a` , `-o`





## 条件 if

完整语法

if [ 测试条件 ]
then 执行相应命令 # 如果条件成立 或 返回值为0则进入 then, then 后面无需跟分号
elif [ 测试条件 ] # 即 else if
else 执行相应命令
fi # 结束

可以写成1行

if [ 测试条件 ]; then 执行相应命令; else 执行相应命令; fi

调试时 if true

if :; then ... fi

调试时 if false

if [ ! : ]; then ... fi

判断执行某个命令后的结果

if 命令; then

echo "success"

else

echo "fail"

fi


> `<测试条件>` 如果是个命令或执行的脚本或函数, 那么此时是根据其返回的结果值来判断是否为真.
>
> if 语句支持嵌套使用.





## 分支 case

完整语法

case "$变量" in

情况1)
    命令
;;
情况2)
    命令
;;
*)                        # 此处用了通配符, 匹配其余情况
    都不匹配时执行的命令
;;

esac


匹配条件支持如下通配符

- `*`
- `?`
- `|`
- `[ ]` 表示范围内的任意一个字符, 范围内可以使用 `-`
- `[^]` 逆向选择 ?
- `[!]` 逆向选择 ?



> 上述的 情况 支持通配符, 比如 `*`, `|`, `?`
>
>   
>
> 注意以下两种是不同的表示
>
> - `"情况)"` 此处是将 `情况` 视为一个字符串, 其中的通配符之类的都不会生效
> - `情况)` 此处的 `情况` 中的通配符之类的生效
>
> 因此以下几种匹配是不一样的
>
> ```sh
> start|stop)            # 匹配 start 或 stop
> 
> "start"|"stop")        # 同上, 匹配 start 或 stop
> 
> "start|stop")        # 完整匹配 "start|stop" 整个字符串
> 
> "cmd*")                # 完整匹配 "cmd*" 整个字符串
> 
> "cmd?")                # 完整匹配 "cmd?" 整个字符串
> 
> cmd*)                # 通配符匹配 cmd*
> 
> cmd?)                # 通配符匹配 cmd?
> ```



## 循环

### for 遍历

for 参数名 in 列表
do

:

done


​    

**列表来源**

1. 列表中包含多个变量(空格分隔)

for i in 1 2 3

for i in {1..3} # 使用花括号产生列表


2. 使用  ` `` ` 或 `$()` 方式执行命令, 默认逐行处理(若出现空格会被视为多行)
 for file in `ls`

3. 枚举路径(可使用通配符)

# 枚举出的是完整路径, 可配合使用 basename 命令获取简短的文件名
for file in /etc/profile*
do

   echo $file

done

# 输出如下
/etc/profile
/etc/profile.d





### for 循环

C语言风格的for命令

语法

for ((变量初始化;循环判断条件;变量变化))  
do
    命令
done

示例

for ((i=0;i<=10;i++)); do echo $i; done

> 不太常用

 

### while 循环

语法

# 满足条件就执行
while test测试条件为真
do
    命令
done

示例

i=0
while [[ i<=10 ]]
do
    ((i+=3))
done

> 常用于构建交互式菜单

 

可配合 `shift` 命令偏移参数处理

位置参数位移, 如果偏移数为n, 则新的 $1 值为原来的 ${n+1}, 以此类推

shift <偏移数=1>

示例

while [ $# -gt 0 ] do
    shift 1
done







### until 循环

语法

# 不满足条件就执行
until test测试条件为假
do
    命令
done



### break 和 continue

while :
do

break;
continue;

done




 

# 函数

## 自定义函数

定义

函数名称 () {
    local 函数局部变量名;        # 变量作用域仅在函数内部
    echo $1;                   # 
}

注意

语法要求: 第一个花括号后面需要有空(空格, 换行等)

使用

函数名

可将自定义的函数统一放在一个脚本文件中, 赋予执行权限后通过 `source` 或 `.` 执行以在当前Shell环境调用.

 

示例

hello() { echo hello $USER; }

hello # 输出 hello root




顺序执行多个函数的示例

A1 () { : }
A2 () { : }
A3 () { : }

cmdList=(
A1
A2
A3
)

for i in "${cmdList[@]}"; do

if $i; then
    echo "success"
else
    echo "error with code: $?"
fi

done




## 获取函数名

在函数内部有一些预定义变量

$FUNCNAME 数组, 包含调用栈名




示例

!/bin/bash

abc() {

echo "in abc: ${FUNCNAME[@]}"
ef

}

ef() {

echo "in ef: ${FUNCNAME[@]}"

}

abc

输出内容

in abc: abc main
in ef: ef abc main






## 删除函数

unset -f 函数名








## 系统脚本

系统自建的函数库: `/etc/init.d/functions`





# 脚本控制

## 脚本优先级控制

可以使用 nice 和 renice 调整脚本优先级



CPU 的计算和创建子进程都会造成系统 开销.



创建死循环大量消耗cpu导致死机的情况

1. while 和 for 创建死循环会导致cpu占用过高,   

2. fork 炸弹: 程序大量创建子进程,  CPU不响应任何信息.

# 示例 - 定义一个 func 的函数
func () { func | func& }

# 调用之后系统会疯狂创建子进程, 此时 ctrl+c 已经不生效了
func

# 另一个常见的fork炸弹
.(){ .|.&}; .




 

 

`ulimit -a` 的输出

core file size (blocks, -c) 0
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
pending signals (-i) 5642
max locked memory (kbytes, -l) 64
max memory size (kbytes, -m) unlimited
open files (-n) 1024
pipe size (512 bytes, -p) 8
POSIX message queues (bytes, -q) 819200
real-time priority (-r) 0
stack size (kbytes, -s) 8192
cpu time (seconds, -t) unlimited
max user processes (-u) 5642 # 同时可创建的子进程数量, root无效
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited


> 大部分限制对 root 是不生效的.



## 捕获信号

`kill` 默认发送 15号(SIGTERM)信号给应用程序

`ctrl+c` 发送2号(SIGINT)信号给应用程序

9号(SIGKILL)信号不可捕获, 不可阻塞, 强行杀进程.



设置捕获信号

trap [参数] 信号

<参数> 表示捕获到信号后的处理, 分为以下几种情况

未设置 <参数>      按默认处理情况处理
值为 -            按默认处理情况处理, 同上
值为 空           捕获信号, 直接忽略
值为其他          捕获信号, 并执行指定命令

示例

# 捕获 SIGINT(2) 信号, 并打印 "got signal 2"
trap "echo got signal 2" 2

> 可在重要不可中断的脚本中设置捕获信号以免被无意中结束掉, 比如备份脚本.







control group? 控制内存?



# 计划任务

##  at 命令


设置一次性计划任务

输入要执行的任务后需要追加 ctrl+d 以表示输入完毕.

at [选项] 时间

时间格式

HH:MM            # 时:分            指定下一个该时间点执行(今天或隔天)
HH:MM today        # 今天指定时间运行
HH:MM tomorrow    # 明天指定时间运行
HH:MM MMDDYY    # 时:分 月日天, 年可以是2位的缩写, 或4位的全写
HH:MM MM/DD/YY
HH:MM MM.DD.YY
HH:MM YYYY-MM-DD

now + 计数      # 当前时间点的偏移, 计数单位: minutes, hours, days, weeks. Eg. 4pm + 3 days

选项

-c <序号>        # 查看具体的任务执行内容
-f <file>     # 从指定文件读取命令, 而不是从标准输入

注意

1. 非内部命令应使用完整路径, 如果是shell脚本应使用 source 来引入环境变量
2. 计划任务的执行是没有终端的, 因此是没有标准输出的, 需自行重定向输出

Tip
鉴于 at 在设置命令时的不方便, 可以考虑配合 cat 一同设置, eg.

cat <<'EOF' | at 时间
具体要执行的命令
EOF

   

注意

- `at` 一次性任务是依赖  atd 服务来执行的
- `at` 目录会以设置任务时所在的目录作为工作目录, 因此若该目录无效(被删除, 权限限制) 则会导致任务执行失败.
- root用户可以在任何情况下使用at命令,而其他用户使用at命令的权限定义在/etc/at.allow(被允许使用计划任务的用户)和/etc/at.deny(被拒绝使用计划任务的用户)文件中,默认没有文件需要自己创建允许用户和拒绝用户文件;
  - 如果/etc/at.allow文件存在,只有在该文件中的用户名对应的用户才能使用at;
  - 如果/etc/at.allow文件不存在,/etc/at.deny存在,所有不在/etc/at.deny文件中的用户可以使用at;
  - at.allow比at.deny优先级高,执行用户是否可以执行at命令,先看at.allow文件中有没有才看at.deny文件;
  - 如果/etc/at.allow和/etc/at.deny文件都不存在,则只有root用户能使用at;



**atq 命令**

查询等待执行的一次性计划任务队列

最左边的数字即计划任务的任务id

atq




**atrm 命令**

移除未执行的计划任务

atrm 任务序号






## 周期性计划任务 cron

crontab [选项]

选项

-e        # 编辑, 进入vim编辑器 
-l        # 查看已配置项

配置格式

分 时 日 月 周 命令
*            # 任意
1,2,3        # 逗号分隔
1-3            # 等价表示 1,2,3    

注意

命令应使用完整路径 



配置文件(每个用户有各自的一份配置)

`/var/spool/cron/用户名`



crond 相关日志(不包括任务的输出)

`/var/log/cron`



## 计划任务和锁

### anacron 周期命令调度程序

anacron 是一个用于周期性执行命令的工具(适用于非24小时开机, 同时需要确保 每日/每周/每月 运行指定任务).

它的时间粒度是"天", 比如配置了 logrotate 每天运行一次, 那么它默认会在 3~22 点之间每小时尝试运行该任务(因此主机不一定需要保持24小时开机)



**适合**: 需要定期执行至少1次的脚本(对具体执行时机没有严格要求的)可以使用 anacron 来配置定时任务.

> 比如要求每3天至少执行1次, 但对于在哪一天的哪一个小时执行没有严格要求.



**Crond 服务调用 Anacron 的过程**

1. crond 服务每分钟会执行 `/etc/cron.d/`  目录下配置的定时任务(crontab 格式):

   - `/etc/cron.d/0hourly` 配置每小时的第1分钟执行 `/etc/cron.hourly/` 目录下的所有**<u>脚本</u>**
   - `/etc/cron.d/sysstat` 配置每10分钟系统性能统计收集, 每天接近凌晨时生成一份每日报告

2. `/etc/cron.hourly/0anacron` 脚本

   > 脚本的主要逻辑: 若今日未执行过 anacrontab, 则会执行 `/usr/sbin/anacron -s` 命令
   >
   > 意思是顺序(非并行)执行延时计划任务.





**Anacron 的执行过程**

1. 读取配置文件 `/etc/anacrontab` 

# the maximal random delay added to the base delay of the jobs
RANDOM_DELAY=45
# the jobs will be started during the following hours only
START_HOURS_RANGE=3-22

#period in days delay in minutes job-identifier command
1 5 cron.daily nice run-parts /etc/cron.daily
7 25 cron.weekly nice run-parts /etc/cron.weekly
@monthly 45 cron.monthly nice run-parts /etc/cron.monthly


2. 依次执行上面配置的任务

Anacron 会在每天的 3点~22点时间范围内, 每次随机延迟 0~45 分钟开始执行上面配置的 每日/每周/每月 任务.

- `/etc/cron.daily/logrotate` 日志轮转工具调用
- ...





### flock 锁文件

flock [选项]

示例

flocak -xn "/tmp/f.lock"  -c "需执行的命令或脚本"

选项

锁类型
-x         # 排他锁


-n, --nb, --nonblock        # 加锁失败时直接退出 (默认是等待)

-c, --command <command>        # 需执行的命令

注意

锁文件被删除后会失效.







# 其他待整理

##

# run-parts 命令

运行指定目录下的所有可执行文件(具有执行权限)

- 可通过在目录下配置 jobs.deny 和 jobs.allow 来配置黑/白名单
- 常用于crond定时任务执行某个目录下的所有脚本

run-parts <目录>






## mktemp 命令

创建一个临时文件或目录

mktemp [选项] [模板]

模板

需包含至少连续3个 "X", 若未指定, 则会在 /tmp 目录下创建 tmp.XXXXXXXXXX

选项

-d, --directory        # 创建一个目录(而不是默认的文件)
-t                    # 在 /tmp 目录下创建临时文件

示例

mktemp                    # /tmp/tmp.CaE8KvS8HI
mktemp log.XXXXXXX        # log.KyvESFk            在当前目录下
mktemp -t log.XXXX        # /tmp/log.RNwC



## yes 命令

在命令行中输出指定的字符串,直到yes进程被杀死

yes [选项] <string=y>

示例

常用语需要简单交互: 输入 y/yes 以继续操作的情况
yes|yum install ...        # 这里只是示例, 实际 yum 一般是配合 -y

嘉兴ing
284 声望24 粉丝

PHPer@厦门