【问题标题】:How to detect the last line in awk before END如何在 END 之前检测 awk 中的最后一行
【发布时间】:2012-08-27 12:24:33
【问题描述】:

我正在尝试将最后一行添加到我正在创建的文件中。如何在 END 之前检测 awk 中文件的最后一行?我需要这样做,因为变量在 END 块中不起作用, 所以我尽量避免使用END

awk ' { do some things..; add a new last line into file;}'

END 之前,我不想要这个:

awk 'END{print "something new" >> "newfile.txt"}'

【问题讨论】:

  • 您特别需要哪些变量在 END 块中不可用?大多数变量(NRNFFNR 等)在 END 块中都有非常合理的值。
  • 我需要的变量是一些在创建文件中起主要作用的局部变量。例如:print $0 >> sprintf("%s/%s_%s.txt", user, mode, FILENAME) 那些usermodeEND 中不可用,它们正在成为文件名...
  • 这个问题毫无意义。 awk 没有局部变量,除了函数参数。如果您计算文件名并将其放入fname,则fname 将保留其值,直到 Awk 终止或将新值分配给 fname
  • 当您处理输入的任何记录时,假设它可能是最后一个,并计算在这种情况下所需的文件名,把它到一个名为fname 的变量中。如果该记录是最后一条,则将执行 END 块,然后 fname 将可用。

标签: shell awk line


【解决方案1】:

一种选择是使用getline 函数来处理文件。成功返回1,文件结束返回0,出错返回-1

awk '
    FNR == 1 {

        ## Process first line.
        print FNR ": " $0;

        while ( getline == 1 ) {
            ## Process from second to last line.
            print FNR ": " $0;
        }

        ## Here all lines have been processed.
        print "After last line";
    }
' infile

假设infile 有这个数据:

one
two
three
four
five

输出将是:

1: one                                                                                                                                                                                                                                       
2: two                                                                                                                                                                                                                                       
3: three
4: four
5: five
After last line

【讨论】:

  • 太棒了,这就是我想要的,我想。让我试一试
  • @doniyor:那么不要接受答案,因为它可能会阻止其他用户发布不同的解决方案。当你确定的时候去做。时间不是问题,不是吗?
  • 干得好;除非需要特别对待第一行,否则您可以重写循环以使用do { ... } while (getline == 1),在这种情况下,您不需要在循环之前使用print 语句。
  • 这已被接受,但我真的不明白这一点,因为它可以简单地通过: cat ; echo "line" >> .. 和使用 awk 添加东西可以简单地用你“不想要的”来完成。使用 getline 听起来您想使用其他东西,因为在该过程完成之前您无法通过管道传输它。
  • Fo pipe 我的意思是你的 awk 可能会全部加载到 RAM 中......我有同样的问题,但我认为我需要使用不同的方法。我不能对这个解决方案投反对票,但请投反对票!
【解决方案2】:

打印上一行。 当前行为 2 时,打印第 1 行, 当前行为 3 时,打印第 2 行。 …… 到最后

awk '{
    if (NR>1) {
        # process str
        print str;
    }
    str=$0;
}
END {
    # process whatever needed before printing the last line and then print the last line.
    print str;
}'

【讨论】:

  • 通常避免只使用代码的答案。考虑添加有助于解释代码的description。谢谢
【解决方案3】:

你可以使用ENDFILE,它在END之前执行:

$ awk 'END {print "end"} ENDFILE{print "last line"}'  /dev/null /dev/null
last line
last line
end

ENDFILE 存在于最新版本的 GNU awk(我认为是 >4.0)中。

