shell队列实现线程并发控制

shell队列实现线程并发控制

需求:并发检测1000台web服务器状态(或者并发为1000台web服务器分发文件等)如何用shell实现?

方案一:(这应该是大多数人都第一时间想到的方法吧)

思路:一个for循环1000次,顺序执行1000次任务。

实现:

#!/bin/bash
start_time=`date +%s` #定义脚本运行的开始时间

for ((i=1;i<=1000;i++))
do
       sleep 1  #sleep 1用来模仿执行一条命令需要花费的时间(可以用真实命令来代替)
       echo 'success'$i;      
done

stop_time=`date +%s`  #定义脚本运行的结束时间

echo "TIME:`expr $stop_time - $start_time`"

运行结果:

[root@iZ94yyzmpgvZ ~]# . test.sh 
success1
success2
success3
success4
success5
success6
success7
........此处省略
success999
success1000
TIME:1000

代码解析以及问题:

一个for循环1000次相当于需要处理1000个任务,循环体用sleep 1代表运行一条命令需要的时间,用success$i来标示每条任务.

这样写的问题是,1000条命令都是顺序执行的,完全是阻塞时的运行,假如每条命令的运行时间是1秒的话,那么1000条命令的运行时间是1000秒,效率相当低,而我的要求是并发检测1000台web的存活,如果采用这种顺序的方式,那么假如我有1000台web,这时候第900台机器挂掉了,检测到这台机器状态所需要的时间就是900s,吃屎都吃不上热乎的。

所以,问题的关键集中在一点:如何并发

方案二:

思路:一个for循环1000次,循环体里面的每个任务都放入后台运行(在命令后面加&符号代表后台运行)。

实现:

#!/bin/bash
start=`date +%s` #定义脚本运行的开始时间

for ((i=1;i<=1000;i++))
do
{
       sleep 1  #sleep 1用来模仿执行一条命令需要花费的时间(可以用真实命令来代替)
       echo 'success'$i;
}&              #用{}把循环体括起来,后加一个&符号,代表每次循环都把命令放入后台运行
                #一旦放入后台,就意味着{}里面的命令交给操作系统的一个线程处理了
                #循环了1000次,就有1000个&把任务放入后台,操作系统会并发1000个线程来处理
                #这些任务        
done    
wait             #wait命令的意思是,等待(wait命令)上面的命令(放入后台的)都执行完毕了再
                #往下执行。
                #在这里写wait是因为,一条命令一旦被放入后台后,这条任务就交给了操作系统
                #shell脚本会继续往下运行(也就是说:shell脚本里面一旦碰到&符号就只管把它
                #前面的命令放入后台就算完成任务了,具体执行交给操作系统去做,脚本会继续
                #往下执行),所以要在这个位置加上wait命令,等待操作系统执行完所有后台命令
end=`date +%s`  #定义脚本运行的结束时间

echo "TIME:`expr $end - $start`"

运行结果:

[root@iZ94yyzmpgvZ /]# . test1.sh 
......
[989]   Done                   { sleep 1; echo 'success'$i; }
[990]   Done                   { sleep 1; echo 'success'$i; }
success992
[991]   Done                   { sleep 1; echo 'success'$i; }
[992]   Done                   { sleep 1; echo 'success'$i; }
success993
[993]   Done                   { sleep 1; echo 'success'$i; }
success994
success995
[994]   Done                   { sleep 1; echo 'success'$i; }
success996
[995]   Done                   { sleep 1; echo 'success'$i; }
[996]   Done                   { sleep 1; echo 'success'$i; }
success997
success998
[997]   Done                   { sleep 1; echo 'success'$i; }
success999
[998]   Done                   { sleep 1; echo 'success'$i; }
[999]- Done                   { sleep 1; echo 'success'$i; }
success1000
[1000]+ Done                   { sleep 1; echo 'success'$i; }
TIME:2

代码解析以及问题:

shell中实现并发,就是把循环体的命令用&符号放入后台运行,1000个任务就会并发1000个线程,运行时间2s,比起方案一的1000s,已经非常快了。

可以看到输出结果success4 …success3完全都是无序的,因为大家都是后台运行的,这时候就是cpu随机运行了,所以并没有什么顺序

这样写确实可以实现并发,然后,大家可以想象一下,1000个任务就要并发1000个线程,这样对操作系统造成的压力非常大,它会随着并发任务数的增多,操作系统处理速度会变慢甚至出现其他不稳定因素,就好比你在对nginx调优后,你认为你的nginx理论上最大可以支持1w并发了,实际上呢,你的系统会随着高并发压力会不断攀升,处理速度会越来越慢(你以为你扛着500斤的东西你还能跑的跟原来一样快吗)

方案三:

思路:基于方案二,使用linux管道文件特性制作队列,控制线程数目

知识储备:

一.管道文件

1:无名管道(ps aux | grep nginx)

2:有名管道(mkfifo /tmp/fd1)

有名管道特性:

1.cat /tmp/fd1(如果管道内容为空,则阻塞)

实验:

2.echo “test” > /tmp/fd1(如果没有读管道的操作,则阻塞)

总结:

利用有名管道的上述特性就可以实现一个队列控制了

你可以这样想:一个女士公共厕所总共就10个蹲位,这个蹲位就是队列长度,女厕

所门口放着10把药匙,要想上厕所必须拿一把药匙,上完厕所后归

还药匙,下一个人就可以拿药匙进去上厕所了,这样同时来了1千

位美女上厕所,那前十个人抢到药匙进去上厕所了,后面的990人

需要等一个人出来归还药匙才可以拿到药匙进去上厕所,这样10把

药匙就实现了控制1000人上厕所的任务(os中称之为信号量)

二.文件描述符

1.管道具有存一个读一个,读完一个就少一个,没有则阻塞,放回的可以重复取,这正是队列

特性,但是问题是当往管道文件里面放入一段内容,没人取则会阻塞,这样你永远也没办法

往管道里面同时放入10段内容(想当与10把药匙),解决这个问题的关键就是文件描述符了。

\2. mkfifo /tmp/fd1

创建有名管道文件exec 3<>/tmp/fd1,创建文件描述符3关联管道文件,这时候3这个文件描述符就拥有了管道的所有特性,还具有一个管道不具有的特性:无限存不阻塞,无限取不阻塞,而不用关心管道内是否为空,也不用关心是否有内容写入引用文件描述符: &3可以执行n次echo >&3 往管道里放入n把钥匙

exec命令用法:http://blog.sina.com.cn/s/blog_7099ca0b0100nby8.html

实现:

#!/bin/bash
start_time=`date +%s`              #定义脚本运行的开始时间
[ -e /tmp/fd1 ] || mkfifo /tmp/fd1 #创建有名管道
exec 3<>/tmp/fd1                   #创建文件描述符,以可读(<)可写(>)的方式关联管道文件,这时候文件描述符3就有了有名管道文件的所有特性
rm -rf /tmp/fd1                    #关联后的文件描述符拥有管道文件的所有特性,所以这时候管道文件可以删除,我们留下文件描述符来用就可以了
for ((i=1;i<=10;i++))
do
       echo >&3                   #&3代表引用文件描述符3,这条命令代表往管道里面放入了一个"令牌"
done

for ((i=1;i<=1000;i++))
do
read -u3                           #代表从管道中读取一个令牌
{
       sleep 1  #sleep 1用来模仿执行一条命令需要花费的时间(可以用真实命令来代替)
       echo 'success'$i      
       echo >&3                   #代表我这一次命令执行到最后,把令牌放回管道
}&
done
wait

stop_time=`date +%s`  #定义脚本运行的结束时间

echo "TIME:`expr $stop_time - $start_time`"
exec 3<&-                       #关闭文件描述符的读
exec 3>&-                       #关闭文件描述符的写

运行结果:

[root@iZ94yyzmpgvZ /]# . test2.sh
success4
success6
success7
success8
success9
success5
......
success935
success941
success942
......
success992
[992]   Done                   { sleep 1; echo 'success'$i; echo 1>&3; }
success993
[993]   Done                   { sleep 1; echo 'success'$i; echo 1>&3; }
success994
[994]   Done                   { sleep 1; echo 'success'$i; echo 1>&3; }
success998
success999
success1000
success997
success995
success996
[995]   Done                   { sleep 1; echo 'success'$i; echo 1>&3; }
TIME:101

代码解析以及问题:

两个for循环,第一个for循环10次,相当于在女士公共厕所门口放了10把钥匙,第二个for

循环1000次,相当于1000个人来上厕所,read -u3相当于取走一把药匙,{}里面最后一行代码echo >&3相当于上完厕所送还药匙。

这样就实现了10把药匙控制1000个任务的运行,运行时间为101s,肯定不如方案二快,但是比方案一已经快很多了,这就是队列控制同一时间只有最多10个线程的并发,既提高了效率,又实现了并发控制。

注意:创建一个文件描述符exec 3<>/tmp/fd1 不能有空格,代表文件描述符3有可读(<)可写(>)权限,注意,打开的时候可以写在一起,关闭的时候必须分开关,exec 3<&-关闭读,exec 3>&-关闭写

进程锁

进程锁

防止进程被重复运行

[root@aliyun ~]# cat lock.sh 
#!/bin/bash
lock_file=/tmp/echo1.lock

#判断进程是否正在运行
if [ -f $lock_file ];then
pid=`cat $lock_file`
ps $pid &>/dev/null
[ $? -eq 0 ] && echo "Script1 is running..." && exit 1
#if [ $? -eq 0 ];then
# echo "Script1 is running..."
# exit 1
#fi
fi

#创建锁
echo $$ > $lock_file

echo "lock1 begin..."
sleep 500
echo "lock1 end"

#释放锁
rm -rf $lock_file

systemctl管理脚本

systemctl管理脚本

一 介绍

systemctl脚本存放在:/usr/lib/systemd/,有系统(system)和用户(user)之分

  • 1、/usr/lib/systemd/system #系统服务,开机不需要登陆就能运行的程序(相当于开启自启)
  • 2、/usr/lib/systemd/user #用户服务,需要登录后才能运行的程序

/usr/lib/systemd/目录下又存在两种类型的文件:

  • 1、*.service # 服务unit文件
  • 2、*.target # 开机级别unit

centos7 的每一个服务以。service 结尾,一般分为3部分:【unit】、【service】、【install】

[Unit]   # 主要是服务说明
Description=test   # 简单描述服务
After=network.target # 描述服务类别,表示本服务需要在network服务启动后在启动
Before=xxx.service #表示需要在某些服务启动之前启动,After和Before字段只涉及启动顺序,不涉及依赖关系。

[Service]  # 核心区域
Type=forking     # 表示后台运行模式。
User=user        # 设置服务运行的用户
Group=user       # 设置服务运行的用户组
KillMode=control-group   # 定义systemd如何停止服务
PIDFile=/usr/local/test/test.pid    # 存放PID的绝对路径
Restart=no        # 定义服务进程退出后,systemd的重启方式,默认是不重启
ExecStart=/usr/local/test/bin/startup.sh    # 服务启动命令,命令需要绝对路径
PrivateTmp=true                               # 表示给服务分配独立的临时空间

[Install]  
WantedBy=multi-user.target  # 多用户

字段详细说明

1、Type类型有:

simple(默认):#以Execstart字段启动的进程为主进程

forking:#Execstart 字段以fox()方式启动,,此时父进程将退出,子进程将成为主进程(后台运行),一般都设置为forking

oneshot : #类似于simple,但只执行一次,systemd会等他执行完,才执行其他服务

dbus: #类似于simple,但会等待D—Bus信号后启动

notify: #类似与simple ,但结束后会发出通知信号,然后systemd才启动其他服务

idle: #类似与simple,但要等到其他任务都执行完,才启动该服务

2、EnvironmentFile:指定配置文件,和连词号组合使用,可以避免配置文件不存在的异常。

Environment:
后面接多个不同的shell变量。
例如:
Environment=DATA_DIR=/data/elk
Environment=LOG_DIR=/var/log/elasticsearch
Environment=PID_DIR=/var/run/elasticsearch
EnvironmentFile=-/etc/sysconfig/elasticsearch

连词号(-):在所有启动设置之前,添加的变量字段,都可以加上连词号
表示抑制错误,即发生错误时,不影响其他命令的执行。
比如EnviromentFile=-/etc/sysconfig/xxx表示即使文件不存在,也不会抛异常

3、Killmode的类型

contorl-group (默认) # 当前控制组里所有的子进程都会被杀掉

process : #只杀主进程

mixed: #主进程将收到SIGTERM(终止进程)信号,子进程将收到SIGKILL(无条件终止)信号

none: # 没有进程会被杀掉,只是执行服务的stop命令

4、Restart类型

  no (默认):#退出后无操作

 on-success :#只有正常退出时(退出状态码为0),才会重启

 on-failure: #非正常退出时,重启,包括信号终止,和超时

 on-abnaomal:  #只有信号终止或超时,才会重启

 on-abort : #只有在收到没有捕捉到信号终止时,才会重启

 on-watchdog: #超市退出时,才会重启

 always: #不管什么退出原因,都会重启

 #对于守护进程,推荐使用on-failure

5、RestartSec

表示systemd重启服务之前,需要等待的秒数:RestartSec:30

6、各种Exec*字段

Exec*后面的命令,仅接受‘指令 参数 参数..’格式,不能接受<> |&等特殊字符,很多bash语法也不支持,如果想要支持bash语法,需要设置Tyep=oneshot

