文件编辑:将不存在的行追加到文件
1.概述
本文将指导我们如何使用多种工具来达到同一个目标。我们的目标是仅在文件中没有这样的行时才在文件中附加一行。例如,当我们想要使用一些通用包含更新配置文件时,这非常有用。
2. 我们的示例文件
在本文中,我们将使用一个真实的示例。
假设我们要确保所有用户的*.bashrc文件都包含一行来包含我们的特殊项目变量。一些用户已经有这个包括。他们是高级用户,可能有一些个人覆盖,并且绝对不希望我们弄乱他们的.bashrc*文件。
所以,普通用户的*.bashrc*将包含:
# regular.bashrc
VAR1=foo
而高级用户的*.bashrc*将是:
# power-user.bashrc
OVERRIDE_VAR1=foo
source shared/projectx/project.bashrc
PROJECT_VAR=$OVERRIDE_VAR1
最后,我们项目的*.bashrc*包含我们想要为所有用户设置的特殊变量:
# shared/projectx/project.bashrc
# Source this file in your .bashrc to include ProjectX's special variables
PROJECT_VAR=bar
我们的目标是将行source shared/projectx/project.bashrc添加到regular.bashrc而不是从命令行添加到power-user.bashrc。
3. 使用grep和echo
使用 Linux 命令行时,我们手头的第一个工具是grep 和echo 。我们将使用grep来搜索文件中是否存在该行,并使用echo将其写入文件:
for bashrc_file in *.bashrc; do
if ! grep -qF 'source shared/projectx/project.bashrc' $bashrc_file; then
echo "source shared/projectx/project.bashrc" >> $bashrc_file
fi
done
我们在这里结合grep命令的两个标志来检查我们的行是否存在:
- -q告诉grep保持安静——我们只想检查命令的退出状态
- -F指示grep将我们的模式解释为固定字符串,而不是正则表达式
4. 使用sed
sed 是流编辑器的简写,是原始 Unix 编辑器ed的改进版本。
我们可以使用sed在文件中搜索我们的字符串,并在找到我们的字符串后尝试使用*“q”命令退出,如果我们没有找到这样的字符串则追加*。
不幸的是,这不起作用,因为sed会逐行读取我们的文件,然后将其写回,因此退出将阻止sed继续读取文件,从而有效地导致文件的其余部分被删除。
因此,我们将使用一个聪明的小技巧来测试我们的字符串是否存在。
当我们找到字符串时,我们将把该行复制到hold空间,然后我们将继续到文件的末尾。 此时,我们的pattern 空间中的最后一行和我们在hold空间中搜索的字符串,或者如果未找到字符串,则为空的hold空间。
棘手的部分来了。Sed有一个测试命令,允许我们检查pattern空间中的成功替换。要使用此功能,我们将首先交换hold和pattern空间,并用空pattern替换我们的字符串,从而有效地擦除它。
在这一点上,我们可以进行测试,但不能在我们从保留空间获取最后一行之前进行。
** 测试命令失败将导致sed完成循环,实际上不会运行下一个命令,该命令将在未找到字符串的情况下附加字符串。**
让我们把它们放在一起:
source_str='source shared/projectx/project.bashrc'
for bashrc_file in *.bashrc; do
sed -i -e "\|$source_str|h;" `# Search for the source string and copy it to the hold space` \
-e "\${" `# Go to the end of the file and run the following commands` \
-e "x;" `# Exchange the last line with the hold space` \
-e "s|$source_str||;" `# Erase the source string if it was actually found` \
-e "{g;" `# Bring back the last line` \
-e "t};" `# Test if the substitution succeeded (the source string was found)` \
-e "a\\" -e "$source_str" `# Append the source string if we didn't move to the next cycle` \
-e "}" $bashrc_file
done
5. 使用awk
创建awk 编程语言是为了有效地处理大型文本文件。
让我们记住我们的目标:在该行不存在的文件中追加一行,如果存在则不理会它。
与sed的*-i*标志允许就地修改文件不同,awk不允许我们写入我们正在读取的同一个文件。为此,我们将使用 Linux mktemp 实用程序。
我们将从使用print命令打印记录开始。接下来,**如果我们找到源字符串,我们会将found变量设置为1。**最后,一旦我们到达最后一条记录,如果没有设置found,我们将打印我们的字符串。
要使用环境变量,我们将使用awk命令的*-v* 标志传递由我们的 shell 设置的环境变量。
让我们把它们放在一起:
source_str='source shared/projectx/project.bashrc'
tempfile=$(mktemp)
for bashrc_file in *.bashrc; do
awk -v source_str="$source_str" \
'{print}; $0~source_str {found=1}; END {if (!found) {print source_str}}' $bashrc_file > $tempfile
mv $tempfile $bashrc_file
done
6. 比较我们的方法
在比较我们的方法时,我们希望根据清晰度和性能对它们进行评分。
就清晰度而言,我们绝对的赢家是使用grep和echo。它清晰简洁,除了两个标志,其中一个对于我们的解决方案是可选的,在使用方面不需要知道其他任何东西。
但是性能呢?我们将比较两种情况:
- 我们的一些用户 (10%) 需要编辑文件
- 我们的大多数用户 (90%) 需要编辑文件
6.1. 创建我们的测试场景
我们将从使用一些随机单词创建 500 个可变长度的文件开始:
for number in {1..500}; do
cat /usr/share/dict/words | sort -R | head -$(shuf -i 1000-10000 -n 1) > file$number.bashrc
done
我们将使用我们的简短脚本稍作修改,在一些文件中插入我们的源代码行:
for bashrc_file in $(ls | shuf -n 50); do
if ! grep -qF 'source shared/projectx/project.bashrc' $bashrc_file; then
echo "source shared/projectx/project.bashrc" >> $bashrc_file
cat $bashrc_file | shuf -o $bashrc_file
fi
done
为了给我们的结果计时,我们将使用time 工具。任何 Linux 命令之前的运行time都会为我们运行的命令提供三个不同的时间值。我们想看看真正的价值,它向我们展示了所谓的挂钟时间。
6.2. 分析结果
让我们看一下结果:
grep+echo | awk | sed | |
---|---|---|---|
更改 50 个文件 | 0.358 秒 | 2.047 秒 | 2.194 秒 |
更改 450 个文件 | 0.378 秒 | 2.171 秒 | 2.128 秒 |
使用sed的数字起初看起来违反直觉。更改 450 文件不应该比更改 50 花费更多时间吗?直到我们意识到如果sed在文件中找到该行并测试替换,它将使用保持缓冲区。
如果替换测试通过(意味着找到了字符串),sed将通过发出brk 系统调用来停止程序。在sed的情况下,这些额外的系统调用反转了性能逻辑。因此,更新更多文件将花费更少的时间。
如果我们稍微修改一下脚本以对所有文件并行运行我们的工具会怎样?在这种情况下,我们将看到修复最大文件所需的时间:
grep+echo | awk | sed | |
---|---|---|---|
更改 50 个文件 | 0.101 秒 | 0.364 秒 | 0.378 秒 |
更改 450 个文件 | 0.107 秒 | 0.405 秒 | 0.381 秒 |
我们不难看出,grep解决方案不仅是最清晰的解决方案,而且也是最快的解决方案。这是有道理的,因为awk和sed实际上会遍历整个文件,而grep可以在找到该行后退出。