Linux・Unix cp 安全ディレクトリコピーガイド:末尾スラッシュ、グロブ、属性検証

cp はトランザクションではなく、全実装に共通するプレビューモードもありません。単純に見えるディレクトリコピーでも、コピー先の有無、shell によるグロブ展開、GNU と BSD/macOS の実装差、シンボリックリンクやマウントポイントによって結果が変わります。このガイドでは、意図する結果を先に定義し、新規ステージングディレクトリ、検証、復元可能な切り替えでリスクを下げます。

この保守版は 2026 年に書き直しました。短い 2011 年の原文には、-r とエラー原因について不正確な説明があります。全文は末尾の不活性アーカイブに保存していますが、現在の操作手順ではありません。

最初にツールと目的を確認する

Linux で一度見た動作を、すべての Unix の規則と考えないでください。実際にコピーするマシンで実装とマニュアルを記録します。

command -V cp
cp --version
man cp
uname -a

cp --version は GNU 形式です。BSD/macOS では未対応としてエラーになる場合があるため、インストール済みコマンドについてはローカルの man cp を基準にします。開始前に、ディレクトリ自体か内容か、コピー先は未作成でなければならないかマージ可能か、上書きを許可するか、ファイルシステム境界を越えるか、リンクとメタデータをどう扱うか、最終切り替えを誰が承認するかも明記します。

ディレクトリ自体、内容、末尾スラッシュ

次のパスは意味を説明する例にすぎません。実データのあるディレクトリへ貼り付けないでください。

cp -R -- source new-copy
mkdir -p -- existing
cp -R -- source existing/
cp -R -- source/. existing/

コピー先が存在するか

意図一般的な結果境界
source を存在しない new-copy へコピーsource に対応する内容を持つ new-copy を作成親ディレクトリが存在し、書き込み可能であること
source を既存の existing/ へコピー通常は existing/source を作成またはマージ同名項目を上書きする可能性があり、クリーンな複製ではない
source/. を既存の existing/ へコピードットファイルを含む内容を existing へマージコピー先は新規の空ステージングディレクトリにする

同じファイル、同じ inode のハードリンク別名、同じ場所へ解決されるシンボリックリンク、ディレクトリをその子孫へコピーする操作は、すべて停止条件です。実装は同一ファイルや自己コピーの一部を検出しますが、その検出を完全な安全境界としてはいけません。

cprsync ではない

cp のコピー元末尾 / には、実装をまたいで使える一つの規則はありません。

  • GNU cp は通常、一般ディレクトリの末尾に / があるだけで「内容だけ」に切り替わりません。ただし、末尾スラッシュはパスがディレクトリへ解決されることを要求し、コマンドライン上のシンボリックリンク解決を変える場合があります。GNU には --strip-trailing-slashes もあります。
  • 現在の FreeBSD および Apple オープンソース版 cp のマニュアルは、-R と共に使う場合、末尾 / のあるコピー元はディレクトリ自体ではなく内容をコピーすると明記しています。
  • rsyncsourcesource/ を明示的に区別し、それぞれ「ディレクトリ名を含める」と「ディレクトリ内容をコピーする」を表します。この rsync の規則を GNU cp に当てはめないでください。

