5

I wrote a script that converts images in a folder. The script uses a for loop:

i="1"
for file in *.jpg; do
     outputFile=$(echo "image"$(echo $i))
     convert "$file" -resize 50x50 $outputFile
     i=$[i+1]
done

What I want to do is allow the script to run on multiple file type extensions. Now I've tried doing:

for file in *.jpg *.JPG *.jpeg; do
....
done

The problem I have with this is that if you are in a folder with all *.JPG and no *.jpg, the script still bumps i+1 even though there was no images, because it runs the for loop anyways, upon not finding any *.jpg it goes onto *.JPG.

How can I target multiple file extensions without it running itself per type? Example, is their syntax something like this:

for file in [*.jpg | *.JPG]; do
...

?
That way my output folder always contains images labeled like this:
image1.jpg
image2.jpg
image3.jpg
etc..

Instead of ending up with a folder missing image1.jpg because it had no *.jpg to run on first.

1
  • ...but what about to use an if [ -e "$file" ]...fi and execute conversion and increment only when the file exists? (You can use other tests to check for link etc etc). It should protect even in case you have a directory named Jpg. – Hastur Feb 5 '16 at 10:09
7

The Solution

Use nullglob. Put the following line before the for loop:

shopt -s nullglob

nullglob means that if there is no such file, the glob is removed from the list. Observe without nullglob:

$ echo  *.jpg *.JPG *.jpeg
test.jpg *.JPG *.jpeg

Now, with nullglob:

$ shopt -s nullglob
$ echo  *.jpg *.JPG *.jpeg
test.jpg

Revised Script

The complete script could be:

i=1
shopt -s nullglob
for file in *.jpg *.JPG *.jpeg; do
     convert "$file" -resize 50x50 "image$i.jpg"
     i=$((i+1))
done

Three notes:

  1. In the definition of outputFile, all those echo statements were unnecessary.

  2. Following convention, I added an extension, .jpg, to your output file name. If you really don't want the extension, just remove it.

  3. I replace $[...] with its more standard form: $((...)).

1
  • 1
    A more reliable way to find all JPEGs in the current directory: file * | grep 'JPEG image data' | cut -d: -f1 – Dummy00001 May 9 '15 at 13:25
3
  1. Since you want to look for multiple distinct (case insensitive), the shopt -s nullglob answer is a very good one to use.
  2. If you were simply looking for *.jpg, *.JPG, and the other six various combinations of upper case and lower case, the easiest thing to do would be

    for file in *.[Jj][Pp][Gg]
    

    which looks for files whose names are

    • (something), followed by
    • . (a period), followed by
    • J or j, followed by
    • P or p, followed by
    • G or g

    in other words,

    • (something), followed by
    • . (a period), followed by
    • "jpg", in some combination of upper case and lower case
  3. So one way to solve your problem is

    shopt -s nullglob
    for file in *.[Jj][Pp][Gg] *.[Jj][Pp][Ee][Gg]
    do …
    
  4. The […] trick is a good one to know, but there a better way.  If I told you that there was a shell option called nocaseglob, could you guess what it does?

    shopt -s nullglob nocaseglob
    for file in *.jpg *.jpeg
    do …
    

    so now *.jpg means

    • (something), followed by
    • . (a period), followed by
    • "jpg", in some combination of upper case and lower case

    and so the above command will match *.jpg, *.JPG, *.Jpg, *.jpeg, *.jPeG, etc.

  5. Another way is to use the extglob shell option to enable extended pattern matching operators.  You'll still want nullglob and nocaseglob.  The extended pattern matching operators all look like

    (some_special_character)  (  pattern-list  )

    where spaces are added for clarity, and pattern-list is a list of patterns separated by | character(s).  The one that looks most interesting for your purposes is

    @(pattern-list)

    which means match one of the listed patterns.  So now the script becomes

    shopt -s nullglob nocaseglob extglob
    for file in *.@(jpg|jpeg)
    do …
    

    This may look like a little more typing, but the tradeoff shifts as the number of extensions grows.  Consider,

    for file in *.@(jpg|jpeg|jfif|exif|tif|tiff|gif|bmp|png|svg)
    

    is a bit shorter than

    for file in *.jpg *.jpeg *.jfif *.exif *.tif *.tiff *.gif *.bmp *.png *.svg
    
  6. Where did you learn to do $(echo …)?  Sure, $(some_command(s) …) can certainly be useful, and maybe even $(echo …) sometimes, but you're overusing it.  It's perfectly good to say

    outputFile="image$i"
    

    and you probably don't even need the quotes (in the assignment statement).

  7. OK, you can be excused just this once, since you know that the value of outputFile is benign, since you set it from a constant string of non-blank, non-special characters (image) plus a variable which you set to a constant string of non-blank, non-special characters (1) and did only trivial arithmetic operations on it.  But, as a general rule, you should always quote all reference to shell variables, unless you have a very good reason not to and you're sure you know what you're doing.  So your convert command, ideally, should be

    convert "$file" -resize 50x50 "$outputFile"
    

    (with quotes around $outputFile.)

-1

You would want to replace this part:

for file in *.jpg; do

By this:

for file in $(ls *.jpg *.JPG); do
1

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.