要將「下載項目」資料夾最上層的檔案依副檔名分類到不同資料夾,請使用一個檢查最上層檔案、建立 txt 和 csv 等資料夾,並略過已有目標名稱的指令碼。先使用下方的可丟棄範例,預覽檔案移動方式,再從「終端機」或 Apple「捷徑」執行。

本指南適用於想將散落的檔案實際移入資料夾、同時讓現有子資料夾及其內容保持原位的 Mac 使用者。主要的判斷依據是預覽報告:每個符合條件的檔案都會顯示建議的目的地,發生名稱衝突或名稱不受支援時則會明確顯示略過訊息。

哪些檔案會移動?哪些會留在原位?

Finder 提供排序與分組控制項,以及一般資料夾和智慧型資料夾。Apple 說明,智慧型資料夾會將符合條件的檔案顯示在一起,但不會移動檔案。此任務希望得到的結果是建立實際的副檔名資料夾,並將檔案移入其中。在 Mac 上使用 Finder 整理檔案。

查看範例前,請開啟 Finder,選擇 Finder > 設定 > 進階,然後選取「顯示所有檔案副檔名」。這樣就能看到分類器會使用的副檔名。顯示副檔名不會改變檔案格式。在 Mac 上顯示或隱藏檔案副檔名。

以下指令碼採用這些明確規則。這些規則描述的是此實作方式,不代表 Finder 或「捷徑」的通用行為:

遇到的項目 分類器的行為
一般的最上層檔案,例如 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

在 Finder 中開啟「下載項目」,再開啟 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) — 移動檔案。

在 Finder 中檢查示範資料夾。你應該會在 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 顯示。該報告不是可提供給其他檔案動作使用的一組已移動檔案物件。若要查看已安裝版本中某個動作接受的輸入及產生的輸出,請按住 Control 鍵並按一下該動作圖示,然後選擇「顯示資訊」,如 Apple 文件所述。在 Mac 上瀏覽「捷徑」中的動作清單。

如果「捷徑」要求啟用指令碼執行功能,Apple 文件所列的設定位置為 捷徑 > 設定 > 進階 > 允許執行指令碼。如果你的安裝版本沒有 shell 動作,請使用上方完整的「終端機」程式;它能獨立完成相同的分類工作。在 Mac 上設定「捷徑」的進階隱私權與安全性設定。

將規則套用到「下載項目」最上層

完成可丟棄的測試後,若要處理實際的「下載項目」資料夾,只需在分類器中以以下單行內容取代目前設定。這是設定片段,並非可獨立執行的分類程式:

root="$HOME/Downloads"

每次更改根目錄時,請回到 mode=preview。啟用移動功能前,請檢查每個建議目的地和略過訊息。程式會一併處理符合條件的最上層檔案,無法逐一提示選取檔案。

請在檔案都已下載完成的資料夾中手動批次執行。指令碼不會判斷下載是否完成、不會監控後續新增項目,也不會協調同時執行的工作。檢查與移動是分開的操作,因此執行期間請避免在該資料夾中進行其他檔案移動。預覽只是反映目前狀態的報告,不會保留那些目的地。

發生衝突時,請在 Finder 中檢查兩個檔案,再決定要重新命名最上層檔案,還是讓它留在原位。若副檔名資料夾受阻,請先處理佔用該資料夾名稱的項目,再重新執行。這兩種情況都不需要修改程式來覆寫檔案。

最後,請在 Finder 中檢查實際的目的地資料夾,並對照報告查看任何仍留在最上層的檔案。可丟棄的範例提供了具體的驗收測試:預定的檔案會移動,現有名稱保持不變,巢狀內容也留在原位。

來源