# ExecStart:   # 启动服务时执行的命令
# ExecReload:   # 重启服务时执行的命令
# ExecStop:     # 停止服务时执行的命令
# ExecStartPre: # 启动服务前执行的命令
# ExecStartPost:# 启动服务后执行的命令
# ExecStopPost: # 停止服务后执行的命令
# PrivateTmp=True #表示给服务分配独立的临时空间,
# 注意:[Service]部分的启动、重启、停止命令全部要求使用绝对路径,使用相对路径则会报错!

[Service]
Type=forking
PIDFile=/home/developer/web/gunicorn.pid
ExecStart=/usr/local/bin/forever start
ExecReload=/bin/kill -s HUP $MAINPID
ExecStop=/bin/kill -s QUIT $MAINPID
PrivateTmp=true

7、[Install]部分是服务安装的相关设置,可设置为多用户的

[Install]
WantedBy=multi-user.target
# WantedBy字段:
# multi-user.target: # 表示多用户命令行状态,这个设置很重要
# graphical.target: # 表示图形用户状体,它依赖于multi-user.target

修改配置文件以后,以754的权限保存在/usr/lib/systemd/system目录下,需要重新加载配置文件方可生效 $ systemctl daemon-reload

这时就可以利用systemctl进行配置了

首先,使用systemctl start [服务名(也是文件名)]可测试服务是否可以成功运行,如果不能运行则可以使用systemctl status [服务名(也是文件名)]查看错误信息和其他服务信息,然后根据报错进行修改,直到可以start,如果不放心还可以测试restart和stop命令。

接着,只要使用systemctl enable xxxxx就可以将所编写的服务添加至开机启动即可。

二 实操

编写脚本如下,并且保证脚本有可执行权限

[root@aliyun ~]# cat nginx.sh 
#!/bin/bash

. /etc/init.d/functions
args=$1

fun(){
  [ $? -eq 0 ] && action "Nginx $args is " /bin/true  || echo "Nginx $args is " /bin/false
}

case $1 in
  start)
      netstat -lntup|grep  ":8080\b" &>/dev/null
      if [ $? -eq 0 ]
      then
         echo "Nginx is runing..."
      else
          /usr/local/nginx/sbin/nginx
          fun
      fi
      ;;
  stop)
      /usr/local/nginx/sbin/nginx -s stop
      fun
      ;;
  reload)
      /usr/local/nginx/sbin/nginx -s reload
      fun
      ;;
 restart)
      netstat -lntup|grep  ":8800\b" &>/dev/null
      if [ $? -ne 0 ]
      then
         /usr/local/nginx/sbin/nginx
        [ $? -eq 0 ] && echo "Nginx start is ok" || echo "Nginx start is failed"
      else
         /usr/local/nginx/sbin/nginx -s stop                            
        [ $? -eq 0 ] && echo "Nginx stop is ok" || echo "Nginx stop is failed"
         sleep 2
         /usr/local/nginx/sbin/nginx
         fun
      fi
      ;;
  status)
      netstat -lntup|grep  ":8080\b" &>/dev/null
      if [ $? -eq 0 ]
      then
         echo "Nginx is runing ..."
      else
         echo "Nginx is not runing ..."
      fi
      ;;
   *)
       echo "Usage: $0 {start|stop|status|restart|reload}"
       exit 2
esac

[root@aliyun ~]# chmod +x nginx.sh

配置

[root@aliyun ~]# cat /usr/lib/systemd/system/nginx.service 
[Unit]
Description=Nginx server daemon

[Service]
Type=forking
ExecStart=/root/nginx.sh start
ExecStop=/root/nginx.sh stop
ExecReload=/root/nginx.sh reload
PrivateTmp=true

[Install]
WantedBy=multi-user.target

重新加载

systemctl daemon-reload

测试

[root@aliyun ~]# systemctl start nginx
[root@aliyun ~]# systemctl status nginx
● nginx.service - Nginx server daemon
   Loaded: loaded (/usr/lib/systemd/system/nginx.service; disabled; vendor preset: disabled)
   Active: active (running) since Mon 2020-08-24 00:10:44 CST; 1s ago
  Process: 4166 ExecStart=/root/nginx.sh start (code=exited, status=0/SUCCESS)
 Main PID: 4173 (nginx)
   CGroup: /system.slice/nginx.service
           ├─4173 nginx: master process /usr/local/nginx/sbin/nginx
           └─4175 nginx: worker process

Aug 24 00:10:44 aliyun systemd[1]: Starting Nginx server daemon...
Aug 24 00:10:44 aliyun nginx.sh[4166]: Nginx start is  [  OK  ]
Aug 24 00:10:44 aliyun systemd[1]: Started Nginx server daemon.
[root@aliyun ~]# systemctl reload nginx
[root@aliyun ~]# systemctl status nginx
● nginx.service - Nginx server daemon
   Loaded: loaded (/usr/lib/systemd/system/nginx.service; disabled; vendor preset: disabled)
   Active: active (running) since Mon 2020-08-24 00:10:44 CST; 13s ago
  Process: 4186 ExecReload=/root/nginx.sh reload (code=exited, status=0/SUCCESS)
  Process: 4166 ExecStart=/root/nginx.sh start (code=exited, status=0/SUCCESS)
 Main PID: 4173 (nginx)
   CGroup: /system.slice/nginx.service
           ├─4173 nginx: master process /usr/local/nginx/sbin/nginx
           └─4193 nginx: worker process

Aug 24 00:10:44 aliyun systemd[1]: Starting Nginx server daemon...
Aug 24 00:10:44 aliyun nginx.sh[4166]: Nginx start is  [  OK  ]
Aug 24 00:10:44 aliyun systemd[1]: Started Nginx server daemon.
Aug 24 00:10:56 aliyun systemd[1]: Reloading Nginx server daemon.
Aug 24 00:10:56 aliyun nginx.sh[4186]: Nginx reload is  [  OK  ]
Aug 24 00:10:56 aliyun systemd[1]: Reloaded Nginx server daemon.
[root@aliyun ~]# 

三 练习

把下述脚本sync.sh添加到systemctl

#!/bin/bash

case $1 in
start)
ps -ef|grep [s]ersync &>/dev/null
if [ $? -eq 0 ]
then
action "sersync is running..."   /bin/true
else
/usr/local/sersync/bin/sersync -dro /usr/local/sersync/conf/confxml.xml &>/dev/null
[ $? -eq 0 ] && action "sersync start is " /bin/true || action "sersync start is" /bin/false
fi
;;
stop)
ps -ef|grep [s]ersync &>/dev/null
if [ $? -eq 0 ]
then
Pid_num=$(ps -ef|grep [s]ersync|awk '{print $2}')
kill $Pid_num
ps -ef|grep [s]ersync &>/dev/null
[ $? -ne 0 ] && action "sersync stop is"  /bin/true  || action "sersync stop is"  /bin/false
else
action "sersync is not runing ... "  /bin/false
fi
esac

测试





[root@web01 ~]# cat /usr/lib/systemd/system/rs.service
[Unit]
Description=Hello Chenyang, Hello China!
After=network.target

[Service]
Type=forking
PIDFile=/var/run/redis_6379.pid
ExecStart=/etc/redis.sh start
ExecReload=/etc/redis.sh restart
ExecStop=/etc/redis.sh stop

[Install]
WantedBy=multi-user.target

[root@web01 ~]# cat /etc/redis.sh

!/bin/bash

. /etc/init.d/functions

args=$1

function print_echo(){

[ $2 -eq 0 ] && action “Redis $1 is Success!” /bin/true || action “Redis $1 is Fiald!” /bin/false

}

case $args in

start)
netstat -nutlp | grep -Eq “\<6379>”

status=$?

[ $status -ne 0 ] && /usr/bin/redis-server /etc/redis.conf && status=$?

print_echo $args $status
;;

restart)
netstat -nutlp | grep -Eq “\<6379>”

# PID=ps -ef | grep -E "\<[6]379\>" | awk '{print $2}'

[ $? -eq 0 ] && PID=ps -ef | grep -E "\<[6]379\>" | awk '{print $2}' && kill -s TERM $PID

sleep 2

/usr/bin/redis-server /etc/redis.conf

print_echo $args $?
;;

stop)

PID=ps -ef | grep -E "\<[6]379\>" | awk '{print $2}'

# [ $PID -eq 0 ] &&
kill -s TERM $PID

;;

*)

echo “Please have (start|stop|restart)”

esac

三剑客之sed命令

三剑客之sed命令

一 awk简介

awk命名源自于它的三大作者名字的首字母,分别是Alfred Aho、Brian Kernighan、Peter Weinberger。(gawk是awk的GNU版本,它提供了Bell实验室和GNU的一些扩展)。

awk 是一种编程语言,用于在linux/unix下对文本和数据进行处理。数据可以来自标准输入、一个 或多个文件,或其它命令的输出。它支持用户自定义函数和动态正则表达式等先进功能,是linux/unix 下的一个强大编程工具。它在命令行中使用,但更多是作为脚本来使用。 ​ awk的处理文本和数据的方式是这样的,它逐行扫描文件,从第一行到最后一行,寻找匹配的特定 模式的行,并在这些行上进行你想要的操作。如果没有指定处理动作,则把匹配的行显示到标准输出( 屏幕),如果没有指定模式,则所有被操作所指定的行都被处理

awk的两种语法格式

awk [options] 'commands' filename
awk [options] -f awk-script-file filename

awk选项options

-F      定义字段分隔符,默认的分隔符是空格或制表符(tab)

awk的命令commands总共由三部分组成

BEGIN{}                 {}                 END{}
读所有行之前做的事情       读一行处理一行   所有读完之后要做的事情

可以省略BEGIN{} 和END{},只进行{}行处理,并且{}行处理前可以加匹配,匹配成功后再处理

awk 'pattern' filename              示例:awk -F: '/root/' /etc/passwd     
awk '{action}' filename 示例:awk -F: '{print $1}' /etc/passwd
awk 'pattern{action}' filename 示例:awk -F: '/root/{print $1,$3}' /etc/passwd
示例:awk 'BEGIN{FS=":"} /root/{print $1,$3}' /etc/passwd
其他命令 |awk 'pattern'
其他命令 |awk '{action}'
其他命令 |awk 'pattern{action}'

# 匹配pattern可以是:/正则表达式/也可以是条件,如下
示例:df -P |awk '$4 > 999999{print $0}'  # 也可以省略{print $0}

模式pattern还可以是其他,详解第五章节

二 awk工作原理

awk -F: '{print $1,$3}' /etc/passwd
   
(1)awk会接收一行作为输入,并将这一行赋给awk的内部变量$0,每一行也可称为一个记录,行的边界是以换行符作为结束

(2)然后,刚刚读入的行被以:为分隔符分解成若干字段(或域),每个字段存储在已编号的变量中,编号从$1开始,最多达100个字段
注意:如果未指定行分隔符,awk将使用内置变量FS的值作为默认的行分隔符,FS默认值为空格

(3)使用print函数打印,如果$1$3之间没有逗号,它俩在输出时将贴在一起,应该在$1,$3之间加逗号,该逗号与awk的内置变量OFS保持一致,OFS默认为空格,于是以空格为分隔符输出$1和$3
我们可以指定:awk -F: 'BEGIN{OFS="-"}{print $1,$3}' /etc/passwd

(4)输出之后,将从文件中获取另一行,然后覆盖给$0,继续(2)的步骤将该行内容分隔成字段。。。继续(3)的步骤
该过程一直持续到所有行处理完毕

三 记录与字段相关内部变量

$0: 保存当前行的内容                # awk -F: '{print $0}' /etc/passwd
NR: 记录号,每处理完一条记录,NR值加1  # awk -F: '{print NR, $0}' /etc/passwd
NF: 保存记录的字段数,$1,$2...$100 # awk -F: '{print $0,NF}' /etc/passwd
FS: 输入字段分隔符,默认空格 # awk -F: '/alice/{print $1, $3}' /etc/passwd
# awk -F'[ :\t]' '{print $1,$2,$3}' /etc/passwd
# awk 'BEGIN{FS=":"} {print $1,$3}' /etc/passwd
OFS:输出字段分隔符 # awk -F: '/root/{print $1,$2,$3,$4}' /etc/passwd
# awk -F: 'BEGIN{OFS="+++"} /^root/{print $1,$2,$3,$4}' /etc/passwd
    # awk 'BEGIN{OFS="-";FS=":"}/root/{print NR,$0,NF}' /etc/passwd

四 格式化输出

================print函数===================
[root@egon ~]# date | awk '{print "月:",$2,"\n年:",$1}'
月: 09月
年: 2020年
[root@egon ~]#
[root@egon ~]# awk -F: '{print "用户名:",$1,"用户id:",$3}' /etc/passwd

================printf函数===================
[root@egon ~]# awk -F: '{printf "用户名:%s 用户id:%s\n",$1,$3}' /etc/passwd
[root@egon ~]# awk -F: '{printf "|%-15s| %-10s| %-15s|\n", $1,$2,$3}' /etc/passwd

%s 字符类型
%d 数值类型
占15格的字符串
- 表示左对齐,默认是右对齐
printf默认不会在行尾自动换行,加\n

五 模式pattern与动作action

awk ‘pattern{action}’ filename

