statblk 1.0__tar.gz

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.
statblk-1.0/PKG-INFO ADDED
@@ -0,0 +1,43 @@
1
+ Metadata-Version: 2.4
2
+ Name: statblk
3
+ Version: 1.0
4
+ Summary: Gather essential disk and partition info for block devices and print it in a nice table
5
+ Home-page: https://github.com/yufei-pan/statblk
6
+ Author: Yufei Pan
7
+ Author-email: pan@zopyr.us
8
+ License: GPLv3+
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Operating System :: POSIX :: Linux
11
+ Requires-Python: >=3.6
12
+ Description-Content-Type: text/plain
13
+ Dynamic: author
14
+ Dynamic: author-email
15
+ Dynamic: classifier
16
+ Dynamic: description
17
+ Dynamic: description-content-type
18
+ Dynamic: home-page
19
+ Dynamic: license
20
+ Dynamic: requires-python
21
+ Dynamic: summary
22
+
23
+ usage: statblk.py [-h] [-j] [-b] [-H] [-F] [-M] [-B] [--show_zero_size_devices] [-V]
24
+
25
+ Gather disk and partition info for block devices.
26
+
27
+ options:
28
+ -h, --help show this help message and exit
29
+ -j, --json Produce JSON output
30
+ -b, --bytes Print the SIZE column in bytes rather than in a human-readable format
31
+ -H, --si Use powers of 1000 not 1024 for SIZE column
32
+ -F, -fo, --formated_only
33
+ Show only formated filesystems
34
+ -M, -mo, --mounted_only
35
+ Show only mounted filesystems
36
+ -B, -bo, --best_only Show only best mount for each device
37
+ --show_zero_size_devices
38
+ Show devices with zero size
39
+ -V, --version show program's version number and exit
40
+
41
+
42
+ Install:
43
+ sudo pip install statblk
statblk-1.0/README.txt ADDED
@@ -0,0 +1,21 @@
1
+ usage: statblk.py [-h] [-j] [-b] [-H] [-F] [-M] [-B] [--show_zero_size_devices] [-V]
2
+
3
+ Gather disk and partition info for block devices.
4
+
5
+ options:
6
+ -h, --help show this help message and exit
7
+ -j, --json Produce JSON output
8
+ -b, --bytes Print the SIZE column in bytes rather than in a human-readable format
9
+ -H, --si Use powers of 1000 not 1024 for SIZE column
10
+ -F, -fo, --formated_only
11
+ Show only formated filesystems
12
+ -M, -mo, --mounted_only
13
+ Show only mounted filesystems
14
+ -B, -bo, --best_only Show only best mount for each device
15
+ --show_zero_size_devices
16
+ Show devices with zero size
17
+ -V, --version show program's version number and exit
18
+
19
+
20
+ Install:
21
+ sudo pip install statblk
statblk-1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
statblk-1.0/setup.py ADDED
@@ -0,0 +1,25 @@
1
+ from setuptools import setup
2
+ from statblk import version
3
+
4
+ setup(
5
+ name='statblk',
6
+ version=version,
7
+ description='Gather essential disk and partition info for block devices and print it in a nice table',
8
+ long_description=open('README.txt').read(),
9
+ long_description_content_type='text/plain',
10
+ author='Yufei Pan',
11
+ author_email='pan@zopyr.us',
12
+ url='https://github.com/yufei-pan/statblk',
13
+ py_modules=['statblk'],
14
+ entry_points={
15
+ 'console_scripts': [
16
+ 'statblk=statblk:main',
17
+ ],
18
+ },
19
+ classifiers=[
20
+ 'Programming Language :: Python :: 3',
21
+ 'Operating System :: POSIX :: Linux',
22
+ ],
23
+ python_requires='>=3.6',
24
+ license='GPLv3+',
25
+ )
@@ -0,0 +1,43 @@
1
+ Metadata-Version: 2.4
2
+ Name: statblk
3
+ Version: 1.0
4
+ Summary: Gather essential disk and partition info for block devices and print it in a nice table
5
+ Home-page: https://github.com/yufei-pan/statblk
6
+ Author: Yufei Pan
7
+ Author-email: pan@zopyr.us
8
+ License: GPLv3+
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Operating System :: POSIX :: Linux
11
+ Requires-Python: >=3.6
12
+ Description-Content-Type: text/plain
13
+ Dynamic: author
14
+ Dynamic: author-email
15
+ Dynamic: classifier
16
+ Dynamic: description
17
+ Dynamic: description-content-type
18
+ Dynamic: home-page
19
+ Dynamic: license
20
+ Dynamic: requires-python
21
+ Dynamic: summary
22
+
23
+ usage: statblk.py [-h] [-j] [-b] [-H] [-F] [-M] [-B] [--show_zero_size_devices] [-V]
24
+
25
+ Gather disk and partition info for block devices.
26
+
27
+ options:
28
+ -h, --help show this help message and exit
29
+ -j, --json Produce JSON output
30
+ -b, --bytes Print the SIZE column in bytes rather than in a human-readable format
31
+ -H, --si Use powers of 1000 not 1024 for SIZE column
32
+ -F, -fo, --formated_only
33
+ Show only formated filesystems
34
+ -M, -mo, --mounted_only
35
+ Show only mounted filesystems
36
+ -B, -bo, --best_only Show only best mount for each device
37
+ --show_zero_size_devices
38
+ Show devices with zero size
39
+ -V, --version show program's version number and exit
40
+
41
+
42
+ Install:
43
+ sudo pip install statblk
@@ -0,0 +1,8 @@
1
+ README.txt
2
+ setup.py
3
+ statblk.py
4
+ statblk.egg-info/PKG-INFO
5
+ statblk.egg-info/SOURCES.txt
6
+ statblk.egg-info/dependency_links.txt
7
+ statblk.egg-info/entry_points.txt
8
+ statblk.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ statblk = statblk:main
@@ -0,0 +1 @@
1
+ statblk
statblk-1.0/statblk.py ADDED
@@ -0,0 +1,580 @@
1
+ #!/usr/bin/env python3
2
+ # requires-python = ">=3.6"
3
+ # -*- coding: utf-8 -*-
4
+
5
+
6
+
7
+ import os
8
+ import re
9
+ import stat
10
+ from collections import defaultdict
11
+ import argparse
12
+ import shutil
13
+ import subprocess
14
+
15
+ version = '1.0'
16
+ VERSION = version
17
+ __version__ = version
18
+ COMMIT_DATE = '2025-08-26'
19
+
20
+
21
+ SMARTCTL_PATH = shutil.which("smartctl")
22
+
23
+ def read_text(path):
24
+ try:
25
+ with open(path, "r", encoding="utf-8", errors="ignore") as f:
26
+ return f.read().strip()
27
+ except Exception:
28
+ return None
29
+
30
+
31
+ def read_int(path):
32
+ s = read_text(path)
33
+ if s is None:
34
+ return None
35
+ try:
36
+ return int(s)
37
+ except Exception:
38
+ return None
39
+
40
+
41
+ def build_symlink_map(dir_path):
42
+ """
43
+ Build map: devname -> token (uuid or label string) using symlinks under
44
+ /dev/disk/by-uuid or /dev/disk/by-label.
45
+ """
46
+ mapping = {}
47
+ if not os.path.isdir(dir_path):
48
+ return mapping
49
+ try:
50
+ for entry in os.listdir(dir_path):
51
+ p = os.path.join(dir_path, entry)
52
+ try:
53
+ if os.path.islink(p):
54
+ tgt = os.path.realpath(p)
55
+ devname = os.path.basename(tgt)
56
+ mapping.setdefault(devname, entry)
57
+ except Exception:
58
+ continue
59
+ except Exception:
60
+ pass
61
+ return mapping
62
+
63
+
64
+ def parse_mountinfo_enhanced(uuid_rev, label_rev):
65
+ """
66
+ Parse /proc/self/mountinfo.
67
+
68
+ Returns:
69
+ - by_majmin: dict "major:minor" -> list of mounts
70
+ - by_devname: dict devname -> list of mounts (resolved from source)
71
+ - all_mounts: list of mount dicts
72
+
73
+ Each mount dict: {mountpoint, fstype, source, majmin}
74
+ """
75
+ by_majmin = defaultdict(list)
76
+ by_devname = defaultdict(list)
77
+ all_mounts = []
78
+
79
+ def resolve_source_to_devnames(src):
80
+ # Returns list of candidate devnames for a given source string
81
+ if not src:
82
+ return []
83
+ cands = []
84
+ try:
85
+ if src.startswith("/dev/"):
86
+ real = os.path.realpath(src)
87
+ dn = os.path.basename(real)
88
+ if dn:
89
+ cands.append(dn)
90
+ elif src.startswith("UUID="):
91
+ u = src[5:]
92
+ dn = uuid_rev.get(u)
93
+ if dn:
94
+ cands.append(dn)
95
+ elif src.startswith("LABEL="):
96
+ l = src[6:]
97
+ dn = label_rev.get(l)
98
+ if dn:
99
+ cands.append(dn)
100
+ except Exception:
101
+ pass
102
+ return cands
103
+
104
+ try:
105
+ with open("/proc/self/mountinfo", "r", encoding="utf-8") as f:
106
+ for line in f:
107
+ line = line.strip()
108
+ if not line:
109
+ continue
110
+ parts = line.split()
111
+ try:
112
+ majmin = parts[2]
113
+ mnt_point = parts[4]
114
+ dash_idx = parts.index("-")
115
+ fstype = parts[dash_idx + 1]
116
+ source = parts[dash_idx + 2] if len(parts) > dash_idx + 2 else ""
117
+ rec = {
118
+ "MOUNTPOINT": mnt_point,
119
+ "FSTYPE": fstype,
120
+ "SOURCE": source,
121
+ "MAJMIN": majmin,
122
+ }
123
+ all_mounts.append(rec)
124
+ by_majmin[majmin].append(rec)
125
+ # Build secondary index by devname from source
126
+ for dn in resolve_source_to_devnames(source):
127
+ by_devname[dn].append(rec)
128
+ except Exception:
129
+ continue
130
+ except Exception:
131
+ pass
132
+
133
+ return by_majmin, by_devname, all_mounts
134
+
135
+
136
+ def get_statvfs_use_percent(mountpoint):
137
+ try:
138
+ st = os.statvfs(mountpoint)
139
+ if st.f_blocks == 0:
140
+ return None
141
+ used_pct = 100.0 * (1.0 - (st.f_bavail / float(st.f_blocks)))
142
+ return int(round(used_pct))
143
+ except Exception:
144
+ return None
145
+
146
+
147
+ def read_discard_support(sys_block_path):
148
+ if not sys_block_path or not os.path.isdir(sys_block_path):
149
+ return False
150
+ dmbytes = read_int(os.path.join(sys_block_path, "queue", "discard_max_bytes"))
151
+ dgran = read_int(os.path.join(sys_block_path, "queue", "discard_granularity"))
152
+ try:
153
+ return (dmbytes or 0) > 0 or (dgran or 0) > 0
154
+ except Exception:
155
+ return False
156
+
157
+
158
+ def get_parent_device_sysfs(block_sysfs_path):
159
+ """
160
+ Return the sysfs 'device' directory for this block node (resolves partition
161
+ to its parent device as well).
162
+ """
163
+ dev_link = os.path.join(block_sysfs_path, "device")
164
+ try:
165
+ return os.path.realpath(dev_link)
166
+ except Exception:
167
+ return dev_link
168
+
169
+
170
+ def read_model_and_serial(block_sysfs_path):
171
+ if not block_sysfs_path or not os.path.isdir(block_sysfs_path):
172
+ return None, None
173
+ device_path = get_parent_device_sysfs(block_sysfs_path)
174
+ model = read_text(os.path.join(device_path, "model"))
175
+ serial = read_text(os.path.join(device_path, "serial"))
176
+ if serial is None:
177
+ serial = read_text(os.path.join(device_path, "wwid"))
178
+ if model:
179
+ model = " ".join(model.split())
180
+ if serial:
181
+ serial = " ".join(serial.split())
182
+ return model, serial
183
+
184
+
185
+ def get_udev_props(major, minor):
186
+ """
187
+ Read udev properties for a block device from /run/udev/data/bMAJ:MIN
188
+ Returns keys like ID_FS_TYPE, ID_FS_LABEL, ID_FS_UUID when available.
189
+ """
190
+ props = {}
191
+ path = f"/run/udev/data/b{major}:{minor}"
192
+ try:
193
+ with open(path, "r", encoding="utf-8", errors="ignore") as f:
194
+ for line in f:
195
+ line = line.strip()
196
+ if not line or "=" not in line or line.startswith("#"):
197
+ continue
198
+ k, v = line.split("=", 1)
199
+ props[k] = v
200
+ except Exception:
201
+ pass
202
+ return props
203
+
204
+
205
+ def choose_mount_for_dev(devname, mounts):
206
+ """
207
+ Choose the most relevant mount for a device:
208
+ - Prefer mounts whose source resolves to /dev/<devname>.
209
+ - If multiple, prefer '/' then shortest mountpoint path.
210
+ - Otherwise return the first entry.
211
+ """
212
+ if not mounts:
213
+ return None
214
+
215
+ def score(m):
216
+ mp = m.get("MOUNTPOINT") or "~"
217
+ s = m.get("SOURCE") or ""
218
+ exact = 1 if (s.startswith("/dev/") and os.path.basename(os.path.realpath(s)) == devname) else 0
219
+ root = 1 if mp == "/" else 0
220
+ return (exact, root, -len(mp))
221
+
222
+ best = sorted(mounts, key=score, reverse=True)[0]
223
+ return best
224
+
225
+
226
+ def is_block_device(devpath):
227
+ try:
228
+ st_mode = os.stat(devpath).st_mode
229
+ return stat.S_ISBLK(st_mode)
230
+ except Exception:
231
+ return False
232
+
233
+
234
+ def pretty_format_table(data, delimiter="\t", header=None):
235
+ version = 1.11
236
+ _ = version
237
+ if not data:
238
+ return ""
239
+ if isinstance(data, str):
240
+ data = data.strip("\n").split("\n")
241
+ data = [line.split(delimiter) for line in data]
242
+ elif isinstance(data, dict):
243
+ if isinstance(next(iter(data.values())), dict):
244
+ tempData = [["key"] + list(next(iter(data.values())).keys())]
245
+ tempData.extend(
246
+ [[key] + list(value.values()) for key, value in data.items()]
247
+ )
248
+ data = tempData
249
+ else:
250
+ data = [[key] + list(value) for key, value in data.items()]
251
+ elif not isinstance(data, list):
252
+ data = list(data)
253
+ if isinstance(data[0], dict):
254
+ tempData = [data[0].keys()]
255
+ tempData.extend([list(item.values()) for item in data])
256
+ data = tempData
257
+ data = [[str(item) for item in row] for row in data]
258
+ num_cols = len(data[0])
259
+ col_widths = [0] * num_cols
260
+ for c in range(num_cols):
261
+ col_widths[c] = max(
262
+ len(re.sub(r"\x1b\[[0-?]*[ -/]*[@-~]", "", row[c])) for row in data
263
+ )
264
+ if header:
265
+ header_widths = [
266
+ len(re.sub(r"\x1b\[[0-?]*[ -/]*[@-~]", "", col)) for col in header
267
+ ]
268
+ col_widths = [max(col_widths[i], header_widths[i]) for i in range(num_cols)]
269
+ row_format = " | ".join("{{:<{}}}".format(width) for width in col_widths)
270
+ if not header:
271
+ header = data[0]
272
+ outTable = []
273
+ outTable.append(row_format.format(*header))
274
+ outTable.append("-+-".join("-" * width for width in col_widths))
275
+ for row in data[1:]:
276
+ if not any(row):
277
+ outTable.append("-+-".join("-" * width for width in col_widths))
278
+ else:
279
+ outTable.append(row_format.format(*row))
280
+ else:
281
+ if isinstance(header, str):
282
+ header = header.split(delimiter)
283
+ if len(header) < num_cols:
284
+ header += [""] * (num_cols - len(header))
285
+ elif len(header) > num_cols:
286
+ header = header[:num_cols]
287
+ outTable = []
288
+ outTable.append(row_format.format(*header))
289
+ outTable.append("-+-".join("-" * width for width in col_widths))
290
+ for row in data:
291
+ if not any(row):
292
+ outTable.append("-+-".join("-" * width for width in col_widths))
293
+ else:
294
+ outTable.append(row_format.format(*row))
295
+ return "\n".join(outTable) + "\n"
296
+
297
+
298
+ def format_bytes(
299
+ size, use_1024_bytes=None, to_int=False, to_str=False, str_format=".2f"
300
+ ):
301
+ if to_int or isinstance(size, str):
302
+ if isinstance(size, int):
303
+ return size
304
+ elif isinstance(size, str):
305
+ match = re.match(r"(\d+(\.\d+)?)\s*([a-zA-Z]*)", size)
306
+ if not match:
307
+ if to_str:
308
+ return size
309
+ print(
310
+ "Invalid size format. Expected format: 'number [unit]', "
311
+ "e.g., '1.5 GiB' or '1.5GiB'"
312
+ )
313
+ print(f"Got: {size}")
314
+ return 0
315
+ number, _, unit = match.groups()
316
+ number = float(number)
317
+ unit = unit.strip().lower().rstrip("b")
318
+ if unit.endswith("i"):
319
+ use_1024_bytes = True
320
+ elif use_1024_bytes is None:
321
+ use_1024_bytes = False
322
+ unit = unit.rstrip("i")
323
+ if use_1024_bytes:
324
+ power = 2**10
325
+ else:
326
+ power = 10**3
327
+ unit_labels = {
328
+ "": 0,
329
+ "k": 1,
330
+ "m": 2,
331
+ "g": 3,
332
+ "t": 4,
333
+ "p": 5,
334
+ "e": 6,
335
+ "z": 7,
336
+ "y": 8,
337
+ }
338
+ if unit not in unit_labels:
339
+ if to_str:
340
+ return size
341
+ else:
342
+ if to_str:
343
+ return format_bytes(
344
+ size=int(number * (power**unit_labels[unit])),
345
+ use_1024_bytes=use_1024_bytes,
346
+ to_str=True,
347
+ str_format=str_format,
348
+ )
349
+ return int(number * (power**unit_labels[unit]))
350
+ else:
351
+ try:
352
+ return int(size)
353
+ except Exception:
354
+ return 0
355
+ elif to_str or isinstance(size, int) or isinstance(size, float):
356
+ if isinstance(size, str):
357
+ try:
358
+ size = size.rstrip("B").rstrip("b")
359
+ size = float(size.lower().strip())
360
+ except Exception:
361
+ return size
362
+ if use_1024_bytes or use_1024_bytes is None:
363
+ power = 2**10
364
+ n = 0
365
+ power_labels = {
366
+ 0: "",
367
+ 1: "Ki",
368
+ 2: "Mi",
369
+ 3: "Gi",
370
+ 4: "Ti",
371
+ 5: "Pi",
372
+ 6: "Ei",
373
+ 7: "Zi",
374
+ 8: "Yi",
375
+ }
376
+ while size > power:
377
+ size /= power
378
+ n += 1
379
+ return f"{size:{str_format}} {' '}{power_labels[n]}".replace(" ", " ")
380
+ else:
381
+ power = 10**3
382
+ n = 0
383
+ power_labels = {
384
+ 0: "",
385
+ 1: "K",
386
+ 2: "M",
387
+ 3: "G",
388
+ 4: "T",
389
+ 5: "P",
390
+ 6: "E",
391
+ 7: "Z",
392
+ 8: "Y",
393
+ }
394
+ while size > power:
395
+ size /= power
396
+ n += 1
397
+ return f"{size:{str_format}} {' '}{power_labels[n]}".replace(" ", " ")
398
+ else:
399
+ try:
400
+ return format_bytes(float(size), use_1024_bytes)
401
+ except Exception:
402
+ pass
403
+ return 0
404
+
405
+
406
+ def is_partition(name):
407
+ real = os.path.realpath(os.path.join("/sys/class/block", name))
408
+ return os.path.exists(os.path.join(real, "partition"))
409
+
410
+
411
+ def get_partition_parent_name(name):
412
+ real = os.path.realpath(os.path.join("/sys/class/block", name))
413
+ part_file = os.path.join(real, "partition")
414
+ if not os.path.exists(part_file):
415
+ return None
416
+ parent = os.path.basename(os.path.dirname(real))
417
+ return parent if parent and parent != name else None
418
+
419
+ def get_drives_info(print_bytes = False, use_1024 = False, mounted_only=False, best_only=False, formated_only=False, show_zero_size_devices=False):
420
+ # Build UUID/LABEL maps and reverse maps for resolving mount "source"
421
+ uuid_map = build_symlink_map("/dev/disk/by-uuid")
422
+ label_map = build_symlink_map("/dev/disk/by-label")
423
+ uuid_rev = {v: k for k, v in uuid_map.items()}
424
+ label_rev = {v: k for k, v in label_map.items()}
425
+
426
+ # Mount info maps
427
+ by_majmin, by_devname, _ = parse_mountinfo_enhanced(uuid_rev, label_rev)
428
+
429
+ results = []
430
+ results_by_name = {}
431
+ df_stats_by_name = {} # name -> (blocks, bavail)
432
+ parent_to_children = defaultdict(list)
433
+
434
+ sys_class_block = "/sys/class/block"
435
+ try:
436
+ entries = sorted(os.listdir(sys_class_block))
437
+ except Exception:
438
+ entries = []
439
+
440
+ for name in entries:
441
+ block_path = os.path.join(sys_class_block, name)
442
+
443
+ devnode = os.path.join("/dev", name)
444
+ if not is_block_device(devnode):
445
+ pass
446
+
447
+ parent_name = get_partition_parent_name(name)
448
+ if parent_name:
449
+ parent_to_children[parent_name].append(name)
450
+
451
+ majmin_str = read_text(os.path.join(block_path, "dev"))
452
+ if not majmin_str or ":" not in majmin_str:
453
+ continue
454
+ try:
455
+ major, minor = majmin_str.split(":", 1)
456
+ major = int(major)
457
+ minor = int(minor)
458
+ except Exception:
459
+ continue
460
+
461
+ sectors = read_int(os.path.join(block_path, "size")) or 0
462
+ lb_size = (
463
+ read_int(os.path.join(block_path, "queue", "logical_block_size")) or 512
464
+ )
465
+ size_bytes = sectors * lb_size
466
+ if not show_zero_size_devices and size_bytes == 0:
467
+ continue
468
+
469
+ parent_block_path = os.path.join(sys_class_block, parent_name) if parent_name else None
470
+ model, serial = read_model_and_serial(block_path)
471
+ parent_model, parent_serial = read_model_and_serial(parent_block_path)
472
+
473
+ # UUID/Label from by-uuid/by-label symlinks
474
+
475
+ props = get_udev_props(major, minor=minor)
476
+ # Fallback to udev props
477
+ rec = {
478
+ "NAME": name,
479
+ "FSTYPE": props.get("ID_FS_TYPE"),
480
+ "LABEL": label_map.get(name, props.get("ID_FS_LABEL")),
481
+ "UUID": uuid_map.get(name, props.get("ID_FS_UUID")),
482
+ "MOUNTPOINT": None,
483
+ "MODEL": model if model else parent_model,
484
+ "SERIAL": serial if serial else parent_serial,
485
+ "DISCARD": bool(read_discard_support(block_path) or read_discard_support(parent_block_path)),
486
+ "SIZE": f"{format_bytes(size_bytes, to_int=print_bytes, use_1024_bytes=use_1024)}B",
487
+ "FSUSE%": None,
488
+ "SMART": 'N/A',
489
+ }
490
+ if SMARTCTL_PATH:
491
+ # if do not have read permission on the denode, set SMART to 'DENIED'
492
+ if not os.access(devnode, os.R_OK):
493
+ rec["SMART"] = 'DENIED'
494
+ try:
495
+ # run smartctl -H <device>
496
+ outputLines = subprocess.check_output([SMARTCTL_PATH, "-H", devnode], stderr=subprocess.STDOUT).decode().splitlines()
497
+ # Parse output for SMART status
498
+ for line in outputLines:
499
+ line = line.lower()
500
+ if "health" in line:
501
+ smartinfo = line.rpartition(':')[2].strip().upper()
502
+ rec["SMART"] = smartinfo.replace('PASSED', 'OK')
503
+ except:
504
+ pass
505
+ # Mount and fstype: try maj:min first, then by resolved devname
506
+ mounts = by_majmin.get(majmin_str, [])
507
+ if not mounts:
508
+ mounts = by_devname.get(name, [])
509
+ if best_only:
510
+ mounts = [choose_mount_for_dev(name, mounts)] if mounts else []
511
+ if not mounted_only and not mounts:
512
+ if formated_only and not rec.get("FSTYPE"):
513
+ continue
514
+ results.append(rec)
515
+ results_by_name[name] = rec
516
+ for mount in mounts:
517
+ rec = rec.copy()
518
+ mountpoint = mount.get("MOUNTPOINT")
519
+ if mount.get("FSTYPE"):
520
+ rec["FSTYPE"] = mount.get("FSTYPE")
521
+ if formated_only and not rec.get("FSTYPE"):
522
+ continue
523
+ # Use% via statvfs and collect raw stats for aggregation
524
+ if mountpoint:
525
+ try:
526
+ st = os.statvfs(mountpoint)
527
+ if st.f_blocks > 0:
528
+ use_pct = 100.0 * (1.0 - (st.f_bavail / float(st.f_blocks)))
529
+ rec["FSUSE%"] = f'{use_pct:.1f}%'
530
+ df_stats_by_name[name] = (st.f_blocks, st.f_bavail)
531
+ except Exception:
532
+ pass
533
+ rec["MOUNTPOINT"] = mountpoint
534
+ results.append(rec)
535
+ results_by_name[name] = rec
536
+
537
+ # Aggregate use% for parent devices with partitions:
538
+ # parent's use% = 1 - sum(bavail)/sum(blocks) over mounted partitions
539
+ for parent, children in parent_to_children.items():
540
+ sum_blocks = 0
541
+ sum_bavail = 0
542
+ for ch in children:
543
+ vals = df_stats_by_name.get(ch)
544
+ if not vals:
545
+ continue
546
+ b, ba = vals
547
+ if b and b > 0:
548
+ sum_blocks += b
549
+ sum_bavail += ba if ba is not None else 0
550
+ if sum_blocks > 0 and parent in results_by_name:
551
+ pct = 100.0 * (1.0 - (sum_bavail / float(sum_blocks)))
552
+ results_by_name[parent]["FSUSE%"] = f'{pct:.1f}%'
553
+
554
+ results.sort(key=lambda x: x["NAME"]) # type: ignore
555
+ return results
556
+
557
+ def main():
558
+ parser = argparse.ArgumentParser(description="Gather disk and partition info for block devices.")
559
+ parser.add_argument('-j','--json', help="Produce JSON output", action="store_true")
560
+ parser.add_argument('-b','--bytes', help="Print the SIZE column in bytes rather than in a human-readable format", action="store_true")
561
+ parser.add_argument('-H','--si', help="Use powers of 1000 not 1024 for SIZE column", action="store_true")
562
+ parser.add_argument('-F','-fo','--formated_only', help="Show only formated filesystems", action="store_true")
563
+ parser.add_argument('-M','-mo','--mounted_only', help="Show only mounted filesystems", action="store_true")
564
+ parser.add_argument('-B','-bo','--best_only', help="Show only best mount for each device", action="store_true")
565
+ parser.add_argument('--show_zero_size_devices', help="Show devices with zero size", action="store_true")
566
+ parser.add_argument('-V', '--version', action='version', version=f"%(prog)s {version} @ {COMMIT_DATE} stat drives by pan@zopyr.us")
567
+
568
+ args = parser.parse_args()
569
+ results = get_drives_info(print_bytes = args.bytes, use_1024 = not args.si,
570
+ mounted_only=args.mounted_only, best_only=args.best_only,
571
+ formated_only=args.formated_only, show_zero_size_devices=args.show_zero_size_devices)
572
+ if args.json:
573
+ import json
574
+ print(json.dumps(results, indent=4))
575
+ else:
576
+ print(pretty_format_table(results))
577
+
578
+
579
+ if __name__ == "__main__":
580
+ main()