王志远,微医前端技术部。爱好吉他、健身、桌游,最最关键,资深大厂员工(kfc外卖小哥),trust me,好奇心使生命有趣起来!

可视化shell调试?shell函数库?智能提示shell补全的vscode插件?这全都有,还不快到碗里来

前置信息

当你遇到了服务器内存占用过多导致卡死的情况,你会怎么办?度娘 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=$1eval local innerArr=(\${${name}[@]})local item=$2if [[ "${innerArr[@]}"  =~ "${item}" ]]; thenecho 0elseecho 1fi
}
function boolean () {local bool=1if [[ $1 == 0 ]]; then# source ../install/git/git.shbool=0fireturn $bool
}
function killProcessByMemory() {params=($*)# 杀死进程的命令killCmd="kill -9"# 查询进程号的命令memoryInfoCmd='ps aux | sort -rn -k +3 | head -${1} | awk "{print $2}"'# echo memoryInfo:$memoryInfoneedkill=`arr_includes params kill`if boolean $needkill; thenecho "需要删除"handledCmd="${memoryInfoCmd} | ${killCmd}"elseecho "只需要查询"handledCmd=$memoryInfoCmdfiecho '最终需要的命令为'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

  • 将命令的结果复制给变量(常用):使用或者:(ls -l /etc)

  • 变量间的赋值可以设定默认值:a=${[变量 b]-[默认值]},假设默认值是*,a=${b-*}

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

引用+查询

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

  • 双引号:部分引用,使用这种引用时,$、`(反引号)、(转义符) 这 3 个还是会解析成特殊的意义

  • 单引号:完全引用,只原样输出

  • 反引号`:命令替换

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

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

set | grep []

举例:查询wzy变量的值

删除

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

unset [变量名]

特殊数据结构之数组

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

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

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

  • 显示数组第一个元素:echo ${变量名[0]}(如果直接访问时也会只显示第一个元素)

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 命令相同 . bash
~ 家目录 cd ~回到家目录;cd -/+ 回到上/下一个目录;
, 分隔目录
* 通配符
? 条件测试 或 通配符 ls ?.sh 查询文件名为单个字或没有字的 sh 文件
$ 取值符号
| 管道符
& 后台运行
_ 空格
# if
if [ "$PS1" ]; thenenif
# case
case $TERM inxterm*|vte*)语句 1;;screen*)语句 2;;*)语句 3;;
esac
运算的优先级

shell 之语句

  • 顺序

  • 测试:退出与退出状态、测试命令 test

  • 分支:if-then

  • 循环:

测试命令(学习分支语句的前提)

参考文档:

  • https://www.cnblogs.com/f-ck-need-u/p/7427357.html

状态:$? 和 exit

根据程序是否正常执行(程序退出的状态)进行判断

  • exit:手动退出 shell 的命令

  • exit 10 返回 10 给 shell,返回值非 0 为不正常退出

  • $? 用于判读昂当前 shell 前一个进程是否正常退出(非 0 为不正常退出)

命令:test

tesh 命令可以用于检查文件或者比较值,可用于如下功能(可以用man test查看更具体的命令)

  • 整数比较测试

  • 字符串测试

  • 文件测试

test 测试语句可以简化为[]符号,而[]符号还有扩展写法[[]] ,支持 &&、||、<、>(推荐使用[[]])

字符串测试

两个字符串是否相等:[str1 = str2] 和 [str1 != str2]

#!/bin/bash
str1='a'
str2='a'
if [ $str1 = $str2 ]; thenecho '等于'elif [ $str1 != $str2 ]; thenecho '不等于'
fi

读取字符串长度是否是 0(空字符串):-z

