示例使用163邮箱服务
mailx方式
centos编辑mail.rc
ubuntu安装heirloom-mailx,编辑s-nail.rc文件

set from=username@163.com smtp=smtp.163.com
set smtp-auth-user=username@163.com smtp-auth-password=授权码 smtp-auth=login
# SSL方式,更安全
set nss-config-dir=/etc/pki/nssdb
set ssl-verify=ignore
set smtp=smtps://smtp.163.com:465
set from=username@163.com
set smtp-auth-user=username@163.com
set smtp-auth-password=授权码
set smtp-auth=login

发送命令

echo -e "内容" | mail -v -s "标题" username@domain.com

sendmail方式

sendmail -S "smtp.163.com" -f "username@163.com"(发) -auusername@163.com -ap授权码 username@domain.com(收) -v < mail.txt
# SSL方式
sendmail -f "username@163.com"(发) -H 'exec openssl s_client -quiet -tls1 -connect smtp.163.com:465' < mail.txt -auusername@163.com -ap授权码 username@qq.com(收)

mail.txt文件内容

From:Frost-Router <username@163.com>
To: username@domain.com
Subject:邮件内容

适用范围:MongoDB单机模式。

可备份本机或者远程服务器,MongoDB文件磁盘占用较大,备份后使用7zip压缩,7zip支持多线程,速度和压缩率相当高,2.9GB样本实测压缩后大小为263MB,压缩率为8%,CPU使用率高达600%-700%(8核心),另外备份失败或压缩失败都会发出邮件提醒(邮件配置此处不列出)

#!/bin/bash
sourcepath='/usr/local/mongodb2.6.6/bin'
targetpath='/usr/backup/mongodb'
nowtime=$(date "+%Y-%m-%d")
backup()
{
	echo "[$(date "+%Y-%m-%d %H:%M:%S")]===========backup start================="
	if
	${sourcepath}/mongodump -h 127.0.0.1 -o ${targetpath}/mongodb_${nowtime}
	then
		echo -e "\033[41;37m ===========backup successfully================= \033[0m"
		return 1
	else
		echo -e "\033[41;37m ===========backup failure================= \033[0m"
		echo -e "MongoDB使用mongodump工具备份失败,请登陆服务器查看日志\n日志路径:/usr/backup/backup.log" | mail -v -s "MongoDB备份" name@domain.com
	fi
}

compress()
{
	if [ $? -eq 1 ]
	then
		echo -e "\033[41;37m ===========现在开始打包压缩================= \033[0m"
		cd /usr/backup/mongodb
		if
		7za a mongodb_${nowtime}.7z mongodb_${nowtime}/
		then
			echo -e "\033[41;37m [$(date "+%Y-%m-%d %H:%M:%S")]===========压缩完成,现在删除原文件夹================= \033[0m"
			rm -rf mongodb_${nowtime}/
			echo -e "\033[41;37m [$(date "+%Y-%m-%d %H:%M:%S")]===========执行完毕,脚本退出================= \033[0m"
		else
			echo -e "MongoDB备份成功,但打包失败,请登陆服务器查看日志\n日志路径:/usr/backup/backup.log" | mail -v -s "MongoDB备份" name@domain.com
		fi
	fi
}

backup

compress

变量说明:
$$
Shell本身的PID(ProcessID)
$!
Shell最后运行的后台Process的PID
$?
最后运行的命令的结束代码(返回值)
$-
使用Set命令设定的Flag一览
$*
所有参数列表。如”$*”用「”」括起来的情况、以”$1 $2 … $n”的形式输出所有参数。
$@
所有参数列表。如”$@”用「”」括起来的情况、以”$1″ “$2” … “$n” 的形式输出所有参数。
$#
添加到Shell的参数个数
$0
Shell本身的文件名
$1~$n
添加到Shell的各参数值。$1是第1参数、$2是第2参数…。

 

if  条件
then
Command
else
Command
fi                              别忘了这个结尾
If语句忘了结尾fi
test.sh: line 14: syntax error: unexpected end of fi

if 的三种条件表达式

if
command
then

if
函数
then

 命令执行成功,等于返回0 (比如grep ,找到匹配)
执行失败,返回非0 (grep,没找到匹配)
if [ expression_r_r_r  ]
then
 表达式结果为真,则返回0,if把0值引向then
if test expression_r_r_r
then
 表达式结果为假,则返回非0,if把非0值引向then

[ ] &&  ——快捷if

[ -f “/etc/shadow” ] && echo “This computer uses shadow passwors”
   && 可以理解为then
如果左边的表达式为真则执行右边的语句

shell的if与c语言if的功能上的区别

 shell if     c语言if
0为真,走then  正好相反,非0走then
 不支持整数变量直接if
必须:if [ i –ne 0 ]

但支持字符串变量直接if
if [ str ] 如果字符串非0

 支持变量直接if
if (i )

=================================以command作为if 条件===================================

以多条command或者函数作为if 条件

echo –n “input:”
read user

if
多条指令,这些命令之间相当于“and”(与)
grep $user /etc/passwd >/tmp/null
who -u | grep $user
then             上边的指令都执行成功,返回值$?为0,0为真,运行then
echo “$user has logged”
else     指令执行失败,$?为1,运行else
echo “$user has not logged”
fi

# sh test.sh
input : macg
macg     pts/0        May 15 15:55   .          2075 (192.168.1.100)
macg has logged

# sh test.sh
input : ddd
ddd has not logged

以函数作为if条件  (函数就相当于command,函数的优点是其return值可以自定义)

if
以函数作为if条件,
getyn
then   函数reture值0为真,走then
echo ” your answer is yes”
else  函数return值非0为假,走else
echo “your anser is no”
fi

if command  等价于 command+if $?

$ vi testsh.sh
#!/bin/sh

if
cat 111-tmp.txt | grep ting1
then
echo found
else
echo “no found”
fi

 $ vi testsh.sh
#!/bin/sh

cat 111-tmp.txt | grep ting1

if [ $? -eq 0 ]
then
echo $?
echo found
else
echo $?
echo “no found”
fi

$ sh testsh.sh
no found
$ sh testsh.sh
1
no found
$ vi 111-tmp.txt
that is 222file
thisting1 is 111file

$ sh testsh.sh
thisting1 is 111file
found

$ vi 111-tmp.txt
that is 222file
thisting1 is 111file

$ sh testsh.sh
thisting1 is 111file
0
found

========================================以条件表达式作为 if条件=============================

传统if 从句子——以条件表达式作为 if条件
if [ 条件表达式 ]
then
command
command
command
else
command
command
fi

条件表达式

  • 文件表达式

if [ -f  file ]    如果文件存在
if [ -d …   ]    如果目录存在
if [ -s file  ]    如果文件存在且非空
if [ -r file  ]    如果文件存在且可读
if [ -w file  ]    如果文件存在且可写
if [ -x file  ]    如果文件存在且可执行

  • 整数变量表达式

if [ int1 -eq int2 ]    如果int1等于int2
if [ int1 -ne int2 ]    如果不等于
if [ int1 -ge int2 ]       如果>=
if [ int1 -gt int2 ]       如果>
if [ int1 -le int2 ]       如果<=
if [ int1 -lt int2 ]       如果<

  •    字符串变量表达式

If  [ $a = $b ]                 如果string1等于string2
字符串允许使用赋值号做等号
if  [ $string1 !=  $string2 ]   如果string1不等于string2
if  [ -n $string  ]             如果string 非空(非0),返回0(true)
if  [ -z $string  ]             如果string 为空
if  [ $sting ]                  如果string 非空,返回0 (和-n类似)

条件表达式引用变量要带$

if [ a = b ] ;then
echo equal
else
echo no equal
fi
[macg@machome ~]$ sh test.sh
input a:
5
input b:
5
no equal  (等于表达式没比较$a和$b,而是比较和a和b,自然a!=b)

改正:

if [ $a = $b ] ;then
echo equal
else
echo no equal
fi
[macg@machome ~]$ sh test.sh
input a:
5
input b:
5
equal

-eq  -ne  -lt  -nt只能用于整数,不适用于字符串,字符串等于用赋值号=

[macg@machome ~]$ vi test.sh
echo -n “input your choice:”
read var
if  [ $var -eq “yes” ]
then
echo $var
fi
[macg@machome ~]$ sh -x test.sh
input your choice:
y
test.sh: line 3: test: y: integer expression_r_r_r expected
期望整数形式,即-eq不支持字符串

=放在别的地方是赋值,放在if [ ] 里就是字符串等于,shell里面没有==的,那是c语言的等于

无空格的字符串,可以加” “,也可以不加

[macg@machome ~]$ vi test.sh
echo “input a:”
read a
echo “input is $a”
if [ $a = 123 ] ; then
echo equal123
fi
[macg@machome ~]$ sh test.sh
input a:
123
input is 123
equal123

= 作为等于时,其两边都必须加空格,否则失效
等号也是操作符,必须和其他变量,关键字,用空格格开 (等号做赋值号时正好相反,两边不能有空格)

[macg@machome ~]$ vi test.sh

echo “input your choice:”
read var
if [ $var=”yes” ]
then
echo $var
echo “input is correct”
else
echo $var
echo “input error”
fi

