Script to Organize Desktop into Folders/Directories by Extension

6,874

Solution 1

If what you want is a directory per file type, then you want something more like:

mkdir txt || { echo "Can't mkdir txt" ; exit 1 ; }
for file in *.txt ; do
    mv -- "$file" txt/
done

Note the failure handling on mkdir. In your code, if there is already a file with the name contained in the variable $dir, your mkdir --"$dir" command will silently fail, and then your mv command will overwrite that file.


To handle a bunch of arbitrarily named files—all the files with three-character extensions in the current directory—and put them in directories by file extension, you could use the following code (untested):

for file in *.??? ; do
    [ -f "$file" ] || continue
    dir="$(echo "$file" | rev | cut -c-3 | rev)"
    mkdir -p "$dir" || { echo "Couldn't mkdir -p $dir; exiting" ; exit 1 ; }
    mv -- "$file" "$dir"
done

Explanation:

The pattern *.??? will match all files and directories in the current directory whose name ends in a period (.) followed by exactly three characters.

The test [ -f "$file" ] returns true only if $file is a regular file (not a directory or other more specialized type like a device.) Otherwise, the continue statement will skip the rest of that pass through the for loop.

The assignment to the dir variable looks fancy but really it's just taking the contents of the $file variable, reversing it, extracting the first 3 characters (of the reversed string), then reversing that again, and assigning the result as the value of $dir. In other words it's the last three characters of the filename.

Then, mkdir -p will return success if the directory already exists, or will create it if it doesn't (and return success). If it doesn't return success, you don't want to try to move files into it! Hence the error handling.

Then, the mv command just like normal.


A note about error handling:

You might see constructs like this:

mkdir -p "$dir" || echo "Failed, exiting" && exit 1

This is imperfect because the exit command is only executed if the echo command succeeds after the mkdir command fails. What you want instead is to have the exit command executed regardless of whether the echo command succeeds, assuming that mkdir failed. This is why I use the curly braces as above.

Solution 2

If what you want is a directory per file type, then you want something more like:

for file in *.*[!.]
do
    ext=${file##*.}
    mkdir -p -- "$ext"  &&  mv -- "$file" "$ext"/
done

Unlike other operating systems, in Unix, *.* means, not all files, but all files whose names contain at least one . (dot, a.k.a. period), thus omitting files with names like txt, todolist and oddball.  (To confuse matters, shell wildcards exclude files whose names begin with a . unless the dotglob option is set.)  *.*[!.] (or, equivalently, *.*[^.]) further restricts this to files whose names contain at least one . but do not have . as the last character, thus omitting files with names like oddball. and odd.ball..  AFAICT, this is a good characterization of files whose names have a non-null extension.

ext=${file##*.} extracts that extension, by deleting everything up to and including the last dot.  If the above analysis is correct, this should be non-null.  The -p option causes mkdir not to fail if the argument already exists (and is a directory).  If you have plain files with names like txt, pdf, and jpg, and also files with names like shopping_list.txt (which will yield ext=txt), or in some other edge cases (e.g., you don’t have write permission in the current directory, or there’s no space left in the filesystem), the mkdir will fail and will issue its own diagnostic message.  AND, because of the && (as suggested by @Wildcard), the following mv will not be attempted.

The / at the end of the mv command is a safety net to prevent the clobbering of plain files with names like txt, pdf, and jpg.

    mv -- "$file" "$ext"/

will simply fail if the destination name exists as a plain file (or any file type other than a directory).

Share:
6,874

Related videos on Youtube

systemfive
Author by

systemfive

Linux, Bash, Python, Ansible, Docker, PHP

Updated on September 18, 2022

Comments

  • systemfive
    systemfive over 1 year

    I'd like to organize desktops by running a script that places all of your files into folders by extension. I found some code through googling and the code I found creates a folder with the name of the file and places the file in said folder. I don't want a folder per file name... I want one folder to have all .txt, another for .jpg... etc. I use OS X.

    Here's what I have... any direction would be great, thanks!

    for file in *.txt
        do
            dir="${file%.txt}"
            mkdir -- "$dir"
            mv -- "$file" "$dir"
    done
    
  • gardenhead
    gardenhead over 8 years
    Wow thanks for the tip on error handling! That's something I hadn't thought about.
  • Wildcard
    Wildcard over 8 years
    The assignment of ext here is simple to look at, but will fail (badly) on some edge cases. For instance, a filename that ends in a . will cause ext to be set to a null string, and both mkdir and mv will fail silently. (With mv -- "$file" "$ext"/ it will fail unless you have permission to write to /, which you hopefully don't when you run this script.) (cont'd)
  • Wildcard
    Wildcard over 8 years
    Also consider odd "extensions" such as a filename birthday party.todolist. If you also have a file called todolist, it would be silently overwritten by your birthday party.todolist. Further, if you have a whole bunch of files.todolist, all of them will disappear except for the last one, which will be renamed as todolist. Always test for error conditions.
  • G-Man Says 'Reinstate Monica'
    G-Man Says 'Reinstate Monica' over 8 years
    OK, good point about filenames ending with ., but what do you mean “mv will fail silently”?  A command like mv oddfile. "" fails with “mv: cannot move ‘oddfile.’ to ‘’: No such file or directory”.  And I’ve already acknowledged that a plain file called txt causes problems.  What does todolist add to the discussion?
  • G-Man Says 'Reinstate Monica'
    G-Man Says 'Reinstate Monica' over 8 years
    Not everything that needs to be done, needs to be done well.  Absolutely, if you’re writing a polished gem of a script to install on your system for future use, and to distribute to other people, it should include error checking.  This is the sort of thing that might be a one-off situation.  In such a situation, it might be good enough to run the script interactively and look for the error message(s) from mkdir and mv.
  • systemfive
    systemfive over 8 years
    Question: Does this script only group extensions with 3 letters? "???" What about extensions with "????" like .html?
  • Wildcard
    Wildcard over 8 years
    Sorry, rather than "fail silently" I should have said "fail without preventing bad stuff". I do take your meaning about one-off situations—my comments are the sort of info re robustness that I loved to find on this site when I was first learning bash scripting. (Pay it forward.... ;)
  • Wildcard
    Wildcard over 8 years
    @JovanHernandez, yes, I designed it to only handle 3-letter extensions, just because I like my code to be fairly explicit. You could adjust it to handle 4-letter extensions only (by adding a ? to the glob pattern and fixing the cut -c-3 command) or handle both 3 and 4 letters by running two separate for loops in sequence. If you want to handle all extensions of any length, use G-Man's answer. (See my further comments below his answer.)