How to Execute Command on a Range of Directories in Bash

bashindexingls

Is it possible to 'partition' a directory listing – say in to blocks of some number, to perform different actions on each 'range'?

For example, lets say I have the following directories inside another folder:

$ ls test
abc/
def/
hij/
klm/
nop/
qrs/
tuv/
wxy/
zzz/

And I want to perform some action on the first 3 directories, another on the second 3, and so on.

My thought was that perhaps a loop across numbers would work, based on the output of something like ls | nl (but before anyone mentions it, I know parsing ls is a no-no!)

This obviously doesn't work, but illustrates my point I hope:

for dir in `ls | nl`; 
do
    do-something-with ${dir{1..3}} # Where $dir has taken on some numerical value linked to the folder)
    do-something-with ${dir{4..6}} 
# And so on...
    do-something-with ${dir{n-3..n}} 
done

The folders I intend to actually do this on, can be worked on in any order (i.e. the final splits can be totally arbitrary), but they have no logical naming consistency – by which I mean they can't be organised sensibly alphabetically or numerically based on any key within the directory name themselves.

Best Answer

There's no reason to do either of the following mistakes for this situation:

  1. Use non-portable GNU extensions (such as xargs -0)
  2. Parse the filenames as a stream of text only, pretending that they can't contain newlines.

You can handle this portably without much difficulty at all:

set -- */
while [ "$#" -gt 0 ]; do
  case "$#" in
    1)
      my-command "$1"
      shift 1
      ;;
    2)
      my-command "$1" "$2"
      shift 2
      ;;
    *)
      my-command "$1" "$2" "$3"
      shift 3
      ;;
  esac
done

Really this is more verbose than necessary, but it is readable this way.

For a different type of partitioning, where you want to split all directories into exactly three different methods of handling (but don't need to handle them at the same time), you could do it like so:

x=1
for d in *; do
  [ ! -d "$d" ] && continue
  case "$x" in
    1)
      # do stuff with "$d"
      ;;
    2)
      # do stuff with "$d"
      ;;
    3)
      # do stuff with "$d"
      ;;
  esac
  x=$(($x+1))
  [ "$x" -eq 4 ] && x=1
done
Related Question