[macg@machome ~]$ vi test.sh

echo “input your choice:”
read var
if [ $var = “yes” ]   在等号两边加空格
then
echo $var
echo “input is correct”
else
echo $var
echo “input error”
fi

[macg@machome ~]$ sh test.sh
input your choice:
y
y
input is correct
[macg@machome ~]$ sh test.sh
input your choice:
n
n
input is correct
输错了也走then,都走then,为什么?
因为if把$var=”yes”连读成一个变量,而此变量为空,返回1,则走else
 [macg@machome ~]$ sh test.sh
input your choice:
y
y
input error
[macg@machome ~]$ sh test.sh
input your choice:
no
no
input error
一切正常

If  [  $ANS  ]     等价于  if [ -n $ANS ]
如果字符串变量非空(then) , 空(else)

echo “input your choice:”
read ANS

if [ $ANS ]
then
echo no empty
else
echo empth
fi

[macg@machome ~]$ sh test.sh
input your choice:                       回车

empth                                    说明“回车”就是空串
[macg@machome ~]$ sh test.sh
input your choice:
34
no empty

整数条件表达式,大于,小于,shell里没有> 和< ,会被当作尖括号,只有-ge,-gt,-le,lt

[macg@machome ~]$ vi test.sh

echo “input a:”
read a
if  [ $a -ge 100 ] ; then
echo 3bit
else
echo 2bit
fi

[macg@machome ~]$ sh test.sh
input a:
123
3bit
[macg@machome ~]$ sh test.sh
input a:
20
2bit

整数操作符号-ge,-gt,-le,-lt, 别忘了加-

if  test $a  ge 100 ; then

[macg@machome ~]$ sh test.sh
test.sh: line 4: test: ge: binary operator expected

if  test $a -ge 100 ; then

[macg@machome ~]$ sh test.sh
input a:
123
3bit

============================逻辑表达式=========================================

逻辑非 !                   条件表达式的相反
if [ ! 表达式 ]
if [ ! -d $num ]                        如果不存在目录$num

逻辑与 –a                    条件表达式的并列
if [ 表达式1  –a  表达式2 ]

逻辑或 -o                    条件表达式的或
if [ 表达式1  –o 表达式2 ]

逻辑表达式

  •     表达式与前面的=  != -d –f –x -ne -eq -lt等合用
  •     逻辑符号就正常的接其他表达式,没有任何括号( ),就是并列

if [ -z “$JHHOME” -a -d $HOME/$num ]

  •     注意逻辑与-a与逻辑或-o很容易和其他字符串或文件的运算符号搞混了

最常见的赋值形式,赋值前对=两边的变量都进行评测
左边测变量是否为空,右边测目录(值)是否存在(值是否有效)

[macg@mac-home ~]$ vi test.sh
:
echo “input the num:”
read num
echo “input is $num”

if [ -z “$JHHOME” -a -d $HOME/$num ]   如果变量$JHHOME为空,且$HOME/$num目录存在
then
JHHOME=$HOME/$num                      则赋值
fi

echo “JHHOME is $JHHOME”

———————–
[macg@mac-home ~]$ sh test.sh
input the num:
ppp
input is ppp
JHHOME is

目录-d $HOME/$num   不存在,所以$JHHOME没被then赋值

[macg@mac-home ~]$ mkdir ppp
[macg@mac-home ~]$ sh test.sh
input the num:
ppp
input is ppp
JHHOME is /home/macg/ppp

一个-o的例子,其中却揭示了”=”必须两边留空格的问题

echo “input your choice:”
read ANS

if [ $ANS=”Yes” -o $ANS=”yes” -o $ANS=”y” -o $ANS=”Y” ]
then
ANS=”y”
else
ANS=”n”
fi

echo $ANS

[macg@machome ~]$ sh test.sh
input your choice:
n
y
[macg@machome ~]$ sh test.sh
input your choice:
no
y
为什么输入不是yes,结果仍是y(走then)
因为=被连读了,成了变量$ANS=”Yes”,而变量又为空,所以走else了

 

[macg@machome ~]$ vi test.sh

echo “input your choice:”
read ANS    echo “input your choice:”
read ANS

if [ $ANS = “Yes” -o $ANS = “yes” -o $ANS = “y” -o $ANS = “Y” ]
then
ANS=”y”
else
ANS=”n”
fi

echo $ANS

[macg@machome ~]$ sh test.sh
input your choice:
no
n
[macg@machome ~]$ sh test.sh
input your choice:
yes
y
[macg@machome ~]$ sh test.sh
input your choice:
y
y

===================以  test 条件表达式 作为if条件===================================

if test $num -eq 0      等价于   if [ $num –eq 0 ]

test  表达式,没有 [  ]
if test $num -eq 0
then
echo “try again”
else
echo “good”
fi

man test