#!/bin/bash
str1='a'
str2=''
if [ -z $str1 ]; thenecho 'str1 为空字符串'
elseecho 'str1 不为空字符串'
fiif [ -z $str2 ]; thenecho 'str2 为空字符串'
elseecho 'str2 不为空字符串'
fi
整数比较测试
功能 []中的符号 [[]]中的支持
等于 -eq 和 = 均支持 =
大于等于 -ge 暂无
大于 -gt >
小于等于 -le 暂无
小于 -lt <
不等于 -ne !=
#!/bin/bash
a=1
b=2# if [ $a = $b ]; then
if [ $a -eq $b ]; thenecho 'a 等于 b'
elif [ $a -ge $b ]; thenecho 'a 大于等于 b'
elif [ $a -gt $b ]; thenecho 'a 大于 b'
elif [ $a -le $b ]; thenecho 'a 小于等于 b'
elif [ $a -lt $b ]; thenecho 'a 小于 b'
elif [ $a -ne $b ]; thenecho 'a 不等于 b'
fiif [[ $a = $b ]]; thenecho 'a 等于 b'# elif [[ $a >= $b ]]; then# echo 'a 大于等于 b'
elif [[ $a > $b ]]; thenecho 'a 大于 b'
# elif [[ $a <= $b ]]; then
#     echo 'a 小于等于 b'
elif [[ $a < $b ]]; thenecho 'a 小于 b'
elif [[ $a != $b ]]; thenecho 'a 不等于 b'
fi
文件测试
功能 符号 例子
-b
-e 路径存在,不区分文件和目录 test -e [路径] 或 [ -e [路径]]
-d 目录是否存在 test -d [路径] 或 [ -d [路径]]
-f 文件是否存在 test -f [路径] 或 [ -f [路径]]
-x 文件是否可执行

分支语句

  • 基础 if

  • if-else

  • if-elif

  • 嵌套 if

  • case 语句

基本用法:基础 if

  • 测试语句

  • 命令返回值

if [ 测试条件成立 || 命令返回值为 0 ]; then
相关行为
fi # 结束

实例

if [ $UID = 0 ]; thenecho " root user "
fi
if [ pwd ]; thenecho " root user "
fi

基本用法:if-else

if [ 测试条件成立 || 命令返回值为 0 ]; then
相关行为
else
相关行为
fi # 结束

实例

#!/bin/bash
if [ $UID = 0 ]; thenecho " root user "
elseecho " $UID user "
fi

基本用法:if-elif-else

if [ 测试条件成立 || 命令返回值为 0 ]; then
相关行为
elif [ 测试条件成立 || 命令返回值为 0 ]; then
相关行为
else
相关行为
fi # 结束

实例

#!/bin/bash
if [ $UID = 0 ]; thenecho " root user "
elif [ $UID = 501 ]; thenecho " wangzy user "
elseecho " $UID user "
fi

基本用法:嵌套 if

#!/bin/bash
if [ 测试条件成立 || 命令返回值为 0 ]; thenif [ 测试条件成立 || 命令返回值为 0 ]; then相关行为else相关行为fi # 结束
else
相关行为
fi # 结束

实例实现:先判断是不是王志远账户,是则再执行对应文件

#!/bin/bash
if [ $UID = 0 ]; thenecho " root user "
elif [ $UID = 501 ]; thenif [ -x /Users/wzyan/Documents/selfspace/kkb-down/demo/if/index.sh ]; then$(pwd)/index.shelseecho " wangzy user error $(pwd)"fi
elseif [ $(pwd) = "/Users/wzyan/Documents/selfspace/kkb-down/demo/if" ]; thenecho " wangzy user  path"elseecho " wangzy user error $(pwd)"fi
fi

case 语句

case "$变量" in
"情况 1" )
命令...;;
"情况 2" )
命令...;;
* )
命令...;;
esac

实例实现:先判断是不是王志远账户,是则再执行对应文件

#!/bin/bash
case "$1" in
"start" | "restart")echo $0 starting...;;
"stop")echo $0 stoping...;;
*)echo "$0 传参错误 {start|stop|restart|reload}";;
esac

循环语句

  • for

  • c 语言风格的 for

  • while

  • util

  • 嵌套循环

  • 使用关键字 break 和 continue

for 循环

注意:

  1. 当使用反引号或者$()执行命令时,其结果会被当作列表处理

  2. 列表中包含多个变量,变量间用空格分隔

  3. 对文本处理时,要用文本查看命令取出文本内容,默认装处理,如果出现空格就会当成多行处理

for 参数 in 列表
do 执行的命令
done #封闭一个循环

实例实现:遍历列表输出

