multiCMD 1.33__tar.gz → 1.35__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: multiCMD
3
- Version: 1.33
3
+ Version: 1.35
4
4
  Summary: Run commands simultaneously
5
5
  Home-page: https://github.com/yufei-pan/multiCMD
6
6
  Author: Yufei Pan
File without changes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: multiCMD
3
- Version: 1.33
3
+ Version: 1.35
4
4
  Summary: Run commands simultaneously
5
5
  Home-page: https://github.com/yufei-pan/multiCMD
6
6
  Author: Yufei Pan
@@ -5,6 +5,7 @@
5
5
  # "argparse",
6
6
  # ]
7
7
  # ///
8
+ #%% imports
8
9
  import time
9
10
  import threading
10
11
  import io
@@ -18,11 +19,20 @@ import re
18
19
  import itertools
19
20
  import signal
20
21
 
21
- version = '1.33'
22
+ #%% global vars
23
+ version = '1.35'
22
24
  __version__ = version
23
-
25
+ COMMIT_DATE = '2025-09-10'
24
26
  __running_threads = set()
25
27
  __variables = {}
28
+
29
+ # immutable helpers compiled once at import time
30
+ _BRACKET_RX = re.compile(r'\[([^\]]+)\]')
31
+ _ALPHANUM = string.digits + string.ascii_letters
32
+ _ALPHA_IDX = {c: i for i, c in enumerate(_ALPHANUM)}
33
+
34
+
35
+ #%% objects
26
36
  class Task:
27
37
  def __init__(self, command):
28
38
  self.command = command
@@ -41,7 +51,7 @@ class Task:
41
51
  if self.thread is not None:
42
52
  return self.thread.is_alive()
43
53
  return False
44
-
54
+
45
55
  class AsyncExecutor:
46
56
  def __init__(self, max_threads=1,semaphore=...,timeout=0,quiet=True,dry_run=False,parse=False):
47
57
  '''
@@ -227,11 +237,7 @@ class AsyncExecutor:
227
237
  '''
228
238
  return [task.returncode for task in self.tasks]
229
239
 
230
- # immutable helpers compiled once at import time
231
- _BRACKET_RX = re.compile(r'\[([^\]]+)\]')
232
- _ALPHANUM = string.digits + string.ascii_letters
233
- _ALPHA_IDX = {c: i for i, c in enumerate(_ALPHANUM)}
234
-
240
+ #%% helper functions
235
241
  def _expand_piece(piece, vars_):
236
242
  """Turn one comma-separated element from inside [...] into a list of strings."""
237
243
  piece = piece.strip()
@@ -287,69 +293,6 @@ def _expand_ranges_fast(inStr):
287
293
  # cartesian product of all segments
288
294
  return [''.join(parts) for parts in itertools.product(*segments)]
289
295
 
290
- def _expand_ranges(inStr):
291
- '''
292
- Expand ranges in a string
293
-
294
- @params:
295
- inStr: The string to expand
296
-
297
- @returns:
298
- list[str]: The expanded string
299
- '''
300
- global __variables
301
- expandingStr = [inStr]
302
- expandedList = []
303
- # all valid alphanumeric characters
304
- alphanumeric = string.digits + string.ascii_letters
305
- while len(expandingStr) > 0:
306
- currentStr = expandingStr.pop()
307
- match = re.search(r'\[(.*?)]', currentStr)
308
- if not match:
309
- expandedList.append(currentStr)
310
- continue
311
- group = match.group(1)
312
- parts = group.split(',')
313
- for part in parts:
314
- part = part.strip()
315
- if ':' in part:
316
- variableName, _, part = part.partition(':')
317
- __variables[variableName] = part
318
- expandingStr.append(currentStr.replace(match.group(0), '', 1))
319
- elif '-' in part:
320
- try:
321
- range_start,_, range_end = part.partition('-')
322
- except ValueError:
323
- expandedList.append(currentStr)
324
- continue
325
- range_start = range_start.strip()
326
- if range_start in __variables:
327
- range_start = __variables[range_start]
328
- range_end = range_end.strip()
329
- if range_end in __variables:
330
- range_end = __variables[range_end]
331
- if range_start.isdigit() and range_end.isdigit():
332
- padding_length = min(len(range_start), len(range_end))
333
- format_str = "{:0" + str(padding_length) + "d}"
334
- for i in range(int(range_start), int(range_end) + 1):
335
- formatted_i = format_str.format(i)
336
- expandingStr.append(currentStr.replace(match.group(0), formatted_i, 1))
337
- elif all(c in string.hexdigits for c in range_start + range_end):
338
- for i in range(int(range_start, 16), int(range_end, 16) + 1):
339
- expandingStr.append(currentStr.replace(match.group(0), format(i, 'x'), 1))
340
- else:
341
- try:
342
- start_index = alphanumeric.index(range_start)
343
- end_index = alphanumeric.index(range_end)
344
- for i in range(start_index, end_index + 1):
345
- expandingStr.append(currentStr.replace(match.group(0), alphanumeric[i], 1))
346
- except ValueError:
347
- expandedList.append(currentStr)
348
- else:
349
- expandingStr.append(currentStr.replace(match.group(0), part, 1))
350
- expandedList.reverse()
351
- return expandedList
352
-
353
296
  def __handle_stream(stream,target,pre='',post='',quiet=False):
