statblk 1.25__tar.gz → 1.28__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: statblk
3
- Version: 1.25
3
+ Version: 1.28
4
4
  Summary: Gather essential disk and partition info for block devices and print it in a nice table
5
5
  Home-page: https://github.com/yufei-pan/statblk
6
6
  Author: Yufei Pan
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: statblk
3
- Version: 1.25
3
+ Version: 1.28
4
4
  Summary: Gather essential disk and partition info for block devices and print it in a nice table
5
5
  Home-page: https://github.com/yufei-pan/statblk
6
6
  Author: Yufei Pan
@@ -1,9 +1,6 @@
1
1
  #!/usr/bin/env python3
2
2
  # requires-python = ">=3.6"
3
3
  # -*- coding: utf-8 -*-
4
-
5
-
6
-
7
4
  import os
8
5
  import re
9
6
  import stat
@@ -139,6 +136,10 @@ except:
139
136
  elif return_object:return A
140
137
  elif with_stdErr:return[A.stdout+A.stderr for A in A]
141
138
  else:return[A.stdout for A in A]
139
+ def join_threads(threads=...,timeout=None):
140
+ A=multiCMD.__running_threads if threads is ... else threads
141
+ for B in A:B.join(timeout=timeout)
142
+ if A is multiCMD.__running_threads:multiCMD.__running_threads={A for A in A if A.is_alive()}
142
143
  def pretty_format_table(data,delimiter='\t',header=None,full=False):
143
144
  O=delimiter;B=header;A=data;import re;S=1.12;Z=S
144
145
  def J(s):return len(re.sub('\\x1b\\[[0-?]*[ -/]*[@-~]','',s))
@@ -276,10 +277,10 @@ except :
276
277
  def cache_decorator(func):
277
278
  return func
278
279
 
279
- version = '1.25'
280
+ version = '1.28'
280
281
  VERSION = version
281
282
  __version__ = version
282
- COMMIT_DATE = '2025-09-10'
283
+ COMMIT_DATE = '2025-09-11'
283
284
 
284
285
  SMARTCTL_PATH = shutil.which("smartctl")
285
286
 
@@ -499,20 +500,44 @@ def get_read_write_rate_throughput_iter(sysfs_block_path):
499
500
  except Exception:
500
501
  yield 0, 0
501
502
 
503
+ ALL_OUTPUT_FIELDS = ["NAME", "FSTYPE", "SIZE", "FSUSE%", "MOUNTPOINT", "SMART", "LABEL", "UUID", "MODEL", "SERIAL", "DISCARD", "READ", "WRITE"]
504
+
502
505
  # DRIVE_INFO = namedtuple("DRIVE_INFO",
503
506
  # ["NAME", "FSTYPE", "SIZE", "FSUSEPCT", "MOUNTPOINT", "SMART","RTPT",'WTPT', "LABEL", "UUID", "MODEL", "SERIAL", "DISCARD"])
504
507
  def get_drives_info(print_bytes = False, use_1024 = False, mounted_only=False, best_only=False,
505
508
  formated_only=False, show_zero_size_devices=False,pseudo=False,tptDict = {},
506
- full=False,active_only=False):
507
- lsblk_result = multiCMD.run_command(f'lsblk -brnp -o NAME,SIZE,FSTYPE,UUID,LABEL',timeout=2,quiet=True,wait_for_return=False,return_object=True)
509
+ full=False,active_only=False,output="all",exclude=""):
510
+ global SMARTCTL_PATH
511
+ global ALL_OUTPUT_FIELDS
512
+ if output == "all":
513
+ output_fields = ALL_OUTPUT_FIELDS
514
+ else:
515
+ output_fields = [x.strip().upper() for x in output.split(',')]
516
+ for field in output_fields:
517
+ if field not in ALL_OUTPUT_FIELDS:
518
+ print(f"Ignoring invalid output field: {field}.", file=sys.stderr)
519
+ output_fields.remove(field)
520
+ if exclude:
521
+ exclude_fields = [x.strip().upper() for x in exclude.split(',')]
522
+ for field in exclude_fields:
523
+ if field in output_fields:
524
+ output_fields.remove(field)
525
+ if not output_fields:
526
+ print("No valid output fields specified.", file=sys.stderr)
527
+ return []
528
+ output_list = [output_fields]
529
+ output_fields_set = set(output_fields)
530
+ if {'SIZE','FSTYPE','UUID','LABEL'}.intersection(output_fields_set):
531
+ lsblk_result = multiCMD.run_command(f'lsblk -brnp -o NAME,SIZE,FSTYPE,UUID,LABEL',timeout=2,quiet=True,wait_for_return=False,return_object=True)
508
532
  block_devices = get_blocks()
509
533
  smart_infos = {}
510
534
  for block_device in block_devices:
511
- parent_name = get_partition_parent_name(block_device)
512
- if parent_name:
513
- if parent_name not in smart_infos:
514
- smart_infos[parent_name] = multiCMD.run_command(f'{SMARTCTL_PATH} -H {parent_name}',timeout=2,quiet=True,wait_for_return=False,return_object=True)
515
- if block_device not in tptDict:
535
+ if 'SMART' in output_fields_set:
536
+ parent_name = get_partition_parent_name(block_device)
537
+ if parent_name:
538
+ if parent_name not in smart_infos:
539
+ smart_infos[parent_name] = multiCMD.run_command(f'{SMARTCTL_PATH} -H {parent_name}',timeout=2,quiet=True,wait_for_return=False,return_object=True)
540
+ if ('READ' in output_fields_set or 'WRITE' in output_fields_set) and block_device not in tptDict:
516
541
  sysfs_block_path = os.path.join('/sys/class/block', os.path.basename(block_device))
517
542
  tptDict[block_device] = get_read_write_rate_throughput_iter(sysfs_block_path)
518
543
  mount_table = parseMount()
@@ -520,56 +545,56 @@ def get_drives_info(print_bytes = False, use_1024 = False, mounted_only=False, b
520
545
  if pseudo:
521
546
  target_devices.update(mount_table.keys())
522
547
  target_devices = sorted(target_devices)
523
- uuid_dict = build_symlink_dict("/dev/disk/by-uuid")
524
- label_dict = build_symlink_dict("/dev/disk/by-label")
548
+ uuid_dict = {}
549
+ if 'UUID' in output_fields_set:
550
+ uuid_dict = build_symlink_dict("/dev/disk/by-uuid")
551
+ label_dict = {}
552
+ if 'LABEL' in output_fields_set:
553
+ label_dict = build_symlink_dict("/dev/disk/by-label")
525
554
  fstype_dict = {}
526
555
  size_dict = {}
527
- lsblk_result.thread.join()
528
- if lsblk_result.returncode == 0:
529
- for line in lsblk_result.stdout:
530
- lsblk_name, lsblk_size, lsblk_fstype, lsblk_uuid, lsblk_label = line.split(' ', 4)
531
- if lsblk_name.startswith(os.path.sep):
532
- lsblk_name = os.path.realpath(lsblk_name)
533
- # the label can be \x escaped, we need to decode it
534
- lsblk_uuid = bytes(lsblk_uuid, "utf-8").decode("unicode_escape")
535
- lsblk_fstype = bytes(lsblk_fstype, "utf-8").decode("unicode_escape")
536
- lsblk_label = bytes(lsblk_label, "utf-8").decode("unicode_escape")
537
- if lsblk_uuid:
538
- uuid_dict[lsblk_name] = lsblk_uuid
539
- if lsblk_fstype:
540
- fstype_dict[lsblk_name] = lsblk_fstype
541
- if lsblk_label:
542
- label_dict[lsblk_name] = lsblk_label
543
- try:
544
- size_dict[lsblk_name] = int(lsblk_size)
545
- except Exception:
546
- pass
547
- output = [["NAME", "FSTYPE", "SIZE", "FSUSE%", "MOUNTPOINT", "SMART", "LABEL", "UUID", "MODEL", "SERIAL", "DISCARD","READ",'WRITE']]
556
+ if {'SIZE','FSTYPE','UUID','LABEL'}.intersection(output_fields_set):
557
+ lsblk_result.thread.join()
558
+ if lsblk_result.returncode == 0:
559
+ for line in lsblk_result.stdout:
560
+ lsblk_name, lsblk_size, lsblk_fstype, lsblk_uuid, lsblk_label = line.split(' ', 4)
561
+ if lsblk_name.startswith(os.path.sep):
562
+ lsblk_name = os.path.realpath(lsblk_name)
563
+ # the label can be \x escaped, we need to decode it
564
+ if 'UUID' in output_fields_set:
565
+ lsblk_uuid = bytes(lsblk_uuid, "utf-8").decode("unicode_escape")
566
+ if lsblk_uuid:
567
+ uuid_dict[lsblk_name] = lsblk_uuid
568
+ if 'FSTYPE' in output_fields_set:
569
+ lsblk_fstype = bytes(lsblk_fstype, "utf-8").decode("unicode_escape")
570
+ if lsblk_fstype:
571
+ fstype_dict[lsblk_name] = lsblk_fstype
572
+ if 'LABEL' in output_fields_set:
573
+ lsblk_label = bytes(lsblk_label, "utf-8").decode("unicode_escape")
574
+ if lsblk_label:
575
+ label_dict[lsblk_name] = lsblk_label
576
+ if 'SIZE' in output_fields_set:
577
+ try:
578
+ size_dict[lsblk_name] = int(lsblk_size)
579
+ except Exception:
580
+ pass
548
581
  for device_name in target_devices:
549
582
  if mounted_only and device_name not in mount_table:
550
583
  continue
551
584
  if active_only and device_name not in tptDict:
552
585
  continue
553
- fstype = ''
554
- size = ''
555
- fsusepct = ''
556
- mountpoint = ''
557
- smart = ''
558
- label = ''
559
- uuid = ''
560
- model = ''
561
- serial = ''
562
- discard = ''
563
- rtpt = ''
564
- wtpt = ''
586
+ device_properties = defaultdict(str)
587
+ device_properties['NAME'] = device_name if full else device_name.replace('/dev/', '')
565
588
  # fstype, size, fsuse%, mountpoint, rtpt, wtpt, lable, uuid are partition specific
566
589
  # smart, model, serial, discard are device specific, and only for block devices
567
590
  # fstype, size, fsuse%, mountpoint does not require block device and can have multiple values per device
568
591
  if is_block_device(device_name):
569
592
  parent_name = get_partition_parent_name(device_name)
570
593
  parent_sysfs_path = os.path.realpath(os.path.join('/sys/class/block', os.path.basename(parent_name))) if parent_name else None
571
- model, serial = read_model_and_serial(parent_sysfs_path)
572
- discard = read_discard_support(parent_sysfs_path)
594
+ if 'MODEL' in output_fields_set or 'SERIAL' in output_fields_set:
595
+ device_properties['MODEL'], device_properties['SERIAL'] = read_model_and_serial(parent_sysfs_path)
596
+ if 'DISCARD' in output_fields_set:
597
+ device_properties['DISCARD'] = read_discard_support(parent_sysfs_path)
573
598
  if parent_name in smart_infos and SMARTCTL_PATH:
574
599
  smart_info_obj = smart_infos[parent_name]
575
600
  smart_info_obj.thread.join()
@@ -577,67 +602,64 @@ def get_drives_info(print_bytes = False, use_1024 = False, mounted_only=False, b
577
602
  line = line.lower()
578
603
  if "health" in line:
579
604
  smartinfo = line.rpartition(':')[2].strip().upper()
580
- smart = smartinfo.replace('PASSED', 'OK')
605
+ device_properties['SMART'] = smartinfo.replace('PASSED', 'OK')
581
606
  break
582
607
  elif "denied" in line:
583
- smart = 'DENIED'
608
+ device_properties['SMART'] = 'DENIED'
584
609
  break
585
610
  size_bytes = read_size(os.path.join('/sys/class/block', os.path.basename(device_name)))
586
611
  if device_name in tptDict:
587
612
  try:
588
- rtpt, wtpt = next(tptDict[device_name])
589
- if active_only and rtpt == 0 and wtpt == 0:
613
+ device_properties['READ'], device_properties['WRITE'] = next(tptDict[device_name])
614
+ if active_only and device_properties['READ'] == 0 and device_properties['WRITE'] == 0:
590
615
  continue
591
616
  if print_bytes:
592
- rtpt = str(rtpt)
593
- wtpt = str(wtpt)
617
+ device_properties['READ'] = str(device_properties['READ'])
618
+ device_properties['WRITE'] = str(device_properties['WRITE'])
594
619
  else:
595
- rtpt = multiCMD.format_bytes(rtpt, use_1024_bytes=use_1024, to_str=True,str_format='.0f') + 'B/s'
596
- wtpt = multiCMD.format_bytes(wtpt, use_1024_bytes=use_1024, to_str=True,str_format='.0f') + 'B/s'
620
+ device_properties['READ'] = multiCMD.format_bytes(device_properties['READ'], use_1024_bytes=use_1024, to_str=True,str_format='.0f') + 'B/s'
621
+ device_properties['WRITE'] = multiCMD.format_bytes(device_properties['WRITE'], use_1024_bytes=use_1024, to_str=True,str_format='.0f') + 'B/s'
597
622
  except Exception:
598
- rtpt = ''
599
- wtpt = ''
623
+ device_properties['READ'] = ''
624
+ device_properties['WRITE'] = ''
600
625
  if device_name in label_dict:
601
- label = label_dict[device_name]
626
+ device_properties['LABEL'] = label_dict[device_name]
602
627
  if device_name in uuid_dict:
603
- uuid = uuid_dict[device_name]
628
+ device_properties['UUID'] = uuid_dict[device_name]
604
629
  mount_points = mount_table.get(device_name, [])
605
630
  if best_only:
606
631
  if mount_points:
607
632
  mount_points = [sorted(mount_points, key=lambda x: len(x.MOUNTPOINT))[0]]
608
633
  if mount_points:
609
634
  for mount_entry in mount_points:
610
- fstype = mount_entry.FSTYPE
611
- if formated_only and not fstype:
635
+ device_properties['FSTYPE'] = mount_entry.FSTYPE
636
+ if formated_only and not device_properties['FSTYPE']:
612
637
  continue
613
- mountpoint = mount_entry.MOUNTPOINT
614
- size_bytes, used_bytes = get_statvfs_use_size(mountpoint)
638
+ device_properties['MOUNTPOINT'] = mount_entry.MOUNTPOINT
639
+ size_bytes, used_bytes = get_statvfs_use_size(device_properties['MOUNTPOINT'])
615
640
  if size_bytes == 0 and not show_zero_size_devices:
616
641
  continue
617
- fsusepct = f"{int(round(100.0 * used_bytes / size_bytes))}%" if size_bytes > 0 else "N/A"
642
+ device_properties['FSUSE%'] = f"{int(round(100.0 * used_bytes / size_bytes))}%" if size_bytes > 0 else "N/A"
618
643
  if print_bytes:
619
- size = str(size_bytes)
644
+ device_properties['SIZE'] = str(size_bytes)
620
645
  else:
621
- size = multiCMD.format_bytes(size_bytes, use_1024_bytes=use_1024, to_str=True) + 'B'
622
- if not full:
623
- device_name = device_name.replace('/dev/', '')
624
- output.append([device_name, fstype, size, fsusepct, mountpoint, smart, label, uuid, model, serial, discard, rtpt, wtpt])
646
+ device_properties['SIZE'] = multiCMD.format_bytes(size_bytes, use_1024_bytes=use_1024, to_str=True) + 'B'
647
+ output_list.append([device_properties[output_field] for output_field in output_fields])
625
648
  else:
626
649
  if formated_only and device_name not in fstype_dict:
627
650
  continue
628
- fstype = fstype_dict.get(device_name, '')
651
+ device_properties['FSTYPE'] = fstype_dict.get(device_name, '')
629
652
  if not size_bytes:
630
653
  size_bytes = size_dict.get(device_name, 0)
631
654
  if size_bytes == 0 and not show_zero_size_devices:
632
655
  continue
633
656
  if print_bytes:
634
- size = str(size_bytes)
657
+ device_properties['SIZE'] = str(size_bytes)
635
658
  else:
636
- size = multiCMD.format_bytes(size_bytes, use_1024_bytes=use_1024, to_str=True) + 'B'
637
- if not full:
638
- device_name = device_name.replace('/dev/', '')
639
- output.append([device_name, fstype, size, fsusepct, mountpoint, smart, label, uuid, model, serial, discard, rtpt, wtpt])
640
- return output
659
+ device_properties['SIZE'] = multiCMD.format_bytes(size_bytes, use_1024_bytes=use_1024, to_str=True) + 'B'
660
+ output_list.append([device_properties[output_field] for output_field in output_fields])
661
+ multiCMD.join_threads()
662
+ return output_list
641
663
 
642
664
 
643
665
  def main():
@@ -651,6 +673,8 @@ def main():
651
673
  parser.add_argument('-A','-ao','--active_only', help="Show only active devices (positive read/write activity)", action="store_true")
652
674
  parser.add_argument('-R','--full', help="Show full device information, do not collapse drive info when length > console length", action="store_true")
653
675
  parser.add_argument('-P','--pseudo', help="Include pseudo file systems as well (tmpfs / nfs / cifs etc.)", action="store_true")
676
+ parser.add_argument('-o','--output', help="Specify which output columns to print.Use comma to separate columns. default: all available", default="all", type=str)
677
+ parser.add_argument('-x','--exclude', help="Specify which output columns to exclude.Use comma to separate columns. default: none", default="", type=str)
654
678
  parser.add_argument('--show_zero_size_devices', help="Show devices with zero size", action="store_true")
655
679
  parser.add_argument('print_period', nargs='?', default=0, type=int, help="If specified as a number, repeat the output every N seconds")
656
680
  parser.add_argument('-V', '--version', action='version', version=f"%(prog)s {version} @ {COMMIT_DATE} stat drives by pan@zopyr.us")
@@ -661,7 +685,7 @@ def main():
661
685
  results = get_drives_info(print_bytes = args.bytes, use_1024 = not args.si,
662
686
  mounted_only=args.mounted_only, best_only=args.best_only,
663
687
  formated_only=args.formated_only, show_zero_size_devices=args.show_zero_size_devices,
664
- pseudo=args.pseudo,tptDict=tptDict,full=args.full,active_only=args.active_only)
688
+ pseudo=args.pseudo,tptDict=tptDict,full=args.full,active_only=args.active_only,output=args.output,exclude=args.exclude)
665
689
  if args.json:
666
690
  import json
667
691
  print(json.dumps(results, indent=1))
File without changes
File without changes
File without changes