Zsh case-insensitive globbing

case sensitivitywildcardszsh

Context: macOS Catalina (zsh)

This script is to process all JPEG files. This script does not process .JPG files, however it does process .jpg files.

top=/Users/user/Desktop/
for file in $top/**/*.jp*g(NDn.); do      #selects filetypes: .jpg .jpeg 
  mogrify -auto-orient \
          -gravity northWest \
          -font "Arial-Bold-Italic" \
          -pointsize 175 \
          -fill red \
          -annotate +30+30 $n \
          -- $file &&
  echo  $file "was watermarked with" $n | tee -a forLooplog.txt
  (( n++ ))
done

How can the second line be modified to be case insensitive and trap .JPG .JPEG files?

Best Answer

Especially for your case where the glob is $top/**/*.jpg, I would not turn the caseglob option off (same as turning nocaseglob on¹) globally, as that affects all path components in glob patterns:

$ top=a zsh +o caseglob -c 'print -rC1 --  $top/*.jpg'
a/foo.jpg
a/foo.JPG
a/FOO.jpg
a/FOO.JPG
A/foo.jpg
A/foo.JPG
A/FOO.jpg
A/FOO.JPG

See how it did find all the jpg and JPG files in $top (a), but also the ones in an unrelated directory (A) which just happened to have the same name but in upper case. Even if you don't have such directories, zsh will still look for them which means it needs to list the contents of every directory that makes up the components of $top making that glob expansion more costly.

IMO, that nocaseglob option is better left forgotten. It was only added to zsh for compatibility with bash², and there most likely added to make the life of users of systems like Cygwin / macos that have case insensitive filesystem APIs easier.

Instead, I'd used the (#i) glob operator (with extendedglob) where you can specify which part of which glob should be case insensitive (similar to the ~(i) of ksh93):

set -o extendedglob # needed for (#i)
for file in $top/**/*.(#i)jp(e|)g(NDn.); do

Or you can always do:

for file in $top/**/*.[jJ][pP]([eE]|)[gG](NDn.); do

as you would in sh or any shell without case-insensitive glob operators.

Also note that *.jp(|e)g instead of *.jp*g which would match on filenames such as my.jpeg.import.log for instance.


¹ or CASEGLOB, CASE_GLOB, C_A_se_G_lob, case and underscores are ignored in option names, and the support of no to turn an option off is to try and accommodate the mess with POSIX sh options (and other shells' including zsh itself) where some options are named with a no prefix and some without for no immediately obvious reason.

² though the bash behaviour is different (and preferable IMO, at least on systems with case sensitive file names) in this instance in that a/*.jpg would only find jpg/JPG files in a, not A as it only does case insensitive matching for path components that do have glob operators ([a]/*.jpg would also find the jpg/JPG files in A).

Related Question