我有一个案例,我希望在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。
答案 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'子句(仅在第一个文件上执行)从第一个文件的每一行添加值(fmtid
,fmt
)作为命令行分配,然后将数据文件名作为命令行参数插入。在awk
中,作为命令行参数的赋值被简单地执行,好像它们是来自带有隐式引号的字符串常量的赋值,包括反斜杠转义处理(除非参数中的最后一个字符是反斜杠,否则它不会' t转义隐式结束双引号)。这种行为是由Posix强制执行的,因为处理参数的顺序可以随时添加参数。
如上所述,脚本必须提供两个参数:格式和数据(按此顺序)。显然,还有一些改进的空间。
该代码段还显示了两种连接尾随字段的方法。
在格式文件中,我假设这些行表现良好(没有前导空格;格式为id后只有一个空格)。有了这些约束,substr($0, length($1)+2)
恰好是第一个字段和单个空格之后的行的一部分。
处理数据文件时,可能需要使用较少的约束来执行此操作。首先,使用正则表达式match
调用内置/^ *[^ ]+[ ]+[^ ]+[ ]+/
函数,该表达式匹配前导空格(如果有)和两个空格分隔的字段以及以下空格。 (最好也允许选项卡。)一旦正则表达式匹配(并且不应该假设匹配,所以还有另一件事需要修复),设置变量RSTART
和RLENGTH
,所以substr($0, RLENGTH+1)
从第三个字段开始接收所有内容。 (同样,这是所有Posix标准行为。)
老实说,我会使用shell printf
来解决这个问题,我不明白为什么你觉得解决方案在某种程度上是次优的。 shell printf
以格式解释反斜杠转义,而shell read -r
将按照您想要的方式进行分割。所以就我所见,根本就没有awk的理由。
答案 3 :(得分:2)
@Ed Morton's answer很好地解释了这个问题。
一个简单的解决方法是:
awk
变量传递格式字符串文件内容,使用命令替换,使用GNU awk
或mawk
:
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
注意:
awk
上,这个几乎有效,但是 - 遗憾的是 - split()
仍然按换行分开,尽管有一个明确的分隔符 - 这闻起来像一个bug。在版本20070501
(OS X 10.9.4)和20121220
(FreeBSD 10.0)上观察到。说明:
tr '\n' '\3' <fmtStrings
使用\3
(0x3
)个字符替换格式字符串文件中的实际换行符,以便以后能够将它们与行中嵌入的\n
转义序列,awk
在分配给变量formats
时(根据需要)变为实际换行符。\3
(0x3
) - ASCII结尾文字字符。 - 被任意选为辅助分隔符,假定输入文件中不存在该分隔符
请注意,使用\0
(NUL
)不是一个选项,因为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
对特殊字符\n
和echo
的不解释:它确保将它们理解为字符串,而不是表格和换行符。这种行为可以通过你给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");
'\''
)。\n
的转义换行序列替换为显示为\\\\n
的序列。使用\\\\n
作为实际的替换字符串就足够了(如果你打印它就会打印\\n
),但gawk的版本我在POSIX模式下搞砸了。printf '%b' 'escape'\''d format'
并使用awk的getline语句检索该行。\\n
以产生换行符。如果POSIX模式中的gawk播放得很好,那么这个步骤是不必要的。\n
以产生换行符。否则,您可以为每个可能的转义序列调用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");
'