zsh で find を使わずに簡単にファイルを絞り込む

ファイルの種類、タイムスタンプなんかで条件を指定してファイルを絞り込みたくなる時があるだろう。例えばこんな場合だ。

- 通常ファイルだけを別の場所に移動させる(ディレクトリはそのまま残す)
- シンボリックリンクだけ削除する
- タイムスタンプが1日以上古いファイルだけ削除する

こういうときは普通は `find` コマンドでファイルを探すんだけど、zsh の「ファイル修飾子」を使うと1行で簡単にできるようになるので紹介する。

## ファイル修飾子とは

「ファイル名修飾子」とは、ファイル名指定の後に付けるファイルの種類で絞り込みを行うための記号のこと。
例えばこんな感じ。

```
% ls *(.)
```

この `(.)` の部分がファイル名修飾子になる。`.` 以外にも色々種類があるけど、よく使う、または役に立ちそうなのは以下。

| 記号                      | 意味                                      |
|:--------------------------|:------------------------------------------|
| .                         | 通常ファイル                              |
| /                         | ディレクトリ                              |
| @                         | シンボリックリンク                        |
| *                         | 実行ファイル                              |
| D                         | ドットで始まるファイルも含める            |
| ^                         | 後ろの修飾子の意味を反転させる            |
| f<var>spec</var>          | アクセス権で絞り込む                      |
| m<var>[Mwhms]-n</var>     | 修正時刻(modify time)で絞り込む           |
| a<var>[Mwhms]-n</var>     | アクセス時刻(access time)で絞り込む       |
| c<var>[Mwhms]-n</var>     | ステータス変更時刻(change time)で絞り込む |


## 使い方の例
簡単な使い方については例を見たほうが早いと思う。

まず、以下のようなファイル/ディレクトリがあったとする。

```
file1.txt
file2.txt
file3.txt
dir1/        (ディレクトリ)
dir2/        (ディレクトリ)
script1.sh*  (実行可能ファイル
script2.sh*  (実行可能ファイル
text -> file1.txt (シンボリックリンク)
.hidden      (. で始まるファイル)
```

ファイル修飾子で絞り込みを行った結果は以下の通り。

```zsh
# 通常ファイルのみ
% echo *(.)
file1.txt file2.txt file3.txt script1.sh script2.sh

# ディレクトリのみ
% echo *(/)
dir1 dir2

# シンボリックリンクのみ
% echo *(@)
text

# 実行可能ファイルのみ
% echo *(*)
script1.sh script2.sh

# . で始まるファイルも含める
% echo *(D)
.hidden dir1 dir2 file1.txt file2.txt file3.txt script1.sh script2.sh text
```

もちろん `()` の前は `*` 以外に普通のファイル名指定、ワイルドカード指定ができる。

```zsh
# .txt で終わる通常ファイル
% echo *.txt(.)
file1.txt file2.txt file3.txt

# dir1 という名前のディレクトリ
% echo dir1(/)
dir1
```

以降この記事の例では `()` の前は `*` を使うことにする。

### 条件の反転
`^` を使うと意味が反転される。

```zsh
# 通常ファイル以外
% echo *(^.)
dir1 dir2 text

# シンボリックリンク以外
% echo *(^@)
dir1 dir2 file1.txt file2.txt file3.txt script1.sh script2.sh
```

残りの f, m, a, c は引数が必要になって使い方がちょっと複雑なので、個別に説明する。

### ファイルのアクセス権での絞り込み
f<var>spec</var> の形式で、ファイルのアクセス権(パーミッション)で絞り込める。
<var>spec</var> の部分を644とか755とかの8進数で書くと、そのパーミッションに一致(完全一致)するものだけが選ばれる。
3桁の数字のうち一部を `?` に置き換えると「その部分は何でもよい」という意味になる。

```zsh
# こんなふうにファイルが4つあったとする
% ls -l
-rw-r--r-- file1
-rw-r--r-- file2
-rw-rw-r-- file3
-rwxr-xr-x file4

# 644 なファイルのみ
% echo *(f644)
file1 file2

# 所有者のパーミッションが6のみ(グループ、その他はなんでもよい)
% echo *(f6??)
file1 file2 file3
```

### タイムスタンプでの絞り込み
m<var>-n</var> の形式で修正時刻がn日以内であるもの、m<var>+n</var> の形式で修正時刻がn日より前(過去)であるもの、という意味になる。

日以外の単位を使いたい時は、`mh-n` のように `m` の後に単位を指定する。
単位として指定できる記号は以下。

| 記号 | 意味     |
|:-----|:---------|
| M    | 月(30日) |
| w    | 週(7日)  |
| h    | 時間     |
| m    | 分       |
| s    | 秒       |

使い方の例はこんな感じ。

```zsh
# 修正時刻が現在から3日以内であるもの
% echo *(m-3)

# 修正時間が現在から5時間以内であるもの
% echo *(mh-5)

# 修正時間が現在から10秒より前(過去)であるもの
% echo *(ms+10)
```

また、`m` の代わりに `a` を使うと「修正時刻」ではなく「アクセス時刻」、`c` を使うと「ステータス変更時刻」という意味になる。

「修正時刻」とか「アクセス時刻」とかについて軽く説明しておく。
「修正時刻」はファイルを変更した時刻、「アクセス時刻」はファイル読み込みを行った時刻、という意味。「ステータス変更時刻」というのはちょっとややこしいけど、ざっくり言うとファイルの内容を変更した、またはファイル名、パーミッションなどを変更した時刻、という意味。

普通に使うときは `m` が一番役に立つと思う。

## AND, OR
`()` の中に修飾子を並べて書くとAND検索になる。

```zsh
# 通常ファイルかつパーミッションが755
% echo *(.f755)
script1.sh script2.sh

# ディレクトリかつパーミッションが755
% echo *(/f755)
dir1 dir2

# ファイルの修正時刻が1日より過去かつ7日より最近である
% echo *(m+1m-7)
file1.txt file3.txt
```

`,` で連結するとOR検索になる。

```zsh
# 通常ファイルまたはディレクトリ
% echo *(.,/)
dir1 dir2 file1.txt file2.txt file3.txt script1.sh script2.sh

# 「パーミッションが644の通常ファイル」、または「ディレクトリ」
% echo *(.f644,/)
dir1 dir2 file1.txt file2.txt file3.txt
```

最後の例のように、ORよりANDの方が結合度が上になる。

## 忘れたときはタブ

「ファイル名修飾子」は他にも色々種類があって、なかなか覚えにくいと思う。そういうときは `(` の後にタブを押すと候補が補完されるので、それを見て選ぶといい。

```zsh
% echo *( [TAB]を押す
%  -- device files
)  -- end of qualifiers
*  -- executable plain files
+  -- + command name
-  -- follow symlinks toggle
.  -- plain files
/  -- directories
... 略
```

その他細かいことは man zshexpn の「FILENAME GENERATION」-&gt;「Glob Qualifiers」を読むともっと詳しく書いてある。

あんまりやり過ぎると逆にわけわかんなくなるけど、うまく使うとスマートに書けると思うので使ってみてください!