4

Usually when I write a shell script for a specific task, I just make a list of files like so:

#/bin/sh
read -d '' imagefiles <<EOF

./01/IMG0000.jpg
./01/IMG0001.jpg
./01/IMG0002.jpg
./01/IMG0003.jpg
./01/IMG0004.jpg
./01/IMG0005.jpg
./01/IMG0006.jpg
./01/IMG0007.jpg
(a whole bunch of files down to ./10/IMG0102.jpg)

EOF

for i in $imagefiles
  for j in range(len(commands))
do

mv $i ./$j.jpg

  done
done

In this instance I wanted to be able to iterate over the output of seq but following Gilles's suggestion simply wrote that part out of it out as Python (yeah I know that as it is it would execute each command j times, leading to about 100000 executions). Earlier on in the day I was renaming 736 files in sequence, but now I'm renaming 1000-odd files. I'm sure there's a better way to do that (please don't hesitate to tell me), but it would still be nice to know how to iterate over the list of commands and some other iterable.

10
  • Why do you need seq.. can't you just increment a number each time you process a file name?
    – Peter.O
    Aug 23, 2011 at 14:39
  • Your example code iterating over a list of commands is incomplete and I'm not quite sure I see the purpose or now it is supposed to interact with your sequence. Could you be more specific?
    – Caleb
    Aug 23, 2011 at 14:56
  • ixtmixilix, when you ask a question about shell scripting, it is very helpful to indicate what shell you are using (use echo $SHELL for example if you are not sure). Or otherwise, say that you are interested in a portable solution. Aug 23, 2011 at 20:09
  • Done. (See updated answer). Aug 23, 2011 at 21:35
  • I don't understand what you're trying to do. What does “iterate over two sets of iterables” mean? Explain what you want the script to do. In a pinch, even Python code would be better than nothing. Aug 23, 2011 at 21:40

4 Answers 4

5

Ok, so you want to zip two iterables, or in other words you want a single loop, iterating over a bunch of strings, with an additional counter. It's quite easy to implement a counter.

n=0
for x in $commands; do
  mv -- "$x" "$n.jpg"
  n=$(($n+1))
done

Note that this only works if none of the elements that you're iterating over contains any whitespace (nor globbing characters). If you have items separated by newlines, turn off globbing and split only on newlines.

n=0
IFS='
'; set -f
for x in $commands; do
  mv -- "$x" "$n.jpg"
  n=$(($n+1))
done
set +f; unset IFS

If you only need to iterate over the data once, loop around read (see Why is while IFS= read used so often, instead of IFS=; while read..? for more explanations).

n=0
while IFS= read -r x; do
  mv -- "$x" "$n.jpg"
  n=$(($n+1))
done <<EOF
…
EOF

If you're using a shell that has arrays (bash, ksh or zsh), store the elements in an array. In zsh, either run setopt ksh_arrays to number array elements from 0, or adapt the code for array element numbering starting at 1.

commands=(
    ./01/IMG0000.jpg
    …
)
n=0
while [[ $n -lt ${#commands} ]]; do
  mv -- "${commands[$n]}" "$n.jpg"
done
3

If your shell is bash, you don't need to run the seq program. Just use

for i in {1..736}
  do

If you want a sequence with leading zeros, just use {001..736} (or even {0001..0736}) instead.

Also, when possible, try not to use backticks (see this question for how it can mess things up).

Another thing is, when you want to operate on a set of files you might find this useful:

for file in /some/path/*
  do

Which will operate on all the files (and folders) in the directory /some/path/. To narrow this down to files only, you can use a test like [ -f "$file" ], for example

for file in /some/path/*
  do
  [ -f "$file" ] || continue
  (do your stuff here)

Furthermore, the construct /some/path/* can be tuned by shell patterns like /some/path[123]/*, /some/path[A-Z]/b*[e-g]*, /some/path{1,5,23}/* etc.

As far as the first part of your question is concerned, you can just use a nested for (or while or any) loop like this:

for cm in $commands; do
 for file in /some/path[ab]{1,4,2}/*.log; do
    mv "${file}" "${file//text/replacement}"
 done
done
3

I don't think you can iterate over multiple things in parallel in bash.

I would start out i=1. then iterate over your list of files and include let i=$i+1 in the loop. Also include a test to break out of the loop if $i -ge 736 if you want to quit iterating over whatever your input is after that number of loops.

i=0
for file in ./{01..10}/*jpg; do
    let i=$i+1
    mv "$file" "$i-$file"
done

If you were trying to quite after X number of files you could use a test like this:

[[ $i -ge 736 ]] && return || let i=$i+1
-4

bash supports arrays:

http://tldp.org/LDP/abs/html/arrays.html

OR

COUNT=0
for i in `ls *.txt`
do
  COUNT=`expr $COUNT + 1`
  mv "$i" "$COUNT-$i"
done

This would rename files a.txt b.txt c.txt to 1-a.txt 2-b.txt 3-c.txt.

7
  • Can you give a demonstration of how arrays could make this particularly scripting issue easier / faster / more elegant?
    – Caleb
    Aug 23, 2011 at 14:56
  • 5
    Don't parse the output of ls like that! The shell globing will handle that correctly, using ls will break it!
    – Caleb
    Aug 23, 2011 at 15:16
  • 1
    If you're using bash, this approach to incrementing a variable can be done within the shell: (( COUNT++ )) Aug 23, 2011 at 15:46
  • 1
    The reported example is one of the reason to stay away from ABS.
    – enzotib
    Aug 23, 2011 at 19:37
  • 2
    No, Caleb is right. Don't parse the output of ls. It doesn't work. Also, unless you're running on an antique, use shell arithmetic, you don't need expr any more. Aug 23, 2011 at 22:29

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .