multiSSH3 6.16__tar.gz → 6.18__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: multiSSH3
3
- Version: 6.16
3
+ Version: 6.18
4
4
  Summary: Run commands on multiple hosts via SSH
5
5
  Home-page: https://github.com/yufei-pan/multiSSH3
6
6
  Author: Yufei Pan
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: multiSSH3
3
- Version: 6.16
3
+ Version: 6.18
4
4
  Summary: Run commands on multiple hosts via SSH
5
5
  Home-page: https://github.com/yufei-pan/multiSSH3
6
6
  Author: Yufei Pan
@@ -34,20 +34,114 @@ from collections import Counter, defaultdict, deque
34
34
  from itertools import count, product
35
35
  from pprint import pformat
36
36
 
37
- __curses_available = False
38
- __resource_lib_available = False
39
- try:
40
- import curses
41
- import curses.panel
42
- __curses_available = True
43
- except ImportError:
44
- pass
45
- try:
46
- import resource
47
- __resource_lib_available = True
48
- except ImportError:
49
- pass
50
37
 
38
+ version = '6.18'
39
+ VERSION = version
40
+ __version__ = version
41
+ COMMIT_DATE = '2026-04-09'
42
+ # TODO: Add terminal TUI
43
+
44
+ CONFIG_FILE_CHAIN = ['./multiSSH3.config.json',
45
+ '~/multiSSH3.config.json',
46
+ '~/.multiSSH3.config.json',
47
+ '~/.config/multiSSH3/multiSSH3.config.json',
48
+ '/etc/multiSSH3.d/multiSSH3.config.json',
49
+ '/etc/multiSSH3.config.json'] # The first one has the highest priority
50
+
51
+ ERRORS = []
52
+
53
+ #%% ------------ User Defined Global Variables ----------------
54
+ AUTHOR = 'Yufei Pan'
55
+ AUTHOR_EMAIL = 'pan@zopyr.us'
56
+ DEFAULT_HOSTS = 'all'
57
+ DEFAULT_USERNAME = None
58
+ DEFAULT_PASSWORD = ''
59
+ DEFAULT_IDENTITY_FILE = None
60
+ DEFAULT_SSH_KEY_SEARCH_PATH = '~/.ssh/'
61
+ DEFAULT_USE_KEY = False
62
+ DEFAULT_EXTRA_ARGS = None
63
+ DEFAULT_ONE_ON_ONE = False
64
+ DEFAULT_SCP = False
65
+ DEFAULT_FILE_SYNC = False
66
+ DEFAULT_TIMEOUT = 50
67
+ DEFAULT_CLI_TIMEOUT = 0
68
+ DEFAULT_UNAVAILABLE_HOST_EXPIRY = 600
69
+ DEFAULT_REPEAT = 1
70
+ DEFAULT_INTERVAL = 0
71
+ DEFAULT_IPMI = False
72
+ DEFAULT_IPMI_INTERFACE_IP_PREFIX = ''
73
+ DEFAULT_INTERFACE_IP_PREFIX = None
74
+ DEFAULT_IPMI_USERNAME = 'ADMIN'
75
+ DEFAULT_IPMI_PASSWORD = ''
76
+ DEAFULT_IPMI_ARGS=''
77
+ DEFAULT_NO_WATCH = False
78
+ DEFAULT_WINDOW_WIDTH = 40
79
+ DEFAULT_WINDOW_HEIGHT = 1
80
+ DEFAULT_SINGLE_WINDOW = False
81
+ DEFAULT_ERROR_ONLY = False
82
+ DEFAULT_NO_OUTPUT = False
83
+ DEFAULT_RETURN_ZERO = False
84
+ DEFAULT_NO_ENV = False
85
+ DEFAULT_ENV_FILE = ''
86
+ DEFAULT_ENV_FILES = ['/etc/profile.d/hosts.sh',
87
+ '~/.bashrc',
88
+ '~/.zshrc',
89
+ '~/host.env',
90
+ '~/hosts.env',
91
+ '.env',
92
+ 'host.env',
93
+ 'hosts.env',
94
+ ]
95
+ DEFAULT_NO_HISTORY = False
96
+ DEFAULT_HISTORY_FILE = '~/.mssh_history'
97
+ DEFAULT_MAX_CONNECTIONS = 4 * os.cpu_count()
98
+ DEFAULT_JSON_MODE = False
99
+ DEFAULT_PRINT_SUCCESS_HOSTS = False
100
+ DEFAULT_GREPPABLE_MODE = False
101
+ DEFAULT_SKIP_UNREACHABLE = True
102
+ DEFAULT_SKIP_HOSTS = ''
103
+ DEFAULT_HOST_FILE = '/etc/hosts'
104
+ DEFAULT_ENCODING = 'utf-8'
105
+ DEFAULT_DIFF_DISPLAY_THRESHOLD = 0.9
106
+ SSH_STRICT_HOST_KEY_CHECKING = False
107
+ FORCE_TRUECOLOR = False
108
+ ERROR_MESSAGES_TO_IGNORE = [
109
+ 'Pseudo-terminal will not be allocated because stdin is not a terminal',
110
+ 'Connection to .* closed',
111
+ 'Warning: Permanently added',
112
+ 'mux_client_request_session',
113
+ 'disabling multiplexing',
114
+ 'Killed by signal',
115
+ 'Connection reset by peer',
116
+ ]
117
+ POSSIBLE_SSH_KEY_FILES = ['id_ed25519','id_ed25519_sk','id_ecdsa','id_ecdsa_sk','id_rsa','id_dsa']
118
+ __DEFAULT_COLOR_PALETTE = {
119
+ 'cyan': (86, 173, 188),
120
+ 'green': (114, 180, 43),
121
+ 'magenta': (140, 107, 200),
122
+ 'red': (196, 38, 94),
123
+ 'white': (227, 227, 221),
124
+ 'yellow': (179, 180, 43),
125
+ 'blue': (106, 126, 200),
126
+ 'bright_black': (102, 102, 102),
127
+ 'bright_blue': (129, 154, 255),
128
+ 'bright_cyan': (102, 217, 239),
129
+ 'bright_green': (126, 226, 46),
130
+ 'bright_magenta': (174, 129, 255),
131
+ 'bright_red': (249, 38, 114),
132
+ 'bright_white': (248, 248, 242),
133
+ 'bright_yellow': (226, 226, 46),
134
+ }
135
+ COLOR_PALETTE = __DEFAULT_COLOR_PALETTE.copy()
136
+ _DEFAULT_CALLED = True
137
+ _DEFAULT_RETURN_UNFINISHED = False
138
+ _DEFAULT_UPDATE_UNREACHABLE_HOSTS = True
139
+ _DEFAULT_NO_START = False
140
+ _etc_hosts = {}
141
+ __ERROR_MESSAGES_TO_IGNORE_REGEX =None
142
+ __DEBUG_MODE = False
143
+
144
+ #%% ------------ Pre Helper Functions ----------------
51
145
  try:
52
146
  # Check if functiools.cache is available
53
147
  # cache_decorator = functools.cache
@@ -85,23 +179,7 @@ except Exception:
85
179
  print('Warning: functools.lru_cache is not available, multiSSH3 will run slower without cache.',file=sys.stderr)
86
180
  def cache_decorator(func):
87
181
  return func
88
- version = '6.16'
89
- VERSION = version
90
- __version__ = version
91
- COMMIT_DATE = '2026-03-16'
92
-
93
- CONFIG_FILE_CHAIN = ['./multiSSH3.config.json',
94
- '~/multiSSH3.config.json',
95
- '~/.multiSSH3.config.json',
96
- '~/.config/multiSSH3/multiSSH3.config.json',
97
- '/etc/multiSSH3.d/multiSSH3.config.json',
98
- '/etc/multiSSH3.config.json'] # The first one has the highest priority
99
-
100
- ERRORS = []
101
-
102
- # TODO: Add terminal TUI
103
182
 
104
- #%% ------------ Pre Helper Functions ----------------
105
183
  def eprint(*args, **kwargs):
106
184
  global ERRORS
107
185
  try:
@@ -245,76 +323,6 @@ def _get_i():
245
323
  '''
246
324
  return next(_i_counter)
247
325
 
248
- #%% ------------ Host Object ----------------
249
- class Host:
250
- def __init__(self, name, command, files = None,ipmi = False,interface_ip_prefix = None,scp=False,extraargs=None,gatherMode=False,identity_file=None,shell=False,i = -1,uuid=uuid.uuid4(),ip = None):
251
- self.name = name # the name of the host (hostname or IP address)
252
- self.command = command # the command to run on the host
253
- self.returncode = None # the return code of the command
254
- self.output = [] # the output of the command for curses
255
- self.stdout = [] # the stdout of the command
256
- self.stderr = [] # the stderr of the command
257
- self.lineNumToPrintSet = set() # line numbers to reprint
258
- self.lastUpdateTime = time.monotonic() # the last time the output was updated
259
- self.lastPrintedUpdateTime = 0 # the last time the output was printed
260
- self.files = files # the files to be copied to the host
261
- self.ipmi = ipmi # whether to use ipmi to connect to the host
262
- self.shell = shell # whether to use shell to run the command
263
- self.interface_ip_prefix = interface_ip_prefix # the prefix of the ip address of the interface to be used to connect to the host
264
- self.scp = scp # whether to use scp to copy files to the host
265
- self.gatherMode = gatherMode # whether the host is in gather mode
266
- self.extraargs = extraargs # extra arguments to be passed to ssh
267
- self.resolvedName = None # the resolved IP address of the host
268
- # also store a globally unique integer i from 0
269
- self.i = i if i != -1 else _get_i()
270
- self.uuid = uuid
271
- self.identity_file = identity_file
272
- self.ip = ip if ip else getIP(name)
273
- self.current_color_pair = [-1, -1, 1]
274
- self.output_buffer = io.BytesIO()
275
- self.stdout_buffer = io.BytesIO()
276
- self.stderr_buffer = io.BytesIO()
277
- self.thread = None
278
-
279
- def __iter__(self):
280
- return zip(['name', 'command', 'returncode', 'stdout', 'stderr'], [self.name, self.command, self.returncode, self.stdout, self.stderr])
281
- def __repr__(self):
282
- return f"Host(\n{pformat(self.__dict__)}\n)"
283
- def __str__(self):
284
- return f"Host(\n{pformat(dict(self))}\n)"
285
- def get_output_hash(self):
286
- return hash((
287
- self.command,
288
- tuple(self.stdout),
289
- tuple(self.stderr),
290
- self.returncode
291
- ))
292
- def copy(self):
293
- newHost = Host(
294
- name=self.name,
295
- command=self.command,
296
- files=self.files,
297
- ipmi=self.ipmi,
298
- interface_ip_prefix=self.interface_ip_prefix,
299
- scp=self.scp,
300
- extraargs=self.extraargs,
301
- gatherMode=self.gatherMode,
302
- identity_file=self.identity_file,
303
- shell=self.shell,
304
- i=self.i,
305
- uuid=self.uuid,
306
- ip=self.ip
307
- )
308
- newHost.returncode = self.returncode
309
- newHost.output = self.output.copy()
310
- newHost.stdout = self.stdout.copy()
311
- newHost.stderr = self.stderr.copy()
312
- newHost.lineNumToPrintSet = self.lineNumToPrintSet.copy()
313
- newHost.lastUpdateTime = self.lastUpdateTime
314
- newHost.lastPrintedUpdateTime = self.lastPrintedUpdateTime
315
- newHost.current_color_pair = self.current_color_pair.copy()
316
- return newHost
317
-
318
326
  #%% ------------ Load Defaults ( Config ) File ----------------
319
327
  def load_config_file(config_file):
320
328
  '''
