在awk中,如何在printf中使用包含多个格式字符串的文件?

时间:2014-07-04 13:59:59

标签: awk printf

我有一个案例,我希望在awk中使用文件中的输入作为printf()的格式。当我在代码中的字符串中设置它时,我的格式化工作正常,但是当我从输入中加载它时它不起作用。

以下是问题的一个小例子:

$ # putting the format in a variable works just fine:
$ echo "" | awk -vs="hello:\t%s\n\tfoo" '{printf(s "bar\n", "world");}'
hello:  world
        foobar
$ # But getting the format from an input file does not.
$ echo "hello:\t%s\n\tfoo" | awk '{s=$0; printf(s "bar\n", "world");}'
hello:\tworld\n\tfoobar
$ 

所以......格式替换工作(" %s"),但不是像tab和换行符这样的特殊字符。知道为什么会这样吗?是否有办法做某事"输入数据以使其可用作格式字符串?

更新#1:

作为另一个例子,请考虑以下使用bash heretext:

[me@here ~]$ awk -vs="hello: %s\nworld: %s\n" '{printf(s, "foo", "bar");}' <<<""
hello: foo
world: bar
[me@here ~]$ awk '{s=$0; printf(s, "foo", "bar");}' <<<"hello: %s\nworld: %s\n"
hello: foo\nworld: bar\n[me@here ~]$

据我所知,多个不同的awk解释器也会发生同样的事情,而且我还没能找到解释原因的文档。

更新#2:

我尝试替换的代码当前看起来像这样,在shell中嵌套循环。目前,awk 用于其printf,可以替换为基于shell的printf

#!/bin/sh

while read -r fmtid fmt; do
  while read cid name addy; do
    awk -vfmt="$fmt" -vcid="$cid" -vname="$name" -vaddy="$addy" \
      'BEGIN{printf(fmt,cid,name,addy)}' > /path/$fmtid/$cid
  done < /path/to/sampledata
done < /path/to/fmtstrings

示例输入为:

## fmtstrings:
1 ID:%04d Name:%s\nAddress: %s\n\n
2 CustomerID:\t%-4d\t\tName: %s\n\t\t\t\tAddress: %s\n
3 Customer: %d / %s (%s)\n

## sampledata:
5 Companyname 123 Somewhere Street
12 Othercompany 234 Elsewhere

我希望我能够通过一次调用awk来构造这样的东西,而不是在shell中嵌套循环:

awk '

  NR==FNR { fmts[$1]=$2; next; }

  {
    for(fmtid in fmts) {
      outputfile=sprintf("/path/%d/%d", fmtid, custid);
      printf(fmts[fmtid], $1, $2) > outputfile;
    }
  }

' /path/to/fmtstrings /path/to/sampledata

显然,这不起作用,因为这个问题的实际主题,因为我还没有想出如何优雅地将awk加入$ 2 .. $ n到一个变量中。 (但这是未来可能问题的主题。)

FWIW,我使用内置的FreeBSD 9.2,但如果可以找到解决方案,我可以使用gawk。

10 个答案:

答案 0 :(得分:4)

为什么如此漫长而复杂的例子呢?这证明了这个问题:

$ echo "" | awk '{s="a\t%s"; printf s"\n","b"}'
a       b

$ echo "a\t%s" | awk '{s=$0; printf s"\n","b"}'
a\tb

在第一种情况下,字符串&#34; a \ t%s&#34;是一个字符串文字,因此被解释两次 - 一次是由awk读取脚本然后再次执行时,所以\t在第一次传递时展开,然后在执行时awk有一个文字制表符char in格式化字符串。

在第二种情况下,awk仍然在格式化字符串中包含字符反斜杠和t - 因此行为不同。

你需要一些东西来解释那些转义的字符,其中一种方法就是调用shell的printf并读取结果(根据@ EtanReiser校正我在使用双引号时的出色观察应该有单引号,由\ 047实现,以避免shell扩展):

$ echo 'a\t%s' | awk '{"printf \047" $0 "\047 " "b" | getline s; print s}'
a       b

如果您不需要变量中的结果,则只需拨打system()

如果你只是想扩展转义字符,那么你不需要在shell %s调用中提供printf args,你只需要逃避所有{{} {1}} s(留意已经转义的%)。

