根据扩展名将文件移动到目录(zsh、macOS)

根据扩展名将文件移动到目录(zsh、macOS)

我想要一个脚本来自动将所有文件移动到我已有的目录结构中(看起来像~/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$2csh$f:t:r$f:ezmv$f

zmv '**/*.(#i)(jpg|png)(#qD.)' '$HOME/$f:e:l/$f:t:r'

使用findand 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} jk关联数组的 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

pxeger 说

这在 Zsh 中不是必需的(大多数时候),除非您设置了该-y选项

但是,具体来说,你必须不是引用*.$ext单身的引号,因为这会妨碍 shell 变量的正确解释 $ext。你呢必须用单引号*、双引号或用 , 来引用 \*, 以防止 shell 扩展该 glob;您需要将其按find原样 传递 (不扩展)。

您不需要引用{}(但这并没有什么坏处)。

这并不直接支持您的多种拼写(即jpegjpg 和 aifaiff)问题 - 至少不容易 - 但您可以使用符号链接在脚本外部(在文件系统中)处理该问题:

光盘                      # 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 中工作。)

相关内容