@@ -336,96 +344,6 @@ def load_config_file(config_file):
336
344
  return {}
337
345
  return config
338
346
 
339
- #%% ------------ Global Variables ----------------
340
- AUTHOR = 'Yufei Pan'
341
- AUTHOR_EMAIL = 'pan@zopyr.us'
342
- DEFAULT_HOSTS = 'all'
343
- DEFAULT_USERNAME = None
344
- DEFAULT_PASSWORD = ''
345
- DEFAULT_IDENTITY_FILE = None
346
- DEFAULT_SSH_KEY_SEARCH_PATH = '~/.ssh/'
347
- DEFAULT_USE_KEY = False
348
- DEFAULT_EXTRA_ARGS = None
349
- DEFAULT_ONE_ON_ONE = False
350
- DEFAULT_SCP = False
351
- DEFAULT_FILE_SYNC = False
352
- DEFAULT_TIMEOUT = 50
353
- DEFAULT_CLI_TIMEOUT = 0
354
- DEFAULT_UNAVAILABLE_HOST_EXPIRY = 600
355
- DEFAULT_REPEAT = 1
356
- DEFAULT_INTERVAL = 0
357
- DEFAULT_IPMI = False
358
- DEFAULT_IPMI_INTERFACE_IP_PREFIX = ''
359
- DEFAULT_INTERFACE_IP_PREFIX = None
360
- DEFAULT_IPMI_USERNAME = 'ADMIN'
361
- DEFAULT_IPMI_PASSWORD = ''
362
- DEAFULT_IPMI_ARGS=''
363
- DEFAULT_NO_WATCH = False
364
- DEFAULT_WINDOW_WIDTH = 40
365
- DEFAULT_WINDOW_HEIGHT = 1
366
- DEFAULT_SINGLE_WINDOW = False
367
- DEFAULT_ERROR_ONLY = False
368
- DEFAULT_NO_OUTPUT = False
369
- DEFAULT_RETURN_ZERO = False
370
- DEFAULT_NO_ENV = False
371
- DEFAULT_ENV_FILE = ''
372
- DEFAULT_ENV_FILES = ['/etc/profile.d/hosts.sh',
373
- '~/.bashrc',
374
- '~/.zshrc',
375
- '~/host.env',
376
- '~/hosts.env',
377
- '.env',
378
- 'host.env',
379
- 'hosts.env',
380
- ]
381
- DEFAULT_NO_HISTORY = False
382
- DEFAULT_HISTORY_FILE = '~/.mssh_history'
383
- DEFAULT_MAX_CONNECTIONS = 4 * os.cpu_count()
384
- DEFAULT_JSON_MODE = False
385
- DEFAULT_PRINT_SUCCESS_HOSTS = False
386
- DEFAULT_GREPPABLE_MODE = False
387
- DEFAULT_SKIP_UNREACHABLE = True
388
- DEFAULT_SKIP_HOSTS = ''
389
- DEFAULT_HOST_FILE = '/etc/hosts'
390
- DEFAULT_ENCODING = 'utf-8'
391
- DEFAULT_DIFF_DISPLAY_THRESHOLD = 0.6
392
- SSH_STRICT_HOST_KEY_CHECKING = False
393
- FORCE_TRUECOLOR = False
394
- ERROR_MESSAGES_TO_IGNORE = [
395
- 'Pseudo-terminal will not be allocated because stdin is not a terminal',
396
- 'Connection to .* closed',
397
- 'Warning: Permanently added',
398
- 'mux_client_request_session',
399
- 'disabling multiplexing',
400
- 'Killed by signal',
401
- 'Connection reset by peer',
402
- ]
403
- __DEFAULT_COLOR_PALETTE = {
404
- 'cyan': (86, 173, 188),
405
- 'green': (114, 180, 43),
406
- 'magenta': (140, 107, 200),
407
- 'red': (196, 38, 94),
408
- 'white': (227, 227, 221),
409
- 'yellow': (179, 180, 43),
410
- 'blue': (106, 126, 200),
411
- 'bright_black': (102, 102, 102),
412
- 'bright_blue': (129, 154, 255),
413
- 'bright_cyan': (102, 217, 239),
414
- 'bright_green': (126, 226, 46),
415
- 'bright_magenta': (174, 129, 255),
416
- 'bright_red': (249, 38, 114),
417
- 'bright_white': (248, 248, 242),
418
- 'bright_yellow': (226, 226, 46),
419
- }
420
- COLOR_PALETTE = __DEFAULT_COLOR_PALETTE.copy()
421
- _DEFAULT_CALLED = True
422
- _DEFAULT_RETURN_UNFINISHED = False
423
- _DEFAULT_UPDATE_UNREACHABLE_HOSTS = True
424
- _DEFAULT_NO_START = False
425
- _etc_hosts = {}
426
- __ERROR_MESSAGES_TO_IGNORE_REGEX =None
427
- __DEBUG_MODE = False
428
-
429
347
  #%% Load Config Based Default Global variables
430
348
  __configs_from_file = {}
431
349
  for config_file in reversed(CONFIG_FILE_CHAIN.copy()):
@@ -467,12 +385,42 @@ __control_master_string = '''Host *
467
385
  ControlPath /run/user/%i/ssh_sockets_%C
468
386
  ControlPersist 3600
469
387
  '''
388
+ __curses_available = False
389
+ __resource_lib_available = False
390
+ try:
391
+ import curses
392
+ import curses.panel
393
+ __curses_available = True
394
+ except ImportError:
395
+ pass
396
+ try:
397
+ import resource
398
+ __resource_lib_available = True
399
+ except ImportError:
400
+ pass
470
401
  if __resource_lib_available:
471
402
  # Get the current limits
472
403
  _, __system_nofile_limit = resource.getrlimit(resource.RLIMIT_NOFILE)
473
404
  # Set the soft limit to the hard limit
474
405
  resource.setrlimit(resource.RLIMIT_NOFILE, (__system_nofile_limit, __system_nofile_limit))
475
406
  __max_connections_nofile_limit_supported = int((__system_nofile_limit - 10) / 3)
407
+ _binPaths = {}
408
+ _binCalled = set(['sshpass', 'ssh', 'scp', 'ipmitool','rsync','sh','ssh-copy-id'])
409
+ def check_path(program_name):
410
+ global __configs_from_file
411
+ global _binPaths
412
+ config_key = f'_{program_name}Path'
413
+ program_path = (
414
+ __configs_from_file.get(config_key) or
415
+ globals().get(config_key) or
416
+ shutil.which(program_name)
417
+ )
418
+ if program_path:
419
+ _binPaths[program_name] = program_path
420
+ return True
421
+ return False
422
+
423
+ [check_path(program) for program in _binCalled]
476
424
 
