【Shell编程】 变量

时间:2021-08-10 14:06来源:未知 作者:中博IT教育

变量 一 变量介绍 什么是变量? 量指的是记录事物的状态 变指的是事物的状态是可以发生变化的 变量本质就是一种数据存取的机制,变量的数据都是存放于内存中的 为何要有变量?
变量
一 变量介绍
什么是变量?
 
量指的是记录事物的状态
 
变指的是事物的状态是可以发生变化的
 
变量本质就是一种数据存取的机制,变量的数据都是存放于内存中的
 
 
 
为何要有变量?
 
程序=数据+功能,程序运行的本质就是一系列状态的变化,变量就是用来记录状态并且可以修改的一种机制
 
二 变量的使用
2.1 定义、引用、删除
先定义
 
# 1、语法:变量名=值
# 2、注意:等号左右两边不能有空格
# 3、例如:
[root@localhost shell]# name="egon"
 
# 4、注意:变量名的命令应该见名知意,同时遵循如下规则
以字母或下划线开头,剩下的部分可以是:字母、数字、下划线,最好遵循下述规范:
    1.以字母开头
    2.使用中划线或者下划线做单词的连接
    3.同类型的用数字区分
    4.对于文件最好加上拓展名
例如: sql_bak.tar.gz,log_bak.tar.bz2  
    5、不要带有空格、?、*等特殊字符
    6、不能使用bash中的关键字,例如if,for,while,do等
    7、不要和系统环境变量冲突
后引用
 
[root@localhost shell]# ip="192.168.11.10"
[root@localhost shell]# echo $ip
192.168.11.10
 
注意:如果是打印百分比,建议使用${变量名}%
[root@localhost shell]# percent=33
[root@localhost shell]# echo ${percent}%
33%
删除变量
 
[root@localhost shell]# x=111
[root@localhost shell]# unset x
[root@localhost shell]# echo $x
 
[root@localhost shell]#
2.2 变量值的三种来源
(1)直接赋值
 
# 1. 显式赋值:变量名=变量值
示例:
ip1=192.168.11.200
school="Shanghai oldboy"
today1=`date +%F`
today2=$(date +%F)
 
# 2、应用示例
[root@localhost ~]# url="www.baidu.com"
[root@localhost ~]# echo $url
www.baidu.com
[root@localhost ~]# url="www.sina.com.cn"
[root@localhost ~]# echo $url
www.sina.com.cn
(2)参数
 
从调用脚本时传入的位置参数获取变量值:./b.sh a1 a2 a3
需要用到$n获取第n个位置参数值,超过10需要用${n},如下
$0 $1 $2 $3 $4 $5 $6 $7 $8 $9 ${10}
 
# 示例
[root@egon ~]# cat b.sh 
#!/usr/bin/env bash
echo ${0}
echo $1
echo $2
echo $3
echo $4
echo $5
echo $6
echo $7
echo $8
echo $9
echo ${10}
echo ${11}
echo ${12}
 
# 运行
[root@egon ~]# chmod +x b.sh 
[root@egon ~]# ./b.sh a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13 a14 a15
./b.sh
a1
a2
a3
a4
a5
a6
a7
a8
a9
a10
a11
a12
 
# 企业使用:可以实现脚本的不同功能(服务程序管理脚本-启动功能 关闭功能 重启功能)
server.sh  start|stop|restart
(3)read从键盘输入读取
 
# 1. read 从键盘读入变量值
read 变量名
read -p "提示信息: "  变量名
read -t 5 -p "提示信息: "  变量名  # -t指定秒数
read -n 2 变量名  # -n读取的字符个数
 
# 2. 应用示例:vim first.sh
back_dir1=/var/backup
read -p "请输入你的备份目录: " back_dir2
echo $back_dir1
echo $back_dir2
 
# 企业使用:可以根据需求调整变量值,可以便于新员工快速掌握企业脚本使用
2.3 变量值操作
例1:获取变量值的长度
 
[root@localhost ~]# echo ${#url}
15
 
# 企业面试题:已知变量msg='hello world!',请统计出变量中包含的字符数量
# 方法一:
[root@egon /]# echo ${#msg}
12
# 方法二:
[root@egon /]# echo $msg | wc -L
12
# 方法三:
[root@egon /]# echo $msg|awk '{print length}'
12
 
