dirlens 1.0.7 → 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 +95 -94
- package/dirlens.py +335 -161
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -111,23 +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`
|
|
130
|
-
-
|
|
135
|
+
- **クリップボードコピー** — `-c` で出力を自動コピー(ANSIコードを自動除去)
|
|
136
|
+
- **AIモード** — `--ai` 一発で gitignore除外・日時・Markdown・クリップボードコピーを全適用
|
|
137
|
+
- **隠しファイル対応** — `-a` で表示切り替え
|
|
131
138
|
- **サイズ順ソート** — `-s` で大きいものから表示
|
|
132
139
|
|
|
133
140
|
---
|
|
@@ -135,94 +142,86 @@ Desktop/ (2 dirs, 2 files, 3.74 MB)
|
|
|
135
142
|
## 使い方
|
|
136
143
|
|
|
137
144
|
```bash
|
|
138
|
-
#
|
|
139
|
-
dirlens
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
#
|
|
145
|
-
dirlens -d 2
|
|
146
|
-
|
|
147
|
-
#
|
|
148
|
-
dirlens -
|
|
149
|
-
|
|
150
|
-
#
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
# .gitignore
|
|
154
|
-
dirlens -g
|
|
155
|
-
|
|
156
|
-
#
|
|
157
|
-
dirlens
|
|
158
|
-
|
|
159
|
-
#
|
|
160
|
-
dirlens --
|
|
161
|
-
|
|
162
|
-
#
|
|
163
|
-
dirlens
|
|
164
|
-
|
|
165
|
-
#
|
|
166
|
-
dirlens
|
|
167
|
-
|
|
168
|
-
#
|
|
169
|
-
dirlens
|
|
170
|
-
|
|
171
|
-
#
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
#
|
|
175
|
-
dirlens --
|
|
176
|
-
dirlens --
|
|
177
|
-
|
|
178
|
-
#
|
|
179
|
-
dirlens -
|
|
180
|
-
|
|
181
|
-
# JSON 形式で出力(スクリプト連携)
|
|
182
|
-
dirlens --json
|
|
183
|
-
|
|
184
|
-
# HTML レポートを生成(デフォルト: dirlens.html)
|
|
185
|
-
dirlens --html
|
|
186
|
-
dirlens --html report.html # ファイル名を指定
|
|
187
|
-
|
|
188
|
-
# 出力をクリップボードにコピー
|
|
189
|
-
dirlens -c
|
|
190
|
-
|
|
191
|
-
# カラーなし(パイプ・ファイル書き出し向け)
|
|
192
|
-
dirlens --no-color
|
|
193
|
-
|
|
194
|
-
# ── AI チャットへの貼り付け(推奨の組み合わせ)────────────────
|
|
195
|
-
# gitignore 除外 → Markdown → クリップボードコピー(そのまま貼れる)
|
|
196
|
-
dirlens -g -m -c
|
|
197
|
-
|
|
198
|
-
# テキストファイルに書き出す
|
|
199
|
-
dirlens --no-color > dirlens.txt
|
|
145
|
+
# ── AI チャットへの貼り付け(最もよく使うコマンド)─────────────
|
|
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 # テキストファイルに書き出す
|
|
200
187
|
```
|
|
201
188
|
|
|
202
189
|
---
|
|
203
190
|
|
|
204
191
|
## オプション一覧
|
|
205
192
|
|
|
206
|
-
| オプション | 省略形
|
|
207
|
-
|
|
208
|
-
| `path` | —
|
|
209
|
-
|
|
|
210
|
-
| `--
|
|
211
|
-
| `--
|
|
212
|
-
|
|
|
213
|
-
| `--
|
|
214
|
-
|
|
|
215
|
-
|
|
|
216
|
-
| `--
|
|
217
|
-
| `--
|
|
218
|
-
| `--
|
|
219
|
-
| `--
|
|
220
|
-
| `--
|
|
221
|
-
| `--
|
|
222
|
-
| `--
|
|
223
|
-
|
|
|
224
|
-
| `--
|
|
225
|
-
| `--
|
|
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` | カラー表示を無効化 |
|
|
226
225
|
|
|
227
226
|
---
|
|
228
227
|
|
|
@@ -241,8 +240,10 @@ dirlens --no-color > dirlens.txt
|
|
|
241
240
|
## 仕様・注意事項
|
|
242
241
|
|
|
243
242
|
- ディレクトリのサイズは **全サブファイルの合計**(隠しファイルを含む、`.gitignore` 対象も含む)
|
|
244
|
-
-
|
|
243
|
+
- ディレクトリサイズはルート直下を **並列プリフェッチ** して高速化(透過的な最適化)
|
|
244
|
+
- **シンボリックリンク** は `→ リンク先パス` で表示。`-l` でリンク先ディレクトリを展開(循環検出あり)
|
|
245
245
|
- 権限がないディレクトリは `[アクセス拒否]` と表示してスキップ
|
|
246
|
-
- 非常に深いディレクトリ(1万階層以上)は `-d` で深さを制限してください
|
|
247
|
-
- **ホームフォルダ(`~/`)やルート(`/`)で実行すると固まる場合があります** —
|
|
248
|
-
- **`-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,30 +70,53 @@ 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
|
|
116
|
+
|
|
117
|
+
import re as _re
|
|
118
|
+
def strip_ansi(text):
|
|
119
|
+
return _re.sub(r'\033\[[0-9;]*[mK]', '', text)
|
|
96
120
|
|
|
97
121
|
|
|
98
122
|
# ─── 絵文字 ───────────────────────────────────────────────────
|
|
@@ -123,7 +147,7 @@ def get_emoji(name, is_dir=False):
|
|
|
123
147
|
return _EMOJI_NAME.get(lower) or _EMOJI_EXT.get(os.path.splitext(lower)[1], "📄")
|
|
124
148
|
|
|
125
149
|
|
|
126
|
-
# ─── .gitignore
|
|
150
|
+
# ─── .gitignore(否定パターン対応) ──────────────────────────
|
|
127
151
|
_gi_cache = {}
|
|
128
152
|
|
|
129
153
|
def load_gitignore(directory):
|
|
@@ -142,22 +166,28 @@ def load_gitignore(directory):
|
|
|
142
166
|
return pats
|
|
143
167
|
|
|
144
168
|
def is_ignored(name, rel_path, is_dir, patterns):
|
|
169
|
+
"""パターンを順番に評価し最後にマッチしたルールが勝つ(!否定対応)。"""
|
|
145
170
|
rel = rel_path.replace("\\", "/")
|
|
171
|
+
result = False
|
|
146
172
|
for pat in patterns:
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
|
151
180
|
if p.startswith("/"):
|
|
152
|
-
|
|
181
|
+
matched = fnmatch.fnmatch(rel, p.lstrip("/"))
|
|
153
182
|
else:
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
|
158
189
|
|
|
159
190
|
def _extend_pats(active_pats, path, cfg):
|
|
160
|
-
"""サブディレクトリの .gitignore を読み込んでパターンを拡張する(ルートは除く)。"""
|
|
161
191
|
if not cfg.use_gitignore: return active_pats
|
|
162
192
|
if os.path.normpath(path) == os.path.normpath(cfg.root): return active_pats
|
|
163
193
|
local = load_gitignore(path)
|
|
@@ -165,8 +195,10 @@ def _extend_pats(active_pats, path, cfg):
|
|
|
165
195
|
rel_dir = os.path.relpath(path, cfg.root).replace("\\", "/")
|
|
166
196
|
adjusted = []
|
|
167
197
|
for pat in local:
|
|
168
|
-
|
|
169
|
-
|
|
198
|
+
neg = pat.startswith("!")
|
|
199
|
+
p = pat.lstrip("!")
|
|
200
|
+
if p.startswith("/"):
|
|
201
|
+
adjusted.append(("!" if neg else "") + "/" + rel_dir + p)
|
|
170
202
|
else:
|
|
171
203
|
adjusted.append(pat)
|
|
172
204
|
return active_pats + adjusted
|
|
@@ -191,6 +223,16 @@ def dir_size(path):
|
|
|
191
223
|
_sz_cache[path] = total
|
|
192
224
|
return total
|
|
193
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
|
+
|
|
194
236
|
|
|
195
237
|
# ─── 設定クラス ───────────────────────────────────────────────
|
|
196
238
|
class Cfg:
|
|
@@ -198,21 +240,31 @@ class Cfg:
|
|
|
198
240
|
self.root = root
|
|
199
241
|
self.max_depth = args.depth
|
|
200
242
|
self.show_all = args.all
|
|
201
|
-
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)
|
|
202
246
|
self.show_date = args.date
|
|
203
|
-
self.use_gitignore = args.gitignore
|
|
247
|
+
self.use_gitignore = args.gitignore # -G
|
|
204
248
|
self.show_bar = args.bar
|
|
205
249
|
self.min_size = parse_size(args.min_size) if args.min_size else None
|
|
206
250
|
self.max_size = parse_size(args.max_size) if args.max_size else None
|
|
207
251
|
self.excludes = args.exclude or []
|
|
208
252
|
self.includes = args.include or []
|
|
209
253
|
self.show_emoji = args.emoji
|
|
210
|
-
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
|
|
211
264
|
|
|
212
265
|
|
|
213
266
|
# ─── 共通フィルタリング ───────────────────────────────────────
|
|
214
267
|
def _filter(path, cfg, active_pats):
|
|
215
|
-
"""エントリを取得してフィルタリングする。"""
|
|
216
268
|
try: raw = list(os.scandir(path))
|
|
217
269
|
except PermissionError: return [], []
|
|
218
270
|
|
|
@@ -225,8 +277,20 @@ def _filter(path, cfg, active_pats):
|
|
|
225
277
|
e.is_dir(follow_symlinks=False),
|
|
226
278
|
active_pats)]
|
|
227
279
|
|
|
228
|
-
dirs = [e for e in entries if
|
|
229
|
-
|
|
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))]
|
|
230
294
|
|
|
231
295
|
if cfg.excludes:
|
|
232
296
|
dirs = [d for d in dirs if not any(fnmatch.fnmatch(d.name, p) for p in cfg.excludes)]
|
|
@@ -254,16 +318,70 @@ def count_entries(path, cfg, active_pats):
|
|
|
254
318
|
dirs, files = _filter(path, cfg, pats)
|
|
255
319
|
return len(dirs), len(files)
|
|
256
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
|
+
|
|
257
360
|
|
|
258
361
|
# ─── ツリー描画 ───────────────────────────────────────────────
|
|
259
362
|
PIPE = "│ "; FORK = "├── "; LAST = "└── "; BLANK = " "
|
|
260
363
|
|
|
261
|
-
def render(path, prefix, depth, cfg, stats, active_pats):
|
|
364
|
+
def render(path, prefix, depth, cfg, stats, active_pats, _seen=None):
|
|
262
365
|
if cfg.max_depth is not None and depth >= cfg.max_depth: return
|
|
263
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
|
+
|
|
264
375
|
cur_pats = _extend_pats(active_pats, path, cfg)
|
|
265
376
|
dirs, files = _filter(path, cfg, cur_pats)
|
|
266
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
|
+
|
|
267
385
|
def esz(e):
|
|
268
386
|
try: return e.stat(follow_symlinks=True).st_size
|
|
269
387
|
except OSError: return 0
|
|
@@ -271,22 +389,26 @@ def render(path, prefix, depth, cfg, stats, active_pats):
|
|
|
271
389
|
try: return e.stat(follow_symlinks=True).st_mtime
|
|
272
390
|
except OSError: return 0
|
|
273
391
|
|
|
274
|
-
if cfg.by_size:
|
|
275
|
-
dirs.sort(key=lambda e: dir_size(e.path), reverse=True)
|
|
276
|
-
files.sort(key=lambda e: esz(e), reverse=True)
|
|
277
|
-
else:
|
|
278
|
-
dirs.sort(key=lambda e: e.name.casefold())
|
|
279
|
-
files.sort(key=lambda e: e.name.casefold())
|
|
280
|
-
|
|
281
|
-
combined = dirs + files
|
|
282
|
-
cur_dir_size = dir_size(path)
|
|
283
|
-
|
|
284
392
|
for i, entry in enumerate(combined):
|
|
285
393
|
is_last = (i == len(combined) - 1)
|
|
286
394
|
branch = LAST if is_last else FORK
|
|
287
395
|
cont = BLANK if is_last else PIPE
|
|
288
396
|
|
|
289
|
-
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:
|
|
290
412
|
sz = dir_size(entry.path)
|
|
291
413
|
nd, nf = count_entries(entry.path, cfg, cur_pats)
|
|
292
414
|
stats["dirs"] += 1
|
|
@@ -298,14 +420,13 @@ def render(path, prefix, depth, cfg, stats, active_pats):
|
|
|
298
420
|
except OSError: pass
|
|
299
421
|
bar = (" " + fmt_bar(sz, cur_dir_size)) if cfg.show_bar and cur_dir_size else ""
|
|
300
422
|
|
|
301
|
-
name = c(f"{emoji}{
|
|
423
|
+
name = c(f"{emoji}{display}{sym_target}/", BOLD, CYAN)
|
|
302
424
|
meta = c(f"({', '.join(parts)}){bar}", DIM)
|
|
303
|
-
print(f"{prefix}{branch}{name} {meta}")
|
|
304
|
-
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)
|
|
305
427
|
|
|
306
428
|
else:
|
|
307
429
|
sz = esz(entry)
|
|
308
|
-
sym = " →" if entry.is_symlink() else ""
|
|
309
430
|
stats["files"] += 1
|
|
310
431
|
ext = os.path.splitext(entry.name)[1].lower()
|
|
311
432
|
stats["extensions"][ext or "(no ext)"] = \
|
|
@@ -318,40 +439,39 @@ def render(path, prefix, depth, cfg, stats, active_pats):
|
|
|
318
439
|
if mt: parts.append(fmt_date(mt))
|
|
319
440
|
bar = (" " + fmt_bar(sz, cur_dir_size)) if cfg.show_bar and cur_dir_size else ""
|
|
320
441
|
|
|
321
|
-
name = c(f"{emoji}{
|
|
442
|
+
name = c(f"{emoji}{display}{sym_target}",
|
|
443
|
+
MAGENTA if entry.is_symlink() else GREEN)
|
|
322
444
|
meta = c(f"({', '.join(parts)}){bar}", DIM)
|
|
323
|
-
print(f"{prefix}{branch}{name} {meta}")
|
|
445
|
+
print(f"{prefix}{branch}{perm_prefix}{name} {meta}")
|
|
324
446
|
|
|
325
447
|
|
|
326
448
|
# ─── JSON出力 ─────────────────────────────────────────────────
|
|
327
449
|
def build_json_tree(path, depth, cfg, active_pats):
|
|
328
450
|
cur_pats = _extend_pats(active_pats, path, cfg)
|
|
329
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)
|
|
330
455
|
sz = dir_size(path)
|
|
331
456
|
|
|
332
457
|
def esz(e):
|
|
333
458
|
try: return e.stat(follow_symlinks=True).st_size
|
|
334
459
|
except OSError: return 0
|
|
335
460
|
|
|
336
|
-
if cfg.by_size:
|
|
337
|
-
dirs.sort(key=lambda e: dir_size(e.path), reverse=True)
|
|
338
|
-
files.sort(key=lambda e: esz(e), reverse=True)
|
|
339
|
-
else:
|
|
340
|
-
dirs.sort(key=lambda e: e.name.casefold())
|
|
341
|
-
files.sort(key=lambda e: e.name.casefold())
|
|
342
|
-
|
|
343
461
|
children = []
|
|
344
462
|
if cfg.max_depth is None or depth < cfg.max_depth:
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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
|
+
})
|
|
355
475
|
|
|
356
476
|
name = os.path.basename(path) or path
|
|
357
477
|
return {
|
|
@@ -368,6 +488,9 @@ def generate_html(root_path, cfg, active_pats):
|
|
|
368
488
|
def _node(path, depth, cur_pats):
|
|
369
489
|
pats = _extend_pats(cur_pats, path, cfg)
|
|
370
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)
|
|
371
494
|
sz = dir_size(path)
|
|
372
495
|
name = os.path.basename(path) or path
|
|
373
496
|
|
|
@@ -375,28 +498,25 @@ def generate_html(root_path, cfg, active_pats):
|
|
|
375
498
|
try: return e.stat(follow_symlinks=True).st_size
|
|
376
499
|
except OSError: return 0
|
|
377
500
|
|
|
378
|
-
if cfg.
|
|
379
|
-
dirs.sort(key=lambda e: dir_size(e.path), reverse=True)
|
|
380
|
-
files.sort(key=lambda e: esz(e), reverse=True)
|
|
381
|
-
else:
|
|
382
|
-
dirs.sort(key=lambda e: e.name.casefold())
|
|
383
|
-
files.sort(key=lambda e: e.name.casefold())
|
|
384
|
-
|
|
501
|
+
combined = (files + dirs) if cfg.files_first else (dirs + files)
|
|
385
502
|
ch = ""
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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')
|
|
400
520
|
|
|
401
521
|
nd, nf = len(dirs), len(files)
|
|
402
522
|
opened = " open" if depth == 0 else ""
|
|
@@ -406,11 +526,9 @@ def generate_html(root_path, cfg, active_pats):
|
|
|
406
526
|
|
|
407
527
|
root_name = os.path.basename(root_path) or root_path
|
|
408
528
|
tree = _node(root_path, 0, active_pats)
|
|
409
|
-
|
|
410
529
|
return f'''<!DOCTYPE html>
|
|
411
530
|
<html lang="ja"><head>
|
|
412
|
-
<meta charset="UTF-8">
|
|
413
|
-
<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">
|
|
414
532
|
<title>dirlens — {root_name}</title>
|
|
415
533
|
<style>
|
|
416
534
|
*{{box-sizing:border-box;margin:0;padding:0}}
|
|
@@ -429,22 +547,32 @@ summary:hover{{background:rgba(255,255,255,.06)}}
|
|
|
429
547
|
.ch{{border-left:1px solid rgba(255,255,255,.08);margin-left:10px}}
|
|
430
548
|
.item{{padding:2px 6px;white-space:nowrap;margin-left:18px}}
|
|
431
549
|
.item:hover{{background:rgba(255,255,255,.05);border-radius:4px}}
|
|
432
|
-
.fname{{color:#a6e3a1}}
|
|
433
|
-
.
|
|
434
|
-
.emoji{{width:1.6em;display:inline-block}}
|
|
435
|
-
.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}}
|
|
436
552
|
</style></head><body>
|
|
437
553
|
<h1>🌳 dirlens — {root_name}</h1>
|
|
438
554
|
<input id="q" type="text" placeholder="ファイル名で検索…" oninput="search(this.value)">
|
|
439
555
|
<div id="tree">{tree}</div>
|
|
440
556
|
<script>
|
|
441
557
|
function search(q){{
|
|
442
|
-
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
|
+
// マッチしないファイルを非表示
|
|
443
565
|
document.querySelectorAll('.file').forEach(el=>{{
|
|
444
566
|
const n=el.querySelector('.fname')?.textContent.toLowerCase()||'';
|
|
445
|
-
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');
|
|
446
575
|
}});
|
|
447
|
-
if(q) document.querySelectorAll('details').forEach(d=>d.open=true);
|
|
448
576
|
}}
|
|
449
577
|
</script></body></html>'''
|
|
450
578
|
|
|
@@ -460,49 +588,97 @@ def main():
|
|
|
460
588
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
461
589
|
epilog=(
|
|
462
590
|
"使用例:\n"
|
|
463
|
-
" dirlens
|
|
464
|
-
" dirlens
|
|
465
|
-
" dirlens
|
|
466
|
-
" dirlens
|
|
467
|
-
" dirlens --
|
|
468
|
-
" dirlens
|
|
469
|
-
" dirlens
|
|
470
|
-
" dirlens
|
|
471
|
-
" dirlens
|
|
472
|
-
" dirlens > 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 ファイルに書き出す"
|
|
473
601
|
),
|
|
474
602
|
)
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
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互換フラグ(変更なし) ────────────────────────────
|
|
478
627
|
ap.add_argument("-a", "--all", action="store_true")
|
|
479
|
-
ap.add_argument("-
|
|
480
|
-
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 と同じ)")
|
|
481
650
|
ap.add_argument("--date", action="store_true")
|
|
482
|
-
ap.add_argument("-t", "--type", metavar="EXT")
|
|
483
651
|
ap.add_argument("-m", "--markdown", action="store_true")
|
|
484
652
|
ap.add_argument("--no-color", action="store_true")
|
|
485
|
-
|
|
486
|
-
ap.add_argument("--
|
|
487
|
-
|
|
488
|
-
ap.add_argument("--
|
|
489
|
-
|
|
490
|
-
ap.add_argument("--
|
|
491
|
-
|
|
492
|
-
ap.add_argument("--
|
|
493
|
-
|
|
494
|
-
ap.add_argument("--
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
help="拡張子に応じた絵文字を表示")
|
|
498
|
-
ap.add_argument("--json", action="store_true",
|
|
499
|
-
help="JSON形式で標準出力に出力")
|
|
500
|
-
ap.add_argument("--html", nargs="?", const="dirlens.html", metavar="FILE",
|
|
501
|
-
help="HTMLレポートを生成 (デフォルト: dirlens.html)")
|
|
502
|
-
ap.add_argument("-c", "--copy", action="store_true",
|
|
503
|
-
help="出力をクリップボードにコピー")
|
|
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 のショートカット")
|
|
504
665
|
args = ap.parse_args()
|
|
505
666
|
|
|
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 のショートカット
|
|
676
|
+
if args.ai:
|
|
677
|
+
args.gitignore = True
|
|
678
|
+
args.date = True
|
|
679
|
+
args.markdown = True
|
|
680
|
+
args.copy = True
|
|
681
|
+
|
|
506
682
|
if args.no_color or args.markdown or args.json:
|
|
507
683
|
USE_COLOR = False
|
|
508
684
|
|
|
@@ -514,28 +690,24 @@ def main():
|
|
|
514
690
|
|
|
515
691
|
cfg = Cfg(args, str(target))
|
|
516
692
|
active_pats = load_gitignore(str(target)) if args.gitignore else []
|
|
693
|
+
_prefetch_sizes(str(target))
|
|
517
694
|
|
|
518
|
-
# ── JSON
|
|
695
|
+
# ── JSON ─────────────────────────────────────────────────
|
|
519
696
|
if args.json:
|
|
520
697
|
print(json.dumps(build_json_tree(str(target), 0, cfg, active_pats),
|
|
521
|
-
ensure_ascii=False, indent=2))
|
|
522
|
-
return
|
|
698
|
+
ensure_ascii=False, indent=2)); return
|
|
523
699
|
|
|
524
|
-
# ── HTML
|
|
700
|
+
# ── HTML ─────────────────────────────────────────────────
|
|
525
701
|
if args.html:
|
|
526
702
|
out = Path(args.html)
|
|
527
703
|
out.write_text(generate_html(str(target), cfg, active_pats), encoding="utf-8")
|
|
528
|
-
print(f"✓ {out} を生成しました ({fmt_size(out.stat().st_size)})")
|
|
529
|
-
return
|
|
704
|
+
print(f"✓ {out} を生成しました ({fmt_size(out.stat().st_size)})"); return
|
|
530
705
|
|
|
531
|
-
# ──
|
|
706
|
+
# ── テキスト出力 ─────────────────────────────────────────
|
|
532
707
|
if args.copy:
|
|
533
|
-
_buf = io.StringIO()
|
|
534
|
-
_old = sys.stdout
|
|
535
|
-
sys.stdout = _buf
|
|
708
|
+
_buf = io.StringIO(); _old = sys.stdout; sys.stdout = _buf
|
|
536
709
|
|
|
537
|
-
if args.markdown:
|
|
538
|
-
print("```")
|
|
710
|
+
if args.markdown: print("```")
|
|
539
711
|
|
|
540
712
|
root_sz = dir_size(str(target))
|
|
541
713
|
root_nd, root_nf = count_entries(str(target), cfg, active_pats)
|
|
@@ -553,31 +725,33 @@ def main():
|
|
|
553
725
|
render(str(target), "", 0, cfg, stats, active_pats)
|
|
554
726
|
|
|
555
727
|
print()
|
|
556
|
-
summary = f" 合計 {stats['dirs']}
|
|
557
|
-
if
|
|
558
|
-
|
|
559
|
-
if
|
|
560
|
-
if cfg.
|
|
561
|
-
if cfg.
|
|
562
|
-
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 += " (ディレクトリのみ)"
|
|
563
739
|
print(c(summary, DIM))
|
|
564
740
|
|
|
565
|
-
if stats["extensions"]:
|
|
741
|
+
if not cfg.dirs_only and stats["extensions"]:
|
|
566
742
|
exts = sorted(stats["extensions"].items(), key=lambda x: -x[1])
|
|
567
743
|
print(c(" " + " ".join(f"{e} ×{n}" for e, n in exts[:8]), DIM))
|
|
568
744
|
|
|
569
|
-
if args.markdown:
|
|
570
|
-
print("```")
|
|
745
|
+
if args.markdown: print("```")
|
|
571
746
|
|
|
572
|
-
# ── クリップボードコピー ──────────────────────────────────
|
|
573
747
|
if args.copy:
|
|
574
748
|
sys.stdout = _old
|
|
575
749
|
text = _buf.getvalue()
|
|
576
750
|
print(text, end="")
|
|
577
|
-
ok = copy_to_clipboard(text)
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
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)
|
|
581
755
|
|
|
582
756
|
|
|
583
757
|
if __name__ == "__main__":
|