一、Shell基础概念
概念
终端:获取用户输入,展示运算结果的硬件设备。 台式机是由主机(机箱)和很多外围设备组成的。主机里边包括主板、CPU、内存、硬盘、和其他芯片等等。外围设备包括显示器、鼠标、键盘、耳机、麦克风、和摄像头等等。这些外围设备就被称为终端,负责向主机输入数据的就叫输入终端,比如鼠标、键盘、麦克风、摄像头,负责接收主机输出数据的设备就被称作输出终端,比如显示器、耳机。
终端 = 输入终端 + 输出终端
tty:teletypeWriter 的简称,和终端等价,早期指电报打印机,在 linux 中是输入输出环境。 tty或者说终端最开始指的是获取用户输入并输出的物理设备,比如电传打字机 在linux.中是接收用户输入、输出结果的终端仿真软件,比如我们用的mac terminal、iterm2等,更强输入辅助功能、画面绘制输出的模拟终端器;而tty变成一个虚拟概念,是linux的一个程序,每个终端模拟器关联一个虚拟tty,和内核打交道。我们可以在终端模拟器中输入tty查看关联到的虚拟ttybash是shell的一种具体实现,可以理解成实例和类的关系
终端模拟器:Mac Terminal、iTerm2 等,关联虚拟 tty 的输入输出软件。终端成为内核的一个模块,它可以直接向 TTY 驱动发送字符,并从 TTY 驱动读取响应然后打印到屏幕上。也就是说,用内核模块模拟物理终端设备,因此被称为终端模拟器。
Shell:command interpreter,处理来自终端模拟器的输入,解释执行之后输出结果给终端。Shell 是一个用 C 语言编写的程序,它是用户使用 Linux 的桥梁。Shell 既是一种命令语言,又是一种程序设计语言。Shell 是指一种应用程序,这个应用程序提供了一个界面,用户通过这个界面访问操作系统内核的服务。
Bash:Shell 的一种具体实现。它被设计用来听从用户的命令,做用户让它做的事。
上图是一个典型的 Linux 桌面系统。终端模拟器就像过去的物理终端一样,它监听来自键盘的事件将其发送到 TTY 驱动,并从 TTY 驱动读取响应,通过显卡驱动将结果渲染到显示器上。
发展
1、Ken Thompson(来自贝尔实验室)在 1971 年为 UNIX 开发了第一个 shell,称为 V6 shell。
2、Stephen Bourne 在贝尔实验室 为 V7 UNIX 所开发的 Bourne shell,即 sh。
3、开源组织 GNU 为了取代 Bourne shell 开发的 Bourne-Again shell,即 Bash。
-
除了替代 v6 shell,sh 还有几个优点,把控制流程,循环,变量引入了脚本,提供了一种更具功能性的语言
-
主流 Linux 系统使用的 shell,许多都以它为锚点。
-
bash是 sh 的超集,可以直接执行大部分 sh 脚本。 Bash 在兼容 Bourne shell 脚本编程的同时,集成了 Korn shell 和 C shell 的功能,包括命令历史,命令行编辑,目录堆栈(pushd 和 popd),一些实用环境变量,命令自动补全等。
构成
1、shell 不仅提供了与内核和设备交互的方法,还集成了一些今天软件开发中通用的设计模式(比如管道和过滤器),具备控制流程,循环,变量,命令查找的机制。
2、既是命令解释器,也是一门编程语言,作为命令解释器,它提供给用户接口,使用丰富的 GNU 工具集,第三方的或者内置的,比如 cd、pwd、exec、test、netstat 等。
二、语法和命令
变量
- 自定义变量中默认声明的是字符串型,其余需要手动声明。
父子Shell
- 自定义变量在父进程中使用,环境变量和系统环境变量在父进程和子进程中都可以使用到。
自定义变量
- !/bin/bash 第一行的内容指定了shell脚本解释器的路径,而且这个指定路径只能放在文件的第一行。第一行写错或者不写时,系统会有一个默认的解释器进行解释。
- 使用=赋值时,=左右不能有空格,有空格代表其他命令。
- 将命令复制给变量,这样可以通过变量名来执行命令。
- 将命令结果赋值给变量,可以通过变量名来访问命令结果。
- 声明整形或者进行数学运算,需要用到let,或declare -i来进行声明。
declare声明变量的含义
系统环境变量
- $n:当前脚本的第n个参数,n=1,2,…,9。
- $?:命令或程序执行完后的状态,返回0表示执行成功。
- \h:显示简写的主机名。
@:显示 12 小时制时间,格式为"HH:MM am/pm"。
\u:显示当前用户名。
\w:显示当前所在目录的完整名称。
配置文件加载
1、login shell,登录式的 Shell,首先会读取和执行 /etc/profiles,这是所有用户的全局配置文件,接着会到用户主目录中寻找 /.bash_profile,或者/.bash_login 或者 ~/.profile,它们都是用户个人的配置文件。
- 如果三个文件同时存在的话。它们的优先级顺序是 ~/.bash_profile > ~/.bash_login > ~/.profile。
- 如果 ~/.bash_profile 存在,那么一切以该文件为准,并且到此结束,不再加载其它的配置文件。
- 如果 ~/.bash_profile 不存在,那么尝试加载 ~/.bash_login。
- 如果~/.bash_login 存在的话就到此结束,不存在的话就加载 ~/.profile。
2、non—login shell,非登录时的shell,直接读取 ~/.bashrc。
3、 ~/.bashrc 文件还会嵌套加载 /etc/bashrc
- ~/.bashrc是针对整个系统所有用户的,/etc/bashrc是针对特定用户的./etc/bashrc修改了以后要重启系统才生效,而用户目录下.bashrc修改了以后重新登录就生效。
4、修改某配置文件,需要它在shell中生效时,使用source filename执行这个脚本。
运算符和引用
管道
如果需要互通,比如第一个命令的返回传递给第二个命令,就需要用到管道了,管道的本质就是将多个程序进行了一个连接,和信号一样,也是进程通信的方式之一
1、第一个命令将文件platform.access.log的内容传递给grep命令,用于查找文件中包含字符串ERROR的行。这个命令的作用是查找platform.access.log文件中的错误信息。
2、第二个命令使用netstat命令来显示所有的网络连接状态,然后将结果传递给grep命令,用于查找所有处于ESTABLISHED状态的连接。最后,将结果传递给wc -l命令,用于统计行数。这个命令的作用是统计当前系统中所有处于ESTABLISHED状态的网络连接数量。
3、第三个命令使用find命令在当前目录中查找所有扩展名为.sh的文件,然后将结果传递给xargs命令,将查找到的文件名作为参数传递给ls -l命令,用于显示这些文件的详细信息。这个命令的作用是列出当前目录中所有扩展名为.sh的文件的详细信息。
重定向
每个shell命令在执行时都会打开三个文件描述符,文件描述符0、1、2,分别对应itdin,stout,ster,这三个文件描述符默认默认指向终端输入、终端翰H出,那么当命令需要获取输入的时候,它会去读取fdO,当要输出的时候它会像fd1、fd2写入,改变这些描述符指向的行为叫做重定向。
例子:
#将hello world写入users文件
$ echo "hello world" > users
$ cat users
hello world
#追加
$ echo "hello world" >> users
$ cat users
hello world
hello world
#用wc -l统计users文件的行数
$ wc -l < users
2
判断命令
shell中提供了test、[、[[三种判断符号,可用于:
- 整数测试
- 字符串测试
- 文件测试
语法:
- test condition
- [ condition ]
- [[ condition ]]
注意:
- 中括号前后要有空格符
- [ 和test是命令,只能使用自己支持的标志位,<、>、=只能用来比较字符串
- 中括号内的变量,最好都是用引号括起来
- [[更丰富,在整型比较中支持<、>、=,在字符串比较中支持=~正则
shell中的比较运算符:
-eq //等于
-ne //不等于
-gt //大于 (greater)
-lt //小于 (less)
-ge //大于等于
-le //小于等于
shell中的逻辑运算符:
逻辑与: &&
第一个条件为假时,第二个条件不用再判断,最终结果已经有;
第一个条件为真时,第二个条件必须得判断。
逻辑或: ||
逻辑非: !
注意点:
- shell中如果是等于、不等于,既可以用 -eq、-ne (外面需要加中括号),也可以用 == 、!=(外面加中括号或双括号都行)
- shell中如果是大于,大于等于,小于,小于等于,用 -gt, -ge,-lt,-le 的话,则需要加中括号。
- shell中大于、大于等于,小于,小于等于想用 >,>=,<,<=,则需要加双括号,而不是中括号
分支语句
1、if语句
if语句就是shell脚本中实现分支的命令,if脚本有单分支、双分支和多分支三种形式。
if单分支:
if 表达式; then
执行语句
fi
if双分支:
if 表达式; then
执行语句
else
执行语句
fi
if多分支:
if 表达式; then
执行语句
elif 表达式;then
执行语句
elif 表达式;then
执行语句
…………
else
执行语句
fi
案例: 输入一个文件,判断是否存在
#!/bin/bash
read -p "Please input a file:" file
if [ -f $file ]; then
echo "File: $file exists!"
else
echo "File: $file not exists!"
fi
if表达式中常见的判断运算符如下:
-f 判断文件是否存在
-d 判断目录是否存在
-z 判断是否是空字符串
2、cause语句
case语句主要适用情况
- 某个变量存在多种取值
- 需要对其中的每一种取值分别执行不同命令序列
case 分支语句的语法结构:
case 变量值 in
模式 1)
命令序列1
;;
模式 2)
命令序列2
;;
.......
*)
默认命令序列
esac
案例:检查用户输入字符类型
read -p "请输入一个字符,并按Enter键确定:" KEF
case "$KEY" in
[a-z]|[A-Z])
echo "您输入的是字母!"
;;
[0-9])
echo "您输入的是数字!"
;;
*)
echo "您输入的是空格,功能键或其他控制字符。"
esac
循环
while循环
语法:
while 表达式
do
command
done
案例:
#!/bin/bash
a=0
while [ $a -le 10 ]
do
echo $a
let a++
done
until循环
语法:
until 条件测试操作
do
命令序列
done
案例:
#!/bin/bash
i=0
sum=0
until [ $i -gt 100]
do
sum=$(($sum+$i))
let i++
done
echo "1-100的和为:$sum"
for循环
语法:
for 变量名 in 取值列表
do
命令序列(命令行)
done
案例:
#!/bin/bash
#for 1+...+100
a=0
for ((i=1;i<=100;i++))
do
a=$(($a+$i))
done
echo "1-100所有数字相加的和为$a"
函数
案例:
#!/bin/bash
# 函数定义
function show(){
echo "Hello world"
}
# 函数调用
show
运行结果:
Hello world
注意点:
- shell自上而下执行,函数必须在使用前定义
- 函数获取变量和 shell script类似,1、$2 ...获取函数内 return仅仅表示函数执行状态,不代表函数执行结果
- 返回结果一般使用echo、 printf,在外面使用$() 、`` 获取结果
- 如果没有return ,函数状态是上一条命令的执行状态,存储在$?中
模块化
模块化的原理是在当前shell内执行函数文件,方式:source[函数库的路径]
常用命令
三、执行过程和原理
- shell脚本一般以.sh结尾,也可以没有,这是一个约定;第一行需要指定用什么命令解释器来执行
2、启动方式
执行过程
1、字符解析
- 识别换行符、分号(;)做行的分割
- 识别命令连接符(||&&管道)做命令的分割
- 识别空格、tab符,做命令和参数的分割
2、shell展开,例如{1...3}解析为1 2 3
3、重定向,将stdin、stdout、stderr的文件描述符进行指向变更
4、执行命令
- builtin直接执行
- 非builtin使用$PATH查找,然后启动子进程执行
5、收集状态并返回给脚本
shell展开
1、大括号展开{...}
- 一般由三部分构成,前缀、一对大括号、后缀,大括号内可以是逗号分割的字符串序列,也可以是序列表达式{x..y[..incr]}
2、波浪号展开~
3、参数展开${}
- 间接参数扩展${!parameter},其中引用的参数并不是parameter而是parameter的实际的值
- 参数长度${#parameter}
-
空参数处理
${parameter:-word}#为空替换
${parameter:=word}#为空替换,并将值赋给parameter变量
${parameter:?word}#为空报错
${parameter:+word}#不为空替换
-
参数切片
${parameter:offset}
${parameter:offset:length}
-
参数部分删除
${parameter%word} #最小限度从后面截取word
${parameter%%word}#最大限度从后面截取word
${parameter#word}#最小限度从前面截取word
${parameter##word}#最大限度从前面截取word
4、命令替换
- 在子进程中执行命令,并用得到的结果替换包裹的内容,形式上有两种:$(...)或
...
5、数学计算$((..))
- 使用$( ( ) ) 包裹数学运算表达式,得到结果并替换
6、文件名展开*?[..]外壳文件名模式匹配
- 当有单词没有被引号包裹,且其中出现了‘*’,‘?’,and ‘[’ 字符,则shell会去按照正则匹配的方式查找文件名进行替换,如果没找到则保持不变。
| 元字符 | 含义 |
|---|---|
| * | 匹配零或多个字符 |
| ? | 匹配一个字符 |
| [abc] | 匹配一个字符集内的一个字符,如a,b,c |
| {a,ile,ax} | 匹配一个字符或字符集 |
| [!a-z] | 匹配从a~z范围以外的一个字符 |
| |转义或禁止元字符 | |
| {1...10} | 可以通过范围操作符来进行批量匹配 |
四、调试和前端集成
调试
- 普通log,使用echo、printf
- 使用set命令
- vscode debug插件
VSCode配置
-
shellman:代码提示和自动补全
-
shellcheck:代码语法校验
-
shell-format:代码格式化
-
Bash Debug:支持单步调试
- 安装vscode插件
- 编写launch.json文件
- 升级bash到4.x以上版本
前端集成
- node中通过exec、spawn调用shell命令
- exec启动一个子shll进程执行传入的命令,并且将执行结果保存在缓冲区中,并且缓冲区是有大小限制的,执行完毕通过回调函数返回
- spawn默认不使用shell,而是直接启动子进程执行命令,且会直接返回一个流对象,支持写入或者读取流数据,这个在大数据量交互的场景比较适合
- shell脚本中调用node命令
-
借助zx等库进行javascript、shell script的融合
- 借助shell完成系统操作,文件io、内存、磁盘系统状态查看
- 借助nodejs完成应用层能力,网络io、计算等