# 方法四:
[root@egon ~]# expr length "$msg" #length是一个函数,注意因为msg的值有空格,所以$msg必须用引号包含
12
例2:切片
 
${paramter:offset:length}
 
[root@egon /]# msg="abcdef"
[root@egon /]# echo ${msg:3}  # 从3号索引开始,一直到最后
def
[root@egon /]# echo ${msg:3:2}  # 从3号索引开始,往后数2个字符
de
[root@egon /]# echo ${msg::3}  # 从0开始,往后数3个字符
abc
例3:截断
 
# =================》一、砍掉左边的字符《=================
# 1.1 简单使用 
[root@egon ~]# url="www.sina.com.cn"
[root@egon ~]# echo ${url#www.}
sina.com.cn
 
# 1.2 结合*=》非贪婪,默认情况下*是非贪婪,尽可能地少“吃”字符
[root@egon ~]# echo ${url#*w}
ww.sina.com.cn
 
# 1.3 结合*=》贪婪,尽可能地多“吃”字符
[root@egon ~]# echo ${url##*w}  # *会尽可能多地吃掉字符,一直匹配到最远的那个w才停下来
.sina.com.cn
 
# =================》二、砍掉右边的字符《=================
# 1.1 简单使用
[root@egon ~]# url="www.sina.com.cn"
[root@egon ~]# echo ${url%.cn}
www.sina.com
 
# 1.2 结合*=》非贪婪
[root@egon ~]# echo ${url%.*}
www.sina.com
# 1.3 结合*=》贪婪
[root@egon ~]# echo ${url%%.*}
www
 
# =================》三、应用示例《=================
[root@egon ~]# hostname
egon.xxx.com
[root@egon ~]# echo $HOSTNAME
egon.xxx.com
[root@egon ~]# echo ${HOSTNAME%.*}
egon.xxx
[root@egon ~]# echo ${HOSTNAME%%.*}
egon
例4:内容的替换
 
[root@egon ~]# url="www.sina.com.cn"
[root@egon ~]# echo ${url/sina/baidu}
www.baidu.com.cn
[root@egon ~]# echo ${url/n/N}
www.siNa.com.cn
[root@egon ~]# echo ${url//n/N}  # 贪婪
www.siNa.com.cN
 
# 应用示例:批量修改文件名称
[root@egon shell]# touch egon_2020_{01..05}_linux.txt
[root@egon shell]# ls
egon_2020_01_linux.txt  egon_2020_02_linux.txt  egon_2020_03_linux.txt  egon_2020_04_linux.txt  egon_2020_05_linux.txt
[root@egon shell]# for i in `ls *linux.txt`;do mv $i ${i/_linux/};done
[root@egon shell]# ls
egon_2020_01.txt  egon_2020_02.txt  egon_2020_03.txt  egon_2020_04.txt  egon_2020_05.txt
例5:变量的替代
 
${x:-临时变量信息}
${x:=新的变量信息}
${x:?没有设置变量提示信息}   
${x:+有设置变量提示信息} 
 
 
#1、${parameter-word}: 当调取变量没有定义过, 就返回word字符串信息
[root@egon ~]# unset name 
[root@egon ~]# echo ${name}
 
[root@egon ~]# echo ${name-"egon"}  # 没有定义过变量name,则使用-后的值
egon
[root@egon ~]# 
[root@egon ~]# gender=  # 定义过变量了,则使用变量的原值,哪怕变量的值为空值
[root@egon ~]# echo ${gender-"male"}
 
[root@egon ~]# 
[root@egon ~]# age=18  
[root@egon ~]# echo ${age-19}  # 定义过变量了,则使用变量的原值
18
[root@egon ~]# 
 
#2、${parameter:-word}: 当调取变量信息值为空时或未定义变量, 就返回word字符串信息
[root@egon ~]# unset x  
[root@egon ~]# unset y
[root@egon ~]# unset z
[root@egon ~]# 
[root@egon ~]# echo ${x:-aaa}  # 没有定义过变量x,则使用-后的值
aaa
[root@egon ~]# y=
[root@egon ~]# echo ${y:-aaa}  # 定义过变量y,但变量y的值为空值,则使用-后的值
aaa
[root@egon ~]# z=333
[root@egon ~]# echo ${z:-aaa}  # 定义过变量了,并且变量有一个非空的原值,则使用变量的原值
333
 
