macOSのターミナル（zsh）を使ってWindowsで文字化けしないZIPファイルを作る

## 要点

- Windowsで解凍しても文字化けしない
- `.DS_Store` 等の不要な隠しファイルを含まない[^1]

[^1]:今回の方法では全ての隠しファイルを取り除いてZIPファイルを作成します．隠しファイルも含めてZIPファイルを作成する場合は，`.DS_Store`等の不要なファイルをピンポイントに取り除くような別の方法を利用してください．

上記2点を満たしたZIPファイルをコマンドラインシェル（zsh）から作成します．

## きっかけ

macOSでZIPファイルを作成する際にはFinderの「圧縮」機能を利用できます．
しかしこの方法で作成したZIPファイルは，Windows PCへ送付した場合に困ることが二つあります．

**その1.ファイル名が文字化けする**  
macOSで作成したZIPファイルをWindowsで解凍すると，ファイル名に含まれる日本語が文字化けしてしまいます．
これはmacOSとWindowsで用いられる文字コードが異なるために起きます[^2]．
~~Windows上で作ったZIPファイルはmacOSで開いても大丈夫なのに逆はダメなので，仕方ないですが~~ Windowsで解凍しても文字化けしないZIPファイルを作成したいところです．

[^2]:macOSはUTF-8，WindowsはShift-JISを利用．

**その2.不要なファイルが入る**  
macOSでは`.DS_Store`のようなOS固有のファイルが自動で生成されます．
macOS上では隠しファイルになっているため，不要であるにも関わらずZIPファイルへの圧縮時に含めてしまうことがあります．
~~そのままWindowsユーザに送付するとお小言をいただくので，仕方ないですが~~ このような他OSでは不要なファイルを除いた状態でZIPファイルを作成したいです．

## 利用するソフト

今回は

