dirlens 1.0.8 → 1.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +93 -97
- package/dirlens.py +326 -167
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -111,24 +111,30 @@ Desktop/ (2 dirs, 2 files, 3.74 MB)
|
|
|
111
111
|
## 特徴
|
|
112
112
|
|
|
113
113
|
- **クロスプラットフォーム** — macOS / Linux / Windows
|
|
114
|
+
- **tree コマンドとの高い互換性** — **`-a -d -f -g -l -p -u -r -s -t -c -L -D -P -I -n -J --prune --gitignore` など主要フラグが `tree` と互換**。dirlens 独自機能は `-G`(gitignore)・`-S`(サイズ順)・`-e`(拡張子)・`-C`(クリップボード)で提供
|
|
114
115
|
- **カラー表示** — ディレクトリ・ファイル・シンボリックリンクを色で識別
|
|
115
116
|
- **自動サイズ変換** — bytes / KB / MB / GB / TB
|
|
116
|
-
- **ディレクトリサイズ** —
|
|
117
|
+
- **ディレクトリサイズ** — サブディレクトリの合計サイズを自動計算(並列プリフェッチで高速化)
|
|
117
118
|
- **アイテム数表示** — 各ディレクトリの直下にある dirs / files 数を表示
|
|
118
119
|
- **拡張子統計** — ツリー全体のファイル種別を集計してサマリーに表示
|
|
119
|
-
- **`.gitignore` 対応** — `-g`
|
|
120
|
-
- **最終更新日時** — `--date`
|
|
120
|
+
- **`.gitignore` 対応** — `-g` で除外(サブディレクトリも対応・否定パターン `!` 対応)
|
|
121
|
+
- **最終更新日時** — `--date` / `-D` で相対表示
|
|
121
122
|
- **拡張子フィルタ** — `-t py` など指定した拡張子のみ表示
|
|
122
|
-
- **パターンフィルタ** — `--exclude` /
|
|
123
|
+
- **パターンフィルタ** — `--exclude` / `-I`、`--include` / `-P` でワイルドカード指定(複数可)
|
|
123
124
|
- **サイズフィルタ** — `--min-size` / `--max-size` で容量による絞り込み
|
|
125
|
+
- **空ディレクトリの剪定** — `--prune` でフィルタ後に空になる枝を非表示(tree --prune 互換)
|
|
126
|
+
- **パーミッション表示** — `-p` でパーミッション文字列、`-u` で所有者名(tree -p/-u 互換)
|
|
127
|
+
- **シンボリックリンク展開** — `-l` でリンク先を追跡、リンク先パスを `→` で表示
|
|
128
|
+
- **フルパス表示** — `-f` でルートからのパスを表示(tree -f 互換)
|
|
129
|
+
- **逆順ソート** — `-r` でソート順を逆転(tree -r 互換)
|
|
124
130
|
- **ディスク占有率バー** — `--bar` で親ディレクトリに対する占有率を視覚表示
|
|
125
131
|
- **絵文字アイコン** — `--emoji` で拡張子に応じた絵文字を付与
|
|
126
|
-
- **Markdown出力** — `-m`
|
|
127
|
-
- **JSON出力** — `--json`
|
|
132
|
+
- **Markdown出力** — `-m` でコードブロック形式に出力
|
|
133
|
+
- **JSON出力** — `--json` / `-J` で機械可読な構造データを出力
|
|
128
134
|
- **HTMLレポート** — `--html` でブラウザで閲覧できる折りたたみツリーを生成
|
|
129
|
-
- **クリップボードコピー** — `-c`
|
|
135
|
+
- **クリップボードコピー** — `-c` で出力を自動コピー(ANSIコードを自動除去)
|
|
130
136
|
- **AIモード** — `--ai` 一発で gitignore除外・日時・Markdown・クリップボードコピーを全適用
|
|
131
|
-
- **隠しファイル対応** — `-a`
|
|
137
|
+
- **隠しファイル対応** — `-a` で表示切り替え
|
|
132
138
|
- **サイズ順ソート** — `-s` で大きいものから表示
|
|
133
139
|
|
|
134
140
|
---
|
|
@@ -137,97 +143,85 @@ Desktop/ (2 dirs, 2 files, 3.74 MB)
|
|
|
137
143
|
|
|
138
144
|
```bash
|
|
139
145
|
# ── AI チャットへの貼り付け(最もよく使うコマンド)─────────────
|
|
140
|
-
# gitignore除外 + 日時 + Markdown +
|
|
141
|
-
dirlens --ai
|
|
142
|
-
|
|
143
|
-
#
|
|
144
|
-
dirlens
|
|
145
|
-
|
|
146
|
-
#
|
|
147
|
-
dirlens
|
|
148
|
-
|
|
149
|
-
#
|
|
150
|
-
dirlens
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
#
|
|
156
|
-
dirlens -
|
|
157
|
-
|
|
158
|
-
#
|
|
159
|
-
dirlens
|
|
160
|
-
|
|
161
|
-
#
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
#
|
|
165
|
-
dirlens --date
|
|
166
|
-
|
|
167
|
-
# ディスク占有率バーを表示
|
|
168
|
-
dirlens --
|
|
169
|
-
|
|
170
|
-
#
|
|
171
|
-
dirlens
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
#
|
|
177
|
-
dirlens --
|
|
178
|
-
|
|
179
|
-
#
|
|
180
|
-
dirlens --
|
|
181
|
-
|
|
182
|
-
# サイズで絞り込み
|
|
183
|
-
dirlens --min-size 1M # 1MB 以上のみ
|
|
184
|
-
dirlens --max-size 100K # 100KB 以下のみ
|
|
185
|
-
|
|
186
|
-
# Markdown コードブロック形式で出力
|
|
187
|
-
dirlens -m
|
|
188
|
-
|
|
189
|
-
# JSON 形式で出力(スクリプト連携)
|
|
190
|
-
dirlens --json
|
|
191
|
-
|
|
192
|
-
# HTML レポートを生成(デフォルト: dirlens.html)
|
|
193
|
-
dirlens --html
|
|
194
|
-
dirlens --html report.html # ファイル名を指定
|
|
195
|
-
|
|
196
|
-
# 出力をクリップボードにコピー
|
|
197
|
-
dirlens -c
|
|
198
|
-
|
|
199
|
-
# カラーなし(パイプ・ファイル書き出し向け)
|
|
200
|
-
dirlens --no-color
|
|
201
|
-
|
|
202
|
-
# テキストファイルに書き出す
|
|
203
|
-
dirlens --no-color > dirlens.txt
|
|
146
|
+
dirlens --ai # gitignore除外 + 日時 + Markdown + クリップボードコピー
|
|
147
|
+
dirlens --ai -d 3 # 深さ指定と組み合わせ可
|
|
148
|
+
|
|
149
|
+
# ── 表示制御 ──────────────────────────────────────────────────
|
|
150
|
+
dirlens # カレントディレクトリ
|
|
151
|
+
dirlens ~/Desktop # 指定ディレクトリ
|
|
152
|
+
dirlens -d 2 # 深さ2階層まで(tree -L 互換は -L 2)
|
|
153
|
+
dirlens -L 2 # 同上(tree -L 互換)
|
|
154
|
+
dirlens -a # 隠しファイルも表示
|
|
155
|
+
dirlens -r # 逆順ソート(tree -r 互換)
|
|
156
|
+
dirlens --filesfirst # ファイルをディレクトリより先に表示
|
|
157
|
+
dirlens -f # ルートからのフルパスで表示(tree -f 互換)
|
|
158
|
+
|
|
159
|
+
# ── フィルタリング ────────────────────────────────────────────
|
|
160
|
+
dirlens -g # .gitignore のファイルを除外
|
|
161
|
+
dirlens -g --prune # gitignore除外 + 空になった枝を剪定
|
|
162
|
+
dirlens -t py # .py のみ表示
|
|
163
|
+
dirlens -P '*.md' # .md のみ表示(tree -P 互換)
|
|
164
|
+
dirlens -I '*.log' # .log を除外(tree -I 互換)
|
|
165
|
+
dirlens --exclude 'dist' --exclude '*.log' # 複数除外
|
|
166
|
+
dirlens --min-size 1M # 1MB 以上のファイルのみ
|
|
167
|
+
dirlens --max-size 100K # 100KB 以下のファイルのみ
|
|
168
|
+
|
|
169
|
+
# ── 詳細情報 ──────────────────────────────────────────────────
|
|
170
|
+
dirlens -s # サイズの大きい順に表示
|
|
171
|
+
dirlens --date # 最終更新日時を相対表示(tree -D 互換は -D)
|
|
172
|
+
dirlens -D # 同上(tree -D 互換)
|
|
173
|
+
dirlens --bar # ディスク占有率バーを表示
|
|
174
|
+
dirlens --emoji # 絵文字アイコンを表示
|
|
175
|
+
dirlens -p # パーミッションを表示(tree -p 互換)
|
|
176
|
+
dirlens -u # 所有者名を表示(tree -u 互換)
|
|
177
|
+
dirlens -p -u # 両方表示
|
|
178
|
+
dirlens -l # シンボリックリンク先を展開(tree -l 互換)
|
|
179
|
+
|
|
180
|
+
# ── 出力形式 ──────────────────────────────────────────────────
|
|
181
|
+
dirlens -m # Markdown コードブロック形式で出力
|
|
182
|
+
dirlens --json # JSON 形式(tree -J 互換は -J)
|
|
183
|
+
dirlens --html # HTML レポートを生成(デフォルト: dirlens.html)
|
|
184
|
+
dirlens -c # クリップボードにコピー
|
|
185
|
+
dirlens --no-color # カラーなし(tree -n 互換は -n)
|
|
186
|
+
dirlens --no-color > dirlens.txt # テキストファイルに書き出す
|
|
204
187
|
```
|
|
205
188
|
|
|
206
189
|
---
|
|
207
190
|
|
|
208
191
|
## オプション一覧
|
|
209
192
|
|
|
210
|
-
| オプション | 省略形
|
|
211
|
-
|
|
212
|
-
| `path` | —
|
|
213
|
-
| **`--ai`** | —
|
|
214
|
-
| `--depth N` | `-
|
|
215
|
-
| `--all` | `-a`
|
|
216
|
-
|
|
|
217
|
-
| `--
|
|
218
|
-
|
|
|
219
|
-
|
|
|
220
|
-
| `--
|
|
221
|
-
| `--
|
|
222
|
-
| `--
|
|
223
|
-
| `--
|
|
224
|
-
| `--
|
|
225
|
-
| `--
|
|
226
|
-
| `--
|
|
227
|
-
|
|
|
228
|
-
| `--
|
|
229
|
-
| `--
|
|
230
|
-
| `--
|
|
193
|
+
| オプション | 省略形 | 説明 |
|
|
194
|
+
|---------------------|--------------|-------------------------------------------------------------|
|
|
195
|
+
| `path` | — | 対象ディレクトリ(省略時はカレント) |
|
|
196
|
+
| **`--ai`** | — | **`-G --date -m -C` のショートカット。AIチャット用** |
|
|
197
|
+
| `--depth N` | `-L N` | 表示する最大の深さ |
|
|
198
|
+
| `--all` | `-a` | 隠しファイル・ディレクトリも表示 |
|
|
199
|
+
| `-d` | — | ディレクトリのみ表示 |
|
|
200
|
+
| `--sort-size` | `-S` | サイズが大きい順に並べる |
|
|
201
|
+
| `-t` | — | 更新日時順にソート(新しい順) |
|
|
202
|
+
| `-c` | — | ステータス変更日時順にソート |
|
|
203
|
+
| `--reverse` | `-r` | ソート順を逆にする |
|
|
204
|
+
| `--filesfirst` | — | ファイルをディレクトリより先に表示 |
|
|
205
|
+
| `--gitignore` | `-G` | `.gitignore` に記載されたファイルを除外(サブディレクトリも対応)|
|
|
206
|
+
| `--prune` | — | フィルタ後に空になるディレクトリを非表示 |
|
|
207
|
+
| `--date` | `-D` | 最終更新日時を相対表示 |
|
|
208
|
+
| `--perms` | `-p` | パーミッション文字列を表示 |
|
|
209
|
+
| `--user` | `-u` | 所有者のユーザー名を表示 |
|
|
210
|
+
| `-g` | — | グループ名を表示 |
|
|
211
|
+
| `--follow` | `-l` | シンボリックリンク先ディレクトリを展開(循環検出あり) |
|
|
212
|
+
| `--full-path` | `-f` | ルートからのフルパスで表示 |
|
|
213
|
+
| `--type EXT` | `-e EXT` | 指定した拡張子のファイルのみ表示(例: `-e py`) |
|
|
214
|
+
| `--include PATTERN` | `-P PATTERN` | このパターンのみ表示(複数指定可) |
|
|
215
|
+
| `--exclude PATTERN` | `-I PATTERN` | 除外パターン(複数指定可) |
|
|
216
|
+
| `--min-size SIZE` | — | 指定サイズ以上のファイルのみ表示(例: `1M`, `500K`) |
|
|
217
|
+
| `--max-size SIZE` | — | 指定サイズ以下のファイルのみ表示 |
|
|
218
|
+
| `--bar` | — | ディスク占有率バーを表示 |
|
|
219
|
+
| `--emoji` | — | 拡張子に応じた絵文字アイコンを表示 |
|
|
220
|
+
| `--markdown` | `-m` | Markdown コードブロック形式で出力(カラー自動無効) |
|
|
221
|
+
| `--json` | `-J` | JSON 形式で標準出力に出力 |
|
|
222
|
+
| `--html [FILE]` | — | HTML レポートを生成(デフォルト: `dirlens.html`) |
|
|
223
|
+
| `--copy` | `-C` | 出力をクリップボードにコピー(ANSIコードを自動除去) |
|
|
224
|
+
| `--no-color` | `-n` | カラー表示を無効化 |
|
|
231
225
|
|
|
232
226
|
---
|
|
233
227
|
|
|
@@ -246,8 +240,10 @@ dirlens --no-color > dirlens.txt
|
|
|
246
240
|
## 仕様・注意事項
|
|
247
241
|
|
|
248
242
|
- ディレクトリのサイズは **全サブファイルの合計**(隠しファイルを含む、`.gitignore` 対象も含む)
|
|
249
|
-
-
|
|
243
|
+
- ディレクトリサイズはルート直下を **並列プリフェッチ** して高速化(透過的な最適化)
|
|
244
|
+
- **シンボリックリンク** は `→ リンク先パス` で表示。`-l` でリンク先ディレクトリを展開(循環検出あり)
|
|
250
245
|
- 権限がないディレクトリは `[アクセス拒否]` と表示してスキップ
|
|
251
|
-
- 非常に深いディレクトリ(1万階層以上)は `-d` で深さを制限してください
|
|
252
|
-
- **ホームフォルダ(`~/`)やルート(`/`)で実行すると固まる場合があります** —
|
|
253
|
-
- **`-g`
|
|
246
|
+
- 非常に深いディレクトリ(1万階層以上)は `-d`/`-L` で深さを制限してください
|
|
247
|
+
- **ホームフォルダ(`~/`)やルート(`/`)で実行すると固まる場合があります** — サイズ計算は表示深さに関わらず底まで全再帰するため、`~/Library` や iCloud Drive など大容量ディレクトリで時間がかかります
|
|
248
|
+
- **`-g` の否定パターン(`!`)** は対応していますが、ディレクトリを除外した後にその中のファイルを `!` で復活させることは非対応です(`*.log` + `!important.log` のようなファイル単位の否定は動作します)
|
|
249
|
+
- `-p`(パーミッション)・`-u`(ユーザー名)は macOS / Linux のみ対応(Windows では UID 番号を表示)
|
package/dirlens.py
CHANGED
|
@@ -4,7 +4,8 @@ dirlens – ファイルサイズ付きディレクトリツリー表示ツー
|
|
|
4
4
|
対応環境: macOS / Linux / Windows (Python 3.8+)
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
import io, json, os, sys, argparse, fnmatch, datetime, subprocess
|
|
7
|
+
import io, json, os, sys, stat as _stat, argparse, fnmatch, datetime, subprocess
|
|
8
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
|
|
10
11
|
# ─── カラー設定 ──────────────────────────────────────────────
|
|
@@ -69,34 +70,52 @@ def parse_size(s):
|
|
|
69
70
|
except ValueError:
|
|
70
71
|
raise argparse.ArgumentTypeError(f"無効なサイズ: '{s}'(例: 50M, 1G, 500K)")
|
|
71
72
|
|
|
73
|
+
def fmt_perm_info(entry, cfg):
|
|
74
|
+
"""パーミッション・ユーザー・グループ情報を返す。"""
|
|
75
|
+
if not cfg.show_perms and not cfg.show_user and not cfg.show_group:
|
|
76
|
+
return ""
|
|
77
|
+
try:
|
|
78
|
+
st = entry.stat(follow_symlinks=False)
|
|
79
|
+
except OSError:
|
|
80
|
+
return ""
|
|
81
|
+
parts = []
|
|
82
|
+
if cfg.show_perms:
|
|
83
|
+
parts.append(_stat.filemode(st.st_mode))
|
|
84
|
+
if cfg.show_user:
|
|
85
|
+
try:
|
|
86
|
+
import pwd
|
|
87
|
+
parts.append(pwd.getpwuid(st.st_uid).pw_name)
|
|
88
|
+
except (ImportError, KeyError, AttributeError):
|
|
89
|
+
parts.append(str(getattr(st, "st_uid", "?")))
|
|
90
|
+
if cfg.show_group:
|
|
91
|
+
try:
|
|
92
|
+
import grp
|
|
93
|
+
parts.append(grp.getgrgid(st.st_gid).gr_name)
|
|
94
|
+
except (ImportError, KeyError, AttributeError):
|
|
95
|
+
parts.append(str(getattr(st, "st_gid", "?")))
|
|
96
|
+
return c("[" + " ".join(parts) + "] ", DIM) if parts else ""
|
|
97
|
+
|
|
72
98
|
|
|
73
99
|
# ─── クリップボード ───────────────────────────────────────────
|
|
74
100
|
def copy_to_clipboard(text):
|
|
75
101
|
try:
|
|
76
102
|
if sys.platform == "darwin":
|
|
77
103
|
subprocess.run(["pbcopy"], input=text.encode(), check=True,
|
|
78
|
-
stderr=subprocess.DEVNULL)
|
|
79
|
-
return True
|
|
104
|
+
stderr=subprocess.DEVNULL); return True
|
|
80
105
|
if sys.platform == "win32":
|
|
81
106
|
subprocess.run(["clip"], input=text.encode("utf-16"), check=True,
|
|
82
|
-
stderr=subprocess.DEVNULL)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
["xclip", "-selection", "clipboard"],
|
|
86
|
-
["xsel", "--clipboard", "--input"]]:
|
|
107
|
+
stderr=subprocess.DEVNULL); return True
|
|
108
|
+
for cmd in [["wl-copy"],["xclip","-selection","clipboard"],
|
|
109
|
+
["xsel","--clipboard","--input"]]:
|
|
87
110
|
try:
|
|
88
111
|
subprocess.run(cmd, input=text.encode(), check=True,
|
|
89
|
-
stderr=subprocess.DEVNULL)
|
|
90
|
-
|
|
91
|
-
except FileNotFoundError:
|
|
92
|
-
continue
|
|
93
|
-
return False
|
|
94
|
-
except Exception:
|
|
112
|
+
stderr=subprocess.DEVNULL); return True
|
|
113
|
+
except FileNotFoundError: continue
|
|
95
114
|
return False
|
|
115
|
+
except Exception: return False
|
|
96
116
|
|
|
97
117
|
import re as _re
|
|
98
118
|
def strip_ansi(text):
|
|
99
|
-
"""ANSIエスケープシーケンスを除去する。"""
|
|
100
119
|
return _re.sub(r'\033\[[0-9;]*[mK]', '', text)
|
|
101
120
|
|
|
102
121
|
|
|
@@ -128,7 +147,7 @@ def get_emoji(name, is_dir=False):
|
|
|
128
147
|
return _EMOJI_NAME.get(lower) or _EMOJI_EXT.get(os.path.splitext(lower)[1], "📄")
|
|
129
148
|
|
|
130
149
|
|
|
131
|
-
# ─── .gitignore
|
|
150
|
+
# ─── .gitignore(否定パターン対応) ──────────────────────────
|
|
132
151
|
_gi_cache = {}
|
|
133
152
|
|
|
134
153
|
def load_gitignore(directory):
|
|
@@ -147,22 +166,28 @@ def load_gitignore(directory):
|
|
|
147
166
|
return pats
|
|
148
167
|
|
|
149
168
|
def is_ignored(name, rel_path, is_dir, patterns):
|
|
169
|
+
"""パターンを順番に評価し最後にマッチしたルールが勝つ(!否定対応)。"""
|
|
150
170
|
rel = rel_path.replace("\\", "/")
|
|
171
|
+
result = False
|
|
151
172
|
for pat in patterns:
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
173
|
+
negated = pat.startswith("!")
|
|
174
|
+
p = pat.lstrip("!")
|
|
175
|
+
dir_only = p.endswith("/")
|
|
176
|
+
p = p.rstrip("/")
|
|
177
|
+
if dir_only and not is_dir:
|
|
178
|
+
continue
|
|
179
|
+
matched = False
|
|
156
180
|
if p.startswith("/"):
|
|
157
|
-
|
|
181
|
+
matched = fnmatch.fnmatch(rel, p.lstrip("/"))
|
|
158
182
|
else:
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
183
|
+
matched = (fnmatch.fnmatch(name, p) or
|
|
184
|
+
fnmatch.fnmatch(rel, p) or
|
|
185
|
+
fnmatch.fnmatch(rel, "*/" + p))
|
|
186
|
+
if matched:
|
|
187
|
+
result = not negated
|
|
188
|
+
return result
|
|
163
189
|
|
|
164
190
|
def _extend_pats(active_pats, path, cfg):
|
|
165
|
-
"""サブディレクトリの .gitignore を読み込んでパターンを拡張する(ルートは除く)。"""
|
|
166
191
|
if not cfg.use_gitignore: return active_pats
|
|
167
192
|
if os.path.normpath(path) == os.path.normpath(cfg.root): return active_pats
|
|
168
193
|
local = load_gitignore(path)
|
|
@@ -170,8 +195,10 @@ def _extend_pats(active_pats, path, cfg):
|
|
|
170
195
|
rel_dir = os.path.relpath(path, cfg.root).replace("\\", "/")
|
|
171
196
|
adjusted = []
|
|
172
197
|
for pat in local:
|
|
173
|
-
|
|
174
|
-
|
|
198
|
+
neg = pat.startswith("!")
|
|
199
|
+
p = pat.lstrip("!")
|
|
200
|
+
if p.startswith("/"):
|
|
201
|
+
adjusted.append(("!" if neg else "") + "/" + rel_dir + p)
|
|
175
202
|
else:
|
|
176
203
|
adjusted.append(pat)
|
|
177
204
|
return active_pats + adjusted
|
|
@@ -196,6 +223,16 @@ def dir_size(path):
|
|
|
196
223
|
_sz_cache[path] = total
|
|
197
224
|
return total
|
|
198
225
|
|
|
226
|
+
def _prefetch_sizes(root_path):
|
|
227
|
+
try:
|
|
228
|
+
top = [e.path for e in os.scandir(root_path)
|
|
229
|
+
if e.is_dir(follow_symlinks=False)]
|
|
230
|
+
except OSError: return
|
|
231
|
+
if len(top) < 2: return
|
|
232
|
+
workers = min(len(top), (os.cpu_count() or 1), 8)
|
|
233
|
+
with ThreadPoolExecutor(max_workers=workers) as ex:
|
|
234
|
+
list(ex.map(dir_size, top))
|
|
235
|
+
|
|
199
236
|
|
|
200
237
|
# ─── 設定クラス ───────────────────────────────────────────────
|
|
201
238
|
class Cfg:
|
|
@@ -203,21 +240,31 @@ class Cfg:
|
|
|
203
240
|
self.root = root
|
|
204
241
|
self.max_depth = args.depth
|
|
205
242
|
self.show_all = args.all
|
|
206
|
-
self.by_size = args.sort_size
|
|
243
|
+
self.by_size = args.sort_size # -S
|
|
244
|
+
self.sort_mtime = args.sort_mtime # -t (tree)
|
|
245
|
+
self.sort_ctime = args.sort_ctime # -c (tree)
|
|
207
246
|
self.show_date = args.date
|
|
208
|
-
self.use_gitignore = args.gitignore
|
|
247
|
+
self.use_gitignore = args.gitignore # -G
|
|
209
248
|
self.show_bar = args.bar
|
|
210
249
|
self.min_size = parse_size(args.min_size) if args.min_size else None
|
|
211
250
|
self.max_size = parse_size(args.max_size) if args.max_size else None
|
|
212
251
|
self.excludes = args.exclude or []
|
|
213
252
|
self.includes = args.include or []
|
|
214
253
|
self.show_emoji = args.emoji
|
|
215
|
-
self.type_ext = ("." + args.type.lstrip(".")).lower() if args.type else None
|
|
254
|
+
self.type_ext = ("." + args.type.lstrip(".")).lower() if args.type else None # -e
|
|
255
|
+
self.show_perms = args.perms
|
|
256
|
+
self.show_user = args.user
|
|
257
|
+
self.show_group = args.show_group # -g (tree)
|
|
258
|
+
self.dirs_only = args.dirs_only # -d (tree)
|
|
259
|
+
self.follow_syms = args.follow
|
|
260
|
+
self.full_path = args.full_path
|
|
261
|
+
self.prune = args.prune
|
|
262
|
+
self.reverse = args.reverse
|
|
263
|
+
self.files_first = args.filesfirst
|
|
216
264
|
|
|
217
265
|
|
|
218
266
|
# ─── 共通フィルタリング ───────────────────────────────────────
|
|
219
267
|
def _filter(path, cfg, active_pats):
|
|
220
|
-
"""エントリを取得してフィルタリングする。"""
|
|
221
268
|
try: raw = list(os.scandir(path))
|
|
222
269
|
except PermissionError: return [], []
|
|
223
270
|
|
|
@@ -230,8 +277,20 @@ def _filter(path, cfg, active_pats):
|
|
|
230
277
|
e.is_dir(follow_symlinks=False),
|
|
231
278
|
active_pats)]
|
|
232
279
|
|
|
233
|
-
dirs = [e for e in entries if
|
|
234
|
-
|
|
280
|
+
dirs = [e for e in entries if e.is_dir(follow_symlinks=False)]
|
|
281
|
+
|
|
282
|
+
if cfg.follow_syms:
|
|
283
|
+
sym_dirs = [e for e in entries
|
|
284
|
+
if e.is_symlink() and not e.is_dir(follow_symlinks=False)
|
|
285
|
+
and e.is_dir(follow_symlinks=True)]
|
|
286
|
+
dirs = dirs + sym_dirs
|
|
287
|
+
|
|
288
|
+
# -d (dirs only): ファイルを非表示
|
|
289
|
+
if cfg.dirs_only:
|
|
290
|
+
files = []
|
|
291
|
+
else:
|
|
292
|
+
files = [e for e in entries if not e.is_dir(follow_symlinks=False)
|
|
293
|
+
and not (cfg.follow_syms and e.is_symlink() and e.is_dir(follow_symlinks=True))]
|
|
235
294
|
|
|
236
295
|
if cfg.excludes:
|
|
237
296
|
dirs = [d for d in dirs if not any(fnmatch.fnmatch(d.name, p) for p in cfg.excludes)]
|
|
@@ -259,16 +318,70 @@ def count_entries(path, cfg, active_pats):
|
|
|
259
318
|
dirs, files = _filter(path, cfg, pats)
|
|
260
319
|
return len(dirs), len(files)
|
|
261
320
|
|
|
321
|
+
def _has_content(path, depth, cfg, active_pats):
|
|
322
|
+
if cfg.max_depth is not None and depth >= cfg.max_depth: return False
|
|
323
|
+
pats = _extend_pats(active_pats, path, cfg)
|
|
324
|
+
dirs, files = _filter(path, cfg, pats)
|
|
325
|
+
if files: return True
|
|
326
|
+
for d in dirs:
|
|
327
|
+
if _has_content(d.path, depth + 1, cfg, pats): return True
|
|
328
|
+
return False
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
# ─── ソートヘルパー ───────────────────────────────────────────
|
|
332
|
+
def _sort_entries(dirs, files, cfg):
|
|
333
|
+
"""設定に基づいてdirs/filesをソートする(-t/-c/-S/-r対応)。"""
|
|
334
|
+
def emtime(e):
|
|
335
|
+
try: return e.stat(follow_symlinks=True).st_mtime
|
|
336
|
+
except OSError: return 0
|
|
337
|
+
def ectime(e):
|
|
338
|
+
try: return e.stat(follow_symlinks=True).st_ctime
|
|
339
|
+
except OSError: return 0
|
|
340
|
+
def esz(e):
|
|
341
|
+
try: return e.stat(follow_symlinks=True).st_size
|
|
342
|
+
except OSError: return 0
|
|
343
|
+
|
|
344
|
+
rev = cfg.reverse
|
|
345
|
+
if cfg.sort_mtime: # -t: 更新日時順(新しい順)
|
|
346
|
+
dirs.sort(key=emtime, reverse=not rev)
|
|
347
|
+
files.sort(key=emtime, reverse=not rev)
|
|
348
|
+
elif cfg.sort_ctime: # -c: ctime順(新しい順)
|
|
349
|
+
dirs.sort(key=ectime, reverse=not rev)
|
|
350
|
+
files.sort(key=ectime, reverse=not rev)
|
|
351
|
+
elif cfg.by_size: # -S: サイズ順(大きい順)
|
|
352
|
+
dirs.sort(key=lambda e: dir_size(e.path), reverse=not rev)
|
|
353
|
+
files.sort(key=esz, reverse=not rev)
|
|
354
|
+
else: # デフォルト: アルファベット順
|
|
355
|
+
dirs.sort(key=lambda e: e.name.casefold(), reverse=rev)
|
|
356
|
+
files.sort(key=lambda e: e.name.casefold(), reverse=rev)
|
|
357
|
+
|
|
358
|
+
return dirs, files
|
|
359
|
+
|
|
262
360
|
|
|
263
361
|
# ─── ツリー描画 ───────────────────────────────────────────────
|
|
264
362
|
PIPE = "│ "; FORK = "├── "; LAST = "└── "; BLANK = " "
|
|
265
363
|
|
|
266
|
-
def render(path, prefix, depth, cfg, stats, active_pats):
|
|
364
|
+
def render(path, prefix, depth, cfg, stats, active_pats, _seen=None):
|
|
267
365
|
if cfg.max_depth is not None and depth >= cfg.max_depth: return
|
|
268
366
|
|
|
367
|
+
if cfg.follow_syms:
|
|
368
|
+
if _seen is None: _seen = set()
|
|
369
|
+
real = os.path.realpath(path)
|
|
370
|
+
if real in _seen:
|
|
371
|
+
print(f"{prefix}{LAST}{c('[循環リンク]', DIM)}")
|
|
372
|
+
return
|
|
373
|
+
_seen = _seen | {real}
|
|
374
|
+
|
|
269
375
|
cur_pats = _extend_pats(active_pats, path, cfg)
|
|
270
376
|
dirs, files = _filter(path, cfg, cur_pats)
|
|
271
377
|
|
|
378
|
+
if cfg.prune:
|
|
379
|
+
dirs = [d for d in dirs if _has_content(d.path, depth + 1, cfg, cur_pats)]
|
|
380
|
+
|
|
381
|
+
dirs, files = _sort_entries(dirs, files, cfg)
|
|
382
|
+
combined = (files + dirs) if cfg.files_first else (dirs + files)
|
|
383
|
+
cur_dir_size = dir_size(path)
|
|
384
|
+
|
|
272
385
|
def esz(e):
|
|
273
386
|
try: return e.stat(follow_symlinks=True).st_size
|
|
274
387
|
except OSError: return 0
|
|
@@ -276,22 +389,26 @@ def render(path, prefix, depth, cfg, stats, active_pats):
|
|
|
276
389
|
try: return e.stat(follow_symlinks=True).st_mtime
|
|
277
390
|
except OSError: return 0
|
|
278
391
|
|
|
279
|
-
if cfg.by_size:
|
|
280
|
-
dirs.sort(key=lambda e: dir_size(e.path), reverse=True)
|
|
281
|
-
files.sort(key=lambda e: esz(e), reverse=True)
|
|
282
|
-
else:
|
|
283
|
-
dirs.sort(key=lambda e: e.name.casefold())
|
|
284
|
-
files.sort(key=lambda e: e.name.casefold())
|
|
285
|
-
|
|
286
|
-
combined = dirs + files
|
|
287
|
-
cur_dir_size = dir_size(path)
|
|
288
|
-
|
|
289
392
|
for i, entry in enumerate(combined):
|
|
290
393
|
is_last = (i == len(combined) - 1)
|
|
291
394
|
branch = LAST if is_last else FORK
|
|
292
395
|
cont = BLANK if is_last else PIPE
|
|
293
396
|
|
|
294
|
-
if entry.
|
|
397
|
+
if entry.is_symlink():
|
|
398
|
+
try: sym_target = f" → {os.readlink(entry.path)}"
|
|
399
|
+
except OSError: sym_target = " →"
|
|
400
|
+
else:
|
|
401
|
+
sym_target = ""
|
|
402
|
+
|
|
403
|
+
display = ("./" + os.path.relpath(entry.path, cfg.root).replace("\\", "/")) \
|
|
404
|
+
if cfg.full_path else entry.name
|
|
405
|
+
|
|
406
|
+
perm_prefix = fmt_perm_info(entry, cfg)
|
|
407
|
+
|
|
408
|
+
is_dir_entry = entry.is_dir(follow_symlinks=False) or \
|
|
409
|
+
(cfg.follow_syms and entry.is_symlink() and entry.is_dir(follow_symlinks=True))
|
|
410
|
+
|
|
411
|
+
if is_dir_entry:
|
|
295
412
|
sz = dir_size(entry.path)
|
|
296
413
|
nd, nf = count_entries(entry.path, cfg, cur_pats)
|
|
297
414
|
stats["dirs"] += 1
|
|
@@ -303,14 +420,13 @@ def render(path, prefix, depth, cfg, stats, active_pats):
|
|
|
303
420
|
except OSError: pass
|
|
304
421
|
bar = (" " + fmt_bar(sz, cur_dir_size)) if cfg.show_bar and cur_dir_size else ""
|
|
305
422
|
|
|
306
|
-
name = c(f"{emoji}{
|
|
423
|
+
name = c(f"{emoji}{display}{sym_target}/", BOLD, CYAN)
|
|
307
424
|
meta = c(f"({', '.join(parts)}){bar}", DIM)
|
|
308
|
-
print(f"{prefix}{branch}{name} {meta}")
|
|
309
|
-
render(entry.path, prefix + cont, depth + 1, cfg, stats, cur_pats)
|
|
425
|
+
print(f"{prefix}{branch}{perm_prefix}{name} {meta}")
|
|
426
|
+
render(entry.path, prefix + cont, depth + 1, cfg, stats, cur_pats, _seen)
|
|
310
427
|
|
|
311
428
|
else:
|
|
312
429
|
sz = esz(entry)
|
|
313
|
-
sym = " →" if entry.is_symlink() else ""
|
|
314
430
|
stats["files"] += 1
|
|
315
431
|
ext = os.path.splitext(entry.name)[1].lower()
|
|
316
432
|
stats["extensions"][ext or "(no ext)"] = \
|
|
@@ -323,40 +439,39 @@ def render(path, prefix, depth, cfg, stats, active_pats):
|
|
|
323
439
|
if mt: parts.append(fmt_date(mt))
|
|
324
440
|
bar = (" " + fmt_bar(sz, cur_dir_size)) if cfg.show_bar and cur_dir_size else ""
|
|
325
441
|
|
|
326
|
-
name = c(f"{emoji}{
|
|
442
|
+
name = c(f"{emoji}{display}{sym_target}",
|
|
443
|
+
MAGENTA if entry.is_symlink() else GREEN)
|
|
327
444
|
meta = c(f"({', '.join(parts)}){bar}", DIM)
|
|
328
|
-
print(f"{prefix}{branch}{name} {meta}")
|
|
445
|
+
print(f"{prefix}{branch}{perm_prefix}{name} {meta}")
|
|
329
446
|
|
|
330
447
|
|
|
331
448
|
# ─── JSON出力 ─────────────────────────────────────────────────
|
|
332
449
|
def build_json_tree(path, depth, cfg, active_pats):
|
|
333
450
|
cur_pats = _extend_pats(active_pats, path, cfg)
|
|
334
451
|
dirs, files = _filter(path, cfg, cur_pats)
|
|
452
|
+
if cfg.prune:
|
|
453
|
+
dirs = [d for d in dirs if _has_content(d.path, depth + 1, cfg, cur_pats)]
|
|
454
|
+
dirs, files = _sort_entries(dirs, files, cfg)
|
|
335
455
|
sz = dir_size(path)
|
|
336
456
|
|
|
337
457
|
def esz(e):
|
|
338
458
|
try: return e.stat(follow_symlinks=True).st_size
|
|
339
459
|
except OSError: return 0
|
|
340
460
|
|
|
341
|
-
if cfg.by_size:
|
|
342
|
-
dirs.sort(key=lambda e: dir_size(e.path), reverse=True)
|
|
343
|
-
files.sort(key=lambda e: esz(e), reverse=True)
|
|
344
|
-
else:
|
|
345
|
-
dirs.sort(key=lambda e: e.name.casefold())
|
|
346
|
-
files.sort(key=lambda e: e.name.casefold())
|
|
347
|
-
|
|
348
461
|
children = []
|
|
349
462
|
if cfg.max_depth is None or depth < cfg.max_depth:
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
463
|
+
combined = (files + dirs) if cfg.files_first else (dirs + files)
|
|
464
|
+
for entry in combined:
|
|
465
|
+
if entry.is_dir(follow_symlinks=False):
|
|
466
|
+
children.append(build_json_tree(entry.path, depth + 1, cfg, cur_pats))
|
|
467
|
+
else:
|
|
468
|
+
f_sz = esz(entry)
|
|
469
|
+
children.append({
|
|
470
|
+
"name": entry.name, "type": "file",
|
|
471
|
+
"size": f_sz, "size_human": fmt_size(f_sz),
|
|
472
|
+
"ext": os.path.splitext(entry.name)[1].lower(),
|
|
473
|
+
"path": os.path.relpath(entry.path, cfg.root),
|
|
474
|
+
})
|
|
360
475
|
|
|
361
476
|
name = os.path.basename(path) or path
|
|
362
477
|
return {
|
|
@@ -373,6 +488,9 @@ def generate_html(root_path, cfg, active_pats):
|
|
|
373
488
|
def _node(path, depth, cur_pats):
|
|
374
489
|
pats = _extend_pats(cur_pats, path, cfg)
|
|
375
490
|
dirs, files = _filter(path, cfg, pats)
|
|
491
|
+
if cfg.prune:
|
|
492
|
+
dirs = [d for d in dirs if _has_content(d.path, depth + 1, cfg, pats)]
|
|
493
|
+
dirs, files = _sort_entries(dirs, files, cfg)
|
|
376
494
|
sz = dir_size(path)
|
|
377
495
|
name = os.path.basename(path) or path
|
|
378
496
|
|
|
@@ -380,28 +498,25 @@ def generate_html(root_path, cfg, active_pats):
|
|
|
380
498
|
try: return e.stat(follow_symlinks=True).st_size
|
|
381
499
|
except OSError: return 0
|
|
382
500
|
|
|
383
|
-
if cfg.
|
|
384
|
-
dirs.sort(key=lambda e: dir_size(e.path), reverse=True)
|
|
385
|
-
files.sort(key=lambda e: esz(e), reverse=True)
|
|
386
|
-
else:
|
|
387
|
-
dirs.sort(key=lambda e: e.name.casefold())
|
|
388
|
-
files.sort(key=lambda e: e.name.casefold())
|
|
389
|
-
|
|
501
|
+
combined = (files + dirs) if cfg.files_first else (dirs + files)
|
|
390
502
|
ch = ""
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
503
|
+
for entry in combined:
|
|
504
|
+
if entry.is_dir(follow_symlinks=False):
|
|
505
|
+
if cfg.max_depth is None or depth < cfg.max_depth:
|
|
506
|
+
ch += _node(entry.path, depth + 1, pats)
|
|
507
|
+
else:
|
|
508
|
+
ch += (f'<div class="item dir-leaf">📁 {entry.name}/'
|
|
509
|
+
f' <span class="sz">{fmt_size(dir_size(entry.path))}</span></div>\n')
|
|
510
|
+
else:
|
|
511
|
+
f_sz = esz(entry)
|
|
512
|
+
sym = ""
|
|
513
|
+
if entry.is_symlink():
|
|
514
|
+
try: sym = f' → {os.readlink(entry.path)}'
|
|
515
|
+
except OSError: sym = ' →'
|
|
516
|
+
ch += (f'<div class="item file">'
|
|
517
|
+
f'<span class="emoji">{get_emoji(entry.name)}</span>'
|
|
518
|
+
f'<span class="fname"> {entry.name}{sym}</span>'
|
|
519
|
+
f'<span class="sz"> {fmt_size(f_sz)}</span></div>\n')
|
|
405
520
|
|
|
406
521
|
nd, nf = len(dirs), len(files)
|
|
407
522
|
opened = " open" if depth == 0 else ""
|
|
@@ -411,11 +526,9 @@ def generate_html(root_path, cfg, active_pats):
|
|
|
411
526
|
|
|
412
527
|
root_name = os.path.basename(root_path) or root_path
|
|
413
528
|
tree = _node(root_path, 0, active_pats)
|
|
414
|
-
|
|
415
529
|
return f'''<!DOCTYPE html>
|
|
416
530
|
<html lang="ja"><head>
|
|
417
|
-
<meta charset="UTF-8">
|
|
418
|
-
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
531
|
+
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
419
532
|
<title>dirlens — {root_name}</title>
|
|
420
533
|
<style>
|
|
421
534
|
*{{box-sizing:border-box;margin:0;padding:0}}
|
|
@@ -434,22 +547,32 @@ summary:hover{{background:rgba(255,255,255,.06)}}
|
|
|
434
547
|
.ch{{border-left:1px solid rgba(255,255,255,.08);margin-left:10px}}
|
|
435
548
|
.item{{padding:2px 6px;white-space:nowrap;margin-left:18px}}
|
|
436
549
|
.item:hover{{background:rgba(255,255,255,.05);border-radius:4px}}
|
|
437
|
-
.fname{{color:#a6e3a1}}
|
|
438
|
-
.
|
|
439
|
-
.emoji{{width:1.6em;display:inline-block}}
|
|
440
|
-
.hidden{{display:none!important}}
|
|
550
|
+
.fname{{color:#a6e3a1}}.sz{{color:#585b70;font-size:12px}}
|
|
551
|
+
.emoji{{width:1.6em;display:inline-block}}.hidden{{display:none!important}}
|
|
441
552
|
</style></head><body>
|
|
442
553
|
<h1>🌳 dirlens — {root_name}</h1>
|
|
443
554
|
<input id="q" type="text" placeholder="ファイル名で検索…" oninput="search(this.value)">
|
|
444
555
|
<div id="tree">{tree}</div>
|
|
445
556
|
<script>
|
|
446
557
|
function search(q){{
|
|
447
|
-
q=q.toLowerCase();
|
|
558
|
+
q=q.toLowerCase().trim();
|
|
559
|
+
// まず全要素を表示に戻す
|
|
560
|
+
document.querySelectorAll('.hidden').forEach(el=>el.classList.remove('hidden'));
|
|
561
|
+
if(!q) return;
|
|
562
|
+
// 全ディレクトリを展開
|
|
563
|
+
document.querySelectorAll('details').forEach(d=>d.open=true);
|
|
564
|
+
// マッチしないファイルを非表示
|
|
448
565
|
document.querySelectorAll('.file').forEach(el=>{{
|
|
449
566
|
const n=el.querySelector('.fname')?.textContent.toLowerCase()||'';
|
|
450
|
-
el.classList.
|
|
567
|
+
if(!n.includes(q)) el.classList.add('hidden');
|
|
568
|
+
}});
|
|
569
|
+
// visible なファイルを1つも持たないディレクトリを非表示
|
|
570
|
+
// (空ディレクトリ・検索に引っかからないディレクトリ両方を処理)
|
|
571
|
+
document.querySelectorAll('#tree details').forEach(detail=>{{
|
|
572
|
+
const hasVisible=[...detail.querySelectorAll('.file')]
|
|
573
|
+
.some(f=>!f.classList.contains('hidden'));
|
|
574
|
+
if(!hasVisible) detail.classList.add('hidden');
|
|
451
575
|
}});
|
|
452
|
-
if(q) document.querySelectorAll('details').forEach(d=>d.open=true);
|
|
453
576
|
}}
|
|
454
577
|
</script></body></html>'''
|
|
455
578
|
|
|
@@ -465,53 +588,91 @@ def main():
|
|
|
465
588
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
466
589
|
epilog=(
|
|
467
590
|
"使用例:\n"
|
|
468
|
-
" dirlens --ai
|
|
469
|
-
" dirlens -
|
|
470
|
-
" dirlens
|
|
471
|
-
" dirlens
|
|
472
|
-
" dirlens --
|
|
473
|
-
" dirlens
|
|
474
|
-
" dirlens
|
|
475
|
-
" dirlens
|
|
476
|
-
" dirlens
|
|
477
|
-
" dirlens --no-color
|
|
478
|
-
" dirlens > dirlens.txt dirlens.txt に書き出す"
|
|
591
|
+
" dirlens --ai AIチャット貼り付け用(推奨)\n"
|
|
592
|
+
" dirlens -d ディレクトリのみ表示(tree -d 互換)\n"
|
|
593
|
+
" dirlens -L 2 深さ 2 まで表示(tree -L 互換)\n"
|
|
594
|
+
" dirlens -t 更新日時順にソート(tree -t 互換)\n"
|
|
595
|
+
" dirlens -G --prune gitignore除外 + 空枝を剪定\n"
|
|
596
|
+
" dirlens -p -u -g パーミッション・ユーザー・グループ表示\n"
|
|
597
|
+
" dirlens -e py .pyのみ表示(旧 -t)\n"
|
|
598
|
+
" dirlens -S サイズ順ソート(旧 -s)\n"
|
|
599
|
+
" dirlens -C クリップボードにコピー(旧 -c)\n"
|
|
600
|
+
" dirlens --no-color > dirlens.txt ファイルに書き出す"
|
|
479
601
|
),
|
|
480
602
|
)
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
ap.add_argument("-d",
|
|
603
|
+
|
|
604
|
+
# ── tree互換フラグ(5つ) ─────────────────────────────────
|
|
605
|
+
ap.add_argument("-d", action="store_true", dest="dirs_only",
|
|
606
|
+
help="ディレクトリのみ表示(tree -d 互換)")
|
|
607
|
+
ap.add_argument("-g", action="store_true", dest="show_group",
|
|
608
|
+
help="グループ名を表示(tree -g 互換)")
|
|
609
|
+
ap.add_argument("-s", action="store_true", dest="show_size_compat",
|
|
610
|
+
help="サイズ表示(常時有効・tree -s 互換)")
|
|
611
|
+
ap.add_argument("-t", action="store_true", dest="sort_mtime",
|
|
612
|
+
help="更新日時順にソート(tree -t 互換)")
|
|
613
|
+
ap.add_argument("-c", action="store_true", dest="sort_ctime",
|
|
614
|
+
help="ステータス変更日時順にソート(tree -c 互換)")
|
|
615
|
+
|
|
616
|
+
# ── dirlens独自フラグ(tree互換でないもの) ───────────────
|
|
617
|
+
ap.add_argument("-G", "--gitignore", action="store_true",
|
|
618
|
+
help=".gitignoreのファイルを除外(旧 -g)")
|
|
619
|
+
ap.add_argument("-S", "--sort-size", action="store_true",
|
|
620
|
+
help="サイズ順にソート(旧 -s)")
|
|
621
|
+
ap.add_argument("-e", "--type", metavar="EXT",
|
|
622
|
+
help="指定した拡張子のみ表示(旧 -t)")
|
|
623
|
+
ap.add_argument("-C", "--copy", action="store_true",
|
|
624
|
+
help="クリップボードにコピー(旧 -c)")
|
|
625
|
+
|
|
626
|
+
# ── tree互換フラグ(変更なし) ────────────────────────────
|
|
484
627
|
ap.add_argument("-a", "--all", action="store_true")
|
|
485
|
-
ap.add_argument("-
|
|
486
|
-
ap.add_argument("-
|
|
628
|
+
ap.add_argument("-f", "--full-path", action="store_true", dest="full_path")
|
|
629
|
+
ap.add_argument("-l", "--follow", action="store_true")
|
|
630
|
+
ap.add_argument("-p", "--perms", action="store_true")
|
|
631
|
+
ap.add_argument("-u", "--user", action="store_true")
|
|
632
|
+
ap.add_argument("-r", "--reverse", action="store_true")
|
|
633
|
+
ap.add_argument("-n", dest="no_color_tree", action="store_true",
|
|
634
|
+
help="カラーなし(tree -n 互換)")
|
|
635
|
+
ap.add_argument("-J", dest="json_tree", action="store_true",
|
|
636
|
+
help="JSON形式で出力(tree -J 互換)")
|
|
637
|
+
ap.add_argument("-L", dest="level", type=int, metavar="N",
|
|
638
|
+
help="表示する最大の深さ(tree -L 互換)")
|
|
639
|
+
ap.add_argument("-D", dest="date_tree", action="store_true",
|
|
640
|
+
help="最終更新日時を表示(tree -D 互換)")
|
|
641
|
+
ap.add_argument("-P", dest="include_tree", metavar="PATTERN", action="append",
|
|
642
|
+
help="このパターンのみ表示(tree -P 互換)")
|
|
643
|
+
ap.add_argument("-I", dest="exclude_tree", metavar="PATTERN", action="append",
|
|
644
|
+
help="除外パターン(tree -I 互換)")
|
|
645
|
+
|
|
646
|
+
# ── dirlens独自オプション ─────────────────────────────────
|
|
647
|
+
ap.add_argument("path", nargs="?", default=".")
|
|
648
|
+
ap.add_argument("--depth", type=int, metavar="N",
|
|
649
|
+
help="表示する最大の深さ(-L と同じ)")
|
|
487
650
|
ap.add_argument("--date", action="store_true")
|
|
488
|
-
ap.add_argument("-t", "--type", metavar="EXT")
|
|
489
651
|
ap.add_argument("-m", "--markdown", action="store_true")
|
|
490
652
|
ap.add_argument("--no-color", action="store_true")
|
|
491
|
-
|
|
492
|
-
ap.add_argument("--
|
|
493
|
-
|
|
494
|
-
ap.add_argument("--
|
|
495
|
-
|
|
496
|
-
ap.add_argument("--
|
|
497
|
-
|
|
498
|
-
ap.add_argument("--
|
|
499
|
-
|
|
500
|
-
ap.add_argument("--
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
help="拡張子に応じた絵文字を表示")
|
|
504
|
-
ap.add_argument("--json", action="store_true",
|
|
505
|
-
help="JSON形式で標準出力に出力")
|
|
506
|
-
ap.add_argument("--html", nargs="?", const="dirlens.html", metavar="FILE",
|
|
507
|
-
help="HTMLレポートを生成 (デフォルト: dirlens.html)")
|
|
508
|
-
ap.add_argument("-c", "--copy", action="store_true",
|
|
509
|
-
help="出力をクリップボードにコピー")
|
|
510
|
-
ap.add_argument("--ai", action="store_true",
|
|
511
|
-
help="-g --date -m -c のショートカット。AIチャットへの貼り付けに最適化")
|
|
653
|
+
ap.add_argument("--bar", action="store_true")
|
|
654
|
+
ap.add_argument("--min-size", metavar="SIZE")
|
|
655
|
+
ap.add_argument("--max-size", metavar="SIZE")
|
|
656
|
+
ap.add_argument("--exclude", metavar="PATTERN", action="append")
|
|
657
|
+
ap.add_argument("--include", metavar="PATTERN", action="append")
|
|
658
|
+
ap.add_argument("--emoji", action="store_true")
|
|
659
|
+
ap.add_argument("--json", action="store_true")
|
|
660
|
+
ap.add_argument("--html", nargs="?", const="dirlens.html", metavar="FILE")
|
|
661
|
+
ap.add_argument("--prune", action="store_true")
|
|
662
|
+
ap.add_argument("--filesfirst", action="store_true")
|
|
663
|
+
ap.add_argument("--ai", action="store_true",
|
|
664
|
+
help="-G --date -m -C のショートカット")
|
|
512
665
|
args = ap.parse_args()
|
|
513
666
|
|
|
514
|
-
#
|
|
667
|
+
# ── エイリアスのマージ ────────────────────────────────────
|
|
668
|
+
if args.level is not None: args.depth = args.level
|
|
669
|
+
if args.date_tree: args.date = True
|
|
670
|
+
if args.include_tree: args.include = (args.include or []) + args.include_tree
|
|
671
|
+
if args.exclude_tree: args.exclude = (args.exclude or []) + args.exclude_tree
|
|
672
|
+
if args.no_color_tree: args.no_color = True
|
|
673
|
+
if args.json_tree: args.json = True
|
|
674
|
+
|
|
675
|
+
# --ai: -G --date -m -C のショートカット
|
|
515
676
|
if args.ai:
|
|
516
677
|
args.gitignore = True
|
|
517
678
|
args.date = True
|
|
@@ -529,28 +690,24 @@ def main():
|
|
|
529
690
|
|
|
530
691
|
cfg = Cfg(args, str(target))
|
|
531
692
|
active_pats = load_gitignore(str(target)) if args.gitignore else []
|
|
693
|
+
_prefetch_sizes(str(target))
|
|
532
694
|
|
|
533
|
-
# ── JSON
|
|
695
|
+
# ── JSON ─────────────────────────────────────────────────
|
|
534
696
|
if args.json:
|
|
535
697
|
print(json.dumps(build_json_tree(str(target), 0, cfg, active_pats),
|
|
536
|
-
ensure_ascii=False, indent=2))
|
|
537
|
-
return
|
|
698
|
+
ensure_ascii=False, indent=2)); return
|
|
538
699
|
|
|
539
|
-
# ── HTML
|
|
700
|
+
# ── HTML ─────────────────────────────────────────────────
|
|
540
701
|
if args.html:
|
|
541
702
|
out = Path(args.html)
|
|
542
703
|
out.write_text(generate_html(str(target), cfg, active_pats), encoding="utf-8")
|
|
543
|
-
print(f"✓ {out} を生成しました ({fmt_size(out.stat().st_size)})")
|
|
544
|
-
return
|
|
704
|
+
print(f"✓ {out} を生成しました ({fmt_size(out.stat().st_size)})"); return
|
|
545
705
|
|
|
546
|
-
# ──
|
|
706
|
+
# ── テキスト出力 ─────────────────────────────────────────
|
|
547
707
|
if args.copy:
|
|
548
|
-
_buf = io.StringIO()
|
|
549
|
-
_old = sys.stdout
|
|
550
|
-
sys.stdout = _buf
|
|
708
|
+
_buf = io.StringIO(); _old = sys.stdout; sys.stdout = _buf
|
|
551
709
|
|
|
552
|
-
if args.markdown:
|
|
553
|
-
print("```")
|
|
710
|
+
if args.markdown: print("```")
|
|
554
711
|
|
|
555
712
|
root_sz = dir_size(str(target))
|
|
556
713
|
root_nd, root_nf = count_entries(str(target), cfg, active_pats)
|
|
@@ -568,31 +725,33 @@ def main():
|
|
|
568
725
|
render(str(target), "", 0, cfg, stats, active_pats)
|
|
569
726
|
|
|
570
727
|
print()
|
|
571
|
-
summary = f" 合計 {stats['dirs']}
|
|
572
|
-
if
|
|
573
|
-
|
|
574
|
-
if
|
|
575
|
-
if cfg.
|
|
576
|
-
if cfg.
|
|
577
|
-
if cfg.
|
|
728
|
+
summary = f" 合計 {stats['dirs']} ディレクトリ"
|
|
729
|
+
if not cfg.dirs_only:
|
|
730
|
+
summary += f", {stats['files']} ファイル"
|
|
731
|
+
if args.gitignore: summary += " (.gitignore 適用済み)"
|
|
732
|
+
if cfg.type_ext: summary += f" (フィルタ: {cfg.type_ext})"
|
|
733
|
+
if cfg.excludes: summary += f" (除外: {', '.join(cfg.excludes)})"
|
|
734
|
+
if cfg.includes: summary += f" (抽出: {', '.join(cfg.includes)})"
|
|
735
|
+
if cfg.min_size: summary += f" (最小: {fmt_size(cfg.min_size)})"
|
|
736
|
+
if cfg.max_size: summary += f" (最大: {fmt_size(cfg.max_size)})"
|
|
737
|
+
if cfg.prune: summary += " (剪定済み)"
|
|
738
|
+
if cfg.dirs_only: summary += " (ディレクトリのみ)"
|
|
578
739
|
print(c(summary, DIM))
|
|
579
740
|
|
|
580
|
-
if stats["extensions"]:
|
|
741
|
+
if not cfg.dirs_only and stats["extensions"]:
|
|
581
742
|
exts = sorted(stats["extensions"].items(), key=lambda x: -x[1])
|
|
582
743
|
print(c(" " + " ".join(f"{e} ×{n}" for e, n in exts[:8]), DIM))
|
|
583
744
|
|
|
584
|
-
if args.markdown:
|
|
585
|
-
print("```")
|
|
745
|
+
if args.markdown: print("```")
|
|
586
746
|
|
|
587
|
-
# ── クリップボードコピー ──────────────────────────────────
|
|
588
747
|
if args.copy:
|
|
589
748
|
sys.stdout = _old
|
|
590
749
|
text = _buf.getvalue()
|
|
591
|
-
print(text, end="")
|
|
592
|
-
ok = copy_to_clipboard(strip_ansi(text))
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
750
|
+
print(text, end="")
|
|
751
|
+
ok = copy_to_clipboard(strip_ansi(text))
|
|
752
|
+
print(c("✓ クリップボードにコピーしました" if ok
|
|
753
|
+
else "✗ コピー失敗 (pbcopy / xclip / wl-copy が必要)",
|
|
754
|
+
BOLD, GREEN if ok else DIM), file=sys.stderr)
|
|
596
755
|
|
|
597
756
|
|
|
598
757
|
if __name__ == "__main__":
|