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. Mar 16, 2011 at 11:19
-
16It's a famous
sed
one-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. 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." 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). Apr 15, 2011 at 20:39 -
1Note 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. 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
-
5
-
@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 first
perl reverse<>` it the best/fastest answer on the page (to me), at 10 times faster than thisawk
answer (all the awk anseres are about the same, time-wise)– Peter.OAug 6, 2012 at 17:26 -
2
-
@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 a
for any input data as it'll fail when that data contains printf formatting strings, always doprintf "%s", a
instead. Oct 1, 2021 at 19:59 -
@ninjalj what does the
a{}
part do. normally{}
contains some command, here it is, however, empty? Mar 7 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 UngureanuMar 16, 2011 at 11:04
-
5
-
2I think that's called Schwartzian transform, or decorate-sort-undecorate– ninjaljMar 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.– mc0eAug 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 usingcut
for it's sole purpose instead of makingsed
do whatcut
exists to do, and is only sorting on the one, necessary field that contains the line number. 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.OAug 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; }'
-
2
-
2(Actually, there's a shorter one, but it's really ugly, witness its awfulness:
perl -pe '$\=$_.$\}{'
)– ninjaljMar 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.OAug 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
).– Kusalananda ♦Aug 8, 2018 at 11:19
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
. Jul 31, 2015 at 12:56 -
Use
IFS=''
andread -r
to prevent all kinds of escapes and trailing IFS removal from screwing it up. I think the bashmapfile ARRAY_NAME
builtin is a better solution for reading into arrays though. Sep 18, 2015 at 16:17
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'
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. Oct 1, 2021 at 20:54
- 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
nl
does not number blank lines. There is already another answer implementing this Schwartzian transform idea.– Kusalananda ♦Apr 27, 2021 at 20:19 -
You're right, nl needs the option
-ba
for empty lines and might not exist for everynl
. Apr 28, 2021 at 5:23 -
nl
isn't a mandatory POSIX tool so iif you don't havetac
you probably don't havenl
either. Oct 1, 2021 at 20:53