multiSSH3 5.92__py3-none-any.whl → 5.94__py3-none-any.whl

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.

Potentially problematic release.


This version of multiSSH3 might be problematic. Click here for more details.

multiSSH3.py CHANGED
@@ -47,7 +47,6 @@ try:
47
47
  except ImportError:
48
48
  pass
49
49
 
50
-
51
50
  try:
52
51
  # Check if functiools.cache is available
53
52
  # cache_decorator = functools.cache
@@ -85,10 +84,10 @@ except Exception:
85
84
  print('Warning: functools.lru_cache is not available, multiSSH3 will run slower without cache.',file=sys.stderr)
86
85
  def cache_decorator(func):
87
86
  return func
88
- version = '5.92'
87
+ version = '5.94'
89
88
  VERSION = version
90
89
  __version__ = version
91
- COMMIT_DATE = '2025-10-20'
90
+ COMMIT_DATE = '2025-10-21'
92
91
 
93
92
  CONFIG_FILE_CHAIN = ['./multiSSH3.config.json',
94
93
  '~/multiSSH3.config.json',
@@ -346,7 +345,16 @@ DEFAULT_ERROR_ONLY = False
346
345
  DEFAULT_NO_OUTPUT = False
347
346
  DEFAULT_RETURN_ZERO = False
348
347
  DEFAULT_NO_ENV = False
349
- DEFAULT_ENV_FILE = '/etc/profile.d/hosts.sh'
348
+ DEFAULT_ENV_FILE = ''
349
+ DEFAULT_ENV_FILES = ['/etc/profile.d/hosts.sh',
350
+ '~/.bashrc',
351
+ '~/.zshrc',
352
+ '~/host.env',
353
+ '~/hosts.env',
354
+ '.env',
355
+ 'host.env',
356
+ 'hosts.env',
357
+ ]
350
358
  DEFAULT_NO_HISTORY = False
351
359
  DEFAULT_HISTORY_FILE = '~/.mssh_history'
352
360
  DEFAULT_MAX_CONNECTIONS = 4 * os.cpu_count()
@@ -387,6 +395,9 @@ if __ERROR_MESSAGES_TO_IGNORE_REGEX:
387
395
  __ERROR_MESSAGES_TO_IGNORE_REGEX = re.compile(__ERROR_MESSAGES_TO_IGNORE_REGEX)
388
396
  else:
389
397
  __ERROR_MESSAGES_TO_IGNORE_REGEX = re.compile('|'.join(ERROR_MESSAGES_TO_IGNORE))
398
+ if DEFAULT_ENV_FILE:
399
+ if DEFAULT_ENV_FILE not in DEFAULT_ENV_FILES:
400
+ DEFAULT_ENV_FILES.append(DEFAULT_ENV_FILE)
390
401
 
391
402
  #%% Load mssh Functional Global Variables
392
403
  __global_suppress_printout = False
@@ -394,7 +405,7 @@ __mainReturnCode = 0
394
405
  __failedHosts = set()
395
406
  __wildCharacters = ['*','?','x']
396
407
  _no_env = DEFAULT_NO_ENV
397
- _env_file = DEFAULT_ENV_FILE
408
+ _env_files = DEFAULT_ENV_FILES
398
409
  __globalUnavailableHosts = dict()
399
410
  __ipmiiInterfaceIPPrefix = DEFAULT_IPMI_INTERFACE_IP_PREFIX
400
411
  __keyPressesIn = [[]]
@@ -476,35 +487,65 @@ def find_ssh_key_file(searchPath = DEDAULT_SSH_KEY_SEARCH_PATH):
476
487
  return None
477
488
 
478
489
  @cache_decorator
479
- def readEnvFromFile(environemnt_file = ''):
490
+ def readEnvFromFile():
480
491
  '''
481
492
  Read the environment variables from env_file
482
493
  Returns:
483
494
  dict: A dictionary of environment variables
484
495
  '''
485
- global env
486
- try:
487
- if env:
488
- return env
489
- except Exception:
490
- env = {}
491
- global _env_file
492
- if environemnt_file:
493
- envf = environemnt_file
494
- else:
495
- envf = _env_file if _env_file else DEFAULT_ENV_FILE
496
- if os.path.exists(envf):
497
- with open(envf,'r') as f:
498
- for line in f:
499
- if line.startswith('#') or not line.strip():
496
+ global _env_files
497
+ global _no_env
498
+ envfs = _env_files if _env_files else DEFAULT_ENV_FILES
499
+ translator = str.maketrans('&|"', ';;\'')
500
+ replacement_re = re.compile(r'\$(?:[A-Za-z_]\w*|\{[A-Za-z_]\w*\})')
501
+ environemnt = {}
502
+ scrubCounter = 0
503
+ for envf in envfs:
504
+ envf = os.path.expanduser(os.path.expandvars(envf))
505
+ if os.path.exists(envf):
506
+ with open(envf,'r') as f:
507
+ lines = f.readlines()
508
+ for line in lines:
509
+ line = line.strip()
510
+ if not line or line.startswith('#') or '=' not in line:
500
511
  continue
501
- key, value = line.replace('export ', '', 1).strip().split('=', 1)
502
- key = key.strip().strip('"').strip("'")
503
- value = value.strip().strip('"').strip("'")
504
- # avoid infinite recursion
505
- if key != value:
506
- env[key] = value.strip('"').strip("'")
507
- return env
512
+ line = line.translate(translator)
513
+ commands = re.split(r";(?=(?:[^']*'[^']*')*[^']*$)", line)
514
+ for command in commands:
515
+ if not command or command.startswith('#') or '=' not in command or command.startswith('alias '):
516
+ continue
517
+ fields = re.split(r" (?=(?:[^']*'[^']*')*[^']*$)", command)
518
+ for field in fields:
519
+ try:
520
+ if field.startswith('export '):
521
+ field = field.replace('export ', '', 1).strip()
522
+ if not field or field.startswith('#') or '=' not in field:
523
+ continue
524
+ key, _, values = field.partition('=')
525
+ key = key.strip().strip("'")
526
+ values = values.strip().strip("'")
527
+ if '$' in values:
528
+ scrubCounter += 16
529
+ if key and values and key != values:
530
+ environemnt[key] = values
531
+ except Exception:
532
+ continue
533
+ while scrubCounter:
534
+ scrubCounter -= 1
535
+ found = False
536
+ for key, value in environemnt.items():
537
+ if '$' in value:
538
+ for match in replacement_re.findall(value):
539
+ ref_key = match.strip('${}')
540
+ ref_value = environemnt.get(ref_key) if ref_key != key else None
541
+ if not ref_value and not _no_env:
542
+ ref_value = os.environ.get(ref_key)
543
+ if ref_value:
544
+ environemnt[key] = value.replace(match, ref_value)
545
+ found = True
546
+ if not found:
547
+ break
548
+ return environemnt
508
549
 
509
550
  def replace_magic_strings(string,keys,value,case_sensitive=False):
510
551
  '''
@@ -3100,7 +3141,7 @@ def __formCommandArgStr(oneonone = DEFAULT_ONE_ON_ONE, timeout = DEFAULT_TIMEOUT
3100
3141
  no_env=DEFAULT_NO_ENV,greppable=DEFAULT_GREPPABLE_MODE,skip_hosts = DEFAULT_SKIP_HOSTS,
3101
3142
  file_sync = False, error_only = DEFAULT_ERROR_ONLY, identity_file = DEFAULT_IDENTITY_FILE,
3102
3143
  copy_id = False, unavailable_host_expiry = DEFAULT_UNAVAILABLE_HOST_EXPIRY, no_history = DEFAULT_NO_HISTORY,
3103
- history_file = DEFAULT_HISTORY_FILE, env_file = DEFAULT_ENV_FILE,
3144
+ history_file = DEFAULT_HISTORY_FILE, env_file = DEFAULT_ENV_FILES,
3104
3145
  repeat = DEFAULT_REPEAT,interval = DEFAULT_INTERVAL,
3105
3146
  shortend = False) -> str:
3106
3147
  argsList = []
@@ -3144,8 +3185,8 @@ def __formCommandArgStr(oneonone = DEFAULT_ONE_ON_ONE, timeout = DEFAULT_TIMEOUT
3144
3185
  argsList.append(f'--unavailable_host_expiry={unavailable_host_expiry}' if not shortend else f'-uhe={unavailable_host_expiry}')
3145
3186
  if no_env:
3146
3187
  argsList.append('--no_env')
3147
- if env_file and env_file != DEFAULT_ENV_FILE:
3148
- argsList.append(f'--env_file="{env_file}"' if not shortend else f'-ef="{env_file}"')
3188
+ if env_file and env_file != DEFAULT_ENV_FILES:
3189
+ argsList.extend([f'--env_file="{ef}"' for ef in env_file] if not shortend else [f'-ef="{ef}"' for ef in env_file])
3149
3190
  if no_history:
3150
3191
  argsList.append('--no_history' if not shortend else '-nh')
3151
3192
  if history_file and history_file != DEFAULT_HISTORY_FILE:
@@ -3168,7 +3209,7 @@ def getStrCommand(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAULT_ONE_O
3168
3209
  skip_hosts = DEFAULT_SKIP_HOSTS, curses_min_char_len = DEFAULT_CURSES_MINIMUM_CHAR_LEN, curses_min_line_len = DEFAULT_CURSES_MINIMUM_LINE_LEN,
3169
3210
  single_window = DEFAULT_SINGLE_WINDOW,file_sync = False,error_only = DEFAULT_ERROR_ONLY, identity_file = DEFAULT_IDENTITY_FILE,
3170
3211
  copy_id = False, unavailable_host_expiry = DEFAULT_UNAVAILABLE_HOST_EXPIRY,no_history = DEFAULT_NO_HISTORY,
3171
- history_file = DEFAULT_HISTORY_FILE, env_file = DEFAULT_ENV_FILE,
3212
+ history_file = DEFAULT_HISTORY_FILE, env_file = DEFAULT_ENV_FILES,
3172
3213
  repeat = DEFAULT_REPEAT,interval = DEFAULT_INTERVAL,
3173
3214
  shortend = False,tabSeperated = False):
3174
3215
  _ = called
@@ -3580,7 +3621,7 @@ def generate_default_config(args):
3580
3621
  'DEFAULT_NO_OUTPUT': args.no_output,
3581
3622
  'DEFAULT_RETURN_ZERO': args.return_zero,
3582
3623
  'DEFAULT_NO_ENV': args.no_env,
3583
- 'DEFAULT_ENV_FILE': args.env_file,
3624
+ 'DEFAULT_ENV_FILES': args.env_file,
3584
3625
  'DEFAULT_NO_HISTORY': args.no_history,
3585
3626
  'DEFAULT_HISTORY_FILE': args.history_file,
3586
3627
  'DEFAULT_MAX_CONNECTIONS': args.max_connections if args.max_connections != 4 * os.cpu_count() else None,
@@ -3669,7 +3710,7 @@ def get_parser():
3669
3710
  parser.add_argument('-Q',"-no","--no_output", action='store_true', help=f"Do not print the output. (default: {DEFAULT_NO_OUTPUT})", default=DEFAULT_NO_OUTPUT)
3670
3711
  parser.add_argument('-Z','-rz','--return_zero', action='store_true', help=f"Return 0 even if there are errors. (default: {DEFAULT_RETURN_ZERO})", default=DEFAULT_RETURN_ZERO)
3671
3712
  parser.add_argument('-C','--no_env', action='store_true', help=f'Do not load the command line environment variables. (default: {DEFAULT_NO_ENV})', default=DEFAULT_NO_ENV)
3672
- parser.add_argument("--env_file", type=str, help=f"The file to load the mssh file based environment variables from. ( Still work with --no_env ) (default: {DEFAULT_ENV_FILE})", default=DEFAULT_ENV_FILE)
3713
+ parser.add_argument("--env_file", action='append', help=f"The files to load the mssh file based environment variables from. Can specify multiple. Load first to last. ( Still work with --no_env ) (default: {DEFAULT_ENV_FILES})", default=DEFAULT_ENV_FILES)
3673
3714
  parser.add_argument("-m","--max_connections", type=int, help="Max number of connections to use (default: 4 * cpu_count)", default=DEFAULT_MAX_CONNECTIONS)
3674
3715
  parser.add_argument("-j","--json", action='store_true', help=F"Output in json format. (default: {DEFAULT_JSON_MODE})", default=DEFAULT_JSON_MODE)
3675
3716
  parser.add_argument('-w',"--success_hosts", action='store_true', help=f"Output the hosts that succeeded in summary as well. (default: {DEFAULT_PRINT_SUCCESS_HOSTS})", default=DEFAULT_PRINT_SUCCESS_HOSTS)
@@ -3688,7 +3729,7 @@ def get_parser():
3688
3729
  parser.add_argument('-hf','--history_file', type=str, help=f'The file to store the history. (default: {DEFAULT_HISTORY_FILE})', default=DEFAULT_HISTORY_FILE)
3689
3730
  parser.add_argument('--script', action='store_true', help='Run the command in script mode, short for -SCRIPT or --no_watch --skip_unreachable --no_env --no_history --greppable --error_only')
3690
3731
  parser.add_argument('-e','--encoding', type=str, help=f'The encoding to use for the output. (default: {DEFAULT_ENCODING})', default=DEFAULT_ENCODING)
3691
- parser.add_argument('-ddt','--diff_display_threshold', type=float, help=f'The threshold of lines to display the diff when files differ. Set to 0 to always display the diff. (default: {DEFAULT_DIFF_DISPLAY_THRESHOLD})', default=DEFAULT_DIFF_DISPLAY_THRESHOLD)
3732
+ parser.add_argument('-dt','--diff_display_threshold', type=float, help=f'The threshold of lines to display the diff when files differ. {{0-1}} Set to 0 to always display the diff. Set to 1 to disable diff. (Only merge same) (default: {DEFAULT_DIFF_DISPLAY_THRESHOLD})', default=DEFAULT_DIFF_DISPLAY_THRESHOLD)
3692
3733
  parser.add_argument('--force_truecolor', action='store_true', help=f'Force truecolor output even when not in a truecolor terminal. (default: {FORCE_TRUECOLOR})', default=FORCE_TRUECOLOR)
3693
3734
  parser.add_argument("-V","--version", action='version', version=f'%(prog)s {version} @ {COMMIT_DATE} with [ {", ".join(_binPaths.keys())} ] by {AUTHOR} ({AUTHOR_EMAIL})')
3694
3735
  return parser
@@ -3784,7 +3825,7 @@ def process_keys(args):
3784
3825
  def set_global_with_args(args):
3785
3826
  global _emo
3786
3827
  global __ipmiiInterfaceIPPrefix
3787
- global _env_file
3828
+ global _env_files
3788
3829
  global __DEBUG_MODE
3789
3830
  global __configs_from_file
3790
3831
  global _encoding
@@ -3795,7 +3836,7 @@ def set_global_with_args(args):
3795
3836
  global FORCE_TRUECOLOR
3796
3837
  _emo = False
3797
3838
  __ipmiiInterfaceIPPrefix = args.ipmi_interface_ip_prefix
3798
- _env_file = args.env_file
3839
+ _env_files = args.env_file
3799
3840
  __DEBUG_MODE = args.debug
3800
3841
  _encoding = args.encoding
3801
3842
  if args.return_zero:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: multiSSH3
3
- Version: 5.92
3
+ Version: 5.94
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
@@ -0,0 +1,6 @@
1
+ multiSSH3.py,sha256=G9MnmKCEXXqZIwd_ALOAbNCpo0RUMVB_ohKVezLhTaI,177073
2
+ multissh3-5.94.dist-info/METADATA,sha256=RVISAr97zLajx6Baaqt9b0MOy2Jv7hfGGj74SZVOB_I,18093
3
+ multissh3-5.94.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
4
+ multissh3-5.94.dist-info/entry_points.txt,sha256=xi2rWWNfmHx6gS8Mmx0rZL2KZz6XWBYP3DWBpWAnnZ0,143
5
+ multissh3-5.94.dist-info/top_level.txt,sha256=tUwttxlnpLkZorSsroIprNo41lYSxjd2ASuL8-EJIJw,10
6
+ multissh3-5.94.dist-info/RECORD,,
@@ -1,6 +0,0 @@
1
- multiSSH3.py,sha256=Qd1CLxhgZIpxtyVZGCHvG0WgWMRoWG2MMOYc9C4XgdA,175456
2
- multissh3-5.92.dist-info/METADATA,sha256=5_7RfWwwIAQF25p_sQf46j0oKqfTVCYwzPcjo1fsFsk,18093
3
- multissh3-5.92.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
4
- multissh3-5.92.dist-info/entry_points.txt,sha256=xi2rWWNfmHx6gS8Mmx0rZL2KZz6XWBYP3DWBpWAnnZ0,143
5
- multissh3-5.92.dist-info/top_level.txt,sha256=tUwttxlnpLkZorSsroIprNo41lYSxjd2ASuL8-EJIJw,10
6
- multissh3-5.92.dist-info/RECORD,,