【问题标题】:How to preserve the original whitespace between fields in awk?如何保留awk中字段之间的原始空白?
【发布时间】:2018-04-22 05:21:20
【问题描述】:

使用awk 处理输入时,有时我想编辑其中一个字段,而不涉及其他任何内容。考虑一下:

$ ls -l | awk 1
total 88
-rw-r--r-- 1 jack jack     8 Jun 19  2013 qunit-1.11.0.css
-rw-r--r-- 1 jack jack 56908 Jun 19  2013 qunit-1.11.0.js
-rw-r--r-- 1 jack jack  4306 Dec 29 09:16 test1.html
-rw-r--r-- 1 jack jack  5476 Dec  7 08:09 test1.js

如果我不编辑任何字段($1$2、...),所有内容都会保持原样。但如果假设我只想保留第一个字段的前 3 个字符:

$ ls -l | awk '{$1 = substr($1, 1, 3) } 1'
tot 88
-rw 1 jack jack 8 Jun 19 2013 qunit-1.11.0.css
-rw 1 jack jack 56908 Jun 19 2013 qunit-1.11.0.js
-rw 1 jack jack 4306 Dec 29 09:16 test1.html
-rw 1 jack jack 5476 Dec 7 08:09 test1.js

所有字段之间的原始空白被替换为一个简单的空格。

有没有办法保留字段之间的原始空白?

更新

在此示例中,编辑前 4 个字段相对容易。但是,如果我只想保留 $5 的第一个字母以获得此输出:

-rw-r--r-- 1 jack jack     8 J 19  2013 qunit-1.11.0.css
-rw-r--r-- 1 jack jack 56908 J 19  2013 qunit-1.11.0.js
-rw-r--r-- 1 jack jack  4306 D 29 09:16 test1.html
-rw-r--r-- 1 jack jack  5476 D  7 08:09 test1.js