模式pattern可以是

  • 正则表达式# 匹配整行
    awk -F: ‘/egon/{print $1,$3}’ /etc/passwd    
    awk ‘/^root/’  /etc/passwd

    # 匹配一行的某个字段
    # awk ‘$0 ~ /^root/’ /etc/passwd
    # awk ‘$1 ~ /^root/’ /etc/passwd
    # awk ‘$7 !~ /bash$/’ /etc/passwd
  • 比较表达式比较表达式指的是使用关系运算符来比较数字以及字符串,只有当条件为真,才执行指定的动作

    关系运算符
    运算符  含义 示例
    < 小于  x<y
    <= 小于或等于 x<=y
    == 等于  x==y
    != 不等于     x!=y
    >= 大于等于 x>=y
    > 大于  x>y
    ~ 正则表达式匹配   x~/y/
    !~ 正则表达式不匹配  x!~/y/


    示例:
    # awk -F: ‘$3 == 0’ /etc/passwd
    # awk -F: ‘$3 < 10’ /etc/passwd
    # awk -F: ‘$7 == “/bin/bash”‘ /etc/passwd
    # awk -F: ‘$1 == “root” ‘ /etc/passwd
  • 条件表达式# awk -F: ‘{if($3>300) {print $0}}’ /etc/passwd
    # awk -F: ‘{if($3>300) {print $3} else{print $1}}’ /etc/passwd

    # awk -F: ‘{if($3>300) {max=$3;print max} else{max=$1;print max}}’ /etc/passwd
    # awk -F: ‘{max=($3>300) ? $3 : $1; print max}’ /etc/passwd

    # awk -F: ‘{if($3>$4) {max=$3;print max} else{max=$4; print max}}’ /etc/passwd
    # awk -F: ‘{max=($3 > $4) ? $3: $4; print max}’ /etc/passwd
    相当于:
    if ($3 > $4)
    max=$3
    else
    max=$4
  • 算数运算+ – * / %(模) ^(幂2^3)
    可以在模式中执行计算,awk都将按浮点数方式执行算术运算

    # awk -F: ‘$3 * 10 > 500’ /etc/passwd
  • 逻辑运算和复合模式&& 逻辑与 a&&b
    || 逻辑或 a||b
    ! 逻辑非 !a

    示例:
    # awk ‘$2 > 5 && $2 <= 15’ filename
    # awk ‘$3 == 100 || $4 > 50’ filename
    # awk ‘!($2 < 100 && $3 < 20)’ filename
  • 范围模式# 正则
    awk ‘/root/,/egon/’ filename

    说明:
    awk将显示从root首次出现的行到egon首次出现的行这个范围内的所有行,包括两个边界在内。如果没有找到egon,awk将继续打印各行直至文件末尾。

    如果打印完root到egon的内容之后,又出现了root, awk就又一次开始显示行,直至找到下一个egon或文件末尾。

    [root@aliyun ~]# cat a.txt
    1111root
    2222root22222
    egon123123123123
    4444
    5555
    6666
    root7777
    1asf
    asdfasdf
    egon
    7788
    [root@aliyun ~]# awk ‘/root/,/egon/{print NR,$0}’ a.txt
    1 1111root
    2 2222root22222
    3 egon123123123123
    7 root7777
    8 1asf
    9 asdfasdf
    10 egon
    [root@aliyun ~]#

    # 行号
    awk -F: ‘NR>=1 && NR <=3{print $1}’ test.txt

六 awk示例

# awk '/west/' datafile
# awk '/^north/' datafile
# awk '/^(no|so)/' datafile
# awk '{print $3,$2}' datafile
# awk '{print $3 $2}' datafile
# awk '{print $0}' datafile
# awk '{print "Number of fields: "NF}' datafile
# awk '/northeast/{print $3,$2}' datafile
# awk '/E/' datafile
# awk '/^[ns]/{print $1}' datafile
# awk '$5 ~ /\.[7-9]+/' datafile
# awk '$2 !~ /E/{print $1,$2}' datafile
# awk '$3 ~ /^Joel/{print $3 " is a nice guy."}' datafile
# awk '$8 ~ /[0-9][0-9]$/{print $8}' datafile
# awk '$4 ~ /Chin$/{print "The price is $" $8 "."}' datafile
# awk '/Tj/{print $0}' datafile
# awk '{print $1}' datafile2
# awk -F: '{print $1}' datafile2
# awk '{print "Number of fields: "NF}' datafile2
# awk -F: '{print "Number of fields: "NF}' datafile2
# awk -F"[ :]" '{print $1,$2}' datafile2
 
# awk '$7 == 5' datafile
# awk '$2 == "CT" {print $1, $2}' datafile
# awk '$7 != 5' datafile
# awk '$7 < 5 {print $4, $7}' datafile
# awk '$6 > .9 {print $1,$6}' datafile
# awk '$8 <= 17 {print $8}' datafile
# awk '$8 >= 17 {print $8}' datafile
# awk '$8 > 10 && $8 < 17' datafile
# awk '$2 == "NW" || $1 ~ /south/ {print $1, $2}' datafile
# awk '!($8 == 13){print $8}' datafile
# awk '/southem/{print $5 + 10}' datafile

# awk '/southem/{print $8 + 10}' datafile
# awk '/southem/{print $5 + 10.56}' datafile
# awk '/southem/{print $8 - 10}' datafile
# awk '/southem/{print $8 / 2 }' datafile
# awk '/southem/{print $8 / 3 }' datafile
# awk '/southem/{print $8 * 2 }' datafile
# awk '/southem/{print $8 % 2 }' datafile

# awk '$3 ~ /^Suan/ {print "Percentage: "$6 + .2   " Volume: " $8}' datafile
# awk '/^western/,/^eastern/' datafile
# awk '{print ($7 > 4 ? "high "$7 : "low "$7)}' datafile //条件运算符
# awk '$3 == "Chris" {$3 = "Christian"; print}' datafile //赋值运算符
# awk '/Derek/ {$8 += 12; print $8}' datafile //$8 += 12等价于$8 = $8 + 12
# awk '{$7 %= 3; print $7}' datafile //$7 %= 3等价于$7 = $7 % 3

七 awk流程控制

==条件判断
if语句:
格式
{if(表达式){语句;语句;...}}
awk -F: '{if($3==0) print $1 " is administrator."}' /etc/passwd
awk -F: '{if($3>0 && $3<500){count++; print $1}} END{print count}' /etc/passwd //统计系统用户数

if...else语句:
格式
{if(表达式){语句;语句;...}else{语句;语句;...}}
awk -F: '{if($3==0){print $1} else {print $7}}' /etc/passwd
awk -F: '{if($3>0) {count++} else{i++}' /etc/passwd
awk -F: '{if($3>0){count++} else{i++}} END{print "管理员个数: "i "\n系统用户数: "count}' /etc/passwd

if...else if...else语句:
格式
{if(表达式){语句;语句;...}else if(表达式){语句;语句;...}else if(表达式){语句;语句;...}else{语句;语句;...}}
awk -F: '{if($3==0){i++} else if($3>499){k++} else{j++}} END{print i; print k; print j}' /etc/passwd
awk -F: '{if($3==0){i++} else if($3>499){k++} else{j++}} END{print "管理员个数: "i; print "普通用个数: "k; print "系统用户: "j}' /etc/passwd


==循环
while:
awk -F: '{i=1; while(i<=10) {print $0; i++}}' /etc/passwd //将每行打印10次

for:
awk -F: '{for(i=1;i<=10;i++) print $0}' /etc/passwd //将每行打印10次


==数组(索引或key对应值)
# awk -F: '{username[++i]=$1} END{print username[1]}' /etc/passwd
root
# awk -F: '{username[i++]=$1} END{print username[1]}' /etc/passwd
bin
# awk -F: '{username[i++]=$1} END{print username[0]}' /etc/passwd
root

# awk -F: '{username[x++]=$1} END{for(i=0;i<NR;i++) print i,username[i]}' /etc/passwd
0 root
1 bin
2 daemon
3 adm
4 lp
5 sync
6 shutdown
7 halt
...
# awk -F: 'BEGIN{x=1} {user[x++]=$1} END{for(i=1;i<=NR;i++) {print i,user[i]} }' /etc/passwd
# awk -F: 'BEGIN{j=1} {if($3<5){user[j++]=$1}}   END{for(i=1;i<j;i++) {print i,user[i]} }' /etc/passwd

# awk -F: 'BEGIN{i=1} {username[i]=$1;i++}'

# awk -F: 'BEGIN{i=1} $3<10{username[i]=$1;++i} END{for(j=1;j<i;j++){print j,username[j]}}' /etc/passwd
1 root
2 bin
3 daemon
4 adm
5 lp
6 sync
7 shutdown
8 halt
9 mail
10 admin

========================================================
# awk -F: '{username[++x]=$1} END{for(i=1;i<=NR;i++) {print i,username[i]}}' passwd1
1 root
2 bin
3 daemon
4 adm
5 lp
6 sync
7 shutdown
8 halt
9 mail
10 uucp

# awk -F: '{username[++x]=$1} END{for(i in username) {print username[i]} }' passwd1
adm
lp
sync
shutdown
halt
mail
uucp
root
bin
daemon

》》》》》》》》》》》》key:value《《《《《《《《《《《《
# awk -F: '{user_id[$1]=$3} END{for(i in user_id) {print i,user_id[i]}}' passwd1
bin 1
uucp 10
mail 8
sync 5
shutdown 6
adm 3
daemon 2
halt 7
root 0
lp 4


========================================================

统计用户名为4个字符的用户:
[root@aliyun ~]# awk -F: '$1~/^....$/{count++; print $1} END{print "count is: " count}' /etc/passwd
root
sync
halt
mail
news
uucp
nscd
vcsa
pcap
sshd
dbus
jack
count is: 12


[root@aliyun ~]# awk -F: 'length($1)==4{count++; print $1} END{print "count is: "count}' /etc/passwd
root
sync
halt
mail
news
uucp
nscd
vcsa
pcap
sshd
dbus
jack
count is: 12

作业

1. 取得网卡IP(除ipv6以外的所有IP)
2. 获得内存使用情况
3. 获得磁盘使用情况
4. 清空本机的ARP缓存
5. 打印出/etc/hosts文件的最后一个字段(按空格分隔)
6. 打印指定目录下的目录名


[root@aliyun dir1]# arp -a |awk -F"[()]" '{print "arp -d", $2}'
arp -d 192.168.2.26
arp -d 192.168.2.44
arp -d 192.168.2.28
arp -d 192.168.2.130
arp -d 192.168.2.90
arp -d 192.168.2.18
arp -d 192.168.2.129
[root@aliyun dir1]# arp -a |awk -F"[()]" '{print "arp -d " $2}' |sh


[root@aliyun ~]# awk -F: '{print $7}' /etc/passwd
[root@aliyun ~]# awk -F: '{print $NF}' /etc/passwd
[root@aliyun ~]# awk -F: '{print $(NF-1)}' /etc/passwd

[root@aliyun ~]# ll |grep '^d'
drwxr-xr-x 104 root root     12288 09-22 05:37 192.168.0.48
drwxr-xr-x   2 root root      4096 10-30 15:47 apache_log
drwxr-xr-x   2 root root      4096 10-30 15:23 awk
drwxr-xr-x   2 root root      4096 10-24 09:09 Desktop
drwxr-xr-x  12 root root      4096 10-08 06:12 LEMP_Soft
drwxr-xr-x   2 root root      4096 10-24 07:38 scripts
drwxr-xr-x   6 root root      4096 2012-03-29 uplayer
drwxr-xr-x   7 root root      4096 10-23 04:53 vmware
[root@aliyun ~]#
[root@aliyun ~]# ll |grep '^d' |awk '{print $NF}'
192.168.0.48
apache_log
awk
Desktop
LEMP_Soft
scripts
uplayer
vmware



awk脚本:
user1.awk
BEGIN {
       FS=":"
}

{
       if($3==0){
               print $1
      }
       else{
               print $7
      }
}


user2.awk
BEGIN{
       FS=":"
       OFS="\t\t"
       print "username\tuid"
       print "-------------------"
}

{if($3==0){
       print $1,$3;i++
      }

}

END{
       print "-------------------"
       print "total users is: "i
}

八 练习题

已知一个变量 msg="I am a teacher, my name is egon",打印字符长度小于3的单词

# 方式一:
[root@egon /]# for i in $msg;do [ ${#i} -lt 3 ] && echo $i;done
I
am
a
my
is

# 方式二:
[root@egon /]# echo $msg |xargs -n1 |awk '{if(length<3) print}'
I
am
a
my
is

# 方式三:
[root@egon /]# echo $msg |awk '{for(i=1;i<=NF;i++) if(length($i)<3) print $i}'
I
am
a
my
is

# 方式四:
[root@egon /]# echo $msg |egrep -wo '[a-z]{1,3}'
am
a
my
is

三剑客之sed命令

三剑客之sed命令

一 sed介绍

sed全称(stream editor)流式编辑器,Sed主要用来自动编辑一个或多个文件、简化对文件的反复操作、编写转换程序等,工作流程如下

sed 是一种在线的、非交互式的编辑器,它一次处理一行内容。处理时,把当前处理的行存储在
临时缓冲区中,称为“模式空间”(pattern space),接着用sed命令处理缓冲区中的内容,处理完
成后,把缓冲区的内容送往屏幕。接着处理下一行,这样不断重复,直到文件末尾。文件内容并没有
改变,除非你使用重定向存储输出,或者使用sed -i选项
-i选项就是将本该输出到屏幕上的内容输出/流入文件中