#3、{parameter:=word}:当调取变量信息值为空时或未定义,则设置指定字符串为新的变量值
[root@egon /]# unset x
[root@egon /]# echo ${x:=123}
123
[root@egon /]# echo $x
123
 
#4、${parameter:?word}:当调取变量信息值为空时或未定义,指定为赋值的错误提示信息
[root@egon /]# unset x
[root@egon /]# echo ${x:?该变量没有定义过}
-bash: x: 该变量没有定义过
        
#5、${parameter:+word}:当调取变量信息值为空时或未定义,不做任何处理,否则word字符串将替代变量值
[root@egon /]# unset x
[root@egon /]# echo ${x:+哈哈哈}
 
[root@egon /]# x=123
[root@egon /]# echo ${x:+哈哈哈}
哈哈哈
例6:let
 
# (1) 变量的值
[root@egon ~]# j=1
[root@egon ~]# let ++j
[root@egon ~]# echo $j
2
[root@egon ~]# 
 
# (2) 表达式的值
[root@egon ~]# unset i
[root@egon ~]# unset j
[root@egon ~]# 
[root@egon ~]# i=1
[root@egon ~]# j=1
[root@egon ~]# 
[root@egon ~]# let x=i++  # 先把i赋值给x,然后再++
[root@egon ~]# let y=++j  # 先++j,然后再把j的结果赋值给y
[root@egon ~]# echo $i
2
[root@egon ~]# echo $j
3
[root@egon ~]# echo $x
1
[root@egon ~]# echo $y
2
2.4 单引号与双引号
" "   弱引用,引号的特殊字符有意义
' '  强引用,引号内所有特殊字符都取消意义
[root@localhost ~]# school=“oldboy”
[root@localhost ~]# echo "${school} is good"
oldboy is good
[root@localhost ~]# echo '${school} is good'
${school} is good  
2.5 将命令的结果赋值给变量:
# ``与$()
` `  命令替换 等价于 $()   反引号中的shell命令会被先执行
[root@localhost ~]# touch `date +%F`_file1.txt  
[root@localhost ~]# touch $(date +%F)_file2.txt 
 
[root@localhost ~]# disk_free3="df -Ph |grep '/$' |awk '{print $4}'"  # 错误
[root@localhost ~]# disk_free4=$(df -Ph |grep '/$' |awk '{print $4}') # 正确
[root@localhost ~]# disk_free5=`df -Ph |grep '/$' |awk '{print $4}'`  # 正确
2.6 只读变量
使用 readonly 命令可以将变量定义为只读变量,只读变量的值不能被改变。
 
[root@egon ~]# x=111
[root@egon ~]# readonly x
[root@egon ~]# x=666
-bash: x: 只读变量
2.7 预定变量
$* 所有的参数
$@  所有的参数
$#  参数的个数
$$  当前进程的PID  # 此外,可以使用只读变量来获取父进程的PID:$PPID、获取执行脚本的用户ID:$UID
$? 上一个命令的返回值 0表示成功 
 
 
示例1:
[root@egon ~]# chmod +x b.sh 
[root@egon ~]# ./b.sh a1 a2 a3 a4 a5
a1 a2 a3 a4 a5
a1 a2 a3 a4 a5
5
18988
1
[root@egon ~]# cat b.sh 
#!/usr/bin/env bash
echo $*
echo $@
echo $#
echo $$
 
ping -c1 192.168.11.10 &>/dev/null
echo $?
 
示例2:
[root@egon ~]# vim ping.sh
#!/bin/bash       
ping -c2 $1 &>/dev/null  
if [ $? = 0 ];then     
 echo "host $1 is ok"  
else           
 echo "host $1 is fail" 
fi
 
[root@egon ~]# chmod +x ping.sh
[root@egon ~]# ./ping.sh 192.168.11.10
了解:
 
如果我们想从命令行中获取脚本调用者传入的参数值,用$n可以取到,但如果脚本调用者在命令行传入的参数个数不固定,那么此时就需要用$*或$@来获取了
 
