王志远,微医前端技术部
系列文目录
| 小节主题 | 文档 | 期待产出 | 补充 |
|---|---|---|---|
| Shell 的基本介绍、常用命令以及 Shell 的流程控制 | 本文 | ||
| Shell 中的数组应用、参数处理 | |||
| Shell 的函数、模块化、公共函数库 | |||
| Shell 周边:语言特殊点/调试 Debug 方案/vscode 对 shell 的支持 |
Shell 的基本介绍、常用命令以及 Shell 的流程控制
我们为什么要学习 shell
当你遇到了服务器内存占用过多导致卡死的情况,你会怎么办?度娘 or 谷歌,你得到了这样的答案
查看占用 cpu/内存 资源最多的进程并杀死
对啊,占用多了就干掉啊!
但怎么干?习惯图形化界面的咱们找不到删除图标啊!!!没关系,度娘 or 谷歌,你得到了这样的答案(下面命令执行后会删除占用内存最多的两个进程)
ps aux|sort -rn -k +3|head -2 | awk '{print $2}' | xargs kill -9
看懂是不可能看懂的,这辈子是不可能看懂的。
抱着【试试就试试,反正不会怀孕】的态度,你登陆了 shell 并且执行了,然后你惊喜的发现,成了!腰也不酸了腿也不疼了,比新盖中盖都好使。
至此,你的问题得以解决,好学的同学可能会深入学习一番(关于此命令的详解咱们放在文末),然后随着时间推移,短期记忆海马体突触消散而告终。此文结束。。。。。慢着 QAQ,问一句,下次再出现这个情况咋办?再找一遍吗?我们陷入了循环
显然,我们需要破除循环。怎么破除?想想我们熟悉的高级语言怎么解决这个问题的,封装呀,以 js 为例,各种稀奇古怪的操作被封装在一个又一个的库里,工具库的 lodash、网络请求的 axios 等等,函数名取的清晰易懂,取来即可。这才是我们想要的效果。
可 linux 是命令啊,一个个命令用分号或者管道符链接执行,咋封装?而且对于【批量执行命令】这个需求而言,用这两种方法真的不会感觉很冗长吗?这就迎来了我们的主角:shell 语言
让我们来看看对于 shell 而言怎么实现上面的需求:删除占用内存最多的两个进程
- 创建 shell 文件,作为函数库,假定取名为
shell-libs.sh - 定义对应函数,假定取名为
killProcessByMemory,实现功能为,传递进程数 n,杀死占用内存排名前 n 位进程 - 引入函数库文件:
source shell-libs.sh
shell-libs.sh
function arr_includes () {
local name=$1
eval local innerArr=(\${${name}[@]})
local item=$2
if [[ "${innerArr[@]}" =~ "${item}" ]]; then
echo 0
else
echo 1
fi
}
function boolean () {
local bool=1
if [[ $1 == 0 ]]; then
# source ../install/git/git.sh
bool=0
fi
return $bool
}
function killProcessByMemory() {
params=($*)
# 杀死进程的命令
killCmd="kill -9"
# 查询进程号的命令
memoryInfoCmd='ps aux | sort -rn -k +3 | head -${1} | awk "{print $2}"'
# echo memoryInfo:$memoryInfo
needkill=`arr_includes params kill`
if boolean $needkill; then
echo "需要删除"
handledCmd="${memoryInfoCmd} | ${killCmd}"
else
echo "只需要查询"
handledCmd=$memoryInfoCmd
fi
echo '最终需要的命令为'
echo $handledCmd
}
当做完这几步初始动作后,我们要实现需求只需要执行下面命令(执行函数)即可
killProcessByMemory 2 # 只需要查询进程号 最终需要的命令为 ps aux | sort -rn -k +3 | head -${1} | awk "{print $2}"
killProcessByMemory 2 kill # 需要删除 最终需要的命令为 ps aux | sort -rn -k +3 | head -${1} | awk "{print $2}" | kill -9
函数库很多,但最重要的是,我们的初始动作只需要做一遍,一劳永逸!!
其实不只是做一个常用命令的封装,我们还可以做到
- 操作系统的初始化:git、nvm、node 等等等的安装一键完成、常见软件的自动安装(win、mac 也可以!)
- 远程服务器配置:远程服务器免密登陆一键完成
- 爬虫动作的封装:定时任务不再是执行命令,而是一系列动作的合集
- 等等等
好了,好处显而易见,我们现在对于 shell 有啥念想?兄弟姐妹们,学它!
整体定义
诱惑了这么久,还是来给个定义吧:shell 是一个命令行解释器,它为用户提供了一个向 Linux 内核发送请求以便运行程序的界面系统级程序;用户可以用 Shell 来启动、挂起、停止或者编写一些程序;Shell 还是一个功能相当强大的编程语言,易编写,易调试,灵活性较强,是解释执行的脚本语言,在 Shell 中可以直接调用 Linux 系统命令。
乱入一下:案例解释
对于上面【传递进程数 n,杀死占用内存排名前 n 位进程】案例而言
ps aux|sort -rn -k +3|head -2 | awk '{print $2}' | xargs kill -9
ps 命令查找与进程相关的 PID 号:
- a:显示现行终端机下的所有程序,包括其他用户的程序
- u:以用户为主的格式来显示程序状况
- x:显示所有程序,不以终端机来区分
sort 对内容根据指定列进行排序
- r:表示是结果倒序排列
- n:以数值大小排序
- -k +3:则是针对第 3 列的内容进行排序(第三列是 cpu,第四列是内存)
head 只显示前指定行的数据,获取默认前 10 行数据
awk 数据处理神器,这里用于获取第二列(PID,进程 id)
乱入结束,让我们开始叭!
整体目录:如何学习一门语言
语言大同小异,核心不过【变量、运算、语句、函数、框架】,shell 也是门语言,所以我们也会根据这个脉络进行学习;
- 变量:定义、赋值、作用域
- 运算:算术运算符, 逻辑运算符
- 语句:测试,顺序,分支,循环
- 函数:类,接口,包
- 框架:系统函数库之上的封装
- 特殊点:脚本控制、计划任务、管道、重定向、可视化调试 shell 脚本(vscode 插件 Bash Debug)
但 shell 又有所不同,因为它的出现强依赖【unix】,unix 的哲学:一条命令只做一件事情;为了组合命令和多次执行,最先是分号;用于同行组合,但不利于展示,于是出现了 shell 脚本文件,用来保存需要执行的命令,所以有别于其他语言又会出【环境变量、配置文件等等概念】,也强依赖 unix 的各个命令,这点是需要贯彻整个 shell 学习脉络的。
注意:本文不会过多的牵扯 linux 的知识,awk、xarg、print 等等等,小伙伴放心,我们学习最核心的语言逻辑,至于工具,多用即可,放心食用
shell 脚本基础概念
- 什么是 shell
- linux 的启动过程
- 怎么编写一个 shell 脚本
- shell 脚本的执行方式
- 内建命令和外部命令的区别
什么是 shell
命令解释器,用于解释用户对操作系统的操作,命令解释器有很多种,具体可见:(默认的是 bash,其中的 a 是指 again,意思是汇总其他 shell 解释器的优点,重写实现)
cat /etc/shells
linux 的启动过程
BIOS - MBR - BootLoader(grub) - kernel - systemd - 系统初始化 - shell
六步骤
1. BIOS 引导,BIOS 系统是基本的输入输出系统,功能嵌在主板上,用于选择使用哪个介质(硬盘、光盘、网络)
2. MBR(主引导记录):用于确定硬盘是否可以被引导
3. BootLoader(grub):用于启动和引导内核(种类和版本)
4. kernel:
5. systemd:1 号进程
shell 脚本的创建与执行
怎么编写一个 shell 脚本
unix 的哲学:一条命令只做一件事情;
为了组合命令和多次执行,于是出现了 shell 脚本文件,用来保存需要执行的命令。
- 指定解释器:
#!/bin/xxx,默认是#!/bin/bash;这其实是后缀,因为在 Linux 中,文件后缀是没有意义的,所以操作系统要知道这个脚本文件该用什么应用来执行,就需要这个注释来指明,比如 node 就是#!/usr/bin/node - 为文件赋予可执行权限,即
chmod u+x [filename]
案例:实现 sh 脚本文件,查看/var 下所有子目录空间
- 显示当前目录下的所有子目录所占空间:du -sh *
cat >> memo.sh << EOF
#!/bin/bash
cd /var
du -sh *
EOF
# 赋予权限 chmod u+x memo.sh
# 执行 ./memo.sh
怎么执行一个 shell 脚本
存在四种执行方式
- [解释器] [文件名]:bash ./filename.sh,不需要赋予可执行权限,新创建进程,采用解释器进行执行
- [./文件名]:./filename.sh,新创建进程,需要赋予可执行权限,采用#!指定解释器进行执行
- source [./文件名]: source ./filename.sh,在当前进程进行执行
- .[文件名]:在当前进程进行执行
| 执行方式 | 案例 | 是否需要可执行权限 | 是否会创建新进程 | 补充 |
|---|---|---|---|---|
| [解释器] [文件名] | bash ./filename.sh | 不需要 | 会 | 内部和当前的终端变量不互通 |
| [./文件名] | ./filename.sh | 需要 | 会 | 当使用默认解释器时的缩写 |
| source [./文件名] | source ./filename.sh | 不需要 | 不会 | 内部和当前的终端变量互通 |
| .[文件名] | .filename.sh | 不需要 | 不会 | 其实就是 source 的缩写 |
- 内建命令不需要创建子进程
- 内建命令只对当前 shell 生效
shell 之变量
- 增删改查:定义、赋值、引用+查询、删除
- 作用范围
- 系统环境变量
- 环境变量配置文件
增删改查:定义、赋值、引用、删除、类型处理
定义
即变化的量,字母、数字、下划线的组合,且不能以数字开头(shell 变量是弱类型,不区分变量类型);
-
变量必须以字母或下划线开头,名字中间只能由字母,数字和下划线组成
-
变量名的长度不得超过 255 个字符
-
变量名在有效范围内必须唯一
-
变量默认类型都是字符串(存在类型【字符串、整型、浮点型、日期型】)
$ x=1 $ y=2 $ z=3 $ k=$x+$y+$z $ echo $k 1+2+3
赋值
非交互式赋值存在四种赋值方式
- 变量名=变量值(等号左右不能有空格): a=20
- let 进行赋值:let a=20
- 将命令赋值给变量:l=ls
- 将命令的结果复制给变量(常用):使用(ls -l /etc)
- 变量间的赋值可以设定默认值:
a=${[变量 b]-[默认值]},假设默认值是*,a=${b-*}
注意:变量值如果有空格等特殊字符,可以包含在""或''中
引用+查询
在 shell 中存在四种引用:单引号、双引号、${}、``
- 双引号:部分引用,使用这种引用时,$、`(反引号)、(转义符) 这 3 个还是会解析成特殊的意义
- 单引号:完全引用,只原样输出
- 反引号`:命令替换
变量名(不产生歧义,比如字符串拼接就不能缩写);
而查询则可以使用set命令,其会默认查询系统中默认所有已经生效的变量,包括系统变量,也包括自定义变量,结合管道运算符可以精准查询
set | grep []
举例:查询wzy变量的值
删除
删除则是unset命令,使用方式如下
unset [变量名]
特殊数据结构之数组
-
定义:[变量名]=(a b c)
-
显示所有元素:echo ${变量名[@]}
-
显示数组元素个数:echo ${#变量名[@]}
-
显示数组第一个元素:echo 变量名)
colors=( yellow red blue )
echo ${colors[@]}
echo ${#colors[@]}
echo ${colors[0]}
echo $colors
类型处理
shell 变量为弱类型并且默认是字符串类型,要想设置变量的类型,可以使用declare命令。
declare 命令用来声明变量类型
declare [+/-] [选项] 变量名
| 选项 | 含义 |
|---|---|
| - | 给变量设定类型属性 |
| + | 取消变量的类型属性 |
| -a | 将变量声明为数组类型 |
| -i | 将变量声明为整数型 |
| -x | 将变量声明为环境变量 |
| -r | 将变量声明为只读变量 |
| -p | 显示指定变量的被声明的类型 |
设定类型
整数型
举例,将变量设定为整数型从而实现加和,通过配置项-i进行设定
数组类型
通过配置项-a进行设定
定义
取第一个值
取第二个值
输出所有
取消变量的类型属性
承接上面,此时变量c已经是整型了,我们希望实现取消变量c的类型(即变回字符型),从而实现拼接的效果
设定为只读变量
通过配置项-r进行设定
declare -r [变量名]
查询变量
此时也可以通过配置项-p查询变量类型
declare -p [变量名] #显示指定变量的被声明的类型
设置为环境变量
通过配置项-x设置变量为环境变量
declare -x [变量名]
由此也可以看到,之前定义全局环境变量的方式export [变量名]其实就是declare -x [变量名]的语法糖
变量的作用范围
-
默认范围:只针对当前的终端(shell)生效
-
支持子进程访问父进程的变量:
export [变量名]=[变量值];(取消变量可以使用unset [变量名]) -
系统环境变量:每个 shell 打开都能获取到的变量
其中第一条可以使用bash命令创建一个新的 shell 进行测试;第二条就是 export 和 unset 关键字;关键是系统环境变量,重点分析:
系统环境变量
环境变量主要根据两个角度进行划分:用户级别是属于系统还是属于用户;shell 级别是属于当前的 shell 还是所有 shell。而这些是通过配置文件进行区分和记录的,不同作用范围和功能的变量分属不同的配置文件中,主要有四个文件/etc/profile、~/.bash_profile、~/.bashrc、/etc/bashrc。系统环境变量的查询:env 和 set
常见变量
| 变量名 | 含义 | 常见操作 | 注意 |
|---|---|---|---|
| $PATH | 搜索路径;当执行全局命令(即直接执行命令名)时,会在 PATH 值内所有路径进行查找并执行 | PATH=$PATH:[新增的全局路径] | 当前定义的变量,只会会当前终端和其子 shell 生效(因为所有的环境变量都被 export 导出过了) |
| $PS1 | 当前提示的终端信息 | 添加完整路径信息、时间等等信息 | |
| $? | 上条命令是否正确执行 | 0(正确) | 1(错误,非 0 值) | 预定义变量 |
| $$ | 当前进程的 PID | 预定义变量 | |
| $0 | 当前进程(或执行文件)的名称 | 预定义变量;bash xx.sh 时是 bash;.xx.sh 时是 xx.sh | |
| $# | 传递到脚本的参数的个数 | ||
| $* | 以一个单字符串显示所有向脚本传递的参数 | ||
| $1-9(10 之后要用{}包裹) | 用于获取命令执行参数 | 位置变量 | |
环境变量配置文件
- /etc/profile:
- /etc/profile.d:目录
- ~/.bash_profile
- ~/.bashrc
- /etc/bashrc
需要注意的是
- etc 下的是所有用户通用,而~则是用户用户家目录,只对指定用户生效;
- profile 文件和 bashrc 文件的加载取决于 shell 的种类是 loginShell 还是 noLoginShell(在登陆时采用的是
su - [用户名],这时如果有这个-号就是 loginShell,不加则是 noLoginShell)- noLoginShell 只会加载执行 bashrc
- loginShell 则会全部加载执行
加载顺序为
/etc/profile - ~/.bash_profile - ~/.bashrc - /etc/bashrc
如果修改了配置文件,是不会立即重新加载的,需要我们重启终端或者执行 source 命令
source [修改的配置文件地址]
在操作系统加载过程中,主要按如下顺序进行加载
而作用范围如下图
shell 之运算
- 赋值运算符:用于字符串赋值,赋值使用=,取消赋值使用 unset(想到于 js 中的 delete)
- 算数运算符:用于算数赋值,+-*/**%,使用方式如下
- 使用 expr 声明,如 a=`expr 4 + 5`(不支持浮点数)
- 数字常量:比 expr 精简,可以使用 let 进行赋值操作,如 let a=1+2;
- 双圆括号:是 let 的缩写,如((a=10))、((a++))
其实 expr、let 或者(())都是为了向 shell 声明,我目前在做算数赋值的动作,这也就能理解为什么要把整个式子都放在双圆括号中了。
shell 之特殊符号
引号
- 双引号:部分引用,使用这种引用时,$、`(反引号)、(转义符) 这 3 个还是会解析成特殊的意义
- 单引号:完全引用,只原样输出
- 反引号:执行命令
括号
- 圆括号
- ():单独使用圆括号会产生一个子 shell(这样执行的表达式的输出就不会在当前 shell 中显示),也可用于数组赋值(colors=(yellow red blue))
- (()):算数运算赋值,是 let 的缩写
- $():将命令赋值给变量
- 方括号
- []:单独使用是测试或数组元素功能
- [[]]:表示测试表达式
[ 5 -gt 4 ] # 5 是否大于 4
[[ 5 -gt 4 ]] # 5 是否大于 4
echo $? # 测试上条命令执行结果
- 尖括号<>:重定向符号
- 花括号{}:
- 输出范围:echo {0..9}
- 文件复制
# 下面两条命令等价
cp /etc/passwd{,.bak}
cp /etc/passwd /etc/passwd.bak
运算符号和逻辑符号
- 算数运算符:+-*/ %
- 比较运算符:><=
- 逻辑运算符:&&||!
(( 5 > 4 ))
echo $?
(( 5 > 4 && 6 < 5))
echo $?
(( 5 > 4 || 6 < 5))
echo $?
(( ! 5 > 4 ))
echo $?
转义符号
- \转义某字符
- 普通字符转义后有不同的功能,如\n
- 特殊字符转义后消除了特殊功能,如
\’
其他特殊字符
| 字符 | 含义 | 补充 |
|---|---|---|
| # | 注释 | |
| ; | 命令分隔符 | case 语句的分隔符要转义 ;; |
| : | 空指令 | 返回值永远是真 |
| . | 和 source 命令相同 | . sh 文件名 |
| ~ | 家目录 | cd ~回到家目录;cd -/+ 回到上/下一个目录; |
| , | 分隔目录 | |
| * | 通配符 | |
| ? | 条件测试 或 通配符 | ls ?.sh 查询文件名为单个字或没有字的 sh 文件 |
| $ | 取值符号 | |
| | | 管道符 | |
| & | 后台运行 | |
| _ | 空格 | |
# if
if [ "$PS1" ]; then
enif
# case
case $TERM in
xterm*|vte*)
语句 1
;;
screen*)
语句 2
;;
*)
语句 3
;;
esac
运算的优先级
尾声
至此,我们的 shell 训练营首篇就结束啦。