【问题讨论】:

    标签: awk


    【解决方案1】:

    如果你想保留空格,你也可以试试split 函数。 在 Gnu Awk 版本 4 中,split 函数接受 4 个参数,其中后者是字段之间的分隔符。例如,

    echo "a  2   4  6" | gawk ' {
     n=split($0,a," ",b)
     a[3]=7
     line=b[0]
     for (i=1;i<=n; i++)
         line=(line a[i] b[i])
     print line
    }' 
    

    给出输出

    a  2   7  6
    

    【讨论】:

    • 这是 THE 正确答案,也是引入 split() 的第四个参数的主要原因。在 FS 可以是任何正则表达式的一般情况下,其他任何东西都会变得非常复杂,而不仅仅是默认空格或任何其他你可以简单地在括号表达式中取反的东西。
    【解决方案2】:

    我知道这是一个老问题,但我认为必须有更好的东西。这个答案适用于那些在搜索时偶然发现这个问题的人。在网上浏览时,我不得不说@Håkon Hægland 有最好的答案,这就是我最初使用的。

    但这是我的解决方案。使用FPAT。它可以设置一个正则表达式来说明一个字段应该是什么。

     FPAT = "([[:space:]]*[[:alnum:][:punct:][:digit:]]+)";
    在这种情况下,我是说该字段应该以零个或多个空白字符开头,并且基本上以除空白字符之外的任何其他字符结尾。如果您在理解 POSIX 括号表达式时遇到困难,Here 是一个链接。

    另外,将输出字段更改为OFS = ""; 分隔符,因为一旦对行进行了操作,如果您不更改默认的 OFS,输出将添加一个额外的空格作为分隔符。

    我用同样的例子来测试。

    $ cat example-output.txt
    -rw-r--r-- 1 jack jack     8 Jun 19  2013 qunit-1.11.0.css
    -rw-r--r-- 1 jack jack 56908 Jun 19  2013 qunit-1.11.0.js
    -rw-r--r-- 1 jack jack  4306 Dec 29 09:16 test1.html
    -rw-r--r-- 1 jack jack  5476 Dec  7 08:09 test1.js
    $ awk 'BEGIN { FPAT = "([[:space:]]*[[:alnum:][:punct:][:digit:]]+)"; OFS = ""; } { $6 = substr( $6, 1, 2);  print $0; }' example-output.txt
    -rw-r--r-- 1 jack jack     8 J 19  2013 qunit-1.11.0.css
    -rw-r--r-- 1 jack jack 56908 J 19  2013 qunit-1.11.0.js
    -rw-r--r-- 1 jack jack  4306 D 29 09:16 test1.html
    -rw-r--r-- 1 jack jack  5476 D  7 08:09 test1.js
    

    请记住。这些字段现在有前导空格。因此,如果该字段需要替换为其他内容,您可以这样做

    len = length($1); 
    $1 = sprintf("%"(len)"s", "-42-");
    $ awk 'BEGIN { FPAT = "([[:space:]]*[[:alnum:][:punct:][:digit:]]+)"; OFS = ""; } { if(NR==1){ len = length($1); $1 = sprintf("%"(len)"s", "-42-"); } print $0; }' example-output.txt
          -42- 1 jack jack     8 Jun 19  2013 qunit-1.11.0.css
    -rw-r--r-- 1 jack jack 56908 Jun 19  2013 qunit-1.11.0.js
    -rw-r--r-- 1 jack jack  4306 Dec 29 09:16 test1.html
    -rw-r--r-- 1 jack jack  5476 Dec  7 08:09 test1.js
    

    【讨论】:

    • 您可以将[[:alnum:][:punct:][:digit:]] 替换为[^[:space:]],除了更简洁之外,该解决方案将更加强大。我知道-42- 的内容是什么,但如果你只是想在字段宽度中显示一些东西,它会写成$1 = sprintf("%*s", len, "-42-"),而不是$1 = sprintf("%"(len)"s", "-42-")。显然,当使用默认 FS 以外的其他解决方案时,整个解决方案就会崩溃,因此首选 @Hakon's solution
    【解决方案3】:

    最简单的解决方案是确保在每个空间上都进行字段拆分。这是通过使字段分隔符[ ]:

    $ awk -F '[ ]' '{$1=substr($1,1,3)}1' infile
    
    -rw 1 jack jack     8 Jun 19  2013 qunit-1.11.0.css
    -rw 1 jack jack 56908 Jun 19  2013 qunit-1.11.0.js
    -rw 1 jack jack  4306 Dec 29 09:16 test1.html
    -rw 1 jack jack  5476 Dec  7 08:09 test1.js
    

    默认情况下,awk 会在任何重复空格(制表符和空格,类似于[ \t]+)上进行拆分。手册指出:

    在 FS 是单个空格的特殊情况下,字段由空格和/或制表符和/或换行符分隔。

    这会将空格、制表符和换行符的运行折叠为输出中的一个 OFS 值。如果 OFS 也是一个空格(也是默认值),结果是每次运行的空白只打印一个空格。

    但是可以告诉 awk 使用仅匹配 一个 字符的正则表达式选择一个空格作为字段分隔符:[ ]

    请注意,这将更改字段的字段编号。每个空间都会开始一个新的领域。因此,请注意您提供的数据中的以下结果:

    $ awk -F '[ ]' '{print($4,$5,$6)}' infile
    jack
    jack 56908 Jun
    jack  4306
    jack  5476
    

    在这种特定情况下,第一个字段之前没有空格,之后只有一个空格,这就是它正常工作的原因。

    【讨论】:

      【解决方案4】:

      可以通过编辑$0 而不是单个字段($1$2、...)来保留原始空格,例如:

      $ ls -l | awk '{$0 = substr($1, 1, 3) substr($0, length($1) + 1)} 1'
      tot 88
      -rw 1 jack jack     8 Jun 19  2013 qunit-1.11.0.css
      -rw 1 jack jack 56908 Jun 19  2013 qunit-1.11.0.js
      -rw 1 jack jack  4306 Dec 29 09:16 test1.html
      -rw 1 jack jack  5476 Dec  7 08:09 test1.js
      

      这在编辑第一列时相对容易做到,但在编辑其他列时会很麻烦($2,...,$4),并且在中间的空白宽度不是的字段之后分解已修复(在此示例中为$5 及以上)。

      更新

      根据@Håkon Hægland的回答,这里有一种方法可以保留第6个字段(月份)的前2个字符:

      {
          n = split($0, f, " ", sep)
          f[6] = substr(f[6], 1, 2)
          line = sep[0]
          for (i = 1; i <= n; ++i) line = line f[i] sep[i]
          print line
      }
      

      【讨论】:

      • 对于 GNU awk,我建议使用 if (match($0, "^([^ \t]+)[ \t]+([^ \t]+)[ \t]+([^ \t]+)", fields)) { … } 之类的东西来找出字段的位置。然后可以使用fields[2, "start"]fields[2, "start"] + fields[2, "length"] - 1,例如获取第二个字段的开始和结束的索引。
      猜你喜欢
      • 1970-01-01
      • 2010-10-06
      • 1970-01-01
      • 2017-12-02
      • 1970-01-01
      • 2012-08-17
      • 1970-01-01
      • 1970-01-01
      • 2020-06-30
      相关资源
      最近更新 更多