[macg@machome ~]$ man test
[(1)                             User Commands                            [(1)

SYNOPSIS
test EXPRESSION
[ EXPRESSION ]

[-n] STRING
the length of STRING is nonzero          -n和直接$str都是非0条件

-z STRING
the length of STRING is zero

STRING1 = STRING2
the strings are equal

STRING1 != STRING2
the strings are not equal

INTEGER1 -eq INTEGER2
INTEGER1 is equal to INTEGER2

INTEGER1 -ge INTEGER2
INTEGER1 is greater than or equal to INTEGER2

INTEGER1 -gt INTEGER2
INTEGER1 is greater than INTEGER2

INTEGER1 -le INTEGER2
INTEGER1 is less than or equal to INTEGER2

INTEGER1 -lt INTEGER2
INTEGER1 is less than INTEGER2

INTEGER1 -ne INTEGER2
INTEGER1 is not equal to INTEGER2

FILE1 -nt FILE2
FILE1 is newer (modification date) than FILE2

FILE1 -ot FILE2
FILE1 is older than FILE2

-b FILE
FILE exists and is block special

-c FILE
FILE exists and is character special

-d FILE
FILE exists and is a directory

-e FILE
FILE exists                                 文件存在

-f FILE
FILE exists and is a regular file     文件存在且是普通文件

-h FILE
FILE exists and is a symbolic link (same as -L)

-L FILE
FILE exists and is a symbolic link (same as -h)

-G FILE
FILE exists and is owned by the effective group ID

-O FILE
FILE exists and is owned by the effective user ID

-p FILE
FILE exists and is a named pipe

-s FILE
FILE exists and has a size greater than zero

-S FILE
FILE exists and is a socket

-w FILE
FILE exists and is writable

-x FILE
FILE exists and is executable

======================if简化语句=================================

最常用的简化if语句

   && 如果是“前面”,则“后面”
[ -f /var/run/dhcpd.pid ] && rm /var/run/dhcpd.pid    检查 文件是否存在,如果存在就删掉
   ||   如果不是“前面”,则后面
[ -f /usr/sbin/dhcpd ] || exit 0    检验文件是否存在,如果存在就退出

用简化 if 和$1,$2,$3来检测参数,不合理就调用help
[ -z “$1” ] && help                 如果第一个参数不存在(-z  字符串长度为0 )
[ “$1” = “-h” ] && help                        如果第一个参数是-h,就显示help

例子
#!/bin/sh

[ -f “/etc/sysconfig/network-scripts/ifcfg-eth0” ] && rm -f /etc/sysconfig/network-scripts/ifcfg-eth0
cp ifcfg-eth0.bridge /etc/sysconfig/network-scripts/ifcfg-eth0

[ -f “/etc/sysconfig/network-scripts/ifcfg-eth1” ] && rm -f /etc/sysconfig/network-scripts/ifcfg-eth1
cp ifcfg-eth1.bridge /etc/sysconfig/network-scripts/ifcfg-eth1

[ -f “/etc/sysconfig/network-scripts/ifcfg-eth0:1” ] && rm -f /etc/sysconfig/network-scripts/ifcfg-eth0:1

目前路由器做了端口映射,外网访问路由器及ESXI的web client都需要输入端口号,但是强迫症总是看着不舒服

既然实现了顶级域名的动态解析,是否可用反向代理的方式来代替端口映射呢,于是测了下443端口,好,没有被block,然后就是用nginx进行反向代理了,之前的极路由内置的就是nginx,极路由基于openwrt,所以路由器上运行nginx可行,那么有没有梅林可用的nginx呢?找了半天终于找到了,而nginx反向代理https需要ssl证书,虽说自建的ssl证书也可行但既然有免费的为什么不用呢,沃通证书不考虑,Let’s Encrypt证书比较新奇就用这个吧,使用DNS验证更是相当方便。

以下是步骤:

1、挂载U盘。

Enware-ng推荐安装在扩展设备上,内置存储虽然也可但毕竟寸土寸金,而且扩展看设备空间大不怕折腾

mkdir /mnt/sda1 && mount /dev/sda /mnt/sda1

2、梅林固件安装Enware-ng,按照说明进行安装,然后执行

opkg install nginx

3、获得Let’s Encrypt证书,DNS方式验证,github地址:https://github.com/xdtianyu/scripts/tree/master/le-dns,此脚本依赖另一个脚本letsencrypt.sh,须先下载letsencrypt.sh并进入letsencrypt.sh才可调用letsencrypt.sh脚本(目录名和脚本名都是letsencrypt.sh)。

mkdir -p /opt/usr/ssl/ && cd /opt/usr/ssl/
git clone https://github.com/lukas2511/letsencrypt.sh.git
cd letsencrypt.sh
wget https://github.com/xdtianyu/scripts/raw/master/le-dns/le-cloudxns.sh
wget https://github.com/xdtianyu/scripts/raw/master/le-dns/cloudxns.conf
chmod +x le-cloudxns.sh

修改cloudxns.conf

API_KEY="YOUR_API_KEY"
SECRET_KEY="YOUR_SECRET_KEY"
DOMAIN="example.com"
CERT_DOMAINS="example.com www.example.com im.example.com"
#ECC=TRUE

修改其中的 API_KEY 及 SECRET_KEY 为您的 cloudxns api key ,修改 DOMAIN 为你的根域名,修改 CERT_DOMAINS 为您要签的域名列表,需要 ECC 证书时请取消 #ECC=TRUE 的注释。

4、执行le-cloudxns.sh

./le-cloudxns.sh cloudxns.conf

如果是一般linux环境下,直接执行不会有任何问题,但是在梅林固件下,会因为缺少一些命令而导致执行失败,所以在执行le-cloudxns.sh前需进行以下两步:

①安装缺少的相关命令

opkg install bash coreutils-mktemp

②由于bash是额外安装的,路径不再是不再是默认的/bin/bash,所以脚本的解释行要修改以下,查看当前目录下的所有*.sh文件的第一行,如果为

#!/bin/bash

则修改为

#!/usr/bin/env bash

5、修改nginx配置文件nginx.conf


user  nobody;
worker_processes  2;

events {
    use epoll;
    worker_connections  64;
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    sendfile	on;
    tcp_nopush	on;
    keepalive_timeout	65;

    server {
		listen	443;
		server_name domain;

		ssl on;
		ssl_certificate	/opt/usr/ssl/letsencrypt.sh/certs/fullchain.pem;
		ssl_certificate_key /opt/usr/ssl/letsencrypt.sh/certs/privkey.pem; 
		ssl_ciphers	EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+ECDSA+AES128:EECDH+aRSA+AES128:RSA+AES128:EECDH+ECDSA+AES256:EECDH+aRSA+AES256:RSA+AES256:EECDH+ECDSA+3DES:EECDH+aRSA+3DES:RSA+3DES:!MD5;
		ssl_prefer_server_ciphers   on;
	
		location / {
			proxy_set_header Host $host;
			proxy_set_header X-Forwarded-For $remote_addr;
			proxy_pass https://192.168.199.1:7520/;
	}
}
	server {
		listen	443;
		server_name domain;
	
		ssl on;
		ssl_certificate	/opt/usr/ssl/letsencrypt.sh/certs/fullchain.pem;
		ssl_certificate_key /opt/usr/ssl/letsencrypt.sh/certs/privkey.pem; 
		ssl_ciphers	EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+ECDSA+AES128:EECDH+aRSA+AES128:RSA+AES128:EECDH+ECDSA+AES256:EECDH+aRSA+AES256:RSA+AES256:EECDH+ECDSA+3DES:EECDH+aRSA+3DES:RSA+3DES:!MD5;
		ssl_prefer_server_ciphers   on;
	
		location / {
			proxy_set_header Host          $host;
			proxy_set_header X-Real-IP     $remote_addr;
			proxy_set_header X-Forward-For $remote_addr;
			proxy_set_header X-Forward-For $proxy_add_x_forwarded_for;
			proxy_pass https://192.168.199.200/;
		}
    }

}

6、启动nginx

nginx

补充:
到这里就可以运行了,但是在我的是设备上(RV 6300 V2)重启就出现问题了。
问题出现在U盘挂载目录上,Enware-ng默认认为U盘挂载到/mnt/sda1上,但实际上是挂载到了/mnt/sda上,所以要修改一下:

修改/jffs/scripts/post-mount文件

#!/bin/sh

if [ "$1" = "/tmp/mnt/sda1" ] ; then #将sda1改为sda
ln -nsf $1/entware-ng.arm /tmp/opt
fi

sleep 2
if [ -f /opt/swap ]
then
echo -e "Mounting swap file..."
swapon /opt/swap
else
echo -e "Swap file not found or /opt is not mounted..."
fi

重启然后查看存在nginx进程,问题解决

最后在/jffs/scripts/services-start加入定时任务

cru a sslupdate "42 4 10/25 * * /opt/usr/ssl/letsencrypt.sh/le-dnspod.sh /opt/usr/ssl/letsencrypt.sh/dnspod.conf >> /opt/var/log/le-dnspod.log 2>&1"

梅林固件默认不打开443端口,而且在管理界面也不允许设置443端口,所以使用iptables命令打开443端口

iptables -I INPUT 7 -p tcp --dport 443 -j ACCEPT

但是随便设置一条端口转发规则都会使443失效,要解决这个问题可以建立一个定时脚本,检测443端口

vim open443.sh
#!/bin/sh
path=`iptables -nvL | grep 443 | awk '{print $1}'`
if [ ! -n "$path" ]
then
iptables -I INPUT 7 -p tcp --dport 443 -j ACCEPT
fi

之前用的都是免费的域名,全都是二三级域名,相当难记,难到手上的顶级域名就不能免费实现动态解析吗?当然可以,目前DNS服务商都有自己的api,通过这些api即可实现多种功能,当然动态解析也不在话下,当然,只是实现解析的话最好用shell,移植到路由器上也方便

一、dnspod(路由器实现)

路由器是R6300 V2 梅林固件,没有内置dnspod ddns,但有custom ddns support

首先需要ddns脚本,已经找到了,但是脚本结尾返回值判断写反了,开始一直提示注册不成功


if [ $? -eq 0 ]; then #此处0改为1
/sbin/ddns_custom_updated 1 #通知系统注册成功
else
/sbin/ddns_custom_updated 0 #通知系统注册失败
fi

将脚本命名为ddns-start,放在/jffs/scripts/目录下并添加执行权限

进入路由器ddns设置界面进行相关设置就可以了

二、alidns(服务器实现)

同样是shell脚本,地址在这里,作者说的也很详细

公司的局域网很复杂,之前一直没有时间了解一下公司的网络拓扑结构。

用到的工具只需要路由追踪工具和ping工具。

路由追踪工具:

windows端:tracert命令,Best Trace

Linux端:traceroute命令

Android端:Best Trace APP

我所在的部门有两台路由器,一台交换机,三个网段

交换机:192.168.71.*

TP-LINK路由器:192.168.0.*

H3C路由器:192.168.0.*

OK,介绍完毕

 

交换机IP段:

首先PC进行路由追踪,PC连接交换机,IP地址为192.167.71.172

可以看到数据包经过了3个节点,到第三个节点到达公网IP。

使用windows下的tracert命令测试,结果相同

 

H3C路由器IP段:

然后在H3C路由器下进行路由追踪,正巧一台内网服务器在其IP段,SSH登陆后测试

数据包经过了4个节点到达外网,其中第二跳与交换机所在IP段第一跳相同,说明H3C路由器WAN口与交换机处在同一级

 

TP-LINK路由器:

TP-LINK路由器走的是另一条外网,使用手机连入WIFI进行测试

可以看到数据包先后经历了5跳到达外网,而部门这几台网络设备都接在192.168.71.254这台路由器上,然后下一跳是172.16.100.253,依旧是内网,下一跳是外网的出口,可以看到这台路由器是公司局域网的根路由器

所以公司的网络拓扑结构基本上就出来了,而后发现TP-LINK路由器下的设备可以访问IP段192.167.71.*的设备,应该是做了静态路由

 

所以最终的网络拓扑结构图为

URL:https://linuxtoy.org/archives/the-best-tips-and-tricks-for-bash.html

Bash 是我们经常与之打交道的 Shell 程序,本文针对其使用技巧进行了搜罗。相信在你看过这些内容之后,定会在 Bash 的世界里游刃有余。

  • 从历史中执行命令

    有时候,我们需要在 Bash 中重复执行先前的命令。你当然可以使用上方向键来查看之前曾经运行过的命令。但这里有一种更好的方式:你可以按 Ctrl + r 组合键进入历史搜索模式,一旦找到需要重复执行的命令,按回车键即可。

  • 重复命令参数

    先来看一个例子:

    mkdir /path/to/exampledir
    cd !$
    

    本例中,第一行命令将创建一个目录,而第二行的命令则转到刚创建的目录。这里,!$ 的作用就是重复前一个命令的参数。事实上,不仅是命令的参数可以重复,命令的选项同样可以。另外,Esc + . 快捷键可以切换这些命令参数或选项。

  • 用于编辑的快捷键
    • Ctrl + a:将光标定位到命令的开头
    • Ctrl + e:与上一个快捷键相反,将光标定位到命令的结尾
    • Ctrl + u:剪切光标之前的内容
    • Ctrl + k:与上一个快捷键相反,剪切光标之后的内容
    • Ctrl + y:粘贴以上两个快捷键所剪切的内容
    • Ctrl + t:交换光标之前两个字符的顺序
    • Ctrl + w:删除光标左边的参数(选项)或内容
    • Ctrl + l:清屏
  • 处理作业

    首先,使用 Ctrl + z 快捷键可以让正在执行的命令挂起。如果要让该进程在后台执行,那么可以执行 bg 命令。而 fg 命令则可以让该进程重新回到前台来。使用 jobs 命令能够查看到哪些进程在后台执行。

    你也可以在 fg 或 bg 命令中使用作业 id,如:fg %3

    又如:bg %7

  • 使用置换
    • 命令置换

      先看例子:

      du -h -a -c $(find . -name *.conf 2>&-)
      

      注意 $() 中的部分,这将告诉 Bash 运行 find 命令,然后把返回的结果作为 du 的参数。

    • 进程置换

      仍然先看例子:

      diff <(ps axo comm) <(ssh user@host ps axo comm)
      

      该命令将比较本地系统和远程系统中正在运行的进程。请注意 <() 中的部分。

    • xargs

      看例:

      find . -name *.conf -print0 | xargs -0 grep -l -Z mem_limit | xargs -0 -i cp {} {}.bak
      

      该命令将备份当前目录中的所有 .conf 文件。

  • 使用管道

    下面是一个简单的使用管道的例子:

    ps aux | grep init
    

    这里,| 操作符将 ps aux 的输出重定向给 grep init。

    下面还有两个稍微复杂点的例子:

    ps aux | tee filename | grep init
    

    及:

    ps aux | tee -a filename | grep init
    
  • 将标准输出保存为文件

    你可以将命令的标准输出内容保存到一个文件中,举例如下:ps aux > filename

    注意其中的 > 符号。

    你也可以将这些输出内容追加到一个已存在的文件中:ps aux >> filename

    你还可以分割一个较长的行:

    command1 | command2 | ... | commandN > tempfile1
    cat tempfile1 | command1 | command2 | ... | commandN > tempfile2
    
  • 标准流:重定向与组合

    重定向流的例子:

    ps aux 2>&1 | grep init
    

    这里的数字代表:

    • 0:stdin
    • 1:stdout
    • 2:sterr

    上面的命令中,grep init 不仅搜索 ps aux 的标准输出,而且搜索 sterr 输出。

本文是 shell 编程系列的第二篇,主要介绍 bash 脚本是如何执行命令的。

目录

前言

本文是 shell 编程系列的第二篇,主要介绍 bash 脚本是如何执行命令的。通过本文,您应该可以解决以下问题:

  1. 脚本开始的 #! 到底是怎么起作用的?
  2. bash 执行过程中的字符串判断顺序究竟是什么样?
  3. 如果我们定义了一个函数叫 ls,那么调用 ls 的时候,到底 bash 是执行 ls 函数还是 ls 命令?
  4. 内建命令和外建命令到底有什么差别?
  5. 程序退出的时候要注意什么?

以魔法 #! 开始

一个脚本程序的开始方式都比较统一,它们几乎都开始于一个 #! 符号。这个符号的作用大家似乎也都知道,叫做声明解释器。脚本语言跟编译型语言的不一样之处主要是脚本语言需要解释器。因为脚本语言主要是文本,而系统中能够执行的文件实际上都是可执行的二进制文件,就是编译好的文件。文本的好处是人看方便,但是操作系统并不能直接执行,所以就需要将文本内容传递给一个可执行的二进制文件进行解析,再由这个可执行的二进制文件根据脚本的内容所确定的行为进行执行。可以做这种解析执行的二进制可执行程序都可以叫做解释器。

脚本开头的 #! 就是用来声明本文件的文本内容是交给哪个解释器进行解释的。比如我们写 bash 脚本,一般声明的方法是 #!/bin/bash#!/bin/sh。如果写的是一个 Python 脚本,就用 #!/usr/bin/python。当然,在不同环境的系统中,这个解释器放的路径可能不一样,所以固定写一个路径的方式就可能造成脚本在不同环境的系统中不通用的情况,于是就出现了这样的写法:

 #!/usr/bin/env 脚本解释器名称

这就利用了 env 命令可以得到可执行程序执行路径的功能,让脚本自行找到在当前系统上到底解释器在什么路径。让脚本更具通用性。但是大家有没有想过一个问题,大多数脚本语言都是将 #后面出现的字符当作是注释,在脚本中并不起作用。这个 #! 和这个注释的规则不冲突么?

这就要从 #! 符号起作用的原因说起,其实也很简单,这个功能是由操作系统的程序载入器做的。在 Linux 操作系统上,除了 1 号进程以外,我们可以认为其它所有进程都是由父进程 fork 出来的。所以对 bash 来说,所谓的载入一个脚本执行,无非就是父进程调用 fork()exec() 来产生一个子进程。这 #! 就是在内核处理 exec 的时候进行解析的。

内核中整个调用过程如下(Linux 4.4),内核处理 exec 族函数的主要实现在 fs/exec.c 文件的 do_execveat_common() 方法中,其中调用 exec_binprm() 方法处理执行逻辑,这函数中使用 search_binary_handler() 对要加载的文件进行各种格式的判断,脚本(script)只是其中的一种。确定是 script 格式后,就会调用 script 格式对应的 load_binary 方法:load_script() 进行处理, #! 就是在这个函数中解析的。解析到了 #! 以后,内核会取其后面的可执行程序路径,再传递给 search_binary_handler() 重新解析。这样最终找到真正的可执行二进制文件进行相关执行操作。

因此,对脚本第一行的 #! 解析,其实是内核给我们变的魔术#! 后面的路径内容在起作用的时候还没有交给脚本解释器。很多人认为 #! 这一行是脚本解释器去解析的,然而并不是。了解了原理之后,也顺便明白了为什么 #! 一定要写在第一行的前两个字符,因为这是在内核里写死的,它就只检查前两个字符。当内核帮你选好了脚本解释器之后,后续的工作就都交给解释器做了。脚本的所有内容也都会原封不动的交给解释器再次解释,是的,包括 #!。但是由于对于解释器来说,# 开头的字符串都是注释,并不生效,所以解释器自然对 #! 后面所有的内容无感,继续解释对于它来说有意义的字符串去了。

我们可以用一个自显示脚本来观察一下这个事情,什么是自显示脚本?无非就是 #!/bin/cat,这样文本的所有内容包括 #! 行都会交给 cat 进行显示:

[zorro@zorrozou-pc0 bash]$ cat cat.sh 
#!/bin/cat

echo "hello world!"
[zorro@zorrozou-pc0 bash]$ ./cat.sh 
#!/bin/cat

echo "hello world!"

或者自删除脚本:

[zorro@zorrozou-pc0 bash]$ cat rm.sh 
#!/bin/rm

echo "hello world!"
[zorro@zorrozou-pc0 bash]$ chmod +x rm.sh 
[zorro@zorrozou-pc0 bash]$ ./rm.sh 
[zorro@zorrozou-pc0 bash]$ cat rm.sh
cat: rm.sh: No such file or directory

这就是 #! 的本质。

bash 如何执行 shell 命令?

刚才我们从 #! 的作用原理讲解了一个 bash 脚本是如何被加载的。就是说当 #!/bin/bash 的时候,实际上内核给我们启动了一个 bash 进程,然后把脚本内容都传递给 bash 进行解析执行。实际上,无论在脚本里还是在命令行中,bash 对文本的解析方法大致都是一样的。首先,bash 会以一些特殊字符作为分隔符,将文本进行分段解析。最主要的分隔符无疑就是回车,类似功能的分隔符还有分号”;“。所以在 bash 脚本中是以回车或者分号作为一行命令结束的标志的。这基本上就是第一层级的解析,主要目的是将大段的命令行进行分段。

之后是第二层级解析,这一层级主要是区分所要执行的命令。这一层级主要解析的字符是管道”|“,&&||这样的可以起到连接命令作用的特殊字符。这一层级解析完后,bash 就能拿到最基本的一个个的要执行的命令了。

当然拿到命令之后还要继续第三层解析,这一层主要是区分出要执行的命令和其参数,主要解析的是空格和 tab 字符。这一层次解析完之后,bash 才开始对最基本的字符串进行解释工作。当然,绝大多数解析完的字符串,bash 都是在 fork 之后将其传递给 exec 进行执行,然后 wait 其执行完毕之后再解析下一行。这就是 bash 脚本也被叫做批处理脚本的原因,主要执行过程是一个一个指令串行执行的,上一个执行完才执行下一个。以上这个过程并不能涵盖 bash 解释字符串的全过程,实际情况要比这复杂。

bash 在解释命令的时候为了方便一些操作和提高某些效率做了不少特性,包括 alias 功能和外部命令路径的 hash 功能。bash 还因为某些功能不能做成外部命令,所以必须实现一些内建命令,比如 cdpwd 等命令。当然除了内建命令以外,bash 还要实现一些关键字,比如其编程语法结构的 if或是 while 这样的功能。实际上作为一种编程语言,bash 还要实现函数功能,我们可以理解为,bash 的函数就是将一堆命令做成一个命令,然后调用执行这个名字,bash 就是去执行事先封装好的那堆命令。

好吧,问题来了:我们已知有一个内建命令叫做 cd,如果此时我们又建立一个 alias 也叫 cd,那么当我在 bash 中敲入 cd 并回车之后,bash 究竟是将它当成内建命令解释还是当成 alias 解释?同样,如果 cd 又是一个外部命令呢?如果又是一个 hash 索引呢?如果又是一个关键字或函数呢?

实际上 bash 在做这些功能的时候已经安排好了它们在名字冲突的情况下究竟该先以什么方式解释。优先顺序是:

  1. 别名:alias
  2. 关键字:keyword
  3. 函数:function
  4. 内建命令:built in
  5. 哈希索引:hash
  6. 外部命令:command

这些 bash 要判断的字符串类型都可以用 type 命令进行判断,如:

[zorro@zorrozou-pc0 bash]$ type egrep
egrep is aliased to `egrep --color=auto'
[zorro@zorrozou-pc0 bash]$ type if
if is a  shell  keyword
[zorro@zorrozou-pc0 bash]$ type pwd
pwd is a  shell  builtin
[zorro@zorrozou-pc0 bash]$ type passwd
passwd is /usr/bin/passwd

别名 alias

bash 提供了一种别名(alias)功能,可以将某一个字符串做成另一个字符串的别名,使用方法如下:

[zorro@zorrozou-pc0 bash]$ alias cat='cat -n'
[zorro@zorrozou-pc0 bash]$ cat /etc/passwd
     1  root:x:0:0:root:/root:/bin/bash 
     2  bin:x:1:1:bin:/bin:/usr/bin/nologin
     3  daemon:x:2:2:daemon:/:/usr/bin/nologin
     4  mail:x:8:12:mail:/var/spool/mail:/usr/bin/nologin
     ......

于是我们再使用 cat 命令的时候,bash 会将其解释为 cat -n

这个功能在交互方式进行 bash 操作的时候可以提高不少效率。如果我们发现我们常用到某命令的某个参数的时候,就可以将其做成 alias,以后就可以方便使用了。交互 bash 中,我们可以用 alias 命令查看目前已经有的 alias 列表。可以用 unalias 取消这个别名设置:

[zorro@zorrozou-pc0 bash]$ alias 
alias cat='cat -n'

[zorro@zorrozou-pc0 bash]$ unalias cat

alias 功能在交互打开的 bash 中是默认开启的,但是在 bash 脚本中是默认关闭的。

1
2
3
4
5
6
#!/bin/bash

#shopt -s expand_aliases

alias ls='ls -l'
ls /etc

此时本程序输出:

[zorro@zorrozou-pc0 bash]$ ./alias.sh 
adjtime       cgconfig.conf         docker       group      ifplugd     libao.conf      mail.rc      netconfig       passwd   request-key.conf   shell s           udisks2
adobe         cgrules.conf          drirc   ...

使用注释行中的 shopt -s expand_aliases 命令可以打开 alias 功能支持,我们将这行注释取消掉之后的执行结果为:

[zorro@zorrozou-pc0 bash]$ ./alias.sh 
total 1544
-rw-r--r-- 1 root    root        44 11月 13 19:53 adjtime
drwxr-xr-x 2 root    root      4096 4月  20 09:34 adobe
-rw-r--r-- 1 root    root       389 4月  18 22:19 appstream.conf
-rw-r--r-- 1 root    root         0 10月  1 2015 arch-release
-rw-r--r-- 1 root    root       260 7月   1 2014 asound.conf
drwxr-xr-x 3 root    root      4096 3月  11 10:09 avahi

这就是 bash 的 alias 功能。

关键字:keyword

关键字的概念很简单,主要就是 bash 提供的语法。比如 ifwhilefunction 等等。对这些关键字使用 type 命令会显示:

[zorro@zorrozou-pc0 bash]$ type function
function is a  shell  keyword

说明这是一个 keyword。我想这个概念没什么可以解释的了,无非就是 bash 提供的一种语法而已。只是要注意,bash 会在判断 alias 之后才来判断字符串是不是个 keyword。就是说,我们还是可以创建一个叫 ifalias,并且在执行的时候,bash 只把它当成 alias 看。

[zorro@zorrozou-pc0 bash]$ alias if='echo zorro'
[zorro@zorrozou-pc0 bash]$ if
zorro
[zorro@zorrozou-pc0 bash]$ unalias if

函数:function

bash 在判断完字符串不是一个关键字之后,将会检查其是不是一个函数。在 bash 编程中,我们可以使用关键字 function 来定义一个函数,当然这个关键字其实也可以省略:

name () compound-command [redirection]
function name [()] compound-command [redirection]

语法结构中的 compound-command 一般是放在 {} 里的一个命令列表(list)。定义好的函数其实就是一系列 shell 命令的封装,并且它还具有很多 bash 程序的特征,比如在函数内部可以使用 $1$2 等这样的变量来判断函数的参数,也可以对函数使用重定向功能。

关于函数的更细节讨论我们会在后续的文章中展开说明,在这里我们只需要知道它对于 bash 来说是第几个被解释的即可。

内建命令:built in

在判断完函数之后,bash 将查看给的字符串是不是一个内建命令。内建命令是相对于外建命令来说的。其实我们在 bash 中执行的命令最常见的是外建(外部)命令。比如常见的 lsfindpasswd 等。这些外建命令的特点是,它们是作为一个可执行程序放在 $PATH 变量所包含的目录中的。bash 在执行这些命令的时候,都会进行 fork(),exec()并且wait()。就是用标准的打开子进程的方式处理外部命令。但是内建命令不同,这些命令都是 bash 自身实现的命令,它们不依靠外部的可执行文件存在。只要有 bash,这些命令就可以执行。典型的内建命令有 cdpwd 等。大家可以直接 help cd 或者任何一个内建命令来查看它们的帮助。大家还可以 man bash 来查看 bash 相关的帮助,当然也包括所有的内建命令。

其实内建命令的个数并不会很多,一共大概就这些:

:,  ., [, alias, bg, bind, break, builtin, caller, cd, command, compgen, complete, compopt, continue, declare, dirs, disown, echo, enable, eval, exec, exit, export, false, fc,
   fg, getopts, hash, help, history, jobs, kill, let, local, logout, mapfile, popd, printf, pushd, pwd, read, readonly, return, set, shift, shopt, source, suspend,  test,  times,  trap,
   true, type, typeset, ulimit, umask, unalias, unset, wait

我们在后续的文章中会展开讲解这些命令的功能。

哈希索引:hash

hash 功能实际上是针对外部命令做的一个功能。刚才我们已经知道了,外部命令都是放在 $PATH 变量对应的路径中的可执行文件。bash 在执行一个外部命令时所需要做的操作是:如果发现这个命令是个外部命令就按照 $PATH变量中按照目录路径的顺序,在每个目录中都遍历一遍,看看有没有对应的文件名。如果有,就 forkexecwait。我们系统上一般的 $PATH 内容如下:

[zorro@zorrozou-pc0 bash]$ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/lib/jvm/default/bin:/usr/bin/site_perl:/usr/bin/vendor_perl:/usr/bin/core_perl:/home/zorro/.local/bin:/home/zorro/bin

当然,很多系统上的 $PATH 变量包含的路径可能更多,目录中的文件数量也可能会很多。于是,遍历这些目录去查询文件名的行为就可能比较耗时。于是 bash 提供了一种功能,就是建立一个 hash 表,在第一次找到一个命令的路径之后,对其命令名和对应的路径建立一个 hash 索引。这样下次再执行这个命令的时候,就不用去遍历所有的目录了,只要查询索引就可以更快的找到命令路径,以加快执行程序的速度。

我们可以使用内建命令 hash 来查看当前已经建立缓存关系的命令和其命中次数:

[zorro@zorrozou-pc0 bash]$ hash
hits    command
   1    /usr/bin/flock
   4    /usr/bin/chmod
  20    /usr/bin/vim
   4    /usr/bin/cat
   1    /usr/bin/cp
   1    /usr/bin/mkdir
  16    /usr/bin/man
  27    /usr/bin/ls

这个命令也可以对当前的 hash 表进行操作,-r 参数用来清空当前 hash 表。手工创建一个 hash

[root@zorrozou-pc0 bash]# hash -p /usr/sbin/passwd psw
[root@zorrozou-pc0 bash]# psw
Enter new UNIX password: 
Retype new UNIX password:

此时我们就可以通过执行 psw 来执行 passwd 命令了。查看更详细的 hash 对应关系:

[root@zorrozou-pc0 bash]# hash -l
builtin hash -p /usr/bin/netdata netdata
builtin hash -p /usr/bin/df df
builtin hash -p /usr/bin/chmod chmod
builtin hash -p /usr/bin/vim vim
builtin hash -p /usr/bin/ps ps
builtin hash -p /usr/bin/man man
builtin hash -p /usr/bin/pacman pacman
builtin hash -p /usr/sbin/passwd psw
builtin hash -p /usr/bin/ls ls
builtin hash -p /usr/bin/ss ss
builtin hash -p /usr/bin/ip ip

删除某一个 hash 对应:

[root@zorrozou-pc0 bash]# hash -d psw
[root@zorrozou-pc0 bash]# hash -l
builtin hash -p /usr/bin/netdata netdata
builtin hash -p /usr/bin/df df
builtin hash -p /usr/bin/chmod chmod
builtin hash -p /usr/bin/vim vim
builtin hash -p /usr/bin/ps ps
builtin hash -p /usr/bin/man man
builtin hash -p /usr/bin/pacman pacman
builtin hash -p /usr/bin/ls ls
builtin hash -p /usr/bin/ss ss
builtin hash -p /usr/bin/ip ip

显示某一个 hash 对应的路径:

[root@zorrozou-pc0 bash]# hash -t chmod
/usr/bin/chmod

在交互式 bash 操作和 bash 编程中,hash 功能总是打开的,我们可以用 set +h 关闭 hash 功能。

[zorro@zorrozou-pc0 bash]$ cat hash.sh 
#!/bin/bash

#set +h

hash

hash -p /usr/bin/useradd uad

hash -t uad

uad

默认打开 hash 的脚本输出:

[zorro@zorrozou-pc0 bash]$ ./hash.sh 
hash: hash table empty
/usr/bin/useradd
Usage: uad [options] LOGIN
       uad -D
       uad -D [options]

Options:
  -b, --base-dir BASE_DIR       base directory for the home directory of the
                            new account
  -c, --comment COMMENT         GECOS field of the new account
  -d, --home-dir HOME_DIR       home directory of the new account
  -D, --defaults                print or change default useradd configuration
  -e, --expiredate EXPIRE_DATE  expiration date of the new account
  -f, --inactive INACTIVE       password inactivity period of the new account
  -g, --gid GROUP               name or ID of the primary group of the new
                            account
  -G, --groups GROUPS           list of supplementary groups of the new
                            account
  -h, --help                    display this help message and exit
  -k, --skel SKEL_DIR           use this alternative skeleton directory
  -K, --key KEY=VALUE           override /etc/login.defs defaults
  -l, --no-log-init             do not add the user to the lastlog and
                            faillog databases
  -m, --create-home             create the user's home directory
  -M, --no-create-home          do not create the user's home directory
  -N, --no-user-group           do not create a group with the same name as
                            the user
  -o, --non-unique              allow to create users with duplicate
                            (non-unique) UID
  -p, --password PASSWORD       encrypted password of the new account
  -r, --system                  create a system account
  -R, --root CHROOT_DIR         directory to chroot into
  -s, --shell SHELL             login shell of the new account
  -u, --uid UID                 user ID of the new account
  -U, --user-group              create a group with the same name as the user

关闭 hash 之后的输出:

[zorro@zorrozou-pc0 bash]$ ./hash.sh 
./hash.sh: line 5: hash: hashing disabled
./hash.sh: line 7: hash: hashing disabled
./hash.sh: line 9: hash: hashing disabled
./hash.sh: line 11: uad: command not found

外部命令:command

除了以上说明之外的命令都会当作外部命令处理。执行外部命令的固定动作就是在 $PATH 路径下找命令,找到之后 forkexecwait。如果没有这个可执行文件名,就报告命令不存在。这也是 bash 最后去判断的字符串类型。

外建命令都是通过 fork 调用打开子进程执行的,所以 bash 单纯只用外建命令是不能实现部分功能的。比如大家都知道 cd 命令是用来修改当前进程的工作目录的,如果这个功能使用外部命令实现,那么进程将 fork 打开一个子进程,子进程通过 chdir() 进行当前工作目录的修改时,实际上只改变了子进程本身的当前工作目录,而父进程 bash 的工作目录没变。之后子进程退出,返回到父进程的交互操作环境之后,用户会发现,当前的 bash 的 pwd 还在原来的目录下。所以大家应该可以理解,虽然我们的原则是尽量将所有命令都外部实现,但是还是有一些功能不能以创建子进程的方式达到目的,那么这些功能就必须内部实现。这就是内建命令必须存在的原因。另外要注意:bash 在正常调用内部命令的时候并不会像外部命令一样产生一个子进程

脚本的退出

一个 bash 脚本的退出一般有多种方式,比如使用 exit 退出或者所有脚本命令执行完之后退出。无论怎么样退出,脚本都会有个返回码,而且返回码可能不同。

任何命令执行完之后都有返回码,主要用来判断这个命令是否执行成功。在交互 bash 中,我们可以使用 $? 来查看上一个命令的返回码:

[zorro@zorrozou-pc0 bash]$ ls /123
ls: cannot access '/123': No such file or directory
[zorro@zorrozou-pc0 bash]$ echo $?
2
[zorro@zorrozou-pc0 bash]$ ls /
bin  boot  cgroup  data  dev  etc  home  lib  lib64  lost+found  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var
[zorro@zorrozou-pc0 bash]$ echo $?
0

返回码逻辑上有两类,0 为真,非零为假。就是说,返回为 0 表示命令执行成功,非零表示执行失败。返回码的取值范围为 0-255。其中错误返回码为 1-255。bash 为我们提供了一个内建命令 exit,通过这个命令可以人为指定退出的返回码是多少。这个命令的使用是一般进行 bash 编程的运维人员所不太注意的。我们在上一篇的 bash 编程语法结构的讲解中说过,ifwhile 语句的条件判断实际上就是判断命令的返回值,如果我们自己写的 bash 脚本不注意规范的使用脚本退出时的返回码的话,那么这样的 bash 脚本将可能不可以在别人编写脚本的时候,直接使用 if 将其作为条件判断,这可能会对程序的兼容性造成影响。因此,请大家注意自己写的 bash 程序的返回码状态。如果我们的 bash 程序没有显示的以一个 exit 指定返回码退出的话,那么其最后执行命令的返回码将成为整个 bash 脚本退出的返回码。

当然,一个 bash 程序的退出还可能因为被中间打断而发生,这一般是因为进程接收到了一个需要程序退出的信号。比如我们日常使用的 Ctrl+c 操作,就是给进程发送了一个 2 号 SIGINT 信号。考虑到程序退出可能性的各种可能,系统将错误返回码设计成 1-255,这其中还分成两类:

  1. 程序退出的返回码:1-127。这部分返回码一般用来作为给程序员自行设定错误退出用的返回码,比如:如果一个文件不存在,ls 将返回 2。如果要执行的命令不存在,则 bash 统一返回 127。返回码 125 和 126 有特殊用处,一个是程序命令不存在的返回码,另一个是命令的文件在,但是不可执行的返回码。
  2. 程序被信号打断的返回码:128-255。这部分系统习惯上是用来表示进程被信号打断的退出返回码的。一个进程如果被信号打断了,其退出返回码一般是 128+信号编号的数字。

比如说,如果一个进程被 2 号信号打断的话,其返回码一般是 128+2=130。如:

[zorro@zorrozou-pc0 bash]$ sleep 1000
^C
[zorro@zorrozou-pc0 bash]$ echo $?
130

在执行 sleep 命令的过程中,我使用 Ctrl+c 中断了进程的执行。此时返回值为 130。可以用内建命令 kill -l 查看所有信号和其对应的编号:

[zorro@zorrozou-pc0 bash]$ kill -l
 1) SIGHUP   2) SIGINT   3) SIGQUIT  4) SIGILL   5) SIGTRAP
 6) SIGABRT  7) SIGBUS   8) SIGFPE   9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG  24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF 28) SIGWINCH    29) SIGIO   30) SIGPWR
31) SIGSYS  34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX

在我们编写 bash 脚本的时候,一般可以指定的返回码范围是 1-124。建议大家养成编写返回码的编程习惯,但是系统并不对这做出限制,作为程序员你依然可以使用 0-255 的所有返回码。但是如果你滥用这些返回码,很可能会给未来程序的扩展造成不必要的麻烦。

最后

本文中我们描述了一个脚本的执行过程,从 #! 开始,到中间的解析过程,再到最后的退出返回码。希望这些对大家深入理解 bash 的执行过程和编写更高质量的脚本有帮助。通过本文我们明确了以下知识点:

  1. 脚本开始的 #! 的作用原理。
  2. bash 的字符串解析过程。
  3. 什么是 alias。
  4. 什么是关键字。
  5. 什么是 function。
  6. 什么是内建命令,hash 和外建命令以及它们的执行方法。
  7. 如何退出一个 bash 脚本以及返回码的含义。

转载链接:https://linuxtoy.org/archives/shell-programming-execute.html

原作者信息

微博 ID : orroz

微信公众号: Linux 系统技术

https://linuxtoy.org/archives/history-command-usage-examples.html

使用 HISTTIMEFORMAT 显示时间戳

当你从命令行执行 history 命令后,通常只会显示已执行命令的序号和命令本身。如果你想要查看命令历史的时间戳,那么可以执行:

# export HISTTIMEFORMAT='%F %T '
# history | more
1  2008-08-05 19:02:39 service network restart
2  2008-08-05 19:02:39 exit
3  2008-08-05 19:02:39 id
4  2008-08-05 19:02:39 cat /etc/redhat-release

注意:这个功能只能用在当 HISTTIMEFORMAT 这个环境变量被设置之后,之后的那些新执行的 bash 命令才会被打上正确的时间戳。在此之前的所有命令,都将会显示成设置 HISTTIMEFORMAT 变量的时间。[感谢 NightOwl 读者补充]

使用 Ctrl+R 搜索历史

Ctrl+R 是我经常使用的一个快捷键。此快捷键让你对命令历史进行搜索,对于想要重复执行某个命令的时候非常有用。当找到命令后,通常再按回车键就可以执行该命令。如果想对找到的命令进行调整后再执行,则可以按一下左或右方向键。

# [Press Ctrl+R from the command prompt, which will display the reverse-i-search prompt]
(reverse-i-search)`red‘: cat /etc/redhat-release
[Note: Press enter when you see your command, which will execute the command from the history]
# cat /etc/redhat-release
Fedora release 9 (Sulphur)

快速重复执行上一条命令

有 4 种方法可以重复执行上一条命令:

  1. 使用上方向键,并回车执行。
  2. 按 !! 并回车执行。
  3. 输入 !-1 并回车执行。
  4. 按 Ctrl+P 并回车执行。

从命令历史中执行一个指定的命令

在下面的例子中,如果你想重复执行第 4 条命令,那么可以执行 !4:

# history | more
1  service network restart
2  exit
3  id
4  cat /etc/redhat-release
# !4
cat /etc/redhat-release
Fedora release 9 (Sulphur)

通过指定关键字来执行以前的命令

在下面的例子,输入 !ps 并回车,将执行以 ps 打头的命令:

# !ps
ps aux | grep yp
root     16947  0.0  0.1  36516  1264 ?        Sl   13:10   0:00 ypbind
root     17503  0.0  0.0   4124   740 pts/0    S+   19:19   0:00 grep yp

使用 HISTSIZE 控制历史命令记录的总行数

将下面两行内容追加到 .bash_profile 文件并重新登录 bash shell,命令历史的记录数将变成 450 条:

# vi ~/.bash_profile
HISTSIZE=450
HISTFILESIZE=450

使用 HISTFILE 更改历史文件名称

默认情况下,命令历史存储在 ~/.bash_history 文件中。添加下列内容到 .bash_profile 文件并重新登录 bash shell,将使用 .commandline_warrior 来存储命令历史:

# vi ~/.bash_profile
HISTFILE=/root/.commandline_warrior

使用 HISTCONTROL 从命令历史中剔除连续重复的条目

在下面的例子中,pwd 命令被连续执行了三次。执行 history 后你会看到三条重复的条目。要剔除这些重复的条目,你可以将 HISTCONTROL 设置为 ignoredups:

# pwd
# pwd
# pwd
# history | tail -4
44  pwd
45  pwd
46  pwd [Note that there are three pwd commands in history, after executing pwd 3 times as shown above]
47  history | tail -4
# export HISTCONTROL=ignoredups
# pwd
# pwd
# pwd
# history | tail -3
56  export HISTCONTROL=ignoredups
57  pwd [Note that there is only one pwd command in the history, even after executing pwd 3 times as shown above]
58  history | tail -4

使用 HISTCONTROL 清除整个命令历史中的重复条目

上例中的 ignoredups 只能剔除连续的重复条目。要清除整个命令历史中的重复条目,可以将 HISTCONTROL 设置成 erasedups:

# export HISTCONTROL=erasedups
# pwd
# service httpd stop
# history | tail -3
38  pwd
39  service httpd stop
40  history | tail -3
# ls -ltr
# service httpd stop
# history | tail -6
35  export HISTCONTROL=erasedups
36  pwd
37  history | tail -3
38  ls -ltr
39  service httpd stop
[Note that the previous service httpd stop after pwd got erased]
40  history | tail -6

使用 HISTCONTROL 强制 history 不记住特定的命令

将 HISTCONTROL 设置为 ignorespace,并在不想被记住的命令前面输入一个空格:

# export HISTCONTROL=ignorespace
# ls -ltr
# pwd
#  service httpd stop [Note that there is a space at the beginning of service, to ignore this command from history]
# history | tail -3
67  ls -ltr
68  pwd
69  history | tail -3

使用 -c 选项清除所有的命令历史

如果你想清除所有的命令历史,可以执行:

# history -c

命令替换

在下面的例子里,!!:$ 将为当前的命令获得上一条命令的参数:

# ls anaconda-ks.cfg
anaconda-ks.cfg
# vi !!:$
vi anaconda-ks.cfg

补充:使用 !$ 可以达到同样的效果,而且更简单。[感谢 wanzigunzi 读者补充]

下例中,!^ 从上一条命令获得第一项参数:

# cp anaconda-ks.cfg anaconda-ks.cfg.bak
anaconda-ks.cfg
# vi -5 !^
vi anaconda-ks.cfg

为特定的命令替换指定的参数

在下面的例子,!cp:2 从命令历史中搜索以 cp 开头的命令,并获取它的第二项参数:

# cp ~/longname.txt /really/a/very/long/path/long-filename.txt
# ls -l !cp:2
ls -l /really/a/very/long/path/long-filename.txt

下例里,!cp:$ 获取 cp 命令的最后一项参数:

# ls -l !cp:$
ls -l /really/a/very/long/path/long-filename.txt

使用 HISTSIZE 禁用 history

如果你想禁用 history,可以将 HISTSIZE 设置为 0:

# export HISTSIZE=0
# history
# [Note that history did not display anything]

使用 HISTIGNORE 忽略历史中的特定命令

下面的例子,将忽略 pwd、ls、ls -ltr 等命令:

# export HISTIGNORE=”pwd:ls:ls -ltr:”
# pwd
# ls
# ls -ltr
# service httpd stop
# history | tail -3
79  export HISTIGNORE=”pwd:ls:ls -ltr:”
80  service httpd stop
81  history
[Note that history did not record pwd, ls and ls -ltr]

Linux基础知识之history的详细说明

转自:Linux公社

背景:history是Linux中常会用到内容,在工作中一些用户会突然发现其安装不了某个软件,于是寻求运维人员的帮助,而不给你说明他到底做了哪些坑爹的操作。此时你第一件要做的就是要查看其history命令历史。查看后兴许你就会发现他到底干了什么坑爹的操作。
history可以让你在一定情况下快速定位问题所在。

本文的history介绍及其实践都是来自CentOS7.2环境
[root@localhost ~]# cat /etc/RedHat-release
CentOS Linux release 7.2.1511 (Core)

history的介绍
history是shell的内置命令,其内容在系统默认的shell的man手册中。
history是显示在终端输入并执行的过命令,系统默认保留1000条。
[root@localhost ~]# history
1  ls
2  vi /etc/sysconfig/network-scripts/ifcfg-eno16777728
3  service network restart
4  ifconfig
5  ping www.linuxidc.com
6  systemctl disable firewalld.service
7  systemctl stop firewalld.service
8  exit
9  ls
10  type which
11  which ls
12  file /usr/bin/ls
13  which clock
14  file /usr/sbin/clock
15  man which
16  man what
17  man who
18  who
19  man who
20  man w
21  man who
22  who -q
23  man w

..
.

系统在关闭后会将现有history内容保存在文件~/.bash_history

与history相关的环境变量
HISTFILE          指定存放历史文件位置,默认位置在~/.bash_profile(针对用户)、
/etc/profile(针对全局,如果~/.bash_profile内没有相关环境变量内容则使用全局变量设置)
HISTFILESIZE      命令历史文件记录历史的条数
HISTSIZE          命令历史记录的条数,默认为1000
HISTTIMEFORMAT=”%F %T”  显示命令发生的时间
HISTIGNORE=”str1:str2:…” 忽略string1,string2历史
HISTCONTROL      包含一下4项,让哪一项生效只需要让其=下面一项即可
ignoredups:  忽略重复的命令;连续且相同方为“重复”
ignorespace:  忽略所有以空白开头的命令
ignoreboth:ignoredups,ignorespace
erasedups:    删除重复命令

让上述环境变量生效方式:
1、直接在当前shell内输入相关变量,比如我们不想留存命令历史,我们把HISTSIZE设置为0
[root@localhost ~]# HISTSIZE=0
[root@localhost ~]# history

经测试,成功。不过这种设置的局限性是其作用范围仅仅针对当前shell及其子shell,如果切换用户或登出再登入其设置失效。不过其特点是设置完立刻生效。
下面通过实验说明这个问题
[root@localhost ~]# bash
[root@localhost ~]# history
[root@localhost ~]# history

以上结果说明在当前shell内设置history的环境变量后,其作用范围为当前shell及子shell
Last login: Fri Jul 29 17:26:41 2016 from 10.1.250.62
[root@localhost ~]# history
1  history

重新登陆后原有的history环境变量失效
2、另一种让history环境变量生效的方式是修改~/.bash_profile文件
[root@localhost ~]# vi ~/.bash_profile
# .bash_profile

# Get the aliases and functions
if [ -f ~/.bashrc ]; then
. ~/.bashrc
fi

# User specific environment and startup programs

PATH=$PATH:$HOME/bin
HISTTIMEFORMAT=”%F %T ”        此为新添加的history环境变量,我添加了发生命令操作的时间
export PATH

wq保存退出。
[root@localhost ~]# history
1  history
2  ll
3  cd
4  hostory
5  history
6  vi ~/.bash_profile
7  history

由结果可知变量并没有生效,我们重新登录再尝试下。
[root@localhost ~]# history
1  2016-07-29 20:00:29 history
2  2016-07-29 20:00:29 ll
3  2016-07-29 20:00:29 cd
4  2016-07-29 20:00:29 hostory
5  2016-07-29 20:00:29 history
6  2016-07-29 20:00:29 vi ~/.bash_profile
7  2016-07-29 20:00:29 history
8  2016-07-29 20:00:29 logout
9  2016-07-29 20:00:33 history

通过上面的两个结果,我们可以发现第二种修改配置文件虽然也可以成功修改history环境变量但是其生效需要重新登陆,并不是改完就生效。但是它的特点是此修改始终有效,时效性为永久。

history命令的使用
-c: 清空命令历史
-d n: 删除历史中指定的命令,n表示命令号
#: 显示最近的#条历史
-a: 追加本次会话新执行的命令历史列表至历史文件,因为多终端所以如果想看当前都发生了什么操作就可以执行-a进行查看
-n: 读历史文件(本地数据)中未读过的行到历史列表(内存数据)
-r: 读历史文件(本地数据)附加到历史列表(内存数据)
-w: 保存历史列表(内存数据)到指定的历史文件(本地数据)
-s: 展开历史参数成一行,附加在历史列表后。用于伪造命令历史
下面来演示上面几种命令的使用
[root@localhost ~]# history
1  2016-07-29 20:00:29 history
2  2016-07-29 20:00:29 ll
3  2016-07-29 20:00:29 cd
4  2016-07-29 20:00:29 hostory
5  2016-07-29 20:00:29 history
6  2016-07-29 20:00:29 vi ~/.bash_profile
7  2016-07-29 20:00:29 history
8  2016-07-29 20:00:29 logout
9  2016-07-29 20:00:33 history
10  2016-07-29 20:07:41  cd
11  2016-07-29 20:07:44  ls
12  2016-07-29 20:07:50  history
13  2016-07-29 20:08:12 cat /etc/profile
14  2016-07-29 20:12:10 HISTCONTROL=ignorespace
15  2016-07-29 20:27:28 history -p
16  2016-07-29 20:27:31 history
17  2016-07-29 20:28:10 ls /etc
18  2016-07-29 20:28:14 history
19  2016-07-29 20:28:57 history

清空历史
[root@localhost ~]# history -c
[root@localhost ~]# history
2016-07-29 20:29:26 history

从历史文件读取之前的命令历史
[root@localhost ~]# history -r
[root@localhost ~]# history
1  2016-07-29 20:29:26 history
2  2016-07-29 20:30:59 history -r
3  2016-07-29 20:30:59 history
4  2016-07-29 20:30:59 ll
5  2016-07-29 20:30:59 cd
6  2016-07-29 20:30:59 hostory
7  2016-07-29 20:30:59 history
8  2016-07-29 20:30:59 vi ~/.bash_profile
9  2016-07-29 20:30:59 history
10  2016-07-29 20:30:59 logout
11  2016-07-29 20:31:01 history

删除指定命令历史
[root@localhost ~]# history -d 4
[root@localhost ~]# history
1  2016-07-29 20:29:26 history
2  2016-07-29 20:30:59 history -r
3  2016-07-29 20:30:59 history
4  2016-07-29 20:30:59 cd
5  2016-07-29 20:30:59 hostory
6  2016-07-29 20:30:59 history
7  2016-07-29 20:30:59 vi ~/.bash_profile
8  2016-07-29 20:30:59 history
9  2016-07-29 20:30:59 logout
10  2016-07-29 20:31:01 history
11  2016-07-29 20:32:50 history -d 4
12  2016-07-29 20:32:52 history

伪造历史命令
12345678910111213141516 [root@localhost ~]# history -s rm -rf /*  做下恶作剧
[root@localhost ~]# history
1  2016-07-29 20:29:26 history
2  2016-07-29 20:30:59 history -r
3  2016-07-29 20:30:59 history
4  2016-07-29 20:30:59 cd
5  2016-07-29 20:30:59 hostory
6  2016-07-29 20:30:59 history
7  2016-07-29 20:30:59 vi ~/.bash_profile
8  2016-07-29 20:30:59 history
9  2016-07-29 20:30:59 logout
10  2016-07-29 20:31:01 history
11  2016-07-29 20:32:50 history -d 4
12  2016-07-29 20:32:52 history
13  2016-07-29 20:33:57 rm -rf /bin /boot /dev /etc /home /lib /lib64 /media /mnt /opt /proc /root /run /sbin /srv /sys /tmp /usr /var
14  2016-07-29 20:34:00 history

我相信任谁输入history看到这个命令都会吓一身汗。
调用历史参数详解

#cmd !^ : 利用上一个命令的第一个参数做cmd的参数
#cmd !$ : 利用上一个命令的最后一个参数做cmd的参数
#cmd !* : 利用上一个命令的全部参数做cmd的参数
#cmd !:n : 利用上一个命令的第n个参数做cmd的参数
#!n :调用第n条命令
#!-n:调用倒数第n条命令
#!!:执行上一条命令
#!$:引用前一个命令的最后一个参数同组合键Esc,.
#!n:^ 调用第n条命令的第一个参数
#!n:$ 调用第n条命令的最后一个参数
#!m:n 调用第m条命令的第n个参数
#!n:* 调用第n条命令的所有参数
#!string:执行命令历史中最近一个以指定string开头的命令
#!string:^ 从命令历史中搜索以string 开头的命令,并获取它的第一个参数
#!string:$ 从命令历史中搜索以string 开头的命令,并获取它的最后一个参数
#!string:n 从命令历史中搜索以string 开头的命令,并获取它的第n个参数
#!string:* 从命令历史中搜索以string 开头的命令,并获取它的所有参数

下面通过实验来实践上面的历史参数的具体用法
[root@localhost ~]# history
1  2016-07-29 20:29:26 history
2  2016-07-29 20:30:59 history -r
3  2016-07-29 20:30:59 history
4  2016-07-29 20:30:59 cd
5  2016-07-29 20:30:59 hostory
6  2016-07-29 20:30:59 history
7  2016-07-29 20:30:59 vi ~/.bash_profile
8  2016-07-29 20:30:59 history
9  2016-07-29 20:30:59 logout
10  2016-07-29 20:31:01 history
11  2016-07-29 20:32:50 history -d 4
12  2016-07-29 20:32:52 history
13  2016-07-29 20:33:57 rm -rf /bin /boot /dev /etc /home /lib /lib64 /media /mnt /opt /proc /root /run /sbin /srv /sys /tmp /usr /var
14  2016-07-29 20:34:00 history
15  2016-07-29 20:40:32 ls
16  2016-07-29 20:40:33 cd
17  2016-07-29 20:40:45 ls /etc/passwd
18  2016-07-29 20:41:35 history

我们先使用!!来调用上一条命令

[root@localhost ~]# !!
history
1  2016-07-29 20:29:26 history
2  2016-07-29 20:30:59 history -r
3  2016-07-29 20:30:59 history
4  2016-07-29 20:30:59 cd
5  2016-07-29 20:30:59 hostory
6  2016-07-29 20:30:59 history
7  2016-07-29 20:30:59 vi ~/.bash_profile
8  2016-07-29 20:30:59 history
9  2016-07-29 20:30:59 logout
10  2016-07-29 20:31:01 history
11  2016-07-29 20:32:50 history -d 4
12  2016-07-29 20:32:52 history
13  2016-07-29 20:33:57 rm -rf /bin /boot /dev /etc /home /lib /lib64 /media /mnt /opt /proc /root /run /sbin /srv /sys /tmp /usr /var
14  2016-07-29 20:34:00 history
15  2016-07-29 20:40:32 ls
16  2016-07-29 20:40:33 cd
17  2016-07-29 20:40:45 ls /etc/passwd
18  2016-07-29 20:41:35 history

[root@localhost ~]# !h
history
1  2016-07-29 20:29:26 history
2  2016-07-29 20:30:59 history -r
3  2016-07-29 20:30:59 history
4  2016-07-29 20:30:59 cd
5  2016-07-29 20:30:59 hostory
6  2016-07-29 20:30:59 history
7  2016-07-29 20:30:59 vi ~/.bash_profile
8  2016-07-29 20:30:59 history
9  2016-07-29 20:30:59 logout
10  2016-07-29 20:31:01 history
11  2016-07-29 20:32:50 history -d 4
12  2016-07-29 20:32:52 history
13  2016-07-29 20:33:57 rm -rf /bin /boot /dev /etc /home /lib /lib64 /media /mnt /opt /proc /root /run /sbin /srv /sys /tmp /usr /var
14  2016-07-29 20:34:00 history
15  2016-07-29 20:40:32 ls
16  2016-07-29 20:40:33 cd
17  2016-07-29 20:40:45 ls /etc/passwd
18  2016-07-29 20:41:35 history
19  2016-07-29 20:47:03 history
20  2016-07-29 20:48:22 history

大家感兴趣可以一个个实验。本文就介绍到这里。
常用的快捷键
重新调用前一个命令中最后一个参数:
!$
Esc, .(点击Esc键后松开,然后点击. 键)
这两个很常用,特别是Esc,.
我们在创建文件后,通常会对其进行修改或其他的读操作,这时候键入命令后利用上述快捷键即可快速补全所需命令。