要将“下载”文件夹顶层的文件按扩展名分类到文件夹中,请使用一个脚本检查当前目录中的文件,创建 txt 和 csv 等文件夹,并跳过名称已存在的目标项。先运行下面的可丢弃示例,预览要移动的文件,然后从终端或 Apple 快捷指令运行脚本。

本指南适用于想要将散落的文件实际移入文件夹,同时让现有子文件夹及其内容保持原位的 Mac 用户。主要的决策辅助工具是预览报告:每个符合条件的文件都会显示建议的目标位置,而名称冲突和不受支持的名称会明确显示跳过原因。

哪些文件会移动,哪些会留在原处?

访达提供排序和分组控件,以及文件夹和智能文件夹。Apple 说明,智能文件夹会将匹配的文件集中显示,而不会移动它们。本任务的目标是创建实际的扩展名文件夹,并将文件移入其中。在 Mac 上的访达中整理文件。

查看示例前,打开访达并选择 Finder > Settings > Advanced,然后选择“显示所有文件扩展名”。这样就能看到分类脚本使用的后缀。显示扩展名不会改变文件格式。在 Mac 上显示或隐藏文件扩展名。

下面的脚本遵循这些明确的规则。这些规则描述的是此实现,而不是访达或快捷指令的通用行为:

遇到的项目 分类脚本的行为
普通的当前目录文件,例如 note.TXT 提议移动到 txt/note.TXT;保留原文件名
复合名称,例如 bundle.tar.gz 使用最后一个后缀:gz/bundle.tar.gz
不含点号或以点号结尾的文件 留在原处并报告跳过
文件夹(包括名称带扩展名的文件夹) 文件夹及其内容均留在原处
以点号开头的名称 从扫描中排除
符号链接 留在原处
扩展名包含 ASCII 字母和数字以外的字符,或长度超过 32 个字符 留在原处并报告跳过
目标位置已有同名文件 源文件留在原处并报告冲突
文件夹名称被普通文件或符号链接占用 报告文件夹受阻,源文件留在原处

扩展名限制让文件夹命名规则保持简单、可预测。跳过的文件仍可供手动处理;脚本不会自行编造替代名称,也不会悄悄丢弃文件。

创建一个可丢弃的“下载”文件夹示例

打开终端并粘贴下面完整的设置代码块。它只会在 Downloads/ExtensionSortDemo 中创建模拟文本文件。有些文件带有 .gz 等示例性扩展名,但它们的内容只是普通示例文本,并非对应格式的文件。若该示例文件夹已存在,设置过程就会停止;因此重新运行不会重置之前的测试。

Apple 介绍了如何使用终端执行命令和 shell 脚本,也介绍了如何使用 mv 移动本地文件。在这里,终端提供了一种清晰的方式,可在实际移动前预览每项计划操作。Mac 上的终端是什么?,在 Mac 上通过终端管理文件。

/bin/bash <<'BASH'
set -eu
demo="$HOME/Downloads/ExtensionSortDemo"
/bin/mkdir "$demo"
/bin/mkdir "$demo/nested" "$demo/txt"
printf 'sample text\n' > "$demo/sample.txt"
printf 'uppercase suffix example\n' > "$demo/note.TXT"
printf 'name,value\nblue,1\n' > "$demo/palette.csv"
printf 'synthetic compound-name example\n' > "$demo/bundle.tar.gz"
printf 'no extension example\n' > "$demo/README"
printf 'trailing dot example\n' > "$demo/trailing."
printf 'hidden example\n' > "$demo/.hidden.txt"
printf 'nested example\n' > "$demo/nested/inside.txt"
printf 'existing destination example\n' > "$demo/txt/sample.txt"
printf 'blocks creation of a csv folder\n' > "$demo/csv"
printf 'Created demo folder: %s\n' "$demo"
BASH

在访达中打开“下载”,然后打开 ExtensionSortDemo。注意两个名为 sample.txt 的不同文件:一个位于示例文件夹顶层,另一个已位于 txt 内。还要注意,csv 是一个文件。这些都是针对两种常见跳过原因的有意测试。

移动文件前预览分类脚本