sed命令格式如下

sed [options] 'command' file(s)
sed [options] -f scriptfile file(s)

# 注:
sed和grep不一样,不管是否找到指定的模式,它的退出状态都是0
只有当命令存在语法错误时,sed的退出状态才不是0

二 sed选项与基本用法示例

###2.1 sed选项

选项             功能
-e 允许多项编辑
-n 取消默认的输出(模式空间的内容输出)
-i inplace,就地编辑
-r 支持扩展元字符
-f 指定sed脚本文件名

示例
# sed -r '' /etc/passwd
# sed -r 'p' /etc/passwd
# sed -r -n 'p' /etc/passwd


文件的一行行内容相当与水流,连续两个-e就是设置了两道关卡
[root@aliyun ~]# sed '' test.txt
1111111
2222222egon
333333egon
444444egon
555555eon
[root@aliyun ~]# sed -e '3d' -e '1d' test.txt  
2222222egon
444444egon
555555eon
[root@aliyun ~]# sed -rn -e '1,3d' -e 'p' test.txt
444444egon
555555eon
[root@aliyun ~]#

也可以将多道关卡写入一个文件中
[root@aliyun ~]# cat sed.txt
1,3d
p
[root@aliyun ~]# sed -rn -f sed.txt test.txt
444444egon
555555eon
[root@aliyun ~]#

###2.2 sed命令组成

命令由”地址+命令“两部分组成,命令如p、d,更多详解第三章节,本节我们主要介绍地址

地址用于决定对流入模式空间的哪些行进行编辑,如果没有指定地址,sed将处理流入模式空间的所有行。

地址可以是

  • 1、数字sed -n ‘p’ /etc/passwd
    sed -n ‘1,3p’ /etc/passwd
    sed ‘1,47d’ /etc/passwd
  • 2、正则表达式与grep一样,sed在文件中查找模式时也可以使用正则表达式(RE)和各种元字符。正则表达式是
    括在斜杠间的模式,用于查找和替换,以下是sed支持的元字符。

    # 使用基本元字符集
    ^, $, ., *, [], [^], \< \>,\(\),\{\}

    # 使用扩展元字符集
    ?, +, { }, |, ( )

    # 使用扩展元字符的方式:
    转义,如\+
    -r参数,如sed -r

    [root@aliyun ~]# cat test.txt
    1111111
    2222222egon
    333333egon
    444444egon
    555555eon
    [root@aliyun ~]# sed -rn ‘/egon/p’ test.txt
    2222222egon
    333333egon
    444444egon
    [root@aliyun ~]#
  • 3、数字+正则表达式[root@aliyun ~]# cat test.txt
    1111111
    2222222egon
    333333egon
    444444egon
    555555eon
    [root@aliyun ~]# sed -rn ‘1,/egon/p’ test.txt
    1111111
    2222222egon
    [root@aliyun ~]#

    解释:
    # “1,8p”代表打印1到8行,”1,/egon/p”则代表取从第1行到首次匹配到/egon/的行

2.3 \cregexpc

地址可以是正则表达式,而正则表达式需要放置在\c与c中间,其中c可以是任意字符,但必须要加\转义

[root@aliyun ~]# cat test.txt 
1111111
2222222egon
333333egon
444444egon
555555eon
[root@aliyun ~]# sed -rn '#egon#p' test.txt
[root@aliyun ~]# sed -rn '\#egon#p' test.txt
2222222egon
333333egon
444444egon
[root@aliyun ~]#

如果c是左斜杠,不需要转义也可以

[root@aliyun ~]# sed -rn '\/egon/p' test.txt 
2222222egon
333333egon
444444egon
[root@aliyun ~]# sed -rn '/egon/p' test.txt
2222222egon
333333egon
444444egon
[root@aliyun ~]#

如果匹配的正则里有左斜杠,要么将正则转义,要么将c转义

[root@aliyun ~]# cat a.txt 
/etc/egon/666
etc
[root@aliyun ~]# sed -rn '//etc/egon/666/p' a.txt # 错误
sed: -e expression #1, char 0: no previous regular expression
   
[root@aliyun ~]# sed -rn '/\/etc\/egon\/666/p' a.txt # 正则转义
/etc/egon/666

[root@aliyun ~]# sed -rn '#/etc/egon/666#p' a.txt # 转义c,必须是\c
[root@aliyun ~]# sed -rn '\#/etc/egon/666#p' a.txt # 转义c
/etc/egon/666
[root@aliyun ~]#


# 示例
[root@aliyun ~]# cat a.txt
/etc/egon/666
etc
[root@aliyun ~]# sed -ri '/\/etc\/egon\/666/s/.*/xxx/' a.txt
[root@aliyun ~]# cat a.txt
xxx
etc
[root@aliyun ~]#

三 sed常用命令

sed命令告诉sed对指定行进行何种操作,包括打印、删除、修改等。

命令             功能
a 在当前行后添加一行或多行
c 用新文本修改(替换)当前行中的文本
d 删除行
i 在当前行之前插入文本
l 会用$符号标识出文件中看不到的字符的位置
p 打印行
n 把下一行内容读入模式空间,后续的处理命令处理的都是刚读入的新内容
q 结束或退出sed,不会将后续内容读入模式空间
r 从文件中读
! 对所选行以外的所有行应用命令
s 用一个字符串替换另一个
w 将行写入文件
y 将字符转换为另一字符(不支持正则表达式),y/egon/1234/  e->1 g->2 o->3 n->4

h 把模式空间里的内容复制到暂存缓冲区(覆盖)
H 把模式空间里的内容追加到暂存缓冲区
g 取出暂存缓冲区的内容,将其复制到模式空间,覆盖该处原有内容
G 取出暂存缓冲区的内容,将其复制到模式空间,追加在原有内容后面
x 交换暂存缓冲区与模式空间的内容

替换标志 s
g 在行内进行全局替换
i 忽略大小写

sed命令示例

打印命令:p
# sed -r "/egon/p" a.txt
# sed -r -n "/egon/p" a.txt

删除命令:d,注意用单引号
# sed -r '3d' a.txt
# sed -r '3,$d' a.txt
# sed -r '$d' a.txt
# sed -r '/egon/d' a.txt
# sed -r '1,/egon/{/egon/d}' a.txt # 只删除模式匹配成功的第一行


[root@egon ~]# cat a.txt
Egon111111
egon222222
333Egon333
444444egon
5555555555
6666666666
egon777777
8888888888
[root@egon ~]#
[root@egon ~]# sed -r '/egon/d' a.txt # 只删除模式匹配成功的所有行
Egon111111
333Egon333
5555555555
6666666666
8888888888
[root@egon ~]# sed -r '1,/egon/{/egon/d}' a.txt # 只删除模式匹配成功的第一行
Egon111111
333Egon333
444444egon
5555555555
6666666666
egon777777
8888888888


替换命令:s
# sed -r 's/egon/Bigegon/' a.txt
# sed -r 's/egon/Bigegon/g' a.txt
# sed -r 's/^egon/Bigegon/g' a.txt
# sed -r -n 's/root/egon/gip' /etc/passwd
# sed -r 's/[0-9]$/&.change/' a.txt # &代表取到匹配成功的整行内容

# sed -r 's/^([a-zA-Z]+)([^[a-zA-Z]+)/\2\1/' a.txt
# sed -r 's#egon#bigegon#g' a.txt

多重编辑命令:e
# sed -r -e '1,3d' -e 's/[Ee]gon/EGON/g' a.txt # 在前一个-e的基础之上进行第二个-e操作
# sed -r '1,3d;s/[Ee]gon/EGON/g' a.txt

# sed -r '3{s/[0-9]/x/g;s/[Ee]gon/EGON/g}' a.txt # 只处理第三行
# sed -r '1,3{s/[0-9]/x/g;s/[Ee]gon/EGON/g}' a.txt # 处理1到3行

# sed -r -n '1p;p' a.txt # ;分隔依次运行,先针对第一行进行p操作,再针对所有行进行p操作
# sed -r -n '1{p;p}' a.txt # 只针对第一行,连续进行两次p操作

反向选择!
# sed -r '3d' a.txt
# sed -r '3!d' a.txt


读文件命令:r
# sed -r '/^Egon/r b.txt' a.txt # 在匹配成功的行后添加文件b.txt的内容
# sed -r '/2/r b.txt' a.txt # 在第2行后面添加文件b.txt的内容

写文件命令:w
# sed -r '/[Ee]gon/w b.txt' a.txt # 将匹配成功的行写入新文件b.txt
# sed -r '3,$w /root/new.txt' a.txt # 将第3行到最后一行写入/root/new.txt

追加命令:a
# sed -r '2aXXXXXXXXXXXXXXXXXXXX' a.txt # 在第2行后添加一行
# sed -r '2a1111111111111\               # 可以用\续行
> 222222222222\
> 333333333333' a.txt

插入命令:i
# sed -r '2i1111111111111' /etc/hosts
# sed -r '2i111111111\
> 2222222222\
> 3333333333' a.txt

修改命令:c
# sed -r '2c1111111111111' a.txt
# sed -r '2c111111111111\
> 22222222222\
> 33333333333' a.txt

把下一行内容读入模式空间:n
# sed -r '/^Egon/{n;s/[0-9]/x/g}' a.txt # 将匹配/^Egon/成功的行的下一行读入模式空间进行s处理
[root@aliyun ~]# cat a.txt
/etc/egon/666
etc
[root@aliyun ~]# sed -r '\#/etc/egon/666#n;c 1111' a.txt
/etc/egon/666
1111
[root@aliyun ~]#

转换命令:y
# sed -r '1,3y/Eeo/12X/' a.txt # 1到3行进行转换 对应规则:a->1 e->2 o->X

退出:q
# sed -r '5q' a.txt
# sed -r '/[Ee]gon/{ s/[0-9]/X/; q; }' a.txt # 匹配成功/[Ee]gon/则执行{}内命令,q代表退出,即替换一次则退出,如果文件中多行符合规则的内容也只替换了第一个

四 模式空间与保持空间

sed 有两个内置的存储空间:

  • 模式空间(pattern space): 如你所知,模式空间用于 sed 执行的正常流程中。该空间 sed 内置的一个缓冲区,用来存放、修改从输入文件读取的内容。
  • 保持空间(hold space): 保持空间是另外一个缓冲区,用来存放临时数据。Sed 可以在保持空间和模式空间交换数据,但是不能在保持空间上执行普通的 sed 命令。

我们已经讨论过,每次循环读取数据过程中,模式空间的内容都会被清空,然而保持空间的内容则保持不变,不会在循环中被删除。

模式空间与保持空间的操作命令

x:命令x(exchange) 用于交换模式空间和保持空间的内容

h:模式空间复制/覆盖到保持空间
H:模式空间追加到保持空间

g:保持空间复制/覆盖到模式空间
G:保持空间追加到模式空间

n:读取下一行到/覆盖到模式空间
N:将下一行添加到模式空间


d:删除pattern space中的所有行,并读入下一新行到pattern space中

示例:交换文件的行

[root@egon ~]# cat test.txt 
1111
2222
3333

# ======================方式1:======================
[root@egon ~]# tac test.txt
3333
2222
1111
[root@egon ~]#

# ======================方式2:======================
思路:
# 1、读取文件第一行内容到模式空间,进行的操作如下  
# 将模式空间内容覆盖到保持空间
# 删除模式空间内容
 
# 2、读取文件第二行内容到模式空间,进行的操作如下  
# 将保持内容追加到模式空间
# 将模式空间内容覆盖到保持空间
# 删除模式空间内容

# 3、读取文件第三行内容到模式空间,进行的操作如下  
# 将保持空间内容追加到模式空间

实现:
sed -r '1h;1d;2G;2h;2d;3G' test.txt
或者
sed '1!G;h;$!d' test.txt

五 sed脚本

sed脚本就是写在文件中的一系列sed命令,使用-f 选项指定sed脚本文件名,需要注意的问题如下

  • 脚本末尾不能有任何多余的空格或文本
  • 如果命令不能独占一行,就必须以\结尾
  • 脚本中不能使用引号,除非它们是查找串的一部分
  • 反斜杠起到续行的作用
[root@egon ~]# cat sed.sh #永久存储,存了多行sed命令,相当于多道关卡,每读入一行内容将经历一道道关卡
1h;1d;2G;2h;2d;3G
1h;1d;2G;2h;2d;3G

[root@egon ~]# sed -r '' a.txt
1111
2222
3333
[root@egon ~]#
[root@egon ~]# sed -r -f sed.sh test.txt
3333
2222
1111
2222
1111
[root@egon ~]#

六 练习


删除配置文件中用井号#注释的行
sed -r -i '/^#/d' file.conf
sed -r -i '/^[ \t]*#/d' file.conf

删除配置文件中用双斜杠//注释的行
sed -r -i '\c//cd' file.conf

删除无内容空行
sed -r '/^$/d' file.conf
sed -r '/^[\t]*$/d' file.conf
sed -r '/^[ \t]*$/d' file.conf


示例:
# 删除#号注释和无内容的空行
sed -r -i '/^[ \t]*#/d; /^[ \t]*$/d' /etc/vsftpd/vsftpd.conf
sed -r -i '/^[ \t]*#|^[ \t]*$/d' /etc/vsftpd/vsftpd.conf # 同上

