某神必的 Bash 代码分析

这个样例代码是$$的一键搭建脚本,老朽常年懒得看代码,唯独对这个又臭又长的神必脚本有点好奇(这里面可是有一千多行呢)

控制台下的颜色

颜色定义

1
2
3
4
red='\033[0;31m'
green='\033[0;32m'
yellow='\033[0;33m'
plain='\033[0m'

使用方法

1
echo -e "[${red}Error${plain}] This script must be run as root!"

记得把颜色切换回来啊

判断是否为ROOT用户执行脚本

if的简略写法

1
[[ $EUID -ne 0 ]] && echo -e "[${red}Error${plain}] This script must be run as root!" && exit 1

RUID即用户UID,进程真实用户号。创建该进程的用户的UID为该进程的真实用户号RUID EUID用于系统决定用户对文件资源的访问权限,一般情况下等同于RUID原文链接

关闭SELinux

bash函数 if, sed, grep的用法

1
2
3
4
5
6
disable_selinux(){
if [ -s /etc/selinux/config ] && grep 'SELINUX=enforcing' /etc/selinux/config; then
sed -i 's/SELINUX=enforcing/SELINUX=disabled/g' /etc/selinux/config
setenforce 0
fi
}

安全增强式Linux(SELinux,Security-Enhanced Linux)是一个Linux内核的安全模块,其提供了访问控制安全策略机制 全文链接

setenforce 0: 临时关闭selinux 原文链接

-s 的表达式用于判断文件存在且不为空。当config文件存在时,该表达式为真 原文链接

grep判断文件内是否存在某字符串,若字符串存在,即该命令成功执行,该表达式为真 参考文章

sed s/textA/textB 用于把textA替换为textB -i 参数代表直接修改读取的文件内容,而不是输出到终端 /g 代表若一行内多次出现被textA,则将所有textA全部替换为textB 更多资料

系统发行版判断

grep, if用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
if [[ -f /etc/redhat-release ]]; then
release="centos"
systemPackage="yum"
elif grep -Eqi "debian|raspbian" /etc/issue; then
release="debian"
systemPackage="apt"
elif grep -Eqi "ubuntu" /etc/issue; then
release="ubuntu"
systemPackage="apt"
elif grep -Eqi "centos|red hat|redhat" /etc/issue; then
release="centos"
systemPackage="yum"
elif grep -Eqi "debian|raspbian" /proc/version; then
release="debian"
systemPackage="apt"
elif grep -Eqi "ubuntu" /proc/version; then
release="ubuntu"
systemPackage="apt"
elif grep -Eqi "centos|red hat|redhat" /proc/version; then
release="centos"
systemPackage="yum"
fi

if-f判断文件是否存在 参考资料 grep-E是使用正则表达式的扩展语法, -q不打印任何标准输出, -i忽略大小写 更多资料

检查内核版本是否高于3.7.0

tr, cut, sort, head, test, uname的用法,Linux特殊变量

1
2
3
4
5
6
7
8
9
10
11
12
version_gt(){
test "$(echo "$@" | tr " " "\n" | sort -V | head -n 1)" != "$1"
}

check_kernel_version(){
local kernel_version=$(uname -r | cut -d- -f1)
if version_gt ${kernel_version} 3.7.0; then
return 0
else
return 1
fi
}

uname用于打印当前系统相关信息 -r 显示操作系统的发行编号 uname的更多用法 cut命令用来显示行中的指定部分,删除文件中指定字段 -d 指定字段的分隔符 -f 显示指定字段的内容 cut的更多用法

Example

1
2
3
# uname -r
# uname -r | cut -d- -f1
# uname -r | cut -d- -f2

1
2
3
4.15.0-52-generic
4.15.0
52

test 命令用于检查某个条件是否成立 test的样例代码 $@ 传递给脚本或函数的所有参数 $1 表示第一个参数 更多特殊变量 tr 命令用于转换或删除文件中的字符 tr的更多资料 sort -V 命令用于版本的排序 参考资料 head -n 1 只显示第一行的内容 head的更多资料

Example 脚本文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
fun1() {
echo ---fun1---
echo $@
echo ---end---
}
fun2() {
echo ---fun2---
echo $1
echo ---end---
}

fun3() {
echo ---fun3---
echo "$@" | tr " " "\n" | sort -V
echo ---end---
}

fun1 4.15.0 3.7.0
fun2 4.15.0 3.7.0
fun3 4.15.0 3.7.0
fun3 2.15.0 3.7.0

脚本输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
---fun1---
4.15.0 3.7.0
---end---
---fun2---
4.15.0
---end---
---fun3---
3.7.0
4.15.0
---end---
---fun3---
2.15.0
3.7.0
---end---

判断是否为64位系统

if判断命令输出内容

1
2
3
4
5
6
7
is_64bit(){
if [ `getconf WORD_BIT` = '32' ] && [ `getconf LONG_BIT` = '64' ] ; then
return 0
else
return 1
fi
}

