王志远,微医前端技术部。爱好吉他、健身、桌游,最最关键,资深大厂员工(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 而言怎么实现上面的需求:删除占用内存最多的两个进程
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
函数库很多,但最重要的是,我们的初始动作只需要做一遍,一劳永逸!!
其实不只是做一个常用命令的封装,我们还可以做到
好了,好处显而易见,我们现在对于 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 号:
sort 对内容根据指定列进行排序
head 只显示前指定行的数据,获取默认前 10 行数据
awk 数据处理神器,这里用于获取第二列(PID,进程 id)
乱入结束,让我们开始叭!
语言大同小异,核心不过【变量、运算、语句、函数、框架】,shell 也是门语言,所以我们也会根据这个脉络进行学习;
但 shell 又有所不同,因为它的出现强依赖【unix】,unix 的哲学:一条命令只做一件事情;为了组合命令和多次执行,最先是分号;用于同行组合,但不利于展示,于是出现了 shell 脚本文件,用来保存需要执行的命令,所以有别于其他语言又会出【环境变量、配置文件等等概念】,也强依赖 unix 的各个命令,这点是需要贯彻整个 shell 学习脉络的。
注意:本文不会过多的牵扯 linux 的知识,awk、xarg、print 等等等,小伙伴放心,我们学习最核心的语言逻辑,至于工具,多用即可,放心食用
命令解释器,用于解释用户对操作系统的操作,命令解释器有很多种,具体可见:(默认的是 bash,其中的 a 是指 again,意思是汇总其他 shell 解释器的优点,重写实现)
cat /etc/shells
BIOS - MBR - BootLoader(grub) - kernel - systemd - 系统初始化 - shell
六步骤
1. BIOS 引导,BIOS 系统是基本的输入输出系统,功能嵌在主板上,用于选择使用哪个介质(硬盘、光盘、网络)
2. MBR(主引导记录):用于确定硬盘是否可以被引导
3. BootLoader(grub):用于启动和引导内核(种类和版本)
4. kernel:
5. systemd:1 号进程
unix 的哲学:一条命令只做一件事情;
为了组合命令和多次执行,于是出现了 shell 脚本文件,用来保存需要执行的命令。
#!/bin/xxx
,默认是#!/bin/bash
;这其实是后缀,因为在 Linux 中,文件后缀是没有意义的,所以操作系统要知道这个脚本文件该用什么应用来执行,就需要这个注释来指明,比如 node 就是#!/usr/bin/node
chmod u+x [filename]
cat >> memo.sh << EOF
#!/bin/bash
cd /var
du -sh *
EOF
# 赋予权限 chmod u+x memo.sh
# 执行 ./memo.sh
存在四种执行方式
执行方式 | 案例 | 是否需要可执行权限 | 是否会创建新进程 | 补充 |
---|---|---|---|---|
[解释器] [文件名] | bash ./filename.sh | 不需要 | 会 | 内部和当前的终端变量不互通 |
[./文件名] | ./filename.sh | 需要 | 会 | 当使用默认解释器时的缩写 |
source [./文件名] | source ./filename.sh | 不需要 | 不会 | 内部和当前的终端变量互通 |
.[文件名] | .filename.sh | 不需要 | 不会 | 其实就是 source 的缩写 |
即变化的量,字母、数字、下划线的组合,且不能以数字开头(shell 变量是弱类型,不区分变量类型);
变量必须以字母或下划线开头,名字中间只能由字母,数字和下划线组成
变量名的长度不得超过 255 个字符
变量名在有效范围内必须唯一
变量默认类型都是字符串(存在类型【字符串、整型、浮点型、日期型】)
$ x=1
$ y=2
$ z=3
$ k=$x+$y+$z
$ echo $k
1+2+3
非交互式赋值存在四种赋值方式
a=${[变量 b]-[默认值]}
,假设默认值是*,a=${b-*}
注意:变量值如果有空格等特殊字符,可以包含在""或''中
在 shell 中存在四种引用:单引号、双引号、${}、``
变量名(不产生歧义,比如字符串拼接就不能缩写);
而查询则可以使用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 [变量名]
的语法糖
默认范围:只针对当前的终端(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 之后要用{}包裹) | 用于获取命令执行参数 | 位置变量 | |
需要注意的是
su - [用户名]
,这时如果有这个-号就是 loginShell,不加则是 noLoginShell)加载顺序为
/etc/profile - ~/.bash_profile - ~/.bashrc - /etc/bashrc
如果修改了配置文件,是不会立即重新加载的,需要我们重启终端或者执行 source 命令
source [修改的配置文件地址]
在操作系统加载过程中,主要按如下顺序进行加载
而作用范围如下图
其实 expr、let 或者(())都是为了向 shell 声明,我目前在做算数赋值的动作,这也就能理解为什么要把整个式子都放在双圆括号中了。
[ 5 -gt 4 ] # 5 是否大于 4
[[ 5 -gt 4 ]] # 5 是否大于 4
echo $? # 测试上条命令执行结果
# 下面两条命令等价
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 $?
\’
字符 | 含义 | 补充 |
---|---|---|
# | 注释 | |
; | 命令分隔符 | case 语句的分隔符要转义 ;; |
: | 空指令 | 返回值永远是真 |
. | 和 source 命令相同 | . bash |
~ | 家目录 | cd ~回到家目录;cd -/+ 回到上/下一个目录; |
, | 分隔目录 | |
* | 通配符 | |
? | 条件测试 或 通配符 | ls ?.sh 查询文件名为单个字或没有字的 sh 文件 |
$ | 取值符号 | |
| | 管道符 | |
& | 后台运行 | |
_ | 空格 | |
# if
if [ "$PS1" ]; then
enif
# case
case $TERM in
xterm*|vte*)
语句 1
;;
screen*)
语句 2
;;
*)
语句 3
;;
esac
参考文档:
根据程序是否正常执行(程序退出的状态)进行判断
tesh 命令可以用于检查文件或者比较值,可用于如下功能(可以用man test
查看更具体的命令)
test 测试语句可以简化为[]符号,而[]符号还有扩展写法[[]] ,支持 &&、||、<、>(推荐使用[[]])
两个字符串是否相等:[str1 = str2] 和 [str1 != str2]
#!/bin/bash
str1='a'
str2='a'
if [ $str1 = $str2 ]; then
echo '等于'
elif [ $str1 != $str2 ]; then
echo '不等于'
fi
读取字符串长度是否是 0(空字符串):-z
#!/bin/bash
str1='a'
str2=''
if [ -z $str1 ]; then
echo 'str1 为空字符串'
else
echo 'str1 不为空字符串'
fi
if [ -z $str2 ]; then
echo 'str2 为空字符串'
else
echo 'str2 不为空字符串'
fi
功能 | []中的符号 | [[]]中的支持 |
---|---|---|
等于 | -eq 和 = 均支持 | = |
大于等于 | -ge | 暂无 |
大于 | -gt | > |
小于等于 | -le | 暂无 |
小于 | -lt | < |
不等于 | -ne | != |
#!/bin/bash
a=1
b=2
# if [ $a = $b ]; then
if [ $a -eq $b ]; then
echo 'a 等于 b'
elif [ $a -ge $b ]; then
echo 'a 大于等于 b'
elif [ $a -gt $b ]; then
echo 'a 大于 b'
elif [ $a -le $b ]; then
echo 'a 小于等于 b'
elif [ $a -lt $b ]; then
echo 'a 小于 b'
elif [ $a -ne $b ]; then
echo 'a 不等于 b'
fi
if [[ $a = $b ]]; then
echo 'a 等于 b'
# elif [[ $a >= $b ]]; then
# echo 'a 大于等于 b'
elif [[ $a > $b ]]; then
echo 'a 大于 b'
# elif [[ $a <= $b ]]; then
# echo 'a 小于等于 b'
elif [[ $a < $b ]]; then
echo 'a 小于 b'
elif [[ $a != $b ]]; then
echo 'a 不等于 b'
fi
功能 | 符号 | 例子 |
---|---|---|
-b | ||
-e | 路径存在,不区分文件和目录 | test -e [路径] 或 [ -e [路径]] |
-d | 目录是否存在 | test -d [路径] 或 [ -d [路径]] |
-f | 文件是否存在 | test -f [路径] 或 [ -f [路径]] |
-x | 文件是否可执行 |
if [ 测试条件成立 || 命令返回值为 0 ]; then
相关行为
fi # 结束
实例
if [ $UID = 0 ]; then
echo " root user "
fi
if [ pwd ]; then
echo " root user "
fi
if [ 测试条件成立 || 命令返回值为 0 ]; then
相关行为
else
相关行为
fi # 结束
实例
#!/bin/bash
if [ $UID = 0 ]; then
echo " root user "
else
echo " $UID user "
fi
if [ 测试条件成立 || 命令返回值为 0 ]; then
相关行为
elif [ 测试条件成立 || 命令返回值为 0 ]; then
相关行为
else
相关行为
fi # 结束
实例
#!/bin/bash
if [ $UID = 0 ]; then
echo " root user "
elif [ $UID = 501 ]; then
echo " wangzy user "
else
echo " $UID user "
fi
#!/bin/bash
if [ 测试条件成立 || 命令返回值为 0 ]; then
if [ 测试条件成立 || 命令返回值为 0 ]; then
相关行为
else
相关行为
fi # 结束
else
相关行为
fi # 结束
实例实现:先判断是不是王志远账户,是则再执行对应文件
#!/bin/bash
if [ $UID = 0 ]; then
echo " root user "
elif [ $UID = 501 ]; then
if [ -x /Users/wzyan/Documents/selfspace/kkb-down/demo/if/index.sh ]; then
$(pwd)/index.sh
else
echo " wangzy user error $(pwd)"
fi
else
if [ $(pwd) = "/Users/wzyan/Documents/selfspace/kkb-down/demo/if" ]; then
echo " wangzy user path"
else
echo " wangzy user error $(pwd)"
fi
fi
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 参数 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`
do
mv $filename $(basename $filename .mp3).mp4
done
for((变量初始化;循环判断条件;变量变化))
do
循环执行命令
done
实例实现:输出 1-10
#!/bin/bash
for((i=1;i<=10;i++))
do
echo $i
done
特点:直到输入为非 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
特点:与 while 相反,直到输入为 0 才中止
until [test 测试为真时中止]
do
命令
done
实例实现:输出 1-10
#!/bin/bash
while []
do
命令
done
找出/etc/profile.d 目录下所有可执行文件
for sc_name in /etc/profile.d/*.sh
do
echo $sc_name
done
找出/etc/profile.d 目录下所有可执行文件并执行
for sc_name in /Users/wzyan/Documents/selfspace/kkb-down/demo/for/test-x/*.sh; do
if [ -x $sc_name ]; then
. $sc_name
fi
done
找出/etc/profile.d 目录下所有可执行文件并执行
for num in {1..9}
do
echo $num
done
for pos in $*; do
if [ "$pos" = "help" ]; then
echo $pos
fi
done
用于重复命令逻辑的集合
定义
# function 关键词可以省略
function fname(){
命令
}
执行
fname
实例
# function 关键词可以省略
function cdls() {
cd /var
ls
}
cdls
分为内部定义的变量和外部的传参
local 变量名
调用
funcName a b
这里有巨坑:
function funcName(){
echo aaa
}
returnContent=`funcName`
arr=(a b c)
funcName "${arr[@]}"
<<'COMMENT'
追加元素
@param arr 数组名
@param item 被追加的元素
@return 改变后的数组字符串
COMMENT
function push() {
# 获取数组名
local name=$1
# 获取数组内容,创造一个对应的内部数组
eval local innerArr=(\${${name}[@]})
local item=$2
innerArr+=($item)
# 先清除的尝试 失败
# local length=${#innerArr[@]}
# eval unset $name
# for((i=0;i<$length;i++)); do
# eval unset $name[$i]
# done
echo ${innerArr[@]}
}
arr=(a b c)
arr=(`push arr aaa`) # 这里的数组已经成为(a b c d)
echo arr===${arr[@]}
$1 $2 $3...$n
注意点
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 $1
ls
}
cdls /tmp
# function 关键词可以省略
function checkpid() {
local i
for i in $*; do
# linux 中,每个在运行的进程都会有一个在/proc 目录下对应的子目录,目录名为 pid,这也是 ps
[ -d "/proc/$i" ] && return 0
done
}
checkpid 5380
echo $?
其实 shell 的模块化(即函数的导入导出)非常简单,我们知道 shell 会话间的变量是互通的(函数中的 local 变量除外),这时我们只要以当前 shell 会话执行下定义函数的 sh,就能访问到对应的函数了。而如何以当前 shell 执行呢?就是之前讲到的知识:以 source 或者.方式执行 sh 文件。
引入方式很简单
source [函数库的绝对路径]
存在函数库 libs/os.sh
#!/bin/bash
# 获取当前系统类型
# @return platForm
function getPlatForm() {
local innerPlatForm
if [[ $(uname) == 'Darwin' ]]; then
innerPlatForm=mac
fi
if [[ $(uname) == 'Linux' ]]; then
innerPlatForm=linux
fi
if [[ $(uname) == 'win32' ]]; then
innerPlatForm=window
fi
# shell 的 function 只能返回整数值,所以如果想接收字符串类型 echo+$()方式获取
echo $innerPlatForm
}
引入使用时
source ./libs/os.sh
# 执行函数
platForm=$(getPlatForm)
# 看输出结果
echo $platForm
linux 系统中存在自带的函数库,地址如下
/etc/init.d/functions
在配置文件中也是用了大量的循环判断,可以看/etc/profile
或者/etc/.bashrc
一门语言的出现必然存在其特殊点,java 的 jvm 和面向对象、js 的解释型、python 的自动化等等,shell 也不例外
cpu 和资源的分配
当执行如下命令时,系统会进入假死状态
.()^C|.&}.
捕获语句语法如下
trap "[行为]" [信号编号]
循环监听 15 信号,当此信号被下发时,打印sig 15
;
#!/bin/bash
trap "echo sig 15" 15
echo $$ # 打印端口
while :; do
:
done
启动之后会首先打印端口口,然后我们执行如下命令像指定进程发送 15 信号
kill -15 [pid]
循环监听 15 信号,当此信号被下发时,打印sig 15
;
#!/bin/bash
trap "echo sig 2" 2
echo $$ # 打印端口
while :; do
:
done
启动之后我们ctrl+c
实现触发
注意事项:
>
at [选项] [时间]
录入命令:回车后就会出现at>
这种要求输入命令的提示,输入需要在指定时间输入命令
完成记录:输入完成后按下 ctrl + D 即完成了命令的存入
查看记录:可以使用命令atq
查看当前一次性任务列表
删除记录:可以使用atrm + [编号]
进行删除
at
的守护进程 atd
会以后台模式运行,检查作业队列来运行。atd
守护进程每 60
秒检查作业队列,有作业时,会检查作业运行时间,如果时间与当前时间匹配,则运行此作业。at
命令是一次性定时计划任务,执行完一个任务后不再执行此任务了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
crontab [选项]
crontab 是根据选项执行对应行为的
_ \* \* \* \* /usr/bin/date >> /tmp/deepinout.com.txt
tail -f /var/log/cron
查看定时任务执行记录crontab -l
,其实就等同于cat /var/spool/cron/和用户同名的文件
,只是做了个文件读取到标准输出上操作而已crontab -r [编号]
进行删除每个用户都有自己的周期性计划任务配置文件,保存在/var/spool/cron/下面,以用户名作为文件名。
每分钟将日期保存在指定文件中
* * * * * /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
为了缓解 cron 中一瞬间大量任务并发执行而导致系统压力过大的问题。相关文件
# /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 点这个时间段内才允许执行任务。
由四部分组成:
/var/spool/anacron/$job-identifier
文件中run-parts
是一个运行指定目录中所有程序与脚本的命令,可以通过 man run-parts
来查看它的说明在使用 crontab 管理定时脚本时,如果设定的脚本执行时间间隔较短,例如 5 分钟执行一次,正常情况下,脚本执行耗时 1 分钟,在非正常情况下(如服务器压力较大的情况下,或数据量突然增大),脚本执行时间超过 5 分钟,这时就会造成多个脚本同时执行,严重时甚至拖垮服务器,影响服务器上的其它服务。
当多个进程可能会执行同一个脚本,这些进程需要保证其它进程没有在操作,以免重复执行,这就是 flock 的作用。
通常,这样的进程会使用一个锁文件,也就是建立一个文件来告诉别的进程自己在运行,如果检测到那个文件存在则认为有操作同样数据的进程在工作。
flock -h
Usage:
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
)ls /etc/ | more
netstat -an | grep ESTABLISHED | wc -l
注意:因为管道是以子进程方式进行执行的,所以内建命令的执行不会传递给父进程。
重定向的本质就是将文件和输入、输出(包含标准输出、错误输出)进行了一个连接
多行内容写入
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
说了这么多,最后还是要落到写的程度来,【纸上得来终觉浅,绝知此事要躬行】,写自然免不了有问题,当有问题的时候我们就需要一些手段去调试我们的代码了。怎么调呢?对我个人而言经历了三个阶段
set
命令用来修改 Shell 环境的运行参数,也就是可以定制环境,这里就可以做到自动输出日志逐一分享啦
这个很简单,唯一要注意的就是 shell 变量类型导致的奇葩输出,在 shell 中默认变量是字符串类型,而其他类型的输出则有不同
这些就是 🐢 的臀部--规定了,踩坑良久,献于诸君。我们来看看我说的那个 vscode 插件支持【一键输出打印语句】,目前支持 js 和 shell。【如需试用可以搜索:weiyi-tools】
set
命令用来修改 Shell 环境的运行参数,也就是可以定制环境。一共有十几个参数可以定制,官方手册有完整清单。
使用方式很简单,在脚本的顶部放上 set 命令+对应配置即可
set -[...options]
具体配置如下
配置 | 不加时情况 | 加上后 | 补充 |
---|---|---|---|
u | 如果遇到不存在的变量,Bash 默认忽略它。 | 遇到不存在的变量就会报错,并停止执行。 | 另一种写法-o nounset ,两者是等价的。 |
x | 屏幕只显示运行结果,没有其他内容。如果多个命令连续执行,它们的运行结果就会连续输出。有时会分不清,某一段内容是什么命令产生的。 | 运行结果之前,先输出执行的那一行命令。 | 还有另一种写法-o xtrace。 |
e | 有运行失败的命令(返回值非 0),Bash 默认会继续执行后面的命令。 | 只要发生错误,就终止执行 | 等价-o errexit;这个一定要注意!!!因为我们的函数入出参是用非 0 实现的,如果加了这个就会中止了 |
o pipefail | -e 有一个例外情况,就是不适用于管道命令。 | 管道符链接的,只要一个子命令失败,整个管道命令就失败,脚本就会终止执行。 |
顺便提一下,如果命令行下不带任何参数,直接运行set
,会显示所有的环境变量和 Shell 函数。
将下面内容放在脚本顶部可以做到
set -euxo pipefail
补充:其实本人还是喜欢下面这样就好啦,因为我会封装很多函数,如果加上了 e 就会导致中止
set -ux
在这,会发现自动化时期其实并没有做到随时随地查看自己想看的运行状态,甚至还会打印很多很多不需要看的内容。有没有一种办法,可以直接图形化的查看当前调试状态下参数的值、卡住程序快照、逐步运行呢?聪明的小伙伴肯定想到了,这不就是 debugger 调试嘛。
js 我们肯定是可以的,shell 呢?其实也可以,只是有几个小要求
这几步的动作原博主写的很赞,我就不赘述了,可见:https://liushiming.cn/article/debug-bash-on-macos.html;唯一有一点补充的就是,记得把 program 替换成${file}
(原文复制下面的也可以),这代表要调试当前打开的 shell 文件。
{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "bashdb",
"request": "launch",
"name": "Bash-Debug (simplest configuration)",
"program": "${file}"
}
]
}
最后,我们来看下效果
直接看效果【如需试用可以搜索:weiyi-tools】
至此,shell 之调试篇,完成。
除此之外的内容
写不动了,到时候另起一文,看下效果吧,对 vscode 插件开发想了解的同学也可以前往我的专栏查看:https://juejin.cn/column/7078626256777904165
救命,内容实在是太多了,而且还有非常非常多踩的坑没有分享【封装shell函数库、vscode智能补全等等等】,开始以为能一文结束,写着写着才发现 shell 的世界浩如烟海,我只是抓住了一个角而已,还好,名字是带你走进这个世界,本文只能作为一个脉络文了,后续会根据本文进行拆分,输出一个专栏,希望同学们有遇到 shell 中有趣的知识点也能够留言和我分享。
老规矩,鸡汤一下
当你埋头苦读的时候,阿拉斯加的鳕鱼正跃出水面,当你伏案写作的时候,南太平洋的海鸥正掠过海岸,当你认真工作的时候,地球的极圈正五彩斑斓,但梦要你亲自实现,那些你觉得看不到的人,和遇不到的风景,都终将在你生命里出现
每到一个新世界,都是一片新的美好风景,luck!
前往微医互联网医院在线诊疗平台,快速问诊,3分钟为你找到三甲医生。(https://wy.guahao.com/?channel=influence)