> For the complete documentation index, see [llms.txt](https://yangsx95.gitbook.io/notes/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://yangsx95.gitbook.io/notes/programming-language/script-language/shell.md).

# Shell

Shell：壳

Shell是一个命令行解释器，它为用户提供了一个向Linux内核发送请求以便运行程序界面的系统级程序，用户可以用Shell来启动、挂起、停止甚至是编写程序。

计算机不认识ASCII字符，需要Shell接收ASCII并翻译给计算机。

![1540905917702](/files/zBnDHwCZ5iesm3mYJn4D)

目前，Shell共有两种类型：

* **Bourne Shell**： 1979年Unix就开始使用Bourne Shell，主文件名为sh（功能比较简单，不常用）
* **C Shell**：C Shell 主要在BSD版本的Unix系统中使用，其语法与C类似而得名（BSD：Unix版本）

Shell的两种主要语法类型就是 Bourne Shell 和 C Shell，这两种语法不兼容：

* Bourne Shell 家族：
  * sh
  * ksh
  * bash（linux标准）
  * psh
  * zsh
* C Shell 家族：
  * csh
  * tcsh （unix）

## 配置Shell

我们可以通过如下命令查看当前操作系统使用的shell类型：

```sh
echo $SHELL
```

也可以查看当前操作系统支持的shell版本：

```sh
vi /etc/shells
```

切换当前的shell：

```sh
sh #切换到sh，这是子shell，退出就是父shell
bash #切换到bash，这是子shell，退出就是父shell
```

## 编写并执行shell脚本

一个shell脚本需要在最前面指定脚本解释器：

```shell
#!/bin/bash
```

创建一个hello world脚本：

```shell
echo '#!/bin/bash
echo Hello World!
' > hello.sh

# 给脚本赋予执行权限
chmod ug+x hello.sh

# 直接运行脚本
./hello.sh

# 使用sh命令运行，等同于 bash ./hello.sh
sh ./hello.sh

# 使用source命令运行
# 该种方式不同于上面两种，不会打开新的进程去运行脚本文件，他的父进程直接为bash进程
# 而上面两种方式会打开一个 sh ./hello.sh 的子进程运行脚本文件
# 即，使用source执行脚本时，仍然处在bash这个进程中，而不是新进程，故可以使用bash中的任何变量
source ./hello.sh
```

> 子进程默认无法访问父进程中的变量，这是因为父进程中的变量默认对子进程不可见。 如果想要子进程可以访问父进程中变量，可以借助export命令将变量导出，被导出的变量对于进程的子进程是可见的这样就解决了无法访问父进程变量的问题。

## 变量

变量的分类：

1. 用户自定义变量：脚本中的变量
2. 环境变量：用于保存与操作系统操作环境相关的数据。变量可以自定义，但是对系统生效的环境变量名和变量作用是固定的。
3. 位置参数变量：主要用于向脚本传递参数或者数据，变量名称不能自定义，变量作用是固定的
4. 预定义变量：是Bash中已经定义好的变量，变量名不能自定义，变量作用也是固定的

变量命名规则：

1. 变量名只能由字母数字和下划线组成，并且开头不可以是数字
2. 变量名不得超过255个字符
3. 变量名在有效范围内是唯一的

变量数据类型：

1. 在Bash中，变量的默认类型是字符串类型
2. Linux中，准确的说，**只有string类型**，所以不可以对数字进行直接运算

### 环境变量

**用户自定义的环境变量**：

```bash
export 变量名=变量值
或者
变量名=变量值
export 变量名
```

查看、修改、删除与普通变量一致。

> 对系统生效的环境变量喝变量作用是固定的。

**查看系统中的所有环境变量**：

```bash
# set 命令也会输出环境变量，但是也包含普通变量，比较乱
# env会输出当前系统中定义的所有环境变量
env
```

**常用环境变量：**

```bash
HOSTNAME:主机名
SHELL:当前SHELL
TERM:终端环境
HISTSIZE:历史命令最大条数（定义在/etc/profile文件中）
SSH_CLIENT:当前操作环境是用ssh连接的，这里记录客户端ip
SSH_TRY:ssh连接的终端时pts/1
USER:当前登陆用户
```

**环境变量为什么大写：**

因为系统命令全都是小写，这样可以将环境变量与系统命令区分。

### 自定义变量

定义：

```bash
变量名=变量值 #等号两侧不可以由空格
变量名="a b" #变量值之间有空格，需要双引号或者单引号包裹
```

调用：

```bash
echo $var_name
```

变量叠加：

```bash
x=1
y=${x}456    # 方式1
z="$x"456    # 方式2 
```

只读变量（注意不可unset）：

```bash
url = 'http://baidu.com'
readonly url
```

查看所有变量：

```bash
set
set -u #默认输出一个不存在的变量，将会输出空字符串，执行此命令，将会提示变量不存在
```

![1541149257950](/files/F0v1HYwHD5iB3fiEUa6l)

删除变量：

```bash
unset 变量名
```

#### PATH变量

```bash
echo $PATH

/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin:/root/node-v10/bin:/root/bin:/usr/local/git/bin:/root/node-v8/bin:/root/bin
# 多个路径之间使用冒号:间隔
```

当终端输入一个执行文件的非绝对路径时，linux默认不支持直接执行这个命令，他将会先从命令别名中寻找命令，再从`$PATH`中定义的路径中依次寻找，找不到就抛出 `command not found`

**往$PATH中添加路径：**

```bash
# 使用变量叠加
PATH="$PATH:/root/sh"
```

#### 什么是PS1和PS2

linux下的bash shell有两级用户提示符：

1. 第一级是bash在等待命令输入时的提示符，可以通过在用户主目录下`.bash_profile`文件里设置`PS1`变量来实现。
2. 当bash期待输入更多的信息以完成命令时将显示第二级提示符。

比如：你输入`cp filename1 \`，回车，此时就出现第二级提示符。`\`是续行的意思，默认的第二级提示符是`>`。 如果要改变第二级提示符，可以通过在`.bash_profile`文件里设置`PS2`变量来实现。

#### PS1变量

```shell
[root@Feathers ~]# echo $PS1
[\u@\h \W]\$

# $PS1 定义了当前系统的操作符
[ 对应[
\u 当前登陆用户
@  对应@
\h 当前主机名
\W 当前所在目录最后一个目录
]  对应]
\$ 当前用户提示符：超级用户#， 普通用户是$
```

**命令提示符设置：**

* `\d`: 显示日期， 格式`星期 月 日`
* `\t`: 显示24小时制时间，格式`HH:MM:SS`
* `\A`: 显示24小时制时间，格式`HH:MM`
* `\H`: 显示完整主机名
* `\$`: 用户提示符。#root，$普通
* `\u`: 用户名
* `\w`: 显示所在目录完整名称
* `\W`: 显示当前目录最后一个目录

#### PS2变量

定义换行提示符：

```shell
[root@Feathers ~]# echo $PS2
>

# 命令换行后
[root@Feathers ~]# ls \
>
# 其中 > 就是换行提示符
```

#### 语系变量查询

**locale**：查询当前系统语系变量

```bash
[root@Feathers ~]# locale
LANG=en_US.UTF-8    #定义系统主语系变量，英文语系+UTF8编码，绝对了操作系统语言
LC_CTYPE="en_US.UTF-8"
LC_NUMERIC="en_US.UTF-8"
LC_TIME="en_US.UTF-8"
LC_COLLATE="en_US.UTF-8"
LC_MONETARY="en_US.UTF-8"
LC_MESSAGES="en_US.UTF-8"
LC_PAPER="en_US.UTF-8"
LC_NAME="en_US.UTF-8"
LC_ADDRESS="en_US.UTF-8"
LC_TELEPHONE="en_US.UTF-8"
LC_MEASUREMENT="en_US.UTF-8"
LC_IDENTIFICATION="en_US.UTF-8"
LC_ALL=     #定义整体语系变量
```

查看linux支持的所有语系：

```bash
locale -a | more 
```

查看系统默认语系（下次开机以后的系统生效的语言环境）：

```bash
cat /etc/sysconfig/i18n
```

**Linux中文支持**：

1. 前提：正确安装中文字体和中文语系
2. 纯字符界面不支持中文显示，可以使用如zhcon插件来完成此功能（不推荐）
3. 如果使用第三方工具连接，只需要保证第三方工具的语系设置正确即可
4. 图形界面会自动支持

#### 位置参数变量

位置参数：

```bash
命令 位置参数1 位置参数2 ...
```

| 位置参数变量 | 作用                                                |
| ------ | ------------------------------------------------- |
| $n     | n代表数字，$0代表命令本身，$1-$9代表第一到第九个参数，10以上的参数需要使用${10}包含 |
| $\*    | 代表命令行中所有的参数，$\*把所有的参数看成一个整体                       |
| $@     | 同样代表命令行中的所有参数，但是$@将每个参数区分对待                       |
| $#     | 代表命令行中所有参数的个数                                     |

具体示例：

```bash
#!/bin/bash
echo 共有参数"$#"
echo 参数是："我是一个字符串，参数为，$*"
echo 参数是："我是一个可循环的参数列表，$@"



num1=$1
num2=$2
sum=$(( $num1 + $num2))

echo 计算结果为：$sum
```

#### 预定义变量

| 预定义变量 | 作用                        |
| ----- | ------------------------- |
| $?    | 最后一条执行命令的返回状态，如果非0，代表正确执行 |
| $$    | 当前进程的进程号 PID              |
| $!    | **后台**运行的最后一个进程的进程号       |

## 注释

```shell
# 以#作为单行注释
# 多行以#开头为多行注释

:<<EOF
我是注释内容
EOF

:<<ABC
我是注释内容，这是可以自定义结束符的注释
ABC
```

## 数据类型

准确地说，shell的数据类型只有string，没有数字等其他类型。

### string 字符串

shell的字符串有两种表达方式：

1. 使用单引号`''`表示的多行字符串，被包裹的文本不会进行变量转义

   ```shell
   str1='apple'
   ```
2. 使用双引号`""`表示的多行字符串，被包裹的文本会做变量转义(有必要需要加入花括号区分界限)

   ```shell
   str2="${str1}很好吃"
   ```
3. 如果字符串没有空格，可以去除单双引号表示

   ```shell
   str3=orange
   ```

字符串拼接：

```shell
str1="hello1"
str2="hello2"

str3=$str1$str2
echo $str3

str4='你好'$str1
echo $str4

str5="你好$str2"
echo $str5
```

获取字符串长度：

```shell
str1=myqq.com
echo ${#str1}
```

字符串截取：

```shell
str1='abcdefg'

# 从0截取，截取4位字符
# abc
echo ${str1:0:4}

# 从1截取，共截取3位字符
# bcd
echo ${str1:1:3}
```

### 数组类型

bash支持一维数组，并且不限定数组大小。数组的下标从0开始编号，且元素使用小括号定义。

定义并访问数组：

```shell
# 定义数组 idea -> array create
favs=("足球" "篮球" "乒乓球" "保龄球")

# 访问指定的下标 idea -> array at index
fav=${favs[1]}

# 设置指定下标的值 idea -> array set element
fav[1]="哈哈"

# 添加元素 idea -> array add 
favs+=("羽毛球")

# 获取数组的所有元素 idea  -> array all
echo ${favs[@]}

# 获取数组的长度 idea  -> array length
length=${#favs[@]}
# 或者
length=${#favs[*]}

# 循环数组 idea -> array iteration
for item in ${favs[@]}; do
    echo "$item"
done

# 删除数组的某个元素 idea -> array delete at
unset favs[0]

# 删除真个数组 -> idea -> array delete
unse favs
```

### 可执行的字符串

```shell
echo `echo hello`
```

## 运算符

### 算数运算符

使用算数运算符共有以下几种方式：

* `$((运算符))`或`$[运算符]` (推荐)
* `expr`
* `let`
* `declare` (不推荐)

#### `$((运算符))`或`$[运算符]`

```bash
aa=11
bb=22
cc=$(($aa+$bb))
dd=$[$aa+$bb]
echo $cc
# 推荐$((运算符))
```

**shell支持的数值运算**：

![1542418674368](/files/lBtnQ14BuIeT5zHyaQgN)

> 与或运算使用0和1表示

#### expr

```shell
a=10
b=20

echo `expr $a + $b`
echo `expr $a - $b`
echo `expr $a \* $b`
echo `expr $b / $a`
echo `expr $b % $a`
```

> 反引号里面的内容将会被作为命令、脚本直接运行得到结果

#### let

let命令是Bash中用于计算的工具，提供常用运算符还提供了方幂\*\*运算符。在变量的房屋计算中不需要加上$来表示变量，如果表达式的值是非0，那么返回的状态值是0；否则，返回的状态值是1。

```shell
let a=5+4 b=9-3
echo $a $b

let "t1 = ((a = 5 + 3, b = 7 - 1, c = 15 - 4))"
echo "t1 = $t1, a = $a, b = $b"
```

#### `declare -i`

使用`declare -i`，不推荐：

```bash
aa=111
bb=222
declare -i cc=${a}+${b}
echo ${cc}
333
```

### 关系运算符

关系运算符用于判断两个数字字符串之间的关系，如果字符串不是数字字符串，将无法处理。

```shell
a=10
b=20

if [$a -eq $b]
then
   echo "$a -eq $b : a 等于 b"
else
   echo "$a -eq $b : a 不等于b"
fi

# -eq 等于         idea-> number equal
# -ne 不等于       idea-> number not equal
# -gt 大于         idea-> number greater
# -lt 小于         idea-> number less
# -ge 大于等于      idea-> number greater or equal
# -le 小于等于      idea-> number less or equal
```

### 布尔运算符

布尔运算符共有三种与或非：

```shell
a=10
b=20

# 非
if [$a != $b]
then
   echo "$a != $b: a不等于b"
else 
   echo "$a == $b: a等于b"
fi

# 与
if [$a -lt 100 -a $b -gt 15 ]
then
   echo "$a 小于 100 且 $b 大于 15：返回true"
else
   echo "$a 小于 100 且 $b 大于 15：返回false"
fi

# 或
if [$a -lt 100 -o $b -gt 15 ]
then
   echo "$a 小于 100 或 $b 大于 15：返回true"
else
   echo "$a 小于 100 或 $b 大于 15：返回false"
fi
```

### 逻辑运算符（短路运算符）

逻辑运算符需要用双方括号包裹

```shell
a=10
b=20

if [[ $a -lt 100 && $b -gt 100]]
then
   echo "返回true"
else 
   echo "返回false"
fi

if [[ $a -lt 100 || $b -gt 100]]
then
   echo "返回true"
else
   echo "返回false"
fi
```

### 字符串运算符

```shell
a=zhangsan
b=lisi

# 相等判断(在idea中，快捷输入为 string equal)
if [$a == $b]
then
   echo 'a 和 b是两个完全相同的字符串'
else
   echo 'a 和 b是两个不同的字符串'
fi

# 不等判断(在idea中，快捷输入为 string not equal)
if [$a != $b]
then
   echo 'a 和 b是两个不同的字符串'
else
   echo 'a 和 b是两个完全相同的字符串'
fi

# 空判断（在idea中，快捷输入为 string is empty）
if [ -z $a ]
then 
   echo "a 变量是一个空字符串"
else
   echo "a 变量不是一个空字符串"
fi

# 非空判断（在idea中，快捷输入为 string not empty）
if [-n $a]
then 
   echo 'a 不是一个空字符串'
else 
   echo 'a 是一个空字符串'
fi 

# 或者
if [$a]
then
   echo 'a 不是一个空字符串'
else
   echo 'a 是一个空字符串'
fi
```

### 文件测试运算符

```shell
file="/var/node/test.sh"

# 文件是否可读（在idea中，快捷输入为 file readable）
if [ -r $file ]
then
   echo "文件可读"
else 
   echo "文件不可读"
fi

# 文件是否可写（在idea中，快捷输入为 file writable）
if [ -w $file ]
then
   echo "文件可写"
else
   echo "文件不可写"
fi

# 文件是否可执行（在idea中，快捷输入为 file executable）
if [ -x $file ]
then
   echo "文件可以执行"
else
   echo "文件不可执行"
fi

# 判断文件是否存在（在idea中，快捷输入为 file exists）
if [ -f $file ]
then
   echo "目标是一个文件，且存在"
else
   echo "文件不存在"
fi   

# 判断目录是否存在 (在idea中，快捷输入为 directory exists )
if [ -d $file]
then
   echo "目标是一个目录，且存在"
else
   echo "目录不存在"
fi

# 判断路径是否存在，既可以判断文件，又可以判断目录（在idea中，快捷输入为 path exists）
if [-e $file]
then
   echo "目标路径存在"
else
   echo "目标路径不存在"
fi
```

## echo

读作 ai kou

```bash
echo "Hello World"
# 显示转义字符
echo "\"Hello World\""
# 显示变量
name=zhangsan
echo "$name HelloWorld"
# echo默认会显示换行
echo "你好"
echo "世界"
# 除了\"，echo的双引号字符不支持任何转义字符，如果需要支持转义字符，需要增加-e选项
echo -e "Hello \n World"
# 使用 \c 去除行为换行
echo -e "Hello\c"
echo "World"
# 使用单引号进行原样输出
echo '"hello"World $name' #"hello"World $name
# 重定向到文件（覆盖）
echo "hello" > a.txt
# 重定向到文件（追加）
echo "hello" >> a.txt
### 颜色
# 30m 黑色，31m 红色，32m 绿色，33m 黄色
# 34m 蓝色，35m 洋红，36m 青色，37m 白色
# \e[1;31m 开启颜色，颜色为红色
# \e[0m 关闭颜色
echo -e "\e[1;31m 我长得很帅 \e[0m" # 注意空格
```

## test

test命令可以用于替换if语句中的中括号：

```shell
num1=100
num2=200
if test $[num1] -eq $[num2]
then
   echo "两数相等"
else
   echo "两数不等"
fi
```

## read

```shell
read [选项] [变量名]
-p "提示信息："在等待read输入时，输出提示信息
-t 秒数：read命令会一直等待用户输入，使用此选项可以指定等待时间
-n 字符数：read命令会只接受指定的字符数
-s 隐藏输入数据，适用于机密信息的输入
```

示例：

```bash
#!/bin/bash
read -p "请输入你的姓名：   " -t 30 name
read -p "请输入密码：      " -s -n 6 password
echo -e "\n$name : $password"
```

## declare

再Linux中变量全是字符串类型，所以变量无法直接进行运算，所以我们需要借助declare命令声明变量类型：

```bash
declare [+/-][选项] 变量名
- ： 给变量设定类型属性
+ ： 取消变量类型属性

-a: 声明为数组类型
-i: 声明为整型  integer
-x: 声明为环境变量  
-r: 声明为只读变量  readonly
-p: 显示指定变量的被声明的类型
```

**整型：**

```bash
#声明一个整型
declare -i test=123
#声明的整型可以进行数值运算
declare a=100
declare b=200
declare -i c=${a}+${b}

$ echo ${c}
300
```

**数组**：

```bash
声明变量：
declare -a onepiece[0]=LuFei
onepiece[1]=NaMei
onepiece[2]=SuoLong

查看数组中的所有元素：
echo ${onepiece[*]}
查看数组中的第一个元素：
echo ${onepiece}
echo ${onepiece[0]}
查看数组中的第n个元素：
echo ${onepiece[n]}
```

声明环境变量：

```bash
declare -x test=123
#其实export只是对declare -x命令的包装，最终调用的都是declare -x命令
```

列出系统中所有变量的类型：

```bash
declare -p
#其中 -- 代表没有声明变量类型，或者变量类型被+取消了
```

只读变量：

```bash
#不要使用，临时变量，但是无法删除，无法修改值，无法修改类型
declare -r test=111
```

## 流程控制

### if

```shell
a=10
b=20
if [ $a == $b ]
then
   echo "a 等于 b"
elif [ $a -gt $b ]
then
   echo "a 大于 b"
elif [ $a -lt $b ]
then
   echo "a 小于 b"
else
   echo "没有符合的条件"
fi
```

> 条件可以为 `[[]]`代表可以使用短路逻辑运算符，`(())`代表可以使用`>`、`<`直接判断数字，比如`if (( $a > $b ))`

### case

通常用来处理枚举：

```shell
echo '输入 1 到 4 之间的数字:'
echo '你输入的数字为:'
read aNum
case $aNum in
    1)  echo '你选择了 1'
    ;;
    2)  echo '你选择了 2'
    ;;
    3)  echo '你选择了 3'
    ;;
    4)  echo '你选择了 4'
    ;;
    *)  echo '你没有输入 1 到 4 之间的数字'
    ;;
