邮件合并和酷炫 Bash 数组的乐趣

作者:Dave Taylor

创建一个基于 sed 的文件替换工具。

几周前,我在我的垃圾邮件文件夹中翻找,发现一封电子邮件,开头是这样的:


Dear #name#
Congratulations on winning the $15.7 million lottery payout!
To learn how to claim your winnings, please...

显然,这是一个骗局(真的有人会上当吗?),但引起我注意的是 #name# 序列。显然,这是发送者的失误,他可能不知道如何使用 AnnoyingSpamTool 1.3 或其他任何他或她使用的工具。

然而,批量电子邮件和文件转换的更通用表示法非常有趣。有很多正当理由使用这种替换,从电子邮件新闻通讯(比如我每周从 AskDaveTaylor.com 发送的新闻通讯——去看看!)到股东公告等等。

以此为灵感,让我们构建一个提供此功能的工具。

简单版本将是 1:1 替换,因此 #name# 变成,例如,“Rick Deckard”,而 #first# 可能是“Rick”,而 #last# 可能是“Deckard”。让我们以此为基础构建,但让我们从小处着手。

Linux 中的简单单词替换

有很多方法可以从命令行处理单词替换,从 Perl 到 awk,但这里我使用最初的 UNIX 命令 sed(流编辑器),它正是为此目的而设计的。替换的通用表示法是 s/old/new/,如果您在末尾添加一个 g,它将匹配行上的每个出现项,而不仅仅是第一个,因此完整命令是 s/old/new/g。

在继续之前,这是一个简单的文档,其中嵌入了必要的替换


$ cat convertme.txt
#date#

Dear #name#, I wanted to start by again thanking you for your
generous donation of #amount# in #month#. We couldn't do our
work without support from humans like you, #first#.

This year we're looking at some unexpected expenses,
particularly in Sector 5, which encompasses #state#, as you
know. I'm hoping you can start the year with an additional
contribution? Even #suggested# would be tremendously helpful.

Thanks for your ongoing support. With regards,

Rick Deckard
Society for the Prevention of Cruelty to Replicants

浏览一下,您会看到有很多替换要做:#date##name##amount##month##first##state##suggested#。事实证明,#date# 将被替换为当前日期,而 #suggested# 是一个将在处理信件时计算出来的,但这稍后会介绍,所以请继续关注。

为了简化操作,逗号分隔列表的源文件允许与源电子表格轻松交互,因此示例输入数据文件可能如下所示


name:first:amount:month:state
Eldon Tyrell:Eldon:500:July:California