$*与$@获取的是所有位置参数,$0除外
当脚本调用者的传参形式如下是
 
[root@localhost ~]# ./script.sh 命令1 命令2 命令3
针对for循环语句:for i in 元素,for循环会按照空格作为分隔符来一个个取元素,所以此时$*与$@并无区别
 
[root@localhost ~]# cat script.sh 
#!/bin/bash
 
for i in $*
do
    echo $i
done
 
echo "=================="
 
for i in $@
do
    echo $i
done
[root@localhost ~]# 
[root@localhost ~]# ./script.sh 命令1 命令2 命令3
命令1
命令2
命令3
==================
命令1
命令2
命令3
当脚本调用者的传参形式如下时
 
[root@localhost ~]# ./script.sh 命令1 命令2 "命令3 参数"
针对for循环语句:for i in 元素,for循环会按照空格作为分隔符来一个个取元素,所以此时$*与$@如果不加引号,第三个命令:"命令3 参数",为被以空格为分隔符识别成两部分
 
[root@localhost ~]# cat script.sh 
#!/bin/bash
 
for i in $*
do
    echo $i
done
 
echo "=================="
 
for i in $@
do
    echo $i
done
[root@localhost ~]# 
[root@localhost ~]# ./script.sh 命令1 命令2 "命令3 参数"
命令1
命令2
命令3
参数
==================
命令1
命令2
命令3
参数
所以需要为$*与$@加上引号
 
[root@localhost ~]# cat script.sh 
#!/bin/bash
 
for i in "$*"
do
    echo $i
done
 
echo "=================="
 
for i in "$@"
do
    echo $i
done
[root@localhost ~]# ./script.sh 命令1 命令2 "命令3 参数"
命令1 命令2 命令3 参数
==================
命令1
命令2
命令3 参数
[root@localhost ~]#
但此时为$*就会把所有位置参数识别成一个整体,所以总结如下:
 
当脚本调用者的传参形式如下时
[root@localhost ~]# ./script.sh 命令1 命令2 "命令3 参数"
 
需要使用"$@"来分别获取一个个完整的命令
for i in "$@"
do
    echo $i
done
 
其余情况$*与$@完全一致
 
 
##三 扩展:僵尸进程与孤儿进程
 
僵尸进程
 
#1、什么是僵尸进程
操作系统负责管理进程
我们的应用程序若想开启子进程,都是在向操作系统发送系统调用
当一个子进程开启起来以后,它的运行与父进程是异步的,彼此互不影响,谁先死都不一定
 
linux操作系统的设计规定:父进程应该具备随时获取子进程状态的能力
如果子进程先于父进程运行完毕,此时若linux操作系统立刻把该子进程的所有资源全部释放掉,那么父进程来查看子进程状态时,会突然发现自己刚刚生了一个儿子,但是儿子没了!!!
这就违背了linux操作系统的设计规定
所以,linux系统出于好心,若子进程先于父进程运行完毕/死掉,那么linux系统在清理子进程的时候,会将子进程占用的重型资源都释放掉(比如占用的内存空间、cpu资源、打开的文件等),但是会保留一部分子进程的关键状态信息,比如进程号the process ID,退出状态the termination status of the process,运行时间the amount of CPU time taken by the process等,此时子进程就相当于死了但是没死干净,因而得名"僵尸进程",其实僵尸进程是linux操作系统出于好心,为父进程准备的一些子进程的状态数据,专门供父进程查阅,也就是说"僵尸进程"是linux系统的一种数据结构,所有的子进程结束后都会进入僵尸进程的状态
 
# 2、那么问题来了,僵尸进程残存的那些数据不需要回收吗???
当然需要回收了,但是僵尸进程毕竟是linux系统出于好心,为父进程准备的数据,至于回收操作,应该是父进程觉得自己无需查看僵尸进程的数据了,父进程觉得留着僵尸进程的数据也没啥用了,然后由父进程发起一个系统调用wait / waitpid来通知linux操作系统说:哥们,谢谢你为我保存着这些僵尸的子进程状态,我现在用不上他了,你可以把他们回收掉了。然后操作系统再清理掉僵尸进程的残余状态,你看,两者配合的非常默契,但是,怕就怕在。。。
 
 
# 3、分三种情况讨论
1、linux系统自带的一些优秀的开源软件,这些软件在开启子进程时,父进程内部都会及时调用wait/waitpid来通知操作系统回收僵尸进程,所以,我们通常看不到优秀的开源软件堆积僵尸进程,因为很及时就回收了,与linux系统配合的很默契
 