追加一行,\可有可无,有更清晰
sed -r -i '$a\chroot_local_user=YES' /etc/vsftpd/vsftpd.conf


给文件每行加注释
sed -r -i 's/^/#/' filename

每指定行加注释
sed -r -i '10,$s/^/#/' filename
sed -r '3,$s/^#*/#/' filename # 将行首连续的零个或多个#换成一个#


sed中使用外部变量
# var1=666
# sed -r 3a$var1 test.txt   # 可以不加引号
# sed -r "3a$var1" test.txt # 也可以加引号,但注意是双引号而不是单引号,因为要用$符号取变量值
# sed -r '3a'"$var1" test.txt # 也可以sed命令用''引起来,而变量用"",注意二者之间不能有空格

三剑客之grep命令

三剑客之grep命令

一 grep介绍

grep命令主要用于过滤文本,grep家族如下

grep: 在文件中全局查找指定的正则表达式,并打印所有包含该表达式的行
egrep:扩展的egrep,支持更多的正则表达式元字符
fgrep:固定grep(fixed grep),有时也被称作快速(fast grep),它按字面解释所有的字符

grep命令格式如下

grep [选项] PATTERN 文件1 文件2 ...

[root@egon ~]# grep 'root' /etc/passwd
[root@egon ~]# fgrep 'bash' /etc/passwd

找到: grep返回的退出状态为0
没找到: grep返回的退出状态为1
找不到指定文件:  grep返回的退出状态为2

grep 命令的输入可以来自标准输入或管道,而不仅仅是文件,例如:

ps aux |grep 'nginx'

二 选项

-n, --line-number           在过滤出的每一行前面加上它在文件中的相对行号
-o, --only-matching 只显示匹配的内容
-q, --quiet, --silent 静默模式,没有任何输出,得用$?来判断执行成功没有,即有没有过滤到想要的内容
--color 颜色
-i, --ignore-case 忽略大小写
-A, --after-context=NUM 如果匹配成功,则将匹配行及其后n行一起打印出来
-B, --before-context=NUM 如果匹配成功,则将匹配行及其前n行一起打印出来
-C, --context=NUM 如果匹配成功,则将匹配行及其前后n行一起打印出来
-c, --count 如果匹配成功,则将匹配到的行数打印出来
-v, --invert-match 反向查找,只显示不匹配的行
-w 匹配单词
-E 等于egrep,扩展



-l, --files-with-matches 如果匹配成功,则只将文件名打印出来,失败则不打印
通常-rl一起用,grep -rl 'root' /etc
-R, -r, --recursive 递归

示例

# 1、-n
[root@egon ~]# grep -n 'root' /etc/passwd
1:root:x:0:0:root:/root:/bin/bash
10:operator:x:11:0:operator:/root:/sbin/nologin
[root@egon ~]#

# 2、-o
[root@egon ~]# grep -o 'root' /etc/passwd
root
root
root
root
[root@egon ~]#

# 3、-q
[root@egon ~]# grep -q 'root' /etc/passwd
[root@egon ~]# echo $?
0

# 4、--color
[root@egon ~]# alias grep
alias grep='grep --color=auto'
[root@egon ~]#

# 5、-i
[root@egon ~]# echo "EGON" |grep -i egon
EGON
[root@egon ~]#

# 6、-A\-B\-C
[root@egon ~]# grep -A 2 'root' /etc/passwd
[root@egon ~]# grep -B 2 'root' /etc/passwd
[root@egon ~]# grep -C 2 'root' /etc/passwd

# 7、-c
[root@egon ~]# grep -c 'root' /etc/passwd
2
[root@egon ~]#

# 8、-v
[root@egon ~]# ps aux | grep nginx |grep -v grep
[root@egon ~]#
[root@egon ~]# ps aux | grep [n]ginx
[root@egon ~]#

# 9、-w
[root@egon ~]# netstat -an |grep -w 80
tcp6       0      0 :::80                   :::*                    LISTEN    
[root@egon ~]# netstat -an |grep '\<80\>'
tcp6       0      0 :::80                   :::*                    LISTEN    
[root@egon ~]# netstat -an |grep '\b80\b'
tcp6       0      0 :::80                   :::*                    LISTEN  
                       
                       
# 10、-rl
[root@egon ~]# grep -rl 'root' /etc # 将/etc目录下所有包含'root'内容的文件都列出来

三 正则表达式

3.1 正则表达式介绍

正则表达式,又称规则表达式(英语:Regular Expression,在代码中常简写为regex、regexp或RE),是计算机科学的一个概念。正则表达式由元字符组成,通常被用来检索、替换那些符合某个模式(规则)的文本(许多程序设计语言都支持利用正则表达式进行字符串操作)。

元字符:是一类可以表达出超越其字面本身含义的特殊字符

shell元字符(也称为通配符): 由shell解释器来解析,如rm -rf *.pdf,元字符*Shell将其解析为任意多个字符
正则表达式元字符 : 由各种执行模式匹配操作的程序来解析,比如vi、grep、sed、awk

例如:vim示例:
:1,$ s/tom/EGON/g # 如anatomy、tomatoes及tomorrow中的“tom”被替换了,而Tom确没被替换
:1,$ s/\<[Tt]om\>/EGON/g

3.2 正则表达式元字符

3.2.1 基本正则元字符集

元字符         功能                                        示例    
^  行首 ^love
$  行尾 love$
.  除了换行符以外的任意单个字符 l..e
*  前导字符的零个或多个    ab*love
.*  所有字符 a.*love
[]  字符组内的任一字符 [lL]ove
[^]  对字符组内的每个字符取反(不匹配字符组内的每个字符)   [^a-z0-9]ove
^[^]      非字符组内的字符开头的行

[a-z]  小写字母
[A-Z]  大写字母
[a-Z]      小写和大写字母
[0-9]      数字

\ 用来转义元字符  love\.
\<     词首定位符 单词一般以空格或特殊字符做分隔、连续的字符组成  \<love
\> 词尾定位符   love\>
\(..\)  匹配稍后将要使用的字符的标签                \(love\)able\1er
                                                        :1,$ s/\(192.168.11\).66/\1.50/g

x\{m\} 字符x重复出现m次     e\{3\}
x\{m,\} 字符x重复出现m次以上 e\{3,\}
x\{m,n\} 字符x重复出现m到n次 e\{3,6\}

示例

# 1、^ 行首
[root@egon ~]# grep '^root' /etc/passwd
root:x:0:0:root:/root:/bin/bash
[root@egon ~]#

# 2、$ 行尾
[root@egon ~]# grep 'bash$' /etc/passwd
root:x:0:0:root:/root:/bin/bash
user1:x:1002:1003::/home/user1:/bin/bash
egon1:x:198:1005::/home/egon1:/bin/bash
gg:x:1004:1006::/home/gg:/bin/bash
egon:x:1005:1007::/home/egon:/bin/bash
tom:x:1006:1008::/home/tom:/bin/bash
[root@egon ~]#


# 3、. 除了换行符以外的任意单个字符
[root@egon ~]# grep 'r..t' /etc/passwd
root:x:0:0:root:/root:/bin/bash
operator:x:11:0:operator:/root:/sbin/nologin
ftp:x:14:50:FTP User:/var/ftp:/sbin/nologin
[root@egon ~]#

# 4、* 前导字符的零个或多个
[root@egon ~]# cat a.txt
a

ab
abb
abbb
bbbbb
[root@egon ~]# grep 'ab*' a.txt
a
ab
abb
abbb
[root@egon ~]#

# 5、.* 所有字符=贪婪
[root@egon ~]# cat a.txt
a123+-*/c11113333c
a1c
a77Ac
a23333c
ac
111
222
333
[root@egon ~]# grep 'a.*c' a.txt
a123+-*/c11113333c
a1c
a77Ac
a23333c
ac
[root@egon ~]#

# 5.1 .*?=》非贪婪,默认情况下,grep不支持非贪婪修饰符,但您可以使用grep -P来使用Perl语法来支持.*?
[root@egon ~]# cat a.txt
<a href="http://www.baidu.com">"我他妈的是百度"</a>
<a href="http://www.sina.com.cn">"我特么的是新浪"</a>
[root@egon ~]#

[root@egon ~]# grep -o 'href=".*"' a.txt # 贪婪
href="http://www.baidu.com">"我他妈的是百度"
href="http://www.sina.com.cn">"我特么的是新浪"
[root@egon ~]#

[root@egon ~]# grep -oP 'href=".*?"' a.txt # 非贪婪
href="http://www.baidu.com"
href="http://www.sina.com.cn"
[root@egon ~]#



# 6、[] 字符组内的任一字符
# 7、[^] 对字符组内的每个字符取反(不匹配字符组内的每个字符)
[root@egon ~]# cat a.txt
a1c
a2c
a33c
aAc
aZc
[root@egon ~]# grep 'a[0-9]c' a.txt
a1c
a2c
[root@egon ~]# grep 'a[^0-9]c' a.txt
aAc
aZc
[root@egon ~]#
[root@egon ~]# grep 'a[0-9][0-9]c' a.txt
a33c
[root@egon ~]#

# 8、^[^] 非字符组内的字符开头的行
[root@egon ~]# cat a.txt
a1c
a2c
a33c
aAc
aZc
[root@egon ~]# grep '^[^0-9]..$' a.txt
a1c
a2c
aAc
aZc
[root@egon ~]#

# 9、[a-z] 小写字母
# 10、[A-Z] 大写字母
# 11、[a-Z] 小写和大写字母
# 12、[0-9] 数字

# 13、\< 单词头 单词一般以空格或特殊字符做分隔,连续的字符串被当做单词
# 14、\> 单词尾
[root@egon ~]# netstat -an |grep -w 80
tcp6       0      0 :::80                   :::*                    LISTEN    
[root@egon ~]# netstat -an |grep '\<80\>'
tcp6       0      0 :::80                   :::*                    LISTEN    
[root@egon ~]# netstat -an |grep '\b80\b'
tcp6       0      0 :::80                   :::*                    LISTEN  

Ps: grep匹配换行符和制表符

[root@egon ~]# echo -e "a\nb" |grep $'a\nb'
a
b
[root@egon ~]#
[root@egon ~]# echo -e "a\tb" |grep $'a\tb'
a b
[root@egon ~]#

3.2.2 扩展正则元字符集

# 扩展正则元字符
+ 匹配一个或多个前导字符   [a-z]+ove
? 匹配零个或一个前导字符   lo?ve
a|b 匹配a或b love|hate
() 组字符 love(able|rs) (egon)+
(..)(..)\1\2 标签匹配字符 (love)able\1er
x{n}    x出现n次    e{3}
x{n,}    x出现n次至无穷次  e{3,}
x{n,m}        x出现n次至m次   e{3,6}

# 若想使用扩展正则
grep加-E 或 egrep 或转义\

sed 加 -r 参数 或转义

AWK 直接支持大多数扩展正则,更多支持需要加选项--posix选项

示例

# ======================grep扩展正则示例======================
[root@egon ~]# cat a.txt
a

ab
abb
abbb
abbbb
abbbbb
bbbbbbb
[root@egon ~]# grep 'ab{2,4}' a.txt # 默认不支持扩展正则,所以没效果
[root@egon ~]# egrep 'ab{2,4}' a.txt
abb
abbb
abbbb
abbbbb
[root@egon ~]#

# ======================sed扩展正则示例======================
[root@egon ~]# sed -n '/roo?/p' /etc/passwd # 默认不支持扩展正则?
[root@egon ~]# sed -n '/roo\?/p' /etc/passwd # 可以用\转义扩展正则符号?
有结果,结果略...
[root@egon ~]# sed -rn '/roo?/p' /etc/passwd # 也可以加-r选项
有结果,结果略...
[root@egon ~]#

# ======================awk扩展正则示例======================
[root@egon ~]# cat a.txt
a

ab
abb
abbb
abbbb
abbbbb
bbbbbbb
[root@egon ~]# awk '/ab{1,3}/{print}' a.txt
ab
abb
abbb
abbbb
abbbbb
[root@egon ~]# awk --posix '/ab{1,3}/{print}' a.txt
ab
abb
abbb
abbbb
abbbbb
[root@egon ~]#

总结

grep:               使用基本元字符集    ^, $, ., *, [], [^], \< \>,\(\),\{\}
egrep(或grep -E): 使用扩展元字符集 ?, +, { }, |, ( )
# 注:grep也可以使用扩展集中的元字符,仅需要对这些元字符前置一个反斜线

\w 所有字母与数字,称为字符[a-zA-Z0-9]   'l[a-zA-Z0-9]*ve'   'l\w*ve'
\W 所有字母与数字之外的字符,称为非字符   'love[^a-zA-Z0-9]+'        'love\W+'
\b 词边界 '\blove\b'  '\<love\>'

3.2.3 posix定义的字符分类

# 表达式             功能                                        示例
[:alnum:]     字母与数字字符                       [[:alnum:]]+  
[:alpha:] 字母字符(包括大小写字母) [[:alpha:]]{4}
[:blank:]     空格与制表符                         [[:blank:]]*
[:digit:]       数字字母                           [[:digit:]]?
[:lower:]     小写字母                           [[:lower:]]{5,}
[:upper:]     大写字母                           [[:upper:]]+
[:punct:]     标点符号                           [[:punct:]]
[:space:]     包括换行符,回车等在内的所有空白[[:space:]]+


