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

0%

好好学Shell:循环读取文本

1. 前言

本文中,记录使用shell循环读取文本的常用方法和技巧。

2. 读取内容并拼接

2.1. 需求描述

已知mobile.txt为:

1
2
haojin 17625160000
voidking 17625160001

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

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

2.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. 读取IP和端口并测试连通性

3.1. 需求描述

使用while read line循环读取文本的方法,对于单纯的拼接需求是没有问题的。但是,如果涉及到输入缓存,就会有问题了。

已知service.txt内容为:

1
2
3
127.0.0.1 80
127.0.0.1 8080
192.168.56.101 8080

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

3.2. 错误实现

我们用while read line方式实现脚本:

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循环结束。

3.3. 更正实现

解决办法:使用重定向将 /dev/null (一个特殊的设备文件,会丢弃所有写入其中的数据,并在读取时立即结束文件)作为输入传递给 nc。

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

实际上,这意味着 nc 会立即得到一个“文件结束”(EOF)的指示,无需等待用户输入。这样做可以让 nc 在尝试连接后立即终止,不会保持等待用户数据输入。

或者,改用 -zv 参数使得nc命令不需要输入。因为这里探讨的是 nc、telnet、ssh 这类等待输入的命令在while循环中的通用解决办法,所以不使用-zv参数。

因此,正确的脚本应该改成:

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. 读取内容并用户交互

4.1. 需求描述

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

已知applist.txt内容为:

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

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

4.2. 错误实现

按照while read line的思路,编写 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会读取缓存中的内容,而不是等待交互。

4.3. 更正实现

那么,怎么解决上面的这个问题?非要使用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 ${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}
done

5. 读取文件并分批

5.1. 需求描述

需求:文件中有N行文本,读取文件内容后,平均分成M份,每份都组合成一个新的string,以逗号分隔。

文件文本示例内容为:

1
2
3
4
111
222
333
444

5.2. 脚本实现

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
#!/bin/bash

# 检查脚本是否接收到一个参数
if [ "$#" -ne 1 ]; then
echo "Usage: $0 <filename>"
exit 1
fi

filename=$1
M=3

# 检查文件是否存在
if [ ! -f "$filename" ]; then
echo "File not found!"
exit 1
fi

# 计算文件中总行数
total_lines=$(wc -l < "$filename")

# 计算每个部分的大概行数
lines_per_part=$(( ( total_lines + M - 1 ) / M ))

# 分割文件并处理每一部分
for (( part=1; part<=M; part++ )); do
start_line=$(( (part - 1) * lines_per_part + 1 ))
end_line=$(( part * lines_per_part ))
if [ $end_line -gt $total_lines ]; then
end_line=$total_lines
fi

# 提取当前部分文本并转化为逗号分隔的字符串
part_content=$(sed -n "${start_line},${end_line}p" "$filename" | tr '\n' ',' | sed 's/,$//')

# 输出当前部分的逗号分隔的字符串
echo "part$part $part_content"
# 执行分批后的逻辑处理

# 判断是否为最后一部分
if [ $end_line -eq $total_lines ]; then
break
fi
done