shell训练营 | 1. Shell 的基本介绍、常用命令以及 Shell 的流程控制

1,783 阅读7分钟

王志远,微医前端技术部

系列文目录

小节主题文档期待产出补充
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 而言怎么实现上面的需求:删除占用内存最多的两个进程

  1. 创建 shell 文件,作为函数库,假定取名为shell-libs.sh
  2. 定义对应函数,假定取名为killProcessByMemory,实现功能为,传递进程数 n,杀死占用内存排名前 n 位进程
  3. 引入函数库文件: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 脚本文件,用来保存需要执行的命令。

  1. 指定解释器:#!/bin/xxx,默认是#!/bin/bash;这其实是后缀,因为在 Linux 中,文件后缀是没有意义的,所以操作系统要知道这个脚本文件该用什么应用来执行,就需要这个注释来指明,比如 node 就是#!/usr/bin/node
  2. 为文件赋予可执行权限,即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
  • 将命令的结果复制给变量(常用):使用()或者letc=()或者``:let c=(ls -l /etc)
  • 变量间的赋值可以设定默认值:a=${[变量 b]-[默认值]},假设默认值是*,a=${b-*}

注意:变量值如果有空格等特殊字符,可以包含在""或''中

引用+查询

在 shell 中存在四种引用:单引号、双引号、${}、``

  • 双引号:部分引用,使用这种引用时,$、`(反引号)、(转义符) 这 3 个还是会解析成特殊的意义
  • 单引号:完全引用,只原样输出
  • 反引号`:命令替换

变量名即对变量的引用,在部分情况下可以缩写为{变量名}即对变量的引用,在部分情况下可以缩写为变量名(不产生歧义,比如字符串拼接就不能缩写);

而查询则可以使用set命令,其会默认查询系统中默认所有已经生效的变量,包括系统变量,也包括自定义变量,结合管道运算符可以精准查询

set | grep []

举例:查询wzy变量的值

删除

删除则是unset命令,使用方式如下

unset [变量名]

特殊数据结构之数组

  • 定义:[变量名]=(a b c)

  • 显示所有元素:echo ${变量名[@]}

  • 显示数组元素个数:echo ${#变量名[@]}

  • 显示数组第一个元素:echo 变量名[0](如果直接访问时也会只显示第一个元素echo{变量名[0]}(如果直接访问时也会只显示第一个元素 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 [变量名]的语法糖

变量的作用范围

  1. 默认范围:只针对当前的终端(shell)生效

  2. 支持子进程访问父进程的变量:export [变量名]=[变量值];(取消变量可以使用unset [变量名]

  3. 系统环境变量:每个 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 训练营首篇就结束啦。