2、一些水平良好的程序员开发的应用程序,这些程序员技术功底深厚,知道父进程要对子进程负责,会在父进程内考虑调用wait/waitpid来通知操作系统回收僵尸进程,但是发起系统调用wait/waitpid的时间可能慢了些,于是我们可以在linux系统中通过命令查看到僵尸进程状态
[root@egon ~]# ps aux | grep [Z]+
 
3、一些垃圾程序员,技术非常垃圾,只知道开子进程,父进程也不结束,就在那傻不拉几地一直开子进程,也压根不知道啥叫僵尸进程,至于wait/waitpid的系统调用更是没听说过,这个时候,就真的垃圾了,操作系统中会堆积很多僵尸进程,此时我们的计算机会进入一个奇怪的现象,就是内存充足、硬盘充足、cpu空闲,但是,启动新的软件就是无法启动起来,为啥,因为操作系统负责管理进程,每启动一个进程就会分配一个pid号,而pid号是有限的,正常情况下pid也用不完,但怕就怕堆积一堆僵尸进程,他吃不了多少内存,但能吃一堆pid
 
# 4、如果清理僵尸进程
针对情况3,只有一种解决方案,就是杀死父进程,那么僵尸的子进程会被linux系统中pid为1的顶级进程(init或systemd)接管,顶级进程会定期发起系统调用wait/waitpid来通知操作系统清理僵尸
 
针对情况2,可以发送信号给父进程,通知它快点发起系统调用wait/waitpid来清理僵尸的儿子
kill -CHLD 父进程PID
 
# 5、结语
僵尸进程是linux系统出于好心设计的一种数据结构,一个子进程死掉后,相当于操作系统出于好心帮它的爸爸保存它的遗体,之说以会在某种场景下有害,是因为它的爸爸不靠谱,儿子死了,也不及时收尸(发起系统调用让操作系统收尸)
 
说白了,僵尸进程本身无害,有害的是那些水平不足的程序员,他们总是喜欢写bug,好吧,如果你想看看垃圾程序员是如何写bug来堆积僵尸进程的,你可以看一下这篇博客https://www.cnblogs.com/linhaifeng/articles/13567273.html
孤儿进程
 
父进程先死掉,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被进程号为1的顶级进程(init或systemd)所收养,并由顶级进程对它们完成状态收集工作。
进程就好像是一个民政局,专门负责处理孤儿进程的善后工作。每当出现一个孤儿进程的时候,内核就把孤儿进程的父进程设置为顶级进程,而顶级进程会循环地wait()它的已经退出的子进程。这样,当一个孤儿进程凄凉地结束了其生命周期的时候,顶级进程就会代表党和政府出面处理它的一切善后工作。因此孤儿进程并不会有什么危害。
 
我们来测试一下(创建完子进程后,主进程所在的这个脚本就退出了,当父进程先于子进程结束时,子进程会被顶级进程收养,成为孤儿进程,而非僵尸进程),文件内容
 
import os
import sys
import time
 
pid = os.getpid()
ppid = os.getppid()
print 'im father', 'pid', pid, 'ppid', ppid
pid = os.fork()
#执行pid=os.fork()则会生成一个子进程
#返回值pid有两种值:
#    如果返回的pid值为0,表示在子进程当中
#    如果返回的pid值>0,表示在父进程当中
if pid > 0:
    print 'father died..'
    sys.exit(0)
 
# 保证主线程退出完毕
time.sleep(1)
print 'im child', os.getpid(), os.getppid()
 
执行文件,输出结果:
im father pid 32515 ppid 32015
father died..
im child 32516 1
 
看,子进程已经被pid为1的顶级进程接收了,所以僵尸进程在这种情况下是不存在的
(责任编辑:中博IT教育)

苏公网安备 32030302000649号