将下面的完整程序粘贴到终端中。其初始设置为 mode=preview,因此它只报告拟议的移动操作,不会创建扩展名文件夹或移动文件。配置的根目录是临时演示文件夹,而不是整个“下载”文件夹。

/bin/bash <<'BASH'
set -u
export LC_ALL=C
unset GLOBIGNORE
shopt -s nullglob
shopt -u dotglob

root="$HOME/Downloads/ExtensionSortDemo"
mode=preview

case "$mode" in
  preview|move) ;;
  *) printf 'Invalid mode\n' >&2; exit 1 ;;
esac
if [ ! -d "$root" ] || [ -L "$root" ]; then
  printf 'Root must be an existing ordinary folder\n' >&2
  exit 1
fi
for file in "$root"/*; do
  if [ -L "$file" ] || [ ! -f "$file" ]; then
    printf 'SKIP non-regular: %q\n' "$file"
    continue
  fi
  name=${file##*/}
  case "$name" in
    *.*) ext=${name##*.} ;;
    *) printf 'SKIP no extension: %q\n' "$name"; continue ;;
  esac
  case "$ext" in
    ''|*[!A-Za-z0-9]*)
      printf 'SKIP unsupported extension: %q\n' "$name"
      continue ;;
  esac
  if [ "${#ext}" -gt 32 ]; then
    printf 'SKIP long extension: %q\n' "$name"
    continue
  fi
  ext=$(printf '%s' "$ext" | /usr/bin/tr '[:upper:]' '[:lower:]')
  folder="$root/$ext"
  dest="$folder/$name"
  if [ -L "$folder" ] || { [ -e "$folder" ] && [ ! -d "$folder" ]; }; then
    printf 'SKIP blocked folder: %q\n' "$folder"
    continue
  fi
  if [ -e "$dest" ] || [ -L "$dest" ]; then
    printf 'SKIP collision: %q\n' "$dest"
    continue
  fi
  if [ "$mode" = preview ]; then
    printf 'WOULD MOVE %q -> %q\n' "$file" "$dest"
    continue
  fi
  /bin/mkdir -p "$folder" || exit 1
  /bin/mv -n "$file" "$dest" || exit 1
  if [ -e "$file" ] || [ -L "$file" ]; then
    printf 'NOT MOVED: %q\n' "$file" >&2
    exit 1
  fi
  printf 'MOVED %q -> %q\n' "$file" "$dest"
done
printf 'Finished (%s). Review all SKIP lines.\n' "$mode"
BASH

