如何在一个文件中定义的内容替换另一文件

如何在一个文件中定义的内容替换另一文件

前言:每隔几天就会出现一个这样的问题,用 很容易解决sed,但解释起来需要时间。我正在写这个问题和答案,因此我稍后可以参考这个通用解决方案,并只解释对特定情况的适应。请随意贡献。

我有带有变量定义的文件。变量由大写字母或下划线组成_,其值位于:=.这些值可以包含其他变量。这是Gnom.def

NAME:=Gnom
FULL_NAME:=$FIRST_NAME $NAME
FIRST_NAME:=Sman
STREET:=Mainstreet 42
TOWN:=Nowhere
BIRTHDAY:=May 1st, 1999

然后还有另一个form.txt带有模板表单的文件:

$NAME
Full name: $FULL_NAME
Address: $STREET in $TOWN
Birthday: $BIRTHDAY
Don't be confused by $NAMES

现在我想要一个脚本,如果需要,可以用其他文件中的定义递归地替换表单中的变量(标记为$和标识符),这样我就可以得到以下文本:

Gnom
Full name: Sman Gnom
Address: Mainstreet 42 in Nowhere
Birthday: May 1st, 1999
Don't be confused by $NAMES

最后一行是确保变量的子字符串不会被意外替换。

答案1

解决此类问题的基本思想是将两个文件传递给sed.首先是定义,存储在保留空间sed。然后,另一个文件的每一行都会附加保留空间,并且在附加定义中重复出现的每个变量都会被替换。

这是脚本:

sed '/^[A-Z_]*:=.*/{H;d;}
  G
 :b
 s/$\([A-Z_]*\)\([^A-Z_].*\n\1:=\)\([^[:cntrl:]]*\)/\3\2\3/
 tb
 P
 d' Gnom.def form.txt

现在详细解释:

/^[A-Z_]*:=.*/{H;d;}

这会将定义收集到保留空间。/^[A-Z_]*:=.*/选择以变量名称和序列开头的所有行:=。在这些行中{},执行命令:将H它们附加到保留空间,d删除它们并重新开始,因此它们不会被打印。

如果您不能确保定义文件中的所有行都遵循此模式,或者如果其他文件中的行可以匹配给定模式,则需要调整此部分,如稍后所述。

G

此时脚本仅处理第二个文件中的行。将G保留空间附加到模式空间,因此我们需要使用模式空间中的所有定义来处理该行,并用换行符分隔。

:b

这将开始一个循环。

 s/$\([A-Z_]*\)\([^A-Z_].*\n\1:=\)\([^[:cntrl:]]*\)/\3\2\3/

这是关键部分,替换。现在我们有类似的东西

At the $FOO<newline><newline>FOO:=bar<newline>BAR:=baz
       ----==================---  ###

在模式空间中。 (详细信息:第一个定义之前有两个换行符,一个是通过附加到保留空间产生的,另一个是通过附加到缓冲区空间产生的。)

----用火柴划下划线的部分$\([A-Z_]*\)。这\(\)使得稍后可以反向引用该字符串。

\([^A-Z_].*\n\)匹配带下划线的部分===,这是反向引用之前的所有内容\1。从无 n 变量字符开始可确保我们不匹配变量的子字符串。用换行符包围反向引用并:=确保定义的子字符串不匹配。

最后,\([^[:cntrl:]]*\)匹配###部分,即定义。请注意,我们假设定义没有控制字符。如果这是可能的,您可以使用[^\n]GNUsed或为 POSIX 做一个解决方法sed

现在$和 变量名称被变量值替换\3,中间部分和定义保持原样:\2\3

 tb

如果已进行替换,该t命令将循环进行标记b并尝试另一个替换。

 P

如果无法进行进一步的替换,则大写P将打印第一个换行符之前的所有内容(因此,定义部分将不会被打印)并且

 d

将删除模式空间并开始下一个循环。完毕。

