How can I organize files based on their filename first letter into A-Z folders
Solution 1:
A late python option:
#!/usr/bin/env python3
import os
import sys
import shutil
def path(dr, f): return os.path.join(dr, f)
dr = sys.argv[1]
for f in os.listdir(dr):
fsrc = path(dr, f)
if os.path.isfile(fsrc):
s = f[0]; target = path(dr, s.upper()) if s.isalpha() else path(dr, "#")
if not os.path.exists(target):
os.mkdir(target)
shutil.move(fsrc, path(target, f))
How to use
- Copy the script into an empty file, save it as
move_files.py
-
Run it with the directory as argument:
python3 /path/to/move_files.py /path/to/files
The script will only create the (sub) directory(-ies) (uppercase) if it is actually needed
Explanation
The script:
-
lists the files, gets the first character (defines the sourcepath):
for f in os.listdir(dr): s = f[0]; fsrc = path(dr, f)
-
checks if the item is a file:
if os.path.isfile(fsrc):
-
defines the targeted folder for either if the first char is alpha or not:
target = path(dr, s.upper()) if s.isalpha() else path(dr, "#")
-
checks if the folder already exists or not, creates it if not:
if not os.path.exists(target): os.mkdir(target)
-
moves the item into its corresponding folder:
shutil.move(fsrc, path(target, f))
Solution 2:
Code-golfed yet readable with just two commands and two regular expressions:
mkdir -p '#' {a..z}
prename -n 's|^[[:alpha:]]|\l$&/$&|; s|^[0-9]|#/$&|' [[:alnum:]]?*
If you have a huge amount of files to move, too many to fit into the process argument list (yes, there's a limit and it may be just a few kilobytes), you can generate the file list with a different command and pipe that to prename
, e. g.:
find -mindepth 1 -maxdepth 1 -name '[[:alnum:]]?*' -printf '%f\n' |
prename -n 's|^[[:alpha:]]|\l$&/$&|; s|^[0-9]|#/$&|'
This has the added benefit of not trying to move the literal file name [[:alnum:]]?*
if no files match the glob pattern. find
also allows many more match criteria than shell globbing.
An alternative is to set the nullglob
shell option and close the standard input stream of prename
.1
In both cases remove the -n
switch to actually move the files and not just show how they would be moved.
Addendum: You can remove the empty directories again with:
rmdir --ignore-fail-on-non-empty '#' {a..z}
1shopt -s nullglob; prename ... <&-
Solution 3:
If you don't mind zsh, a function and a couple of zmv
commands:
mmv() {echo mkdir -p "${2%/*}/"; echo mv -- "$1" "$2";}
autoload -U zmv
zmv -P mmv '([a-zA-Z])(*.ttf)' '${(UC)1}/$1$2'
zmv -P mmv '([!a-zA-Z])(*.ttf)' '#/$1$2'
The mmv
function makes the directory and moves the file. zmv
then provides pattern-matching and substitution. First, moving filenames starting with an alphabet, then everything else:
$ zmv -P mmv '([a-zA-Z])(*.ttf)' '${(UC)1}/$1$2'
mkdir -p A/
mv -- abcd.ttf A/abcd.ttf
mkdir -p A/
mv -- ABCD.ttf A/ABCD.ttf
$ zmv -P mmv '([!a-zA-Z])(*.ttf)' '#/$1$2'
mkdir -p #/
mv -- 123.ttf #/123.ttf
mkdir -p #/
mv -- 七.ttf #/七.ttf
Run again without the echo
in mmv
's definition to actually perform the move.
Solution 4:
I didn't work out a nice way to make the directory names uppercase (or move the files with uppercase letters), although you could do it afterwards with rename
...
mkdir {a..z} \#; for i in {a..z}; do for f in "$i"*; do if [[ -f "$f" ]]; then echo mv -v -- "$f" "$i"; fi; done; done; for g in [![:alpha:]]*; do if [[ -f "$g" ]]; then echo mv -v -- "$g" \#; fi; done
or more readably:
mkdir {a..z} \#;
for i in {a..z}; do
for f in "$i"*; do
if [[ -f "$f" ]]; then
echo mv -v -- "$f" "$i";
fi
done
done
for g in [![:alpha:]]*; do
if [[ -f "$g" ]]; then
echo mv -v -- "$g" \#
fi
done
Remove echo
after testing to actually move the files
And then
rename -n 'y/[a-z]/[A-Z]/' *
remove -n
if it looks good after testing and run again.
Solution 5:
The following commands within the directory containing the fonts should work, if you want to use from outside the font storage directory, change for f in ./*
to for f in /directory/containing/fonts/*
. This is a very shell based method, so quite slow, and is also non-recursive. This will only create directories, if there are files which start with the matching character.
target=/directory/to/store/alphabet/dirs
mkdir "$target"
for f in ./* ; do
if [[ -f "$f" ]]; then
i=${f##*/}
i=${i:0:1}
dir=${i^}
if [[ $dir != [A-Z] ]]; then
mkdir -p "${target}/#" && mv "$f" "${target}/#"
else
mkdir -p "${target}/$dir" && mv "$f" "${target}/$dir"
fi
fi
done
As a one liner, again from within the font storage directory:
target=/directory/to/store/alphabet/dirs; mkdir "$target" && for f in ./* ; do if [[ -f "$f" ]]; then i=${f##*/}; i=${i:0:1} ; dir=${i^} ; if [[ $dir != [A-Z] ]]; then mkdir -p "${target}/#" && mv "$f" "${target}/#"; else mkdir -p "${target}/$dir" && mv "$f" "${target}/$dir" ; fi ; fi ; done
A method using find, with similar string manipulation, using bash parameter expansion, which will be recursive, and should be somewhat quicker than the pure shell version:
find . -type f -exec bash -c 'target=/directory/to/store/alphabet/dirs ; mkdir -p "$target"; f="{}" ; i="${f##*/}"; i="${i:0:1}"; i=${i^}; if [[ $i = [[:alpha:]] ]]; then mkdir -p "${target}/$i" && mv "$f" "${target}/$i"; else mkdir -p "${target}/#" && mv "$f" "${target}/#"; fi' \;
Or more readably:
find . -type f -exec bash -c 'target=/directory/to/store/alphabet/dirs
mkdir -p "$target"
f="{}"
i="${f##*/}"
i="${i:0:1}"
i=${i^}
if [[ $i = [[:alpha:]] ]]; then
mkdir -p "${target}/$i" && mv "$f" "${target}/$i"
else
mkdir -p "${target}/#" && mv "$f" "${target}/#"
fi' \;