The task is to print the lines of a file in the reverse order in which it appears in the file. A bash script and some other tricks would be presented below to do this. We can do this with tac and sed , but here we will do it without any of these tools and use only the bash inbuilt, bash arrays, and redirection.

Idea

The process is to read each line of the input file in an array with each array element containing a line, and after reading is finished we will print the array from last to first, reverse order, which will print the file in reverse order.
We directly present the script below and then describe.

The Script

#!/bin/bash
#This code is a part of https://phoxis.wordpress.com
#

file_name="$1"

     #Check if file name is given, and it exists

if [ "$#" -eq 0 ]
  then
   echo "Syntax: $0 filename"
   exit
elif [ ! -f "$file_name" ]
  then
     echo "File \""$file_name"\" does not exist"
     exit
fi

     #Set the IFS variable to \n this enables reading one \n separated
     # line per read

IFS=$'\n'
declare -a arr

     #Read from file_name and store each line into next array location.
while read -r line
do
  arr+=("${line}");
done < "$file_name"

     #If last line is not \n terminated read returns false, body of while
     # is not executed. Instead of saving it in the array directly print it
     # because it will be the first line in the file. Also make sure not to
     # terminate the line with newline character, so use echo -n 

if [ ! -z "$line" ]
 then
  echo -n "$line"
fi

     #Count number of lines, and adjust index