直接子项模式 "$root"/* 不会递归进入目录。随后,文件检查会排除目录和符号链接。nullglob 可处理空文件夹,而不会将不匹配的模式当作文件名;禁用 dotglob 会排除以点开头的名称。这些行为记录在 Apple 随系统分发的 Bash 手册中。Bash 手册。

脚本会提取最后一个后缀,并使用 tr 将其中的字母转换为小写。因此,note.TXT 会保留文件名,但移入 txt。Apple 的 tr 手册说明了此处使用的大写到小写字符映射。tr(1) — 转换字符。

对此示例,以下是重要的预期结果:

模拟源文件 预期预览结果
note.TXT 将移动到 txt/note.TXT
bundle.tar.gz 将移动到 gz/bundle.tar.gz
sample.txt 与 txt/sample.txt 冲突;跳过
palette.csv 文件 csv 占用了文件夹名称;跳过
README 无扩展名;跳过
trailing. 扩展名为空;跳过
nested/inside.txt 不会检查
.hidden.txt 不会检查

这些是预期的文件系统结果,并不表示准备过程中已执行移动操作。程序已通过 Bash 语法检查,另有一项独立且确定性的文件名映射检查确认了后缀提取和小写转换。本次准备所用环境为只读环境,因此未执行完整的 macOS 移动工作流程。

移动示例文件并检查结果

预览与表格相符后,再次粘贴同一个完整程序,只将 mode=preview 改为 mode=move。此测试中保持示例根目录不变。

在移动模式下,mkdir -p 会创建所需文件夹,或接受已存在的目录。它不会把已有的普通文件变成文件夹,因此脚本会先检查目标文件夹。mkdir(1) — 创建目录。

移动操作使用 macOS 的 mv -n,其文档说明的行为是避免覆盖现有文件。前置的存在性检查会生成清晰易懂的冲突报告;之后对源文件的检查则能发现移动后源文件仍然存在的情况。如果命令报告失败,程序会停止。mv(1) — 移动文件。

在访达中检查示例文件夹。你应该能在 txt 中找到 note.TXT,并在新建的 gz 文件夹中找到 bundle.tar.gz。sample.txt 的两个版本都应仍在原来的位置。palette.csv 应留在顶层,而 nested/inside.txt 应留在 nested 内。

最后的 Finished 行表示扫描已完成;也请查看跳过消息。再次运行分类脚本时,已分类的文件应留在各自文件夹中,因为这些文件夹不会被递归扫描。

从 Apple 快捷指令运行同一个分类脚本

快捷指令可以作为运行此脚本的按钮。Apple 说明,快捷指令编辑器内置了编写和运行 shell 脚本的功能。shell 程序负责扩展名映射和存在性检查;本文不假定原生文件移动操作具有相同的冲突处理行为。认识 macOS 快捷指令。

手动创建快捷指令,并在测试期间让它指向示例文件夹:

  1. 打开“快捷指令”,点击添加按钮,并将快捷指令命名为 Sort Extension Demo。
  2. 在操作列表中搜索 Run Shell Script 并添加该操作。
  3. 将 shell 设置为 /bin/bash。将上面的完整分类脚本代码块粘贴到脚本字段中,包括开头的 /bin/bash 行和结尾的 BASH 分隔符。初始时保留 mode=preview。
  4. 在脚本操作后添加 Quick Look,并将其连接到脚本的输出。
  5. 运行快捷指令并检查预览报告,然后再将脚本改为 mode=move。

Apple 说明了如何通过添加操作来创建快捷指令并进行测试,并建议使用 Quick Look 检查操作之间传递的内容。在 Mac 上创建自定义快捷指令,在 Mac 上测试快捷指令中的操作。

此快捷指令不需要输入文件:根路径已在脚本中定义。成功运行后会输出文本报告,并由 Quick Look 显示。该报告不是可传递给其他文件操作的一组已移动文件对象。Apple 说明,若要查看已安装版本中某项操作接受的输入和生成的输出,请按住 Control 键并点按该操作图标,然后选择 Show Info。在 Mac 上浏览快捷指令中的操作列表。

如果快捷指令要求启用脚本执行,Apple 记载的设置路径是 Shortcuts > Settings > Advanced > Allow Running Scripts。如果你的安装版本不提供 shell 操作,请使用上面的完整终端程序;它可以独立完成相同的分类任务。Mac 上快捷指令的高级隐私与安全性设置。

将规则应用于“下载”文件夹顶层

可丢弃示例测试完成后,要将配置改为实际的“下载”文件夹,只需在分类脚本中用以下单行替换。这是一段配置片段,并非独立的分类程序:

root="$HOME/Downloads"

每次更改根目录后,都返回到 mode=preview。启用移动操作前,请检查每个建议的目标位置和跳过消息。程序会一次处理所有符合条件的当前目录文件;它不会提供逐个选择提示。

请在文件都已下载完成的文件夹中手动启动批处理。脚本无法判断下载是否完成,不会监视之后新增的文件,也不会协调并发运行。脚本的检查和移动是分开的操作,因此运行期间请避免在该文件夹中执行其他文件移动操作。预览只是当前状态的报告,并不会预留这些目标位置。

遇到冲突时,请在访达中检查两个文件,然后决定是重命名顶层文件,还是将其留在原处。如果扩展名文件夹受阻,请先处理占用该文件夹名称的项目,然后再重新运行。无需为解决这两种情况而修改程序以覆盖文件。

最后,在访达中检查实际的目标文件夹,并根据报告核对仍留在顶层的文件。这个可丢弃示例提供了一个具体的验收测试:目标文件会移动,已有名称保持完整,嵌套内容也留在原来的位置。

来源