一、shell的基本知识
1、简介
shell编程可以简化日常的系统维护工作。shell特点是大部分都是linux命令,而且是面向过程的语言,不需要编译即可执行。
shell:实质上是一个命令解释器,它能够识别用户输出的各种命令,并传递给操作系统,他的作用类似于windows中的命令行。在UNIX或者localhost中,shell即是用户交互的界面,也是控制系统的脚本语言。
shell的分类:
Bourne Shell:标识为sh,该shell是root用户默认的shell。
Bourne-Again Shell:标识为bash,大多数的localhost发行版的默认是Shell。
Korn Shell:标识为ksh。
C Shell:标识为csh。
cat /etc/shells #查看当前系统支持的shell

echo $SHELL #查看当前所支持的shell

2、shell脚本语言的书写规范
第一行写声明,声明当前shell脚本是用哪个命令解释器去解释:#!/bin/bash
除了第一行其他以#开头的都为注释
vim /root/.vimrc #自动生成脚本注释
bash
autocmd BufNewFile *.py,*.cc,*.sh,*.java exec ":call SetTitle()"
func SetTitle()
if expand("%:e") == 'sh'
call setline(1,"#!/bin/bash")
call setline(2,"##############################")
call setline(3,"#fire name:".expand("%"))
call setline(4,"#version:v1.0")
call setline(5,"#email:[email protected]")
call setline(6,"#create time".strftime("%F %T"))
call setline(7,"#description:")
call setline(8,"##############################")
call setline(9,"")
endif
endfunc
当新建.sh文件时会自动添加:
3、shell脚本的执行方式:
(1)交互式执行:
for filename in 'ls /etc'
>...
(2)作为程序文件执行(常用)
前两个会生成子进程,后两个不会。
bash ./filename.sh #生成子进程,并不在当前进程,所以只显示执行脚本的结果,不改变当前进程的状态。使用当前的bash shell去执行
./firename.sh #生成子进程,使用脚本里面指定的shell去运行,不过需要x权限
source ./firename.sh #不生成子进程,直接在当前进程中执行命令,执行结束后可能会改变当前进程。
. ./filename.sh #与上面source ./firename.sh作用相同
验证:
echo 'userdir=`pwd`' > demo03.sh #新建shell脚本,其内容是声明一个变量,存入pw的结果。
①bash demo03.sh
echo $userdir #输出变量的值,结果为空
②source demo03.sh
echo $userdir #输出变量的值,结果非空
shell脚本的退出状态码:0为成功,非0表示不成功 (退出状态码范围为0~255)
exit 0 #等同于c语言中的return 0
二、shell的变量
1、作用以及应用范例
变量的作用就是简化代码。
如:想要增加一个用户,那么就可以新增一个变量name,每次添加一个用户只需要修改变量名,不需要重新写一遍代码。
vim demo03.sh
name=test01
useradd $name
echo 123456 | passwd --stdin $name #非交互式设置用户密码
如果想要一次性添加多个用户,就可以使用for循环进行批量新建。
2、查看变量的值
①查看单个变量的值
echo $PATH
echo ${PATH}
printf "$PATH\n" #与echo的区别是不会换行
printf "${PATH}\n"
②查看多个变量的值
set #查看所有变量和函数
declare #查看所有变量和函数
env #显示所有的全局变量
set | less #翻页查看,可以使用vim命令
#在命令行中定义变量:退出当前进程变量就会失效,其他终端上无法使用变量
var = 'bianliang'
var = "`cmd`"
var = "$(cmd)"
在文件中定义变量:永久生效(定义后要重启终端就可以正常使用变量了)
当前用户生效的文件:~/.basnrc和~/.bash_profile
所有用户生效的文件:/etc/bashrc或/etc/profile或/etc/profile.d/*.sh(这个要加x权限)
③当我们执行shell脚本时,直接使用文件名运行会找不到命令,但是当我们将这个shell脚本文件放在/usr/bin目录下就可以直接在命令行中打出文件名就可以运行脚本了。
vim demo01.sh #创建脚本文件
#!/bin/bash
echo hello world #打印hello world
但是直接输入demo01.sh就不行
cp /shell/test02/demo01.sh /usr/bin #复制文件到只当文件夹中,或者建立软连接
(查看存放命令文件的目录echo $PATH)
ln -s /shell/test02/demo01.sh /usr/bin/demo01.sh #建立软连接,注意软连接的名称可有随便起。
④$:会将后面的内容作为一个变量,并且会引用变量的值。
转义字符\:会将后面跟着的特殊字符变为普通字符。
反单引号` `:在反单引号里面的内容当成是一段命令去执行。
单引号' ':会将处于单引号中的所有内容的特殊字符失效。
双引号" ":会将处于双引号中的内容的特殊字符失效,除了$,反单引号,\在双引号里面要保留自己的特殊含义。
注意:引号是就近匹配原则,如:' ' `ls` ' ' 这个命令中的两对单引号没有任何作用。
3、变量的分类
①局部变量:只能在当前进程使用的变量,其子进程不能使用
②全局变量(环境变量):可以创建他们的shell及其派生出来的子进程中使用(su切换用户是就会读取新的环境变量)
自定义环境变量:
export var或export var ="value"
declare -x var="value" #修改为全局变量
declare +x var #修改为局部变量
③特殊变量:
变量 | 说明 |
---|---|
$# | 命令后面跟的参数个数 |
$0 | 当前脚本的名称 |
$n | 第n个参数 |
$* | 以"参数一 参数二 参数三"的格式,返回所有的参数 |
$@ | 以"参数一""参数二""参数三"的格式,返回所有的参数 |
$? | 前一个命令、函数、脚本返回的状态码 |
$$ | 返回进程即PID |
④取消变量:unset var_name
4、变量的运算
设置变量:num1=123,num2=456,str1='i love you'。
(1)数值运算(与其它的语言相同)
①★(()) #用于整数运算的常用运算符,在(())中使用变量是可以前面的符号
例如:echo $((num1+num2)) #其结果为579
②let #用于整数运算时,使用let命令可以执行一个或者多个算术表达式,其中的变量名不需要使用$符号。
例如:let num3=num1+num2 #计算出的结果会存放num3中
echo $num3 #展示结果为579
③expr #用于整数运算,运算符的前后都要加上空格
例如:expr num1+num2 #会直接打印后面的式子,因为expr将其视作是字符串
expr num1 + num2 #会输出正确的结果,579
expr num1 + str1 #会有非整数参数的提示,原因是str1是字符类型的数据,不能跟整数相加减,并且返回状态码非零。但并不会报错。
#用expr统计字符串中字符的个数:expr length "$str1" 输出结果为10
④bc linux下的一个计算机程序,适合整数以及小数运算
例如:seq -s '+' 1 9 | bc #管道符前面的语句是胡输出一个从一加到九的式子,通过管道符传给计算器bc,结果会被输出45。
⑤\[\] #与(())的用法是一样的。
⑥awk #后面会专门讲到
例如:awk 'BEGIN {print 2+3*4}' #结果为10
echo ''6.222 3.111'' | awk '{print (1-2)}' #结果为3.111
⑦declare
例如:declare -i r=2+3 #将后面表达式的计算结果放到r中
echo $r #输出r的值
实例:输入两个实数,返回加减乘除除于等结果:
bash
read -p "please input two number:" a b
echo "##############"
echo "a+b:$[a+b]"
echo "a-b:$[a-b]"
echo "a*b:$[a*b]"
echo "a/b:$[a/b]"
echo "a%b:$[a%b]"
echo "##############"
结果为:

(2)字符串运算
表达式 | 说明 |
---|---|
${filename} | 返回变量的内容 |
${#filename} | 返回变量的长度 |
${filename:num1} | 从位置num1开始提取字串 |
${filename:num1:num2} | 从位置num1到num2提取子串 |
${filename#word} | 从头开始匹配最短子串 |
${filename##word} | 从头开始匹配最长子串 |
{filename%world}和{filename%%word} | 从尾开始匹配最长/最短子串 |
${filename/str1/str2} | 将子串中的第一个str1修改为str2 |
${filename//str1/str2} | 将子串中的全部的str1修改为str2 |
filename=test.tar.gz #定义字符串变量
echo ${filename%%.*} #提取文件的名字
echo ${firename#*.} #提取文件的扩展名
实例1:写一个脚本,计算1到n的相加之和,n值由用户输入:
bash
#!/bin/bash
read -p "please input a number:" n
sum=`seq -s "+" $n | bc`
echo $sum
实例2:写一个脚本,输入一个文件的绝对路径,输出文件的目录,名字,还有扩展名
bash
#!/bin/bash
read -p "please input a string:" str
str2=${str##*/}
echo "output name:${str2%%.*}"
echo "output extension:${str#*.}"
echo "output content:${str%/*}"
实验3:写一个脚本,输入一个数字,返回数的位数
bash
#!/bin/bash
read -p "please input a number:" num
echo `expr length $num`
实验4:写一个脚本,输入一个文件夹的绝对地址,输出文件夹中的文件个数
bash
#!/bin/bash
read -p "please input a content:" content
sum=`ls $content | wc -l`
echo $sum
三、条件测试
1、条件语句的基本用法
条件测试语法 | 说明 |
---|---|
test <测试表达式> | test语句和测试语句中间至少有一个空格 |
[ <测试表达式> ] | 该方法与test命令一样,[]边界与测试表达式之间至少有一个空格 |
[[ <测试表达式> ]] | 与上面的语句的区别是在[[]]中可以使用通配符等进行模式匹配 |
((<测试表达式>)) | 一般用于if语句中,(())两端不需要括号,测试对象必须是整数 |
2、文件的测试表达式
常用的文件测试操作符:
-a/-e 文件 #文件是否存在
-b 文件 #文件是否存在,且为块文件,是返回0
-c 文件 #文件是否存在,且为字符文件,是返回0
-L 文件 #文件存在且为来链接文件则为真
-d 文件 #文件存在且为目录则为真
-f 文件 #文件存在且为普通文件则为真
-s 文件 #文件存在且大小不为零则为真
-u 文件 #文件是否设置suid位,如果设置了suid,则结果为0
-r 文件 #文件存在且可读则为真
-w 文件 #文件存在且可写则为真
-x 文件 #文件存在且可执行则为真
f1 -nt f2,nt为newer than # 文件f1比f2新则为真,根据文件的最新修改时间为准
f1 -ot f2,ot为older than #文件f1比f2旧则为真,根据文件的修改时间计算
测试:
touch file #新建一个普通文件
#前面语句与后面语句没有输入输出关系的时候,不能用管道符,只需要用分号;隔开即可。
test -f file; echo $? #判断file文件是否为普通文件
filename=/etc/nginx #定义临时变量
-d $filename \];echo $? #判断filename变量中存储的是否为目录 注意:如果测试的文件路径使用变量来代替的,那么一定要加上双引号。(最好所有的变量都加上双引号,最起码不出错!) ### 3、字符串测试表达式 | 常用的字符串测试操作符 | 说明 | |----------------|-----------------------------| | -n "字符串" | 若字符串的长度不为0则为真,n可以理解为no zero | | -z "字符串" | 若字符串的长度为0则为真 | | "字符串1"="字符串2" | 若两个字符串相同则为真,等号左右要有空格 | | "字符串1"!="字符串2" | 若两个字符串不同则为真 | 注意:参数是变量的时候,一定要给变量加双引号。 ### 4、整数测试表达式 | 在\[\]以及test中使用的比较符号 | 在\[\[\]\]和(())中使用的比较符号 | 说明 | |---------------------|------------------------|----------------------| | -eq | ==或= | 相等,全拼为equal | | -ne | != | 不相等,全拼为no equal | | -gt | \> | 大于,全拼greater than | | -ge | \>= | 大于等于,全拼greater equal | | -lt | \< | 小于,全拼less than | | -le | \<= | 小于等于,全拼less equal | ### 5、逻辑运算符 | 在\[\]以及test中使用的比较符号 | 在\[\[\]\]和(())中使用的比较符号 | 说明 | |---------------------|------------------------|------------------| | -a | \&\& | and,与,两端都为真则为真 | | -o | \|\| | or,或,一个为真,则结果为真 | | ! | ! | not,非,两端相反,则结果为真 | ## 四、流控制之条件判断 ### 1、if单分支结构 > 第一种语法: > > if \<条件表达式\> > > then > > 指令 > > fi > > 第二种语法: > > if \<条件表达式\>;then > > 指令 > > fi 测试1: > free -m \| grep Mem \| tr -s " " \| cut -d " " -f 4 #获取mem的空闲内存,单位为Mb > > 说明:第一个命令是查看系统中的内存情况,并且以Mb为单位 > > 第二个命令是管道符过滤出Mem的信息 > > 第三个命令是将过滤出的信息进行加工,字符串之间的间隙设置为一个空格 > > 第四个命令是获取第四个字符串 > > (或使用awk命令:free -m \| awk '/Mem:/ {print $4}')awk会将多个空格默认成一个空格,并且各个字符串就是以空格分开的。 举一反三: > echo \`ls -l \| awk '/-/ {print $5}'\` #输出以-为开头的所有行的第五列 > >  > > crontab -e #循环执行例行性检查 > > \*/10 \* \* \* \* /test/free_mem.sh \&\> /dev/null #每十分钟检查一次内存是否小于100MB 判断当前的执行者是谁: whoami echo $USER #会返回名字 id -u echo $UID #会返回数字,0代表是root用户,不是零为普通用户。 测试2: > vim demo02.sh #新建脚本文件 > > ```bash > #!/bin/bash > if [ "$UID" != 0 ] > then echo "please switch user root" > fi > ``` > > chmod a+rx demo02.sh #加权限 > > ./demo02.sh #测试结果 > >  ### 2、if的双分支结构 > 语法格式: > > if \<条件表达式\> > > then > > 语句1 > > else > > 语句2 > > fi 测试1: 判断进程是否运行: 方法一:查看进程 ps -ef \|grep sshd \|grep -v grep #过滤没有sshd的进程,再过滤没有grep的进程 ps -ef \|grep sshd \|grep -v grep \|wc -l #统计符合条件的进程的数量 方法二:查看端口 ss -lntup \|grep -w 22 \| wc -l netstat -lntup \|grep -w 22 \|wc -l #统计使用22端口的进程个数 > #写一个脚本,统计某个程序的进程个数,输入一个程序的名字,输出该程序的进程个数 > > ```bash > #!/bin/bash > read -p "please input a program:" program > sum=`ps -ef |grep $program |grep -v \grep |wc -l ` > if [ sum = 0 ] > then echo "no process" > else > echo "have $sum process" > fi > ``` > > 测试: > >  测试2: 检测主机存活可以使用ping 命令进行检测 ping -c 2 -w 1 192.168.68.99 \&\> /dev/null #测试网络可达性,并且将显示的结果去除 > #写一个脚本测试主机是否存活 > > ```bash > #!/bin/bash > ping -c 2 192.168.68.2 > /etc/null > if [ "$?" -eq 0 ] > then echo host is runing > else > echo host is not runing > fi > ``` ### 3、if多分支结构 > 语法格式: > > if 条件表达式 > > then > > 命令序列 > > elif 条件表达式 > > then > > 命令序列 > > elif 条件表达式 > > then > > 命令序列 > > else > > 命令序列 > > fi 测试1: > #比较两个数的大小 > > ```bash > #!/bin/bash > read -p "please input two number:" a b > if [ $a -eq $b ] > then > echo "$a equal $b" > elif [ $a -gt $b ] > then > echo "$a greater than $b" > else > echo "$a less than $b" > fi > ``` > >  测试2: > #判断字符的种类 > > ```bash > #!/bin/bash > read -p "please input a char:" char > if echo "$char"|grep "[a-zA-Z]" > /dev/null > then echo "this is a letter" > elif echo "$char"|grep "[0-9]" > /dev/null > then echo "this is a number" > else > echo "this is other" > fi > ``` > >  测试3: cat /proc/cpuinfo \|grep vendor_id \|head -1 \|tr -s " " \|cut -d " " -f 2 cat /proc/cpuinfo \|awk '/vendor_id/ {print $3}' \|head -1 #切出cpu厂商的名字 > #写一个脚本,判断cpu的厂商信息 > > ```bash > #!/bin/bash > vendor=`cat /proc/cpuinfo |awk '/vendor_id/ {print $3}' |head -1` > if [ "$vendor" = GenuineIntel ] > then echo "intel" > elif [ "$vendor" = AuthenticAMD ] > then echo "AMD" > else > echo "unknown" > fi > ``` > >  4、多条件判断语句case > 语法格式: > > case 变量名 in > > 值1) > > 指令1 > > ;; > > 值2) > > 指令2 > > ;; > > 值3) > > 指令3 > > ;; > > \*) > > 默认 > > esac 测试1: > #判断字符串的类型 > > ```bash > #!/bin/bash > read -p "please input a char:" char > case "$char" in > [a-zA-Z]) > echo "letter" > ;; > [0-9]) > echo "number" > ;; > *) > echo "other" > esac > ``` > >  ## 五、流控制之循环 ### 1、带列表的for循环 > 语法格式: > > for variable in list > > do > > statement1 > > statrment2 > > ... > > done 测试1: #循环打印IP地址: seq -f 192.168.68.1%02g 0 50 #输出100到150所有的IP地址 %02g 解析:填充两位内容其内容是0\~50,位置不够用0进行填充 ```bash #!/bin/bash for IP in $(seq -f "192.168.68.1%02g" 0 50) do echo $IP done ``` 测试2: #循环测试主机号为0\~20网络是否可达,使用ping 命令 ```bash #!/bin/bash for IP in 192.168.68.{0..9} $(seq -f "192.168.68.%g" 10 20) do ping -c 1 $IP > /etc/null if [ "$?" -eq 0 ] then echo "$IP" is runing else echo "$IP" is not runing fi done ``` 测试3: #列出当前文件夹中所有的普通文件 ls -F #列出所有文件的类型 grep -v /$ #过滤掉斜线结尾的,即过滤掉文件夹 而这种不够严谨,除了文件和文件夹类型,还有很多的文件类型,如软连接等 ```bash #!/bin/bash for FILE in $(ls -F | grep -v /$) do echo $FILE done ``` find ./ -type f -depth 1 #找到当前目录下文件类型为f即普通文件的所有文件,且执行深度为1,即不用找该文件夹下面所有文件夹中的文件。 ```bash #!/bin/bash for FILE in $(find ./ -maxdepth 1 -type f) do echo $FILE done ``` 测试4: #给定一句话,找出所有字符数小于6的字符串 ```bash #!/bin/bash for str in rabbit is favorite to eat cabbage do if [ `expr length $str` -lt 6 ] then echo $str fi done ``` 测试5: 计算一百以内的奇数的个数 可以使用num%2==1来挑选,也可以用花括号的用法来选for num in {start..end..step},将step设置成2,那么就可以挑选出奇数,再通过for循环进行加合运算。 让一个变量去接收两个变量相加后的值一定要使用let命令 ```bash #!/bin/bash sum=0 for num in {1..100..2} do let "sum+=num" #或sum=$[sum+num] done echo "$sum" ``` ### 2、不带列表的for语句 > 语法格式: > > for variable #等同于for variable in $@或$\*(即执行脚本的时候要给定参数) > > do > > statement1 > > statrment2 > > ... > > done 测试1: ```bash #!/bin/bash for i do echo $i done ```  ### 3、类C风格的for循环 > 语法格式: > > for((expression1;expression2;expression3)) > > do > > statement1; > > statement2; > > done 测试1: #批量添加用户 then useradd test0"$i" 2\> /dev/null #这段命令中的2\>的作用就是如果报错,将报错信息丢弃 echo 123456 \| passwd --stdin test"$i" #可以自定添加密码 ```bash #!/bin/bash for((i=1;i<=30;i++)) do if [ $i -lt 10 ] then if id -u test0"$i" &> /dev/null then echo test0"$i" is exist! else useradd test0"$i" 2> /dev/null echo 123456 | passwd --stdin test0"$i" > /dev/null fi else if id -u test"$i" &> /dev/null then echo test"$i" is exist! else useradd test"$i" 2> /dev/null echo 123456 | passwd --stdin test"$i" > /dev/null fi fi done ``` 测试2: #测试192.168.68.0/24这个网段的主机有多少在线 ```bash #!/bin/bash sum=0 for IP in 192.168.68.{0..255} do if ping -c 1 $IP &>/dev/null then let sum+=1 echo "$IP is up" else echo "$IP id down" fi done echo $sum ``` *** ** * ** *** ## 实验过程中问题与解决方法: 虚拟机开启 ifconfig 没有ens160网卡,无法上网,同时 图形化模式 没有有线连接选项 手动启动网卡提示: ```bash Connection 'ens160' is not available on device ens160 because device is strictly unmanaged ``` 有一种临时方案 : ```bash dhclient ens160 ``` 执行后可以上网,可以远程连接,但是每次开机都无法自动启动, 最终找到原因是由于 NM托管未开启导致的 查看托管状态 nmcli n 显示 disabled 则为本文遇到的问题,如果是 enabled 则可以不用往下看了 开启 托管 nmcli n on