Get last 2 lines from multiple files

I can get the last 2 lines of multiple files using tail command like

$ tail -n 2 file1.txt file2.txt
==> file1.txt <==
3,2,1,1
8,8,4,4

==> file2.txt <==
B1 987 6545
C1 876 5434
$

But I need to format the above output to get it like below

3,2,1,1 file1.txt
8,8,4,4 file1.txt
B1 987 6545 file2.txt
C1 876 5434 file2.txt

Is there way to get the above output only using awk?

I tried something like below, but it works for only last line.

$ awk ' { if(FNR==1 && NR!=1)  print p,f; p=$0;f=FILENAME }  END { print p,f } ' file1.txt file2.txt
8,8,4,4 file1.txt
C1 876 5434 file2.txt
$

How to expand it for last 2 or n lines?.

3 answers

  • answered 2019-03-13 18:59 B. Shefter

    It's not the prettiest solution, but it's an awk one-liner as requested:

    awk '{if (FNR==1 && NR!=1) {print secondLast" "prevFname ORS last" "prevFname} prevFname=FILENAME;last=$0} {secondLast=prevLine;prevLine=$0} END {print secondLast" "FILENAME ORS last" "FILENAME}' file1.txt file2.txt
    

    Note that this will become unwieldy as the number of lines you want increases.

  • answered 2019-03-13 19:21 kvantour

    The following answer represents a POSIX compliant solution that works with any awk. As you can see from Ed Morton's solution, GNU awk can make life a bit easier.


    the last two lines: The easiest for two lines would be the following:

    awk '(FNR==1) && f!="" { print t1,f; print t2,f }
         (FNR==1) { f=FILENAME }
         { t1=t2; t2=$0 }
         END { print t1, f; print t2, f}' file1 file2 file3 ...
    

    The program introduces the variables f which stores the filename and t1 and t2 representing the last two lines of the file. Every time we enter a new non-empty file (FNR==1) or hit the end of the program, we use these variables to do the printing.

    This method, however, has a main flaw:

    Files with a single line create wrong results

    the last n lines: If you want to expand this to the last n lines, you'll have to make use of an array t. Furthermore, if you want to be able to handle files with less than n lines, you'll have to keep track of how many lines the file has (fnr). The latter variable fnr will always equal FNR of the previous line at the beginning of the program cycle.

    Also, it will become a bit messy, if you want to use the swap principle

    awk -v n=2 '(FNR==1) && f!="" { for(i=1; i <= (fnr < n ? fnr : n); ++i) print t[i],f }
                (FNR==1) { f=FILENAME }
                { fnr = FNR }
                (fnr <= n) { t[fnr] = $0 }                
                (fnr >  n) { for(i=1; i < n; ++i) t[i] = t[i+1]; t[n]=$0 }
                END { for(i=1; i <= (fnr < n ? fnr : n); ++i) print t[i],f }
               ' file1 file2 file3 ...
    

    You can clean it up a bit if you want to get rid of the swapping and use modulo calculations (cfr Ed Morton):

    awk -v n=2 'function tail { 
                   for(i=1+(fnr < n ? n-fnr : 0); i<=n; ++i) print t[(fnr+i)%n],f  
                }
                (FNR==1) && f!="" { tail() }
                (FNR==1) { f=FILENAME }
                { fnr = FNR; t[FNR%n] = $0 }
                END { tail() }
               ' file1 file2 file3 ...
    

    Which, in GNU awk is just simply:

    awk -v n=2 '{ t[FNR%n] = $0 }
                ENDFILE { 
                  for(i=1+(FNR < n ? n-FNR : 0); i<=n; ++i) print t[(FNR+i)%n],FILENAME                
                }' file1 file2 file3 ...
    

  • answered 2019-03-13 19:45 Ed Morton

    With GNU awk for ENDFILE:

    $ awk '{p2=p1; p1=$0} ENDFILE{print p2, FILENAME ORS p1, FILENAME }' file1 file2
    3,2,1,1 file1
    8,8,4,4 file1
    B1 987 6545 file2
    C1 876 5434 file2
    

    In general for any number n of lines:

    $ awk -v n=2 '{p[NR%n]=$0} ENDFILE{for (i=1; i<=n; i++) print p[(NR+i)%n], FILENAME}' file1 file2
    3,2,1,1 file1
    8,8,4,4 file1
    B1 987 6545 file2
    C1 876 5434 file2
    

    Add a delete p after printing and/or do whatever else you like to handle files with fewer than n lines.