#!/bin/bash
for i in {1..9}
do zzxzxxxzzzzzzzzzzzzzzzzzzzzzzzzzzzxxxxxecho hello; echo $i
done #封闭一个循环
# 读取指定目录下的所有可执行 sh 文件
for sc_name in /etc/profile.d/*.sh
do echo $sc_name; echo $i
done #封闭一个循环

实例实现:读取命令结果进行取出所有 mp3 文件的文件名(basename [文件路径] [文件后缀])

#!/bin/bash
for filename in `ls *.mp3`
domv $filename $(basename $filename .mp3).mp4
done

c 语言风格的 for

for((变量初始化;循环判断条件;变量变化))
do循环执行命令
done

实例实现:输出 1-10

#!/bin/bash
for((i=1;i<=10;i++))
doecho $i
done

while

特点:直到输入为非 0 才中止

while [test 测试为假时中止]
do
命令
done

实例实现:输出 1-10

#!/bin/bash
a=1 # 小于 10
while [ $a -lt 10 ]
do
((a++)); echo $a;
done

实现死循环

while :; do echo ;done

until

特点:与 while 相反,直到输入为 0 才中止

until [test 测试为真时中止]
do
命令
done

实例实现:输出 1-10

#!/bin/bash
while []
do
命令
done

嵌套循环

找出/etc/profile.d 目录下所有可执行文件

for sc_name in /etc/profile.d/*.sh
doecho $sc_name
done

找出/etc/profile.d 目录下所有可执行文件并执行

for sc_name in /Users/wzyan/Documents/selfspace/kkb-down/demo/for/test-x/*.sh; doif [ -x $sc_name ]; then. $sc_namefi
done

使用关键字 break 和 continue

找出/etc/profile.d 目录下所有可执行文件并执行

for num in {1..9}
doecho $num
done

实战:处理命令行参数

  • 命令行参数可以使用2..{n}进行读取

  • $0 代表脚本名称

  • 和@代表所有位置参数

  • $#代表位置参数的数量

for pos in $*; doif [ "$pos" = "help" ]; thenecho $posfi
done

shell 之函数

用于重复命令逻辑的集合

  • 函数的定义和执行

  • 函数参数

  • 函数的作用范围

  • 模块化

  • 系统自建函数库

函数的定义和执行

定义

# function 关键词可以省略
function fname(){命令
}

执行

fname

实例

# function 关键词可以省略
function cdls() {cd /varls
}
cdls

函数变量

分为内部定义的变量和外部的传参

内部定义
local 变量名
外部传参

调用

funcName a b

这里有巨坑:

  1. 函数的返回值(return)只支持数字类型,所以我们需要使用 echo 输出流的形式巧妙的模拟返回其他类型数据

function funcName(){echo aaa
}
returnContent=`funcName`
  1. 如果参数中包含了空格,一定要在传递时用引号包裹,不然会被解析成多个参数(比如数组)

arr=(a b c)
funcName "${arr[@]}"
  1. 参数和返回值只支持字符串,所以如果想支持数组,可以采用如下方式:

    1. 入参:将数组的所有内容转为字符串传递,再在内部转为数组

    2. 返回值:将数组的所有内容转为字符串返回,再在外部转为数组

<<'COMMENT'
追加元素
@param arr 数组名
@param item 被追加的元素
@return  改变后的数组字符串
COMMENTfunction push() {# 获取数组名local name=$1# 获取数组内容,创造一个对应的内部数组eval local innerArr=(\${${name}[@]})local item=$2innerArr+=($item)# 先清除的尝试 失败# local length=${#innerArr[@]}# eval unset $name# for((i=0;i<$length;i++)); do#     eval unset $name[$i]# doneecho ${innerArr[@]}
}
arr=(a b c)
arr=(`push arr aaa`) # 这里的数组已经成为(a b c d)
echo arr===${arr[@]}
内部使用
$1 $2 $3...$n

注意点

  1. 如果两位数了,假如是 10,就需要使用${10}

  2. $#:参数个数总数

  3. $*:作为字符串输出所有参数

  4. :脚本运行的当前进程号

funWithParam(){echo "第一个参数为 $1 !"echo "第二个参数为 $2 !"echo "第十个参数为 $10 !"echo "第十个参数为 ${10} !"echo "第十一个参数为 ${11} !"echo "参数总数有 $# 个!"echo "作为一个字符串输出所有参数 $* !"
}
funWithParam 1 2 3 4 5 6 7 8 9 34 73
实例之传参
# function 关键词可以省略
function cdls() {cd $1ls
}
cdls /tmp
实例之内部变量
# function 关键词可以省略
function checkpid() {local ifor i in $*; do# linux 中,每个在运行的进程都会有一个在/proc 目录下对应的子目录,目录名为 pid,这也是 ps[ -d "/proc/$i" ] && return 0done
}
checkpid 5380
echo $?

模块化

其实 shell 的模块化(即函数的导入导出)非常简单,我们知道 shell 会话间的变量是互通的(函数中的 local 变量除外),这时我们只要以当前 shell 会话执行下定义函数的 sh,就能访问到对应的函数了。而如何以当前 shell 执行呢?就是之前讲到的知识:以 source 或者.方式执行 sh 文件。

引入

引入方式很简单

source [函数库的绝对路径]
案例

存在函数库 libs/os.sh

#!/bin/bash
# 获取当前系统类型
# @return  platForm
function getPlatForm() {local innerPlatFormif [[ $(uname) == 'Darwin' ]]; theninnerPlatForm=macfiif [[ $(uname) == 'Linux' ]]; theninnerPlatForm=linuxfiif [[ $(uname) == 'win32' ]]; theninnerPlatForm=windowfi# shell 的 function 只能返回整数值,所以如果想接收字符串类型 echo+$()方式获取echo $innerPlatForm
}

引入使用时

source ./libs/os.sh
# 执行函数
platForm=$(getPlatForm)
# 看输出结果
echo $platForm
注意事项
  1. shell 的 function 中的 return 语句只能返回整数值,所以如果想接收字符串类型 echo+$()方式获取

系统自建函数库

linux 系统中存在自带的函数库,地址如下

/etc/init.d/functions

在配置文件中也是用了大量的循环判断,可以看/etc/profile或者/etc/.bashrc

语言特殊点

一门语言的出现必然存在其特殊点,java 的 jvm 和面向对象、js 的解释型、python 的自动化等等,shell 也不例外

  • 脚本控制:优先级控制、捕获信号

  • 计划任务:定时执行任务

  • 延时计划任务:anacontab

  • 计算任务加锁:flock

脚本控制

cpu 和资源的分配

  • nice 和 renice 调整优先级

  • 死循环的控制:cpu 占用和死机

当执行如下命令时,系统会进入假死状态

.()^C|.&}.

捕获信号

捕获语句语法如下

trap "[行为]" [信号编号]
  • kill 默认会发送 15 号信号给应用程序

  • ctrl + c 发送 2 号信息给应用程序

  • -9 号信号不可阻塞

案例一:监听 15 号信号

循环监听 15 信号,当此信号被下发时,打印sig 15

#!/bin/bash
trap "echo sig 15" 15
echo $$ # 打印端口
while :; do:
done

启动之后会首先打印端口口,然后我们执行如下命令像指定进程发送 15 信号

kill -15 [pid]
案例二:监听 2 号信号

循环监听 15 信号,当此信号被下发时,打印sig 15

#!/bin/bash
trap "echo sig 2" 2
echo $$ # 打印端口
while :; do:
done

启动之后我们ctrl+c实现触发

计划任务

  • 一次性计划任务:at

  • 周期性计划任务:cron

  • 延时计划任务:anacrontab

  • 计划任务加锁 flock

注意事项:

  1. 定时任务是没有终端的,这也就意味这默认没有输出,如果需要查看输出结果就需要用到重定向符号>

一次性计划任务

at [选项] [时间]
  • 录入命令:回车后就会出现at>这种要求输入命令的提示,输入需要在指定时间输入命令

  • 完成记录:输入完成后按下 ctrl + D 即完成了命令的存入

  • 查看记录:可以使用命令atq查看当前一次性任务列表

  • 删除记录:可以使用atrm + [编号]进行删除

注意事项
  1. at 命令是一次性定时计划任务,at 的守护进程 atd 会以后台模式运行,检查作业队列来运行。

  2. 默认情况下,atd 守护进程每 60 秒检查作业队列,有作业时,会检查作业运行时间,如果时间与当前时间匹配,则运行此作业。

  3. at 命令是一次性定时计划任务,执行完一个任务后不再执行此任务了

  4. 在使用 at 命令的时候,一定要保证 atd 进程的启动 , 可以使用相关指令来查看

ps -ef | grep atd //可以检测 atd 是否在运行

画一个示意图

选项

时间定义
支持格式 案例 补充
接受在当天的 hh:mm(小时:分钟)式的时间指定;假如该时间已过去,那么就放在第二天执行 04:00
接受比较模糊的词语来指定时间 midnight(深夜),noon(中午),teatime(饮茶时间,一般是下午 4 点)等
采用 12 小时计时制,即在时间后面加上 AM(上午)或 PM(下午)来说明是上午还是下午 12pm
指定命令执行的具体日期,指定格式为 month day(月 日)或 mm/dd/yy(月/日/年)或 dd.mm.yy(日.月.年) 04:00 2021-05-12 指定的日期必须跟在指定时间的后面
使用相对计时法。指定格式为:now + count time-units now + 5 minutes now 就是当前时间,time-units 是时间单位,这里能够是 minutes(分钟)、hours(小时)、days(天)、weeks(星期)。count 是时间的数量,几天,几小时
直接使用 today(今天)、tomorrow(明天)来指定完成命令的时间。
案例

1 分钟后将在/tmp 目录下创建hello.txt并写入内容hello

at now + 1 minutes
# at>内容框出现后输入如下内容,然后按 ctrl + D
echo hello > /tmp/hello.txt
# 验证
cat /tmp/hello.txt

周期性计划任务:cron

crontab [选项]

crontab 是根据选项执行对应行为的

  • 录入命令:crontab -e ,回车后打开一个 vim 的编辑界面,输入格式为【分钟 小时 日期 月份 星期 执行的命令】,_代表所有,比如小时位是*,代表每小时;比如_ \* \* \* \* /usr/bin/date >> /tmp/deepinout.com.txt

  • 查看计划执行记录:可以执行tail -f /var/log/cron查看定时任务执行记录

  • 查看用户的计划:crontab -l,其实就等同于cat /var/spool/cron/和用户同名的文件,只是做了个文件读取到标准输出上操作而已

  • 删除记录:可以使用crontab -r [编号]进行删除

注意事项
  1. 最小的设置时间单位是分钟(可以用第三方软件包实现秒)

  2. 注意命令的路径问题

每个用户都有自己的周期性计划任务配置文件,保存在/var/spool/cron/下面,以用户名作为文件名。

crontab 示例

每分钟将日期保存在指定文件中

* * * * * /usr/bin/date >> /tmp/deepinout.com.txt
固定星期几执行计划任务

周一每分钟执行

* * * * 1 /usr/bin/date >> /tmp/deepinout.com.txt

周五每分钟执行

* * * * 5 /usr/bin/date >> /tmp/deepinout.com.txt

周一和周五每分钟执行

* * * * 1,5 /usr/bin/date >> /tmp/deepinout.com.txt

周一至周五每分钟执行

* * * * 1-5 /usr/bin/date >> /tmp/deepinout.com.txt
同时满足指定日期和星期才执行任务

7 月 8 日且是周一至周五,每分钟执行

* * 8 7 1-5 /usr/bin/date >> /tmp/deepinout.com.txt

延时计划任务:anacontab

为了缓解 cron 中一瞬间大量任务并发执行而导致系统压力过大的问题。相关文件

  • /etc/cron.d/0hourly:存储着运行任务的信息

  • /etc/anacontab:

# /etc/anacrontab: configuration file for anacron# See anacron(8) and anacrontab(5) for details.SHELL=/bin/sh
PATH=/sbin:/bin:/usr/sbin:/usr/bin
MAILTO=root
# the maximal random delay added to the base delay of the jobs
RANDOM_DELAY=45
# the jobs will be started during the following hours only
START_HOURS_RANGE=3-22#period in days   delay in minutes   job-identifier   command
1       5       cron.daily              nice run-parts /etc/cron.daily
7       25      cron.weekly             nice run-parts /etc/cron.weekly
@monthly 45     cron.monthly            nice run-parts /etc/cron.monthly
环境变量部分
  • RANDOME_DELAY=45:表示 anacron 在执行任务前先延时一段随记的时间再执行,这段随机的时间为 0-45 分钟之内的随机数。

  • START_HOURS_RANGE=3-22:指定了只有在凌晨 3 点到晚上 22 点这个时间段内才允许执行任务。

执行计划部分

由四部分组成:

  • period in days:轮回天数,表示任务多少天执行一次。

  • delay in minutes:表示启动 Anacron 和运行作业时间之间的延迟,单位为分钟. 当然前提是自最后一次运行之后所经过的时间超出了轮回天数。但是它并不是作业真正运行的时间,真正运行的时间还需要加上 RANDOME_DELAY 中设置的随机分钟数。

  • job-identifier:作业的标识符。anacron 在执行任务时会将日期写入 /var/spool/anacron/$job-identifier 文件中

  • command:实际运行的命令。这里的 run-parts 是一个运行指定目录中所有程序与脚本的命令,可以通过 man run-parts 来查看它的说明

特点如下
  • anacron 只运行每天/周/月的任务

  • 如果一项任务在预定的时间机器处于关机状态,那么在下次开机的时候会执行;

  • anacron 自己没有守护进程运行,需要依赖于外部工具(例如 crond);

  • 可以设置一个延时时间,当到指定时间时,等待一个延时时间再执行任务;

计划任务加锁:flock

为什么出现

在使用 crontab 管理定时脚本时,如果设定的脚本执行时间间隔较短,例如 5 分钟执行一次,正常情况下,脚本执行耗时 1 分钟,在非正常情况下(如服务器压力较大的情况下,或数据量突然增大),脚本执行时间超过 5 分钟,这时就会造成多个脚本同时执行,严重时甚至拖垮服务器,影响服务器上的其它服务。

当多个进程可能会执行同一个脚本,这些进程需要保证其它进程没有在操作,以免重复执行,这就是 flock 的作用。

怎么做到的

通常,这样的进程会使用一个锁文件,也就是建立一个文件来告诉别的进程自己在运行,如果检测到那个文件存在则认为有操作同样数据的进程在工作。

如何使用
flock -hUsage:flock [options] <file|directory> <command> [command args]flock [options] <file|directory> -c <command>flock [options] <file descriptor number>Options:
-s, --shared:    获得一个共享锁
-x, --exclusive: 获得一个独占锁
-u, --unlock:    移除一个锁,通常是不需要的,脚本执行完会自动丢弃锁
-n, --nonblock:  如果没有立即获得锁,直接失败而不是等待
-w, --timeout:   如果没有立即获得锁,等待指定时间
-o, --close:     在运行命令前关闭文件的描述符号。用于如果命令产生子进程时会不受锁的管控
-c, --command:   在 shell 中运行一个单独的命令
-h, --help       显示帮助
-V, --version:   显示版本
使用案例
flock -xn "/tmp/f.lock" -c "/root/a.sh"

下图是两个终端同时运行这条命令,后执行者会因为抢不到锁而得不到执行,马上退出

重点概念:管道

当我们单纯的运行多个程序而不需要它们彼此间有互通时,存在如下方式

多命令执行符 格式 作用 案例
; 命令 1;命令 2 多个命令执行,命令之间没有任何逻辑联系 echo 1;echo 2;
&& 命令 1&&命令 2 逻辑与 当命令 1 正确执行,则命令 2 才会执行 当命令 1 执行不正确,则命令 2 不会执行 echo 1&&echo 2;
\ 命令 1\ 命令 2 逻辑或 当命令 1 执行不正确,则命令 2 才会执行 当命令 1 正确执行,则命令 2 不会执行 echo 1\echo 2;
echo 1;echo 2;
echo 1&&echo 2;
echo 1||echo 2;

而如果需要互通,比如第一个命令的返回传递给第二个命令,就需要用到管道了,管道的本质就是将多个程序进行了一个连接,和信号一样,也是进程通信的方式之一

  • 管道与管道符(即匿名管道,|,作用是将前一个命令的结果传递给后面的命令,如ls -l | more

  • 子进程与子 shell

ls /etc/ | more
netstat -an | grep ESTABLISHED | wc -l

注意:因为管道是以子进程方式进行执行的,所以内建命令的执行不会传递给父进程。

重点概念:重定向

重定向的本质就是将文件和输入、输出(包含标准输出、错误输出)进行了一个连接

  • 输入重定向符号:<

  • 输出重定向符号:

    • >:覆盖写入文件

    • >>:追加写入文件

    • 2>:错误输出写入文件

    • &>:正确和错误输出统一写入到文件中

多行内容写入

cat > [文件名] << EOF
xxxx 内容
EOF

具体整理

设备 设备文件名 文件描述符 类型
键盘 /dev/stdin 0 标准输入
显示器 /dev/stdout 1 标准输出
显示器 /dev/stderr 2 标准错误输出
类型 符号 作用
标准输出重定向 命令 > 文件 以覆盖的方式,把命令的正确输出输出到指定的文件或设备当中
标准输出重定向 命令 >> 文件 以追加的方式,把命令的正确输出输出到指定的文件或设备当中
错误输出重定向 命令 2>文件 以覆盖的方式,把命令的错误输出输出到指定的文件或设备当中
错误输出重定向 命令 2>>文件 以追加的方式,把命令的错误输出输出到指定的文件或设备当中
正确输出和错误输出同时保存 命令>文件 2>&1 以覆盖的方式,把正确输出和错误输出都保存到同一个文件当中
正确输出和错误输出同时保存 命令>文件 2>>&1 以追加的方式,把正确输出和错误输出都保存到同一个文件当中
正确输出和错误输出同时保存 命令&>文件(类似 2>&1) 以覆盖的方式,把正确输出和错误输出都保存到同一个文件当中
正确输出和错误输出同时保存 命令&>>文件 以追加的方式,把正确输出和错误输出都保存到同一个文件当中
正确输出和错误输出同时保存 命令>>文件 1 2>文件 2 以覆盖的方式,正确的输出追加到文件 1 中,把错误输出追加到文件 2 中

案例

# 创建 a.txt 并写入 123
echo '123' > a.txt
# 将文件内容输入进变量
read a < a.txt
# 将 a 变量内容追加入 a.txt
echo $a >> a.txt
# 将命令执行错误的提示内容写入 error.log 中
nocmd 2> error.log

shell 之调试

说了这么多,最后还是要落到写的程度来,【纸上得来终觉浅,绝知此事要躬行】,写自然免不了有问题,当有问题的时候我们就需要一些手段去调试我们的代码了。怎么调呢?对我个人而言经历了三个阶段

  • log 时期:变量使用 echo 输出,个人通过 vscode 插件优化至一键生成日志语句

  • 自动输出时期:set命令用来修改 Shell 环境的运行参数,也就是可以定制环境,这里就可以做到自动输出日志

  • 运行时处理时期:也就是我们常说的 debugger 了,没错,我们可以和 debugger js 一样去可视化处理 shell 脚本(cool~);个人通过 vscode 插件优化至一键完成搭建动作【如需试用可以搜索:weiyi-tools】

逐一分享啦

log 时期

这个很简单,唯一要注意的就是 shell 变量类型导致的奇葩输出,在 shell 中默认变量是字符串类型,而其他类型的输出则有不同

  • string:echo $变量名

  • array:echo 变量名,日后直接变量名会只打印第一个字符

  • function:无法打印函数体(个人没找到),echo 变量名会打印函数名,而且函数的引用不能加$

这些就是

万字长文:带你走进shell世界相关推荐

  1. 万字长文带你走进 JavaScript 的世界

    1.1 概述 1.1.1 JavaScript    JavaScript 是一种具有函数优先的轻量级,解释型或即时编译型的高级编程语言.虽然它是作为开发 Web 页面的脚本语言而出名的,但是它也被用 ...

  2. 万字长文带你 搞定 linux BT 宝塔面板 之外网上快速搭建苹果CMS电影网站

    文章目录 万字长文带你搞定宝塔面板 一.本地搭建宝塔面板及安装ecshop 1.1前言 1.2面板特色功能 1.3安装环境说明 1.4安装BT面板 1.5常用管理命令 1.6 BT面板一键安装LAMP ...

  3. 阿里的27个项目资源带你走进Github世界!

    阿里的27个项目资源带你走进Github世界! 1.设计语言 & 前端框架 Ant Design https://github.com/ant-design/ant-design ★ 1488 ...

  4. 带你走进虚拟化世界之kvm(转载)

    http://chuck.blog.51cto.com/10232880/1720953 带你走进虚拟化世界之kvm 2015-12-08 23:10:46 标签:云计算 虚拟化 kvm 原创作品,允 ...

  5. 万字长文带你一览ICLR2020最新Transformers进展(上)

    原文链接:http://gsarti.com/post/iclr2020-transformers/ 作者:Gabriele Sarti 编译:朴素人工智能 Transformer体系结构最初是在At ...

  6. 仓鼠体重年龄对照表_一文带你走进仓鼠世界:仓鼠种类之叙利亚仓鼠(上)

    (宠鼠百科第76期) 今天咱们要谈的是仓鼠中的巨无霸--叙利亚仓鼠,又名金丝熊,黄金鼠.叙利亚仓鼠,很显然祖籍叙利亚,是位来自戈壁荒漠的浪子.叙利亚仓鼠体长12~15厘米,尾长1~1.5厘米,体重10 ...

  7. 微服务入门篇(二),万字长文带你实操作SpringCloudAlibaba微服务组件

    目录 SpringCloudAlibaba介绍 简介 为什么要学SpringCloudAlibaba 从Spring Cloud netflix 到 Spring Cloud Alibaba 功能组件 ...

  8. ❤️大学三年沉淀,把我的学习经验分享给你,爆肝万字带你走进编程世界!❤️

    Hello,大家好,我是Alex.时光匆匆,暑假过的很快,转眼又到了大学的开学季,我也是又混进了我们学院的新生群,发现大家对计算机充满着迷之向往,啊哈哈哈,不过没有人带着入门还是很容易走弯路的哦,今天 ...

  9. FEMS综述: 如何从微生物网络中的“毛线球”理出头绪(3万字长文带你系统学习网络)...

    如何从微生物网络中的"毛线球"理出头绪 From hairballs to hypotheses–biological insights from microbial Lisa R ...

最新文章

  1. 邮件服务器一些A和MX类型恶意域名请求的解释
  2. 工程师进阶之路(二)
  3. 上云迁移-海量数据迁移解决方案
  4. 前端科普系列(2):Node.js 换个角度看世界,
  5. Java 虚拟机学习笔记 | 类加载过程和对象的创建流程
  6. javascript 校验 非空_前端(js+JQuery非空校验)
  7. [MySQL]--查询性能分析工具-explain关键字
  8. 深度学习的最新研究进展(三)
  9. oc的分类category
  10. Blender插件初始化范例
  11. 客户关系管理式的电子商务
  12. 数据库优化-水平拆分 垂直拆分
  13. AD20使用之用封装创建向导创建封装
  14. python 组合优化 回撤最小_【研究】如何用python实现Markowitz投资组合优化
  15. WMS系统--移库逻辑
  16. 功能测试--分享功能测试
  17. 7个最好的文件存储网站
  18. 【开发日志-已归档】2021-06
  19. 基于知识元的外文专利文献知识描述框架
  20. Fabric背书过程中链码是并行还是串行?

热门文章

  1. 2022全国普通高校大学生机器人竞赛指数正式发布-转
  2. cad模型轻量化_CAD环境下一种支持大装配的产品模型轻量化技术
  3. 如何使用excel计算工龄
  4. 计算机老师一句话,给老师的一句话
  5. Go语言之log日志
  6. 调节e18-d80nk的测量距离_堪称建筑工程指南针,测量放线最常用,这2种测量仪器一定要会...
  7. 网付资讯:刷脸支付代理是正规的吗?
  8. centos7没有ens33网卡的解决方案
  9. 解析:微软收购诺基亚硬件或为Asha
  10. 程序员的 59 条搞笑但却真实无比的编程语录