最基本的是,第一行定义变量名(没有 # 符号),随后的行是一组特定捐赠者或接收者的值。首先,让我们读入变量名


while IFS=',' read -r f1 f2 f3 f4 f5 f6 f7
do
  declare -a varname=($f1 $f2 $f3 $f4 $f5 $f6 $f7)
done

理解这一点的关键是了解 IFS,即内部字段分隔符。通常,它是空格,这就是为什么,例如,ls my file name 查找名为 my、file 和 name 的三个文件。但是您可以更改它,正如我通过将 IFS 更改为逗号所演示的那样。

那些酷炫的 Bash 数组

我声明一个名为 varname 的数组,它接收读入脚本的每个字段。此时只使用了五个字段,但让我们支持最多七个字段,以使生成的脚本更灵活一些。

数组在 Bash 中实际上非常酷,但表示法有点古怪。也就是说,您不能只使用 $array[index],因为它无法正确解析,因此花括号是必要的补充


echo ${varname[1]}

这工作得很好。

对于基本算法,您将有两个并行数组(并行指的是它们的索引将匹配):一个保留所有变量名,另一个包含数据条目列表的此实例的值。

这意味着您需要区分脚本读取第一行和读取数据文件的后续行的情况。很容易做到


(( lines++ ))

if [ $lines -eq 1 ] ; then   # field names
  # variable names
  declare -a varname=($f1 $f2 $f3 $f4 $f5 $f6 $f7)
else
  # values for this line (can contain spaces)
  declare -a value=("$f1" "$f2" "$f3" "$f4" "$f5"
     "$f6" "$f7")
fi

与大多数代码一样,这在这里做出了一些假设,但它们是安全的:变量名没有被引用,因为它们始终是单个单词,但变量值可能包含空格,因此它们最终在 declare 语句中被引用。否则,这应该很容易,(( lines++ )) 表示法应该让您欢呼——这是一个不错的 Bash 快捷方式!

一旦您过了第一行,脚本就可以在 varname[x] 中查找第 x 个变量名,并在 value[x] 中查找该命名变量的值,表示为一系列 sed 友好的替换命令


for ((i=0; i<${#value[*]}; i++))
do
  if [ ! -z "${value[$i]}" ] ; then
    echo "s/#${varname[$i]}#/${value[$i]}/g"
  fi
done

这会产生以下结果


s/#name#/Eldon Tyrell/g
s/#first#/Eldon/g
s/#amount#/500/g
s/#month#/July/g
s/#state#/California/g

这实际上非常接近您想要的。让我们继续前进。

使用 sed

流编辑器 sed 比其不起眼和古老的历史所暗示的要强大得多。正如上面所示,它非常适合这项工作。

您可以将上述行写入临时文件并直接调用 sed,但让我们避免文件 I/O 并根据需要将其全部转换为命令行参数。这可以通过简单地用分号分隔每个命令来完成,您可以通过在临时变量中构建它来做到这一点


for ((i=0; i<${#value[*]}; i++))
do
  if [ ! -z "${value[$i]}" ] ; then
    if [ -z "$SUBS" ] ; then
      SUBS="s/#${varname[$i]}#/${value[$i]}/g"
    else
      SUBS="$SUBS;s/#${varname[$i]}#/${value[$i]}/g"
    fi
  fi
done

毫无疑问,有一种方法可以避免最内层的 if-then-else 语句来省略不必要的分号前缀,但有时拥有几行代码比更多的乱码更容易。

否则,上面是从之前显示的 for 循环的简单扩展。这次,它在 SUBS 替换变量中构建整个 sed 命令。以下是如何测试


echo "   sed \"$SUBS\" $inputfile"

当您使用输入数据文件运行此命令时,以下内容将推送到终端


sed "s/$name$/Eldon Tyrell/g;s/$first$/Eldon/g;
    s/$amount$/500/g;s/$month$/July/g;
    s/$state$/California/g" convertme.txt
sed "s/$name$/Rachel/g;s/$first$/Rachel/g;
    s/$amount$/100/g;s/$month$/March/g;
    s/$state$/New York/g" convertme.txt

(注意:添加换行符仅用于格式化目的。)

从这里到调用命令实际上只有很小一步,所以让我们这样做


$ sub.sh
#date#

Dear Eldon Tyrell, I wanted to start by again thanking you
for your generous donation of 500 in July. We couldn't do
our work without support from humans like you, Eldon.

This year we're looking at some unexpected expenses,
particularly in Sector 5, which encompasses California, as
you know. I'm hoping you can start the year with an
additional contribution? Even #suggested# would be
tremendously helpful.

Thanks for your ongoing support. With regards,

Rick Deckard
Society for the Prevention of Cruelty to Replicants
$

一般来说,这看起来不错。#date##suggested# 仍然未翻译,但这在意料之中。有点奇怪的是它也没有获得第二个条目。一个错误。

我将在这里停止,也许下次,我将添加一些系统替换,如 #date#,并弄清楚如何计算 #suggested#,这可以是实际捐赠的 50%。再见!

Dave Taylor 在 UNIX 和 Linux 系统上编写 shell 脚本已经很长时间了。他是 Learning Unix for Mac OS XWicked Cool Shell Scripts 的作者。您可以在 Twitter 上找到他,账号为 @DaveTaylor,您可以通过他的技术问答网站联系他:Ask Dave Taylor

加载 Disqus 评论