shell脚本

shell脚本

2021-09-09
linux
  • 用户可以透过应用程序来指挥kernel,让kernel来完成我们所需要的硬件任务,因为程序是在最外层,类似鸡蛋的外壳一样,所以就叫壳程序(shell)。shell是提供用户操作系统的一个接口。

  • bash全称就是Bourne Again SHell,是linux预设的shell。

  • 系统中合法的shell都会写入到/etc/shells这个文件。当用户登录的时候,系统会提供一个shell,这个shell就记录在/etc/passwd这个文件中。

  • bash有很多内建指令,如cd等,可以通过type这个指令观察。

  • 一行输不完,可以通过\来换行。

  • shell脚本中第一行的#!/bin/bash宣告这个script使用的是bash的语法。

  • 当使用bash(sh)或者./xxx.sh来下达指令时,是在子程序中执行的;通过source来执行脚本时,是在父程序中执行的。子程序:在当前shell的情况下,去启动另一个新的shell,新的shell就是子程序。子程序会继承父进程的环境变量,但不会继承父进程的自定义变量。

  • login shell是取得bash时需要输入完整的登录账号密码的就是login shell。

  • non-login shell就是取得bash接口不需要重复的登入。如从图形化界面进入linux后,开启每个bash都不需要再次输入用户名和密码。这就是non-login shell。

  • login shell会读取/etc/profile(系统整体的设定,每个使用者登入取得bash时一定会读取的配置文件。)和~/.bash_profile或~/.bash_login或~/.profile(个人的设定)的文件。

  • non-login shell会读取~/.bashrc、/etc/bashrc、/etc/profile.d(不同的linux会有些不同)。

  • source可以立即读入配置文件的内容。

变量定义 #

  • 环境变量如PATH等,通常使用大写字符来表示,当输入了一个命令ls,系统会通过PATH这个遍历里面的内容记录的路径来顺序搜索指令。
  • 变量在使用时,需要加上在前面加上$。如$PATH,${PATH}
  • 通过envexport可以查询系统中所有的环境变量。

变量 #

  1. 设定变量直接使用等号=,如myname=xiaoxiang,等号两边不能有空格;
  2. 变量内容若有空格可以使用单引号或者双引号括起来,但是双引号内的特殊符号如$会保持原本的特性,而单引号内的特殊字符则是纯文字
  3. 当需要为变量扩增内容内容时,可以使用PATH=$PATH:/home/bin
  4. 通过export JAVA_HOME使得变量JAVA_HOME变成了环境变量。
  5. 通常大写字符为系统默认变量,自行设定的变量可以使用小写字符。
  6. 取消变量可以使用unset myname

read,array,declare #

  • read可以读取键盘输入的变量。如read -p "please input:" -t 10 name 。其中-p是提示信息,-t是倒计时,系统不会一直等待输入,name就是变量,会将输入的信息给这个变量。
  • declare、typeset是宣告变量的类型(shell默认类型是字符串)。declare命令如果用在函数中,声明的变量只在函数内部有效,等同于local命令。
    • declare -a var将var声明为数组类型array。
    • declare -i var将var声明为整数类型integer(bash中的数值计算最多只能达到整数形态)。
    • declare -x var将var声明为环境变量。
    • declare -r var将变量声明为只读类型readonly,改变量不可更改,不能unset。
    • declare -p var查看变量的类型。

数组 #

  • 数组可以采用逐个赋值的方法创建。也可以一次性赋值。
ARRAY[INDEX]=value
ARRAY=(value1 value2 ... valueN)
  • 读取元素
#输出单个元素,${}是必须的
echo ${array[0]}
#输出所有元素
echo ${foo[@]}
#循环遍历所有元素,数据要放在双引号中
for i in "${names[@]}"; do
  echo $i
done

