1、shell编程语言必知必会

shell命令解释器:bash编程

常用命令解释器:

命令解释器说明
bash目前应用最广泛的一款命令解释器,红帽系列、debian,Ubuntu等。BASH全称:Boume-Again Shell
dash一般debian、Ubuntu系统默认的,运行脚本推荐使用bash 脚本名字
csh、tcsh一些Unix系统使用
zsh功能多,支持更多的插件,可以更好看

注意:

shell脚本(bash  .sh)运行在Ubuntu中的时候,不推荐使用sh运行,推荐使用bash运行

2、编程语言分类

安装执行方式分类:

解析型:直接解析类,例如,shell、Python、php,书写成文件后,可以通过对应的解释器直接运行。
编译型:需要编译后运行,例如:C、C++、java、go等,下载好源代码,必须要进行编译成可以运行的命令

解释型:

[root@m01 ~]#vim /server/script/show.py
print("Hello World!!!")
[root@m01 ~]#python /server/script/show.py
Hello World!!!
[root@m01 ~]#

编译型:

[root@m01 ~]#vim /server/script/show.c
#include "stdio.h"

void main() {
  printf("hello world!!!");
}

#编译代码
[root@m01 ~]#gcc /server/script/show.c -o /server/script/show.bin
#生成的命令为:/server/script/show.bin
#执行命令:
[root@m01 ~]#/server/script/show.bin
hello world!!!
[root@m01 ~]#

3、编程环境准备

环境说明备注
主机主机名:m01192.168.10.61、172.16.1.61
代码目录/server/script/devops-shell/
编程环境vim、sublime等编程环境IDE这里选择vim或sublime即可
vim配置自动添加说明信息创建.sh或.bash文件自动添加信息

修改vimrc文件,达到控制vim创建,编辑文件的动作。

1、当前用户家目录下~/.vimrc,当前用户生效
2、放在/etc/vimrc文件末尾,所有用户生效

设置如下,在/etc/vimrc文件末尾添加:

set ignorecase
autocmd BufNewFile *.py,.cc,*.sh,*.java exec ":call SetTitle()"

func SetTitle()
if expand("%:e") =~ 'sh\|bash'
call setline(1, "#!/bin/bash")
call setline(2, "#####################################")
call setline(3, "# File Name:".expand("%"))
call setline(4, "# Version:v1.0")
call setline(5, "# Author:xzm")
call setline(6, "# Organization:logmm.org")
call setline(7, "# Desc:")
call setline(8, "#####################################")
endif
endfunc

解释:

set ignorecase  #搜索的时候忽略大小写

#设置创建.py、.sh文件的时候,使用SetTitle()函数
autocmd BufNewFile *.py,.cc,*.sh,*.java exec ":call SetTitle()"

#函数内容
func SetTitle()
if expand("%:e") =~ 'sh\|bash' #判断结尾是.sh或.bash
#setline写入行,1表示行号
call setline(1, "#!/bin/bash")
call setline(2, "#####################################")
call setline(3, "# File Name:".expand("%"))
call setline(4, "# Version:v1.0)
call setline(5, "# Author:xzm")
call setline(6, "# Organization:logmm.org")
call setline(7, "# Desc:")
call setline(8, "#####################################")
endif #判断结束
endfunc #函数结束

4、shell脚本执行方式

4.1 概述

应用及场景

应用场景说明
通过sh或bash书写脚本后,最常用的方式。 在其他非红帽系统中,建议使用bash运行脚本
通过.(点)或source加载/生效配置文件(环境变量,别名) 常用:可以用来实现include功能,把其他脚本引入当前脚本中
通过相对或绝对路径一般不推荐使用这种,一般都是系统脚本/调用的脚本,脚本需要加上可执行权限才能用
输入重定向符号不推荐使用

sh或bash

[root@m01 ~]#cd  /server/script/devops-shell/
[root@m01 /server/script/devops-shell]#vim 01.show.sh
#!/bin/bash
####################################################
# File Name:01.show.sh
# Version:v1.0
# Author:xzm
# Organization:logmm.org
# Desc:
####################################################
whoami
ls
#执行脚本
[root@m01 /server/script/devops-shell]#sh 01.show.sh
root
01.show.sh
[root@m01 /server/script/devops-shell]#bash 01.show.sh
root
01.show.sh
[root@m01 /server/script/devops-shell]#sh < 01.show.sh
root
01.show.sh

4.2 #!符号含义

并非注释的意思,写在脚本开头,用于指定脚本默认的命令解释器。

命令解释器写法
bash#!/bin/bash
python#!/usr/bin/python2或#!/usr/bin/dev python2

5、变量

5.1 变量

变量:variable

变量的本质:

变量:内存中的一块空间,变量名字===>内存空间地址

变量的定义:

变量名称=值

5.2 变量命名

变量命名要求

1、不能以数字开头
2、不能以特殊符号开头,但可以以_开头
3、建议使用字母开头
4、不能与系统中已有的变量或命令名称相同

命名方式:

1、驼峰方式:第一个单词小写,从第二个单词开始,每个单词的首字母大写,其他小写,如:personOfName
2、单词(小写)+“_”,如:person_of_name
3、要见名知义

5.3 变量分类

分类说明
普通变量(局部变量)在脚本中定义,通过变量名=值这种形式创建的
环境变量(全局变量)一般都是系统创建的,如:PATH、PS1….
特殊变量$+特殊符号的变量,shell脚本、命令、各方面
1)普通变量