getconf 命令获取系统配置变量值 更多信息

获取具体发行版版本

awk用法, 捕获标准输出作为变量值

1
2
3
4
5
6
7
8
9
10
get_opsy(){
[ -f /etc/redhat-release ] && awk '{print ($1,$3~/^[0-9]/?$3:$4)}' /etc/redhat-release && return
[ -f /etc/os-release ] && awk -F'[= "]' '/PRETTY_NAME/{print $3,$4,$5}' /etc/os-release && return
[ -f /etc/lsb-release ] && awk -F'[="]+' '/DESCRIPTION/{print $2}' /etc/lsb-release && return
}

debianversion(){
if check_sys sysRelease debian;then
local version=$( get_opsy )
...

AWK 是一种处理文本文件的语言 -F 指定输入文件折分隔符 `[awk的更多例子](http://www.runoob.com/linux/linux-comm-awk.html)/DESCRIPTION/{print $2}用正则表达式匹配DESCRIPTION`字符串,并输出该行第2列的内容

Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# cat /etc/lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=18.04
DISTRIB_CODENAME=bionic
DISTRIB_DESCRIPTION="Ubuntu 18.04.1 LTS"

# awk -F'[="]+' '/DESCRIPTION/{print $2}' /etc/lsb-release
Ubuntu 18.04.1 LTS

# awk -F'[="]+' '/DESCRIPTION/{print $1}' /etc/lsb-release
DISTRIB_DESCRIPTION

# awk -F'[=_"]+' '/DESCRIPTION/{print $1}' /etc/lsb-release
DISTRIB

获得公网IPv4地址

ip, egrep, wget用法

1
2
3
4
5
6
get_ip(){
local IP=$( ip addr | egrep -o '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' | egrep -v "^192\.168|^172\.1[6-9]\.|^172\.2[0-9]\.|^172\.3[0-2]\.|^10\.|^127\.|^255\.|^0\." | head -n 1 )
[ -z ${IP} ] && IP=$( wget -qO- -t1 -T2 ipv4.icanhazip.com )
[ -z ${IP} ] && IP=$( wget -qO- -t1 -T2 ipinfo.io/ip )
echo ${IP}
}

ip命令是网路管理命令,ip addr 查看所有已分配到网络接口的地址 egrep 即等于 grep -E ,使用扩展的正则表达式语法, -o 只输出匹配的行, -v 只输出不匹配的结果

  • egrep -o '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' 是匹配出IPv4地址
  • egrep -v "^192\.168|^172\.1[6-9]\.|^172\.2[0-9]\.|^172\.3[0-2]\.|^10\.|^127\.|^255\.|^0\." 过滤掉本地地址,如192.168.1.1
  • [ -z ${IP} ] 如果IP变量为空,该表达式为真,&&后的语句会被执行

wget命令用来从指定的URL下载文件 -t 设置下载失败重试最大次数 -T 设置网络超时时间 -q 安静模式 -O- 把结果输出到控制台(把结果输出到标准输出

  • ipv4.icanhazip.comipinfo.io/ip 访问这两个网站可以得到公网IP地址

获得IPv6地址

逻辑运算顺序

1
2
3
4
get_ipv6(){
local ipv6=$(wget -qO- -t1 -T2 ipv6.icanhazip.com)
[ -z ${ipv6} ] && return 1 || return 0
}

&&的运算优先级高于|| 百度百科

· 所以这句话可写为( ${ipv6}为空 && 返回1 ) || 返回0,即ipv6变量为空的时候,返回值为1(报错)

下载文件

判断命令执行失败

1
2
3
4
5
6
7
8
9
10
11
12
13
download(){
local filename=$(basename $1)
if [ -f ${1} ]; then
echo "${filename} [found]"
else
echo "${filename} not found, download now..."
wget --no-check-certificate -c -t3 -T60 -O ${1} ${2}
if [ $? -ne 0 ]; then
echo -e "[${red}Error${plain}] Download ${filename} failed."
exit 1
fi
fi
}

wget命令, --no-check-certificate不检查HTTPS站点的证书, -c 继续下载之前未完成下载的文件, -O 输出到文件 $?是上个命令的退出状态,或函数的返回值,为0是正常退出,其他值为异常退出 更多特殊变量

输入密码不显示字符

stty用法

1
2
3
4
5
6
7
8
9
get_char(){
SAVEDSTTY=$(stty -g)
stty -echo
stty cbreak
dd if=/dev/tty bs=1 count=1 2> /dev/null
stty -raw
stty echo
stty $SAVEDSTTY
}

stty命令修改终端命令行的相关设置 更多资料 -g 以stty可读方式打印当前的所有配置 -echo 禁止回显 cbreak 开启立即响应 参考资料 立即响应的解释 dd if=/dev/tty bs=1 count=1 2>/dev/null则是获取刚刚输入的字符 全解析 -raw 不对输入的信息进行处理,如忽略Ctrl+C 更多信息

  • 综上,这串代码可以模拟Linux输入密码无回显,但是一次只能读入一个字符,类似于C语言的getchar()

带异常检测批量安装程序

字符串数组,for循环,stderr重定向

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
error_detect_depends(){
local command=$1
local depend=`echo "${command}" | awk '{print $4}'`
echo -e "[${green}Info${plain}] Starting to install package ${depend}"
${command} > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo -e "[${red}Error${plain}] Failed to install ${red}${depend}${plain}"
echo "Please visit: https://teddyxxx.com/486.html and contact."
exit 1
fi
}

apt_depends=(
gettext build-essential unzip gzip python python-dev python-setuptools curl openssl libssl-dev
autoconf automake libtool gcc make perl cpio libpcre3 libpcre3-dev zlib1g-dev libev-dev libc-ares-dev git qrencode
)

apt-get -y update
for depend in ${apt_depends[@]}; do
error_detect_depends "apt-get -y install ${depend}"
done

${command} > /dev/null 2>&12>&1是指把stderr的错误信息重定向到stdout 更多信息 ${apt_depends[@]}表示apt_depends内的所有元素 更多信息

Example 测试脚本内容

1
2
3
4
5
6
7
8
apt_depends=(
gettext build-essential unzip gzip python python-dev python-setuptools curl openssl libssl-dev
)

echo ${apt_depends[@]}
for depend in ${apt_depends[@]}; do
echo ${depend}
done

输出结果

1
2
3
4
5
6
7
8
9
10
11
gettext build-essential unzip gzip python python-dev python-setuptools curl openssl libssl-dev
gettext
build-essential
unzip
gzip
python
python-dev
python-setuptools
curl
openssl
libssl-dev

用户输入数字,并判断数值是否有效,或使用随机数值

shuf, if, read用法,判断输入为纯数字的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
install_prepare_port() {
while true
do
dport=$(shuf -i 9000-19999 -n 1)
echo -e "Please enter a port for ${software[${selected}-1]} [1-65535]"
read -p "(Default port: ${dport}):" shad0wsocksport
[ -z "${shad0wsocksport}" ] && shad0wsocksport=${dport}
expr ${shad0wsocksport} + 1 &>/dev/null
if [ $? -eq 0 ]; then
if [ ${shad0wsocksport} -ge 1 ] && [ ${shad0wsocksport} -le 65535 ] && [ ${shad0wsocksport:0:1} != 0 ]; then
echo
echo "port = ${shad0wsocksport}"
echo
break
fi
fi
echo -e "[${red}Error${plain}] Please enter a correct number [1-65535]"
done
}

shuf命令的用法有很多,这里用来生成随机数 更多用法 echo -e可让echo处理字符串内的特殊字符,例如输出\n时换行而不是显示一个斜杠和n 参考资料 expr命令是一个手工命令行计数器,用于在UNIX/LINUX下求表达式变量的值 更多资料

  • 值得注意的是,expr ${shad0wsocksport} + 1 &>/dev/null,这一句话利用了给非数字做加法会报错来判断输入内容是否为纯数字
  • ${shad0wsocksport:0:1}这句的:0:1指获取字符串从0位置起长度为1的子串 参考资料

多选一程序

for循环, case, while, 数组用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
software=(shad0wsocks-Python shad0wsocksR shad0wsocks-Go shad0wsocks-libev)

install_select(){
...

clear
while true
do
echo "Which shad0wsocks server you'd select:"
for ((i=1;i<=${#software[@]};i++ )); do
hint="${software[$i-1]}"
echo -e "${green}${i}${plain}) ${hint}"
done
read -p "Please enter a number (Default ${software[0]}):" selected
[ -z "${selected}" ] && selected="1"
case "${selected}" in
1|2|3|4)
echo
echo "You choose = ${software[${selected}-1]}"
echo
break
;;
*)
echo -e "[${red}Error${plain}] Please only enter a number [1-4]"
;;
esac
done
}
  • ${\#software\[@\]}可获取数组长度 更多写法
  • case "${selected}" in 1|2|3|4)指让selected这个变量匹配1-4的数字范围 更多写法
  • *)相当于default, ;;标志着语块的结束 更多写法

脚本输出配置文件

cat, EOF用法

1
2
3
4
5
6
7
8
9
10
11
12
cat > ${shad0wsocks_python_config}<<-EOF
{
"server":"0.0.0.0",
"server_port":${shad0wsocksport},
"local_address":"127.0.0.1",
"local_port":1080,
"password":"${shad0wsockspwd}",
"timeout":300,
"method":"${shad0wsockscipher}",
"fast_open":${fast_open}
}
EOF

cat << EOF是Here Document相关语句,Here Document是在Linux Shell 中的一种特殊的重定向方式,它的作用就是将两个delimiter (EOF) 之间的内容 (Here Document Content部分) 传递给程序 (cat) 作为输入参数 更多信息 如果重定向的操作符是<<-,那么分界符(EOF)所在行的开头部分的制表符(Tab)都将被去除 更多信息