#数组赋值
hobbies=( "${activities[@]}" )
#获取数组长度
echo ${#a[@]}

通配符与特殊符号 #

符号 意义
* 代表0到无穷多个任意字符
代表一定有一个的任意字符
[] 代表一定有一个在括号内的字符
[-] 表示一定有一个在编码顺序内的所有字符,如[0-9]表示一定有一个数字
[^] 表示反向选择,如[^abc]就表示一定有一个字符,但不是abc中的一个。

变量内容的删除与取代 #

符号 含义
# 从最左边开始,删除匹配最短的那个
## 从最左边开始,删除匹配最长的那个
% 从最右边开始,删除匹配最短的那个
%% 从最右边开始,删除匹配最长的那个
/旧字符串/新字符串 若变量内容符合旧字符串,则第一个旧字符串会被新字符串取代
//旧字符串/新字符串 若变量内容符合旧字符串,则全部旧字符串会被新字符串取代
#例:
path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:"
#去掉了开始的/usr/local/sbin:
echo ${path#/*:}
#输出为空
echo ${path##/*:}
#去掉了末尾的/bin:
echo ${path%/*:}
#输出为空
echo ${path%%/*:}
#将第一个出现的usr替换为USR
echo ${path/usr/USR}
#将所有的sbin替换为SBIN
echo ${path//sbin/SBIN}

tr:删除字符串中的一段文字,或者进行文字的替换。 #

  • cat example.txt | tr [a-z] [A-Z]将输出的文件中的所有小写字符变成大写字符。
  • cat /etc/passwd | tr -d ':'删除输出文件中的所有冒号:

变量的测试与内容替换 #

#减号-:当str不存在的时候,str=root,而当str存在(为空也视为存在)则str=$str
str=${username-root}
#:- 当str为空或不存在,str=root;str不为空则str=$str
str=${str:-root}

cut:将一段数据切出来 #

  • cat filename |cut -d '分隔字符' -f num:-d后面跟分隔字符,将数据分为几段,-f表示取出第几段。
  • cat filename |cut -c 12-:-c表示每一行都获取从第12个字符后面的所有字符(注意12后面有个减号)。

命令执行的判断依据 ; && || #

  • 命令之间用;隔开,分号前的指令执行完后会立刻接着执行后面的指令。
  • 命令之间用&&连接,表示当上一个指令正确执行完成后,才会接着执行下一个命令,这里使用到了上面提到的$?变量。如ls /tmp/abc && touch /tmp/abc/hehe,当不存在abc这个目录时,touch命令不会执行。
  • 命令之间用||连接,当上一个指令错误执行完成后,才会接着执行下一个命令。如ls /tmp/abc || mkdir /tmp/abc && touch /tmp/abc/hehe,当abc路径不存在时,会创建abc路径,并执行touch命令;而如果abc路径存在,则会直接执行touch命名。

格式化打印输出 #

  • %ns表示n个字符;%ni表示n个整数数字数;%N.nf表示一共N个数字,小数点占n个。
  • \n表示换行,\t表示tab键

正则 #

  • vi、grep、awk、seq等工具支持正则表达式;cp、ls不支持正则表达式,只能使用bash本身的通配符。
规则 意义
^word 寻找以word开始的行
word$ 寻找以行尾为word的行
. 代表任意一个字符
\ 转义字符
* 重复前一个字符0到无穷次
[abc] 搜寻含有a或b或c的那一行,[]只代表一个待搜寻的字符
[a-z] 搜寻两个字符间的所有连续字符,这个连续与ASCII编码有关。
[^abc] 反向选择,不要有a或者b或者c的行
\{n,m\} \{n,m\} 连续n到m个的前一个字符
\{n\} 连续n个前一个字符
\{n,\} 连续n个以上的前一个字符
[:alnum:] 代表a-z,A-Z,0-9
[:alpha:] 代表A-Z,a-z
[:blank:] 代表空格和[Tab]
[:digit:] 代表数字
[:lower:]、[:upper:] 代表小写字符、代表大写字符
[:space:] 任何会产生空白的字符,包括空格,[Tab],CR等
[:xdigit:] 代表16进位的数字类型。

test指令 #

文件的判断 #

命令 解释
test -e filename 判断文件是否存在
-f 是否为文件
-d 是否为目录
-r 是否有可读的权限
-w 是否有可写的权限
-x 是否有可执行的权限
-nt(newer than) 判断file1是否比file2新,例:test file1 -nt file2
-ot(older than) 判断file1是否比file2旧
-ef 判断两个文件是否是同一个文件,其实是判断两个文件是否指向同一个inode。

两个整数之间的判断 如:test n1 -eq n2 #

命令 介绍
-eq 两数值相等
-ne 两数值不等
-gt(greater than)/ -ge(greater than or equal) n1大于 / 大于等于n2
-lt / -le n1小于 / 小于等于n2

判断字符串的数据 #

命令 介绍
test -z string 若字符串为空串,则为true
test -n string 若字符串为非空串,则为true
test str1 == str2(bash中一个等号和两个等号是一样的) 若str1等于str2,则返回true
test str1 != str2 若str1不等于str2,则返回true

条件判断,如test -r filename -a -x filename #

命令 介绍
-a(and) 与,两条件同时成立返回true
-o(or) 或,任何一个条件成立就返回true
! 非,如test ! -x file,当file不具备执行条件时返回true

shell中的默认变量 #

  • shell定义了一些默认的变量。
tar -c -v -f example.tar example/
$0  $1 $2 $3    $4       $5
  • $#表示命令后面的参数个数,上面就是5。
  • $@表示"$1" “$2” “$3” “$4” “$5”。
  • $* 表示"$1 $2 $3 $4 $5"
  • $$ 当前shell的PID。
  • $? 上一个执行指令的回传值,一般一个指令执行完后,会回传一个0值。
  • $本身也是一个变量,代表着当前shell的PID,通过$$使用该变量。
  • ?是一个变量,代表上一个指令传回来的值,一般来说,当我们成功执行了一个指令,则会返回0值,如果执行错误,就会传回一个错误代码,通过$?来调用。

算数运算 #

  • ((...))语法可以进行整数的算术运算,((...))会自动忽略内部的空格。
((foo = 5 + 5))
echo $foo
  • 如果要结果,需要在((...))前面加上美元符号$((...))
total_mem=$(( 1 + 2 ))
#可以使用括号来改变运算顺序
echo $(( (2 + 3) * 4 ))
  • ((...))支持+ - * / %(取余) **(指数) ++ --++--这两个运算符有前缀和后缀的区别。作为前缀是先运算后返回值,作为后缀是先返回值后运算。

expr #

  • expr命令支持算术运算,可以不使用((...))语法,只支持整数。
foo=3
expr $foo + 2

条件判断式 #

if #

  • 中括号[ ]和test类似,也可以来进行数据的判断,中括号的两端需要有空格来分割。
if [ 条件判断式1 ];then
	指令
	指令
	exit 0
elif [ 条件判断式2 ];then
	指令
	指令
else
	指令
	指令
fi

#可以if后面可以跟上多个条件
if [ 条件判断式1 ] || [ 条件判断式2 ];then
if [ 条件判断式1 ] && [ 条件判断式2 ];then

case #

case $变量名称 in
	"第一个变量名称")
		指令
		指令
		;;
	"第二个变量名称")
		指令
		指令
		;;
	#下面这个有点类似于C语言的default
	*)
		指令
		指令
		;;
esac
#例:
case ${1} in 
    "--host"|"--ip")
        init_dir
        build_image
        modify_ip_address ${2}
        echo -e "文件已安装到\e[36m${base_dir}\e[0m下, 稍后请进入该目录下执行后续操作"
        ;;
    "build_image")
        build_image
        ;;
    *)
        display_help
        ;;
esac

函数function #

  • shell是从上往下执行,所以函数要放在程序的最前面。
#function关键字是可选的
#可以通过test abc来给函数传递参数
function test(){
	指令
	指令
}
  • return命令用于从函数返回一个值。函数执行到这条命令,就不再往下执行了,直接返回了。return后面可以不跟参数。

循环 #

不定循环 #

#当condition条件不成立时终止循环
while [ condition ]
do
	指令
	指令
done

#当condition条件成立时终止循环
until [ conditon ]
do
	指令
	指令
done

固定循环 #

for var in con1 con2 con3
do
	指令
	指令
done

#或

for (( 初始值; 限制值;执行步阶))
do
	指令
	指令
done
#一个案例
for (( i=1; i<=10; i=i+1)); do
	printf "%i\n" $i
done
#一个ping很多机器的shell脚本
#!bin/bash
network=82.156.3
#seq是sequence,会连续生成1到254之间的数
for site in $(seq 1 254)
do
		#-c表示ping几次,-w表示超时时间,单位是秒
        ping -c 1 -w 1 ${network}.${site} > /dev/null 2>&1
        if [ $? -eq 0 ];then
                echo "${network}.${site} is ok"
        else
                echo "${network}.${site} id down"
        fi
done
#计算多个容器使用内存之和

#!/bin/bash
total_mem=0
#获取要计算内存的容器名称
for item in $(docker ps | grep kuam | awk '{print $NF}'); do
    #获取容器的pid
    for pid in $(docker top ${item}  | grep -v PID | awk '{print $2}'); do
        #获取占用的内存
        mem=$(cat /proc/${pid}/status | grep -i vmrss | awk '{print $2}')
        echo "${item} PID:${pid} MEM:${mem}"
        #内存求和
        total_mem=$(( ${total_mem} + ${mem} ))
    done
done
echo "total mem: ${total_mem}"

shell调试 #

  • shell [-nvx] xxx.sh:-n查询语法是否正确;-v先输出文件内容再执行shell脚本;-x执行前先将使用到的script输出到屏幕上。

#

#!/bin/bash

#上传的文件路径
file_path="/tmp/blog.tar"
#文件放的位置
blog_dir="/opt/web/front/xiaoxiang.space"

#检查上传的文件是否存在
if [ !  -e "${file_path}" ];then
    echo "文件不存在"
    exit 0
fi

#检查当前是否有scp进程,没有就说明文件已经上传完成
scp_status=$(ps aux | grep scp | grep -v grep)
while [ -n "${scp_status}" ]
do
    sleep 1s
    scp_status=$(ps aux | grep scp | grep -v grep)
done
echo "传输完成..."

#将上传的文件解压到对应位置
mkdir -p ${blog_dir}
rm -rf "${blog_dir:?}/*"
if tar -xvf ${file_path} -C ${blog_dir};then
    echo "部署成功,文件位置为${blog_dir}"
fi

#删掉上传的文件
rm -f ${file_path}