I'm trying to tidy-up my photos which are, for various historic reasons, scattered all over my system. To enable me to make a start on this task, I've been trying to use the command line to construct a list of all directories that contain one or more jpg files. I'm certain that I don't have to be concerned about looking for other image file formats, but I do have to allow for jpg appearing in upper and lower case.

I'd like each directory name to appear only once in the final list. To provide an example, if I have the following directories each of which contain one or more jpg or JPG files....

~Mike/Family History/Swaine

I'd like the results to appear with each directory listed only once - irrespective of the number of image files it might contain - preferably sorted and then written to a file

~Mike/Family History/Swaine

My command line skills are just not up to this! I can use a lot of the simpler forms of single commands, but once they get complex and/or have to be piped things tend to go wrong.


Assuming JPEG image files have the suffix .jpg:

find "$HOME" -type f -name '*.jpg' \
    -exec sh -c 'for d; do dirname "$d"; done' sh {} + | sort -u -o jpeg_dirs.txt

This relies on you not having funky directory names with newlines in their names.

With GNU find:

find "$HOME" -type f -name '*.jpg' -printf '%h\n' | sort -u -o jpeg_dirs.txt

These find commands will find all JPEG images under your home directory and print the names of the directories where they were found. The sort -u will take this list of directory names, sort it, and remove duplicates. The result will be written to the file jpeg_dirs.txt in the current directory.

  • It needs to look for '*.JPG' as well - he has files with both the upper and lower case extension. – NickD Sep 10 '17 at 18:42
  • @Nick Use -iname instead of -name. -iname does case-insensitive matching. – Kusalananda Sep 10 '17 at 18:43
  • 2
    @Nick use \( -iname '*.jpg' -o -iname '*.jpeg' \) to match on several suffixes. – Kusalananda Sep 10 '17 at 18:44
  • -exec … ; can be slow. -exec sh -c 'for x do dirname "$x"; done' sh {} + is faster and portable. -exec dirname {} + works with GNU dirname. – Gilles Sep 10 '17 at 20:59
  • @Kusalananda Thanks for your suggestion. It works well, but I've found that I do also need to check for directories containing files with the jpeg extension. I've tried numerous versions, but I can't get the bit to handle either file extension to work.... – Midahed Sep 10 '17 at 21:53

A simple way is to list all the .jpg files, then strip off the base names of the files (the part after the final slash), and remove duplicates. You can use sed to strip the part of each line after the final slash. There's a command to remove duplicates, which is called uniq, but it assumes sorted input; if you're need to sort anyway, you can let sort do the uniquification.

find ~Mike -iname '*.jpg' | sed 's!/[^/]*$!!' | sort -u >directories_with_jpeg_files.txt

This assumes that none of the directories or files involved have a newline in their name. File names with newlines do not appear in normal circumstances, but do beware if the file names may have been chosen by a hostile person (e.g. if you're processing files that have been uploaded to a server and the uploader can choose the file name).

If there are directories containing a lot of JPEG files and not many directories containing no JPEG file, this method spends a lot of time reporting then redundant files. There is no way to tell find to shortcut a directory once it's found something in it. But you can restrict find to directories and tell it to search for a JPEG file in each directory. This increases the cost for directories that don't contain JPEG files, however, so it can have poor performance if there are many JPEGless directories.

find ~Mike -type d -exec sh -c '
    for d do
      set -- "$d/*.[Jj][Pp][Gg]";
      if [ -e "$1" ]; then printf %s\\n "$d"; fi
' sh {} + | sort -u >directories_with_jpeg_files.txt

Alternatively, in zsh, you can use the ** wildcard to traverse directories recursively, (#i) to match the following path component case-insensitively to make the pattern **/(#i)*.jpg matching *.jpg and *.JPG (and .Jpg and so on) in a whole directory tree. Add the history modifier h in a glob qualifier to extract the directory part. Stuff this into an array variable dirs=(…) and extract the unique elements of this array with the u parameter expansion flag.

set -o extendedglob # for (#i); best in ~/.zshrc
print -lr -- ${(u)dirs} >directories_with_jpeg_files.txt

The equivalent of the check-per-directory method above is to use the e glob qualifier.

print -lr ~Mike/**/*(/e\''set -- $REPLY/*.(#i)jpg(N[1]); (($# != 0))'\') >directories_with_jpeg_files.txt
  • See also ()(($#)) glob(N) in zsh to test if a glob has any match. – Stéphane Chazelas Sep 10 '17 at 21:59
find . -iname '*.jpg' -execdir sh -c 'pwd' _ {} + | sort -u > dirs_with_jpegs.txt

Ought to work well enough assuming your implementation of find supports -execdir (it probably does). -execdir executes a command in the directory where the found file is. In this case we execute the command pwd, which prints the name of the directory. We wrap the command with sh -c to strip arguments. (Some (all?) implementations of find require the {} argument substitution, which would be the list of jpeg files in the current directory. We want to ignore that list and only print the directory.)

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.