shell 脚本及编程 笔记 | 青训营笔记

127 阅读8分钟

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![image (3)](D:\前端学习资料\2023掘金字节前端课程\03\image (3).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 及其子shellexport 、declare -x
系统环境变量所有 shell启动加载

555

3.2 自定义变量

3.3 系统环境变量

变量名含义常见操作
$0当前 shell 名称或者脚本名称11、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,则继续执行 cmd2cmd1 || cmd2
命令连接&&cmd1 执行完且返回码为0,则继续执行 cmd2cmd1 && 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 类似,0代表函数名,后续参数通过0 代表函数名,后续参数通过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符,做命令和参数的分割
  1. shel1 展开, 例如{1..3} 解析为 123

  2. 重定向, 将stdin、stdout、stderr的文件描述符进行指向变更

  3. 执行命令

  • builtin 直接执行
  • 非builtin使用SPATH查找,然后启动子进程执行
  1. 收集状态并返回

    这个架构类似一个流水线,在里面进行输入分析和解析

    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 前端集成

  1. node 中通过 exec、spawn 调用 shell 命令
  2. shell 脚本中调用 node 命令
  3. 借助 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,而是直接启动子进程执行命令,且会直接返回一个流对象,支持写入或者读取流数据,这个在大数据量交互的场景比较适合