dirlens 1.0.0 → 1.0.2
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 +15 -13
- package/bin/dirlens +0 -0
- package/dirlens.py +203 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -8,13 +8,13 @@
|
|
|
8
8
|
## 出力例
|
|
9
9
|
|
|
10
10
|
```
|
|
11
|
-
Desktop/ (3.74 MB)
|
|
12
|
-
├── EmptyDir/ (0 bytes)
|
|
13
|
-
├── Project/ (712 KB)
|
|
14
|
-
│ ├── assets/ (512 KB)
|
|
15
|
-
│ │ └── images/ (512 KB)
|
|
11
|
+
Desktop/ (2 dirs, 2 files, 3.74 MB)
|
|
12
|
+
├── EmptyDir/ (0 dirs, 0 files, 0 bytes)
|
|
13
|
+
├── Project/ (2 dirs, 1 file, 712 KB)
|
|
14
|
+
│ ├── assets/ (1 dir, 0 files, 512 KB)
|
|
15
|
+
│ │ └── images/ (0 dirs, 1 file, 512 KB)
|
|
16
16
|
│ │ └── logo.png (512 KB)
|
|
17
|
-
│ ├── src/ (80 KB)
|
|
17
|
+
│ ├── src/ (0 dirs, 1 file, 80 KB)
|
|
18
18
|
│ │ └── util.py (80 KB)
|
|
19
19
|
│ └── main.py (120 KB)
|
|
20
20
|
├── archive.zip (3 MB)
|
|
@@ -31,7 +31,8 @@ Desktop/ (3.74 MB)
|
|
|
31
31
|
- **カラー表示** — ディレクトリ・ファイル・シンボリックリンクを色で識別
|
|
32
32
|
- **自動サイズ変換** — bytes / KB / MB / GB / TB
|
|
33
33
|
- **ディレクトリサイズ** — サブディレクトリの合計サイズを自動計算
|
|
34
|
-
-
|
|
34
|
+
- **アイテム数表示** — 各ディレクトリの直下にある dirs / files 数を表示
|
|
35
|
+
- **隠しファイル対応** — `-a` で表示切り替え(アイテム数にも反映)
|
|
35
36
|
- **サイズ順ソート** — `-s` で大きいものから表示
|
|
36
37
|
|
|
37
38
|
---
|
|
@@ -62,16 +63,16 @@ npm uninstall -g dirlens
|
|
|
62
63
|
|
|
63
64
|
### macOS / Linux(スクリプト直接インストール)
|
|
64
65
|
|
|
65
|
-
|
|
66
|
-
# 実行権限を付与
|
|
67
|
-
chmod +x dirlens
|
|
66
|
+
GitHubリポジトリから `dirlens.py` をダウンロードして使用します。
|
|
68
67
|
|
|
68
|
+
```bash
|
|
69
69
|
# /usr/local/bin にインストール(どこからでも呼べるようになる)
|
|
70
|
-
sudo
|
|
70
|
+
sudo install -m 755 dirlens.py /usr/local/bin/dirlens
|
|
71
71
|
|
|
72
72
|
# ── または sudo なしでユーザーローカルにインストール ──
|
|
73
73
|
mkdir -p ~/.local/bin
|
|
74
|
-
cp dirlens ~/.local/bin/
|
|
74
|
+
cp dirlens.py ~/.local/bin/dirlens
|
|
75
|
+
chmod +x ~/.local/bin/dirlens
|
|
75
76
|
|
|
76
77
|
# ~/.zshrc(zsh)または ~/.bashrc(bash)に以下を追記:
|
|
77
78
|
export PATH="$HOME/.local/bin:$PATH"
|
|
@@ -89,7 +90,7 @@ dirlens --help
|
|
|
89
90
|
|
|
90
91
|
### Windows(スクリプト直接インストール)
|
|
91
92
|
|
|
92
|
-
1. `dirlens`
|
|
93
|
+
1. `dirlens.py` と `dirlens.bat` を任意のフォルダへ置く
|
|
93
94
|
(例: `C:\Users\ユーザー名\bin\`)
|
|
94
95
|
|
|
95
96
|
2. 同じフォルダに **`dirlens.bat`** を置く(同梱のものを使用):
|
|
@@ -178,3 +179,4 @@ dirlens --no-color > tree.txt
|
|
|
178
179
|
- **シンボリックリンク先のディレクトリ**は展開せず `→` マークで表示
|
|
179
180
|
- 権限がないディレクトリは `[アクセス拒否]` と表示してスキップ
|
|
180
181
|
- 非常に深いディレクトリ(1万階層以上)は `-d` で深さを制限してください
|
|
182
|
+
- **ホームフォルダ(`~/`)やルート(`/`)で実行すると固まる場合があります** — サイズ計算は `-d` の表示制限に関わらず底まで全再帰するため、`~/Library` や iCloud Drive など大容量・ネットワークマウントのディレクトリで時間がかかります。プロジェクトフォルダなど範囲を絞って実行してください
|
package/bin/dirlens
CHANGED
|
File without changes
|
package/dirlens.py
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
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
|
+
try:
|
|
18
|
+
import ctypes
|
|
19
|
+
kernel32 = ctypes.windll.kernel32
|
|
20
|
+
kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
|
|
21
|
+
except Exception:
|
|
22
|
+
pass
|
|
23
|
+
return bool(
|
|
24
|
+
os.environ.get("WT_SESSION")
|
|
25
|
+
or os.environ.get("TERM_PROGRAM")
|
|
26
|
+
or os.environ.get("TERM")
|
|
27
|
+
or os.environ.get("ANSICON")
|
|
28
|
+
)
|
|
29
|
+
return True
|
|
30
|
+
|
|
31
|
+
USE_COLOR = _enable_color()
|
|
32
|
+
|
|
33
|
+
RESET = "\033[0m"
|
|
34
|
+
BOLD = "\033[1m"
|
|
35
|
+
DIM = "\033[2m"
|
|
36
|
+
BLUE = "\033[34m"
|
|
37
|
+
CYAN = "\033[36m"
|
|
38
|
+
GREEN = "\033[32m"
|
|
39
|
+
MAGENTA = "\033[35m"
|
|
40
|
+
|
|
41
|
+
def c(text, *codes):
|
|
42
|
+
return ("".join(codes) + text + RESET) if USE_COLOR else text
|
|
43
|
+
|
|
44
|
+
# ─── サイズ表示 ───────────────────────────────────────────────
|
|
45
|
+
def fmt_size(n):
|
|
46
|
+
if n == 0:
|
|
47
|
+
return "0 bytes"
|
|
48
|
+
for unit, factor in (("TB", 1 << 40), ("GB", 1 << 30), ("MB", 1 << 20), ("KB", 1 << 10)):
|
|
49
|
+
if n >= factor:
|
|
50
|
+
s = f"{n / factor:.2f}".rstrip("0").rstrip(".")
|
|
51
|
+
return f"{s} {unit}"
|
|
52
|
+
return f"{n} {'byte' if n == 1 else 'bytes'}"
|
|
53
|
+
|
|
54
|
+
# ─── ディレクトリサイズ(キャッシュ付き) ─────────────────────
|
|
55
|
+
_cache = {}
|
|
56
|
+
|
|
57
|
+
def dir_size(path):
|
|
58
|
+
if path in _cache:
|
|
59
|
+
return _cache[path]
|
|
60
|
+
total = 0
|
|
61
|
+
try:
|
|
62
|
+
with os.scandir(path) as it:
|
|
63
|
+
for e in it:
|
|
64
|
+
try:
|
|
65
|
+
if e.is_file(follow_symlinks=False):
|
|
66
|
+
total += e.stat(follow_symlinks=False).st_size
|
|
67
|
+
elif e.is_dir(follow_symlinks=False):
|
|
68
|
+
total += dir_size(e.path)
|
|
69
|
+
except OSError:
|
|
70
|
+
pass
|
|
71
|
+
except OSError:
|
|
72
|
+
pass
|
|
73
|
+
_cache[path] = total
|
|
74
|
+
return total
|
|
75
|
+
|
|
76
|
+
# ─── アイテム数カウント ────────────────────────────────────────
|
|
77
|
+
def count_items(path, show_all):
|
|
78
|
+
"""ディレクトリ直下のアイテム数 (num_dirs, num_files) を返す。"""
|
|
79
|
+
try:
|
|
80
|
+
entries = list(os.scandir(path))
|
|
81
|
+
except OSError:
|
|
82
|
+
return (0, 0)
|
|
83
|
+
if not show_all:
|
|
84
|
+
entries = [e for e in entries if not e.name.startswith(".")]
|
|
85
|
+
nd = sum(1 for e in entries if e.is_dir(follow_symlinks=False))
|
|
86
|
+
nf = sum(1 for e in entries if not e.is_dir(follow_symlinks=False))
|
|
87
|
+
return (nd, nf)
|
|
88
|
+
|
|
89
|
+
def fmt_meta(nd, nf, sz):
|
|
90
|
+
"""アイテム数+サイズをまとめて文字列化する。"""
|
|
91
|
+
d_str = f"{nd} {'dir' if nd == 1 else 'dirs'}"
|
|
92
|
+
f_str = f"{nf} {'file' if nf == 1 else 'files'}"
|
|
93
|
+
return f"({d_str}, {f_str}, {fmt_size(sz)})"
|
|
94
|
+
|
|
95
|
+
# ─── ツリー描画 ───────────────────────────────────────────────
|
|
96
|
+
PIPE = "│ "
|
|
97
|
+
FORK = "├── "
|
|
98
|
+
LAST = "└── "
|
|
99
|
+
BLANK = " "
|
|
100
|
+
|
|
101
|
+
def render(path, prefix, depth, max_depth, show_all, by_size, stats):
|
|
102
|
+
if max_depth is not None and depth >= max_depth:
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
raw = list(os.scandir(path))
|
|
107
|
+
except PermissionError:
|
|
108
|
+
print(f"{prefix}{LAST}{c('[アクセス拒否]', DIM)}")
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
entries = [e for e in raw if show_all or not e.name.startswith(".")]
|
|
112
|
+
dirs = [e for e in entries if e.is_dir(follow_symlinks=False)]
|
|
113
|
+
files = [e for e in entries if not e.is_dir(follow_symlinks=False)]
|
|
114
|
+
|
|
115
|
+
def entry_size(e):
|
|
116
|
+
try:
|
|
117
|
+
return e.stat(follow_symlinks=True).st_size
|
|
118
|
+
except OSError:
|
|
119
|
+
return 0
|
|
120
|
+
|
|
121
|
+
if by_size:
|
|
122
|
+
dirs.sort(key=lambda e: dir_size(e.path), reverse=True)
|
|
123
|
+
files.sort(key=lambda e: entry_size(e), reverse=True)
|
|
124
|
+
else:
|
|
125
|
+
dirs.sort(key=lambda e: e.name.casefold())
|
|
126
|
+
files.sort(key=lambda e: e.name.casefold())
|
|
127
|
+
|
|
128
|
+
combined = dirs + files
|
|
129
|
+
|
|
130
|
+
for i, entry in enumerate(combined):
|
|
131
|
+
is_last = (i == len(combined) - 1)
|
|
132
|
+
branch = LAST if is_last else FORK
|
|
133
|
+
cont = BLANK if is_last else PIPE
|
|
134
|
+
|
|
135
|
+
if entry.is_dir(follow_symlinks=False):
|
|
136
|
+
sz = dir_size(entry.path)
|
|
137
|
+
nd, nf = count_items(entry.path, show_all)
|
|
138
|
+
stats["dirs"] += 1
|
|
139
|
+
name = c(f"{entry.name}/", BOLD, CYAN)
|
|
140
|
+
meta = c(fmt_meta(nd, nf, sz), DIM)
|
|
141
|
+
print(f"{prefix}{branch}{name} {meta}")
|
|
142
|
+
render(entry.path, prefix + cont, depth + 1, max_depth, show_all, by_size, stats)
|
|
143
|
+
else:
|
|
144
|
+
sz = entry_size(entry)
|
|
145
|
+
sym = " →" if entry.is_symlink() else ""
|
|
146
|
+
stats["files"] += 1
|
|
147
|
+
name = c(f"{entry.name}{sym}", MAGENTA if entry.is_symlink() else GREEN)
|
|
148
|
+
size = c(f"({fmt_size(sz)})", DIM)
|
|
149
|
+
print(f"{prefix}{branch}{name} {size}")
|
|
150
|
+
|
|
151
|
+
# ─── エントリポイント ─────────────────────────────────────────
|
|
152
|
+
def main():
|
|
153
|
+
global USE_COLOR
|
|
154
|
+
sys.setrecursionlimit(10_000)
|
|
155
|
+
|
|
156
|
+
ap = argparse.ArgumentParser(
|
|
157
|
+
prog="dirlens",
|
|
158
|
+
description="ファイルサイズ+アイテム数付きのディレクトリツリーを表示します",
|
|
159
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
160
|
+
epilog=(
|
|
161
|
+
"使用例:\n"
|
|
162
|
+
" dirlens カレントディレクトリを表示\n"
|
|
163
|
+
" dirlens ~/Desktop 指定したディレクトリを表示\n"
|
|
164
|
+
" dirlens -d 2 深さ 2 階層まで表示\n"
|
|
165
|
+
" dirlens -a 隠しファイル (.xxx) も表示\n"
|
|
166
|
+
" dirlens -s サイズの大きい順に表示\n"
|
|
167
|
+
" dirlens --no-color カラーなしで表示"
|
|
168
|
+
),
|
|
169
|
+
)
|
|
170
|
+
ap.add_argument("path", nargs="?", default=".", help="対象ディレクトリ(省略時はカレント)")
|
|
171
|
+
ap.add_argument("-d", "--depth", type=int, metavar="N", help="表示する最大の深さ")
|
|
172
|
+
ap.add_argument("-a", "--all", action="store_true", help="隠しファイルも表示する")
|
|
173
|
+
ap.add_argument("-s", "--sort-size", action="store_true", help="サイズが大きい順に並べる")
|
|
174
|
+
ap.add_argument("--no-color", action="store_true", help="カラー表示を無効化する")
|
|
175
|
+
args = ap.parse_args()
|
|
176
|
+
|
|
177
|
+
if args.no_color:
|
|
178
|
+
USE_COLOR = False
|
|
179
|
+
|
|
180
|
+
target = Path(args.path).resolve()
|
|
181
|
+
if not target.exists():
|
|
182
|
+
print(f"エラー: '{args.path}' が見つかりません", file=sys.stderr)
|
|
183
|
+
sys.exit(1)
|
|
184
|
+
if not target.is_dir():
|
|
185
|
+
print(f"エラー: '{args.path}' はディレクトリではありません", file=sys.stderr)
|
|
186
|
+
sys.exit(1)
|
|
187
|
+
|
|
188
|
+
root_label = target.name if target.name else str(target)
|
|
189
|
+
root_sz = dir_size(str(target))
|
|
190
|
+
nd, nf = count_items(str(target), args.all)
|
|
191
|
+
|
|
192
|
+
root_name = c(f"{root_label}/", BOLD, BLUE)
|
|
193
|
+
root_meta = c(fmt_meta(nd, nf, root_sz), DIM)
|
|
194
|
+
print(f"{root_name} {root_meta}")
|
|
195
|
+
|
|
196
|
+
stats = {"files": 0, "dirs": 0}
|
|
197
|
+
render(str(target), "", 0, args.depth, args.all, args.sort_size, stats)
|
|
198
|
+
|
|
199
|
+
print()
|
|
200
|
+
print(c(f" {stats['dirs']} ディレクトリ, {stats['files']} ファイル", DIM))
|
|
201
|
+
|
|
202
|
+
if __name__ == "__main__":
|
|
203
|
+
main()
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dirlens",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
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
|
+
}
|