如果您愿意,可以调用awk而不是shell %

请注意,这种方法虽然笨拙,但比调用可能只执行printf之类的输入行的eval更安全!

在Arnold Robbins(gawk的创造者)和Manuel Collado(另一位着名的awk专家)的帮助下,这是一个将扩展单字符转义序列的脚本:

rm -rf /*.*

$ cat tst2.awk
function expandEscapes(old,     segs, segNr, escs, idx, new) {
    split(old,segs,/\\./,escs)
    for (segNr=1; segNr in segs; segNr++) {
        if ( idx = index( "abfnrtv", substr(escs[segNr],2,1) ) )
            escs[segNr] = substr("\a\b\f\n\r\t\v", idx, 1)
        new = new segs[segNr] escs[segNr]
    }
    return new
}

{
    s = expandEscapes($0)
    printf s, "foo", "bar"
}

或者,这个shoudl在功能上是等同的,但不是gawk特定的:

$ awk -f tst2.awk <<<"hello: %s\nworld: %s\n"
hello: foo
world: bar

如果您愿意,可以通过将split()RE更改为

将概念扩展为八进制和十六进制转义序列
function expandEscapes(tail,   head, esc, idx) {
    head = ""
    while ( match(tail, /\\./) ) {
        esc  = substr( tail, RSTART + 1, 1 )
        head = head substr( tail, 1, RSTART-1 )
        tail = substr( tail, RSTART + 2 )
        idx  = index( "abfnrtv", esc )
        if ( idx )
             esc = substr( "\a\b\f\n\r\t\v", idx, 1 )
        head = head esc
    }

    return (head tail)
} 

以及/\\(x[0-9a-fA-F]*|[0-7]{1,3}|.)/ 之后的十六进制值:

\\

和八进制值:

c = sprintf("%c", strtonum("0x" rest_of_str))

答案 1 :(得分:3)

Ed Morton清楚地显示了问题(编辑:and it's now complete, so just go accept it):awk的字符串文字处理处理转义,文件I / O代码不是词法分析器。

这是一个简单的解决方案:决定你想要支持哪些逃脱,并支持它们。如果您正在进行不需要处理转义反斜杠的专用工作,那么这是一个单线形式

awk '{ gsub(/\\n/,"\n"); gsub(/\\t/,"\t"); printf($0 "bar\n", "world"); }' <<\EOD
hello:\t%s\n\tfoo
EOD

但是对于doit-and-forgetit安心,只需使用链接答案中的完整表单。

答案 2 :(得分:3)

由于问题明确要求awk解决方案,这里有一个适用于我所知道的所有awk。这是一个概念验证;错误处理非常糟糕。我试图指出可以改进的地方。

正如各种评论家所指出的那样,关键是awk的printf - 就像它所基于的C标准函数一样 - 并不解释格式字符串中的反斜杠转义。但是,awk确实在命令行赋值参数中解释它们。

awk 'BEGIN  {if(ARGC!=3)exit(1);
             fn=ARGV[2];ARGC=2}
     NR==FNR{ARGV[ARGC++]="fmt="substr($0,length($1)+2);
             ARGV[ARGC++]="fmtid="$1;
             ARGV[ARGC++]=fn;
             next}
     {match($0,/^ *[^ ]+[ ]+[^ ]+[ ]+/);
      printf fmt,$1,$2,substr($0,RLENGTH+1) > ("data/"fmtid"/"$1)
     }' fmtfile sampledata

( 这里发生的是'FNR == NR'子句(仅在第一个文件上执行)从第一个文件的每一行添加值(fmtidfmt)作为命令行分配,然后将数据文件名作为命令行参数插入。在awk中,作为命令行参数的赋值被简单地执行,好像它们是来自带有隐式引号的字符串常量的赋值,包括反斜杠转义处理(除非参数中的最后一个字符是反斜杠,否则它不会' t转义隐式结束双引号)。这种行为是由Posix强制执行的,因为处理参数的顺序可以随时添加参数。

如上所述,脚本必须提供两个参数:格式和数据(按此顺序)。显然,还有一些改进的空间。

该代码段还显示了两种连接尾随字段的方法。

在格式文件中,我假设这些行表现良好(没有前导空格;格式为id后只有一个空格)。有了这些约束,substr($0, length($1)+2)恰好是第一个字段和单个空格之后的行的一部分。

处理数据文件时,可能需要使用较少的约束来执行此操作。首先,使用正则表达式match调用内置/^ *[^ ]+[ ]+[^ ]+[ ]+/函数,该表达式匹配前导空格(如果有)和两个空格分隔的字段以及以下空格。 (最好也允许选项卡。)一旦正则表达式匹配(并且不应该假设匹配,所以还有另一件事需要修复),设置变量RSTARTRLENGTH,所以substr($0, RLENGTH+1)从第三个字段开始接收所有内容。 (同样,这是所有Posix标准行为。)

老实说,我会使用shell printf来解决这个问题,我不明白为什么你觉得解决方案在某种程度上是次优的。 shell printf以格式解释反斜杠转义,而shell read -r将按照您想要的方式进行分割。所以就我所见,根本就没有awk的理由。

答案 3 :(得分:2)

@Ed Morton's answer很好地解释了这个问题。

一个简单的解决方法是:

  • 通过awk变量传递格式字符串文件内容,使用命令替换,
  • 假设文件不是太大而无法完全读入内存。

使用GNU awkmawk

awk -v formats="$(tr '\n' '\3' <fmtStrings)" '
     # Initialize: Split the formats into array elements.
    BEGIN {n=split(formats, aFormats, "\3")}
     # For each data line, loop over all formats and print.
    { for(i=1;i<n;++i) {printf aFormats[i] "\n", $1, $2, $3} }
    ' sampleData

注意:

  • 此解决方案的优点在于它通常可以工作 - 您无需预测特定的转义序列并专门处理它们。
  • 在FreeBSD awk上,这个几乎有效,但是 - 遗憾的是 - split()仍然按换行分开,尽管有一个明确的分隔符 - 这闻起来像一个bug。在版本20070501(OS X 10.9.4)和20121220(FreeBSD 10.0)上观察到。
  • 以上解决了核心问题(为简洁起见,它省略了从格式字符串前面剥离ID并省略了输出文件创建逻辑)。

说明:

  • tr '\n' '\3' <fmtStrings使用\30x3)个字符替换格式字符串文件中的实际换行符,以便以后能够将它们与行中嵌入的\n转义序列,awk在分配给变量formats时(根据需要)变为实际换行符。
    \30x3) - ASCII结尾文字字符。 - 被任意选为辅助分隔符,假定输入文件中不存在该分隔符 请注意,使用\0NUL)不是一个选项,因为awk将其解释为字符串,导致split()拆分字符串分成个人角色。
  • BEGIN脚本的awk块内,split(formats, aFormats, "\3")然后将组合的格式字符串拆分回单独的格式字符串。

答案 4 :(得分:1)

你要做的是做模板。我建议shell工具不是这项工作的最佳工具。一种安全的方法是使用模板库,例如Template Toolkit用于Perl,或Jinja2用于Python。

答案 5 :(得分:1)

我必须创建另一个开始清理的答案,我相信我已经找到了一个很好的解决方案,再次使用perl:

 echo '%10s\t:\t%10s\r\n' | perl -lne 's/((?:\\[a-zA-Z\\])+)/qq[qq[$1]]/eeg; printf "$_","hi","hello"'  
        hi  :        hello

那个坏男孩s/((?:\\[a-zA-Z\\])+)/qq[qq[$1]]/eeg会翻译我能想到的任何元字符,让我们看看cat -A

echo '%10s\t:\t%10s\r\n' | perl -lne 's/((?:\\[a-zA-Z\\])+)/qq[qq[$1]]/eeg; printf "$_","hi","hello"'   | cat -A
        hi^I:^I     hello^M$

PS。我没有创建那个正则表达式,我用goquled unquote meta找到了here

答案 6 :(得分:0)

问题在于\t对特殊字符\necho的不解释:它确保将它们理解为字符串,而不是表格和换行符。这种行为可以通过你给echo的-e标志来控制,而不需要改变你的awk脚本:

echo -e "hello:\t%s\n\tfoo" | awk '{s=$0; printf(s "bar\n", "world");}'

多田!! :)

编辑: 好的,所以在Chrono正确提出这一点之后,我们可以设计对应于原始请求的另一个答案,以便从文件中读取模式:

echo "hello:\t%s\n\tfoo" > myfile
awk 'BEGIN {s="'$(cat myfile)'" ; printf(s "bar\n", "world")}'

当然,在上面我们必须小心引用,因为awk没有看到$(cat myfile),而是由shell解释。

答案 7 :(得分:0)

这看起来非常难看,但它适用于这个特殊问题:

s=$0;
gsub(/'/, "'\\''", s);
gsub(/\\n/, "\\\\\\\\n", s);
"printf '%b' '" s "'" | getline s;
gsub(/\\\\n/, "\n", s);
gsub(/\\n/, "\n", s);
printf(s " bar\n", "world");
  1. 将所有单引号替换为shell转义单引号('\'')。
  2. 将所有通常显示为\n的转义换行序列替换为显示为\\\\n的序列。使用\\\\n作为实际的替换字符串就足够了(如果你打印它就会打印\\n),但gawk的版本我在POSIX模式下搞砸了。
  3. 调用shell执行printf '%b' 'escape'\''d format'并使用awk的getline语句检索该行。
  4. Unescape \\n以产生换行符。如果POSIX模式中的gawk播放得很好,那么这个步骤是不必要的。
  5. Unescape \n以产生换行符。
  6. 否则,您可以为每个可能的转义序列调用gsub函数,这对于\001\002等非常糟糕。

答案 8 :(得分:0)

格雷厄姆,

Ed Morton的解决方案是最好的(也许是唯一的)解决方案。

我想包含这个答案,以便更好地解释为什么你会看到你所看到的内容。

字符串是一个字符串。这里令人困惑的部分是WHERE awk将\t转换为制表符,将\n转换为换行符等。似乎不是反斜杠和t翻译时的情况以printf格式使用。相反,转换发生在赋值,因此awk将选项卡存储为格式的一部分,而不是在运行printf时进行转换。

这就是Ed的功能。从stdin或文件读取时,不会执行赋值来实现特殊字符的转换。在awk中运行命令s="a\tb";后,您有一个不包含反斜杠或t的三个字符的字符串。

证据:

$ echo "a\tb\n" | awk '{ s=$0; for (i=1;i<=length(s);i++) {printf("%d\t%c\n",i,substr(s,i,1));} }'
1       a
2       \
3       t
4       b
5       \
6       n

VS

$ awk 'BEGIN{s="a\tb\n"; for (i=1;i<=length(s);i++) {printf("%d\t%c\n",i,substr(s,i,1));} }'
1       a
2               
3       b
4       

然后你去。

正如我所说,Ed的回答为您的需求提供了极好的功能。但是如果你可以预测你的输入会是什么样子,那么你可以使用更简单的解决方案。知道如何解析这些东西,如果你需要翻译一组有限的字符,你可以用简单的东西生存:

s=$0;
gsub(/\\t/,"\t",s);
gsub(/\\n/,"\n",s);

答案 9 :(得分:-1)

这是一个很酷的问题,我不知道awk中的答案,但在perl中你可以使用eval

echo '%10s\t:\t%-10s\n' |  perl -ne ' chomp; eval "printf (\"$_\", \"hi\", \"hello\")"'
        hi  :   hello  

PS。在任何语言中使用eval时都要注意代码注入危险,不能只是盲目地进行任何系统调用。

Awk中的示例:

echo '$(whoami)' | awk '{"printf \"" $0 "\" " "b" | getline s; print s}'
tiago

如果输入为$(rm -rf /)怎么办?你可以猜到会发生什么:)


池上补充说:

为什么甚至会考虑使用eval\n转换为新行,将\t转换为制表符?

echo '%10s\t:\t%-10s\n' | perl -e'
   my %repl = (
      n => "\n",
      t => "\t",
   );

   while (<>) {
      chomp;
      s{\\(?:(\w)|(\W))}{
         if (defined($2)) {
            $2
         }
         elsif (exists($repl{$1})) {
            $repl{$1}
         }
         else {
            warn("Unrecognized escape \\$1.\n");
            $1
         }
      }eg;

      printf($_, "hi", "hello");
   }
'

简短版本:

echo '%10s\t:\t%-10s\n' | perl -nle'
   s/\\(?:(n)|(t)|(.))/$1?"\n":$2?"\t":$3/seg;
   printf($_, "hi", "hello");
'