i=${#arr[*]}
i=$((i-1))

     #Print the lines in reverse order with a newline after each line 
     # (no -n after echo ensures it. Include the -E parameter to make sure
     # no slash '\' are interpreted as escape sequences
while [ $i -ge 0 ]
do
   echo -E "${arr[$i]}"
   i=$((i-1))
done

Save the above script as “my_tac.sh” . The syntax to use this script is my_tac.sh file_name

At first we get the passed argument into variable file_name . We check if the number of parameter passed is zero, that is , if nothing is passed. If nothing is passed show error. Else check if the passed file name exists. If it does not exist exit, else do nothing.

We will treat each line of the file as one field when reading into an array, and two lines are separated by a newline character a \n. We set the IFS that is the Input Field Separator to \n . Note: $’\n’ quoting will expand the \n into its corresponding newline character. This enables the process to preserve leading blankspaces in a line.
Next we declare an array.

In the while loop the while read -r line reads from the file whose name is in “file_name”. The contents of the file is redirected into the while loop, and the read gets them from there. In each iteration it reads one line at a time and stores into variable line. It is then assigned to the next element of the array in the body of the loop. Note that when assigning the line we also append a newline \n to the end of the line. It is explained later why it is done.
The -r option in read -r tells the command to treat the backslash \ as a normal character as a part of the file and not an escape character. When the file ends the loop terminates.

There is two things to note here. First, Note the double quotes around "$line" in the array expressions. This will ensure that the blank lines with only a \n is read and also the blankspace formatting in the file is preserved.

Second, consider a whose last line is not a blank line. That is the last line contains some text and ends without a new line and simply terminates with NUL. In this case at the last iteration read command will read in the last line but return false as it is not terminated with any IFS characters, and while loop’s body will not be entered, and thus the last line is not stored in the array. In this case when the loop terminates line is not null. Because this is the last line of the file so in reverse operation this will be the first line, so we directly print it, instead of storing it. Note that because that this line does not end with a new line, that is why echo -n is used to print it. The -n option of echo does not end the line with a new line. So the last line, and the last but one line will be printed adjacent (which is correct) in this case. On the other hand, if the line does end with a new line, then the contents of line at the loop termination is null and does not satisfies the if - fi and simply proceeds. This conditional last line processing lets this process print the last line correctly by not printing a newline at the end of it.

Next we calculate the length of the array and adjust the index of the array, which is the number of lines the file contains, and store into a variable i, and it is iterated from the value of i to 0 and the contents of the array is printed in reverse order. When assigning the lines to array we have not appended newline explicitly to all the lines which were found in the loop. echo -E is used to print the lines in reverse. The -E disables backslash character interpretation, so if a file contains “\n” or “\t” or others as a part of the file text they will not be interpreted as special characters by printed as it is. Absence of the "-n" switch in the echo command also prints a newline after each line printed in each iteration of the loop. Note that we have handled the special case of a file with the last line directly ending with EOF/NUL is handled in the if - fi after the while loop and discussed above.

And thus the file is printed in reverse line sequence.

This shell script will process only the first file passed to it. This can be easily expanded by writing the code in a function, and then calling the function with each new argument one by one. shift can be used to traverse through the parameters. Read : https://phoxis.org/2010/03/14/read-multiple-arg-bash-script/

The tac

There is a program that you can use in bash which comes with coreutilities is tac . This acts just reverse as the program cat .

tac file_name

The above command would simply print the contents of the file in reverse line order. I would prefer using this tool to make reverse of files instead.

sed

There are other ways too to print the file in reverse line order like the cool example with sed below

sed -n '1!G;h;$p' file_name

Work it out yourself, i am not an sedder!

Update

15.03.2011 : Bug removed: Terminal non \n terminated line printing handling.
16.03.2011 : Code and Description Updated : Now nul terminated files’ last line is printed correctly.
13.05.2011 : Bug removed : Previous version printed backslash characters inside the file as special characters. For example for the line “h\n\e” , “h” was output in the first line then “e” was in the next line. This was because we included the -e switch in the echo to print the newlines after each line. In the previous version the newlines after each line was assigned inside the line itself when assigning it in the loop, and to print it -e switch was used, which created the problem.

Advertisements

17 thoughts on “Bash Script : Print lines of a file in reverse order

  1. I think you’ll find that you have a typo error in line #37 where you are adjusting for zero based array indexing:

    i=$(($i-2)) should read i=$(($i-1))

    1. Although it is not a typo but an attempt to stop the last dummy blank line from being printed. But taking a closer look at the code reviled a bug in the code.
      Note that there is an extra line arr+=(“$line”) after the loop ends. If the file does end with a newline then this will be considered by the loop, and the extra line would add an extra newline the array which does not need to be printed. For this the index was being decreased by 2.
      If the file does not ended with a new line, instead the last line is null terminated, then although the read command would read that line but would return false and the loop will break without entering the read in value into the array. In this case the outside arr+=(“$line”) would do the entry. And in this case the last line is important to print so in this case the array adjustment needs to be decreased by 1.
      After your comment i had a closer look at the code, and edited the code that if the last line read in was terminated with a newline then after the loop breaks the $line would contain an empty string, in which case it should not be entered in the array. And if the file ends with a null, then after the loop breaks $line will have the last line which was read in. This line was not entered in the while loop (as told above) and should be added manually to the array. After this conditional last line insertion is done, before printing the array index adjustment needed is only i=$((i-1)) .
      The code is changed as per this change.

        1. Thanks for getting the bug killed. The only known issue with the code is that when printing the contents of a file not ending with a newline, the last line and the last but one line are printed in separate lines, which should not be. I have made another update, files having the last line without a newline are printed correctly. In the previous version the null terminated last line and the last but one line was printed in two lines (which is incorrect), but now they are printed adjacent (as there is no \n in between the two lines) .

          All these stuff, to handle one terminal condition :) , but you can never tell when the overlooked terminal conditions become vital in your application.

  2. Hi,
    Nice blog post! I googled upon this as I was doing some ‘bash’ing :-)
    I have some observations as been playing around with this a bit, as I was contemplating your terminal conditions.
    As you say ‘All these stuff, to handle one terminal condition’, this made me think if it weren’t possible to eliminate the cause of the condition (i.e. the absense or not of the final newline).
    So my first question was, is it really necessary to set IFS=$’\n’? Following on from your assumption that the ‘while read -r line’ test evaluated false if there was no newline, I decided to test this and put in some echo lines in the [ ! -z “$line” ] test. I found that the code worked fine without it. In fact I found that it made no difference whatsoever, with or without it. This could be due to the fact that I only did limited testing with a file containing 5 lines, no escape characters but with and without a terminating newline.
    So even if my input file did not have a terminating newline, the script would still process fine (and the last $line would always be empty).
    I tested this both on an Ubuntu 10.10 and a Mac OS X 10.6.7 machine.
    Digging a lil further I found that bash has a special construct that enables the reading of a file into an array. That way you can eliminate the while read loop altogether, including the test of the last line.
    So with my (albeit limited) testing, you could do the same with this somewhat smaller piece of code:

    #!/bin/bash
    #This code is a part of https://phoxis.wordpress.com
    #
    
    file_name="$1"
         #Check if file name is given, and it exists
    
    if [ "$#" -eq 0 ]
      then
       echo "Syntax: $0 filename"
       exit
    elif [ ! -f "$file_name" ]
      then
         echo "File \""$file_name"\" does not exist"
         exit
    fi
    
         #Set the IFS variable to \n this enables reading one \n separated
         # line per read
    
    IFS=$'\n'
    
         #Read from file_name and store contents into array.
    arr=( $( < $file_name ) )
    
         #Count number of lines, and adjust index
    
    i=${#arr[*]}
    i=$((i-1))
    
         #Print the lines in reverse order
    while [ $i -ge 0 ]
    do
       echo -e "${arr[$i]}"
       i=$((i-1))
    done
    

    Note that I’m a bash n00b who’s trying to learn more so if I’m off on my conclusions (very well possible due to my said limited testing), please correct me.

    Cheers!

      1. “is it really necessary to set IFS=$’\n’”

        Yes it is necessary to set this. Because the default IFS characters seperate the fields of by the blankspace and the tab and the new line. So if you read one single line into the array like that with the default IFS value then you will end up with one single word in each array location. For example if file has a line : hello man this is s test and you execute the arr=( $( < $file_name ) ) , then arr[0] will be “hello”, arr[1] will be “man” etc. You can test the result by not setting the IFS=$'\n'. Setting this makes the field seperator only the new lines. (Note that the code version you have posted in the comment contains the IFS=$'\n' line.

      2. “Following on from your assumption that the ‘while read -r line’ test evaluated false if there was no newline,”

        Please note that a line without a newline terminator can only be when the line is the last line of a file, and the line ends with the end of file. In these cases the read command will return false. This is described in the bash manual. Check man read scroll to the read section. Or have a look at here: http://ss64.com/bash/read.html . This says

        The return code is zero, unless end-of-file is encountered, read times out, or an invalid file descriptor is supplied as the argument to -u.

        Shoot a text editor and write a small line and do not enter a new line after it, save and close. Reopen and recheck if the editor inserted the newlines automatically, if yes then remove them and resave, then try reading this with read it would return false (1), but the line would be read in the variable.

      3. “I decided to test this and put in some echo lines in the [ ! -z “$line” ] test. I found that the code worked fine without it.”

        In the version of the code originally posted in the blog, because for those files of which the last lines are directly terminated with an end-of-file , read returns false, so the body of the while loop does not gets executed. But this last line is still in the variable, and the [ ! -z "$line" ] will become true and this last line is manually assigned to the array. This way we consider all the lines in the file. In case the last line did end with a new line, then at the last iteration the line variable contains null, so the condition becomes false and we do not enter it.

      4. eliminate the while read loop altogether, including the test of the last line.
      5. This is true. I actually wanted to not use this method. Actually there are even more methods, like open the file for reading with some file descriptor with exec and use that descriptor with read -u. The way you have done is a good way, and the last line is included which does not depend wether it ends with a new line or an eof. But still you need to set the IFS to partition the contents of the file at the newline characters.

        The case in which your code will fail is:

      1. If you have a file say with two lines as folows:
        l1
        l2

        where there is no newline after the line 2 , ie. “l2” ends directly with eof.
        Your code would generate output

        l2
        l1
        

        where the output should be

        l2l1
        

        because there is no newline after l2. Compare output with the program tac which performs the same job as the script.

      Where both of our code fails is in:

      1. create a file containing the contents:
        h\ne\nl\nl\no
        

        both of our code will output

        h
        e
        l
        l
        o
        

        where the output should be

        h\ne\nl\nl\no
        

      So at the end we have come to know that this thing can be done in a lot of ways. The best thing is that i have found one more bug in my code when replying to your comment. This is same as after “@DangerousDaren’s” comment i found one and eliminated.

      Thank you for going through the code and commeting with such details. Please let me know if you have any comments or other possibilities. Also i would request you to have a look at the bugfixed version of my code, and the updated description text.

  3. I stumbled upon this, and I think it doesn’t have to load the entire file into memory (as opposed to the sed solution, and most others):

    tail -r
    

    Example:

    [~]> cat testfile.txt 
    first line
    second line
    line 3
    last line
    [~]> tail -r testfile.txt 
    last line
    line 3
    second line
    first line
    

    FYI, from the man page: “The default for the -r option is to display all of the input.”

  4. Probably any other (interpreted) language choice would be better (i.e. *much* faster for large files) than bash, but you probably know that already :P

    I usually forget about tac since I need to do this about once a year, and end up re-inventing the wheel. Off the top of my head:

    $ grep -n ^ FILE | sort -nr -t: -k1 | cut -d: -f2
    1. There are a lot of other ways, and definitely i would prefer other interpreted language or better write a C/C++ program. This was just a demonstration of achieving the thing using bash builtins.

  5. Just stumbled across this post while searching for something.

    As of bash v.4 you can use the new mapfile keyword to load the array. It’s faster, easier, and safer than messing with IFS and looping through the file. You can even set it to start with index 1, allowing you to simplify the printing loop too. And don’t forget that you can do arithmetic inside array index fields. The whole operation can be reduced to three commands, not including error checking.

    (I hope this forum accepts bbcode!)

    maptfile -t -O 1 array <file.txt
    i=${#array[@]}
    while (( i )) ; do
            echo "${array[i--]}"
    done
    

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s