- [fd](https://github.com/sharkdp/fd): rust製`find`コマンド
- [p7zip](https://github.com/p7zip-project/p7zip): コマンドライン版7-Zip

の2つのソフトを利用します．
どちらも[Homebrew](https://brew.sh/ja/)を利用してインストールできます．

```zsh
brew install fd
brew install p7zip
```

## 指定したフォルダをZIPファイルに圧縮する

現在の作業ディレクトリ直下にあるディレクトリ`./target`を`target.zip`という名前で圧縮しましょう．
次のコマンドを実行すれば，きっとうまく行くはずです．

```zsh
fd --type file . target -X 7z a -tzip -scsWIN target.zip {}
```

以下ではこのコマンドがどのように動いているのか解説します．

### Windowsで文字化けしないZIPファイルを作る

p7zip[^3]を用いてWindowsで文字化けしないzipファイルを作成するコマンドは
[^3]:p7zipのシェルスクリプト上での実行コマンドは`7z`です．

```zsh
7z a -tzip -scsWIN 出力ファイル名 圧縮するファイル・ディレクトリ
```

です．
重要なのが`-scsWIN`オプションです．
`-scs`は *Set charset for list files* のことで，`-scsWIN`とすることでWindowsの文字コードに合わせてくれます．

また`7z a` コマンドは通常7-Zip形式の圧縮ファイルを作成しますが，`-tzip`オプションを利用することで出力ファイルの形式をZIPファイルに変更できます．

> 2023年夏頃から，Windowsに標準搭載されているエクスプローラで7-Zipファイルの解凍可能になりました[^4]．
> なので7-Zip形式のままでも一応大丈夫ですが，今回はWindowsで一般的に利用されるZIP形式で作成します．

[^4]:参考：[https://pc.watch.impress.co.jp/docs/topic/feature/1513517.html](https://pc.watch.impress.co.jp/docs/topic/feature/1513517.html)

### 隠しファイルを取り除く

`fd`コマンドを利用して隠しファイルを取り除きます．

```zsh
fd --type file . 対象のディレクトリ
```

で指定したディレクトリ以下に存在するファイルを検索します．
この際，デフォルトでは隠しファイルは検索結果に含まないので[^5]，`.DS_Store`等の不要なファイルを弾くことができます．
デフォルトではファイルとディレクトリの両方を表示するので，`--type file`オプションでファイルのみを検索するよう指定します．

[^5]:`-H`オプションを加えることで隠しファイルを検索対象に含めることができます．

これに加えて`-X`オプション[^6]を追加することで検索結果にコマンドを実行できます．
`fd`による検索結果を利用するには，実行したいコマンドのファイル名を指定する箇所に`{}`を記述します．
具体的に

```zsh
fd --type file . 圧縮したいディレクトリ名 -X 7z a 出力ファイル名.zip {}
```

とすることで，`fd`が圧縮したいディレクトリ以下に含まれるファイルを検索し，その結果が`{}`で記述した`7z a`コマンドの圧縮対象のファイル指定部分に適用されます．

[^6]:`fd`のオプションには`-x`（小文字）と`-X`（大文字）があります．どちらも検索結果に指定したコマンドを実行しますが，小文字だと「検索結果に対して個別に処理」，大文字だと「検索結果に対してバッチ処理」です．

以上のことを組み合わせることで，隠しファイルを取り除きながらWindowsでも文字化けしないZIPファイルを作成できます．

## zshの関数にまとめる

先ほどのコマンドを毎回入力するのは面倒！という人は，zshの関数にまとめて`.zshrc`に記述しておきましょう．
ディレクトリ名を指定し，当該ディレクトリ以下のファイルをZIPファイルに圧縮する関数は以下の通りです．

```zsh
function zipwin () 
{
  if [ -z $1 ] || [ $1 = . ]; then
    # 現在の作業ディレクトリをZIPファイルに圧縮する．
    local zip_name="$(basename $(pwd)).zip"
    fd --type file --strip-cwd-prefix . -X 7z a -tzip -scsWIN $zip_name {}
  else
    # 指定したディレクトリをZIPファイルに圧縮する．
    local loc_dir=$(dirname $1)
    local target=$(basename $1)
    local zip_name="$(pwd)/${target}.zip"
    fd --type file --base-directory=$loc_dir . $target -X 7z a -tzip -scsWIN $zip_name {}
  fi

  # 作成したZIPファイルに含まれるファイルを確認する．不要であればコメントアウトしてください．
  7z l $zip_name

  return 0
}
```

上記の関数を用意すれば

```zsh
zipwin 圧縮したいディレクトリ
```

で指定したディレクトリをWindowsでも文字化けしないZIPファイルに圧縮できます．

引数がない場合，あるいは`.`（作業ディレクトリ）を指定した場合は，作業ディレクトリ以下のファイルをZIPファイルに圧縮するようにしています（`if`文の`true`の方ですね）．
ここで`fd`に`--strip-cwd-prefix`オプションを追加しています．
作業ディレクトリを対象に`fd`で検索をかけると，検索結果のファイルパス最初に`./`が付きます．
そのまま`7z a`コマンドに流し込むと，ディレクトリ構造が壊れて全てのファイルが同じディレクトリに保存されてしまいます．
そこで検索結果から`./`を取り除くオプションである`--strip-cwd-prefix`を追加しました．

また，ディレクトリを指定した場合（`else`以降の方）は`--base-directory=`オプションを追加しています．
例えば`fd . hoge/fuga`のようにサブディレクトリである`fuga`を検索対象にした場合，検索結果のファイルパスには`hoge/fuga/piyo.txt`のように`hoge`ディレクトリの名前が含まれます．
そのまま`7z a`コマンドに流すとZIPファイルの圧縮結果に不要な階層（`hoge`）が生じます．
そこで`--base-directory`オプションで検索結果の表示で起点となるディレクトリを指定し，不要な階層を取り除くようにしました．

最後におまけで，`7z l`コマンドを利用して圧縮したファイル一覧を表示するようにしています．

以上です．  
ご覧いただきありがとうございました．