dirlens 1.0.9 → 1.0.10
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 +23 -15
- package/dirlens.py +54 -29
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -119,7 +119,7 @@ Desktop/ (2 dirs, 2 files, 3.74 MB)
|
|
|
119
119
|
- **拡張子統計** — ツリー全体のファイル種別を集計してサマリーに表示
|
|
120
120
|
- **`.gitignore` 対応** — `-g` で除外(サブディレクトリも対応・否定パターン `!` 対応)
|
|
121
121
|
- **最終更新日時** — `--date` / `-D` で相対表示
|
|
122
|
-
- **拡張子フィルタ** — `-
|
|
122
|
+
- **拡張子フィルタ** — `-e py` など指定した拡張子のみ表示
|
|
123
123
|
- **パターンフィルタ** — `--exclude` / `-I`、`--include` / `-P` でワイルドカード指定(複数可)
|
|
124
124
|
- **サイズフィルタ** — `--min-size` / `--max-size` で容量による絞り込み
|
|
125
125
|
- **空ディレクトリの剪定** — `--prune` でフィルタ後に空になる枝を非表示(tree --prune 互換)
|
|
@@ -132,7 +132,7 @@ Desktop/ (2 dirs, 2 files, 3.74 MB)
|
|
|
132
132
|
- **Markdown出力** — `-m` でコードブロック形式に出力
|
|
133
133
|
- **JSON出力** — `--json` / `-J` で機械可読な構造データを出力
|
|
134
134
|
- **HTMLレポート** — `--html` でブラウザで閲覧できる折りたたみツリーを生成
|
|
135
|
-
- **クリップボードコピー** — `-
|
|
135
|
+
- **クリップボードコピー** — `-C` で出力を自動コピー(ANSIコードを自動除去)
|
|
136
136
|
- **AIモード** — `--ai` 一発で gitignore除外・日時・Markdown・クリップボードコピーを全適用
|
|
137
137
|
- **隠しファイル対応** — `-a` で表示切り替え
|
|
138
138
|
- **サイズ順ソート** — `-s` で大きいものから表示
|
|
@@ -144,44 +144,50 @@ Desktop/ (2 dirs, 2 files, 3.74 MB)
|
|
|
144
144
|
```bash
|
|
145
145
|
# ── AI チャットへの貼り付け(最もよく使うコマンド)─────────────
|
|
146
146
|
dirlens --ai # gitignore除外 + 日時 + Markdown + クリップボードコピー
|
|
147
|
-
dirlens --ai -
|
|
147
|
+
dirlens --ai -L 3 # 深さ指定と組み合わせ可
|
|
148
148
|
|
|
149
149
|
# ── 表示制御 ──────────────────────────────────────────────────
|
|
150
150
|
dirlens # カレントディレクトリ
|
|
151
151
|
dirlens ~/Desktop # 指定ディレクトリ
|
|
152
|
-
dirlens -
|
|
153
|
-
dirlens -
|
|
152
|
+
dirlens -L 2 # 深さ2階層まで(tree -L 互換)
|
|
153
|
+
dirlens -d # ディレクトリのみ表示(tree -d 互換)
|
|
154
154
|
dirlens -a # 隠しファイルも表示
|
|
155
155
|
dirlens -r # 逆順ソート(tree -r 互換)
|
|
156
156
|
dirlens --filesfirst # ファイルをディレクトリより先に表示
|
|
157
157
|
dirlens -f # ルートからのフルパスで表示(tree -f 互換)
|
|
158
158
|
|
|
159
159
|
# ── フィルタリング ────────────────────────────────────────────
|
|
160
|
-
dirlens -
|
|
161
|
-
dirlens -
|
|
162
|
-
dirlens -
|
|
160
|
+
dirlens -G # .gitignore のファイルを除外
|
|
161
|
+
dirlens -G --prune # gitignore除外 + 空になった枝を剪定
|
|
162
|
+
dirlens -e py # .py のみ表示
|
|
163
163
|
dirlens -P '*.md' # .md のみ表示(tree -P 互換)
|
|
164
164
|
dirlens -I '*.log' # .log を除外(tree -I 互換)
|
|
165
165
|
dirlens --exclude 'dist' --exclude '*.log' # 複数除外
|
|
166
166
|
dirlens --min-size 1M # 1MB 以上のファイルのみ
|
|
167
167
|
dirlens --max-size 100K # 100KB 以下のファイルのみ
|
|
168
168
|
|
|
169
|
+
# ── ソート ────────────────────────────────────────────────────
|
|
170
|
+
dirlens -S # サイズの大きい順に表示
|
|
171
|
+
dirlens -t # 更新日時順にソート(新しい順・tree -t 互換)
|
|
172
|
+
dirlens -c # ステータス変更日時順にソート(tree -c 互換)
|
|
173
|
+
dirlens -t -r # 更新日時順・古い順
|
|
174
|
+
|
|
169
175
|
# ── 詳細情報 ──────────────────────────────────────────────────
|
|
170
|
-
dirlens -s # サイズの大きい順に表示
|
|
171
176
|
dirlens --date # 最終更新日時を相対表示(tree -D 互換は -D)
|
|
172
177
|
dirlens -D # 同上(tree -D 互換)
|
|
173
178
|
dirlens --bar # ディスク占有率バーを表示
|
|
174
179
|
dirlens --emoji # 絵文字アイコンを表示
|
|
175
180
|
dirlens -p # パーミッションを表示(tree -p 互換)
|
|
176
181
|
dirlens -u # 所有者名を表示(tree -u 互換)
|
|
177
|
-
dirlens -
|
|
182
|
+
dirlens -g # グループ名を表示(tree -g 互換)
|
|
183
|
+
dirlens -p -u -g # 全部表示
|
|
178
184
|
dirlens -l # シンボリックリンク先を展開(tree -l 互換)
|
|
179
185
|
|
|
180
186
|
# ── 出力形式 ──────────────────────────────────────────────────
|
|
181
187
|
dirlens -m # Markdown コードブロック形式で出力
|
|
182
188
|
dirlens --json # JSON 形式(tree -J 互換は -J)
|
|
183
189
|
dirlens --html # HTML レポートを生成(デフォルト: dirlens.html)
|
|
184
|
-
dirlens -
|
|
190
|
+
dirlens -C # クリップボードにコピー
|
|
185
191
|
dirlens --no-color # カラーなし(tree -n 互換は -n)
|
|
186
192
|
dirlens --no-color > dirlens.txt # テキストファイルに書き出す
|
|
187
193
|
```
|
|
@@ -198,6 +204,7 @@ dirlens --no-color > dirlens.txt # テキストファイルに書き出す
|
|
|
198
204
|
| `--all` | `-a` | 隠しファイル・ディレクトリも表示 |
|
|
199
205
|
| `-d` | — | ディレクトリのみ表示 |
|
|
200
206
|
| `--sort-size` | `-S` | サイズが大きい順に並べる |
|
|
207
|
+
| `-s` | — | サイズ表示(tree互換・常時表示されているため実質no-op) |
|
|
201
208
|
| `-t` | — | 更新日時順にソート(新しい順) |
|
|
202
209
|
| `-c` | — | ステータス変更日時順にソート |
|
|
203
210
|
| `--reverse` | `-r` | ソート順を逆にする |
|
|
@@ -241,9 +248,10 @@ dirlens --no-color > dirlens.txt # テキストファイルに書き出す
|
|
|
241
248
|
|
|
242
249
|
- ディレクトリのサイズは **全サブファイルの合計**(隠しファイルを含む、`.gitignore` 対象も含む)
|
|
243
250
|
- ディレクトリサイズはルート直下を **並列プリフェッチ** して高速化(透過的な最適化)
|
|
251
|
+
- **`+` 表記** — 一部のサブディレクトリが読めなかった場合、サイズは `1.5+ KB`(少なくとも 1.5 KB)、件数は `3+ dirs` のように表示。個々のファイルサイズは親ディレクトリのスキャン時に取得できるので `+` なし(正確な値)
|
|
252
|
+
- **アクセス拒否** — 読めないディレクトリは赤太字で `[アクセス拒否]` を表示してスキップ。そのディレクトリは `0+ dirs, 0+ files, 0+ bytes` として表示される
|
|
244
253
|
- **シンボリックリンク** は `→ リンク先パス` で表示。`-l` でリンク先ディレクトリを展開(循環検出あり)
|
|
245
|
-
-
|
|
246
|
-
- 非常に深いディレクトリ(1万階層以上)は `-d`/`-L` で深さを制限してください
|
|
254
|
+
- 非常に深いディレクトリ(1万階層以上)は `-L` で深さを制限してください
|
|
247
255
|
- **ホームフォルダ(`~/`)やルート(`/`)で実行すると固まる場合があります** — サイズ計算は表示深さに関わらず底まで全再帰するため、`~/Library` や iCloud Drive など大容量ディレクトリで時間がかかります
|
|
248
|
-
- **`-
|
|
249
|
-
- `-p`(パーミッション)・`-u
|
|
256
|
+
- **`-G` の否定パターン(`!`)** は対応していますが、ディレクトリを除外した後にその中のファイルを `!` で復活させることは非対応です(`*.log` + `!important.log` のようなファイル単位の否定は動作します)
|
|
257
|
+
- `-p`(パーミッション)・`-u`(ユーザー名)・`-g`(グループ名)は macOS / Linux のみ対応(Windows では ID 番号を表示)
|
package/dirlens.py
CHANGED
|
@@ -26,21 +26,27 @@ def _enable_color():
|
|
|
26
26
|
USE_COLOR = _enable_color()
|
|
27
27
|
RESET = "\033[0m"; BOLD = "\033[1m"; DIM = "\033[2m"
|
|
28
28
|
BLUE = "\033[34m"; CYAN = "\033[36m"; GREEN = "\033[32m"; MAGENTA = "\033[35m"
|
|
29
|
+
RED = "\033[31m"
|
|
29
30
|
|
|
30
31
|
def c(text, *codes):
|
|
31
32
|
return ("".join(codes) + text + RESET) if USE_COLOR else text
|
|
32
33
|
|
|
33
34
|
|
|
34
35
|
# ─── フォーマット ─────────────────────────────────────────────
|
|
35
|
-
def fmt_size(n):
|
|
36
|
-
|
|
36
|
+
def fmt_size(n, partial=False):
|
|
37
|
+
sfx = "+" if partial else ""
|
|
38
|
+
if n == 0: return f"0{sfx} bytes"
|
|
37
39
|
for unit, f in (("TB",1<<40),("GB",1<<30),("MB",1<<20),("KB",1<<10)):
|
|
38
40
|
if n >= f:
|
|
39
|
-
return f"{str(f'{n/f:.2f}').rstrip('0').rstrip('.')} {unit}"
|
|
40
|
-
return f"{n} {'byte' if n==1 else 'bytes'}"
|
|
41
|
+
return f"{str(f'{n/f:.2f}').rstrip('0').rstrip('.')}{sfx} {unit}"
|
|
42
|
+
return f"{n}{sfx} {'byte' if (n==1 and not partial) else 'bytes'}"
|
|
41
43
|
|
|
42
|
-
def fmt_count(nd, nf):
|
|
43
|
-
|
|
44
|
+
def fmt_count(nd, nf, denied=False):
|
|
45
|
+
sfx = "+" if denied else ""
|
|
46
|
+
# denied=True のとき数が不明なので複数形固定
|
|
47
|
+
d_word = "dir" if (nd == 1 and not denied) else "dirs"
|
|
48
|
+
f_word = "file" if (nf == 1 and not denied) else "files"
|
|
49
|
+
return f"{nd}{sfx} {d_word}, {nf}{sfx} {f_word}"
|
|
44
50
|
|
|
45
51
|
def fmt_date(mtime):
|
|
46
52
|
sec = int((datetime.datetime.now() - datetime.datetime.fromtimestamp(mtime)).total_seconds())
|
|
@@ -208,8 +214,10 @@ def _extend_pats(active_pats, path, cfg):
|
|
|
208
214
|
_sz_cache = {}
|
|
209
215
|
|
|
210
216
|
def dir_size(path):
|
|
217
|
+
"""ディレクトリの合計サイズを返す。(size, has_errors) のタプル。"""
|
|
211
218
|
if path in _sz_cache: return _sz_cache[path]
|
|
212
219
|
total = 0
|
|
220
|
+
has_errors = False
|
|
213
221
|
try:
|
|
214
222
|
with os.scandir(path) as it:
|
|
215
223
|
for e in it:
|
|
@@ -217,11 +225,16 @@ def dir_size(path):
|
|
|
217
225
|
if e.is_file(follow_symlinks=False):
|
|
218
226
|
total += e.stat(follow_symlinks=False).st_size
|
|
219
227
|
elif e.is_dir(follow_symlinks=False):
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
228
|
+
sub_sz, sub_err = dir_size(e.path)
|
|
229
|
+
total += sub_sz
|
|
230
|
+
if sub_err: has_errors = True
|
|
231
|
+
except OSError:
|
|
232
|
+
has_errors = True
|
|
233
|
+
except OSError:
|
|
234
|
+
has_errors = True
|
|
235
|
+
result = (total, has_errors)
|
|
236
|
+
_sz_cache[path] = result
|
|
237
|
+
return result
|
|
225
238
|
|
|
226
239
|
def _prefetch_sizes(root_path):
|
|
227
240
|
try:
|
|
@@ -266,7 +279,7 @@ class Cfg:
|
|
|
266
279
|
# ─── 共通フィルタリング ───────────────────────────────────────
|
|
267
280
|
def _filter(path, cfg, active_pats):
|
|
268
281
|
try: raw = list(os.scandir(path))
|
|
269
|
-
except PermissionError: return
|
|
282
|
+
except PermissionError: return None, None # アクセス拒否シグナル
|
|
270
283
|
|
|
271
284
|
entries = [e for e in raw if cfg.show_all or not e.name.startswith(".")]
|
|
272
285
|
|
|
@@ -316,12 +329,14 @@ def _filter(path, cfg, active_pats):
|
|
|
316
329
|
def count_entries(path, cfg, active_pats):
|
|
317
330
|
pats = _extend_pats(active_pats, path, cfg)
|
|
318
331
|
dirs, files = _filter(path, cfg, pats)
|
|
319
|
-
return
|
|
332
|
+
if dirs is None: return 0, 0, True # アクセス拒否
|
|
333
|
+
return len(dirs), len(files), False
|
|
320
334
|
|
|
321
335
|
def _has_content(path, depth, cfg, active_pats):
|
|
322
336
|
if cfg.max_depth is not None and depth >= cfg.max_depth: return False
|
|
323
337
|
pats = _extend_pats(active_pats, path, cfg)
|
|
324
338
|
dirs, files = _filter(path, cfg, pats)
|
|
339
|
+
if dirs is None: return False # アクセス拒否は空とみなす
|
|
325
340
|
if files: return True
|
|
326
341
|
for d in dirs:
|
|
327
342
|
if _has_content(d.path, depth + 1, cfg, pats): return True
|
|
@@ -349,7 +364,7 @@ def _sort_entries(dirs, files, cfg):
|
|
|
349
364
|
dirs.sort(key=ectime, reverse=not rev)
|
|
350
365
|
files.sort(key=ectime, reverse=not rev)
|
|
351
366
|
elif cfg.by_size: # -S: サイズ順(大きい順)
|
|
352
|
-
dirs.sort(key=lambda e: dir_size(e.path), reverse=not rev)
|
|
367
|
+
dirs.sort(key=lambda e: dir_size(e.path)[0], reverse=not rev)
|
|
353
368
|
files.sort(key=esz, reverse=not rev)
|
|
354
369
|
else: # デフォルト: アルファベット順
|
|
355
370
|
dirs.sort(key=lambda e: e.name.casefold(), reverse=rev)
|
|
@@ -374,13 +389,16 @@ def render(path, prefix, depth, cfg, stats, active_pats, _seen=None):
|
|
|
374
389
|
|
|
375
390
|
cur_pats = _extend_pats(active_pats, path, cfg)
|
|
376
391
|
dirs, files = _filter(path, cfg, cur_pats)
|
|
392
|
+
if dirs is None: # アクセス拒否
|
|
393
|
+
print(f"{prefix}{LAST}{c('[アクセス拒否]', BOLD, RED)}")
|
|
394
|
+
return
|
|
377
395
|
|
|
378
396
|
if cfg.prune:
|
|
379
397
|
dirs = [d for d in dirs if _has_content(d.path, depth + 1, cfg, cur_pats)]
|
|
380
398
|
|
|
381
399
|
dirs, files = _sort_entries(dirs, files, cfg)
|
|
382
400
|
combined = (files + dirs) if cfg.files_first else (dirs + files)
|
|
383
|
-
cur_dir_size = dir_size(path)
|
|
401
|
+
cur_dir_size, _ = dir_size(path)
|
|
384
402
|
|
|
385
403
|
def esz(e):
|
|
386
404
|
try: return e.stat(follow_symlinks=True).st_size
|
|
@@ -409,12 +427,12 @@ def render(path, prefix, depth, cfg, stats, active_pats, _seen=None):
|
|
|
409
427
|
(cfg.follow_syms and entry.is_symlink() and entry.is_dir(follow_symlinks=True))
|
|
410
428
|
|
|
411
429
|
if is_dir_entry:
|
|
412
|
-
sz = dir_size(entry.path)
|
|
413
|
-
nd, nf = count_entries(entry.path, cfg, cur_pats)
|
|
430
|
+
sz, sz_err = dir_size(entry.path)
|
|
431
|
+
nd, nf, denied = count_entries(entry.path, cfg, cur_pats)
|
|
414
432
|
stats["dirs"] += 1
|
|
415
433
|
|
|
416
434
|
emoji = (get_emoji(entry.name, is_dir=True) + " ") if cfg.show_emoji else ""
|
|
417
|
-
parts = [fmt_count(nd, nf), fmt_size(sz)]
|
|
435
|
+
parts = [fmt_count(nd, nf, denied), fmt_size(sz, sz_err)]
|
|
418
436
|
if cfg.show_date:
|
|
419
437
|
try: parts.append(fmt_date(entry.stat(follow_symlinks=False).st_mtime))
|
|
420
438
|
except OSError: pass
|
|
@@ -433,7 +451,7 @@ def render(path, prefix, depth, cfg, stats, active_pats, _seen=None):
|
|
|
433
451
|
stats["extensions"].get(ext or "(no ext)", 0) + 1
|
|
434
452
|
|
|
435
453
|
emoji = (get_emoji(entry.name) + " ") if cfg.show_emoji else ""
|
|
436
|
-
parts = [fmt_size(sz)]
|
|
454
|
+
parts = [fmt_size(sz)] # ファイルサイズは stat() で正確に取れるので + なし
|
|
437
455
|
if cfg.show_date:
|
|
438
456
|
mt = emtime(entry)
|
|
439
457
|
if mt: parts.append(fmt_date(mt))
|
|
@@ -449,10 +467,12 @@ def render(path, prefix, depth, cfg, stats, active_pats, _seen=None):
|
|
|
449
467
|
def build_json_tree(path, depth, cfg, active_pats):
|
|
450
468
|
cur_pats = _extend_pats(active_pats, path, cfg)
|
|
451
469
|
dirs, files = _filter(path, cfg, cur_pats)
|
|
470
|
+
denied = dirs is None
|
|
471
|
+
if denied: dirs, files = [], []
|
|
452
472
|
if cfg.prune:
|
|
453
473
|
dirs = [d for d in dirs if _has_content(d.path, depth + 1, cfg, cur_pats)]
|
|
454
474
|
dirs, files = _sort_entries(dirs, files, cfg)
|
|
455
|
-
sz = dir_size(path)
|
|
475
|
+
sz, sz_err = dir_size(path)
|
|
456
476
|
|
|
457
477
|
def esz(e):
|
|
458
478
|
try: return e.stat(follow_symlinks=True).st_size
|
|
@@ -476,9 +496,10 @@ def build_json_tree(path, depth, cfg, active_pats):
|
|
|
476
496
|
name = os.path.basename(path) or path
|
|
477
497
|
return {
|
|
478
498
|
"name": name, "type": "directory",
|
|
479
|
-
"size": sz, "size_human": fmt_size(sz),
|
|
499
|
+
"size": sz, "size_human": fmt_size(sz, sz_err),
|
|
480
500
|
"path": os.path.relpath(path, cfg.root) if path != cfg.root else ".",
|
|
481
|
-
"item_count": {"dirs": len(dirs), "files": len(files)
|
|
501
|
+
"item_count": {"dirs": len(dirs), "files": len(files),
|
|
502
|
+
"permission_denied": denied},
|
|
482
503
|
"children": children,
|
|
483
504
|
}
|
|
484
505
|
|
|
@@ -488,10 +509,12 @@ def generate_html(root_path, cfg, active_pats):
|
|
|
488
509
|
def _node(path, depth, cur_pats):
|
|
489
510
|
pats = _extend_pats(cur_pats, path, cfg)
|
|
490
511
|
dirs, files = _filter(path, cfg, pats)
|
|
512
|
+
denied = dirs is None
|
|
513
|
+
if denied: dirs, files = [], []
|
|
491
514
|
if cfg.prune:
|
|
492
515
|
dirs = [d for d in dirs if _has_content(d.path, depth + 1, cfg, pats)]
|
|
493
516
|
dirs, files = _sort_entries(dirs, files, cfg)
|
|
494
|
-
sz = dir_size(path)
|
|
517
|
+
sz, sz_err = dir_size(path)
|
|
495
518
|
name = os.path.basename(path) or path
|
|
496
519
|
|
|
497
520
|
def esz(e):
|
|
@@ -505,8 +528,9 @@ def generate_html(root_path, cfg, active_pats):
|
|
|
505
528
|
if cfg.max_depth is None or depth < cfg.max_depth:
|
|
506
529
|
ch += _node(entry.path, depth + 1, pats)
|
|
507
530
|
else:
|
|
531
|
+
e_sz, e_err = dir_size(entry.path)
|
|
508
532
|
ch += (f'<div class="item dir-leaf">📁 {entry.name}/'
|
|
509
|
-
f' <span class="sz">{fmt_size(
|
|
533
|
+
f' <span class="sz">{fmt_size(e_sz, e_err)}</span></div>\n')
|
|
510
534
|
else:
|
|
511
535
|
f_sz = esz(entry)
|
|
512
536
|
sym = ""
|
|
@@ -521,7 +545,7 @@ def generate_html(root_path, cfg, active_pats):
|
|
|
521
545
|
nd, nf = len(dirs), len(files)
|
|
522
546
|
opened = " open" if depth == 0 else ""
|
|
523
547
|
return (f'<details{opened}><summary>📁 <strong>{name}/</strong>'
|
|
524
|
-
f' <span class="sz">({fmt_count(nd, nf)}, {fmt_size(sz)})</span>'
|
|
548
|
+
f' <span class="sz">({fmt_count(nd, nf, denied)}, {fmt_size(sz, sz_err)})</span>'
|
|
525
549
|
f'</summary><div class="ch">{ch}</div></details>\n')
|
|
526
550
|
|
|
527
551
|
root_name = os.path.basename(root_path) or root_path
|
|
@@ -709,10 +733,11 @@ def main():
|
|
|
709
733
|
|
|
710
734
|
if args.markdown: print("```")
|
|
711
735
|
|
|
712
|
-
root_sz = dir_size(str(target))
|
|
713
|
-
root_nd, root_nf = count_entries(str(target), cfg, active_pats)
|
|
714
|
-
root_label
|
|
715
|
-
|
|
736
|
+
root_sz, root_sz_err = dir_size(str(target))
|
|
737
|
+
root_nd, root_nf, root_denied = count_entries(str(target), cfg, active_pats)
|
|
738
|
+
root_label = target.name if target.name else str(target)
|
|
739
|
+
|
|
740
|
+
root_parts = [fmt_count(root_nd, root_nf, root_denied), fmt_size(root_sz, root_sz_err)]
|
|
716
741
|
if args.date:
|
|
717
742
|
try: root_parts.append(fmt_date(target.stat().st_mtime))
|
|
718
743
|
except OSError: pass
|