354
297
  '''
355
298
  Handle a stream
@@ -519,6 +462,43 @@ def __run_command(task,sem, timeout=60, quiet=False,dry_run=False,with_stdErr=Fa
519
462
  else:
520
463
  return task.stdout
521
464
 
465
+ def __format_command(command,expand = False):
466
+ '''
467
+ Format a command
468
+
469
+ @params:
470
+ command: The command to format
471
+ expand: Whether to expand ranges
472
+
473
+ @returns:
474
+ list[list[str]]: The formatted commands sequence
475
+ '''
476
+ if isinstance(command,str):
477
+ if expand:
478
+ commands = _expand_ranges_fast(command)
479
+ else:
480
+ commands = [command]
481
+ return [command.split() for command in commands]
482
+ # elif it is a iterable
483
+ elif hasattr(command,'__iter__'):
484
+ sanitized_command = []
485
+ for field in command:
486
+ if isinstance(field,str):
487
+ sanitized_command.append(field)
488
+ else:
489
+ sanitized_command.append(repr(field))
490
+ if not expand:
491
+ return [sanitized_command]
492
+ sanitized_expanded_command = [_expand_ranges_fast(field) for field in sanitized_command]
493
+ # now the command had been expanded to list of list of fields with each field expanded to all possible options
494
+ # we need to generate all possible combinations of the fields
495
+ # we will use the cartesian product to do this
496
+ commands = list(itertools.product(*sanitized_expanded_command))
497
+ return [list(command) for command in commands]
498
+ else:
499
+ return __format_command(str(command),expand=expand)
500
+
501
+ #%% core funcitons
522
502
  def ping(hosts,timeout=1,max_threads=0,quiet=True,dry_run=False,with_stdErr=False,
523
503
  return_code_only=False,return_object=False,wait_for_return=True,return_true_false=True):
524
504
  '''