477
425
  #%% Mapping of ANSI 4-bit colors to curses colors
478
426
  if __curses_available:
@@ -494,26 +442,78 @@ if __curses_available:
494
442
  96: curses.COLOR_CYAN, # Bright Cyan
495
443
  97: curses.COLOR_WHITE # Bright White
496
444
  }
497
- #%% ------------ Exportable Help Functions ----------------
498
- # check if command sshpass is available
499
- _binPaths = {}
500
- _binCalled = set(['sshpass', 'ssh', 'scp', 'ipmitool','rsync','sh','ssh-copy-id'])
501
- def check_path(program_name):
502
- global __configs_from_file
503
- global _binPaths
504
- config_key = f'_{program_name}Path'
505
- program_path = (
506
- __configs_from_file.get(config_key) or
507
- globals().get(config_key) or
508
- shutil.which(program_name)
509
- )
510
- if program_path:
511
- _binPaths[program_name] = program_path
512
- return True
513
- return False
514
445
 
515
- [check_path(program) for program in _binCalled]
446
+ #%% ------------ Host Object ----------------
447
+ class Host:
448
+ def __init__(self, name, command, files = None,ipmi = False,interface_ip_prefix = None,scp=False,extraargs=None,gatherMode=False,identity_file=None,shell=False,i = -1,uuid=uuid.uuid4(),ip = None):
449
+ self.name = name # the name of the host (hostname or IP address)
450
+ self.command = command # the command to run on the host
451
+ self.returncode = None # the return code of the command
452
+ self.output = [] # the output of the command for curses
453
+ self.stdout = [] # the stdout of the command
454
+ self.stderr = [] # the stderr of the command
455
+ self.lineNumToPrintSet = set() # line numbers to reprint
456
+ self.lastUpdateTime = time.monotonic() # the last time the output was updated
457
+ self.lastPrintedUpdateTime = 0 # the last time the output was printed
458
+ self.files = files # the files to be copied to the host
459
+ self.ipmi = ipmi # whether to use ipmi to connect to the host
460
+ self.shell = shell # whether to use shell to run the command
461
+ self.interface_ip_prefix = interface_ip_prefix # the prefix of the ip address of the interface to be used to connect to the host
462
+ self.scp = scp # whether to use scp to copy files to the host
463
+ self.gatherMode = gatherMode # whether the host is in gather mode
464
+ self.extraargs = extraargs # extra arguments to be passed to ssh
465
+ self.resolvedName = None # the resolved IP address of the host
466
+ # also store a globally unique integer i from 0
467
+ self.i = i if i != -1 else _get_i()
468
+ self.uuid = uuid
469
+ self.identity_file = identity_file
470
+ self.ip = ip if ip else getIP(name)
471
+ self.current_color_pair = [-1, -1, 1]
472
+ self.output_buffer = io.BytesIO()
473
+ self.stdout_buffer = io.BytesIO()
474
+ self.stderr_buffer = io.BytesIO()
475
+ self.thread = None
476
+
477
+ def __iter__(self):
478
+ return zip(['name', 'command', 'returncode', 'stdout', 'stderr'], [self.name, self.command, self.returncode, self.stdout, self.stderr])
479
+ def __repr__(self):
480
+ return f"Host(\n{pformat(self.__dict__)}\n)"
481
+ def __str__(self):
482
+ return f"Host(\n{pformat(dict(self))}\n)"
483
+ def get_output_hash(self):
484
+ return hash((
485
+ self.command,
486
+ tuple(self.stdout),
487
+ tuple(self.stderr),
488
+ self.returncode
489
+ ))
490
+ def copy(self):
491
+ newHost = Host(
492
+ name=self.name,
493
+ command=self.command,
494
+ files=self.files,
495
+ ipmi=self.ipmi,
496
+ interface_ip_prefix=self.interface_ip_prefix,
497
+ scp=self.scp,
498
+ extraargs=self.extraargs,
499
+ gatherMode=self.gatherMode,
500
+ identity_file=self.identity_file,
501
+ shell=self.shell,
502
+ i=self.i,
503
+ uuid=self.uuid,
504
+ ip=self.ip
505
+ )
506
+ newHost.returncode = self.returncode
507
+ newHost.output = self.output.copy()
508
+ newHost.stdout = self.stdout.copy()
509
+ newHost.stderr = self.stderr.copy()
510
+ newHost.lineNumToPrintSet = self.lineNumToPrintSet.copy()
511
+ newHost.lastUpdateTime = self.lastUpdateTime
512
+ newHost.lastPrintedUpdateTime = self.lastPrintedUpdateTime
513
+ newHost.current_color_pair = self.current_color_pair.copy()
514
+ return newHost
516
515
 
