11

I am new to shell scripting. so kindly bear with me if my doubt is too silly.

I have png images in 2 different directories and an executable which takes an images from each directory and processes them to generate a new image.

I am looking for a for loop construct which can take two variables simultaneously..this is possible in C, C++ etc but how do I accomplish something of the following. The code is obviously wrong.

#!/bin/sh

im1_dir=~/prev1/*.png  
im2_dir=~/prev3/*.png
index=0

for i,j in $im1_dir $im2_dir  # i iterates in im1_dir and j iterates in im2_dir 
do
  run_black.sh $i $j  
done

thanks!

4
  • Are you looking for the "cartesian product"? For example, if directory 1 has files A B C and directory 2 has files a b c, you are looking for the 9 output files (Aa, Ab, Ac, Ba, Bb, Bc, Ca, Cb, Cc) right? Commented Jun 26, 2012 at 20:05
  • 2
    @jedwards: I'm thinking the OP wants Aa Bb Cc rather than a Cartesian product. Commented Jun 26, 2012 at 20:12
  • @DennisWilliamson: me too, but thinking about it too much made me unsure Commented Jun 26, 2012 at 20:14
  • I am sorry if my question was ambiguous. but i am not looking for the cartesian product. Just image file names from two different directories. Commented Jun 26, 2012 at 20:23

8 Answers 8

18

If you are depending on the two directories to match up based on a locale sorted order (like your attempt), then an array should work.

im1_files=(~/prev1/*.png)
im2_files=(~/prev3/*.png)

for ((i=0;i<=${#im1_files[@]};i++)); do
   run_black.sh "${im1_files[i]}" "${im2_files[i]}"
done
Sign up to request clarification or add additional context in comments.

7 Comments

Thanks. the images in the two directories are sorted yes.. this could work but I get a syntax error..."Syntax error: "(" unexpected"....should the round brackets be there? and without it "Syntax error: Bad for loop variable"
sorry i mean brackets around ~/prev1/*.png
@user1126374 - That is because your /bin/sh is not bash. Change the shebang to #!/bin/bash
@ jordanm Thanks a lot..yes I realised that
Should not it be less than ("<") and not equal or less than ("<=")? It seems to me it makes one cycle mor ethan necessary.
|
7

Here are a few additional ways to do what you're looking for with notes about the pros and cons.

The following only works with filenames that do not include newlines. It pairs the files in lockstep. It uses an extra file descriptor to read from the first list. If im1_dir contains more files, the loop will stop when im2_dir runs out. If im2_dir contains more files, file1 will be empty for all unmatched file2. Of course if they contain the same number of files, there's no problem.

#!/bin/bash
im1_dir=(~/prev1/*.png) 
im2_dir=(~/prev3/*.png)

exec 3< <(printf '%s\n' "${im1_dir[@]}")

while IFS=$'\n' read -r -u 3 file1; read -r file2
do
    run_black "$file1" "$file2"
done < <(printf '%s\n' "${im2_dir[@]}")

exec 3<&-

You can make the behavior consistent so that the loop stops with only non-empty matched files no matter which list is longer by replacing the semicolon with a double ampersand like so:

while IFS=$'\n' read -r -u 3 file1 && read -r file2

This version uses a for loop instead of a while loop. This one stops when the shorter of the two lists run out.

#!/bin/bash
im1_dir=(~/prev1/*.png) 
im2_dir=(~/prev3/*.png)

for ((i = 0; i < ${#im1_dir[@]} && i < ${#im2_dir[@]}; i++))
do
    run_black "${im1_dir[i]}" "${im2_dir[i]}"
done

This version is similar to the one immediately above, but if one of the lists runs out it wraps around to reuse the items until the other one runs out. It's very ugly and you could do the same thing another way more simply.

#!/bin/bash
im1_dir=(~/prev1/*.png) 
im2_dir=(~/prev3/*.png)

for ((i = 0, j = 0,
          n1 = ${#im1_dir[@]}, 
          n2 = ${#im2_dir[@]}, 
          s = n1 >= n2 ? n1 : n2, 
          is = 0, js = 0; 

      is < s && js < s; 

      i++, is = i, i %= n1, 
          j++, js = j, j %= n2))
do
    run_black "${im1_dir[i]}" "${im2_dir[i]}"
done

This version only uses an array for the inner loop (second directory). It will only execute as many times as there are files in the first directory.

#!/bin/bash
im1_dir=~/prev1/*.png
im2_dir=(~/prev3/*.png)

for file1 in $im1_dir
do
    run_black "$file1" "${im2_dir[i++]}"
done

3 Comments

while IFS=$'\n' read -r -u 3 file1; read -r file2 do run_black "$file1" "$file2" done < <(printf '%s\n' "${im1_dir[@]}") isn't it supposed to done < <(printf '%s\n' "${im2_dir[@]}")
@cantikmemekmu: Yes! Thanks for catching that after almost 11 years! I've corrected that in my answer.
Wouldn't file descriptor definition simply: IFS=$'\n'; exec 3< <(ls ~/prev1/*.png) work? And : Is yours capable of process file name contains space ?
4

If you don't mind going off the beaten path (bash), the Tool Command Language (TCL) has such a loop construct:

#!/usr/bin/env tclsh

set list1 [glob dir1/*]
set list2 [glob dir2/*]

foreach item1 $list1 item2 $list2 {
    exec command_name $item1 $item2
}

Basically, the loop reads: for each item1 taken from list1, and item2 taken from list2. You can then replace command_name with your own command.

1 Comment

can you use tclsh for just a section of code in a src file? I wonder how that would look....
1

The accepted answer can be further simplified using the ${!array[@]} syntax to iterate over array's indexes:

a=(x y z); b=(q w e); for i in ${!a[@]}; do echo ${a[i]}-${b[i]}; done

Comments

0

Another solution. The two lists with filenames are pasted into one.

paste <(ls --quote-name ~/prev1/*.png) <(ls --quote-name ~/prev3/*.png) | \
while read args ; do
  run_black $args
done  

Comments

0

This might be another way to use two variables in the same loop. But you need to know the total number of files (or, the number of times you want to run the loop) in the directory to use it as the value of iteration i.

Get the number of files in the directory:

ls /path/*.png | wc -l

Now run the loop:

im1_dir=(~/prev1/*.png) 
im2_dir=(~/prev3/*.png)

for ((i = 0; i < 4; i++)); do run_black.sh ${im1_dir[i]} ${im2_dir[i]}; done

For more help please see this discussion.

Comments

0

I have this problem for a similar situation where I want a top and bottom range simultaneously. Here was my solution; it's not particularly efficient but it's easy and clean and not at all complicated with icky BASH arrays and all that nonsense.

SEQBOT=$(seq 0  5  $((PEAKTIME-5)))
SEQTOP=$(seq 5  5  $((PEAKTIME-0)))

IDXBOT=0
IDXTOP=0

for bot in $SEQBOT; do
    IDXTOP=0
    for top in $SEQTOP; do
        if [ "$IDXBOT" -eq "$IDXTOP" ]; then
            echo $bot $top
        fi
        IDXTOP=$((IDXTOP + 1))
    done
    IDXBOT=$((IDXBOT + 1))
done

Comments

0

It is very simple you can use two for loop functions in this problem.

#bin bash
index=0
for i in ~/prev1/*.png  
do
    for j ~/prev3/*.png
    do 
        run_black.sh $i $j
    done
done

1 Comment

wont this double the value ? like the ~/prev1/*.png and ~/prev3/*.png will come out in all combinations

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.