一、先手动把 rsync 跑起来
rsync 是一个文件同步和传输工具,本地和远程之间都能用,只传差异部分,效率很高。
写脚本之前得先把它手动跑通一遍,不然脚本里写什么都不知道。
看软件包
centos rpm -qa rsync
ubuntu dpkg -l | grep rsync
root@VM-0-9-ubuntu:~/shell# dpkg -l | grep rsync
ii rsync 3.2.7-1ubuntu1.4 amd64 fast, versatile, remote (and local) file-copying tool
看配置文件
root@VM-0-9-ubuntu:~/shell# ls -l /etc/rsyncd.conf
ls: cannot access '/etc/rsyncd.conf': No such file or directory
# rsyncd.conf 是 rsync 服务端配置文件,默认不会自动生成
# 需要你手动创建
touch /etc/rsyncd.conf
chmod 644 /etc/rsyncd.conf
# 检查是否有 rsync 服务
启动
# rsync配置文件需要自己创建
root@VM-0-9-ubuntu:~# cat > /etc/rsyncd.conf <<EOF
uid = nobody
gid = nogroup
use chroot = no
max connections = 10
pid file = /var/run/rsyncd.pid
[data]
path = /tmp
comment = Test Rsync Module
read only = no
EOF
# 启用服务
root@VM-0-9-ubuntu:~# /usr/bin/rsync --daemon
root@VM-0-9-ubuntu:~# netstat -tunl | grep 873
tcp 0 0 0.0.0.0:873 0.0.0.0:* LISTEN
tcp6 0 0 :::873 :::* LISTEN
# 停止
pkill rsync
每次启停都要敲这么一坨太麻烦了,所以自己写一个启停脚本。系统服务脚本都放在 /etc/init.d 下,可以先看看人家是怎么写的:
# 这是系统的脚本,但是执行时候提示需要传入下面的参数
# 可以按照这个风格开发 rsync 的启停脚本
root@VM-0-9-ubuntu:~# /etc/init.d/ufw
Usage: /etc/init.d/ufw {start|stop|restart|force-reload|status}
照着这个风格写一个 cc_rsync:
#!/bin/bash
# 判断参数个数
if [ "$#" -ne "1" ]
then
echo "$0 {start|stop|restart}"
exit 1
fi
# 用户选择启动
if [ "$1" = "start" ]
then
/usr/bin/rsync --daemon
sleep 2
# 查看端口是否存活
if [ `netstat -tunlp | grep 873 | wc -l` -ge "1" ]
then
echo "Rsync is started"
exit 0
fi
# 这里不能写 fi,会导致 elif 无法匹配到前面的 if
elif [ "$1" = "stop" ]
then
killall rsync &>/dev/null
sleep 2
if [ `netstat -tunlp|grep 873|wc -l` -eq "0" ]
then
echo "Rsync is stopped"
exit 0
fi
# 重启,先停止再启动
elif [ "$1" = "restart" ]
then
killall rsync &>/dev/null
# 定义变量接收端口状态
killpro=`netstat -tunlp|grep 873|wc -l`
/usr/bin/rsync --daemon
# 变量接收启动状态
startpro=`netstat -tunlp|grep 873|wc -l`
if [ "$killpro" -eq "0" ] && [ "$startpro" -ge "1" ]; then
echo "Rsync is restarted"
exit 0
fi
else
# $0 获取文件名
echo "Usage: /etc/init.d/cc_rsync {start|stop|restart}"
# 结束开始的if
fi
root@VM-0-9-ubuntu:~# /etc/init.d/cc_rsync restart
Rsync is restarted
二、函数怎么写
if 写得多了,同一段判断在脚本里出现好几遍就很烦,这时候就该上函数了。定义有三种写法:
# 标准 shell 定义
function 函数名() {
函数体
命令
# 可以不写 return
return 返回值
}
# 使用function关键字可以省略括号
function 函数名 {
同上
}
# 方便写法
函数名() {
同上
}
关于函数执行,有几条基本概念要记住:

- 执行 shell 函数直接写函数名字就行,不用加别的东西
- 函数必须先定义再执行,shell 脚本是自上而下加载的
- 函数体内定义的变量叫局部变量,要用
local关键字声明 - 函数体内加
return是退出函数,并且把返回值给调用它的程序(也就是这个脚本);exit是结束整个 shell 环境,两码事 - 函数如果单独写在一个文件里,需要用
source读进来才能用
同一个 shell 里调用函数,定义和执行都要写在文件里:

写完直接 bash 执行 .sh 脚本就行。
把定义和执行拆到两个文件
函数只写定义不执行,放在一个文件里,另一个文件加载它:
# 1.生成my_func.sh脚本记录函数
#! /bin/bash
zzz(){
echo "zzzzzzzzzzzzzzz"
}
# 2.检查当前shell变量(可能会查到系统变量,可以加 ^ 正则匹配开头)
set | grep ^zzz
# 3.利用. 或者 source 执行shell脚本,把变量加载到当前 shell
source my_func.sh
# 4.验证变量在环境中
set | grep ^zzz
root@VM-0-9-ubuntu:~/shell# cat my_func.sh
#! /bin/bash
zzz(){
echo "zzzzzzzzzzzzzzz"
}
root@VM-0-9-ubuntu:~/shell# set | grep ^zzz
root@VM-0-9-ubuntu:~/shell# source my_func.sh
root@VM-0-9-ubuntu:~/shell# set | grep ^zzz
zzz ()
# 输入 set 到底部可以看到变量的定义
zzz ()
{
echo "zzzzzzzzzzzzzzz"
}
# 退出 shell 会话就会删掉 shell 中的变量,这里不再演示了
# 执行
root@VM-0-9-ubuntu:~/shell# zzz
zzzzzzzzzzzzzzz
再写另一个脚本,负责读取 my_func.sh 并且调用里面的函数:
# 条件测试
# ./root/shell/my_func.sh是有点的,相对路径,不能在绝对路径加 .
# 由于是函数,必须source或者 . 执行变量才能写入shell环境
# 因此可以改为 source
[ -f /root/shell/my_func.sh ] && source /root/shell/my_func.sh || exit
# 执行
zzz
root@VM-0-9-ubuntu:~/shell# bash my_start_func.sh
zzzzzzzzzzzzzzz
root@VM-0-9-ubuntu:~/shell# zzz
zzzzzzzzzzzzzzz
这里注意 . / 和 ./ 的区别:. /路径 是 source 的简写,点后面跟一个空格再跟文件路径,表示在当前 shell 里加载并执行脚本;./文件 是执行当前目录下的可执行文件,会另开一个子 shell 去跑,退出后变量和函数都不会保留。
把上面的步骤换成 bash 跑一遍,差别就很明显了:
# 如果exit后再次登录,执行会发现变量加载不了了
# bash my_start_func.sh 是在子 Shell 里执行的
# 脚本里的函数定义和变量在子 Shell 退出后就没了
# 上个代码块能加载是因为之前执行过. my_func.sh
root@VM-0-9-ubuntu:~/shell# bash my_start_func.sh
zzzzzzzzzzzzzzz
root@VM-0-9-ubuntu:~/shell# zzz
Command 'zzz' not found, did you mean:
command 'zaz' from deb zaz (1.0.1-1)
command 'ezz' from deb fex-utils (20160919-2)
command 'zz' from deb fex-utils (20160919-2)
command '7zz' from deb 7zip-standalone (23.01+dfsg-7)
command 'bzz' from deb djvulibre-bin (3.5.28-2ubuntu0.24.04.2)
command 'zgz' from deb pristine-tar (1.50)
Try: apt install <deb name>
root@VM-0-9-ubuntu:~/shell# set | grep ^zzz
zzz ()
# 必须用source才会在当前加载
三、检测网站存活
函数讲完了,拿它办点正事:监测一个 url 是不是正常。函数的参数和脚本参数一样,$1、$2、$# 都能直接用:
root@VM-0-9-ubuntu:~/shell# cat my_func.sh
#! /bin/bash
zzz(){
echo "zzzzzzzzzzzzzzz"
}
# 处理传入的参数
helloPyyu(){
echo "传入参数:$1 $2 $3,个数:$#"
}
root@VM-0-9-ubuntu:~/shell# cat my_start_func.sh
#! /bin/bash
# 条件测试
[ -f /root/shell/my_func.sh ] && source /root/shell/my_func.sh || exit
# 执行
helloPyyu $1 $2 $3
# 处理参数一般用 bash,不污染当前终端
root@VM-0-9-ubuntu:~/shell# bash my_start_func.sh 666 77 8
传入参数:666 77 8,个数:3
root@VM-0-9-ubuntu:~/shell# set | grep ^hello
不封装版本
一般要对脚本的功能做帮助提示,不然别人不知道怎么执行。执行方式是 bash 脚本 url:
#!/bin/bash
# 限制参数,后面多次使用,可以封装为函数
# 这里先演示不封装的
if [ "$#" -ne "1" ]
then
echo "Usage: $0 url"
exit 1
fi
wget --spider -q -o /dev/null --tries=1 -T 5 $1
# 判断状态码
if [ "$?" -eq "0" ]
then
echo "$1正常运行"
else
echo "$1挂了"
fi
root@VM-0-9-ubuntu:~/shell# bash check_url.sh http://168kaguyachi.site/
http://168kaguyachi.site/正常运行
root@VM-0-9-ubuntu:~/shell# bash check_url.sh 168kaguyachi.site
168kaguyachi.site正常运行
改成函数版本
参数判断和 url 检测各抽成一个函数,再仿照 C 的写法立一个 main 当入口:
#! /bin/bash
# 提示用户
usage(){
# 用户输错了,说明函数用法
echo "Usage: $0 url"
exit 1
}
# 检测url
check_url(){
wget --spider -q -o /dev/null --tries=1 -T 5 $1
# 判断状态码
if [ "$?" -eq "0" ]
then
echo "$1正常运行"
else
echo "$1挂了"
fi
}
# 参考C的写法,设立main函数作为入口函数
# main也是shell函数,需要调用
main() {
# 判断输入是否正确
if [ "$#" -ne "1" ]
then
usage
fi
check_url $1
}
# 调用main,也要传入用户的参数(用户参数当作一个整体传入)
main $*
root@VM-0-9-ubuntu:~/shell# bash check_url2.sh 168kaguyachi.site 1331 13
Usage: check_url2.sh url
root@VM-0-9-ubuntu:~/shell# bash check_url2.sh 168kaguyachi.site
168kaguyachi.site正常运行
把输出美化一下
正经的脚本返回信息都是带颜色高亮的,这个不用自己造轮子,系统里就有现成的,直接拿来用。
centos07 上装的 mysql,用只读模式打开 /etc/init.d/mysql,搜 /log_success 就能看到实现高亮的函数长什么样,linux 内置提供了执行状态美化的函数库:

把这一整段(含前面判断 LSB 函数库在不在的那几行)复制到脚本开头:

然后原来自己 echo 的那两行就能换成现成的函数,成功失败各有各的样式:

四、写一个专业点的 rsync 脚本
系统服务的启停管理脚本都放在 /etc/init.d,可以照着它的样子,把上面结果的美化日志也加进去:
vim /etc/init.d/my_rsync
#!/bin/bash
# 方便维护最好写标准的
function usage(){
echo "Usage: $0 {start|stop|restart}"
}
function start(){
/usr/bin/rsync --daemon
sleep 1
if [ `netstat -tunlp|grep rsync|wc -l` -ge "1" ]
then
echo "Rsync running..."
else
echo "启动失败"
fi
}
function stop(){
pkill -x rsync &>/dev/null
sleep 1
if [ `netstat -tunlp|grep rsync|wc -l` -eq "0" ]
then
echo "服务停止"
else
echo "停止失败"
fi
}
function restart(){
pkill -x rsync &>/dev/null
killpro=`netstat -tunlp|grep rsync|wc -l`
/usr/bin/rsync --daemon
startpro=`netstat -tunlp|grep rsync|wc -l`
# 单括号内不能直接用 &&
if [ "$killpro" -eq "0" -a "$startpro" -ge "1" ]
then
echo "服务重启"
else
echo "重启失败"
fi
}
# 入口函数
# -eq 是数值比较,字符串比较应该用 =
function main() {
if [ "$#" -ne "1" ]
then
usage
fi
if [ "$1" = "start" ]
then
start
elif [ "$1" = "stop" ]
then
stop
#restart函数也可以不写,换一种用法
elif [ "$1" = "restart" ]
then
stop
sleep 1
start
else
usage
fi
}
main $*
执行的时候踩了两个坑,一个是文件没有执行权限,另一个是 pkill 默认按模糊进程名匹配,my_rsync 里也包含 rsync,结果脚本把自己给匹配掉了:
root@VM-0-9-ubuntu:~/shell# netstat -tunlp|grep rsync
tcp 0 0 0.0.0.0:873 0.0.0.0:* LISTEN 463696/rsync
tcp6 0 0 :::873 :::* LISTEN 463696/rsync
root@VM-0-9-ubuntu:~/shell# /etc/init.d/my_rsync stop
-bash: /etc/init.d/my_rsync: Permission denied
root@VM-0-9-ubuntu:~/shell# chmod +x /etc/init.d/my_rsync
root@VM-0-9-ubuntu:~/shell# /etc/init.d/my_rsync stop
Terminated
root@VM-0-9-ubuntu:~/shell# /etc/init.d/my_rsync
Usage: /etc/init.d/my_rsync {start|stop|restart}
Usage: /etc/init.d/my_rsync {start|stop|restart}
root@VM-0-9-ubuntu:~/shell# netstat -tunlp|grep rsync
# pkill 默认按模糊进程名匹配,my_rsync 包含 rsync,所以进程匹配到了自己
# 改为 pkill -x rsync 即可
root@VM-0-9-ubuntu:~/shell# /etc/init.d/my_rsync restart
服务停止
Rsync running...
root@VM-0-9-ubuntu:~/shell# /etc/init.d/my_rsync stop
服务停止
root@VM-0-9-ubuntu:~/shell# /etc/init.d/my_rsync start
Rsync running...
至此 shell 进阶篇彻底结束。