516
+ #%% ------------ Exportable Help Functions ----------------
517
517
  def find_ssh_key_file(searchPath = DEFAULT_SSH_KEY_SEARCH_PATH):
518
518
  '''
519
519
  Find the ssh public key file
@@ -528,8 +528,7 @@ def find_ssh_key_file(searchPath = DEFAULT_SSH_KEY_SEARCH_PATH):
528
528
  sshKeyPath = searchPath
529
529
  else:
530
530
  sshKeyPath ='~/.ssh'
531
- possibleSshKeyFiles = ['id_ed25519','id_ed25519_sk','id_ecdsa','id_ecdsa_sk','id_rsa','id_dsa']
532
- for sshKeyFile in possibleSshKeyFiles:
531
+ for sshKeyFile in POSSIBLE_SSH_KEY_FILES:
533
532
  if os.path.exists(os.path.expanduser(os.path.join(sshKeyPath,sshKeyFile))):
534
533
  return os.path.join(sshKeyPath,sshKeyFile)
535
534
  return None
@@ -3357,7 +3356,6 @@ def processRunOnHosts(timeout, password, max_connections, hosts, return_unfinish
3357
3356
 
3358
3357
 
3359
3358
  #%% ------------ Stringfy Block ----------------
3360
-
3361
3359
  def formHostStr(host) -> str:
3362
3360
  """
3363
3361
  Forms a comma-separated string of hosts.
@@ -3896,6 +3894,7 @@ def generate_default_config(args):
3896
3894
  'SSH_STRICT_HOST_KEY_CHECKING': SSH_STRICT_HOST_KEY_CHECKING,
3897
3895
  'ERROR_MESSAGES_TO_IGNORE': ERROR_MESSAGES_TO_IGNORE,
3898
3896
  'FORCE_TRUECOLOR': args.force_truecolor,
3897
+ 'POSSIBLE_SSH_KEY_FILES': POSSIBLE_SSH_KEY_FILES,
3899
3898
  }
3900
3899
 
3901
3900
  def write_default_config(args,CONFIG_FILE = None, force = False):
@@ -3939,9 +3938,17 @@ def write_default_config(args,CONFIG_FILE = None, force = False):
3939
3938
 
3940
3939
  #%% ------------ Argument Processing -----------------
3941
3940
  def get_parser():
3942
- global _binPaths
3943
- parser = argparse.ArgumentParser(description='Run a command on multiple hosts, Use #HOST# or #HOSTNAME# to replace the host name in the command.',
3944
- epilog=f'Found bins: {list(_binPaths.values())}\n Missing bins: {_binCalled - set(_binPaths.keys())}\n Terminal color capability: {get_terminal_color_capability()}\nConfig file chain: {CONFIG_FILE_CHAIN!r}',)
3941
+ parser = argparse.ArgumentParser(
3942
+ description='Run a command on multiple hosts, Use #HOST# or #HOSTNAME# to replace the host name in the command.',
3943
+ epilog=(
3944
+ f"Terminal color capability: {get_terminal_color_capability()}\n"
3945
+ f"Found bins: \n{pformat(set(_binPaths.values()))}\n"
3946
+ f"Missing bins: \n{pformat(_binCalled - set(_binPaths.keys()))}\n"
3947
+ f"Config file chain: \n{pformat(CONFIG_FILE_CHAIN)}\n"
3948
+ f"Hosts Env files searched:\n{pformat(DEFAULT_ENV_FILES)}"
3949
+ ),
3950
+ formatter_class=argparse.RawTextHelpFormatter,
3951
+ )
3945
3952
  parser.add_argument('hosts', metavar='hosts', type=str, nargs='?', help=f'Hosts to run the command on, use "," to seperate hosts. (default: {DEFAULT_HOSTS})',default=DEFAULT_HOSTS)
3946
3953
  parser.add_argument('commands', metavar='commands', type=str, nargs='*',default=None,help='the command to run on the hosts / the destination of the files #HOST# or #HOSTNAME# will be replaced with the host name.')
3947
3954
  parser.add_argument('-u','--username', type=str,help=f'The general username to use to connect to the hosts. Will get overwrote by individual username@host if specified. (default: {DEFAULT_USERNAME})',default=DEFAULT_USERNAME)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes