📦

【脱・呪文】tarコマンドを現代的に使いこなすための再入門

に公開

tl;dr

この記事で解説するtarコマンドのまとめです。

解凍・確認

# いきなり解凍せず、まずは中身を見る
tar -tf archive.tar.gz | head

# 拡張子(gz/xz/bz2)を気にせず解凍 & 展開先指定 (-C)
tar -xf archive.tar.gz -C ./dist

# 一番上のフォルダを無視して中身だけぶちまける
tar -xf archive.tar.gz --strip-components=1

圧縮・デプロイ

# 一般的な圧縮 (gzip)
tar -czf archive.tar.gz ./target_dir

# コミット済みの綺麗なコードだけをデプロイ用に取り出す (.git等は除外)
git archive HEAD --format=tar | tar -xf - -C /path/to/dest

スクリプト・自動化

# スペース入りファイル名があっても安全に圧縮する
find . -name "*.log" -print0 | tar -czf logs.tar.gz --null -T -

【脱・呪文】tarコマンドを現代的に使いこなすための再入門

はじめに

今までずっと、妄信的に以下のコマンドを使ってきませんでしたか?
私はしてました;;

# 呪文
tar -zxvf hoge.tar.gz

zってなんだっけ? gzip? じゃあ.tar.xzの時はなんだっけ?」
「展開したらカレントディレクトリがファイルだらけになった!」
「git管理下のファイルだけをtarで固めたいけど、.gitまで入っちゃう……」

正直、tar はオプションが膨大ですが、実務で必要な機能は限られています。
本記事では、呪文を呪文のままにせず、理解したうえで実用的tarの使い方を整理します。


使いそうなOption一覧

まずは実用的ですぐ使えそうなオプションを厳選しました。

オプション 役割 備考
基本操作
-c Create(作成) 新しいアーカイブを作る時に必須
-x eXtract(展開) アーカイブを解凍する時に必須
-t lisT(一覧) 展開せずに中身を確認する。解凍前の安全確認に必須
-f File(ファイル) 対象のアーカイブファイル名を指定する
-v Verbose(詳細) 処理したファイル名を表示。大量のファイルの時は外さないと遅くなるので注意
圧縮指定(作成時) ※解凍(-x)時はこれらを省略しても自動判別されます
-z gZip 最も標準的。.tar.gz
-j bzip2 少し圧縮率が高いが遅い。.tar.bz2
-J xz 圧縮率は最強クラスだが激遅。配布用向け。.tar.xz
便利・実用
-C Change directory 指定したディレクトリに移動してから処理を行う。展開先の指定に便利
--strip-components=N 階層無視 展開時に、パスの先頭N階層を取り除く。「ディレクトリの中にディレクトリ」問題を解消
--exclude 除外 node_modules.git など、特定のファイル・ディレクトリを除外する
-X / --exclude-from 除外 渡したファイルに書かれたファイル・ディレクトリを除外する。ファイル数が多かったり除外設定を使いまわす場合に便利
-T / --files-from ファイル指定 処理対象のファイルリストをテキストファイルや標準入力から受け取る

1. 解凍時のオプションは -xf だけでいい

昔のtarは圧縮形式に合わせてオプション(-z, -jなど)を指定する必要がありましたが、現代のtarはアーカイブのマジックナンバーを見て自動判別してくれます。
拡張子が間違っていても(例: 中身がgzipなのに拡張子が.tar)、正しく解凍してくれます。

# これらを覚える必要はありません
tar -xzf archive.tar.gz
tar -xjf archive.tar.bz2
tar -xJf archive.tar.xz

# これで統一できます(vはお好みで)
tar -xf archive.tar.gz
-x の実験

実際に圧縮形式を指定しなくてもうまくいくかを実験してみます。

# tarファイルを作成
tar -cf tar_file.tar test_directory/            # 無圧縮
tar -czf targz_file.tar.gz test_directory/      # gzip圧縮
tar -cJf tarxz_file.tar.xz test_directory/      # xz圧縮
# 意地悪で圧縮ファイルの中身と拡張子を変えてみる(xz圧縮なのに.tar.gzで保存)
tar -cJf tarxz_file.tar.gz test_directory/      # xz圧縮

まずは拡張子を見て圧縮形式のオプションを明示的に指定して解凍します。

# オプションをして解凍
tar -xf tar_file.tar            # 成功
tar -xzf targz_file.tar.gz      # 成功
tar -xJf tarxz_file.tar.xz      # 成功
tar -xzf tarxz_file.tar.gz      # 失敗

拡張子なんて気にしないでtarコマンドに全任せで解凍してみましょう。

# 圧縮形式を指定せず、tarに任せて解凍
tar -xf tar_file.tar            # 成功
tar -xf targz_file.tar.gz       # 成功
tar -xf tarxz_file.tar.xz       # 成功
tar -xf tarxz_file.tar.gz       # 成功

2. 中身をぶちまけないための -C--strip-components

ファイルを解凍する際に、いちいち解凍したいディレクトリにcdで移動していませんか?
-Cオプションを使用すれば簡単に解凍先を指定できます。

GitHubのリリースページからソースコードをダウンロードして解凍すると、repo-name-v1.0.0/src/... のように、トップレベルにディレクトリが一つある構成になっていることが多いです。これを「中身だけカレントディレクトリ(あるいは特定の場所)に置きたい」時に、わざわざ mv するのは面倒です。
そこで--strip-componentsオプションが効ききます。

# -C 展開先
# --strip-components=無視したい階層数
tar -xf source.tar.gz -C ./src --strip-components=1

これで、source.tar.gz の中のトップディレクトリを1つ無視して、その中身を ./src ディレクトリに直接展開できます

--strip-components の実験

例えばこんなファイルを解凍したいとします。

$ tar -tf deeply_nested.tar.gz

useless_wrapper_v1.0/
useless_wrapper_v1.0/README.txt
useless_wrapper_v1.0/my_cool_app/
useless_wrapper_v1.0/my_cool_app/config/
useless_wrapper_v1.0/my_cool_app/config/app.conf
useless_wrapper_v1.0/my_cool_app/src/
useless_wrapper_v1.0/my_cool_app/src/main.c

これを適当に解凍してみるとどうなるでしょうか。

$ mkdir hoge
$ tar -xf deeply_nested.tar.gz -C hoge
$ tree hoge
hoge
└── useless_wrapper_v1.0
    ├── README.txt
    └── my_cool_app
        ├── config
        │   └── app.conf
        └── src
            └── main.c

もちろん、この状態を目指していた場合はこれでよいですが、unsless_wrapper_v1.0直下を解凍したかった場合は別ですね。
--strip-components=1を指定して解凍してみましょう。

$ rm -r hoge/*
$ tar -xf deeply_nested.tar.gz -C hoge --strip-components=1
$ tree hoge
hoge
├── README.txt
└── my_cool_app
    ├── config
    │   └── app.conf
    └── src
        └── main.c

希望通りのディレクトリ構成になりました。
ちなみに--strip-components=2を指定するとどうなるでしょうか。

$ rm -r hoge/*
$ tar -xf deeply_nested.tar.gz -C hoge --strip-components=2
$ tree hoge
hoge
├── config
│   └── app.conf
└── src
    └── main.c

2階層分のディレクトリ及びファイルが無視されて解凍されます。

3. いきなり解凍するな! -t で中身を確認する

tarファイルをもらった時、いきなり -xf で解凍するのは、中身の分からないメールの添付ファイルを開くのと同じくらい危険です。
システムファイルを上書きするリスクを避けるため、まずは -t (list) オプションで中身をチラ見しましょう。

# 全部は多すぎるので先頭だけ見る
tar -tf unknown.tar.gz | head

絶対パスに注意

もし、ファイル名が / から始まっていたら要注意です。

  • 安全: project/... (相対パス) -> カレントディレクトリの下に展開される。
  • 危険: /etc/passwd (絶対パス) -> ルートディレクトリからその場所へ強制的に展開される

最近のtarはデフォルトで先頭の / を削除して安全に展開してくれますが、古いシステムや設定によっては、/etc/passwd などの重要ファイルを上書きされるリスクがあります。
絶対パスを含むような悪意のあるファイルかどうかを確認しておきましょう。

# 先頭が "/" で始まるファイルがあるか検索
tar -tf archive.tar.gz | grep '^/'
絶対パスがある場合の挙動確認

実際に絶対パスがあるようなファイルを展開するとどうなるのでしょうか。

$ tar -tf dangerous_absolute.tar.gz 
tar: Removing leading `/' from member names
/etc/dangerous_file

$ tar -xf dangerous_absolute.tar.gz 
tar: Removing leading `/' from member names

$ tree .
.
├── dangerous_absolute.tar.gz
└── etc
    └── dangerous_file

絶対パスを勝手に相対パスに変換して処理をしてくれています。
ちなみに、絶対パスをそのまま解釈して実行することも可能です。
(危険なのでしっかりとファイルの中身を理解したうえで実行しましょう)

# -P    : 絶対パスをそのままにして解凍
$ tar -xPf dangerous_absolute.tar.gz 
tar: /etc/dangerous_file: Cannot open: Permission denied
tar: Exiting with failure status due to previous errors

パスが/etcだったのでPermission deniedでエラーが出ましたね。
ちなみにsudoで実行したりrootユーザーで実行すれば解凍されます。

4. いらないファイルを除外する --exclude-X

バックアップを取る際、node_modules やログファイルなど、不要なファイルを除外したいケースは多々あります。

少数なら --exclude

コマンドラインで直接パターンを指定します。

# node_modules と .git を除外
tar --exclude='node_modules' --exclude='.git' -czf backup.tar.gz ./project

設定を使い回すなら -X (--exclude-from)

除外したいファイルが多い場合や、バックアップ処理を定型化したい場合は、除外リストを別ファイル(例: .tarignore)に記述し、-X オプションで読み込ませるのがスマートです。

1. 除外リストファイルを作成 (.tarignore)

.git
node_modules
*.log
dist/
secret_config.json

2. オプションでファイルを指定

tar -X .tarignore -czf backup.tar.gz ./project

これで、.gitignore のように管理でき、コマンドもスッキリします。

5. Git管理下のファイルだけをデプロイする git archive との合わせ技

「開発中のゴミファイル(ビルド中間生成物や編集中ファイル)を除いて、コミットされた綺麗なコードだけをサーバーに送りたい」。
そんな時はgit archive コマンドと組み合わせるとよいです。

# HEAD(最新コミット)の状態をtarストリームとして出力し、それをパイプで受け取って展開
git archive HEAD --format=tar | tar -xf - -C /path/to/deploy/
  • 中間ファイルを作らない: ディスクを汚さず高速
  • .gitなどのゴミが入らない: gitで追跡されているファイルのみを対象としているため転送量が減り、セキュリティ的にも安全

6. スクリプトで自動化するなら -T--null で堅牢にする

cronで回すバックアップスクリプトや、CI/CDパイプラインの中でtarを使う場合、「ファイル名の罠」に対処する必要があります。

なぜ普通のコマンドではダメなのか?

例えば、以下のようなコマンドは脆弱です。

# 危険な例
tar czf backup.tar.gz $(find . -name "*.log")

これには2つの致命的な弱点があります。

  1. ファイル名にスペースが含まれると壊れる: space filename.log というファイルが spacefilename.log という2つのファイルとして解釈されてしまい、エラーになります。
  2. ファイル数が多すぎると死ぬ: findで見つかったファイルが多すぎると「Argument list too long」というエラーで実行すらできなくなります。

ヌル文字(\0)で連携する

これを解決するのが、「ファイルリストを標準入力から受け取る」というアプローチです。
Linuxのファイル名で使えない文字である**ヌル文字(Null Byte)**を区切り文字として使うことで、どんな変な名前のファイルでも安全に処理できます。

汎用的な検索には find-print0 を使います。

# -print0 : 検索結果をヌル文字区切りで出力
# --null  : 入力をヌル文字区切りとして扱う
# -T -    : ファイルリストを標準入力(-)から読み込む
find . -name "*.log" -print0 | tar -czf logs.tar.gz --null -T -

これで、スペース、改行、タブなどがファイル名に含まれていても問題なく実行することが可能です。
また、引数が長すぎてエラーになることもありません。

まとめ

今更tarコマンドについていろいろ調べてみた結果、解凍時の新常識や便利なオプションを知ることができました。
パイプラインの一部として組み込んだり、適切にオプションを使いこなすことで、開発フローやデプロイ作業はもっと安全で快適なものにしていきたいと思います。

それではまた。

Discussion