局限性

  • 您可以做一些令人讨厌的事情,例如在定义文件中包含FOO:=$BARBAR:=$FOO并使脚本永远循环。您可以定义处理顺序来避免这种情况,但这会使脚本更难以理解。如果您的脚本不需要白痴证明,请将其保留。

  • 如果定义可以包含控制字符,在 后G,我们可以与另一个字符交换换行符y/\n#/#\n,并在打印之前重复此操作。我不知道更好的解决方法。

  • 如果定义文件可以包含具有不同格式的行,或者另一个文件可以包含具有定义格式的行,则我们需要在两个文件之间使用唯一的分隔符,可以作为定义文件的最后一行,也可以作为另一个文件的第一行,或者作为单独的文件您sed在其他文件之间传递。然后,您有一个循环来收集定义,直到满足分隔线为止,然后对另一个文件的行进行循环。

答案2

为了与 sed 脚本进行比较,这里有一个 POSIX awk 脚本:

$ cat tst.awk
BEGIN { FS=":=" }
NR==FNR {
    map["$"$1] = $2
    next
}
{
    mappedWord = 1
    while ( mappedWord ) {
        mappedWord = 0
        head = ""
        tail = $0
        while ( match(tail,/[$][[:alnum:]_]+/) ) {
            word = substr(tail,RSTART,RLENGTH)
            if ( word in map ) {
                word = map[word]
                mappedWord = 1
            }
            head = head substr(tail,1,RSTART-1) word
            tail = substr(tail,RSTART+RLENGTH)
        }
        $0 = head tail
    }
    print
}

$ awk -f tst.awk Gnom.def form.txt
Gnom
Full name: Sman Gnom
Address: Mainstreet 42 in Nowhere
Birthday: May 1st, 1999
Don't be confused by $NAMES

该脚本不关心您使用的任何字符或字符串(与 sed 版本不同,它显然依赖于不存在控制字符且未:=出现在第二个文件中),只需包含可以组成要在正则表达式 arg 为match(), 目前[$][[:alnum:]_]+.

给定递归定义,上面的方法会失败,但如果您愿意,可以通过简单的调整来合理地检测、报告和处理它,例如:

$ head Gnom.def form.txt
==> Gnom.def <==
FOO:=$BAR
BAR:=$FOO
NAME:=Gnom
FULL_NAME:=$FIRST_NAME $NAME
FIRST_NAME:=Sman
STREET:=Mainstreet 42
TOWN:=Nowhere
BIRTHDAY:=May 1st, 1999

==> form.txt <==
$NAME
Full name: $FULL_NAME
Address: $STREET in $TOWN
Birthday: $BIRTHDAY
Don't be confused by $NAMES
testing recursive $FOO
testing recursive $BAR

$ cat tst.awk
BEGIN { FS=":=" }
NR==FNR {
    map["$"$1] = $2
    next
}
{
    mappedWord = 1
    iter = 0
    delete mapped
    while ( mappedWord ) {
        if ( ++iter == 100 ) {
            printf "%s[%d]: Warning: Breaking out of recursive definitions.\n", FILENAME, FNR | "cat>&2"
            break
        }
        mappedWord = 0
        head = ""
        tail = $0
        while ( match(tail,/[$][[:alnum:]_]+/) ) {
            word = substr(tail,RSTART,RLENGTH)
            if ( word in map ) {
                word = map[word]
                mappedWord = 1
            }
            head = head substr(tail,1,RSTART-1) word
            tail = substr(tail,RSTART+RLENGTH)
        }
        $0 = head tail
        for (word in mapped) {
            mapped[word]++
        }
    }
    print
}

$ awk -f tst.awk Gnom.def form.txt
Gnom
Full name: Sman Gnom
Address: Mainstreet 42 in Nowhere
Birthday: May 1st, 1999
Don't be confused by $NAMES
testing recursive $BAR
testing recursive $FOO
form.txt[6]: Warning: Breaking out of recursive definitions.
form.txt[7]: Warning: Breaking out of recursive definitions.

请注意,上述警告将打印到 stderr,而不是 stdout,因此它们不会扰乱您的输出:

$ awk -f tst.awk Gnom.def form.txt 2>err
Gnom
Full name: Sman Gnom
Address: Mainstreet 42 in Nowhere
Birthday: May 1st, 1999
Don't be confused by $NAMES
testing recursive $BAR
testing recursive $FOO

$ cat err
form.txt[6]: Warning: Breaking out of recursive definitions.
form.txt[7]: Warning: Breaking out of recursive definitions.

相关内容