最近都在看前端的一些知识点,今天Linux想跑个for循环都记不太清了。早先学过的shell脚本编程,随着时间的流逝逐渐的遗忘,生活和工作中也用不到太多的shell,谨在此刻,乘着旧力未消,整理过往学习shell的一些笔记和心得,想要详细了解shell的可以看下方的《高级Bash脚本指南(中文版)》

https://linuxstory.gitbook.io/advanced-bash-scripting-guide-in-chinese/

变量声明和输出

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
29
30
31
32
33
34
35
36
37
# 变量声明和赋值,需要注意的是,shell中的变量声明等号两边不可以有空格,即variable = "value"这种形式是错误的
variable="value"

# 声明数组和将变量声明为数组
array_var={1 2 3 4 5}
declare -A array_var

# 变量输出 不带引号、单引号、双引号的区别:
echo $variable 或 echo "$variable" # 输出结果: value
echo '$variable' # 输出结果:$variable
# 可以看到,不带引号和双引号都是输出的变量,而单引号则是输出字符串本身

# 此外,不带引号的双引号还有一个细小的区别:
# 不带引号会把字符串中的空格默认作为切割符,for循环的时候会逐个取出
cat > outinfo.sh << 'EOF'
var1="hello world";
for i in $var1;
do
echo $i
done
EOF

[root@2021-02-15 ~]# sh outinfo.sh
hello
world

# 带引号则是把字符串看作是一个整体
cat > outinfo.sh << 'EOF'
var1="hello world";
for i in "$var1";
do
echo $i
done
EOF

[root@2021-02-15 ~]# sh outinfo.sh
hello world

Tips: cat配合EOF向文件中输入多行内容时,有一个小细节:

1
2
3
4
5
6
7
8
# 带引号的EOF默认会保留输入语句中的$符号, 不带引号的EOF则是解析$符号,比如我们上面的 cat > outinfo.sh << 'EOF',如果我们把引号去掉,则会变成下面这样(可以看到,原本的$变量都被解析成了相关的值):

[root@2021-02-15 ~]# cat outinfo.sh
var1="hello world";
for i in "hello world"; // 这里<----
do
echo world // 还有这里<----
done

Tips:

1
${array_var[*]} 和 ${array_var[@]} 都表示输出数组所有值,但有所差别;${#array_var[*]} 表示打印数组的长度;${!array_var[*]} 表示列出索引

Google Shell 编程指南标准写法:

1
echo "we have ${fruit}s"

用户输入

使用read命令,我们可以接受用户输入内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 读取一个变量,然后输出到屏幕
read -n 1 var
echo $var


# 用不回显的方式读取密码
read -s var

# 显示提示信息
read -p "Enter input:" var

# 在特定的时间内读取输入
read -t 2 var

# 用定界符结束输入行
read -d ":" var
[root@zen ~]# bash d.sh
nihao:nihao
# 此时var=nihao

数学运算

数学运算,从 2 / 4 开始 …..

需要注意的是,shell中的数学运算并不像其他编程语言那样简单,尤其在浮点数运算方面,出奇的恶心人。

  • 加减乘除
1
2
3
4
5
6
7
8
9
10
11
12
13
n=1
# 加的四种写法,需要注意的是,下面这几种写法都不支持浮点数运算,即 n=$[ 1.1 + 1 ] 是会报错的
let n++
n=$[ $n + 1 ]
n=`expr $n + 1`
n=$(( $n+1 ))

# 减同上

# 乘同上

# 除,可以看一下下面这个例子,理论上我们的结果应该是0.5,但实际输出为0,不行你可以试一试
result=$[ 2 / 4 ]
  • 浮点数运算(从 2除4 说起…. )
1
2
3
4
5
6
7
8
9
10
# 在shell中做浮点数运算需要用到bc命令
echo "4 * 2.2" | bc # ==> 8.8
echo "2 / 4" | bc # ==> 0

# 我们发现会小于1的数还是直接归零,似乎bc也救不了shell的数学运算,不用担心,我们可以通过保留有效位数的方式来显示运算结果,这样我们就可以算出2/4了
echo "scale=3, 2/4" | bc # ==> .500

#
# 但.....,似乎又有个问题,2/4 结果不应该是 0.500吗,运算似乎把前面的0漏了?是的,没错,我们的2/4还没结束,下面带你真正写出 2/4 的最终结果:
printf %.3f `echo "scale=3; 2/4" | bc` # ==> 0.500

浮点数除法

判断比较

1
[],[[]],test,都是比较运算符

基本语法

1
2
3
4
5
6
7
if [ condition ];then
:
elif [ condition ];then
:
fi
# [ condition ] && action 如果condition为true,则执行action
# [ condition ] || action 如果condition为false,则执行action

小数比较中,直接比较是会报错的,正确的写法是:

1
2
[ `echo "1.1 > 1" | bc` -eq 1 ]

echo "1.1 > 1" | bc 正确为1 错误为0

比较运算符

1
2
3
4
5
6
-gt  大于 
-lt 小于
-ge 大于等于
-le 小于等于
-eq 等于
-ne 不等于

逻辑运算符

1
2
3
4
5
6
7
[ $var1 -ne 0 -a $var2 -gt 2 ] 
等价于(and)
[ $var1 -ne 0 ] && [ $var2 -gt 2]

[ $var1 -ne 0 -0 $var2 -gt 2 ]
等价于(or)
[ $var1 -ne 0 ] || [ $var2 -gt 2]

类型比较

1
2
3
4
5
6
7
8
9
[ -f $var ]   文件
[ -x $var ] 可执行文件
[ -d $var ] 目录
[ -e $var ] 文件
[ -c $var ] 字符设备
[ -b $var ] 块设备
[ -w $var ] 可写返回真
[ -r $var ] 可读返回真
[ -L $var ] 符号链接

其他常见比较:

1
2
3
[ -z STRING ] STRING长度为0则为true => string length equal zero
[ -n STRING ] STRING长度为不0则为true => string length not equal zero
[ -s FILE ] 文件存在且大小不为0则为true

字符串比较

1
2
3
4
[[ $str2 == $str2 ]] # 精确比较
# 模糊比较
[[ 'hello' =~ 'hello world' ]] && echo 'ok'
[[ 'hello world' =~ 'hello' ]] && echo "ok" # ok

选择菜单

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash

PS3='Choose your favorite vegetable: ' # 设置提示符字串.
select vegetable in "beans" "carrots" "potatoes" "onions" "rutabagas"
do
echo
echo "Your favorite veggie is $vegetable."
echo "Yuck!"
echo
break # 如果这里没有 'break' 会发生什么?
done

exit 0
1
2
3
4
5
6
7
8
9
1) beans
2) carrots
3) potatoes
4) onions
5) rutabagas
Choose your favorite vegetable: 1

