dirlens 1.0.0 → 1.0.1
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/dirlens.py +193 -0
- package/package.json +3 -3
package/dirlens.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
dirlens – ファイルサイズ付きディレクトリツリー表示ツール
|
|
4
|
+
対応環境: macOS / Linux / Windows (Python 3.8+)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
import argparse
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
# ─── カラー設定 ──────────────────────────────────────────────
|
|
13
|
+
def _enable_color():
|
|
14
|
+
if not hasattr(sys.stdout, "isatty") or not sys.stdout.isatty():
|
|
15
|
+
return False
|
|
16
|
+
if os.name == "nt":
|
|
17
|
+
# Windows: VT100モードを有効にする
|
|
18
|
+
try:
|
|
19
|
+
import ctypes
|
|
20
|
+
kernel32 = ctypes.windll.kernel32
|
|
21
|
+
kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
|
|
22
|
+
except Exception:
|
|
23
|
+
pass
|
|
24
|
+
return bool(
|
|
25
|
+
os.environ.get("WT_SESSION") # Windows Terminal
|
|
26
|
+
or os.environ.get("TERM_PROGRAM") # VS Code 等
|
|
27
|
+
or os.environ.get("TERM")
|
|
28
|
+
or os.environ.get("ANSICON")
|
|
29
|
+
)
|
|
30
|
+
return True
|
|
31
|
+
|
|
32
|
+
USE_COLOR = _enable_color()
|
|
33
|
+
|
|
34
|
+
RESET = "\033[0m"
|
|
35
|
+
BOLD = "\033[1m"
|
|
36
|
+
DIM = "\033[2m"
|
|
37
|
+
BLUE = "\033[34m"
|
|
38
|
+
CYAN = "\033[36m"
|
|
39
|
+
GREEN = "\033[32m"
|
|
40
|
+
MAGENTA = "\033[35m"
|
|
41
|
+
|
|
42
|
+
def c(text, *codes):
|
|
43
|
+
"""ANSIカラーを適用する。カラー無効時はそのまま返す。"""
|
|
44
|
+
return ("".join(codes) + text + RESET) if USE_COLOR else text
|
|
45
|
+
|
|
46
|
+
# ─── サイズ表示 ───────────────────────────────────────────────
|
|
47
|
+
def fmt_size(n):
|
|
48
|
+
"""バイト数を人が読みやすい文字列に変換する。"""
|
|
49
|
+
if n == 0:
|
|
50
|
+
return "0 bytes"
|
|
51
|
+
for unit, factor in (("TB", 1 << 40), ("GB", 1 << 30), ("MB", 1 << 20), ("KB", 1 << 10)):
|
|
52
|
+
if n >= factor:
|
|
53
|
+
s = f"{n / factor:.2f}".rstrip("0").rstrip(".")
|
|
54
|
+
return f"{s} {unit}"
|
|
55
|
+
return f"{n} {'byte' if n == 1 else 'bytes'}"
|
|
56
|
+
|
|
57
|
+
# ─── ディレクトリサイズ(キャッシュ付き) ─────────────────────
|
|
58
|
+
_cache = {}
|
|
59
|
+
|
|
60
|
+
def dir_size(path):
|
|
61
|
+
"""ディレクトリ以下の合計バイト数を再帰的に計算する(シンボリックリンクは追わない)。"""
|
|
62
|
+
if path in _cache:
|
|
63
|
+
return _cache[path]
|
|
64
|
+
total = 0
|
|
65
|
+
try:
|
|
66
|
+
with os.scandir(path) as it:
|
|
67
|
+
for e in it:
|
|
68
|
+
try:
|
|
69
|
+
if e.is_file(follow_symlinks=False):
|
|
70
|
+
total += e.stat(follow_symlinks=False).st_size
|
|
71
|
+
elif e.is_dir(follow_symlinks=False):
|
|
72
|
+
total += dir_size(e.path)
|
|
73
|
+
except OSError:
|
|
74
|
+
pass
|
|
75
|
+
except OSError:
|
|
76
|
+
pass
|
|
77
|
+
_cache[path] = total
|
|
78
|
+
return total
|
|
79
|
+
|
|
80
|
+
# ─── ツリー描画 ───────────────────────────────────────────────
|
|
81
|
+
PIPE = "│ "
|
|
82
|
+
FORK = "├── "
|
|
83
|
+
LAST = "└── "
|
|
84
|
+
BLANK = " "
|
|
85
|
+
|
|
86
|
+
def render(path, prefix, depth, max_depth, show_all, by_size, stats):
|
|
87
|
+
"""ディレクトリ内容を再帰的にツリー表示する。"""
|
|
88
|
+
if max_depth is not None and depth >= max_depth:
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
raw = list(os.scandir(path))
|
|
93
|
+
except PermissionError:
|
|
94
|
+
print(f"{prefix}{LAST}{c('[アクセス拒否]', DIM)}")
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
# 隠しファイルのフィルタ(オプション依存)
|
|
98
|
+
entries = [e for e in raw if show_all or not e.name.startswith(".")]
|
|
99
|
+
|
|
100
|
+
# ディレクトリ(シンボリックリンク除く)とそれ以外(ファイル+シンボリックリンク)に分類
|
|
101
|
+
dirs = [e for e in entries if e.is_dir(follow_symlinks=False)]
|
|
102
|
+
files = [e for e in entries if not e.is_dir(follow_symlinks=False)]
|
|
103
|
+
|
|
104
|
+
def entry_size(e):
|
|
105
|
+
try:
|
|
106
|
+
return e.stat(follow_symlinks=True).st_size
|
|
107
|
+
except OSError:
|
|
108
|
+
return 0
|
|
109
|
+
|
|
110
|
+
# ソート:名前順 or サイズ順
|
|
111
|
+
if by_size:
|
|
112
|
+
dirs.sort(key=lambda e: dir_size(e.path), reverse=True)
|
|
113
|
+
files.sort(key=lambda e: entry_size(e), reverse=True)
|
|
114
|
+
else:
|
|
115
|
+
dirs.sort(key=lambda e: e.name.casefold())
|
|
116
|
+
files.sort(key=lambda e: e.name.casefold())
|
|
117
|
+
|
|
118
|
+
# ディレクトリを先に、次にファイル
|
|
119
|
+
combined = dirs + files
|
|
120
|
+
|
|
121
|
+
for i, entry in enumerate(combined):
|
|
122
|
+
is_last = (i == len(combined) - 1)
|
|
123
|
+
branch = LAST if is_last else FORK
|
|
124
|
+
cont = BLANK if is_last else PIPE
|
|
125
|
+
|
|
126
|
+
if entry.is_dir(follow_symlinks=False):
|
|
127
|
+
sz = dir_size(entry.path)
|
|
128
|
+
stats["dirs"] += 1
|
|
129
|
+
name = c(f"{entry.name}/", BOLD, CYAN)
|
|
130
|
+
size = c(f"({fmt_size(sz)})", DIM)
|
|
131
|
+
print(f"{prefix}{branch}{name} {size}")
|
|
132
|
+
render(entry.path, prefix + cont, depth + 1, max_depth, show_all, by_size, stats)
|
|
133
|
+
else:
|
|
134
|
+
sz = entry_size(entry)
|
|
135
|
+
sym = " →" if entry.is_symlink() else ""
|
|
136
|
+
stats["files"] += 1
|
|
137
|
+
name = c(f"{entry.name}{sym}", MAGENTA if entry.is_symlink() else GREEN)
|
|
138
|
+
size = c(f"({fmt_size(sz)})", DIM)
|
|
139
|
+
print(f"{prefix}{branch}{name} {size}")
|
|
140
|
+
|
|
141
|
+
# ─── エントリポイント ─────────────────────────────────────────
|
|
142
|
+
def main():
|
|
143
|
+
global USE_COLOR
|
|
144
|
+
sys.setrecursionlimit(10_000)
|
|
145
|
+
|
|
146
|
+
ap = argparse.ArgumentParser(
|
|
147
|
+
prog="dirlens",
|
|
148
|
+
description="ファイルサイズ付きのディレクトリツリーを表示します",
|
|
149
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
150
|
+
epilog=(
|
|
151
|
+
"使用例:\n"
|
|
152
|
+
" dirlens カレントディレクトリを表示\n"
|
|
153
|
+
" dirlens ~/Desktop 指定したディレクトリを表示\n"
|
|
154
|
+
" dirlens -d 2 深さ 2 階層まで表示\n"
|
|
155
|
+
" dirlens -a 隠しファイル (.xxx) も表示\n"
|
|
156
|
+
" dirlens -s サイズの大きい順に表示\n"
|
|
157
|
+
" dirlens --no-color カラーなしで表示"
|
|
158
|
+
),
|
|
159
|
+
)
|
|
160
|
+
ap.add_argument("path", nargs="?", default=".", help="対象ディレクトリ(省略時はカレント)")
|
|
161
|
+
ap.add_argument("-d", "--depth", type=int, metavar="N", help="表示する最大の深さ")
|
|
162
|
+
ap.add_argument("-a", "--all", action="store_true", help="隠しファイルも表示する")
|
|
163
|
+
ap.add_argument("-s", "--sort-size", action="store_true", help="サイズが大きい順に並べる")
|
|
164
|
+
ap.add_argument("--no-color", action="store_true", help="カラー表示を無効化する")
|
|
165
|
+
args = ap.parse_args()
|
|
166
|
+
|
|
167
|
+
if args.no_color:
|
|
168
|
+
USE_COLOR = False
|
|
169
|
+
|
|
170
|
+
target = Path(args.path).resolve()
|
|
171
|
+
if not target.exists():
|
|
172
|
+
print(f"エラー: '{args.path}' が見つかりません", file=sys.stderr)
|
|
173
|
+
sys.exit(1)
|
|
174
|
+
if not target.is_dir():
|
|
175
|
+
print(f"エラー: '{args.path}' はディレクトリではありません", file=sys.stderr)
|
|
176
|
+
sys.exit(1)
|
|
177
|
+
|
|
178
|
+
# ドライブルート(Windows の C:\ 等)対応
|
|
179
|
+
root_label = target.name if target.name else str(target)
|
|
180
|
+
|
|
181
|
+
root_sz = dir_size(str(target))
|
|
182
|
+
root_name = c(f"{root_label}/", BOLD, BLUE)
|
|
183
|
+
root_size = c(f"({fmt_size(root_sz)})", DIM)
|
|
184
|
+
print(f"{root_name} {root_size}")
|
|
185
|
+
|
|
186
|
+
stats = {"files": 0, "dirs": 0}
|
|
187
|
+
render(str(target), "", 0, args.depth, args.all, args.sort_size, stats)
|
|
188
|
+
|
|
189
|
+
print()
|
|
190
|
+
print(c(f" {stats['dirs']} ディレクトリ, {stats['files']} ファイル", DIM))
|
|
191
|
+
|
|
192
|
+
if __name__ == "__main__":
|
|
193
|
+
main()
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dirlens",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Directory tree viewer with file sizes / ファイルサイズ付きディレクトリツリー表示ツール",
|
|
5
5
|
"bin": {
|
|
6
6
|
"dirlens": "./bin/dirlens"
|
|
7
7
|
},
|
|
8
8
|
"files": [
|
|
9
9
|
"bin/",
|
|
10
|
-
"dirlens",
|
|
10
|
+
"dirlens.py",
|
|
11
11
|
"dirlens.bat",
|
|
12
12
|
"README.md"
|
|
13
13
|
],
|
|
@@ -24,4 +24,4 @@
|
|
|
24
24
|
"type": "git",
|
|
25
25
|
"url": "https://github.com/igarinpiano/dirlens.git"
|
|
26
26
|
}
|
|
27
|
-
}
|
|
27
|
+
}
|