Nothing particularly wrong with your script (except maybe that if the first find
fails, it will not be reported in the exit status of your script¹).
But since you're using zsh
, there are possible ways to improve it or make it more zsh-like. For instance using zmv
which is zsh's own batch-renaming tool (implemented as an autoloadable function):
#! /bin/zsh -
autoload zmv
zmodload zsh/files # for builtin mv to speed things up as zmv
# is going to run one mv per file below
zmv '**/(*).(#i)(jpg|png)(#qD.)' '$HOME/$2:l/$1'
zmv
does do some sanity checks, to avoid losing data (overwriting files either because the targets already existed or because two files resolve to the same target) before starting doing any renaming.
**/
(for recursive globbing) matches any level of subdirectories. Just remove it if you only want to move files in the current directory.
The (#q...)
introduces glob qualifiers with D
being to not skip dotfiles and .
to restrict the match to regular files (exclude directories, symlinks, fifos...). You could also add the oN
qualifier to N
ot o
rder the list of files as a slight performance optimisation.
(#i)
enables case insensitive matching.
$2:l
converts $2
to lowercase. That's using a (t)csh style modifier (though in tcsh
, you'd need $2:al
). In zsh, you can also use a parameter expansion flag (${(L)2}
).
Instead of capturing (using (
...)
) the root and extension of the base names in $1
and $2
, we could also use csh
-style $f:t:r
, (root of tail) $f:e
(extension) on the whole file path (which zmv
makes available in the $f
variable):
zmv '**/*.(#i)(jpg|png)(#qD.)' '$HOME/$f:e:l/$f:t:r'
With find
and gmv
, you can also do it in one find
invocation to avoid crawling the directory twice:
find . -type f '(' -name '*.[jJ][pP][gG]' -exec gmv -it ~/jpg {} + -o \
-name '*.[pP][nN][gG]' -exec gmv -it ~/jpg {} + ')'
(or use -iname
if your find
implementation supports that non-standard extension. Add ! -name . -prune
before -type f
to stop recursing; some find
implementations also support -mindepth 1 -maxdepth 1
for that, the -mindepth 1
being not necessary here as .
(at depth 0) does not match the patterns anyway).
Without GNU mv
, you could use sh
to rearrange the arguments for the target directory to be last:
find . -type f '(' -name '*.[jJ][pP][gG]' -exec sh -c '
exec mv -i "$@" ~/jpg/' sh {} + -o \
-name '*.[pP][nN][gG]' -exec sh -c '
exec mv -i "$@" ~/png/' sh {} + ')'
(the -type f
being the equivalent of zsh
's .
glob qualifier above).
The only sanity check there is done with -i
for interactive, it is not done in advance.
To build that command based on a list of extension + target-dir, you could do:
cmd=( find . -type f '(' ) or=()
for ext dir (
jpg ~/jpg
jpeg ~/jpg
png ~/png
aif ~/aiff
) cmd+=($or -iname "*.$ext" -exec gmv -it $dir {} +) or=(-o)
cmd+=(')')
$cmd
With the zmv
approach:
typeset -A map=(
jpg ~/jpg
jpeg ~/jpg
png ~/png
aif ~/aiff
)
zmv "**/(*).(#i)(${(kj[|]map})(#qD.)" '$map[$2:l]/$1'
Using a $map
associative array to map extensions to target directory. ${(kj[|])map}
j
oins the k
eys of the associative array with |
s. We switch to double quotes for that to be expanded into the pattern argument that is passed to zmv
.
¹ another potential problem would arise if there were file of type directory that matched those patterns or worse things like dir.png/subdir.png/file.png
, you'd want to skip those with ! -type d
or even go further restrict to regular files only as I show here.
find
on macos has this, but withGNU find
instead of-name
you can use-iname
for case insensitive matching. – fuzzydrawrings Mar 21 '22 at 03:12-iname
works on macOS. That's a perfect solution to that aspect of this script. Now JPEG/JPG both with-iname
is probably the only one I'll need to list two specific spellings for. Thanks – user69136 Mar 21 '22 at 03:23find
) are based on the BSD-flavor of Unix. I've found that all of the native BSD-based tools are different than their Linux equivalents, and most of them are sadly out-of-date. InstallingMacPorts
will give you access to current and maintained tools with a far larger user-base, and access to far more support resources. – Seamus Mar 21 '22 at 03:48find
, if you don't want to include subfolders, you can add the flag-maxdepth 1
and it will search only in the current folder, not subfolders. – frabjous Mar 21 '22 at 04:21
– user69136 Mar 21 '22 at 04:55find . -maxdepth 1 -iname '*.aif' -exec gmv -n --target-directory=$HOME/aiff '{}' +
absolutely decimates a neglected downloads folder in no time.... especially if you can load up the script with enough extensions, it really works wonders.mv -n *.png $HOME/png
. – Michael Homer Mar 21 '22 at 04:59find
was recursive, since the question that you linked to was specifically about specifically about searching a directory tree. – G-Man Says 'Reinstate Monica' Mar 21 '22 at 16:00