スクリプトで「ドットファイルを含む内容をコピーする」と表す場合は、source/* より source/. が明確です。それでも対象システムの使い捨てディレクトリで検証してください。

グロブを展開するのは shell

cp -R source/ staging/ では、cp の開始前に shell が を展開します。Bash は初期状態で .env などのドットファイルを対象にしません。一致がない場合、初期設定ではリテラルのパターンをコマンドへ渡します。nullglob は一致しないパターンを削除し、failglob はそのコマンドを中止し、dotglob* にドットファイルを含めますが、... は除外します。

Bash グロブが本当に必要な場合は、設定が現在のセッションへ漏れないようサブ shell に限定します。

(
  shopt -s dotglob failglob
  cp -R -- source/* staging/
)

それでも source/. より複雑です。引用符の位置も重要です。"source data"/ は空白を保護しつつ を展開対象に残しますが、"source data/*" ではアスタリスクがリテラルになります。-- はオプション処理を終了するため、-report というコピー元がオプションと誤認されません。スクリプトでは ./-report も使えます。使用する構文を対象実装がサポートするか確認してください。

再帰、属性、リンクの方針を選ぶ

オプション意味と制限
-RPOSIX の再帰ディレクトリコピー形式。複数システム向けスクリプトではこちらを優先
-rGNU は現在 -R と同じ扱い。FreeBSD/macOS の履歴互換動作ではリンク追跡を含む場合があり、マニュアルも依存を推奨しない
-p権限の許す範囲で基本メタデータを保存。正確な集合と失敗報告は実装ごとに異なる
-aGNU では再帰、リンク保存、全属性保存要求の短縮形。BSD/macOS の展開と属性範囲は同一契約ではない
-H / -L / -Pコマンドライン上だけ、すべて、または一切リンクをたどらない。再帰コピーでは明示的に選ぶ
-T / -tGNU の「コピー先をディレクトリ扱いしない」と「コピー先ディレクトリを明示する」拡張。POSIX ではなく、BSD/macOS 対応を仮定しない
-xGNU、FreeBSD、現在の macOS ではファイルシステム境界を越えないために使用可能だが POSIX ではない。省略したマウントをすべて記録する

GNU では -T で「コピー先が既存なら一段深くなる」という曖昧さをなくし、-t で複数コピー元のコピー先を明示できます。

cp -aT -- source new-copy
cp -a -t staging -- source-a source-b

この二つは GNU 専用です。-T を使っても、既存の本番ディレクトリを new-copy にしてはいけません。マージや上書きは依然として起こり得ます。

シンボリックリンクと特殊ファイル

リンク自体を保存するか、リンク先の内容をコピーするかを先に決めます。信頼できないツリーへ -L を使うと、ツリー外のファイルを取り込んだり、循環に遭遇したりします。デバイスノード、FIFO、ソケット、処理中に変化するファイルには、アプリケーション整合性の計画が必要です。管理者権限で無差別にコピーせず、--copy-contents でデバイス内容を読み取らないでください。

ハードリンクの動作を -a という名前だけから推測してはいけません。GNU -a はリンク関係の保存を要求しますが、現在の FreeBSD/macOS cp -R マニュアルではハードリンクが別々のファイルになる場合があると説明されています。ハードリンク同一性が重要なら、対象プラットフォームでサポートされ、検証済みのアーカイブまたは同期ツールを使います。

属性、ACL、拡張属性、スパースファイル

内容が同じでも二つのツリーが同等とは限りません。アクセス権、所有者、タイムスタンプ、ACL、拡張属性、セキュリティラベル、リソースフォーク、ファイルフラグ、ハードリンク、スパースホールは、権限とファイルシステム機能に制約されます。GNU -a--preserve=all の短縮形ですが、診断と終了ステータスは必ず確認します。BSD/macOS の -a 定義は異なります。

GNU cp は初期状態でスパースファイルを検出しようとし、--sparse=auto|always|never も提供します。macOS のスパース制御は異なります。ファイルシステム間では論理サイズが一致しても、割り当て済みブロック数が異なることがあります。ファイルサイズだけでは検証不十分です。

ファイルシステム境界

再帰ツリーには別のマウントポイント、bind mount、ネットワークボリューム、コンテナマウント、クラウド系ファイルシステムが含まれる場合があります。初期動作のまま境界を越えると、意図を大幅に超えるデータをコピーする可能性があります。一方、対応する -x は子マウントを意図的に省略します。すべての境界、クォータ、大文字小文字規則、最大ファイル制限、保存可能な属性を事前に一覧化します。同一ファイルシステム内の rename で切り替えるなら、ステージングは最終コピー先と同じファイルシステムに置きます。

変更しない事前確認

cp に共通の --dry-run はありません。まず読み取り専用の一覧を作ります。ファイル名と拡張属性には機密情報が含まれ得るため、出力は許可された場所だけに保存し、共有前に秘匿化してください。

find source -mindepth 1 -maxdepth 2 -print
du -sk source
findmnt -R --target source
find source -xdev -print

最初の二つは広く使われる形式です。findmnt とここでの find -xdev はローカルマニュアルで確認してください。コピー中にコピー元が変化するか、空き容量と inode、読み書き権限、マウント状態、物理解決後のパス重複も調べます。通常の改行区切り find 出力を解析して、一括削除や上書きを駆動してはいけません。

使い捨て演習

次の演習は mktemp が作成した /tmp/cp-guide.* の下だけへ書き込み、削除前にその接頭辞を検査します。このワークステーションの GNU coreutils 9.4 と Bash 5.2 で検証済みです。他のシステムではローカルマニュアルを先に読み、特に truncate は POSIX ツールではない点に注意してください。

work=$(mktemp -d /tmp/cp-guide.XXXXXX)
mkdir -p -- "$work/source/sub" "$work/existing"
printf visible > "$work/source/visible"
printf hidden > "$work/source/.hidden"
ln -s -- visible "$work/source/link"
truncate -s 16M "$work/source/sparse.bin"

cp -R -P -- "$work/source" "$work/clone"
cp -R -P -- "$work/source"/. "$work/existing"/

find "$work" -maxdepth 3 -print
du -h "$work/source/sparse.bin" "$work/existing/sparse.bin"

出力を確認した後、記録した一時パスだけを削除します。

case "$work" in
  /tmp/cp-guide.*) find "$work" -depth -delete ;;
  *) printf 'Refusing unexpected path' >&2; exit 1 ;;
esac

この /tmp 演習をそのまま本番コマンドへ変えないでください。検証したのはこのツールの意味であり、本番データの整合性、権限、容量ではありません。

ステージング、検証、ロールバック

新規ステージングディレクトリへコピー

バックアップまたはスナップショットを用意し、アプリケーション要件に従って書き込み側を停止した後、最終コピー先の親に新しいステージングディレクトリを作ります。次はプレースホルダーパスを使った GNU の例です。

stage=$(mktemp -d -- /srv/import/.cp-stage.XXXXXX)
cp -a -- /srv/source/. "$stage"/

cp の終了ステータスとすべての診断を確認します。I/O、権限、容量、属性、コピー元変更のエラーが一つでもあれば停止します。エラー後の部分コピーを利用開始してはいけません。

検証

最初に構造と内容を比較し、次にアプリケーションが必要とするメタデータを比較します。最初の一組は GNU/Linux 向けで、追加パッケージが必要な場合があります。

diff -qr -- /srv/source "$stage"
getfacl -R -p /srv/source
getfacl -R -p "$stage"
getfattr -R -d -m - /srv/source
getfattr -R -d -m - "$stage"
stat --format='%n size=%s blocks=%b' /srv/source/sparse.bin "$stage/sparse.bin"

macOS では、内容比較とローカルの属性表示から始められます。

diff -qr /srv/source "$stage"
ls -le@ /srv/source "$stage"

diff -qr はすべてのメタデータ一致を証明しません。ACL、拡張属性、リンク同一性、スパース割り当て、アプリケーションレベル整合性を個別に受け入れ判定します。大規模または規制対象データでは、承認済みマニフェストとチェックサムを追加できますが、機密パスを含む報告を公開チャンネルへ送ってはいけません。

切り替えとロールバック

最終パスが存在せず、ステージングが同じファイルシステムにあり、検証を通過し、書き込み側が引き続き制御されている場合だけ、rename による切り替えを検討します。

test ! -e /srv/import/release
mv -- "$stage" /srv/import/release

これは変化中のデータに対するトランザクションスナップショットではありません。切り替え前に旧版と復元点を記録し、その後に読み取り専用検査とアプリケーション受け入れ確認を行います。新しい場所へまだ書き込みがなければ、承認済み手順で名前を戻せます。両側に新規書き込みがあり得る段階では停止し、再度の cp でマージを推測せず、アプリケーションレベルの復元を使います。旧ディレクトリの削除はロールバックではありません。

停止条件

  • コピー元が変化中で、アプリケーション整合性のあるスナップショットまたは書き込み停止がない。
  • コピー先が存在するのに、上書き、マージ、名前衝突の方針が承認されていない。
  • パスが重複するか、予期しないシンボリックリンク、マウントポイント、特殊ファイル、自己コピーが見つかった。
  • 空き容量、inode、権限、ACL、拡張属性、スパース対応が要件を満たさない。
  • cp、検証ツール、ファイルシステムが説明できないエラーを報告した。
  • 単に「動かす」ため root が必要に見えるが、権限モデルと監査が設計されていない。

ログと未提供のステージングコピーを保持し、計画を直して再演習します。-f-L、管理者権限を追加して無理に進めないでください。

参考資料

歴史的原文アーカイブ

以下は source_export にある 2011 年の可視本文を完全に保存した不活性アーカイブです。内容変更はなく、正規化が必要な行末空白もありません。内部の旧リンクと技術的主張は歴史資料であり、保守版の手順ではありません。


终于把cp的各种情况做了个总结。文件夹就是一种特殊的文件,但还是有一些不同的地方。

注:[^]表示空格

假设/a目录下有文件1、2、3

Table of Contents

Toggle

- [cp^-R^/a/*^/b](https://blog.lazying.art/en/html/computer_internet/unix_linux/command_shell_software/740/linux%e5%91%bd%e4%bb%a4cp%e6%96%87%e4%bb%b6%e5%a4%b9%e6%97%b6%e6%b7%bb%e5%8a%a0%e6%ad%a3%e6%96%9c%e6%9d%a0%e5%92%8c%e9%80%9a%e9%85%8d%e7%ac%a6%e7%9a%84%e5%90%84%e7%a7%8d%e7%94%a8%e6%b3%95%e6%80%bb.html/#cp-Rab)
- [cp^-R^/a^/b/c](https://blog.lazying.art/en/html/computer_internet/unix_linux/command_shell_software/740/linux%e5%91%bd%e4%bb%a4cp%e6%96%87%e4%bb%b6%e5%a4%b9%e6%97%b6%e6%b7%bb%e5%8a%a0%e6%ad%a3%e6%96%9c%e6%9d%a0%e5%92%8c%e9%80%9a%e9%85%8d%e7%ac%a6%e7%9a%84%e5%90%84%e7%a7%8d%e7%94%a8%e6%b3%95%e6%80%bb.html/#cp-Rabc)

  - [如果/b/c不存在](https://blog.lazying.art/en/html/computer_internet/unix_linux/command_shell_software/740/linux%e5%91%bd%e4%bb%a4cp%e6%96%87%e4%bb%b6%e5%a4%b9%e6%97%b6%e6%b7%bb%e5%8a%a0%e6%ad%a3%e6%96%9c%e6%9d%a0%e5%92%8c%e9%80%9a%e9%85%8d%e7%ac%a6%e7%9a%84%e5%90%84%e7%a7%8d%e7%94%a8%e6%b3%95%e6%80%bb.html/#%E5%A6%82%E6%9E%9Cbc%E4%B8%8D%E5%AD%98%E5%9C%A8)
  - [如果/b/c存在](https://blog.lazying.art/en/html/computer_internet/unix_linux/command_shell_software/740/linux%e5%91%bd%e4%bb%a4cp%e6%96%87%e4%bb%b6%e5%a4%b9%e6%97%b6%e6%b7%bb%e5%8a%a0%e6%ad%a3%e6%96%9c%e6%9d%a0%e5%92%8c%e9%80%9a%e9%85%8d%e7%ac%a6%e7%9a%84%e5%90%84%e7%a7%8d%e7%94%a8%e6%b3%95%e6%80%bb.html/#%E5%A6%82%E6%9E%9Cbc%E5%AD%98%E5%9C%A8)

- [cp^-R^/a/^/b](https://blog.lazying.art/en/html/computer_internet/unix_linux/command_shell_software/740/linux%e5%91%bd%e4%bb%a4cp%e6%96%87%e4%bb%b6%e5%a4%b9%e6%97%b6%e6%b7%bb%e5%8a%a0%e6%ad%a3%e6%96%9c%e6%9d%a0%e5%92%8c%e9%80%9a%e9%85%8d%e7%ac%a6%e7%9a%84%e5%90%84%e7%a7%8d%e7%94%a8%e6%b3%95%e6%80%bb.html/#cp-Rab-2)
- [cp^-R^/a/^/b/](https://blog.lazying.art/en/html/computer_internet/unix_linux/command_shell_software/740/linux%e5%91%bd%e4%bb%a4cp%e6%96%87%e4%bb%b6%e5%a4%b9%e6%97%b6%e6%b7%bb%e5%8a%a0%e6%ad%a3%e6%96%9c%e6%9d%a0%e5%92%8c%e9%80%9a%e9%85%8d%e7%ac%a6%e7%9a%84%e5%90%84%e7%a7%8d%e7%94%a8%e6%b3%95%e6%80%bb.html/#cp-Rab-3)

## cp^-R^/a/*^/b

等同于cp^-R^/a/*^/b/

这相当于把/a目录下所有文件拷贝到/b目录下,如果要包括/a目录下所有的子目录和文件,请加-R选项或-r选项,-r选项的不同于-R之处在于尝试打开目的地文件前先删除己存在的目的地文件。

## cp^-R^/a^/b/c

分两种情况讨论

### 如果/b/c不存在

则创建。并且把/a中所有的文件和子目录都复制到/b/c中。相当于克隆了一个目录。

### 如果/b/c存在

将把/a文件夹本身复制到/b/c中,复制成功后,目录结构为/b/c/a。

## cp^-R^/a/^/b

出错,提示:

cp: omitting directory ‘/a/’

## cp^-R^/a/^/b/

出错,提示:

cp: omitting directory ‘/a/’

Leave a Reply