@@ -560,7 +540,6 @@ def ping(hosts,timeout=1,max_threads=0,quiet=True,dry_run=False,with_stdErr=Fals
560
540
  else:
561
541
  return results
562
542
 
563
-
564
543
  def run_command(command, timeout=0,max_threads=1,quiet=False,dry_run=False,with_stdErr=False,
565
544
  return_code_only=False,return_object=False,wait_for_return=True, sem = None):
566
545
  '''
@@ -585,42 +564,6 @@ def run_command(command, timeout=0,max_threads=1,quiet=False,dry_run=False,with_
585
564
  dry_run=dry_run, with_stdErr=with_stdErr, return_code_only=return_code_only,
586
565
  return_object=return_object,parse=False,wait_for_return=wait_for_return,sem=sem)[0]
587
566
 
588
- def __format_command(command,expand = False):
589
- '''
590
- Format a command
591
-
592
- @params:
593
- command: The command to format
594
- expand: Whether to expand ranges
595
-
596
- @returns:
597
- list[list[str]]: The formatted commands sequence
598
- '''
599
- if isinstance(command,str):
600
- if expand:
601
- commands = _expand_ranges_fast(command)
602
- else:
603
- commands = [command]
604
- return [command.split() for command in commands]
605
- # elif it is a iterable
606
- elif hasattr(command,'__iter__'):
607
- sanitized_command = []
608
- for field in command:
609
- if isinstance(field,str):
610
- sanitized_command.append(field)
611
- else:
612
- sanitized_command.append(repr(field))
613
- if not expand:
614
- return [sanitized_command]
615
- sanitized_expanded_command = [_expand_ranges_fast(field) for field in sanitized_command]
616
- # now the command had been expanded to list of list of fields with each field expanded to all possible options
617
- # we need to generate all possible combinations of the fields
618
- # we will use the cartesian product to do this
619
- commands = list(itertools.product(*sanitized_expanded_command))
620
- return [list(command) for command in commands]
621
- else:
622
- return __format_command(str(command),expand=expand)
623
-
624
567
  def run_commands(commands, timeout=0,max_threads=1,quiet=False,dry_run=False,with_stdErr=False,
625
568
  return_code_only=False,return_object=False, parse = False, wait_for_return = True, sem : threading.Semaphore = None):
626
569
  '''
@@ -695,6 +638,21 @@ def join_threads(threads=__running_threads,timeout=None):
695
638
  if threads is __running_threads:
696
639
  __running_threads = {t for t in threads if t.is_alive()}
697
640
 
641
+ def main():
642
+ parser = argparse.ArgumentParser(description='Run multiple commands in parallel')
643
+ parser.add_argument('commands', metavar='command', type=str, nargs='+',help='commands to run')
644
+ parser.add_argument('-p','--parse', action='store_true',help='Parse ranged input and expand them into multiple commands')
645
+ parser.add_argument('-t','--timeout', metavar='timeout', type=int, default=60,help='timeout for each command')
646
+ parser.add_argument('-m','--max_threads', metavar='max_threads', type=int, default=1,help='maximum number of threads to use')
647
+ parser.add_argument('-q','--quiet', action='store_true',help='quiet mode')
648
+ parser.add_argument('-V','--version', action='version', version=f'%(prog)s {version} @ {COMMIT_DATE} by pan@zopyr.us')
649
+ args = parser.parse_args()
650
+ run_commands(args.commands, args.timeout, args.max_threads, args.quiet,parse = args.parse, with_stdErr=True)
651
+
652
+ if __name__ == '__main__':
653
+ main()
654
+
655
+ #%% misc functions
698
656
  def input_with_timeout_and_countdown(timeout, prompt='Please enter your selection'):
699
657
  """
700
658
  Read an input from the user with a timeout and a countdown.
@@ -721,6 +679,194 @@ def input_with_timeout_and_countdown(timeout, prompt='Please enter your selectio
721
679
  # If there is no input, return None
722
680
  return None
723
681
 
682
+ def pretty_format_table(data, delimiter="\t", header=None, full=False):
683
+ import re
684
+ version = 1.12
685
+ _ = version
686
+ def visible_len(s):
687
+ return len(re.sub(r"\x1b\[[0-?]*[ -/]*[@-~]", "", s))
688
+ def table_width(col_widths, sep_len):
689
+ # total width = sum of column widths + separators between columns
690
+ return sum(col_widths) + sep_len * (len(col_widths) - 1)
691
+ def truncate_to_width(s, width):
692
+ # If fits, leave as is. If too long and width >= 1, keep width-1 chars + "."
693
+ # If width == 0, nothing fits; return empty string.
694
+ if visible_len(s) <= width:
695
+ return s
696
+ if width <= 0:
697
+ return ""
698
+ # Build a truncated plain string based on visible chars (no ANSI awareness for slicing)
699
+ # For simplicity, slice the raw string. This may cut ANSI; best to avoid ANSI in data if truncation occurs.
700
+ return s[: max(width - 2, 0)] + ".."
701
+ if not data:
702
+ return ""
703
+ # Normalize input data structure
704
+ if isinstance(data, str):
705
+ data = data.strip("\n").split("\n")
706
+ data = [line.split(delimiter) for line in data]
707
+ elif isinstance(data, dict):
708
+ if isinstance(next(iter(data.values())), dict):
709
+ tempData = [["key"] + list(next(iter(data.values())).keys())]
710
+ tempData.extend([[key] + list(value.values()) for key, value in data.items()])
711
+ data = tempData
712
+ else:
713
+ data = [[key] + list(value) for key, value in data.items()]
714
+ elif not isinstance(data, list):
715
+ data = list(data)
716
+ if isinstance(data[0], dict):
717
+ tempData = [list(data[0].keys())]
718
+ tempData.extend([list(item.values()) for item in data])
719
+ data = tempData
720
+ data = [[str(item) for item in row] for row in data]
721
+ num_cols = len(data[0])
722
+ # Resolve header and rows
723
+ using_provided_header = header is not None
724
+ if not using_provided_header:
725
+ header = data[0]
726
+ rows = data[1:]
727
+ else:
728
+ if isinstance(header, str):
729
+ header = header.split(delimiter)
730
+ # Pad/trim header to match num_cols
731
+ if len(header) < num_cols:
732
+ header = header + [""] * (num_cols - len(header))
733
+ elif len(header) > num_cols:
734
+ header = header[:num_cols]
735
+ rows = data
736
+ # Compute initial column widths based on data and header
737
+ def compute_col_widths(hdr, rows_):
738
+ col_w = [0] * len(hdr)
739
+ for i in range(len(hdr)):
740
+ col_w[i] = max(visible_len(hdr[i]), *(visible_len(r[i]) for r in rows_ if i < len(r)))
741
+ return col_w
742
+ # Ensure all rows have the same number of columns
743
+ normalized_rows = []
744
+ for r in rows:
745
+ if len(r) < num_cols:
746
+ r = r + [""] * (num_cols - len(r))
747
+ elif len(r) > num_cols:
748
+ r = r[:num_cols]
749
+ normalized_rows.append(r)
750
+ rows = normalized_rows
751
+ col_widths = compute_col_widths(header, rows)
752
+ # If full=True, keep existing formatting
753
+ # Else try to fit within the terminal width by:
754
+ # 1) Switching to compressed separators if needed
755
+ # 2) Recursively compressing columns (truncating with ".")
756
+ sep = " | "
757
+ hsep = "-+-"
758
+ cols = get_terminal_size()[0]
759
+ def render(hdr, rows, col_w, sep_str, hsep_str):
760
+ row_fmt = sep_str.join("{{:<{}}}".format(w) for w in col_w)
761
+ out = []
762
+ out.append(row_fmt.format(*hdr))
763
+ out.append(hsep_str.join("-" * w for w in col_w))
764
+ for row in rows:
765
+ if not any(row):
766
+ out.append(hsep_str.join("-" * w for w in col_w))
767
+ else:
768
+ row = [truncate_to_width(row[i], col_w[i]) for i in range(len(row))]
769
+ out.append(row_fmt.format(*row))
770
+ return "\n".join(out) + "\n"
771
+ if full:
772
+ return render(header, rows, col_widths, sep, hsep)
773
+ # Try default separators first
774
+ if table_width(col_widths, len(sep)) <= cols:
775
+ return render(header, rows, col_widths, sep, hsep)
776
+ # Use compressed separators (no spaces)
777
+ sep = "|"
778
+ hsep = "+"
779
+ if table_width(col_widths, len(sep)) <= cols:
780
+ return render(header, rows, col_widths, sep, hsep)
781
+ # Begin column compression
782
+ # Track which columns have been compressed already to header width
783
+ header_widths = [visible_len(h) for h in header]
784
+ width_diff = [max(col_widths[i] - header_widths[i],0) for i in range(num_cols)]
785
+ total_overflow_width = table_width(col_widths, len(sep)) - cols
786
+ for i, diff in sorted(enumerate(width_diff), key=lambda x: -x[1]):
787
+ if total_overflow_width <= 0:
788
+ break
789
+ if diff <= 0:
790
+ continue
791
+ reduce_by = min(diff, total_overflow_width)
792
+ col_widths[i] -= reduce_by
793
+ total_overflow_width -= reduce_by
794
+ return render(header, rows, col_widths, sep, hsep)
795
+
796
+ def parseTable(data,sort=False):
797
+ if isinstance(data, str):
798
+ data = data.strip('\n').split('\n')
799
+ header_line = data[0]
800
+ # Use regex to find column names and their positions
801
+ pattern = r'(\S(?:.*?\S)?)(?=\s{2,}|\s*$)'
802
+ matches = list(re.finditer(pattern, header_line))
803
+ data_list = [[]]
804
+ columns = []
805
+ for i, match in enumerate(matches):
806
+ col_name = match.group(1)
807
+ data_list[0].append(col_name)
808
+ start = match.start()
809
+ if i + 1 < len(matches):
810
+ end = matches[i+1].start()
811
+ else:
812
+ end = None # Last column goes till the end
813
+ columns.append((col_name, start, end))
814
+ for line in data[1:]:
815
+ if not line.strip():
816
+ continue # Skip empty lines
817
+ row = []
818
+ for col_name, start, end in columns:
819
+ if end is not None:
820
+ value = line[start:end].strip()
821
+ else:
822
+ value = line[start:].strip()
823
+ row.append(value)
824
+ data_list.append(row)
825
+ # sort data_list[1:] by the first column
826
+ if sort:
827
+ data_list[1:] = sorted(data_list[1:], key=lambda x: x[0])
828
+ return data_list
829
+
830
+ def slugify(value, allow_unicode=False):
831
+ import unicodedata
832
+ """
833
+ Taken from https://github.com/django/django/blob/master/django/utils/text.py
834
+ Convert to ASCII if 'allow_unicode' is False. Convert spaces or repeated
835
+ dashes to single dashes. Remove characters that aren't alphanumerics,
836
+ underscores, or hyphens. Convert to lowercase. Also strip leading and
837
+ trailing whitespace, dashes, and underscores.
838
+ """
839
+ value = str(value)
840
+ if allow_unicode:
841
+ value = unicodedata.normalize('NFKC', value)
842
+ else:
843
+ value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore').decode('ascii')
844
+ value = re.sub(r'[^\w\s-]', '', value.lower())
845
+ return re.sub(r'[-\s]+', '-', value).strip('-_')
846
+
847
+ def get_terminal_size():
848
+ '''
849
+ Get the terminal size
850
+
851
+ @params:
852
+ None
853
+
854
+ @returns:
855
+ (int,int): the number of columns and rows of the terminal
856
+ '''
857
+ try:
858
+ import os
859
+ _tsize = os.get_terminal_size()
860
+ except:
861
+ try:
862
+ import fcntl, termios, struct
863
+ packed = fcntl.ioctl(0, termios.TIOCGWINSZ, struct.pack('HHHH', 0, 0, 0, 0))
864
+ _tsize = struct.unpack('HHHH', packed)[:2]
865
+ except:
866
+ import shutil
867
+ _tsize = shutil.get_terminal_size(fallback=(120, 30))
868
+ return _tsize
869
+
724
870
  def _genrate_progress_bar(iteration, total, prefix='', suffix='',columns=120):
725
871
  '''
726
872
  Generate a progress bar string
@@ -780,29 +926,6 @@ def _genrate_progress_bar(iteration, total, prefix='', suffix='',columns=120):
780
926
  lineOut += suffix
781
927
  return lineOut
782
928
 
783
- def get_terminal_size():
784
- '''
785
- Get the terminal size
786
-
787
- @params:
788
- None
789
-
790
- @returns:
791
- (int,int): the number of columns and rows of the terminal
792
- '''
793
- try:
794
- import os
795
- _tsize = os.get_terminal_size()
796
- except:
797
- try:
798
- import fcntl, termios, struct
799
- packed = fcntl.ioctl(0, termios.TIOCGWINSZ, struct.pack('HHHH', 0, 0, 0, 0))
800
- _tsize = struct.unpack('HHHH', packed)[:2]
801
- except:
802
- import shutil
803
- _tsize = shutil.get_terminal_size(fallback=(120, 30))
804
- return _tsize
805
-
806
929
  def print_progress_bar(iteration, total, prefix='', suffix=''):
807
930
  '''
808
931
  Call in a loop to create terminal progress bar
@@ -827,16 +950,108 @@ def print_progress_bar(iteration, total, prefix='', suffix=''):
827
950
  if iteration % 5 == 0:
828
951
  print(_genrate_progress_bar(iteration, total, prefix, suffix))
829
952
 
830
- def main():
831
- parser = argparse.ArgumentParser(description='Run multiple commands in parallel')
832
- parser.add_argument('commands', metavar='command', type=str, nargs='+',help='commands to run')
833
- parser.add_argument('-p','--parse', action='store_true',help='Parse ranged input and expand them into multiple commands')
834
- parser.add_argument('-t','--timeout', metavar='timeout', type=int, default=60,help='timeout for each command')
835
- parser.add_argument('-m','--max_threads', metavar='max_threads', type=int, default=1,help='maximum number of threads to use')
836
- parser.add_argument('-q','--quiet', action='store_true',help='quiet mode')
837
- parser.add_argument('-V','--version', action='version', version=f'%(prog)s {version} by pan@zopyr.us')
838
- args = parser.parse_args()
839
- run_commands(args.commands, args.timeout, args.max_threads, args.quiet,parse = args.parse, with_stdErr=True)
953
+ def format_bytes(size, use_1024_bytes=None, to_int=False, to_str=False, str_format=".2f"):
954
+ if to_int or isinstance(size, str):
955
+ if isinstance(size, int):
956
+ return size
957
+ elif isinstance(size, str):
958
+ match = re.match(r"(\d+(\.\d+)?)\s*([a-zA-Z]*)", size)
959
+ if not match:
960
+ if to_str:
961
+ return size
962
+ print(
963
+ "Invalid size format. Expected format: 'number [unit]', "
964
+ "e.g., '1.5 GiB' or '1.5GiB'"
965
+ )
966
+ print(f"Got: {size}")
967
+ return 0
968
+ number, _, unit = match.groups()
969
+ number = float(number)
970
+ unit = unit.strip().lower().rstrip("b")
971
+ if unit.endswith("i"):
972
+ use_1024_bytes = True
973
+ elif use_1024_bytes is None:
974
+ use_1024_bytes = False
975
+ unit = unit.rstrip("i")
976
+ if use_1024_bytes:
977
+ power = 2**10
978
+ else:
979
+ power = 10**3
980
+ unit_labels = {
981
+ "": 0,
982
+ "k": 1,
983
+ "m": 2,
984
+ "g": 3,
985
+ "t": 4,
986
+ "p": 5,
987
+ "e": 6,
988
+ "z": 7,
989
+ "y": 8,
990
+ }
991
+ if unit not in unit_labels:
992
+ if to_str:
993
+ return size
994
+ else:
995
+ if to_str:
996
+ return format_bytes(
997
+ size=int(number * (power**unit_labels[unit])),
998
+ use_1024_bytes=use_1024_bytes,
999
+ to_str=True,
1000
+ str_format=str_format,
1001
+ )
1002
+ return int(number * (power**unit_labels[unit]))
1003
+ else:
1004
+ try:
1005
+ return int(size)
1006
+ except Exception:
1007
+ return 0
1008
+ elif to_str or isinstance(size, int) or isinstance(size, float):
1009
+ if isinstance(size, str):
1010
+ try:
1011
+ size = size.rstrip("B").rstrip("b")
1012
+ size = float(size.lower().strip())
1013
+ except Exception:
1014
+ return size
1015
+ if use_1024_bytes or use_1024_bytes is None:
1016
+ power = 2**10
1017
+ n = 0
1018
+ power_labels = {
1019
+ 0: "",
1020
+ 1: "Ki",
1021
+ 2: "Mi",
1022
+ 3: "Gi",
1023
+ 4: "Ti",
1024
+ 5: "Pi",
1025
+ 6: "Ei",
1026
+ 7: "Zi",
1027
+ 8: "Yi",
1028
+ }
1029
+ while size > power:
1030
+ size /= power
1031
+ n += 1
1032
+ return f"{size:{str_format}} {' '}{power_labels[n]}".replace(" ", " ")
1033
+ else:
1034
+ power = 10**3
1035
+ n = 0
1036
+ power_labels = {
1037
+ 0: "",
1038
+ 1: "K",
1039
+ 2: "M",
1040
+ 3: "G",
1041
+ 4: "T",
1042
+ 5: "P",
1043
+ 6: "E",
1044
+ 7: "Z",
1045
+ 8: "Y",
1046
+ }
1047
+ while size > power:
1048
+ size /= power
1049
+ n += 1
1050
+ return f"{size:{str_format}} {' '}{power_labels[n]}".replace(" ", " ")
1051
+ else:
1052
+ try:
1053
+ return format_bytes(float(size), use_1024_bytes)
1054
+ except Exception:
1055
+ pass
1056
+ return 0
840
1057
 
841
- if __name__ == '__main__':
842
- main()
File without changes
File without changes