取出变量的内容:$变量名

温馨提示:

$与${}一致,可以用于取值
例如:
age=32
$age与${age}一样,都是取出age的值

例如:

[root@m01 ~]#day=6
[root@m01 ~]#echo $day
6
[root@m01 ~]#echo ${day}
6
[root@m01 ~]#echo ${day}天
6天
[root@m01 ~]#echo $day'天'
6天
[root@m01 ~]#echo $day"天"
6天
[root@m01 ~]#
2)环境变量

全局变量:一处定义,处处使用

大部分都是系统定义的,我们一般就是修改

使用env命令可以查看系统的环境变量

环境变量名字含义应用场景
PATH记录命令位置的环境变量,运行命令的时候,bash会在PATH的路径中查找通过二进制包或编译安装的软件,需要增加新的命令路径
LANGLANG,language,系统语言与字符集。中文:LANG=zh_CN.UTF8,英文:LANG=en_US.UTF8修改语言字符
PS1命令行格式修改命令行格式
UID、EUID用户的uid可以在脚本中用于判断用户是否是root,root用户uid为0
HOSTNAME主机名
历史四人组history命令相关
HISTSIZEhistory命令记录最多多少条命令,生产环境中尽量少
HISTFILESIZEhistory历史记录文件的大小,~/.bash_history。生产环境尽量少
HISTCONTROL控制历史命令记录或不记录哪些内容。生产环境ignorespace以空格开头的命令,不记录
HISTFILE指定历史命令的记录文件的名字和位置,默认,当前用户的家目录的.bash_history文件
IFS类似于awk -F,指定分隔符(指定每一列的分隔符)一般与循环配合读取文件内容
TMOUT超时自动退出时间
PROMPT_COMMAND存放命令,命令行执行命令后会运行这个变量的内容,用于实现行为审计(记录用户操作)手写跳板机、行为审计

修改环境变量:

export 创建或修改全局变量(环境变量)

案例1:把当前系统语言修改为中文utf8

[root@m01 ~]#export LANG=zh_CN.UTF-8
#永久修改:
[root@m01 ~]#echo "LANG=zh_CN.UTF-8" >> /etc/profile
[root@m01 ~]#source /etc/profile
3)环境变量相关文件及目录
文件或目录说明应用场景
/etc/profile全局生效,存放函数、环境变量、别名
/etc/bashrc全局生效,存放别名
~/.bashrc家目录下,局部生效
~/.bash_profile家目录下,局部生效
/etc/profile.d/目录,每个用户登录的时候(远程连接与su功能),加载目录下的.sh结尾的文件设置一些登录后的提示信息

案例01:书写脚本,每次用户登录后显示系统信息

显示系统信息如下:

主机名: xxx
ip地址:xxx   xxxx
总计内存:xxx
可用内存:xxx
系统负载:xxx xxx xxx

脚本如下:

[root@m01 ~]#vim /etc/profile.d//show_info.sh
#1、vars
hostname=`hostname`
ip_addr=`hostname -I`
mem_total=`free -h | awk 'NR==2{print $2}'`
mem_available=`free -h | awk 'NR==2{print $NF}'`
sys_loadavg=`top -bn1 | awk -F: 'NR==1{print $NF}'`
#2、echo
echo "主机名:$hostname"
echo "ip地址:$ip_addr"
echo "内存总大小:$mem_total"
echo "可用内存:$mem_available"
echo "系统负载:$sys_loadavg"

登录系统,显示如下:

另外,脚本的中的输出部分可以写成:

#2、echo
cat << EOF
  操作需谨慎,删根弹指间
  不要使用rm命令
  主机名:${hostname}
  ip地址:${ip_addr}
  总内存:${mem_total}
  可用内存:${mem_available}
  系统负载:${sys_loadavg}
EOF

效果如下:

这个脚本的小bug,希望只有登录系统后显示,切换用户的时候不显示

判断两个变量是否存在即可,存在表示远程登录,不存在就切换用户
SSH_CLIENT=192.168.10.1 3399 22
SSH-TTY=/dev/pts/0

环境变量的加载顺序:

4)特殊变量

Linux shell编程中有各种各样的特殊变量:

位置相关的特殊变量
状态相关的特殊变量
变量子串
变量扩展

4.1)位置变量

位置的特殊变量含义应用场景
$n(n:数字,n>=1)脚本的第n个参数命令行与脚本内部桥梁
$0脚本的名字用于输出脚本的格式或版主的时候,用于错误提示输出帮助
$#脚本参数的个数一般与判断结合,检查脚本参数个数
$@取出脚本所有参数($1,$2,$3…$n)数组或循环中
$*取出脚本所有参数($1,$2,$3…$n)数组或循环中

案例1:$n、$0、$#参数:

[root@m01 ~]#cd /server/script/devops-shell/
[root@m01 /server/script/devops-shell]#vim 02.test_params.sh
echo '第1个参数 $1:' $1
echo '第2个参数 $2:' $2
echo '脚本的名称 $0:' $0
echo '脚本参数的个数 $#:' $#

#执行脚本:
[root@m01 /server/script/devops-shell]#sh 02.test_params.sh aa bb
第1个参数 $1: aa
第2个参数 $2: bb
脚本的名称 $0: 02.test_params.sh
脚本参数的个数 $#: 2
[root@m01 /server/script/devops-shell]#

$?:上一个命令执行的结果,0表示执行成功,非0表示执行失败

案例2:执行脚本的时候输入用户名,判断用户名是否存在

[root@m01 /server/script/devops-shell]#vim 03.check_user.sh
name=$1
id $name &> /dev/null
if [ $? -eq 0 ]
then
  echo "$1用户存在。"
else
  echo "$1用户不存在!"
fi

#执行效果
[root@m01 /server/script/devops-shell]#sh 03.check_user.sh root
root用户存在。
[root@m01 /server/script/devops-shell]#

案例3:$n,n大于9会有什么问题?

$10
大于9后,会表示为$1+0,即第1个参数+0
${10}才表示第10个参数

例如:

[root@m01 /server/script/devops-shell]#vim 03.params-10.sh
echo $1 $2 $3 $4 $5 $6 $7 $8 $9 $10 $11
echo $1 $2 $3 $4 $5 $6 $7 $8 $9 ${10} ${11}

#测试
[root@m01 /server/script/devops-shell]#sh 03.params-10.sh {a..k}
a b c d e f g h i a0 a1
a b c d e f g h i j k
[root@m01 /server/script/devops-shell]#

因此:

$10:$1+0,所以是a0
$11:$1+1,所以是a1
${10}:表示第10个参数
${11}:表示第11个参数

案例04:$0,如果脚本执行错了,需要输出使用帮助:脚本名+{内容}

先略过如何检查是否出错。
echo "xxx.sh{start|stop|restart}"

脚本如下:

[root@m01 /server/script/devops-shell]#vim 04.print_usage.sh
echo "Usage:$0 start|stop|restart"

[root@m01 /server/script/devops-shell]#sh 04.print_usage.sh
Usage:04.print_usage.sh start|stop|restart
[root@m01 /server/script/devops-shell]#

案例05:$# 脚本参数个数

[root@m01 /server/script/devops-shell]#vim 03.check_user_v2.sh
name=$1
#脚本参数个数不是1
#提示并退出
if [ $# -ne 1 ];then
   echo "请输入:$0 用户名"
   exit 1 #退出
fi

id $name &> /dev/null
if [ $? -eq 0 ]
then
  echo "$1用户存在。"
else
  echo "$1用户不存在!"
fi

案例06:$@与$*

[root@m01 /server/script/devops-shell]#vim 03.params-10-v2.sh
echo $1 $2 $3 $4 $5 $6 $7 $8 $9 ${10} ${11}
echo $@
echo $*
[root@m01 /server/script/devops-shell]#sh 03.params-10-v2.sh {1..12}
1 2 3 4 5 6 7 8 9 10 11
1 2 3 4 5 6 7 8 9 10 11 12
1 2 3 4 5 6 7 8 9 10 11 12
[root@m01 /server/script/devops-shell]#

案例07:书写一个回收站脚本,替代rm命令

执行脚本,传入参数:被删除的文件或目录

如何替代rm?删除—>mv

创建回收站目录:/recyle/年-月-日_周几-时-分-秒

脚本:

[root@m01 /server/script/devops-shell]#vim 06.recyle_rm.sh
#1、vars
time=`date +%F_%w_%H%M%S`
recyle_dir=/recyle/$time

#2、判断
if [ $# -eq 0 ];then
  echo "提示:$0  files dirs"
  exit 1
fi
#3、创建目录及移动
mkdir -p $recyle_dir
mv $* $recyle_dir

#4、测试
[root@m01 /server/script/devops-shell]#touch a{1..3}.txt
[root@m01 /server/script/devops-shell]#sh 06.recyle_rm.sh *.txt
[root@m01 /server/script/devops-shell]#ll /recyle/2023-07-12_3_225939/
总用量 0
-rw-r--r-- 1 root root 0 7月  12 22:59 a1.txt
-rw-r--r-- 1 root root 0 7月  12 22:59 a2.txt
-rw-r--r-- 1 root root 0 7月  12 22:59 a3.txt
[root@m01 /server/script/devops-shell]#

#5、给rm起别名
[root@m01 /server/script/devops-shell]#alias rm='sh /server/script/devops-shell/06.recyle_rm.sh $*'

#这样rm命令就被替换了,这是临时生效,如果要永久,得在/etc/profile文件中定义别名

$@、$*的区别

符号共同点区别
$@取出脚本所有参数($1,$2,$3…$n)加上双引号,会识别合并在一起的参数,每个都是独立参数
$*取出脚本所有参数($1,$2,$3…$n)加上双引号,所有参数会合并为1个参数

例如:

[root@m01 /server/script/devops-shell]#vim 07.sh
set "I am old" 1 2 3
for i in "$*"
do
  echo $i
done
echo "#############"
for j in "$@"
do
  echo $j
done

#执行结果:
[root@m01 /server/script/devops-shell]#sh 07.sh
I am old 1 2 3
#############
I am old
1
2
3
[root@m01 /server/script/devops-shell]#

4.2)状态变量

状态变量含义应用场景
$?上一个命令、脚本的返回值,0表示正确,非0表示错误一般与判断检查命令结果
$$当前脚本的pid一般写在脚本中获取脚本pid
$!上一个脚本、命令(持续运行的)pid
$_上一个命令的最后一个参数,其实下划线是个环境变量,记录了上一个命令、脚本的最后一个参数。使用esc+.(点)

案例07:输入任何一个命令并执行,检查这个命令的执行结果是否正确

分析:

1、输入命令(参数)
2、执行命令
3、执行结果$? 
 if判断,如果是0,则表示成功 
 如果不是0,则表示失败

具体脚本:

[root@m01 /server/script/devops-shell]#vim 07.check_cmd.sh
source /etc/init.d/functions
#1.检查
#脚本参数个数 大于等于1
if [ $# -eq 0 ];then
  echo "Usage: $0 命令"
  exit 1
fi
#检查当前用户是否为root
if [ $UID -ne 0 ];then
  echo "请使用root用户运行脚本"
  exit 2
fi
#2、运行传入进来的命令
$* &> /dev/null 
#3、判断
if [ $? -eq 0 ];then
  echo "命令执行成功"
  action "命令执行成功" /bin/true
else
  echo "命令执行失败"
  action "命令执行失败" /bin/false
fi

执行脚本:

输出颜色:

1、source /etc/init.d/functions
2、action "XXXX" /bin/true 或 /bin/false

例如:

4.3)变量子串

变量子串:parameter expension,变量扩展

格式:${变量},写成”$变量“形式无法识别

作用:用于对变量处理。(统计变量中字符串数量,对变量内容进行替换、删除等)

应用:可以不用

变量子串 parameter表示变量名字含义
基础
${parameter} $para变量取值
${#parameter}统计字符长度(变量中有多少个字符)
删除(开头、结尾)word表示要删除的内容
${parameter#word}从变量左边开始删除,按照最短匹配删除
${parameter##word}从变量左边开始删除,按照最长匹配删除
${parameter%word}从变量右边开始删除,按照最短匹配删除
${parameter%%word}从变量右边开始删除,按照最长匹配删除
截取(切片)类似于cut -c
${var:5}
${var:5:2}从下标是5字符开始向后截取2个字符(注意:下标是从0开始的,0、1、2、3…)
替换
${para/word/replace}把word替换为replace,仅替换第1个
${para//word/replace}把word替换为replace,替换全部

1、统计字符长度

[root@m01 ~]#name="linux"
[root@m01 ~]#echo ${#name}
5
[root@m01 ~]#

案例:I am oldboy teacher welcome to linux class.

打印出这句话中字母数不大于6的单词。

思路:

1、循环取出这句话中每个单词(处理掉非单词的内容,比如点)
2、判断单词的字符数
3、输出

具体脚本

[root@m01 /server/script/devops-shell]#vim 08.print_word.sh
string="I am  oldboy teacher welcome to linux class."
string_del=`echo $string | sed 's#\.##g'`
for i in $string_del
do
 if [ ${#i} -le 6 ];then
    echo $i
 fi
done

或者:

#1、vars
string="I am  oldboy teacher welcome to linux class."
#2、for
for i in ${string%.}
do
 if [ ${#i} -le 6 ];then
    echo $i
 fi
done

使用awk:

echo 'I am  oldboy teacher welcome to linux class.' | xargs -n1 | awk -F "[ .]" 'length()<=6'

2、删除变量中的内容(仅影响输出,不会影响变量本身)

[root@m01 ~]#var=oldboylida996
[root@m01 ~]#echo ${var#oldboy}
lida996
[root@m01 ~]#echo ${var#o}
ldboylida996
[root@m01 ~]#echo ${var##*o}
ylida996
[root@m01 ~]#echo ${var#*o}
ldboylida996
[root@m01 ~]#

取出路径中的文件名:

[root@m01 ~]#dir="/etc/sysconfig/network-scripts/ifcfg-eth0"
[root@m01 ~]#echo ${dir##*/}
ifcfg-eth0

%:使用
[root@m01 ~]#echo ${dir%/*}
/etc/sysconfig/network-scripts
[root@m01 ~]#

3、切片

[root@m01 ~]#a=123456
[root@m01 ~]#echo ${a:2}
3456
[root@m01 ~]#echo ${a:2:2}
34
[root@m01 ~]#

cut的切片:

[root@m01 ~]#echo 12345 | cut -c1
1
[root@m01 ~]#echo 12345 | cut -c1-3
123
[root@m01 ~]#echo 12345 | cut -c2-4
234
[root@m01 ~]#

4、替换

[root@m01 ~]#a=121
[root@m01 ~]#echo ${a/1/a}
a21
[root@m01 ~]#echo ${a//1/a}
a2a
[root@m01 ~]#echo ${a//[1-3]/a}
aaa
[root@m01 ~]#

统计脚本运行多久:time sh 脚本名称

[root@m01 /server/script/devops-shell]#time sh 08.print_word.sh
I
am
oldboy
to
linux
class

real	0m0.008s
user	0m0.002s
sys	0m0.005s

统计字符串个数:

[root@m01 ~]#echo 12345 | wc -L
5
[root@m01 ~]#
4.4)变量扩展

给变量设置默认值

格式含义
${parameter:-word}变量parameter没定义或为空,把word作为默认值,不修改变量内容(输出)
${parameter:=word}变量parameter没定义或为空,把word作为默认值,修改变量内容
${parameter:?word}变量parameter没定义或为空,显示word,错误输出
${parameter:+word}变量parameter没定义或为空,则啥也不做,否则把word替换为变量内容

例如:

:-使用:

[root@m01 ~]#echo $name

[root@m01 ~]#echo ${name:-root}
root
[root@m01 ~]#name=haha
[root@m01 ~]#echo ${name:-root}
haha
[root@m01 ~]#

:=使用:

[root@m01 ~]#unset name
[root@m01 ~]#name=haha
[root@m01 ~]#echo ${name:=root}
haha
[root@m01 ~]#unset name
[root@m01 ~]#echo ${name:=root}
root
[root@m01 ~]#echo $name
root
[root@m01 ~]#

:?使用:

[root@m01 ~]#unset name
[root@m01 ~]#echo ${name:?root}
-bash: name: root
[root@m01 ~]#name=haha
[root@m01 ~]#echo ${name:?root}
haha

:+使用:

[root@m01 ~]#unset name
[root@m01 ~]#echo ${name:+root}

[root@m01 ~]#name=haha
[root@m01 ~]#echo ${name:+root}
root
[root@m01 ~]#echo $name
haha
[root@m01 ~]#