我想要一个脚本来自动将所有文件移动到我已有的目录结构中(看起来像~/jpg
、~/png
、~/mp3
、~/zip
等等)。到目前为止,这几乎完全符合我的要求:
#!/bin/zsh
echo "Executing Script"
find . -iname '*.jpg' -exec gmv -i --target-directory=$HOME/jpg '{}' +
find . -iname '*.png' -exec gmv -i --target-directory=$HOME/png '{}' +
我没有 shell 脚本经验,所以这只是我拼凑出来的东西。
简单地在连续行上发出连续命令是为 shell 编写脚本的正确方法吗?我最初尝试过这个,mv
并且涉及错误处理,而且它并不优雅。我正在尝试扩展用户 EvilSoup 的回答在这里。
除了我已经提到的之外,我还包括了mv -i
标志这样我就不会覆盖任何已经存在的东西(这部分非常重要),但也许这-n
更好,我不太确定。
关于find
,我希望mv
操作仅发生在当前目录中,并且由于某种原因,find
似乎有点递归,尽管我不太明白如何限制它。我想在每个目录中运行我的脚本,并且只包含在当前工作目录中找到的mv
文件。find
+1 对于任何 zsh-on-macOS-具体的介绍性材料:shell 脚本。
答案1
你没有做错任何事。当您开始编写脚本时,第一个脚本只是一个接一个的命令列表,这是完全正常的。
但希望你能学会超越这一点。我并不是说你的剧本不好,但相比之下,我想向你展示我会写什么而不是你的剧本。我使用了条件、循环和安全检查,并且添加了一堆注释,以便您可以看到脚本正在做什么。
#!/usr/bin/env zsh
# announce that the script has started
echo "Running sorting script."
# the variable "$1" means the first argument to the script, i.e., if
# the script is named "mysorter.zsh" if you ran mysorter.zsh ~/tmp
# then $1 would be "~/tmp"
# I'm assigning that to the variable "argument"
argument="$1"
# if $argument is blank, I want the script to run in the present
# directory; if not, I want to move to that directory first
# [[ -n "$argument" ]] means "$argument is not blank" (not length 0)
# for other conditions like -n, see
# https://zsh.sourceforge.io/Doc/Release/Conditional-Expressions.html
if [[ -n "$argument" ]] ; then
# [[ -d "$argument" ]] checks if "$argument" is a directory; if it's
# not, the command is a mistake and the script should quit
# the "!" means "not"
if [[ ! -d "$argument" ]] ; then
# echo prints text; the ">&2" means print to the standard error
# stream, rather than regular output stream, because this is an
# error message
echo "$argument either does not exist or is not a directory" >&2
# exit quits the script; the number indicates the error code; if
# you write "exit 0" that means no error; but this is an error
# so I give it a non-zero code
exit 1
fi
# if we made it here, then "$argument" is indeed a folder, so I
# move into it
cd "$argument"
fi
# this is a loop that will be separately processed for all files in
# the active directory
for file in * ; do
# I indent inside the loop to indicate better where the loop starts
# and stops
# first I check if "$file" is a folder/directory; if it is,
# I don't want to do anything; "continue" means cease this cycle
# through the loop and move on to the next one
if [[ -d "$file" ]] ; then
continue
fi
# what I want to do next depends on the extension of "$file";
# first I will determine what that is
# the notatation "${file##*.}" means cut off everything prior to
# the final . ;
# see https://zsh.sourceforge.io/Doc/Release/Expansion.html#Parameter-Expansion
extension="${file##*.}"
# i want to treat extensions like JPG and jpg alike, so I will make
# the extension lowercase; see the same page above
extension="${(L)extension}"
# I want to move the file to a folder in $HOME named after the
# extension, i.e., $HOME/$extension; first I check if it doesn't
# exist yet
if [[ ! -d "$HOME/$extension" ]] ; then
# since it doesn't exist, I will create it
mkdir "$HOME/$extension"
fi
# by default we want the move target to have the same name
# as the original file
targetname="$file"
# but I may need to add a number to it; see below
num=0
# now I want to check if there is already a file in there with
# that name already, and keep changing the target filename
# while there is
while [[ -e "$HOME/$extension/$targetname" ]] ; do
# a file with that name already exists; but I still want
# to put the file there without overwriting the other one;
# I will keep checking numbers to add to the name until I
# find one for a file that doesn't exist yet
# increase by one
let num++
# the new targetname is filename with the extension cut off,
# plus "-$num" plus the extension
# e.g. "originalfilename-1.zip"
targetname="${file%.*}-${num}.${extension}"
done
# we have now found a safe file name to use for the target,
# so we can move the file
echo "Moving file $file to $HOME/$extension/$targetname"
# here we try to move the file, but if it fails, we
# print an error and quit the script with an error
if ! mv "$file" "$HOME/$extension/$targetname" ; then
echo "Move command failed!" >&2
exit 1
fi
# we're now done with this file and can move on to the next one
done
# done indicates the end of the loop
#
# if we got here everything was successful and quit with exit code 0
echo "All files successfully sorted."
exit 0
这只是为了让您了解什么是可能的。
不要担心你的脚本会立即完美。当你进行更多练习时,你会做得更好。如果这些脚本适合您,那么它们就适合您。
显然我添加了一些链接https://zsh.sourceforge.io这是了解更多有关 zsh 的好资源。
我不会添加任何特定于 mac 的内容,因为你应该尝试学习不是绑定到任何一个平台,特别是如果您可能预见自己会从专有平台转向开源平台并真正拥抱UNIX哲学(我尽力不去说教。这有效吗?)
答案2
您的脚本没有什么特别的问题(除非如果第一个 find
失败,则不会在脚本的退出状态中报告)。
但由于您正在使用zsh
,因此有可能的方法可以改进它或使其更像 zsh 。例如使用zmv
这是 zsh 自己的批量重命名工具(作为自动加载函数实现):
#! /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
在开始进行任何重命名之前,会进行一些健全性检查,以避免丢失数据(由于目标已存在或因为两个文件解析为同一目标而覆盖文件)。
**/
(为了递归通配符) 匹配任何级别的子目录。如果您只想移动当前目录中的文件,只需将其删除即可。
介绍(#q...)
了全局限定符不D
跳过点文件并将.
匹配限制为常规的文件(不包括目录、符号链接、fifos...)。您还可以将oN
限定符添加到文件N
列表o
中,作为轻微的性能优化。
(#i)
启用不区分大小写的匹配。
$2:l
转换$2
为小写。这是使用 (t)csh 风格修饰语(尽管在 中tcsh
,你需要$2:al
)。在 zsh 中,您还可以使用参数扩展标志( ${(L)2}
)。
我们还可以在整个文件路径上使用-style , (root of tail) (extension) (这使得在变量中可用),而不是捕获(使用(
... )和)
中基本名称的根和扩展名:$1
$2
csh
$f:t:r
$f:e
zmv
$f
zmv '**/*.(#i)(jpg|png)(#qD.)' '$HOME/$f:e:l/$f:t:r'
使用find
and gmv
,您还可以在一次find
调用中完成此操作,以避免对目录进行两次爬网:
find . -type f '(' -name '*.[jJ][pP][gG]' -exec gmv -it ~/jpg {} + -o \
-name '*.[pP][nN][gG]' -exec gmv -it ~/jpg {} + ')'
-iname
(或者如果您的find
实现支持该非标准扩展,则使用。! -name . -prune
在之前添加-type f
以停止递归;某些find
实现也支持-mindepth 1 -maxdepth 1
这一点,-mindepth 1
这里没有必要,因为.
(在深度 0 处)无论如何都不匹配模式)。
没有GNU mv
,你可以用于sh
重新排列目标目录的参数到最后:
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 {} + ')'
(-type f
相当于上面zsh
的.
glob 限定符)。
唯一的健全性检查是-i
为了交互而完成的,它不是提前完成的。
要基于扩展名 + target-dir 列表构建该命令,您可以执行以下操作:
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
通过该zmv
方法:
typeset -A map=(
jpg ~/jpg
jpeg ~/jpg
png ~/png
aif ~/aiff
)
zmv "**/(*).(#i)(${(kj[|]map})(#qD.)" '$map[$2:l]/$1'
使用$map
关联数组将扩展名映射到目标目录。${(kj[|])map}
j
将k
关联数组的 ey 与|
s 连接。我们切换到双引号,以便将其扩展为传递给的模式参数zmv
。
¹ 如果存在以下类型的文件,则会出现另一个潜在问题目录与这些模式匹配或更糟糕的事情,例如dir.png/subdir.png/file.png
,您想要跳过那些! -type d
,甚至进一步限制为仅如我在此处所示的常规文件。
答案3
既然您询问了脚本的复杂性和优雅性,我将补充一下弗拉杰斯的回答与其他东西你可以做:
︙
echo "Executing Script"
for ext in jpg png
do
find . -maxdepth 1 -iname "*.$ext" -exec gmv -n --target-directory="$HOME/$ext" {} +
done
如果正如您所说,您使用多个扩展名(例如 mp3、zip、bmp、gif 等)执行相同的操作,这可以减少代码的重复。我告诉 bash 用户
你应该将所有 shell 变量 ( ) 用双引号引起来,如图所示(尽管如您所见,有时不使用引号也能正常工作)。
$something
这在 Zsh 中不是必需的(大多数时候),除非您设置了该
-y
选项
但是,具体来说,你必须不是引用*.$ext
单身的引号,因为这会妨碍 shell 变量的正确解释 $ext
。你呢必须用单引号*
、双引号或用 , 来引用 \*
, 以防止 shell 扩展该 glob;您需要将其按find
原样 传递 (不扩展)。
您不需要引用{}
(但这并没有什么坏处)。
这并不直接支持您的多种拼写(即jpeg
→jpg
和 aif
→ aiff
)问题 - 至少不容易 - 但您可以使用符号链接在脚本外部(在文件系统中)处理该问题:
光盘 # IE,cd“$HOME” mkdir jpg png mp3 zip aiff … ln -s jpg jpeg ln -s aiff aif
然后做
for ext in jpg jpeg png aif aiff …
do
find . -maxdepth 1 -iname "*.$ext" -exec gmv -n --target-directory="$HOME/$ext" {} +
done
是的,-iname
测试find
很好。但你可以相当容易地模拟它:
gmv -n -- *.[Jj][Pp][Gg] *.[Jj][Pp][Ee][Gg] "$HOME"/jpg
但要围绕它进行循环并不那么容易for
——至少在 bash 中不是这样。显然在 zsh 中这很容易;看斯特凡·查泽拉斯的回答。 Stéphane 提醒我,在 zsh 中,如果 glob(通配符)失败,整个命令就会中止,即使还有其他 glob 成功。所以,对于zsh,你需要将上面的内容拆分成
gmv -n -- *.[Jj][Pp][Gg] "$HOME"/jpg
gmv -n -- *.[Jj][Pp][Ee][Gg] "$HOME"/jpg
我对 zsh 了解不多,但我相信上面向您展示的所有内容都可以在 zsh 中运行。 (它肯定会在 bash 中工作。)