【merge処理】
写真整理スクリプトをさらに改良した
(空ディレクトリ削除・bash化)

以前に、NASへの写真整理スクリプトを ditto + rm から mv に書き直しました。
今回は実際に使っていて、気づいた問題を修正した話です。

関連記事

1. 空ディレクトリが残っていた

スクリプトを実行したあと、inboxフォルダを確認するとディレクトリが残っていました。

最初はNASの接続が切れてエラーになっているのかと疑いましたが、ファイル自体はarchiveに移動できていました。
空になったディレクトリだけが残っていた状態です。

mv はファイルを移動しますが、そのファイルが入っていたフォルダは消しません1

2. 修正内容

2.1. 空ディレクトリを削除する

末尾に1行追加しました。

find "$SRC" -mindepth 1 -type d -empty -depth -exec rmdir {} \;Code language: Bash (bash)

-depth を付けると、深い階層から先に処理します2
たとえば inbox/a/b/c という構造でファイルが移動済みなら、cba の順に空かどうか確認して削除します。
-mindepth 1$SRC 自体を削除対象から除くための指定です3

2.2. シェルをbashに変更した

元のコードは #!/bin/sh でしたが、read -d '' はPOSIX sh では標準外です4

そこで、#!/bin/bash に変えました。

2.3. 拡張子の分離をファイル名だけに限定した

同名ファイルへの連番付けで、元のコードはフルパスの文字列に対して拡張子を分離していました。

base="${dest%.*}"
ext="${dest##*.}"Code language: Bash (bash)

ところが、ディレクトリ名に . が含まれると、拡張子の判定がずれる可能性があります。
たとえば /path.v1/photo というパスで photo に拡張子がないのに、.v1 の部分を誤って拡張子として扱ってしまう可能性があります。
そこで、basename でファイル名を取り出してから処理するよう変えました5

name="$(basename "$dest")"Code language: Bash (bash)

3. 完成したコード

#!/bin/bash
set -eu

SRC="/Volumes/network-storage/pic-archive-inbox"
DST="/Volumes/network-storage/pic-archive"

find "$SRC" -type f -print0 | while IFS= read -r -d '' file; do
    rel="${file#"$SRC"/}"
    dest="$DST/$rel"
    destdir="$(dirname "$dest")"
    name="$(basename "$dest")"

    mkdir -p "$destdir"

    if [ -e "$dest" ]; then
        case "$name" in
            *.*)
                base="${name%.*}"
                ext=".${name##*.}"
                ;;
            *)
                base="$name"
                ext=""
                ;;
        esac

        i=1
        while [ -e "$destdir/${base}(${i})${ext}" ]; do
            i=$((i + 1))
        done

        dest="$destdir/${base}(${i})${ext}"
    fi

    mv "$file" "$dest"
done

find "$SRC" -mindepth 1 -type d -empty -depth -exec rmdir {} \;Code language: Bash (bash)

冒頭の set -eu は、コマンドが失敗した時点でスクリプトを停止し、未定義変数の参照をエラーとして扱う指定です6
また、find -print0read -d '' の組み合わせは、スペースや改行を含むファイル名を正しく扱うための定番の書き方です7
macOSの写真ファイルはスペース入りの名前が珍しくないため、この組み合わせが必要です。

4. 補足:実行ログを出したい場合

何が移動されたか確認したい場合は、echo を加えると移動元と移動先が表示されます。

echo "移動: $file"
echo "  -> $dest"
mv "$file" "$dest"Code language: Bash (bash)

空ディレクトリの削除は -print を加えると表示されます。

find "$SRC" -mindepth 1 -type d -empty -depth -print -exec rmdir {} \;Code language: Bash (bash)

ログがなくても動作に影響はありませんが、慣れないうちは付けておくと何が起きているか把握しやすいです。

  1. mvコマンドはファイルの実体(またはinode参照)を移動しますが、空になったディレクトリの削除は行いません。これはPOSIX仕様に準拠した動作です。
  2. findの-depthオプションはディレクトリの内容を、ディレクトリ自体より先に処理します。これにより、inbox/a/b/cのようなネスト構造でも、c→b→aの順に空確認と削除が連鎖します。 – find コマンド | コマンドの使い方(Linux)
  3. -mindepth 1を指定すると、検索対象の起点ディレクトリそのものを処理対象から除外できます。これがないと、中身が空になったinboxフォルダ自体も削除されてしまいます。
  4. read -dはbash拡張であり、POSIX sh仕様には含まれません。macOSでは/bin/shの実体はbashですが、将来的にdashに変わる可能性をApple自身が示唆しています。Ubuntuなど多くのLinuxディストリビューションではすでに/bin/shはdashです。 – Macで行く – bashとdashの違いを知る
  5. basenameはパスからファイル名部分だけを取り出すコマンドです。basename /path/to/photo.jpgphoto.jpgを返します。これにより、ディレクトリ名に含まれるドットが拡張子判定に影響しなくなります。
  6. set -eはコマンドがゼロ以外の終了コードを返した時点でスクリプトを即座に終了させます。set -uは未定義変数を参照した場合にエラーとして扱います。これにより、意図しない変数名のタイプミスなどを早期に検出できます。 – シェルスクリプトを書くときはset -euしておく
  7. 通常のfindはファイル名を改行区切りで出力するため、ファイル名にスペースが含まれると1つのパスが複数に分割されてしまいます。-print0でNULL文字区切りにし、read -d ''でNULL文字を区切り文字として読み込むことでこの問題を回避します。 – findとxargsの間に任意のコマンドを実行する(半角スペース対策)