Your favorite veggie is beans.
Yuck!

循环

循环只需要记住对应的语法结构即可

  1. for循环
1
2
3
4
5
6
7
8
for var in list;do
commands;
done

#list可以是字符串或者序列
#echo {1..50} 生成1-50
#echo {a..z}
#echo {A..Z}
1
2
3
4
while condition
do
commands;
done

函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 定义的两种方式
function fname(){
statements;
}

fname(){
statements
}

# 调用,注意调用的时候不需要加小括号
fname

# 函数传参
fname arg1 arg2

# 函数接收参数
fname(){
echo $1
echo $2
}

# 导出函数,这样函数的作用域可以扩展到子进程中
export -f fname

并发编程

并行代码

首先看一段串行的代码:

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

#循环执行10次command1
for i in `seq 10`;
do
commands1
done
commands2

然后我们通过让其在后台执行,实现command1和command2并行执行:

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

#循环执行10次command1
for i in `seq 10`;
do
{ commands1 }&
done
commands2

如果command2需要command1处理后的结果,那么command2就必须在command1之后执行,我们可以添加wait,让前面的代码执行完了再执行后面的

1
2
3
4
5
6
7
8
9
#!/bin/bash

#循环执行10次command1
for i in `seq 10`;
do
{ commands1 }&
done
wait
commands2

并发代码

我们可以通过xargs命令轻松实现多线程并发,下面是xargs常用的参数