【讨论】:

    【解决方案4】:
    $ cat file 
    1
    2
    3
    4
    5
    

    通过两次读取相同的文件(推荐)

    $ awk 'FNR==NR{last++;next}{print $0, ((last==FNR)?"I am Last":"")}' file file
    1
    2
    3
    4
    5 I am Last
    

    使用getline

    $ awk 'BEGIN{while((getline t < ARGV[1]) > 0)last++;close(ARGV[1])}{print $0, ((last==FNR)?"I am Last":"")}' file
    1
    2
    3
    4
    5 I am Last
    

    【讨论】:

    • 这很奇怪。如果我使用您发布的确切输入文件,它在 Cygwin bash 命令行上对我有用。但如果我使用不同的输入文件,它不会。我的文件由“x line x”行组成,其中 x 从 1 到 8。
    • @ysap 感谢您的报告,我注意到它甚至没有行,现在修改
    • 谢谢。在您发布时尝试它,它不会打印最后一行的内容(8 line 8)。只是消息。
    • 我认为这里可能存在误解——我所说的“不工作”是输入文件的最后一行没有在“我是最后一个”消息之前打印出来。否则,您的脚本会正确检测到最后一行,并打印消息。
    • 哦,好吧,你可以使用RS变量或"\n"看起来像这样print $0 RS (() ? : )
    【解决方案5】:

    您可以在开始块中使用"wc -l" | getline filesize 获取文件中的行数,并使用NR == filesize 测试脚本正文中的最后一行。

    【讨论】:

    • 谢谢,但我的情况是:我有一个巨大的文件,我应该将它分类为多个小文件。我目前正在创建的那些多个小文件应该是 rtf 格式。所以,我事先不知道新文件的行数.. :(
    • 为什么不把它们在awk之后转成rtf?
    • 因为我创建和分发的文件位于不同的目录中。在 awk 之后我必须寻找创建的文件,这是更多的工作。
    • 警告:正如所写,基于wc 的管道命令将仅适用于stdin 输入,因为缺少显式输入——这是一个catch-22:该命令将耗尽 stdin 输入,导致 awk 程序中的后续块没有输入。要使用这种方法,(a) 确保指定了实际文件名,并且 (b) 明确引用它:"wc -l &lt; \""ARGV[1]"\"" | getline lineCount; lineCount+=0lineCount+=0 部分适用于诸如 OSX 之类的平台,其中wc -l 输出前导空格;它确保awk 将lineCount 视为一个数字。)
    • @mklement0 - 感谢更新版本。该解决方案使我能够有效地使用 rewind() 函数,但重复次数未指定(即取决于数据文件)。所以,我没有笨拙地使用getline,而是按照这里的建议测试EOF。
    【解决方案6】:

    我知道答案已被接受,但这完全是错误的。

    因为您确实想将 awk 用作解析器而不是代码。

    Awk 应该在一些 unix 管道中使用,而不应该在任何逻辑中使用。

    我遇到了同样的问题,我在 awk 中解决了这个问题:

    nlines=wc -l &lt;file&gt;

    猫 | awk -v nl=${nlines} '{if (nl != NR) {print $0,",","\";} else {print;}}' >> ${someout}

    这里有一点很重要:管道、刷新和 RAM。

    如果你让 awk 吐出它的输出,你可以通过管道将它传送到下一个处理器。

    如果您使用 getline,尤其是在循环中,您可能看不到结尾。

    getline 应该只用于一行并且最终依赖于下一行。

    我喜欢 awk,但我们不能用它做所有事情!

    已编辑:

    对于谁否决了答案,我只想介绍这个脚本:

    #! /bin/sh
    #
    # Generate random strings
    cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 100000 > x.r.100000
    cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1000000 > x.r.1000000
    cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 5000000 > x.r.5000000
    #
    # To save you time in case
    #cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 10000000 > x.r.10000000
    #
    # Generate awk files
    cat <<"EOF" > awkGetline.sh
    #! /bin/sh
    #
    awk '
        FNR == 1 {
    
            ## Process first line.
            print FNR ": " $0;
    
            while ( getline == 1 ) {
                ## Process from second to last line.
                print FNR ": " $0;
            }
        }
    ' x.r
    #
    EOF
    #
    chmod +x awkGetline.sh
    #
    cat <<"EOF" > awkPlain.sh
    #! /bin/sh
    #
    awk '
        {print FNR ": " $0;}
    ' x.r
    #
    EOF
    #
    # x.r.100000
    #
    chmod +x awkPlain.sh
    #
    # Execute awkGetline.sh 10 times on x.r.100000
    rm -f x.t
    cp x.r.100000 x.r
    for runInstance in 1 2 3 4 5 6 7 8 9 10;
      do
      /usr/bin/time -p -a -o x.t ./awkGetline.sh > x.1.out;
    done;
    #
    cat x.t | grep real | awk 'BEGIN {sum=0.0} {sum=sum+$2; print $2, sum/10;} END {print "SUM Getln", sum;}' | grep SUM
    #
    
    #
    # Execute awkPlain.sh 10 times on x.r.100000
    rm -f x.t
    cp x.r.100000 x.r
    for runInstance in 1 2 3 4 5 6 7 8 9 10;
      do
      /usr/bin/time -p -a -o x.t ./awkPlain.sh > x.1.out;
    done;
    #
    cat x.t | grep real | awk 'BEGIN {sum=0.0} {sum=sum+$2; print $2, sum/10;} END {print "SUM Plain", sum;}' | grep SUM
    #
    
    #
    # x.r.1000000
    #
    chmod +x awkPlain.sh
    #
    # Execute awkGetline.sh 10 times on x.r.1000000
    rm -f x.t
    cp x.r.1000000 x.r
    for runInstance in 1 2 3 4 5 6 7 8 9 10;
      do
      /usr/bin/time -p -a -o x.t ./awkGetline.sh > x.1.out;
    done;
    #
    cat x.t | grep real | awk 'BEGIN {sum=0.0} {sum=sum+$2; print $2, sum/10;} END {print "SUM Getln", sum;}' | grep SUM
    #
    
    #
    # Execute awkPlain.sh 10 times on x.r.1000000
    rm -f x.t
    cp x.r.1000000 x.r
    for runInstance in 1 2 3 4 5 6 7 8 9 10;
      do
      /usr/bin/time -p -a -o x.t ./awkPlain.sh > x.1.out;
    done;
    #
    cat x.t | grep real | awk 'BEGIN {sum=0.0} {sum=sum+$2; print $2, sum/10;} END {print "SUM Plain", sum;}' | grep SUM
    #
    
    
    #
    # x.r.5000000
    #
    chmod +x awkPlain.sh
    #
    # Execute awkGetline.sh 10 times on x.r.5000000
    rm -f x.t
    cp x.r.5000000 x.r
    for runInstance in 1 2 3 4 5 6 7 8 9 10;
      do
      /usr/bin/time -p -a -o x.t ./awkGetline.sh > x.1.out;
    done;
    #
    cat x.t | grep real | awk 'BEGIN {sum=0.0} {sum=sum+$2; print $2, sum/10;} END {print "SUM Getln", sum;}' | grep SUM
    #
    
    #
    # Execute awkPlain.sh 10 times on x.r.5000000
    rm -f x.t
    cp x.r.5000000 x.r
    for runInstance in 1 2 3 4 5 6 7 8 9 10;
      do
      /usr/bin/time -p -a -o x.t ./awkPlain.sh > x.1.out;
    done;
    #
    cat x.t | grep real | awk 'BEGIN {sum=0.0} {sum=sum+$2; print $2, sum/10;} END {print "SUM Plain", sum;}' | grep SUM
    #
    
    exit;
    # To save you time in case
    
    #
    # x.r.10000000
    #
    chmod +x awkPlain.sh
    #
    # Execute awkGetline.sh 10 times on x.r.10000000
    rm -f x.t
    cp x.r.10000000 x.r
    for runInstance in 1 2 3 4 5 6 7 8 9 10;
      do
      /usr/bin/time -p -a -o x.t ./awkGetline.sh > x.1.out;
    done;
    #
    cat x.t | grep real | awk 'BEGIN {sum=0.0} {sum=sum+$2; print $2, sum/10;} END {print "SUM Getln", sum;}' | grep SUM
    #
    
    #
    # Execute awkPlain.sh 10 times on x.r.10000000
    rm -f x.t
    cp x.r.10000000 x.r
    for runInstance in 1 2 3 4 5 6 7 8 9 10;
      do
      /usr/bin/time -p -a -o x.t ./awkPlain.sh > x.1.out;
    done;
    #
    cat x.t | grep real | awk 'BEGIN {sum=0.0} {sum=sum+$2; print $2, sum/10;} END {print "SUM Plain", sum;}' | grep SUM
    #
    

    当然还有第一个结果:

    tmp]$ ./awkRun.sh 
    SUM Getln 0.78
    SUM Plain 0.71
    SUM Getln 7.2
    SUM Plain 6.49
    SUM Getln 35.91
    SUM Plain 32.92
    

    仅仅因为 getline,您可以节省大约 10% 的时间。

    在更复杂的逻辑中考虑这一点,您可能会得到最糟糕的情况。在这个简单的版本中,不考虑内存考虑。 似乎他们在这个简单的版本中没有发挥作用。但是如果你进入更复杂的逻辑,记忆也可能会发挥作用......

    当然可以在你的机器上试试。

    这就是我建议考虑其他选择的原因。

    【讨论】:

    • 注意:使用管道我可以处理大数据,确实需要一点时间,但我可以获得结果。也使用 awk。在商品机器中。请不要让 awk 成为怪物,不要使用 getline。
    • 我同意使用wc 而不是getline,但是,我有两个小问题。首先,使用nlines=$(wc -l &lt; filename)(注意stdin 的重定向)使nlines 看起来不像:80 filename。其次,我认为您的基准无效。在x.r 中,您应该添加执行wc 和测试FNR==nlines 所需的时间。我的猜测是 getline 解决方案实际上更快,因为您只解析 filename 一次。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-10-27
    • 2017-02-25
    • 2019-11-04
    相关资源
    最近更新 更多