I want to print lines from a file backwards without using tac command. Is there any other solution to do such thing with bash?
12 Answers
Using sed to emulate tac:
sed '1!G;h;$!d' "${inputfile}" - 7This is a good solution, but some explanation for how this works will be even better.Chen Levy– Chen Levy2011-03-16 11:19:44 +00:00Commented Mar 16, 2011 at 11:19
- 17It's a famous
sedone-liner. See "36. Reverse order of lines (emulate "tac" Unix command)." in Famous Sed One-Liners Explained for a full explanation of how it works.johnsyweb– johnsyweb2011-03-16 11:37:23 +00:00Commented Mar 16, 2011 at 11:37 - 7Perhaps worth noting: "These two one-liners actually use a lot of memory because they keep the whole file in hold buffer in reverse order before printing it out. Avoid these one-liners for large files."Mr. Shickadance– Mr. Shickadance2011-03-17 14:33:02 +00:00Commented Mar 17, 2011 at 14:33
- So do all the other answers (except maybe the one using
sort- there's a chance it will use a temporary file).Random832– Random8322011-04-15 20:39:03 +00:00Commented Apr 15, 2011 at 20:39 - 2Note that tac is faster for regular files because it reads the file backward. For pipes, it has to do the same as the other solutions (hold in memory or in temp files), so is not significantly faster.Stéphane Chazelas– Stéphane Chazelas2012-09-14 05:48:58 +00:00Commented Sep 14, 2012 at 5:48
With ed:
ed -s infile <<IN g/^/m0 ,p q IN If you're on BSD/OSX (and hopefully soon on GNU/linux too as it will be POSIX):
tail -r infile awk '{a[i++]=$0} END {for (j=i-1; j>=0;) print a[j--] }' file.txt
via awk one liners
- 5A shorter one:
awk 'a=$0RS a{}END{printf a}'ninjalj– ninjalj2011-03-17 23:34:05 +00:00Commented Mar 17, 2011 at 23:34 - @ninjalj: it may be shorter, but it becomes extremely slow as the file size gets larger. I gave up after waiting for 2min 30sec.
but your firstperl reverse<>` it the best/fastest answer on the page (to me), at 10 times faster than thisawkanswer (all the awk anseres are about the same, time-wise)Peter.O– Peter.O2012-08-06 17:26:09 +00:00Commented Aug 6, 2012 at 17:26 - 3Or
awk '{a[NR]=$0} END {while (NR) print a[NR--]}'Stéphane Chazelas– Stéphane Chazelas2012-09-14 05:41:44 +00:00Commented Sep 14, 2012 at 5:41 - @ninjalj appending to a variable is one of the slowest operations in awk because awk has to calculate the resulting size, find new memory, move the contents there, and change the variable to point to that new location. Doing that with a large string isn't tenable. Also never do
printf afor any input data as it'll fail when that data contains printf formatting strings, always doprintf "%s", ainstead.Ed Morton– Ed Morton2021-10-01 19:59:07 +00:00Commented Oct 1, 2021 at 19:59 - @ninjalj what does the
a{}part do. normally{}contains some command, here it is, however, empty?Alexander Cska– Alexander Cska2023-03-07 16:44:17 +00:00Commented Mar 7, 2023 at 16:44
You can pipe it through:
awk '{print NR" "$0}' | sort -k1 -n -r | sed 's/^[^ ]* //g' The awk prefixes each line with the line number followed by a space. The sort reverses the order of the lines by sorting on the first field (line number) in reverse order, numeric style. And the sed strips off the line numbers.
The following example shows this in action:
pax$ echo 'a b c d e f g h i j k l' | awk '{print NR" "$0}' | sort -k1 -n -r | sed 's/^[^ ]* //g' It outputs:
l k j i h g f e d c b a - Ok. Thanks! I'll try this. And thanks for the comments!Ionut Ungureanu– Ionut Ungureanu2011-03-16 11:04:05 +00:00Commented Mar 16, 2011 at 11:04
- 5
cat -nacts much likeawk '{print NR" "$0}'Chen Levy– Chen Levy2011-03-16 11:09:15 +00:00Commented Mar 16, 2011 at 11:09 - 2I think that's called Schwartzian transform, or decorate-sort-undecorateninjalj– ninjalj2011-03-17 23:25:52 +00:00Commented Mar 17, 2011 at 23:25
- This general approach is nice in that sort handles using files on disk if the task is too big to reasonably do in memory. It might be gentler on memory though if you used temporary files between the steps rather than pipes.mc0e– mc0e2016-08-10 06:15:03 +00:00Commented Aug 10, 2016 at 6:15
- 1
awk -v OFS='\t' '{print NR, $0}' file | sort -k1,1nr | cut -f2-is IMHO the cleanest way to write that as it's still using mandatory POSIX tools but is using awks OFS instead of hard-coding a separator char in the print and doing string concatenation, is using awk to generate input that uses the default separator for cut,\t, is usingcutfor it's sole purpose instead of makingseddo whatcutexists to do, and is only sorting on the one, necessary field that contains the line number.Ed Morton– Ed Morton2021-10-01 21:01:23 +00:00Commented Oct 1, 2021 at 21:01
As you asked to do it in bash, here is a solution that doesn't make use of awk, sed or perl, just a bash function:
reverse () { local line if IFS= read -r line then reverse printf '%s\n' "$line" fi } The output of
echo 'a b c d ' | reverse is
d c b a As expected.
But beware that lines are stored in memory, one line in each recursively called instance of the function. So careful with big files.
- 5It quickly becomes impractically slow as file size increases, compared to even the slowest of the other answers, and as you have suggested, it blows memory pretty easily: *bash recursive function... but its an interesting idea. Segmentation fault *Peter.O– Peter.O2012-08-06 16:55:51 +00:00Commented Aug 6, 2012 at 16:55
POSIX vi does this, so also ed or ex.
vi:
:g/^/m0 ex:
ex -s infile <<EOS g/^/move0 x EOS ed:
ed -s infile <<EOF g/^/m0 ,p w EOF In perl:
cat <somefile> | perl -e 'while(<>) { push @a, $_; } foreach (reverse(@a)) { print; }' - 2or the shorter
perl -e 'print reverse<>'ninjalj– ninjalj2011-03-17 23:21:10 +00:00Commented Mar 17, 2011 at 23:21 - 2(Actually, there's a shorter one, but it's really ugly, witness its awfulness:
perl -pe '$\=$_.$\}{')ninjalj– ninjalj2011-03-17 23:21:59 +00:00Commented Mar 17, 2011 at 23:21 - @Frederik Deweerdt: Fast, but it loses the first line... @ ninjalj:
reverse<)is fast: good! but the "really ugly" one is extremely slow as the number of lines increases.!!....Peter.O– Peter.O2012-08-06 16:41:42 +00:00Commented Aug 6, 2012 at 16:41 - The nice thing about this one is that the contents of the file is not necessarily read into memory (except possibly in chunks by
sort).2018-08-08 11:19:29 +00:00Commented Aug 8, 2018 at 11:19
Bash, with mapfile mentioned in comments to fiximan, and actually an possibly better version:
# last [LINES=50] _last_flush(){ BUF=("${BUF[@]:$(($1-LINES)):$1}"); } # flush the lines, can be slow. last(){ local LINES="${1:-10}" BUF ((LINES)) || return 2 mapfile -C _last_flush -c $(( (LINES<5000) ? 5000 : LINES+5 )) BUF BUF=("${BUF[@]}") # Make sure the array subscripts make sence, can be slow. ((LINES="${#BUF[@]}" > LINES ? LINES : "${#BUF[@]}")) for ((i="${#BUF[@]}"; i>"${#BUF[@]}"-LINES; i--)); do echo -n "${BUF[i]}"; done } Its performance is basically comparable to the sed solution, and gets faster as the number of requested lines decreases.
Simple do this with your file to sort the data in reverse order, and it should be unique. sed -n '1h;1d;G;h;$p'
- Number the lines with nl
- sort in reverse order by number
- remove the numbers with sed
as shown here:
echo 'e > f > a > c > ' | nl -ba | sort -nr | sed -r 's/^ *[0-9]+\t//' Result:
c a f e
Note that "nl -ba" for including empty lines in the numbering is an option of GNU-nl and might not work with every nl.
- 1
nldoes not number blank lines. There is already another answer implementing this Schwartzian transform idea.2021-04-27 20:19:51 +00:00Commented Apr 27, 2021 at 20:19 - You're right, nl needs the option
-bafor empty lines and might not exist for everynl.user unknown– user unknown2021-04-28 05:23:36 +00:00Commented Apr 28, 2021 at 5:23 -
nlisn't a mandatory POSIX tool so iif you don't havetacyou probably don't havenleither.Ed Morton– Ed Morton2021-10-01 20:53:15 +00:00Commented Oct 1, 2021 at 20:53
BASH-only solution
read file into bash array ( one line = one element of array ) and print out array in reverse order:
i=0 while read line[$i] ; do i=$(($i+1)) done < FILE for (( i=${#line[@]}-1 ; i>=0 ; i-- )) ; do echo ${line[$i]} done - Try it with indented lines... philfr's version is a bit better but still veeeery slooooow so really, when it comes to text processing, never use
while..read.don_crissti– don_crissti2015-07-31 12:56:16 +00:00Commented Jul 31, 2015 at 12:56 - Use
IFS=''andread -rto prevent all kinds of escapes and trailing IFS removal from screwing it up. I think the bashmapfile ARRAY_NAMEbuiltin is a better solution for reading into arrays though.Mingye Wang– Mingye Wang2015-09-18 16:17:36 +00:00Commented Sep 18, 2015 at 16:17 - This approach should never be used. See unix.stackexchange.com/q/169716/135943Wildcard– Wildcard2024-08-30 19:45:58 +00:00Commented Aug 30, 2024 at 19:45
awk -v OFS='\t' '{ print NR,$0 }' | sort -nr | cut -f 2- This prepends the line number before each line, with a delimiting tab character, sorts the lines in reverse order on these line numbers, and then removes the numbers again with cut.
See also: Schwartzian transform (Wikipedia link)
- I assume this got downvoted because it's essentially the same answer as was posted a year earlier at unix.stackexchange.com/a/9357/133219.Ed Morton– Ed Morton2021-10-01 20:54:30 +00:00Commented Oct 1, 2021 at 20:54