一个计算机技术爱好者与学习者

0%

好好学Shell:循环读取文本

1. 需求

已知mobile.txt为:

1
2
haojin 17625160000
voidking 17625160001

需求:根据 mobile.txt 中的内容拼接成SQL,修改不同用户的手机号。例如:

1
update user set mobile="17625160000" where name="haojin";

2. 简单实现

脚本:

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

grep -v "^$" mobile.txt | while read line
do
name=`echo $line | awk '{print $1}'`
mobile=`echo $line | awk '{print $2}'`
echo "update user set mobile=\"${mobile}\" where name=\"${name}\";"
done

PS:不能使用for line in cat 'mobile.txt',因为这种方法会按照空格或换行切分文本。

3. 更好的实现

以上循环读取的方法,对于上面的需求是没有问题的。但是通用性不好,我们再来看另外一个需求。

已知service.txt内容为:

1
2
3
127.0.0.1 80
127.0.0.1 8080
192.168.56.101 8080

需求:探测service.txt中每个服务的连通性,并记录结果。

我们用同样的思路实现脚本:

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

#cat /dev/null > detectresult.txt
: > detectresult.txt
cat service.txt | while read line
do
ip=$(echo $line | awk '{print $1}')
port=$(echo $line | awk '{print $2}')
res=$(nc -w 2 -v $ip $port)
echo "$res" >> detectresult.txt
done

执行脚本后,我们发现结果文件中只有一条结果!这就不符合预期了。
这是因为while使用重定向机制,while read line一次性将文件信息读入输入缓存,并按行赋值给变量line,直到输入缓存数据为空。而刚好nc、telnet、ssh等命令,会读取输入缓存中的所有数据,这就导致输入缓存被清空了,while循环结束。

解决办法:

1
res=$(nc -w 2 -v $ip $port < /dev/null)

此外,因为管道符左右的命令都是在子shell中执行的,所以容易引起变量赋值不会在父shell中生效的问题。

因此,更好的脚本应该改成:

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

cat /dev/null > detectresult.txt
while read line
do
ip=$(echo $line | awk '{print $1}')
port=$(echo $line | awk '{print $2}')
res=$(nc -w 2 -v $ip $port < /dev/null)
echo "$res" >> detectresult.txt
done < service.txt

4. while read line进阶

使用while read line的循环读取文本的方案,通用性已经很不错,可以应对大多数场景。但是,如果循环读取时,还需要和用户进行交互,那么就不适用了,下面看一个例子。

已知applist.txt内容为:

1
2
3
app1 running hba
app2 stop hbe
app3 running hna

需求:对applist.txt中的app进行修改,修改每个app前都需要进行确认。

按照while的思路,编写 main.sh 内容为:

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

while read line;do
app_id=$(echo $line | awk '{print $1}')
idc=$(echo $app_id | awk -F'.' '{print $NF}')
#echo ${app_id}" "${idc}
bash modify.sh ${idc} ${app_id}
done < applist.txt

modify.sh内容为:

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

idc=$1
app_id=$2

echo -e "idc: ${idc}"
echo -e "app_id: ${app_id}"

read -p "确认进行修改?[Y/N]" input
if [[ $input = "y" || $input = "Y" ]];then
echo -e "continue..."
else
echo -e "exit"
exit 1
fi

# logic code

但是问题来了,最终执行效果不符合预期,modify.sh中的确认交互效果会失效!这是因为read会读取缓存中的内容,而不是等待交互。

那么,怎么解决这个问题?非要使用while read line的话,确实没有好的解决办法。
但是,我们可以把while read line替换掉!新的 main.sh 如下:

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

filename="applist.txt"
lines=$(cat ${filename} | sed '/^$/d')
linenum=$(echo "${lines}" | wc -l)
#echo -e "${lines}"
#echo -e "${linenum}"
index=1
while [[ ${index} -le ${linenum} ]];do
#echo ${index}
line=$(echo "${lines}" | sed -n "${index}p")
app_id=$(echo $line | awk '{print $1}')
idc=$(echo $app_id | awk -F'.' '{print $NF}')
bash modify.sh ${idc} ${app_id}
index=$((${index}+1))
done

或者使用for循环:

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

filename="applist.txt"
lines=$(cat ${filename} | sed '/^$/d')
linenum=$(echo "${lines}" | wc -l)
#echo -e "${lines}"
#echo -e "${linenum}"
for index in `seq 1 ${lines}`;do
#echo ${index}
line=$(echo "${lines}" | sed -n "${index}p")
app_id=$(echo $line | awk '{print $1}')
idc=$(echo $app_id | awk -F'.' '{print $NF}')
bash modify.sh ${idc} ${app_id}
done