You can use the find
command below to recursively (depth-first) move all folders into a folder named after the first letter of their file name, creating it if it does not exist yet, starting from your current working directory:
find . -depth -mindepth 1 -type d -execdir bash -c 'l=./$(basename "{}" | head -c1) ; test -d "${l^^}" || mkdir "${l^^}" ; mv "{}" "${l^^}/"' \;
Explanation:
find . -depth -mindepth 1 -type d -execdir <COMMAND> \;
recursively searches your current directory (.
), depth-first (-depth
), for all directories (-type d
), excluding your current directory itself (-mindepth 1
).
Then it cd
s into each found folder's parent directory and executes <COMMAND>
from there, replacing every occurrence of {}
with the relative found directory name (-execdir
).
bash -c '<COMMAND>'
starts a Bash subshell and lets it interpret and execute the string <COMMAND>
. We need this because we're going to use shell features like variables, pipes, command substitution, etc, which can't be done directly by find -execdir
.
l=./$(basename "{}" | head -c1)
stores the first letter of the found directory name (remember that {}
gets substituted with the search result) in the shell variable $l
.
test -d "${l^^}" || mkdir "${l^^}"
checks whether a directory exists and creates it if not. The name of the directory ("${l^^}"
) is the content of the variable $l
(the first letter of the found directory name) converted to upper case.
mv "{}" "${l^^}/"
moves the found directory (remember again that {}
gets substituted) into the directory named after its first letter converted to upper case (`"${l^^}/", which we prepared earlier).
Example run:
$ tree -F
.
├── apple/
│ ├── hamster/
│ ├── horse/
│ ├── snake/
│ │ ├── blue
│ │ ├── green
│ │ ├── grey
│ │ └── red
│ └── spider/
├── apricot/
├── banana/
├── cherry/
│ ├── pink
│ ├── purple
│ └── yellow
└── coconut/
9 directories, 7 files
$ find . -depth -mindepth 1 -type d -execdir bash -c 'l=./$(basename "{}" | head -c1) ; test -d "${l^^}" || mkdir "${l^^}" ; mv "{}" "${l^^}/"' \;
$ tree -F
.
├── A/
│ ├── apple/
│ │ ├── H/
│ │ │ ├── hamster/
│ │ │ └── horse/
│ │ └── S/
│ │ ├── snake/
│ │ │ ├── blue
│ │ │ ├── green
│ │ │ ├── grey
│ │ │ └── red
│ │ └── spider/
│ └── apricot/
├── B/
│ └── banana/
└── C/
├── cherry/
│ ├── pink
│ ├── purple
│ └── yellow
└── coconut/
14 directories, 7 files
Edit:
If you want it non-recursively, i.e. operating only on the direct first-level subdirectories of your current working directory, simply add the -maxdepth 1
option to the find
command. You can omit the -depth
option then though:
find . -mindepth 1 -maxdepth 1 -type d -execdir bash -c 'l=./$(basename "{}" | head -c1) ; test -d "${l^^}" || mkdir "${l^^}" ; mv "{}" "${l^^}/"' \;
Edit 2:
It is possible to only perform the move if the directory to move contains files matching a specific criterion (here: contain a file with file name ending in .jpg
anywhere, including subdirectories), but it adds another find
command and an if
clause:
find . -depth -mindepth 1 -maxdepth 1 -type d -execdir bash -c 'if find "{}" -type f -iname '*.jpg' | grep -q '.' ; then l=./$(basename "{}" | head -c1) ; test -d "${l^^}" || mkdir "${l^^}" ; mv "{}" "${l^^}/" ; fi' \;
Explanation (after edit 2):
if <CONDITION> ; then <COMMANDS> ; fi
Checks the exit code of the command <CONDITION>
and only runs the <COMMANDS>
between then
and fi
if the condition was fulfilled (exit code 0). <COMMANDS>
is the same as in the previous version.
find "{}" -type f -iname '*.jpg' | grep -q '.'
Searches inside the directory found by the outer find
command (remember once again that {}
gets substituted) for files (-type f
) with a name that ends with .jpg
, case-insensitively (-iname '*.jpg'
).
The grep -q '.'
simply checks whether the find
produces any output on STDOUT (means that files were found) and returns an exit code of 0 then, 1 if no files were found.
Example run (after edit 2):
$ tree -F
.
├── apple/
│ ├── hamster/
│ ├── horse/
│ ├── snake/
│ │ ├── blue
│ │ ├── green
│ │ ├── grey
│ │ └── red
│ └── spider/
├── apricot/
├── banana/
│ └── black.jpg
├── cherry/
│ ├── pink
│ ├── purple
│ └── yellow
└── coconut/
9 directories, 8 files
$ find . -depth -mindepth 1 -maxdepth 1 -type d -execdir bash -c 'if find "{}" -type f -iname '*.jpg' | grep -q '.' ; then l=./$(basename "{}" | head -c1) ; test -d "${l^^}" || mkdir "${l^^}" ; mv "{}" "${l^^}/" ; fi' \;
$ tree -F
.
├── A/
│ └── apple/
│ ├── hamster/
│ ├── horse/
│ │ └── white.jpg
│ ├── snake/
│ │ ├── blue
│ │ ├── green
│ │ ├── grey
│ │ └── red
│ └── spider/
├── apricot/
├── B/
│ └── banana/
│ └── black.jpg
├── cherry/
│ ├── pink
│ ├── purple
│ └── yellow
└── coconut/
11 directories, 9 files
./apples/bananas/cherries
become./A/apples/bananas/cherries
or./A/apples/B/bananas/C/cherries
? And what about files - sort them too, or only directories?