shell中如何做流式任务

超级欧派课程 2024-03-08 00:55:33

有许多技术可以实现这个目标。选择使用哪种技术取决于许多因素,其中最重要的因素是我们正在编辑的内容。这个页面上也包含了来自多个作者的相互矛盾的建议。这是一个非常棘手的主题,没有普遍正确的答案(但有很多普遍错误的方法)。

文件

在开始之前,请注意编辑文件是一个非常糟糕的想法。修改文件的首选方法是在同一文件系统中创建一个新文件,将修改后的内容写入其中,然后将其重命名为原始名称。这是防止在写入过程中发生崩溃时数据丢失的唯一方法。然而,使用临时文件和重命名操作会破坏对文件的硬链接(无法避免),将符号链接转换为硬链接文件,并且您可能需要额外的步骤将原始文件的所有权、权限(以及可能的其他元数据)转移到新文件上。有些人更愿意冒险接受数据丢失的微小可能性,而不愿意面对硬链接丢失的更大可能性,以及chown/chmod(以及可能的setfattr、setfacl、chattr等)的不便。

您将面临的另一个主要问题是,所有用于编辑文件的标准Unix工具都期望搜索模式是某种正则表达式。如果将您没有创建的输入作为搜索模式传递,可能会包含破坏程序解析器的语法,这可能导致失败或Code Injection攻击。

告诉我该怎么做

如果您的搜索字符串或替换字符串来自外部来源(例如环境变量、参数、文件、用户输入),因此不在您的控制之下,以下命令是您最好的选择:

in="$search" out="$replace" perl -pi -e 's/\Q$ENV{"in"}/$ENV{"out"}/g' ./*

这将在当前目录中的所有文件中进行操作。如果您想要递归地操作整个目录树,则可以使用以下命令:

in="$search" out="$replace" find . -type f -exec \ perl -pi -e 's/\Q$ENV{"in"}/$ENV{"out"}/g' -- {} +

当然,您可以向find命令提供其他选项以限制替换的文件范围。

一些挑剔的读者可能会注意到,这些命令使用了Perl,而Perl并不是一个标准工具。这是因为没有任何标准工具能够安全地执行这个任务。

如果由于受限的执行环境而被迫使用标准工具,那么您将不得不权衡下面的选项,并选择对文件造成最小损害的方法。

使用文件编辑器

实际编辑文件的主要工具是ed和ex(vi是ex的可视模式)。

ed是标准的UNIX基于命令的编辑器,而ex则是另一个标准的命令行编辑器。以下是一些常用的语法,用于在名为file的文件中将字符串"olddomain.com"替换为"newdomain.com"。这四个命令执行的功能相同,只是在可移植性和效率方面略有不同:

## Exex -sc '%s/olddomain\.com/newdomain.com/g|x' file## Ed# Bashed -s file <<< $'g/olddomain\\.com/s//newdomain.com/g\nw\nq'# Bourne(使用printf)printf '%s\n' 'g/olddomain\.com/s//newdomain.com/g' w q | ed -s fileprintf 'g/olddomain\\.com/s//newdomain.com/g\nw\nq' | ed -s file# Bourne(不使用printf)ed -s file <<!g/olddomain\\.com/s//newdomain.com/gwq!

如果要在当前目录的所有文件中替换字符串,只需将上述任意一种方式放入循环中:

for file in ./*; do [[ -f $file ]] && ed -s "$file" <<< $'g/old/s//new/g\nw\nq'done

要递归执行此操作,可以在bash 4中启用globstar(使用shopt -s globstar,在您的~/.bashrc中加入此行是个好主意),然后使用以下命令:

# Bash 4+(shopt -s globstar)for file in ./**; do [[ -f $file ]] && ed -s "$file" <<< $'g/old/s//new/g\nw\nq'done

如果您没有bash 4,可以使用find命令。但不幸的是,为每个文件提供ed的标准输入有点麻烦:

find . -type f -exec sh -c 'for f do ed -s "$f" <<!g/old/s//new/gwq!done' sh {} +

由于ex从命令行获取其命令,所以从find中调用它相对简单:

find . -type f -exec ex -sc '%s/old/new/g|x' {} \;

但是请注意,如果您的ex是vim提供的,对于不包含"old"的文件,它可能会卡住。在这种情况下,您可以添加"e"选项来忽略这些文件。当vim作为您的ex时,您还可以使用"argdo"和find的"{} +"来最小化要运行的ex进程的数量:

# Bash 4+(shopt -s globstar)ex -sc 'argdo %s/old/new/ge|x' ./**# Bournefind . -type f -exec ex -sc 'argdo %s/old/new/ge|x' {} +

您还可以要求在每次替换时进行确认。每次都需要输入"y"或"n"。请注意,命令中的"A"在两个地方都被使用。这种方法适用于可能发生错误替换(例如使用自然语言)且数据集较小的情况:

find . -type f -name '*.txt' -exec grep -q 'A' {} \; -exec vim -c '%s/A/B/gc' -c 'wq' {} \;使用临时文件

如果搜索字符串和/或替换字符串使用了shell变量,则ed不适用。sed或任何使用正则表达式的工具也不适用。

gsub_literal "$search" "$rep" < "$file" > tmp && mv -- tmp "$file"# 使用GNU工具以保留所有权/组/权限gsub_literal "$search" "$rep" < "$file" >临时文件 && chown --reference="$file" tmp && chmod --reference="$file" tmp && mv -- tmp "$file"使用非标准工具

sed 是一个流编辑器,而不是一个文件编辑器。然而,人们经常滥用它来尝试编辑文件。它并不直接编辑文件。GNU sed(以及一些 BSD sed)提供了一个 -i 选项,它会创建一个副本,并用副本替换原始文件。这是一个开销较大的操作,但如果你喜欢不可移植的代码、I/O 开销和不良副作用(如破坏符号链接)以及 CodeInjection 的攻击,这可能是一个选择:

sed -i 's/old/new/g' ./* # GNU, OpenBSDsed -i '' 's/old/new/g' ./* # FreeBSD

如果你使用 perl 5,你也可以使用以下代码实现相同的功能:

perl -pi -e 's/old/new/g' ./*

使用 find 递归地进行替换:

find . -type f -exec perl -pi -e 's/old/new/g' -- {} \; # 如果你的 find 还没有 + 选项find . -type f -exec perl -pi -e 's/old/new/g' -- {} + # 如果有 + 选项

如果你想要删除行而不是进行替换:

# 删除包含 perl 正则表达式 "foo" 的任意行perl -ni -e 'print unless /foo/' ./*

如果想要将所有的 "unsigned" 替换为 "unsigned long",除非它是 "unsigned int" 或 "unsigned long" ...:

find . -type f -exec perl -i.bak -pne \ 's/\bunsigned\b(?!\s+(int|short|long|char))/unsigned long/g' -- {} \;

上面的示例都使用了正则表达式,这意味着它们和之前的 sed 代码一样存在同样的问题:尝试在其中嵌入 shell 变量是一个糟糕的主意,将任意值视为字面字符串也可能会带来困难。

如果输入不在你的直接控制下,你可以将它们作为变量传递给搜索和替换字符串,而无需取消引用或与 sigil 字符冲突的可能性:

in="$search" out="$replace" perl -pi -e 's/\Q$ENV{"in"}/$ENV{"out"}/g' ./*

或者,将其封装在一个有用的 shell 函数中:

# Bash# 用法:replace FROM TO [file ...]replace() { in=$1 out=$2 perl -p ${3+'-i'} -e 's/\Q$ENV{"in"}/$ENV{"out"}/g' -- "${@:3}"}

该封装在存在文件名时传递了 perl 的 -i 选项,以便对它们进行“原地编辑”(或者至少在 perl 的范围内进行编辑,详细信息请参阅 perl 文档)。

变量

当涉及到变量替换时,你需要小心处理。在 Bash 中,你可以使用参数扩展来替换变量中的内容。例子如下:

# Bashvar='some string'var=${var//some/another}

然而,如果你将替换字符串保存在变量中,就需要注意不同 Bash 版本的行为可能不一致。以下是一个示例:

# Bashvar='some string'search=some; rep=another# 赋值始终一致的工作。注意引号。var=${var//"$search"/"$rep"}# 在赋值外部的扩展不一致。echo "${var//"$search"/"$rep"}" # 在 bash 4.3 及更高版本中可行。echo "${var//"$search"/$rep}" # 在 bash 5.1 及更早版本中可行。流处理

如果您希望修改流,并且您的搜索和替换字符串在预先已知的情况下,请使用流编辑器(stream editor)sed:

some_command | sed 's/foo/bar/g'

sed使用正则表达式。在我们的示例中,foo和bar是字面字符串。如果它们是变量(例如用户输入),为了防止错误,它们必须严格转义。这非常不实际,试图这样做会使您的代码极易出错。在sed命令中嵌入shell变量永远都不是一个好主意,它是CodeInjection错误的主要来源。

您也可以在Bash中自行完成:

search=foo rep=barwhile IFS= read -r line; do printf '%s\n' "${line//"$search"/"$rep"}"done < <(some_command)# 或者some_command | while IFS= read -r line; do printf '%s\n' "${line//"$search"/"$rep"}"done

如果您想要进行更复杂的处理而不仅仅是简单的搜索/替换,这可能是最好的选择。请注意,最后一个示例在SubShell中运行循环。

但是,您可能会注意到,上述的Bash循环在处理大型数据集时非常慢。那么,我们如何找到更快的方法来替换字面字符串呢?嗯,您可以使用awk。以下函数将从stdin读取并将所有的STR替换为REP,并将结果写入stdout。

# 用法:gsub_literal STR REP# 将所有的STR替换为REP。从stdin读取并写入stdout。gsub_literal() { # STR不能为空 [[ $1 ]] || return str=$1 rep=$2 awk ' # 获取搜索字符串的长度 BEGIN { str = ENVIRON["str"] rep = ENVIRON["rep"] len = length(str); } { # 清空输出字符串 out = ""; # 只要在行中存在搜索字符串,就继续循环 while (i = index($0, str)) { # 将搜索字符串之前的部分和替换字符串追加到输出字符串 out = out substr($0, 1, i-1) rep; # 从行中删除第一次出现的搜索字符串及其之前的内容 $0 = substr($0, i + len); } # 追加剩余的部分 out = out $0; print out; } '}some_command | gsub_literal "$search" "$rep"# 作为一行压缩的写法:some_command | s=$search r=$rep awk 'BEGIN {s=ENVIRON["s"]; r=ENVIRON["r"]; l=length(s)} {o=""; while (i=index($0, s)) {o=o substr($0,1,i-1) r; $0=substr($0,i+l)} print o $0}'更多

如果您觉得文章内容对你有一点帮助可以关注我,我在头条平台会持续分享更多实用的shell技巧和最佳实践,如果想系统的快速学习shell的各种高阶用法和生产环境避坑指南可以看看《shell脚本编程最佳实践》专栏,专栏里有更多的实用小技巧和脚本代码分享。

0 阅读:0

超级欧派课程

简介:感谢大家的关注