# 详解
[:alnum:] Alphanumeric characters.
匹配范围为 [a-zA-Z0-9]
[:alpha:] Alphabetic characters.
匹配范围为 [a-zA-Z]
[:blank:] Space or tab characters.
匹配范围为 空格和TAB键
[:cntrl:] Control characters.
匹配控制键 例如 ^M 要按 ctrl+v 再按回车 才能输出
[:digit:] Numeric characters.
匹配所有数字 [0-9]
[:graph:] Characters that are both printable and visible. (A space is print-
able, but not visible, while an a is both.)
匹配所有可见字符 但不包含空格和TAB 就是你在文本文档中按键盘上能用眼睛观察到的所有符号
[:lower:] Lower-case alphabetic characters.
小写 [a-z]
[:print:] Printable characters (characters that are not control characters.)
匹配所有可见字符 包括空格和TAB
能打印到纸上的所有符号
[:punct:] Punctuation characters (characters that are not letter, digits, con-
trol characters, or space characters).
特殊输入符号 +-=)(*&^%$#@!~`|\"'{}[]:;?/>.<,
注意它不包含空格和TAB
这个集合不等于^[a-zA-Z0-9]
[:space:] Space characters (such as space, tab, and formfeed, to name a few).

[:upper:] Upper-case alphabetic characters.
大写 [A-Z]
[:xdigit:] Characters that are hexadecimal digits.
16进制数 [0-f]

# 使用方法:
[root@egon ~]# grep --color '[[:alnum:]]' /etc/passwd

四 练习

正则表达式及字符处理

目标文件/etc/passwd,使用grep命令或egrep
1.显示出所有含有root的行:
2.输出任何包含bash的所有行,还要输出紧接着这行的上下各两行的内容:
3.  显示出有多少行含有nologin。
4.显示出那些行含有root,并将行号一块输出。
5.显示出文件中
6.新建用户
   abominable
   abominate
   anomie
   atomize
   编写正则表达式,将他们匹配出来
   egrep 'a.omi(nabl|nat|z|)e' /etc/passwd
7.建四个用户
   Alex213sb
   Wpq2222b
   yH438PIG
   egon666
   egon

   过滤出用户名组成是字母+数字+字母的行
[root@MiWiFi-R3-srv ~]# egrep '^[a-Z]+[0-9]+[a-Z]+' /etc/passwd
8.显示出/etc目录下所有包含root的文件名
9. 过滤掉/etc/ssh/sshd_config内所有注释和所有空行
grep -v '^#' /etc/ssh/sshd_config |grep -v '^ *$'

expect

expect

一 expect介绍

expect是一个免费的编程工具,用来实现自动的交互式任务,而无需人为干预。说白了,expect就是一套用来实现自动交互功能的软件。需要安装

yum install -y expect

expect基础

在使用expect时,基本上都是和以下四个命令打交道:

命令作用
spawn启动新的进程
expect从进程接收字符串
send用于向进程发送字符串
interact允许用户交互
  • spawn命令用来启动新的进程,spawn后的expectsend命令都是和使用spawn启动的新进程进行交互。
  • expect通常用来等待一个进程的反馈,我们根据进程的反馈,再使用send命令发送对应的交互命令。
  • send命令接收一个字符串参数,并将该参数发送到进程。
  • interact命令用的其实不是很多,一般情况下使用spawnexpectsend和命令就可以很好的完成我们的任务;但在一些特殊场合下还是需要使用interact命令的,interact命令主要用于退出自动化,进入人工交互。比如我们使用spawnsendexpect命令完成了ftp登陆主机,执行下载文件任务,但是我们希望在文件下载结束以后,仍然可以停留在ftp命令行状态,以便手动的执行后续命令,此时使用interact命令就可以很好的完成这个任务。

总结expect自动应答的基本步骤

第一步: 运行一个程序或命令=>  spawn 命令信息
第二步: 识别产生信息关键字=>  expect 捕获关键字   {send  应答信息}
第三步: 根据识别关键做处理=>  send  应答信息

二 expect实例

自动应答脚本

#!/usr/bin/expect

spawn ssh root@192.168.12.20 uptime

expect "yes/no"
send "yes\n"

expect "*assword"
send "1\n"

expect eof

解释

#1、#!/usr/bin/expect -f:使用expect来解释该脚本

#2、spwan:
spawn是进入expect环境后才可以执行的expect内部命令,如果没有装expect或者直接在默认的SHELL下执行是找不到spawn命令的。它主要的功能是给ssh运行进程加个壳,用来传递交互指令;

#3、expect:
expect "*assword":这里的expect也是expect的一个内部命令,这个命令的意思是判断上次输出结果里是否包含“password”的字符串,如果有则立即返回;否则就等待一段时间后返回,这里等待时长就是前面设置的30秒;

#4、send:
send "1\n":当匹配到对应的输出结果时,就发送密码到打开的ssh进程,执行交互动作;

首次登陆之后,再次登陆,就不会出现yes/no的提示了,所以上述脚本再次运行会出现spawn 命令出现交互式提问的expect 匹配不上的情况,此时脚本会阻塞在原地,我们可以set timeout 3设置超时时间,单位为秒,默认情况下是10秒,以保障脚本超时则结束,

#!/usr/bin/expect -f

spawn ssh root@192.168.12.20 uptime

set timeout 3  # 某一条expect语句在原地匹配,超过了3秒,无论是否匹配成功都会继续执行下一条指令

expect "yes/no"
send "yes\n"

expect "*assword"
send "1\n"

expect eof

设置超时时间的目的仅仅只是为了让脚本不要一直卡在原地,要真正解决上述问题,需要改写成下述形式

#!/usr/bin/expect -f

spawn ssh root@192.168.12.20 hostname

# 注意
# 1、{}一定要换行
# 2、下述语句就一个expect,代表匹配了一次,会匹配完一行就匹配下一行
expect {
   "yes/no" {send "yes\r";exp_continue}
   "*assword" {send "1\n"}
}

expect eof

练习

[root@aliyun ~]# cat 1.sh 
#!/usr/bin/expect -f

spawn ssh egon@127.0.0.1

set timeout -1  # 设置为-1代表永不超时,如果expect没有捕捉到就一直停在原地

expect {
"yes/no" {send "yes\n"}
}

expect {
"password" {send "1\n"}
}

expect "*egon*"
send "ls\n"

expect "\$"
send "pwd\n"

expect "\$"
send "exit\n"  # 注意一定要输入exit结束信号

expect eof  # 最后关闭匹配
[root@aliyun ~]#

interact交互

interact:执行完成后保持交互状态,把控制权交给控制台,这个时候就可以手工操作了。如果没有这一句登录完成后会退出,而不是留在远程终端上。

[root@egon ~]# cat test.sh 
#!/usr/bin/expect -f

spawn ssh root@192.168.12.20

expect {
   "yes/no" {send "yes\r";exp_continue}
   "*assword" {send "1\n"}
}

interact
[root@egon ~]#
[root@egon ~]# ./test.sh
spawn ssh root@192.168.12.20
root@192.168.12.20's password:
Last login: Wed Aug 26 21:28:04 2020 from egon
+--------------------------------------------+
|                                            |
|    你当前登录的是支付业务后台数据库服务    |
|    请不要删库                              |
|                                            |
+--------------------------------------------+
[root@egon ~]# pwd
/root
[root@egon ~]# echo "hello"
hello
[root@egon ~]# exit
登出
Connection to 192.168.12.20 closed.
[root@egon ~]#

三 为expect脚本传参

shell脚本中的变量无法直接在expect中使用的,若expect需要使用变量

一方面可以自己定义

#!/usr/bin/expect -f

set timeout -1
set user "root"
set ip "192.168.12.20"
set cmd "hostname"
set pass "1"


spawn ssh $user@$ip $cmd

expect {
   "yes/no" {send "yes\r";exp_continue}
   "*assword" {send "$pass\n"}
}

expect eof

另外一方面可以通过下述方式引入shell变量,注意此时解释器换成#!/bin/bash

#!/bin/bash

user="root"
ip="192.168.12.20"
cmd="hostname"
pass="1"

expect << EOF
spawn ssh $user@$ip $cmd

expect {
   "yes/no" {send "yes\r";exp_continue}
   "*assword" {send "$pass\n"}
}

expect eof
EOF

此外,expect脚本还可以从命令行获取参数

在expect中,$argc表示参数个数,而参数值存放在$argv中,比如取第一个参数就是[lindex ​$argv 0],以此类推。

[root@egon ~]# cat test.sh 
#!/usr/bin/expect -f

if {$argc != 4} {
   puts "Usage:./script.sh <ip> <username> <password> <cmd>"
   exit 1
}

set ip [lindex $argv 0]
set user [lindex $argv 1]
set pass [lindex $argv 2]
set cmd [lindex $argv 3]
set timeout -1

spawn ssh $user@$ip $cmd

expect {
   "yes/no" {send "yes\r";exp_continue}
   "*assword" {send "$pass\n"}
}

expect eof
[root@egon ~]# ./test.sh
Usage:./script.sh <ip> <username> <password> <cmd>
[root@egon ~]# ./test.sh 192.168.12.20 root 1 hostname
spawn ssh root@192.168.12.20 hostname
root@192.168.12.20's password:
egon
[root@egon ~]#

能够在工作中熟练的使用Shell脚本就可以很大程度的提高工作效率,如果再搭配上expect,那么很多工作都可以自动化进行,对工作的展开如虎添翼。如果你会Python的话,你的视野将会更加开阔,那个时候你又会“嫌弃”expect了。

信号控制

信号控制

一 信号说明

在脚本执行过程中, 可能会被一些键盘操作快捷方式所打断, 影响脚本运行

# HUP(1):  1、挂起信号 2、往往可以让进程重新加载配置
本信号在用户终端连接(正常或非正常)结束时发出, 通常是在终端的控制进程结束时, 通知同一session内的各个作业, 这时它们与控制终端不再关联。

登录Linux时,系统会分配给登录用户一个终端(Session)。在这个终端运行的所有程序,包括前台进程组和后台进程组,一般都 属于这个 Session。当用户退出Linux登录时,前台进程组和后台有对终端输出的进程将会收到SIGHUP信号。这个信号的默认操作为终止进程,因此前台进 程组和后台有终端输出的进程就会中止。不过,可以捕获这个信号,比如wget能捕获SIGHUP信号,并忽略它,这样就算退出了Linux登录,wget也 能继续下载。

此外,对于与终端脱离关系的守护进程,这个信号用于通知它重新读取配置文件。

# INT(2): 中断, 通常因为按下ctrl+c而产生的信号,用于通知前台进程组终止进程。
# QUIT(3): 退出,和SIGINT类似, 但由QUIT字符(通常是Ctrl-\)来控制. 进程在因收到SIGQUIT退出时会产生core文件, 在这个意义上类似于一个程序错误信号。

# TSTP(20): 停止进行运行,通常因为按下ctrl+z而产生的信号

# KILL (9)
用来立即结束程序的运行. 本信号不能被阻塞、处理和忽略。如果管理员发现某个进程终止不了,可尝试发送这个信号。
# TERM(15):
终止,是不带参数时kill默认发送的信号,默认是杀死进程,与SIGKILL不同的是该信号可以被阻塞和处理。通常用TERM信号来要求程序自己正常退出,如果进程终止不了,我们才会尝试SIGKILL。
 
# ===============了解===============
# ABRT(6): 中止, 通常因某些严重错误产生的引号  

# SIGCHLD
子进程结束时, 父进程会收到这个信号。

如果父进程没有处理这个信号,也没有等待(wait)子进程,子进程虽然终止,但是还会在内核进程表中占有表项,这时的子进程称为僵尸 进程。这种情 况我们应该避免(父进程或者忽略SIGCHILD信号,或者捕捉它,或者wait它派生的子进程,或者父进程先终止,这时子进程的终止自动由init进程 来接管)。

# 更多详见:man 7 signal

二 捕捉信号

我们可以用trap命令捕捉信号(trap命令并不能捕获所有信号,但是常用信号HUP、INT、QUIT、TERM都是可以捕获的),执行我们规定的操作

# 操作1:捕捉信号、执行引号内的操作
trap "echo 已经识别中断信号:ctrl+c" INT

# 示例2:捕捉信号、不执行任何操作
trap "" INT  

# 示例3:也可以同时捕获多个信号
trap "" HUP INT QUIT TSTP

例1:

[root@egon test]# cat m.sh 
#!/bin/bash

trap "echo 已经识别到中断信号:ctrl+c" INT
trap 'echo 已经识别到中断信号:ctrl+\\' QUIT
trap 'echo 已经识别到中断信号:ctrl+z' TSTP

read -p "请输入你的名字: " name
echo "你的名字是:$name"
[root@egon test]# chmod +x m.sh
[root@egon test]# ./m.sh
请输入你的名字: ^C已经识别到中断信号:ctrl+c
^C已经识别到中断信号:ctrl+c
^C已经识别到中断信号:ctrl+c
^\已经识别到中断信号:ctrl+\
^\已经识别到中断信号:ctrl+\
^Z已经识别到中断信号:ctrl+z
^Z已经识别到中断信号:ctrl+z
egon
你的名字是:egon
[root@egon test]#

例2:

#!/bin/bash
trap "" HUP INT QUIT TSTP  # kill -HUP 当前进程的pid,也无法终止其运行

clear
i=0
while true
do
  [ $i == 0 ] && i=1 || i=0
if [ $i == 0 ];then
       echo -e "\033[31m 红灯亮 \033[0m"
   else
       echo -e "\033[32m 绿灯亮 \033[0m"
   fi
   sleep 1
   clear
done

可以使用kill -9终止以上进程

##三 关于HUP信号

要了解Linux的HUP信号,需要从hangup说起

在 Unix 的早期版本中,每个终端都会通过 modem 和系统通讯。
当用户 logout 时,modem 就会挂断(hang up)电话。
同理,当 modem 断开连接时,就会给终端发送 hangup 信号来通知其关闭所有子进程。

综上,我们知道,当用户注销(logout)或者网络断开或者终端关闭(注意注意注意,一定是终端整体关闭,不是单纯的exit)时,终端都会收到Linux HUP信号(hangup)信号,然后终端在结束前会关闭其所有子进程。

如果我们想让我们的进程在后台一直运行,不要因为用户注销(logout)或者网络断开或者终端关闭而一起被干掉,那么我们有两种解决方案

  • 方案1:让进程忽略Linux HUP信号
  • 方案2:让进程运行在新的会话里,从而成为不属于此终端的子进程,就不会在当前终端挂掉的情况下一起被带走。

3.3.1 nohup命令

针对方案1,我们可以使用nohup命令,nohup 的用途就是让提交的命令忽略 hangup 信号,该命令通常与&符号一起使用

nohup 的使用是十分方便的,只需在要处理的命令前加上 nohup 即可,但是 nohup 命令会从终端解除进程的关联,进程会丢掉STDOUT,STDERR的链接。标准输出和标准错误缺省会被重定向到 nohup.out 文件中。一般我们可在结尾加上"&"来将命令同时放入后台运行,也可用">filename 2>&1"来更改缺省的重定向文件名。

示例

3.3.2 setsid命令

针对方案1,我们还可以用setsid命令实现,原理与3.1是一样的,setid是直接将进程的父pid设置成1,即让运行的进程归属于init的子进程,那么除非init结束,该子进程才会结束,当前进程所在的终端结束后并不会影响进程的运行

# 1、在终端2中执行命令
[root@egon ~]# setsid ping www.baidu.com # 也可以在后面加&符号

# 2、关闭终端2

# 3、在终端1中查看
[root@egon ~]# ps -ef |grep [p]ing
root     102335      1  0 17:53 ?        00:00:00 ping www.baidu.com

3.3.3 在子shell中提交任务

# 1、在终端2中执行命令
[root@egon ~]# (ping www.baidu.com &) # 提交的作业并不在作业列表中

# 2、关闭终端2

# 3、在终端1中查看
[root@egon ~]# ps -ef |grep [p]ing
root     102361      1  0 17:55 ?        00:00:00 ping www.baidu.com
           
可以看到新提交的进程的父 ID(PPID)为1(init 进程的 PID),并不是当前终端的进程 ID。因此并不属于当前终端的子进程,从而也就不会受到当前终端的Linux HUP信号的影响了。

3.3.4 screen命令

# 1、安装
[root@egon ~]# yum install screen -y

# 2、运行命令
方式一:开启一个窗口并用-S指定窗口名,也可以不指定
[root@egon ~]# screen -S egon_dsb
'''
Screen将创建一个执行shell的全屏窗口。你可以执行任意shell程序,就像在ssh窗口中那样。
在该窗口中键入exit则退出该窗口,如果此时,这是该screen会话的唯一窗口,该screen会话退出,否则screen自动切换到前一个窗口。
'''

方式二:Screen命令后跟你要执行的程序
[root@egon ~]# screen vim test.txt
'''
Screen创建一个执行vim test.txt的单窗口会话,退出vim将退出该窗口/会话。
'''

# 3、原理分析
screen程序会帮我们管理运行的命令,退出screen,我们的命令还会继续运行,若关闭screen所在的终端,则screen程序的ppid变为1,所以screen不会死掉,对应着它帮我们管理的命令也不会退出
测试略

# 4:重新连接会话
在终端1中运行
[root@egon ~]# screen
[root@egon ~]# n=1;while true;do echo $n;sleep 1;((n++));done

[root@egon ~]# 按下ctrl+a,然后再按下ctrl+d,注意要连贯,手别哆嗦,

[root@egon ~]# 此时可以关闭整个终端,我们的程序并不会结束

打开一个新的终端
[root@egon ~]# screen -ls
There is a screen on:
109125.pts-0.egon (Detached)
1 Socket in /var/run/screen/S-root.

[root@egon ~]# screen -r 109125 # 会继续运行
[root@egon ~]#

注意:如果我们刚开始已经用screen -S xxx指定了名字,那么我们其实可以直接
screen -r xxx ,就无须去找进程id了

远程演示

# 在终端1:
[root@egon ~]# screen -S egon_av

# 开启一个新的终端,在该终端执行的命令,终端1会同步显示
[root@egon ~]# screen -x egon_av

四 僵尸进程与孤儿进程

僵尸进程

#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的顶级进程接收了,所以僵尸进程在这种情况下是不存在的,存在只有孤儿进程而已,孤儿进程声明周期结束自然会被顶级进程来销毁。

数组

数组

一 数组介绍

什么是数组?

数组就是一系列元素的集合,一个数组内可以存放多个元素

为何要用数组?

我们可以用数组将多个元素汇总到一起,避免单独定义的麻烦

##二 数组的使用

###2.1 数组的定义

# 方式一:array=(元素1 元素2 元素3)
array=(egon 18 male)

# 方式二:array=([key1]=value1 [key2]=value2 [key3]=value3)
array=([0]=111 [1]="two" [2]=333)

# 方式三:依次赋值
array_new[0]=111
array_new[1]=222
array_new[2]="third"

# 方式四:利用执行命令的结果设置数组元素:array=($(命令)) 或者 array=(`命令`)
该方式会将命令的结果以空格为分隔符切成多个元素然后赋值给数组
[root@aliyun ~]# ls /test
a.txt b.txt
[root@aliyun ~]# array3=(`ls /test`)
[root@aliyun ~]# declare -a |grep array3
declare -a array3='([0]="a.txt" [1]="b.txt")'


# ps:查看声明过的数组
declare -a

###2.2 访问数组内元素

[root@aliyun ~]# array=(egon 18 male)

#1、按照索引访问数组内指定位置的元素
[root@aliyun ~]# echo ${array[0]}
egon
[root@aliyun ~]# echo ${array[1]}
18
[root@aliyun ~]# echo ${array[2]}
male
[root@aliyun ~]# echo ${array[-1]} # 支持负向索引
male


# 2、访问数组内全部元素信息
[root@aliyun ~]# echo ${array[*]}
egon 18 male
[root@aliyun ~]# echo ${array[@]}
egon 18 male

# 3、获取数组元素的长度
[root@aliyun ~]# echo ${#array[*]}
3
[root@aliyun ~]# echo ${#array[@]}
3

2.3 修改/添加数组元素

[root@aliyun ~]# array=(egon 18 male)
[root@aliyun ~]# array[0]="EGON" # 修改
[root@aliyun ~]# array[3]="IT" # 添加
[root@aliyun ~]# declare -a |grep array
declare -a array='([0]="EGON" [1]="18" [2]="male" [3]="IT")'

2.4 删除数组元素

[root@aliyun ~]# unset array[0]
[root@aliyun ~]# echo ${array[*]}
18 male IT
[root@aliyun ~]# declare -a |grep array
declare -a array='([1]="18" [2]="male" [3]="IT")'
[root@aliyun ~]#
[root@aliyun ~]#
[root@aliyun ~]# unset array[2]
[root@aliyun ~]# declare -a |grep array
declare -a array='([1]="18" [3]="IT")'

[root@aliyun ~]# unset array # 删除整个数组
[root@aliyun ~]# echo ${array[*]}

[root@aliyun ~]#

2.5 数组内元素的截取

[root@aliyun ~]# array=(egon 18 male IT 1.80)
[root@aliyun ~]# echo ${array[*]:1}   # 从索引1开始,一直到最后
18 male IT 1.80
[root@aliyun ~]# echo ${array[*]:1:3} # 从索引1开始,访问3个元素
18 male IT

[root@aliyun ~]# array=(one two three four five fire)
[root@aliyun ~]# echo ${array[*]#one}
two three four five fire
[root@aliyun ~]# echo ${array[*]#f*e}
one two three four

2.6 数组内容的替换

[root@aliyun ~]# array=(one two three four five fire)

[root@aliyun ~]# echo ${array[*]/five/abc}
one two three four abc fire

[root@aliyun ~]# echo ${array[*]/f*e/abc}
one two three four abc abc

三 关联数组

数组分为两种

  • 普通数组:只能使用整数作为数组索引,我们前面介绍的都是普通数组
  • 关联数组:可以使用字符串作为数组索引,需要用declare -A声明

声明关联数组

[root@aliyun ~]# declare -A info
[root@aliyun ~]# info["name"]="egon"
[root@aliyun ~]# info["age"]=18
[root@aliyun ~]# info["gender"]="male"
[root@aliyun ~]#
[root@aliyun ~]# declare -A |grep info
declare -A info='([gender]="male" [name]="egon" [age]="18" )'
[root@aliyun ~]#
[root@aliyun ~]# echo ${info[*]}
male egon 18
[root@aliyun ~]#
[root@aliyun ~]# echo ${info["name"]}
egon

四 遍历数组

方法一: 利用获取所有信息进行遍历 (适用于普通数组与关联数组)

# 例1
declare -A array
array=(["name"]="egon" ["age"]=18 ["gender"]="male")

for item in ${array[*]}
do
   echo $item
done

# 例2
array=("egon" 18 "male")
for item in ${array[*]}
do
   echo $item
done

方法二: 通过数组元数的索引进行遍历(适用于普通数组与关联数组)

# 例1
declare -A array
array=(["name"]="egon" ["age"]=18 ["gender"]="male")

for i in ${!array[*]}  # echo ${!array[*]} # 获取的是key信息:name age gender
do
   echo "$i:${array[$i]}"
done

# 例2
array=("egon" 18 "male")
for i in ${!array[*]}  # echo ${!array[*]} 直接获取所有元素的索引信息
do
   echo $i
   echo ${array[i]}
done

方法三:根据数组长度信息进行遍历,(适用于普通数组)

array=("egon" 18 "male")

for ((i=0;i<${#array[*]};i++))
do
   echo "$i:${array[$i]}"
done

五 练习

练习1:对指定的IP地址进行网络测试

#!/bin/bash 
array=(
   10.0.0.7
   10.0.0.8
   10.0.0.9
   10.0.0.41
)

for info in ${array[*]}
do
ping -c 2 -W 1  $info
done

练习2: 统计登录shell的种类及对应的个数(关联数组)

#!/bin/bash 
declare -A  array_for_shell
while read line  # done后面接<将文件重定向给while;while后再接read将文件流赋值给变量
do
   login_shell=`echo $line | cut -d: -f7`
   let array_for_shell["$login_shell"]++  # 当使用let时,变量前面不必加上$                                                                                                                                              
done < /etc/passwd


for k in ${!array_for_shell[*]}
do
   echo $k:${array_for_shell[$k]}
done

练习3:获取文件指定列的信息

[root@egon test]# cat a.sh 
#!/usr/bin/env bash

declare -A array
while read line
do
   let array[`echo $line | cut -d: -f7`]++
done < /etc/passwd

for k in ${!array[*]}
do
   echo $k:${array[$k]}
done
[root@egon test]#
[root@egon test]# ./a.sh
/sbin/nologin:41
/bin/sync:1
/bin/bash:2
/sbin/shutdown:1
/sbin/halt:1

函数

函数

##一 函数介绍

什么是函数???

函数就是用来盛放一组代码的容器,函数内的一组代码完成一个特定的功能,称之为一组代码块,调用函数便可触发函数内代码块的运行,这可以实现代码的复用,所以函数又可以称之为一个工具

为何要用函数

#1、减少代码冗余
#2、提升代码的组织结构性、可读性
#3、增强扩展性

二 函数的基本使用

具备某一功能的工具=>函数
事先准备好哦工具=>函数的定义
遇到应用场景,拿来就用=>函数的调用

所以函数的使用原则:先定义,后调用

定义函数

#语法:
[ function ] funname [()]
{
   命令1;
   命令2;
   命令3;
  ...
  [return int;]
}

# 示例1:完整写法
function 函数名() {
函数要实现的功能代码
}

# 示例2:省略关键字(),注意此时不能省略关键字function
function 函数名 {
函数要实现的功能代码
}

# 示例3:省略关键字function
函数名() {
函数要实现的功能代码
}

调用函数

# 语法:
函数名  # 无参调用
函数名 参数1 参数2  # 有参调用

# 示例
function test1(){
   echo "执行第一个函数"
}


function test2 {
   echo "执行第二个函数"
}


test3(){
   echo "执行第三个函数"
}


# 调用函数:直接引用函数名字即调用函数,会触发函数内代码的运行
test1
test2
test3

# ps:关于有参调用见下一小节

三 函数参数

如果把函数当成一座工厂,函数的参数就是为工厂运送的原材料

  • 调用函数时可以向其传递参数# 调用函数test1,在其后以空格为分隔符依次罗列参数
    test1 111 222 333 444 555
  • 在函数体内部,通过 $n 的形式来获取参数的值,例如,$1表示第一个参数,$2表示第二个参数…当n>=10时,需要使用${n}来获取参数[root@egon ~]# cat b.sh
    function test1(){
       echo “…start…”
       echo $1
       echo $2
       echo $3
       echo “…end…”
    }

    test1 111 222 333 444 555  # 为函数体传参

    [root@egon ~]# ./b.sh
    …start…
    111
    222
    333
    …end…ps:在脚本内获取脚本调用者在命令行里为脚本传入的参数,同样使用的是$n,不要搞混[root@egon ~]# cat b.sh
    function test1(){
       echo “…start…”
       echo “这是函数内:$1”
       echo “这是函数内:$2”
       echo “这是函数内:$3”
       echo “…end…”
    }

    # test1 111 222 333 444 555
    test1

    echo “这是脚本级的参数$1”
    echo “这是脚本级的参数$2”
    echo “这是脚本级的参数$3”

    [root@egon ~]#
    [root@egon ~]#
    [root@egon ~]# ./b.sh xxx yyy zzz mmm nnn
    …start…
    这是函数内:
    这是函数内:
    这是函数内:
    …end…
    这是脚本级的参数xxx
    这是脚本级的参数yyy
    这是脚本级的参数zzz
    [root@egon ~]#

温故知新

参数处理说明
$#传递到脚本或函数的参数个数
$*所有参数
$@所有参数,与$*类似
$$当前脚本进程的ID号
$?获取上一条命令执行完毕后的退出状态。0表示正确,非0代表错误,如果执行的是函数那么$?取的是函数体内return后的值

ps:

#1、当$*和$@没有被引号引用起来的时候,它们确实没有什么区别,都会把位置参数当成一个个体。

#2、"$*" 会把所有位置参数当成一个整体(或者说当成一个单词),如果没有位置参数,则"$*"为空,如果有两个位置参数并且分隔符为空格时,"$*"相当于"$1 $2"

#3、"$@" 会把所有位置参数当成一个单独的字段,如果没有位置参数,则"$@"展开为空(不是空字符串,而是空列表),如果存在一个位置参数,则"$@"相当于"$1",如果有两个参数,则"$@"相当于"$1" "$2"等等

示例

[root@egon ~]# cat b.sh 
echo "=======函数test1==========="
function test1(){
   echo "$*"  # 111 222 333 444 555
   echo "$@"  # 111 222 333 444 555
   echo $#   # 5
   echo $$    # 87380
   echo $?    # 0
}

test1 111 222 333 444 555


echo "=======函数test2==========="
function test2(){
   for i in "$*"  # 注意:$*不加引号结果与$@一模一样
   do
       echo $i
   done
}

test2 111 222 333 "444 555"  # 注意"444 555"被引号引成了一个参数
# 运行结果为:111 222 333 444 555

echo "=======函数test3==========="
function test3(){
   for i in "$@"  # 注意:$*不加引号结果与$@一模一样
   do
       echo $i
   done
}

test3 111 222 333 "444 555"  # 注意"444 555"被引号引成了一个参数
# 运行结果为:
# 111
# 222
# 333
# 444 555

四 函数的返回值

如果把函数当成一座工厂,函数的返回值就是工厂的产品,在函数内使用return关键字返回值,函数内可以有多个return,但只要执行一个,整个函数就会立刻结束

[root@egon ~]# cat b.sh 
function test1(){
   echo 111
   return
   echo 222
   return
   echo 333
}

test1

[root@egon ~]# chmod +x bbbb.sh
[root@egon ~]# ./b.sh
111

需要注意的是shell语言的函数中,通常用return返回函数运行是否成功的状态,0代表成功,非零代表失败,需要用$?获取函数的返回值

  • 1、如果函数内没有return,那么将以最后一条命令运行结果(命令运行成功结果为0,否则为非0)作为返回值[root@egon ~]# cat b.sh
    function test1(){
       echo 111
       echo 222
       echo 333
       xxx  # 运行该命令出错
    }

    test1
    echo $?
    [root@egon ~]# ./b.sh
    111
    222
    333
    ./b.sh:行5: xxx: 未找到命令
    127
  • 2、如果函数内有return,那么return后跟的只能是整型值并且范围为0-255,用于标识函数的运行结果是否正确, 与C 语言不同,shell 语言中 0 代表 true,0 以外的值代表 false[root@egon ~]# cat b.sh
    function test1(){
       echo 111
       echo 222
       echo 333
       return 0
    }

    test1
    echo $?  # 用$?获取函数的返回值
    [root@egon ~]# ./b.sh
    111
    222
    333
    0

五 变量的作用域

Shell 变量的作用域(Scope),就是 Shell 变量的有效范围(可以使用的范围)。

1、局部变量:只能在函数内访问

使用local关键字定义在函数内的变量属于局部范围,只能在函数内使用,如下所示

[root@localhost shell]# cat hello.sh 
#!/bin/bash

# 定义函数
function test(){
   local x=111
   echo "函数内访问x:$x"
}

# 调用函数
test

echo "在函数外即全局访问x:$x"  # 无法访问函数内的局部变量

执行结果

[root@localhost shell]# ./hello.sh 
函数内访问x:111
在函数外即全局访问x:

2、全局变量:可以在当前shell进程中使用

所谓全局变量,就是指变量在当前的整个 Shell 进程中都有效。每个 Shell 进程都有自己的作用域,彼此之间互不影响。在 Shell 中定义的变量,默认就都是全局变量。

[root@localhost shell]# cat hello.sh 
#!/bin/bash
x=2222

function test(){
   echo "函数内访问x:$x"
}
test
echo "在函数外即全局访问x:$x"

执行结果

[root@localhost shell]# ./hello.sh 
函数内访问x:2222
在函数外即全局访问x:2222

请注意

  • 1、在函数内定义的变量,如果没有用local声明,那么默认也是全局变量,shell变量语法的该特性与js的变量是类似的(在js函数内部定义的变量,默认也是全局变量,除非加上关键字var)[root@localhost shell]# cat hello.sh
    #!/bin/bash
    function test(){
       x=2222  # 全局变量
    }
    test
    echo “在函数外即全局访问x:$x”  

    [root@localhost shell]# ./hello.sh
    在函数外即全局访问x:2222
  • 2、每执行一个解释器,都会开启一个解释的shell进程,每个shell进程都有自己的作用域彼此互不干扰[root@localhost shell]# x=111 # 该变量仅仅只在当前shell进程中有效,对新的shell进程无影响
    [root@localhost shell]# echo $x
    111
    [root@localhost shell]# bash # 执行bash解释器,则开启一个新的进程,或者干脆直接打开一个新的终端
    [root@localhost shell]# echo $x

    [root@localhost shell]#
  • 3、需要强调的是:全局变量的作用范围是当前的 Shell 进程,而不是当前的 Shell 脚本文件,它们是不同的概念。打开一个 Shell 窗口就创建了一个 Shell 进程,打开多个 Shell 窗口就创建了多个 Shell 进程,每个 Shell 进程都是独立的,拥有不同的进程 ID。在一个 Shell 进程中可以使用 source 命令执行多个 Shell 脚本文件,此时全局变量在这些脚本文件中都有效。[root@localhost shell]# echo $x

    [root@localhost shell]# cat hello.sh
    #!/bin/bash
    function test(){
       x=2222  # 全局变量
    }
    test

    [root@localhost shell]# source hello.sh # 在当前shell进程中执行,产生一个全局变量x
    [root@localhost shell]# echo $x # 在当前shell进程中访问全局变量x,可以看到
    2222
    [root@localhost shell]#
    [root@localhost shell]#
    [root@localhost shell]# cat aaa.sh
    #!/bin/bash
    echo $x

    [root@localhost shell]# source aaa.sh # 在当前shell进程中访问全局变量x,同样可以看到
    2222结论:函数test内的全局变量x早已超越了文件,即全局变量是超越文件的,作用范围是整个当前bash进程

3、环境变量:在当前进程的子进程中都可以使用

全局变量只在当前 Shell 进程中有效,对其它 Shell 进程和子进程都无效。如果使用export命令将全局变量导出,那么它就在所有的子进程中也有效了,这称为“环境变量”。

环境变量被创建时所处的 Shell 进程称为父进程,如果在父进程中再创建一个新的进程来执行 Shell 命令,那么这个新的进程被称作 Shell 子进程。当 Shell 子进程产生时,它会继承父进程的环境变量为自己所用,所以说环境变量可从父进程传给子进程。不难理解,环境变量还可以传递给孙进程。

[root@localhost shell]# export y=333  # 爷爷
[root@localhost shell]# bash # 爸爸
[root@localhost shell]# echo $y
333
[root@localhost shell]# bash # 孙子
[root@localhost shell]# echo $y
333

ps:通过exit命令可以一层一层地退出 Shell。

ps:命令set 和 env

set:显示所有变量
env:环境变量

注意

  • 1、环境变量只能向下传递而不能向上传递,即“传子不传父”。
  • 2、两个没有父子关系的 Shell 进程是不能传递环境变量的我们一直强调的是环境变量在 Shell 子进程中有效,并没有说它在所有的 Shell 进程中都有效;如果你通过终端创建了一个新的 Shell 窗口,那它就不是当前 Shell 的子进程,环境变量对这个新的 Shell 进程仍然是无效的。
  • 3、环境变量也是临时的[root@localhost ~]# ps aux |grep bash$ |grep -v grep
    root     123436  0.0  0.1 116356  2960 pts/0    Ss   21:52   0:00 -bash
    root     123492  0.0  0.1 116472  2932 pts/0    S    21:54   0:00 bash
    root     123520  0.0  0.1 116440  2988 pts/0    S    21:54   0:00 bash

    注意:
    # -开头的bash代表是在登录终端登录的顶级shell进程
    # 非-开头的bash代表的是子shell进程
    一旦退出了在终端登录的顶级shell,那么该终端下开启的所有子shell都会被回收,export设置的环境变量随即消失所以说环境变量也是临时的,如果想设置成永久的,需要将变量写入shell配置文件中才可以,Shell 进程每次启动时都会执行配置文件中的代码做一些初始化工作,如果将变量放在配置文件中,那么每次启动进程都会定义这个变量。请看下一小节

六 登录shell与非登录shell

承接上一小节末尾留下的疑问,我们首先需要了解一下BASH的两种类型

  • 1、登录shell:就是通过输入用户名 密码后 或 su – 获得的shell
  • 2、非登录shell:则是通过bash命令和脚本开启的shell环境

那么他们有什么区别呢?和我们永久设定环境变量又有什么关系呢?

我们知道在linux里一切皆为文件,同样,shell的属性加载也是写到文件里的
在登陆时就会加载对应文件的内容来初始化shell环境,
非登录与登录区别就在于加载的文件不同 从而导致获得的shell环境不同
我们看看登录shell都加载了那些文件
--> /etc/profile
--> /etc/profile.d/*.sh
--> $HOME/.bash_profile
--> $HOME/.bashrc
--> /etc/bashrc
再看非登录shell加载的文件,非登录shell加载的文件要少很多
--> $HOME/.bashrc
--> /etc/bashrc
--> /etc/profile.d/*.sh

通常,我们会将环境变量设置在 $HOME/.bash_profile

但如果不管哪种登录shell都想使用的变量 可以考虑设置在 $HOME/.bashrc或者/etc/bashrc中,因为它们都属于无论如何都会执行的文件,但是,如果我们真的在这类文件中添加了变量,那么意味着每次执行shell都会重新定义一遍该变量,而定义变量是要耗费内存资源的,这非常不可取,所以我们通常会结合export在/etc/profile文件中声明一个全局变量,这样在每次登录用户时产生的顶级shell里会有一个全局变量,所有的子shell都可以看到了,无需重复定义

[root@localhost ~]# vim /etc/profile
[root@localhost ~]# head -2 /etc/profile
# /etc/profile
name="egon"
[root@localhost ~]# echo $name

[root@localhost ~]# source /etc/profile # 可以在当前shell中立即生效,也可以退出后重新登录终端
[root@localhost ~]# echo $name
egon

七 作业

开发一个计算器程序如下,引入函数减少代码冗余

[root@egon ~]# cat b.sh 
#!/bin/bash

echo " ----------------------------------"
echo "|这是一个简单的整数计算器,biubiu |"
echo " ----------------------------------"
echo

# 接收第一个整数,并校验
while :
do
   read -p  "请输入第一个整数: " num1
   expr $num1 + 0 &> /dev/null
   if [ $? -eq 0 ];then
       break
   else
       echo "必须输入整数"
   fi
done

# 接收第二个整数,并校验
while :
do
   read -p  "请输入第二个整数: " num2
   expr $num2 + 0 &> /dev/null
   if [ $? -eq 0 ];then
       break
   else
       echo "必须输入整数"
   fi
done

# 接收输入的操作
echo "-------------------"
echo "| 1.加法         |"
echo "| 2.减法         |"
echo "| 3.乘法         |"
echo "| 4.除法         |"
echo "-------------------"
read -p "请输入您想执行的操作:" choice
case $choice in
   "1")
       res=`expr $num1 + $num2`
       echo "$num1+$num2=$res"
      ;;
   "2")
       res=`expr $num1 - $num2`
       echo "$num1+$num2=$res"
      ;;
   "3")
       res=`expr $num1 \* $num2`
       echo "$num1*$num2=$res"
      ;;
   "4")
       res=`expr $num1 / $num2`
       echo "$num1/$num2=$res"
      ;;
   *)
       echo "未知的操作"
esac