esac
```

> 不仅是数字，文本也可以：`"hobby") echo "你选择了 'hobby'"`

### for

```shell
# 循环有限的数字
for loop in 1 2 3 4 5
do
    echo "The value is: $loop"
done

# 循环有限的文本
for str in This is a string
do
    echo $str
done
# This
# is
# a
# string

# 循环数字范围
for i in {1..1000} ; do
    echo "$i"
done

# 循环数组
arr=(1 2 3 4)
for i in ${arr[@]}; do
    echo "$i"
done
```

### while

```shell
int=1
while(( $int<=5 ))
do
    echo $int
    let "int++"
done

echo '按下 <CTRL-D> 退出'
echo -n '输入你最喜欢的网站名: '
while read FILM
do
    echo "是的！$FILM 是一个好网站"
done
```

无限循环：

```shell
while :
do
    command
done

while true
do
    command
done

for (( ; ; ))
```

### break 和 continue

```shell
while :
do
    echo -n "输入 1 到 5 之间的数字:"
    read aNum
    case $aNum in
        1|2|3|4|5) echo "你输入的数字为 $aNum!"
        ;;
        *) echo "你输入的数字不是 1 到 5 之间的! 游戏结束"
            break
        ;;
    esac
done

while :
do
    echo -n "输入 1 到 5 之间的数字: "
    read aNum
    case $aNum in
        1|2|3|4|5) echo "你输入的数字为 $aNum!"
        ;;
        *) echo "你输入的数字不是 1 到 5 之间的!"
            continue
            echo "游戏结束"
        ;;
    esac
