How do I properly do a for
loop in reverse order?
for f in /var/logs/foo*.log; do
bar "$f"
done
I need a solution that doesn't break for funky characters in the file names.
In bash or ksh, put the file names in an array, and iterate over that array in reverse order.
files=(/var/logs/foo*.log)
for ((i=${#files[@]}-1; i>=0; i--)); do
bar "${files[$i]}"
done
The code above also works in zsh if the ksh_arrays
option is set (it is in ksh emulation mode). There's a simpler method in zsh, which is to reverse the order of the matches through a glob qualifier:
for f in /var/logs/foo*.log(On); do bar $f; done
POSIX doesn't include arrays, so if you want to be portable, your only option to directly store an array of strings is the positional parameters.
set -- /var/logs/foo*.log
i=$#
while [ $i -gt 0 ]; do
eval "f=\${$i}"
bar "$f"
i=$((i-1))
done
i
and f
; just perform a shift
, use $1
for your call to bar
, and test on [ -z $1 ]
in your while
.
Commented
Feb 15, 2018 at 18:22
[ -z $1 ]
nor [ -z "$1" ]
are useful tests (what if the parameter was *
, or an empty string?). And in any case they don't help here: the question is how to loop in the opposite order.
Commented
Feb 15, 2018 at 18:38
Try this, unless you consider line breaks as "funky characters":
ls /var/logs/foo*.log | tac | while read f; do
bar "$f"
done
f
in my situation. This answer though, acts pretty much as a drop-in replacement for the ordinary for
line.
Commented
Jul 6, 2015 at 23:14
If anyone is trying to figure out how to reverse iterate over a space-delimited string list, this works:
reverse() {
tac <(echo "$@" | tr ' ' '\n') | tr '\n' ' '
}
list="a bb ccc"
for i in `reverse $list`; do
echo "$i"
done
> ccc
> bb
> a
tac
is part of GNU coreutils. But your solution is also a good one.
Commented
Jan 11, 2018 at 21:45
In your example you're looping over several files, but I found this question because of its more general title which could also cover looping over an array, or reversing based on any number of orders.
Here's how to do that in Zsh:
If you're looping over the elements in an array, use this syntax (source)
for f in ${(Oa)your_array}; do
...
done
O
reverses of the order specified in the next flag; a
is the normal array order.
As @Gilles said, On
will reverse order your globbed files, e.g. with my/file/glob/*(On)
. That's because On
is "reverse name order."
Zsh sort flags:
a
array orderL
file lengthl
number of linksm
modification daten
name^o
reverse order (o
is normal order)O
reverse orderFor examples, see https://github.com/grml/zsh-lovers/blob/master/zsh-lovers.1.txt and http://reasoniamhere.com/2014/01/11/outrageously-useful-tips-to-master-your-z-shell/
find /var/logs/ -name 'foo*.log' -print0 | tail -r | xargs -0 bar
Should operate the way you want (this was tested on Mac OS X and I have a caveat below...).
From the man page for find:
-print0
This primary always evaluates to true. It prints the pathname of the current file to standard output, followed by an ASCII NUL character (charac-
ter code 0).
Basically, you're finding the files that match your string + glob and terminating each with a NUL character. If your filenames contain newlines or other strange characters, find should handle this well.
tail -r
takes the standard input through the pipe and reverses it (note that tail -r
prints all of the input to stdout, and not just the last 10 lines, which is the standard default. man tail
for more info).
We then pipe that to xargs -0
:
-0 Change xargs to expect NUL (``\0'') characters as separators, instead of spaces and newlines. This is expected to be used in concert with the
-print0 function in find(1).
Here, xargs expects to see arguments separated by the NUL character, which you passed from find
and reversed with tail
.
My caveat: I've read that tail
doesn't play well with null-terminated strings. This worked well on Mac OS X, but I can't guarantee that's the case for all *nixes. Tread carefully.
I should also mention that GNU Parallel is often used as an xargs
alternative. You may check that out, too.
I may be missing something, so others should chime in.
tail -r
though... am I doing something wrong?
Commented
Dec 22, 2011 at 6:02
tac
as an alternative, so I would try that, instead
tail -r
is specific to OSX, and reverses newline-delimited input, not null-delimited input. Your second solution doesn't work at all (you're piping input to ls
, which doesn't care); there is no easy fix that would make it work reliably.
Commented
Dec 22, 2011 at 8:31
Mac OSX does not support the tac
command. The solution by @tcdyl works when you are calling a single command in a for
loop. For all other cases, the following is the simplest way to get around it.
This approach does not support having newlines in your filenames. The underlying reason is, that tail -r
sorts its input as delimited by newlines.
for i in `ls -1 [filename pattern] | tail -r`; do [commands here]; done
However, there is a way to get around the newline limitation. If you know your filenames do not contain a certain character (say, '='), then you can use tr
to replace all newlines to become this character, and then do the sorting. The result would look as follows:
for i in `find [directory] -name '[filename]' -print0 | tr '\n' '=' | tr '\0' '\n'
| tail -r | tr '\n' '\0' | tr '=' '\n' | xargs -0`; do [commands]; done
Note: depending on your version of tr
, it might not support '\0'
as a character. This can usually be worked around by changing the locale to C (but I don't remember how exactly, since after fixing it once it now works on my computer). If you get an error message, and you cannot find the workaround, then please post it as a comment, so I can help you troubleshoot it.
an easy way is using ls -r
as mentioned by @David Schwartz
for f in $(ls -r /var/logs/foo*.log); do
bar "$f"
done
Try this:
for f in /var/logs/foo*.log; do
bar "$f"
done
I think it is the most simple way.
sort -r
before thefor
, or launder throughls -r
.