1
2
3
4
5
6
7
-0:如果输入的stdin含有特殊字符,例如反引号`、反斜杠\、空格等字符时,xargs可以将它还原成一般字符。为xargs的默认选项。
-n <num>:表示命令在执行的时候一次使用的argument的个数,由num指定,默认是用所有的参数。
-t:表示先打印命令,然后再执行。
-a <file>:从文件中读入作为sdtin。
-i,-I:将xargs的输出每一项参数,单独赋值给后面的命令,参数需要用{}代替。
-s <num>:命令行的最大字符数,指的是xargs后面那个命令的最大命令行字符数,包括命令、空格和换行符。每个参数单独传入xargs后面的命令。
-P:修改最大的进程数,默认是1,为0时候为as many as it can。

比如我们执行如下代码:

1
seq 10 | xargs -n 1 -I {} -P 3 sh -c 'sleep 1;echo {}'

代码解释:

seq 10 : 生成1~10,10个数字

xargs部分:将数据转换为命令参数

​ -n 1 : 每次读取前面生成的一个参数

​ -I {} {} : 将后面一个大括号替换为读取到的参数,这里是替换为1、2、3…..10

​ -P 3 : 同时开启三个进程运行

​ sh -c ‘’ : 执行引号中的代码,其中echo {} 中 {} 被替换成了echo 1

如果不能明白xargs的用处,建议先去看一下xargs是做什么用的:https://www.runoob.com/linux/linux-comm-xargs.html

xargs并发

内置API

调试

1
2
3
4
set -x :在执行时显示参数和命令
set +x :禁止调试
set -v: 当命令进行读取时显示输入
set +v : 禁止打印输入
1
2
3
4
5
# 获取字符串长度
length=${#var}

# 识别当前SHELL版本
echo "$SHELL"

参数处理

参数处理 说明
$# 传递到脚本的参数个数
$* 以一个单字符串显示所有向脚本传递的参数。 如”$*”用「”」括起来的情况、以”$1 $2 … $n”的形式输出所有参数。
$$ 脚本运行的当前进程ID号
$! 后台运行的最后一个进程的ID号
$@ 与$*相同,但是使用时加引号,并在引号中返回每个参数。 如”$@”用「”」括起来的情况、以”$1” “$2” … “$n” 的形式输出所有参数。
$- 显示Shell使用的当前选项,与set命令功能相同。
$? 显示最后命令的退出状态。0表示没有错误,其他任何值表明有错误。
$0 显示当前脚本所在的路径。

奇技淫巧

计数器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/bash
echo -n counter:
tput sc # 存储光标位置
count=0
while true
do
if [ $count -lt 40 ];
then let count++
sleep 1
tput rc # 恢复光标位置
tput ed # 清除从当前光标位置到行尾之间所有的内容
echo -n $count
else exit 0
fi
done

计数器效果

Fork炸弹

1
:(){ :|:& };:

递归函数调用自身,不断生成新的进程,最终造成拒绝服务工具,函数调用前的&将子进程放入后台,这段危险的代码会Fork出大量的进程,因而被称为Fork炸弹

可以通过修改配置文件/etc/security/limits.conf来限制可生成的最大进程数来避开这枚炸弹

常用shell

ip地址转换

1
2
3
4
5
6
7
8
9
10
11
yum -y install jq
awk --re-interval '{match($0,/([0-9]{1,3}\.){3}[0-9]{1,3}/,a); print a[0]}' /var/log/v2ray/access.log | sort | uniq -c | sort -nr | head -n 10 > /tmp/v2ray.log
line=`wc -l /tmp/v2ray.log | awk '{print $1}'`
for nbr in `seq $line`;
do
count=`sed -n "$nbr p" /tmp/v2ray.log | awk '{print $1}'`
ip=`sed -n "$nbr p" /tmp/v2ray.log | awk '{print $2}'`
info=`curl -s https://ipvigilante.com/${ip} | jq -r '.data.ipv4,.data.country_name, .data.subdivision_1_name, .data.city_name' | xargs`
printf "%-6s %s" $count "$info"
echo;
done

效果

1
2
3
4
5
6
7
sh /usr/bin/vray_ip 
5580 103.7.29.7 China Guangdong Shenzhen
27 208.100.26.237 United States Illinois Chicago
5 83.97.20.31 Bulgaria null null
2 89.248.168.252 Seychelles null null
2 193.118.53.202 United Kingdom null null
2 172.104.76.123 Japan Tokyo Tokyo

测速

1
wget -qO- git.io/superbench.sh | bash

防火墙和Selinux

1
2
3
4
systemctl stop firewalld
systemctl disable firewalld
setenforce 0
sed -i 's\SELINUX=enforcing\SELINUX=disabled\' /etc/selinux/config

字符浏览器

1
2
yum -y install lynx 		#此外还有这两个字符浏览器,只是都没有lynx好用 w3m links
lynx http://novel.ymlog.cn/

GIF效果图


历史命令

