How do I reverse a for loop?

46,203

Solution 1

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

Solution 2

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

ls /var/logs/foo*.log | tac | while read f; do
    bar "$f"
done

Solution 3

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

Solution 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 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 order
  • L file length
  • l number of links
  • m modification date
  • n name
  • ^o reverse order (o is normal order)
  • O reverse order

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

Solution 5

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.

Share:
46,203

Related videos on Youtube

user541686
Author by

user541686

Updated on September 18, 2022

Comments

  • user541686
    user541686 over 1 year

    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.

    • ChrisCornwall
      ChrisCornwall over 12 years
      Just pipe to sort -r before the for, or launder through ls -r.
  • user541686
    user541686 over 12 years
    +1 great answer. It seems like Ubuntu doesn't support tail -r though... am I doing something wrong?
  • Johan
    Johan over 12 years
    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'.
  • Ignux02
    Ignux02 over 12 years
    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
  • Ignux02
    Ignux02 over 12 years
    I've also edited the answer to include another alternative
  • user541686
    user541686 over 12 years
    whoops I forgot to +1 when I said +1 :( Done! Sorry about that haha :)
  • Gilles 'SO- stop being evil'
    Gilles 'SO- stop being evil' over 12 years
    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).
  • Gilles 'SO- stop being evil'
    Gilles 'SO- stop being evil' over 12 years
    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.
  • manatwork
    manatwork over 10 years
    The question asked for “for loop in reverse order”.
  • Serge Stroobandt
    Serge Stroobandt almost 9 years
    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.
  • Alex
    Alex almost 7 years
    On OSX, tail -r reverses newline-delimited input. Since your find returns null-delimited input, tail -r does not do anything to it at all. If you omit -print0, however, then find will return newline-delimited input, so tail -r will work. But in that case, your solution is no better than simply piping the output of ls -1 into tail -r.
  • Alex
    Alex almost 7 years
    This solution only works when bar is a single executable. It does not work, for example, with loops.
  • arp
    arp over 6 years
    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
  • ACK_stoverflow
    ACK_stoverflow over 6 years
    @arp That's sort of shocking as tac is part of GNU coreutils. But your solution is also a good one.
  • Kusalananda
    Kusalananda over 6 years
    @ACK_stoverflow There are systems out there without Linux userland tools.
  • user1404316
    user1404316 over 6 years
    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.
  • Gilles 'SO- stop being evil'
    Gilles 'SO- stop being evil' over 6 years
    @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.
  • user1404316
    user1404316 over 6 years
    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.
  • Kusalananda
    Kusalananda over 6 years
    @ACK_stoverflow That's what I'm saying, yes.
  • Taher Ghaleb
    Taher Ghaleb about 2 years
    Instead of tac, just do tail -r.
  • ACK_stoverflow
    ACK_stoverflow about 2 years
    @TaherGhaleb Idk what version of tail is on your system but no linux I have access to (several debian versions and an embedded system) has tail -r. Guessing tail -r is a mac thing.
  • Taher Ghaleb
    Taher Ghaleb about 2 years
    Oh yeah, sorry for not mentioning that : ) Thanks.