1、Shell 介绍
1.1、学习Shell 的价值
1、Linux 服务器的基本操作和管理
2、前端 Node.js 服务的进程管理,问题排查,资源监控等运维操作
3、使用 shell 编写 TCE、SCM、Docker 脚本,完成服务编译和部署
1.2、课程准备
1、一台安装了 Linux 系统的物理机或者云主机,可运行 shell 脚本
2、本地的 vscode 安装 Bash Debug 插件,并升级 bash 到 4.x 以上
3、Npm 全局安装 zx 依赖
2、Shell 基本概念
2.1 终端:
获取用户输入、展示运算结果的硬件设备
2.2 tty
teletpe Writer 的简称,和终端等价,早起指电传打印机,在linux 中是输入、输出 环境
2.3 终端模拟器
Mac terminal、item2 等,关联虚拟 tty 的输入输出软件
2.4 shell
command interpreter, 命令行解释器,处理来自终端模拟器的输入,解释执行之后输出结果给终端
2.5 Bash
shell 的一种具体实现
2.6 发展
-
Ken Thompson(来自贝尔实验室)在 1971 年为 UNIX 开发了第一个 she11,称为 v6 she11
-
Stephen Bourne 在贝尔实验室 为 V7 UNIX 所开发的 Bourne she11 , 即 sh
-
开源组织 GNU 为了取代 Bourne she11 开发的 Bourne-Again shel1, 即 Bash.png)
除了替代v6 shell,sh 还有几个优点,把控制流程,循环,变量引入了脚本,提供了一种更具功能性的语言
主流Linux系统使用的shell,许多都以它为锚点
bash是sh的超集,可以直接执行大部分sh脚本。
Bash 在兼容 Bourne shell 脚本编程的同时,集成了 Korn shell 和C shell的功能,包括命令历史,命令行编辑,目录堆栈(pushd和popd),一些实用环境变量,命令自动补全等。
2.7 构成
3、命令和语法
3.1 变量
| 类型 | 作用域 | 声明方式 | 规范 |
|---|---|---|---|
| 自定义变量 | 当前shell | = | 字符型、整型、 浮点型、日期型 |
| 环境变量 | 当前shell 及其子shell | export 、declare -x | |
| 系统环境变量 | 所有 shell | 启动加载 |
3.2 自定义变量
3.3 系统环境变量
| 变量名 | 含义 | 常见操作 |
|---|---|---|
| $0 | 当前 shell 名称或者脚本名称 | 2等可以获取到传入参数 |
| $# | 传入脚本的参数数量 | if [$# -gt 1 ] |
| $* | 传入脚本的所有参数 | |
| $? | 上条命令执行的状态码 | if [$? -eq 0]; |
| $PS1 | 命令提示符 | export PS="\u@\h\w> " |
| $HOME | 用户主文件夹 | cd ~ |
| $PATH | 全局命令的搜索路径 | PATH=$PATH:[新增路径] |
如何定义变量,写法、导出环境变量、变量作用域、父子shell的关系?
Bash Shell 在启动时总要配置其运行环境,例如初始化环境变量,设置命令提示符,指定系统命令路径等,
3.4 配置文件加载
source ~/.bashrc
通过系统用户登录默认运行的 shell
非登录交互式运行 shell 执行脚本运行非交互式 shell
如果取得 bash 需要完整的登录流程,我们称之为 loain shell ,比如 ssh 远程登录一台主机 不需要登录的 bash 我们称为 non-login bash ,比如在原来的 bash 中执行 bash 开启子进程、执行一些外部命令 如果修改了配置文件,不会立即生效,需要我们重启终端或者执行 source 命令
3.5 运算符和引用
| 类型 | 符号 | 作用 | 用法 |
|---|---|---|---|
| 算数运算符 | + - * / % | & | 常规运算 | |
| 逻辑运算符 | || && ! | ||
| 比较运算符 | == != < > | ||
| 引号 | 双引号"" | 部分引用,仅仅 $ ` \ 保留作用 | foo="${a}123" |
| 引号 | 单引号‘’ | 完全引用,原样输出 | foo='foo$a' |
| 引号 | 反引号`` | 执行命令 | foo=1s-a |
| 圆括号 | (()) | 算数运算 | foo=$((1+2)) |
| 圆括号 | () | 执行命令 | $(1s -a) |
| 命令连接 | || | cmd1 执行完且返回码为非0,则继续执行 cmd2 | cmd1 || cmd2 |
| 命令连接 | && | cmd1 执行完且返回码为0,则继续执行 cmd2 | cmd1 && cmd2 |
| 命令连接 | ; | cmd1、cmd2 串行执行 | cmd1 ; cmd2 |
| 后台运算 | & | 让命令在后台运行,可与nohup 一起使用 | cmd & |
3.6 管道
-
管道与管道符|,作用是将前一个命令的结果传递给后面的命令
-
语法: cmd1|cmd2
-
要求: 管道右侧的命令必须能接受标准输入才行,比如 grep命令,ls、mv等不能直接使用,可以使用xargs预处理
-
注意: 管道命令仅仅处理 stdout,对于stderr会予以忽略,可以使用set-opipefail 设置shell遇到管道错误退出
#! /bin/bash cat platform.access.log | grep ERROR netstat -an | grep ESTABLISHED | wc -l find . -maxdepth 1 -name "*.sh" | xargs ls -l如果需要互通,比如第一个命令的返回传递给第二个命令,就需要用到管道了,管道的本质就是将多个程序进行了一个连接,和信号一样,也是进程通信的方式之一
3.7 重定向
输出重定向符号
>:覆盖写入文件>>:追加写入文件2>:错误输出写入文件&>:正确和错误输出统一写入到文件中
输入重定向符号
<<<
每个shell命令在执行时都会打开三个文件描述符,文件描述符0.1.2,分别对应stdin,stdoutstder,这三个文件描述符默认默认指向终输入,终销输出,那么当命令需要获取输入的时候,它会去读取fdo,当要输出的时候它会像f1。fd2写入,改变这些描述符指向的行为叫做重定向
2>&1必须写在>之后
<<比较特殊,表示继续沿用当前的标准输入,只是当识别到指定的标识符后停止,将接收到的内容作为 stdin
实例:用户在命令行输入内容,当输入EOF的时候停止,所输入的内容写入foo.txt
3.8 判断命令
shell 中提供了 test、 [、[[三种判断符号,可用于:
- 整数测试
- 字符串测试
- 文件测试
语法:
- test condition
- [condition]
- [[condition]]
注意:
-
中括号前后要有空格符;
-
[ 和 test 是命令,只能使用自己支持的标志位,<、>、=只能用来比较字符串
-
中括号内的变量,最好都是用引号括起来
-
[[ 更丰富,在整型比较中支持<、>、=,在字符串比较中支持=~正则
根据程序是否正常执行(程序退出的状态)进行判断
exit: 手动退出shell 的命令 exit10 返回10给shell,返回值非0为不正常退出 $? 用于判读昂当前shell前一个进程是否正常退出(非0为不正常退出)
#! /bin/bash
name ="hello World"
[ $name == "hello" ]
script.sh : line 5: [: too many arguments
Exited with error status 2
#!/bin/bash
# 整数测试
test $n1 -eq $n2
test $n1 -lt $n2
test $n1 -gt $n2
# 字符串测试
test -z $str_a
test -n $str_a
test $str_a = $str_b
# 文件测试
test -e /dmt.&& echo "exist"
test -f /user/bin/npm && echo "file exist"
3.9 分支语句
语法1:
if condition; then
// 程序段
elif condition; then
// 程序段
esle
// 程序段
fi
#!/bin.bash
level=0
if [ -n "$level" ]; then
if [ "$level" == 0 ]; then
prefix=ERROR
elif [ "$level" == 1 ]; then
prefix=IFFO
else
echo "log level not supportted"
fi
fi
echo "[${prefix}] $message"
语法2:
case $变量 in:
"第一个变量的内容"
// 程序段
"第二个变量的内容"
// 程序段
;;
*)
// 程序段
;;
esac
#! /bin/bash
name=john
case $name in
"nick")
echo "hi nick"
;;
"john")
echo "my name is john"
;;
*)
echo "404"
;;
esac
3.10 循环
while 循环
while condition;do 程序段;done
until 循环
until condition;do 程序段;done
for 循环
for car in [words...]; do 程序段;done
#!/bin/bash
let num=0
until [ $num -gt 10 ];
do
echo "current idx: $num"
((num++))
done
#! /bin/bash
# 对列表进行循环
for foo in a b c
do
echo $foo
done
# 数值方式遍历
for((i-0;i<10;i++))
do
echo $i
done
3.11 函数
语法1:
funcName(){ echo "abc"}
语法2:
function funcName(){ echo "abc"}
注意:
- shell 自上而下执行,承数必须在使用前定义
- 函数获取变量和 shell script 类似,1、$2 ... 获取
- 函数内 return 仅仅表示函数执行状态,不代表的数执行结果
- 返回结果一般使用 echo、printf,在外面使用 $() 、`` 获取结果
- 如果没有 return ,承数状态是上一条命令的执行状态,存诸在 $? 中
#! bin/bash
printName() {
if [ $# -lt 2 ]; then
echo "illegal parameter."
exit 1
fi
echo "firstname is : $1"
echo "lastname is: $2"
}
printNanem jacky chen
函数也是命令
exit:手动退出shell、命令 exit10返回10给shell,返回值非0为不正常退出 $?用于判读昂当前shell前一个命令是否正常退出(非0为不正常退出)
为了函数内定义的变量不污流全局,我们最好使用local去定义,或者在函数退出之前使用unset去处理一下
3.12 模块化
模块化的原理是在当前 shell 内执行函数文件,方式: source[函数库的路径]
#!/bin/bash
#add 函数
#@return platForm
function add() {
declare -i res=$1+$2
echo $res
}
#! /bin/bash
source './math.sh'
total=$(add 1 2)
echo $total
3.13 常用命令
| 命令 | 使用 |
|---|---|
| grep | 查找错误日志:grep -n "ERROR" -A3 -B3 cloudfun.log 统计次数: grep -n "ERROR" -c cloudfun.log |
| sort | 指定分隔符后第三列进行排序: sort -t "" -k 3 |
| wc | 统计出现的行数,单词数,字符数、wc -lwm |
| head | 查看前十行: head -n 10 cloudfun.log |
| tail | 等追加内容: tail -f -n 10 cloudfun.log |
| cut | 对数据行的内容进行处理 cut -d "" -f 3 |
| find | 文件和目录查找 |
| xargs | 参数处理 |
| which | 查找命令路径 |
4、执行过程和原理
4.1 执行
1、she11脚本一般以.sh 结尾, 也可以没有, 这是一个约定; 第一行需要指定用什么命令解释器来执行
#! /bin/bash
#! /usr/bin/env bash
2、启动方式
# 文件名运行
./filename.sh
#解释器运行
bash ./filename.sh
#source 运行
source/filename.sh
#! 是内核识别并选择合适的解释器之后,将文本文件再交给解释器执行
4.2 执行过程
1.字符解析
- 识别换行符、分号(:)做行的分割
- 识别命令连接符 (|| && 管道 ) 做命令的分割
- 识别空格、tab符,做命令和参数的分割
-
shel1 展开, 例如{1..3} 解析为 123
-
重定向, 将stdin、stdout、stderr的文件描述符进行指向变更
-
执行命令
- builtin 直接执行
- 非builtin使用SPATH查找,然后启动子进程执行
-
收集状态并返回
这个架构类似一个流水线,在里面进行输入分析和解析
bash 会以一些特殊字符作为分隔符,将文本进行分段解析。最主要是回车还有分号”:”。在bash脚本中是以回车或者分号作为一行命令结束的标志。这就是第一层级的解析,将大段的命令行进行分段符号拓展(使用各种方法,比如大括号、波浪符~、变量和参数的展开/替换、文件名展开),并最终执行命令(通过shell 内置命令或外部命令)。
4.3 shell 展开
大括号展开(Brace Expansion){...}
一般由三部分构成,前缀、一对大括号、后,大括号内可以是逗号分割的字符串序列,也可以是序列表达式{x..v[..incr]
# 字符串序列
a{b,c,d}e=> abe ace ade
# 表达式序列,(数字可以使用 incr 调整增量,字母不行)
{1..5} => 1 2 3 4 5
{1..5..2} => 1 3 5
{a..e} => a b c d e
波浪号展开(Tilde Expansion)2~
# 当前用户目录
~ => $HOME
~/foo => $HOME/foo
# 指定用户的主目录
~fred/foo => 用户fred 的 $HOME/foo
# 当前工作目录
~+/foo => $PWD/foo
# 上一个工作目录
~-/foo => ${$OLDPWD-'~-'}/foo
参数展开(Shell Parameter Expansion)
1.间接参数扩展${!parameter},其中引用的参数并不是parameter而是parameter的实际的值
2.参数长度 ${#parameter}
3.空参数处理
-
${parameter:-word}#为空替换
-
{parameter:=word}#为空替换,并将值赋给parameter变量
-
${parameter:?word}#为空报错
-
${parameter:+word}#不为空替换
#! /bin/sh a=1 echo ${a:-word} #1 echo ${b:-word} #word echo ${par:=word} #word echo ${par:-hello} #word echo ${par:+foo} #foo
4.参数切片
-
${parameter:offset}
-
${parameter:offset:length}
5.参数部分删除
- ${parameter%word}#最小限度从后面截取word
- ${parameter%%word}#最大限度从后面截取word
- ${parameter#word}#最小限度从前面截取word
- ${parameter##word}#最大限度从前面截取word
#! /bin/sh
str=abcdefg
sp1=${str##*d}
sp2=${str%%d*}
echo $sp1 # 输出efg
echo $sp2 # 输出abc
命令替换(Command Substitution)
在子进程中执行命令,并用得到的结果替换包裹的内容,形式上有两种: $(...) 或...
#! bin/bash
echo $(whoimi)
foo(){
echo "asdasd"
}
a=`foo`
数学计算(Arithmetic Expansion) $((..))
使用 $(()) 包裹数学运算表达式, 得到结果并替换
#! /bin/bash
echo $((1+2)) # 3
文件名展开(Filename Expansion)*?[..] 外壳文件名模式匹配
当有单词没有被引号包裹, 且其中出现了'*’,'?’,and '[’ 字符,则 shell 会去按照正则匹配的方式查找文件名进行替换,如果没找到则保持不变。
#! /bin/bash
$ echo D*
# 输出当前目录下所有以D 字母开头的目录、文件
5、调试和前端集成
1.普通log,使用 echo、printf
2.使用 set 命令
3.vscode debug插件
| set 配置 | 作用 | 补充 |
|---|---|---|
| -u | 遇到不存在的变量就会报错,并停止执行 | -o nounset |
| -x | 运行结果之前,先输出执行的那一行命令 | -o xtrace |
| -e | 只要发生错误,就会终止执行 | -o errexit |
| -o pipefail | 管道链接的,只要一个子命令失败,整个管道命令就失败,脚本就会终止执行 |
#! /bin/bash
set -uxe -o pipefail
echo "hello World"
#! /bin/sh
a=1
d=(1 2 3 4 5)
echo $a
echo ${d[3]}
echo ${d[@]}
# 输出
# 1
# 4
# 1 2 3 4 5
5.2 VSCode 配置
-
shellman: 代码提示和自动补全
-
shellcheck: 代码语法校验
-
shell-format: 代码格式化
-
BashDebug: 支持单步调试
- 安装 vscode 插件
- 编写 launch.json 文件
- 升级 bash 到 4.x 以上版本
# 1. 安装最新版本 bash
brew install bash
# 2.查看安装路径
which -a bash
# 3 将新版本 bash 路径加入 PATH
PATH="/user/local/bin/bash:$PATH"
# 4 配置 vscode launch.json 启动文件
{
"version":"0.2.0",
"configurations":[
{
"type":"bashdb",
"request":"launch",
"name":"Bash-Debug (simplest configuration)",
"cwd":"${workspaceFolder}",
"poragram":"debug.sh"
}
]
}
5.3 前端集成
- node 中通过 exec、spawn 调用 shell 命令
- shell 脚本中调用 node 命令
- 借助 zx 等库进行 javascript、shell script 的融合
- 借助 shell 完成系统操作,文件io、内存、磁盘系统状态查
- 借助 nodejs 完成应用层能力,网络io、计算等
const {exec} = require('child_process');
exec('ls',['-l'],(err,student,strerr) => {
if(err){
consoel.error(error);
}
stdout && console.log(studout)
})
#! /usr/bin/env zx
let fils = await $`ls -a | grep node`
console.log(chalk.blue(`files : ${files}.`))
await sleep(1000)
await fetch('https://google.com')
let answer = await question('do you want to create new dir?');
if(answer === 'y'){
await $`mkdir temp`
}
exec 启动一个子shell进程执行传入的命令,并且将执行结果保存在缓冲区中,并且缓冲区是有大小限制的,执行完毕通过回调函数饭回,
spawn默认不使用shell,而是直接启动子进程执行命令,且会直接返回一个流对象,支持写入或者读取流数据,这个在大数据量交互的场景比较适合