超哥 入门
推荐阅读
https://effective-shell.com
一、基础#
shell 的作用是
- 解释执行用户输入的命令或程序等
- 用户输入一条命令,shell 就解释一条
- 键盘输入命令,Linux 给与响应的方式,称之为交互式
shell 的作用_1
shell 是一块包裹着系统核心的壳,处于操作系统的最外层,与用户直接对话,把用户的输入,解释给操作系统,然后处理操作系统的输出结果,输出到屏幕给与用户看到结果。
从我们登录 Linux,输入账号密码到进入 Linux 交互式界面,所有的操作,都是交给 shell 解释并执行
shell 的作用_2
1.1 什么是 shell 脚本#
当命令或者程序语句写在文件中,我们执行文件,读取其中的代码,这个程序文件就称之为 shell 脚本。
在 shell 脚本里定义多条 Linux 命令以及循环控制语句,然后将这些 Linux 命令一次性执行完毕,执行脚本文件的方式称之为,非交互式方式。
windows 中存在*.bat
批处理脚本
Linux 中常用*.sh
脚本文件
shell 脚本规则
在 Linux 系统中,shell 脚本或者称之为 (bash shell 程序)通常都是 vim 编辑,由 Linux 命令、bash shell 指令、逻辑控制语句 和 注释信息组成。
Shebang#
计算机程序中,shebang 指的是出现在文本文件的第一行前两个字符 #!
在 Unix 系统中,程序会分析 shebang 后面的内容,作为解释器的指令,例如
- 以
#! /bin/sh
开头的文件,程序在执行的时候会调用 /bin/sh ,也就是 bash 解释器 - 以
#!/usr/bin/python
开头的文件,代表指定 python 解释器去执行 - 以
#!/usr/bin/env
解释器名称,是一种在不同平台上都能正确找到解释器的办法
注意事项:
- 如果脚本未指定 shebang ,脚本执行的时候,默认用当前 shell 去解释脚本,即
$SHELL
- 如果 shebang 指定了可执行的解释器,如
/bin/bash
/usr/bin/python
,脚本在执行时,文件名会作为参数传递给解释器 - 如果
#!
指定的解释程序没有可执行权限,则会报错 "bad interpreter: Permission denied" - 如果
#!
指定的解释程序不是一个可执行文件,那么指定的解释程序会被忽略,转而交给当前的 SHELL 去执行这个脚本。 - 如果
#!
指定的解释程序不存在,那么会报错 "bad interpreter: No such file or directory" #!
之后的解释程序,需要写其绝对路径 (如︰#!/bin/bash
),它是不会自动到$PATH
中寻找解释器的- 如果你使用
bash test.sh
这样的命令来执行脚本,那么#!
这一行将会被忽略掉,解释器当然是用命令行中显式指定的 bash
#! /bin/bash
# Date: xxx
# Author: xxx
# Blog: xxx
shell 脚本语言很适合处理纯文本类型数据,且 Linux 的哲学思想就是一切皆文件,如日志、配置文件、文本、网页文件,大多数都是纯文本类型的,因此 shell 可以方便的进行文本处理,好比强大的 Linux 三剑客 (grep、sed、awk)
脚本语言#
shell 脚本语言属于一种 弱类型语言 无需声明变量类型,直接定义使用
强类型语言,必须先定义变量类型,确定是数字、字符串等,之后再赋予同类型的值
# Linux 默认 shell
[root@localhost ~]# echo $SHELL
/bin/bash
# Centos7 支持的shell
[root@localhost ~]# cat /etc/shells
/bin/sh
/bin/bash
/usr/bin/sh
/usr/bin/bash
/bin/tcsh
/bin/csh
默认的 sh 解释器
[root@localhost ~]# ll /usr/bin/sh
lrwxrwxrwx. 1 root root 4 Jul 18 19:54 /usr/bin/sh -> bash
1.2 bash#
历史命令
[root@localhost ~]# echo $HISTSIZE
1000
[root@localhost ~]# echo $HISTFILE
/root/.bash_history
history
-c 清除历史
-r $HISTFILE 恢复历史
[root@localhost ~]# history -c
[root@localhost ~]# history
1 history
[root@localhost ~]# history -r ~/.bash_history
!历史ID # 快速执行历史命令
!! # 执行上次的命令
bash 特性
- 文件路径 tab 键补全
- 命令补全
- 快捷键
ctrl + a, e, u, k, l
- 通配符
- 命令历史
- 命令别名
- 命令行展开
1.3 shell 变量#
变量定义与赋值,注意 == 变量与值之间不得有空格 ==
- 变量类型,bash 默认把所有变量都认为是字符串
- bash 变量是弱类型,无需事先声明类型,是将声明和赋值同时进行
变量替换 / 引用
name="This is test!"
echo ${name}
echi $name # 可以省略花括号
单引号变量,不识别特殊语法
双引号变量,能识别特殊符号
变量名规则
- 名称定义要做到见名知意,切按照规则来,切不得引用保留关键字 (help 检查保留字)
- 只能包含数字、字母、下划线
- 不能以数字开头
- 不能用标点符号
- 变量名严格区分大小写
变量的作用域
- 本地变量,只针对当前的 shell 进程
pstree 检查进程树
环境变量:也称为全局变量,针对当前 shell 以及其任意子进程,环境变量也分自定义、内置
两种环境变量
局部变量:针对在 shell 函数 或是 shell 脚本 中定义
位置参数变量:用于 shell 脚本中传递的参数
特殊变量:shell 内置的特殊功效变量
$?
- 0 : 成功
- 1 - 255 : 错误码
自定义变量
- 变量赋值:varName=value
- 变量引用:
${varName}
,$varName
""
,变量名会替换变量值
单引号变量,不识别特殊语法
双引号变量,能识别特殊符号
1. 每次调用 bash/sh 解释器执行脚本,都会开启一个子 shell,因此不保留当前的 shell 变量,通过 pstree 命 令检查进程树
2. 调用 source 或者点符号,在当前 shell 环境加载脚本,因此保留变量
# 1. 开启子 shell 的执行方式
cat make_vars.sh
name="test"
echo $name
aa
bash make_vars.sh
echo $name
aa
# 2. 不开启子 shell 的执行方式
`Linux命令`
反引号
echo dir=`ls`
echo $dir
xxxx xxx
shell 变量面试题
cat test.sh
user1=`whoami`
sh test.sh
echo $user1
B
A. 当前用户
B. 空
1.4 环境变量设置#
环境变量一般指的是用 export 内置命令导出的变量,用于定义 shell 的运行环境、保证 shell 命令的正确执行
shell 通过环境变量确定登录的用户名、PATH 路径、文件系统等各种应用
环境变量可以在命令行中临时创建,但是用户退出 shell 终端,变量即丢失,如要永久生效,需要修改环境变量配置文件
- 用户个人配置文件
~/.bash_profile
、~/.bashrc
远程登录用户特有文件 - 全局配置文件
/etc/profile
、/etc/bashrc
,且系统建议最好创建在/etc/profile.d/
而非直接修改主文件,修改全局配置文件,影响所有登录系统的用户
检查系统环境变量的命令
- set : 当前 shell 中的所有变量,包括全局变量、局部变量,包含函数
- env : 只显示全局变量
- declare : 输出所有的变量,如同 set
- export : 显示和设置环境变量值
撤销环境变量
- unset 变量名,删除变量或函数
设置只读变量
- readonly ,只有 shell 结束,只读变量失效
系统保留环境变量关键字
bash 内嵌了诸多环境变量,用于定义 bash 的工作环境
export | awk -F '[ :=]' '{print $3}'
bash 多命令执行
ls /data/;cd /tmp/;cd /home;cd /data
环境变量初始化与加载顺序
ssh 登录 linux后,系统启动一个bash shell
/etc/profile : 全局环境变量文件,并从/etc/profile.d 目录的配置文件中搜集shell的设置
读取 /etc/profile.d 目录下的脚本
运行 $HOME/.bash_profile (用户环境变量文件)
运行 $HOME/.bashrc
运行 /etc/bashrc
1.5 特殊变量#
特殊参数变量
shell 的特殊变量,用在如脚本,函数传递参数使用,有如下特殊的,位置参数变量
$0 获取shell脚本文件名,以及脚本路径
$n 获取shell脚本的第n个参数,n 在 1~9 之间,如$1, $2,$9 ,大于9则需要写,${10},参数空格隔开
$# 获取执行的shell脚本后面的参数总个数
$* 获取shell脚本所有参数,不加引号等同于 $@ 作用,加上引号 "$*" 作用是接收所有参数为单个字符串,"$1 $2...."
$@ 不加引号,效果同上,加引号,是接收所有参数为独立字符串,如"$1” “$2” “$3” ..., 空格保留
for_variables.sh
#! /bin/bash
for var in "$*"
do
echo "$var"
done
for var in "$@"
echo "$var"
done
bash for_variables.sh test ha 1 2 3 4
特殊状态变量
$? 上一次命令执行状态返回值,0正确,非0失败
$$ 当前shell脚本的进程号
$! 上一次后台进程的PID
$_ 再次之前执行的命令,最后一个参数
man bash 搜索 Special Parameters
echo
-n 不换行输出
-e 解析字符串中的特殊字符
\n 换行
\r 回车
\t 制表
\b 退格
printf
eval
执行多个命令
eval ls;cd /tmp
exec
不创建子进程,执行后续命令,执行完毕后,自动exit
expr
expr 函数名
1.6 shell 子串#
${变量} 返回变量值
${#变量} 返回变量长度,字符长度
${变量:Offset} 返回变量 Offset 数值之后的字符
${变量:Offset:length} 提取 Offset 之后的 length 限制的字符
# 删除
${变量#word} 从变量开头,删除最短匹配的 word 子串
${变量##word} 从变量开头,删除最长匹配的 word
${变量%word} 从变量结尾删除最短的 word
${变量%%word} 从变量结尾开始删除最长匹配的 word
# 替换
${变量/pattern/string}用 string 代替第一个匹配的 pattern
${变量//pattern/string}用 string 代替所有的 pattern
# #指定字符内容截取
a*c 匹配开头为a,中间任意个字符,结尾为c的字符串 (大小写敏感)
seq -s ":" 10
time for n in {1...10000};do char=`seq -s "testtt" 100`;echo ${#char} &>/dev/null;done
time for n in {1...10000};do char=`seq -s "testtt" 100`;echo ${char}|wc -L &>/dev/null;done
time for n in {1...10000};do char=`seq -s "testtt" 100`;expr length "${char}" &>/dev/null;done
time for n in {1...10000};do char=`seq -s "testtt" 100`;echo ${char}|awk "{print length($0)}" &>/dev/null;done
shell 编程,尽量使用 Linux 内置的命令,内置的操作,和内置的函数,效率最高
尽可能的减少管道符的操作
批量修改文件名
for file_name in `ls *.jpg`;do mv $file_name `echo ${file_name//_finished/}` done;
1.7 shell 扩展变量#
${parameter:-word} 如果parameter变量值为空,返回word字符串
${parameter:=word} 如果parameter变量为空,则word替代变量值,且返回其值
${parameter:?word} 如果parameter变量为空,word当作stderr输出,否则输出变量值。用于设置变量为空导致错误时,返回的错误信息
${parameter:+word} 如果parameter变量为空,什么都不做,否则word返回
stdout 1
stderr 2
应用:数据备份,删除过期数据
find xargs
# 删除 7 天以上过期数据
find path -name "" -type "文件类型" -mtime +7 | xargs rm -f
dir_path="/data/my_data"
find ${dir_path} -name '*.tar.gz' -type f -mtime +7 | xargs rm -f
find ${dir_path:=/data/my_data} -name '*.tar.gz' -type f -mtime +7 | xargs rm -f
1.8 父子 shell#
父子 shell_1
pstree
ps -ef
-e 列出所有进程的信息,如同 -A 选项
-f 显示 UID,PID,PPID
ps -ef --forest
父子 shell_2
创建进程列表 (创建子 shell)
# shell 进程列表
(cd ~; pwd; ls; cd /tmp; ls)
检测是否在子 shell 环境中
# 如果是0,就是在当前shell环境中执行的,否则就是开辟子shell去运行的
echo $BASH_SUBSHELL
(cd ~; echo $BASH_SUBSHELL)
子 shell 嵌套
(pwd; echo $BASH_SUBSHELL)
(pwd; (echo $BASH_SUBSHELL))
利用括号,开启 子 shell 的理念,在 shell 脚本开发中,经常会用 子 shell 进行 多进程 的处理提高程序并发执行效率
1.9 内置命令、外置命令#
内置命令:在系统启动时就加载入内存,常驻内存,执行效率更高,但是占用资源
外置命令:用户需要从硬盘中读取程序文件,再读入内存加载
type cd
type ls
外置命令 特点:一定会开启子进程执行
ps -f --forest
内置命令 是 shell 的一部分,不需要单独去某个文件,系统启动后,便被加载到内存中
# 查看所有内置命令
compgen -b
总结#
echo "username: ${USER}"
echo "UID: $UID"
echo "User home: " $HOME
${} 取出变量值
$() 在括号中执行命令,得到执行结果
`` 同上
() 开启子 shell,执行
$vars 取出变量值
二、shell 开发#
2.1 数学运算#
Shell 算术运算符
算术运算符 | 说明 / 含义 |
---|---|
+、- | 加法(或正号)、减法(或负号) |
*、/、% | 乘法、除法、取余(取模) |
** | 幂运算 |
++、-- | 自增和自减,可以放在变量的前面也可以放在变量的后面 |
!、&&、|| | 逻辑非(取反)、逻辑与(and)、逻辑或(or) |
<、<=、>、>= | 比较符号(小于、小于等于、大于、大于等于) |
==、!=、= | 比较符号(相等、不相等;对于字符串,= 也可以表示相当于) |
<<、>> | 向左移位、向右移位 |
~、|、 &、^ | 按位取反、按位或、按位与、按位异或 |
=、+=、-=、*=、/=、%= | 赋值运算符,例如 a+=1 相当于 a=a+1,a-=1 相当于 a=a-1 |
Shell 中常用的六种数学计算方式
运算操作符 / 运算命令 | 说明 |
---|---|
(( )) | 用于整数运算,效率很高,推荐使用。语法层面 |
let | 用于整数运算,和 (()) 类似。 |
$[] | 用于整数运算,不如 (()) 灵活。 |
expr | 可用于整数运算,也可以处理字符串。比较麻烦,需要注意各种细节,不推荐使用。 |
bc | Linux 下的一个计算器程序,可以处理整数和小数。Shell 本身只支持整数运算,想计算小数就得使用 bc 这个外部的计算器。 |
declare -i | 将变量定义为整数,然后再进行数学运算时就不会被当做字符串了。功能有限,仅支持最基本的数学运算(加减乘除和取余),不支持逻辑运算、自增自减等,所以在实际开发中很少使用。 |
2.1.1 双小括号#
$(()) # $ 符号必须
# .sh 脚本中可不写 echo
a=10
b=2
echo $(($a - $b))
echo $(($a > $b))
2.1.2 运算脚本#
#! /bin/bash
print_usage() {
echo "请输入数字!!!"
exit 1
}
read -p "请输入第一个数字: " firstnum
if [ -n "`echo` $firstnum | sed 's/[0-9]//g'" ]
then
print_usage
fi
if [ "${operator}" != "+" ] && [ "${operator}" != "-" ] && [ "${operator}" != "*" ] && [ "${operator}" != "/" ]
then
echo "只能输入 +-*/"
exit 2
fi
read -p "请输入第二个数字: " secondnum
if [ -n "`echo` $secondnum | sed 's/[0-9]//g'" ]
then
print_usage
fi
echo "${firstnum}${operator}${secondnum} 结果是:$((${firstnum}${operator}${secondnum}))"
2.1.3 检测 nginx 服务是否运行#
while true;
do
wget --timeout=${timeout} --tries=1 http://xxx.com/ -q -O /dev/null
if [ $? -ne 0 ]
then
let fails+=1
else
let success+=1
fi
if [ $success -ge 1 ]
then
echo "正常"
exit 0
fi
if [ ${fails} -ge 2 ];then
echo "error"
exit 2
fi
done
-ne 不等于
-ge 大于等于
vim 定位另一个括号,shift + %
2.1.4 expr#
expr 1 + 2
# 模式匹配
expr aa.png ":" ".*"
expr aa.png ":" ".p"
# 判断 jpg 文件
expr bb.jpg ":" ".*\.jpg" # 6
expr bb.jpghhfff ":" ".*\.jpg" # 也能匹配成功 # 6
判断文件名后缀是否合法
#!/bin/bash
if expr "$1" ":" ".*\.jpg" &> /dev/null
then
echo "This is a jpg!"
else
echo "not a jpg!"
fi
找出长度不大于 2 的单词
#!/bin/bash
for str1 in This is a str, you know?
do
if [ `expr length str1` -le 2 ]
then
echo $str1
done
2.1.5 bc, awk, 中括号计算#
bc 交互式 计算
计算 1~100 的和
echo {1..100} | tr " " "+" | bc
seq -s "+" 100 | bc
echo $((`seq -s "+" 100`)) # 效率最高
seq -s "+" 100 | xargs expr
echo "2.2 1.1" | awk '{print ($1 + $2)}'
num=5
res=$[num+4]
echo $res
res=$[num*4]
echo $res
2.2 shell 条件测试#
条件测试语法 | 说明 |
---|---|
test <测试表达式> | 利用 test 命令进行条件测试表达式的方法。test 命令和 <测试表达式> 之间至少有一个空格 |
[ <测试表达式> ] | 通过[](单中括号) 进行条件测试表达式的方法,和 test 命令的用法相同,这是老男孩推荐的方法。[] 的边界和内容之间至少有一个空格 |
[[ <测试表达式> ]] | 通过[](双中括号) 进行条件测试表达式的方法,是比 test 和 [] 更新的语法格式。[[]] 的边界和内容之间至少有一个空格 |
((<测试表达式>)) | 这是通过 (()) 进行条件测试表达式的方法,一般用于 if 语句里。(()) 两端不需要有空格 |
2.2.1 test 条件测试#
-e 判断该文件是否存在,(普通文件,目录),存在就为真(0),否则为假(1)
echo $?
结合 && 和 || 使用
文件类型
选 项 | 作 用 |
---|---|
-b filename | 判断文件是否存在,并且是否为块设备文件 |
-c filename | 判断文件是否存在,并且是否为字符设备文件 |
-d filename | 判断文件是否存在,并且是否为目录文件(常用) |
-e filename | 判断文件是否存在(常用) |
-f filename | 判断文件是否存在,井且是否为普通文件(常用) |
-L filename | 判断文件是否存在,并且是否为符号链接文件 |
-p filename | 判断文件是否存在,并且是否为管道文件 |
-s filename | 判断文件是否存在,并且是否为非空 |
-S filename | 判断该文件是否存在,并且是否为套接字文件 |
文件权限判断
选 项 | 作 用 |
---|---|
-r filename | 判断文件是否存在,并且是否拥有读权限 |
-w filename | 判断文件是否存在,并且是否拥有写权限 |
-x filename | 判断文件是否存在,并且是否拥有执行权限 |
-u filename | 判断文件是否存在,并且是否拥有 SUID 权限 |
-g filename | 判断文件是否存在,并且是否拥有 SGID 权限 |
-k filename | 判断该文件是否存在,并且是否拥有 SBIT 权限 |
文件比较
选 项 | 作 用 |
---|---|
filename1 -nt filename2 | 判断 filename1 的修改时间是否比 filename2 的新 |
filename -ot filename2 | 判断 filename1 的修改时间是否比 filename2 的旧 |
filename1 -ef filename2 | 判断 filename1 是否和 filename2 的 inode 号一致,可以理解为两个文件是否为同一个文件。这个判断用于判断硬链接是很好的方法 |
-z 字符串为空就为真,否则为假
-n 反过来的,字符串是有内容就为真,否则为假
2.2.2 [ ] 条件测试#
test 和 [] 的作用一样
[ ]
中前后有空格
条件测试中使用变量,必须使用双引号
""
[ -f "${file1}" ]
[[ -f "${file1}" ]]
2.2.3 变量测试#
使用双引号
2.2.4 字符串比较测试#
常用字符串测试操作符 | 说明 |
---|---|
-n "字符串" | 若字符串的长度不为 0,则为真。n 可以理解为 no zero |
-z "字符串" | 若字符串的长度为 0,则为真。z 可以理解为 zero 的缩写 |
"串 1" = "串 2" | 若字符串 1 等于字符串 2,则为真。可使用 "== "代替"=" |
"串 1" != "串 2" | 若字符串 1 不等于字符串 2,则为真。但不能用 !== 代替 "!=" |
变量要有双引号
= 两边要有空格
2.2.5 数值比较测试#
在 [] 和 test 中使用 | 在 (()) 和 [[]] 中使用的比较符号 | 说明 |
---|---|---|
-eq | == 或 = | 相等 (equeal) |
-ne | != | 不等 (not equal) |
-gt | > | 大于 (greater than) |
-lt | < | 小于 (less than) |
-ge | >= | 大于等于 (greater than or equal) |
-le | <= | 大于小于 (less than or equal) |
-a | && | and |
-o | || | or |
! | ! | 非 |
一般使用
[]
[[]]
也支持 -eq 等
2.2.6 逻辑运算#
read -p "pls input a char:" var1
[ "$var1" -eq "1" ] && {
echo $var1
exit 0
}
[ "$var2" = "2" ] && {
echo $var1
exit 0
}
[ "$var1" != "2" -a "$var1" != "1" ] && {
echo "pls input 1 or 2!!"
exit 1
}
# 正则表达式
[[ $num =~ [1-3] ]]
总结#
逻辑与 -a
逻辑或 -o
[[]]
无法使用 >=
和 <=
,只能使用 -ge
或 -le
测试表达式符号 | [] | test | [[]] | (()) |
---|---|---|---|---|
边界为是否需要空格 | 需要 | 需要 | 不需要 | 需要 |
逻辑操作符 | !, -a, -o | !, -a, -o | !, &&, || | !, &&, || |
整数比较操作符 | -eq, -gt, -lt, -ge, -le | -eq, -gt, -lt, -ge, -le 或 =, >, <,>=, <= | -eq, -gt, -lt, -ge, -le 或 =, >, <,>=, <= | =, >, <, >=, <= |
字符串比较操作符 | =, ==, != | =, ==, != | =, ==, != | =, ==, != |
是否支持通配符匹配 | 不支持 | 不支持 | 支持 | 不支持 |
最常用 []
2.3 脚本开发#
开发内存检测脚本#
# 第二行,最后一个值
free -m | awk 'NR==2 {print $NF}'
#! /bin/bash
FreeMem=`free -m | awk 'NR==2 {print $NF}'`
CHARS="cruuent mem is $FreeMem"
if [ "$FreeMem" -lt "100" ]; then
echo $CHARS|tee /tmp/message.txt
# mail -s 标题,收件人 < 内容
mail -s "`date +%F-%T`$CHARS" [email protected] < /tmp/message.txt
fi
mysql 监控脚本#
mysql 监控脚本流程
# 本地
netstat -tunlp | grep mysql | wc -l
ss -tunlp | grep mysql | wc -l
lsof -i tcp:3380 | wc -l
# 远程
nmap 127.0.0.1 -p 3380 | grep open | wc -l
# telnet
echo -e "\n" | telnet 127.0.0.1 3380 2>/dev/null | grep Connected | wc -l
Rsync 启停脚本开发#
2.4 函数#
function xx() {
}
function xx{
}
xx(){
}
美化相关
lsb_functions="/lib/lsb/init-functions"
if test -f $lsb_functions;then
. $lsb_functions
else
init_functions="/etc/init.d/functions"
if test -f $init_functions;then
. $init_functions
fi
log_success_msg() {
echo " SUCCESS! $@"
}
log_failure_msg() {
echo " ERROR! $@"
}
fi
log_success_msg echo "test"
函数返回值#
shell 的函数只能返回 整数值,如果想让函数返回字符串,一般有两种方法:将返回值赋值给一个字符串变量、输出返回值,在函数调用处使用变量赋值。
https://www.cnblogs.com/duanxz/p/4661767.html
1. 将返回值赋值给一个字符串变量
get_project_version(){
VERSION=`grep version $1/pom.xml | head -n 1 | sed -E 's:</?version>::g' | sed -E 's/^\t*//' | sed -E 's/^ *//' | sed -E 's/\r$//'`
}
2. 输出返回值,在函数调用处为变量赋值
get_project_version(){
echo `grep version $1/pom.xml | head -n 1 | sed -E 's:</?version>::g' | sed -E 's/^\t*//' | sed -E 's/^ *//' | sed -E 's/\r$//'`
}
version=`get_project_version $PROJECT_PATH`