42

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.

0

10 Answers 10

42

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
3
  • Once you're using positional parameters, there's no need for your variables 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
  • @user1404316 This would be an overly complicated way of iterating over the elements in ascending order. Also, wrong: neither [ -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
  • The question specifically says that it is iterating over file names in /var/log, so it's safe to assume that none of the parameters would be "*" or empty. I do stand corrected, though, on the point of the reverse order. Commented Feb 15, 2018 at 18:42
16

Try this, unless you consider line breaks as "funky characters":

ls /var/logs/foo*.log | tac | while read f; do
    bar "$f"
done
5
  • Is there a method that works with line breaks? (I know it's rare, but at least for the sake of learning I'd like to know if it's possible to write correct code.) Commented Dec 22, 2011 at 5:04
  • Creative to use tac to reverse the flow, and if you like to get rid of some unwanted characters like line breaks you can pipe to tr -d '\n'. Commented Dec 22, 2011 at 6:07
  • 6
    This breaks if the file names contain newlines, backslashes or unprintable characters. See mywiki.wooledge.org/ParsingLs and How to loop over the lines of a file? (and the linked threads). Commented Dec 22, 2011 at 8:26
  • The most voted answer broke the variable 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
  • That tac command just saved my day, thanks! Commented Oct 24, 2023 at 4:04
7

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
7
  • 2
    I don't have tac on my system ; I don't know if it's robust but I've been using for x in ${mylist}; do revv="${x} ${revv}"; done Commented Jan 11, 2018 at 8:31
  • @arp That's sort of shocking as tac is part of GNU coreutils. But your solution is also a good one. Commented Jan 11, 2018 at 21:45
  • @ACK_stoverflow There are systems out there without Linux userland tools. Commented Feb 15, 2018 at 18:21
  • @ACK_stoverflow That's what I'm saying, yes. Commented Feb 16, 2018 at 6:46
  • Instead of tac, just do tail -r. Commented Feb 20, 2022 at 21:55
4

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 sorts the array lexically in reverse, except when combined other ordering flags in which case it reverses the order. Specifically, here with a, it sorts in reverse array order.

As @Gilles said, instead of building an array and then looping over them in reverse order using the O and a parameter expansion flags, you can can directly affect the order of the glob expansion using the oX and OX glob qualifiers.

for f in /var/log/foo*.log(On)

Will reverse order your globbed files. On is short for ^on, that is: order by name in reverse.

Instead of n for name, the o glob qualifier can also be followed by:

  • L file Length
  • l number of links
  • m, a, c for modification/access/change-status time
  • d depth (which can be combined with the others)
  • N Not sort.

And ^o or O can be used in place of o to reverse the order and the n qualifier can affect the name order so it be numeric which is generally useful for log files with numeric suffixes.

For instance:

zgrep -hi error /var/log/syslog*(nOn)

To look for errors in chronological order relying on the names of the files sorted numerically, or:

zgrep -hi error /var/log/syslog*(Om)

Relying on the last modification time of those files.

For more 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/

1
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.

7
  • 2
    +1 great answer. It seems like Ubuntu doesn't support tail -r though... am I doing something wrong? Commented Dec 22, 2011 at 6:02
  • 1
    No, I don't think you are. I don't have my linux machine up but a quick google for 'linux tail man' doesn't show it as an option. sunaku mentioned tac as an alternative, so I would try that, instead Commented Dec 22, 2011 at 6:17
  • I've also edited the answer to include another alternative Commented Dec 22, 2011 at 7:01
  • whoops I forgot to +1 when I said +1 :( Done! Sorry about that haha :) Commented Dec 22, 2011 at 7:22
  • 4
    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
1

Since version 5.3, the bash shell has a GLOBSORT special parameter which affects the sorting order of glob expansions.

So here, you could do:

GLOBSORT=-name
shopt -s nullglob
shopt -u failglob
for file in /var/log/foo*.log; do
  ...
done

As an equivalent of zsh's:

for file in /var/log/foo*.log(NOn); do
  ...
done

bash's GLOBSTART=-numeric is however not equivalent to zsh's n qualifier (as in foo*.log(NnOn)) and is of very limited utility as it only works for all-numeric file paths (not even names).

0

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.

0

an easy way is using ls -ras mentioned by @David Schwartz

for f in $(ls -r /var/logs/foo*.log); do
    bar "$f"
done
3
  • This is the most simple way Commented Jan 11, 2025 at 21:04
  • @AntonSamokat it's also unreliable unless you can guarantee the subset of characters in the selected filenames (restricted punctuation, no unprintable characters, no newlines, no spaces) Commented yesterday
  • The OP explicitly asked for "I need a solution that doesn't break for funky characters in the file names". Commented yesterday
0

Based on https://unix.stackexchange.com/a/27385/508263

You can avoid problems with new lines by using \0 like:

printf "abc\na\0cde\nb\0" | tac -s '' | while read -d '' f; do
    bar "$f";
done

give output:

cde
b
abc
a

tac -s '' is split by char \0

read -d '' is read until char \0

To get logs files you can use find /var/logs/ -name 'foo*.log' -print0 like https://unix.stackexchange.com/a/424431/508263 did

-4

Try this:

for f in /var/logs/foo*.log; do
bar "$f"
done

I think it is the most simple way.

1
  • 5
    The question asked for “for loop in reverse order”. Commented Oct 9, 2013 at 7:52

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.