done
```

## 函数

函数的调用和参数：

```shell
funWithParam(){
    echo "第一个参数为 $1 !"
    echo "第二个参数为 $2 !"
    echo "第十个参数为 $10 !"
    echo "第十个参数为 ${10} !"
    echo "第十一个参数为 ${11} !"
    echo "参数总数有 $# 个!"
    echo "作为一个字符串输出所有参数 $* !"
}
funWithParam 1 2 3 4 5 6 7 8 9 34 73
```

通过`$?`获取函数返回值：

```shell
funWithReturn(){
    echo "这个函数会对输入的两个数字进行相加运算..."
    echo "输入第一个数字: "
    read aNum
    echo "输入第二个数字: "
    read anotherNum
    echo "两个数字分别为 $aNum 和 $anotherNum !"
    return $(($aNum+$anotherNum))
}
funWithReturn
echo "输入的两个数字之和为 $? !"
```

## Shell的参数传递

执行Shell脚本时，可以向脚本传递参数，脚本内可以通过`$n`，n代表一个数字，来获取传入的参数信息。相关的参数处息如下：

| 参数处理 | 说明                           |
| ---- | ---------------------------- |
| `$#` | 参数个数                         |
| `$*` | 所有的参数，以一个字符串显示               |
| `$$` | 脚本运行的当前的id号                  |
| `$!` | 后台运行的最后一个进程的ID号              |
| `$?` | 显示上一个命令的运行状态，如果为0代表成功，其他表示失败 |
| `$0` | 当前执行的脚本的文件名称                 |

```shell
echo "执行的文件名为 $0"
echo "第一个参数为 $1"
echo "第二个参数为 $2"
```