1
2
3
4
5
# Ctrl + i
(reverse-i-search)`docker': systemctl enable docker

# list
$ history | grep field

软件和Epel源

1
2
3
4
5
6
7
yum -y install the_silver_searcher vim zip unzip tree bash-completion lrzsz yum-utils git zsh wget && bash
yum -y install htop mycli

yum -y install wget
wget -O /etc/yum.repos.d/epel.repo http://mirrors.aliyun.com/repo/epel-7.repo
yum clean all
yum makecache


用户登录初始化

因为虚拟机要在不同的电脑间来回迁移,所以不方便设置固定ip,但DHCP之后有需要登录查看ip,太过于繁琐,写了个用户登录脚本,一旦用户登录,立刻执行该脚本。

ps 字符生成用的是vscode中ASSCIIDecorator插件,风格是ANSI Shadow Use the ANSI Shadow font

效果:

用户登录脚本

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# Linux - 便携脚本
# 1、用户登录输出ip地址(方便xshell登录)



cat > /root/boot << 'EOF'
#!/bin/bash
echo "

███████╗███████╗██╗ ███████╗ ██████╗ ██████╗ ███╗ ██╗███████╗██╗ ██████╗
██╔════╝██╔════╝██║ ██╔════╝ ██╔════╝██╔═══██╗████╗ ██║██╔════╝██║██╔════╝
███████╗█████╗ ██║ █████╗█████╗██║ ██║ ██║██╔██╗ ██║█████╗ ██║██║ ███╗
╚════██║██╔══╝ ██║ ██╔══╝╚════╝██║ ██║ ██║██║╚██╗██║██╔══╝ ██║██║ ██║
███████║███████╗███████╗██║ ╚██████╗╚██████╔╝██║ ╚████║██║ ██║╚██████╔╝
╚══════╝╚══════╝╚══════╝╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═╝ ╚═════╝


"

file="/tmp/info-$RANDOM"
curl cip.cc &> $file
publicIp=`cat $file | sed -n '/IP/p' | awk -F: '{print $2}' | tr -d ' '`
location=`cat $file | sed -n '/地址/p' | awk -F: '{print $2}' | tr -d ' '`



printf "%-16s %s\n" Time: "`date '+%Y-%m-%d %H:%M'`"
printf "%-16s %s\n" Ip: "`ip a | grep glo | awk '{print $2}'`"
printf "%-16s %s\n" User: "`whoami`"
printf "%-16s %s\n" PublicIp: "$publicIp"
printf "%-16s %s\n" Location: "$location"
echo;
systemd-analyze
echo;echo;

EOF

sed -i '1,${/sh/{/root/{/boot/d}}}' ~/.bashrc
chmod +x /root/boot
echo "sh /root/boot" >> ~/.bashrc
. ~/.bashrc



Vmware扩容

如图所示,将虚拟机根分区原来的80G扩展到160G,但这么做完之后,虚拟机内更分区还是80G,需要用户手动调整根分区大小

image-20200721114506892
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
cat > addisk << EOF
# 磁盘扩容脚本,需要先在vmware里扩展分区容量,默认60G
# 2020/7/21

echo " add disk +58G "

# 新建sda3分区并格式化
echo -e "n\n\n\n\nw" | fdisk /dev/sda
partprobe /dev/sda
mkfs.ext3 /dev/sda3

# 将sda3分区格式化为pv,并纳入vg
echo -e 'y\n' | pvcreate /dev/sda3
vgextend centos /dev/sda3
# 这里+58G,Vmware中磁盘增加60G
lvextend -L+58G /dev/centos/root /dev/sda3
# 扩展分区大小
lvextend /dev/centos/root /dev/sda3
xfs_growfs /dev/mapper/centos-root
EOF
chmod +x addisk

测试端口连通

测试ip192.168.224.12830443端口

1
2
pip install tcping
tcping -p 30443 192.168.224.128

换个bash吧——zsh

zsh

2020/7/24

效果:

zsh演示

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
# 安装
yum -y install zsh git
sh -c "$(curl -fsSL https://raw.github.com/robbyrussell/oh-my-zsh/master/tools/install.sh)"

# 修改主题
sed -i 's/ZSH_THEME=.*/ZSH_THEME="agnoster"/' ~/.zshrc

# 更改shell
chsh -s `which zsh`

zsh



# 查看当前有哪些shell
chsh -l

# 该文件记录了有效shell的登陆列表,在调用chsh改变登陆shell时,会查询这个文件
$ cat /etc/shells
/bin/sh
/bin/bash
/usr/bin/sh
/usr/bin/bash
/usr/bin/tmux
/bin/zsh

查看文件的绝对路径

1
2
$ realpath test.sh 
/data/home/zendu/issue_task/test.sh

一直以来总是复制目录的路径,文件名手动输入,这样实在是太麻烦了,网上很多都是错的,像什么pwd+filename,where,which,locate等,回答的文不对题。

ag 全局搜索

安装ag命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/bin/bash
yum -y groupinsatll Development Tools
yum -y install pcre-devel xz-devel zlib-devel automake
tempDir=$(mktemp -d agTemp.XXXXXX)
cd "$tempDir"
wget https://github.com/ggreer/the_silver_searcher/archive/master.zip
dir=$(unzip *.zip)
dir=${dir##*: }
dir=${dir%/*}
cd "$dir"
./build.sh&&make install
cd ../../
rm -rf ${tempDir}
ag -V

SSH登录

非交互式登录

1
2
yum -y install sshpass
sshpass -p [password] ssh -p 22 root@47.94.18.223

设置登录延时等

1
ssh -o ConnectTimeout=3 192.168.0.10

最后

shell中还有其他一些内容, 比如进程管道符相关、shell常用的命令(awk、sed、grep)、vim编辑器的使用、正则表达式等等,出于篇幅限制,这里不能一一列举,详情可以参阅《高级Bash脚本指南》。

另外,shell相关的小技巧可以查看笔者的这篇文章:http://ymlog.cn/post/b2c6704d.html