multiSSH3 5.25__tar.gz → 5.30__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.

Potentially problematic release.


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

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: multiSSH3
3
- Version: 5.25
3
+ Version: 5.30
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
@@ -96,7 +96,7 @@ An example .ssh/config:
96
96
  Host *
97
97
  StrictHostKeyChecking no
98
98
  ControlMaster auto
99
- ControlPath /run/ssh_sockets_%r@%h-%p
99
+ ControlPath /tmp/%u_ssh_sockets_%r@%h-%p
100
100
  ControlPersist 3600
101
101
  ```
102
102
 
@@ -124,10 +124,10 @@ While leaving minimum 40 characters / 1 line for each host display by default. Y
124
124
  Use ```mssh --help``` for more info.
125
125
 
126
126
  ```bash
127
- usage: mssh [-h] [-u USERNAME] [-p PASSWORD] [-ea EXTRAARGS] [-11] [-f FILE] [-fs] [--scp] [-gm] [-t TIMEOUT] [-r REPEAT] [-i INTERVAL]
128
- [--ipmi] [-mpre IPMI_INTERFACE_IP_PREFIX] [-pre INTERFACE_IP_PREFIX] [-q] [-ww WINDOW_WIDTH] [-wh WINDOW_HEIGHT] [-sw] [-eo]
129
- [-no] [--no_env] [--env_file ENV_FILE] [-m MAX_CONNECTIONS] [-j] [--success_hosts] [-g] [-su] [-sh SKIP_HOSTS]
130
- [--store_config_file] [--debug] [--copy-id] [-V]
127
+ usage: mssh [-h] [-u USERNAME] [-p PASSWORD] [-k [KEY]] [-uk] [-ea EXTRAARGS] [-11] [-f FILE] [-fs] [--scp] [-gm] [-t TIMEOUT] [-r REPEAT]
128
+ [-i INTERVAL] [--ipmi] [-mpre IPMI_INTERFACE_IP_PREFIX] [-pre INTERFACE_IP_PREFIX] [-q] [-ww WINDOW_WIDTH] [-wh WINDOW_HEIGHT]
129
+ [-sw] [-eo] [-no] [--no_env] [--env_file ENV_FILE] [-m MAX_CONNECTIONS] [-j] [--success_hosts] [-g] [-su | -nsu]
130
+ [-sh SKIP_HOSTS] [--store_config_file] [--debug] [-ci] [-V]
131
131
  [hosts] [commands ...]
132
132
 
133
133
  Run a command on multiple hosts, Use #HOST# or #HOSTNAME# to replace the host name in the command. Config file: /etc/multiSSH3.config.json
@@ -144,6 +144,10 @@ options:
144
144
  (default: None)
145
145
  -p PASSWORD, --password PASSWORD
146
146
  The password to use to connect to the hosts, (default: )
147
+ -k [KEY], --key [KEY], --identity [KEY]
148
+ The identity file to use to connect to the hosts. Implies --use_key. Specify a folder for program to search for a
149
+ key. Use option without value to use ~/.ssh/ (default: None)
150
+ -uk, --use_key Attempt to use public key file to connect to the hosts. (default: False)
147
151
  -ea EXTRAARGS, --extraargs EXTRAARGS
148
152
  Extra arguments to pass to the ssh / rsync / scp command. Put in one string for multiple arguments.Use "=" ! Ex.
149
153
  -ea="--delete" (default: None)
@@ -181,16 +185,20 @@ options:
181
185
  Max number of connections to use (default: 4 * cpu_count)
182
186
  -j, --json Output in json format. (default: False)
183
187
  --success_hosts Output the hosts that succeeded in summary as wells. (default: False)
184
- -g, --greppable Output in greppable format. (default: False)
188
+ -g, --greppable, --table
189
+ Output in greppable format. (default: False)
185
190
  -su, --skip_unreachable
186
- Skip unreachable hosts while using --repeat. Note: Timedout Hosts are considered unreachable. Note: multiple
187
- command sequence will still auto skip unreachable hosts. (default: False)
191
+ Skip unreachable hosts. Note: Timedout Hosts are considered unreachable. Note: multiple command sequence will
192
+ still auto skip unreachable hosts. (default: False)
193
+ -nsu, --no_skip_unreachable
194
+ Do not skip unreachable hosts. Note: Timedout Hosts are considered unreachable. Note: multiple command sequence
195
+ will still auto skip unreachable hosts. (default: True)
188
196
  -sh SKIP_HOSTS, --skip_hosts SKIP_HOSTS
189
197
  Skip the hosts in the list. (default: None)
190
198
  --store_config_file Store / generate the default config file from command line argument and current config at
191
199
  /etc/multiSSH3.config.json
192
200
  --debug Print debug information
193
- --copy-id Copy the ssh id to the hosts
201
+ -ci, --copy_id Copy the ssh id to the hosts
194
202
  -V, --version show program's version number and exit
195
203
  ```
196
204
 
@@ -80,7 +80,7 @@ An example .ssh/config:
80
80
  Host *
81
81
  StrictHostKeyChecking no
82
82
  ControlMaster auto
83
- ControlPath /run/ssh_sockets_%r@%h-%p
83
+ ControlPath /tmp/%u_ssh_sockets_%r@%h-%p
84
84
  ControlPersist 3600
85
85
  ```
86
86
 
@@ -108,10 +108,10 @@ While leaving minimum 40 characters / 1 line for each host display by default. Y
108
108
  Use ```mssh --help``` for more info.
109
109
 
110
110
  ```bash
111
- usage: mssh [-h] [-u USERNAME] [-p PASSWORD] [-ea EXTRAARGS] [-11] [-f FILE] [-fs] [--scp] [-gm] [-t TIMEOUT] [-r REPEAT] [-i INTERVAL]
112
- [--ipmi] [-mpre IPMI_INTERFACE_IP_PREFIX] [-pre INTERFACE_IP_PREFIX] [-q] [-ww WINDOW_WIDTH] [-wh WINDOW_HEIGHT] [-sw] [-eo]
113
- [-no] [--no_env] [--env_file ENV_FILE] [-m MAX_CONNECTIONS] [-j] [--success_hosts] [-g] [-su] [-sh SKIP_HOSTS]
114
- [--store_config_file] [--debug] [--copy-id] [-V]
111
+ usage: mssh [-h] [-u USERNAME] [-p PASSWORD] [-k [KEY]] [-uk] [-ea EXTRAARGS] [-11] [-f FILE] [-fs] [--scp] [-gm] [-t TIMEOUT] [-r REPEAT]
112
+ [-i INTERVAL] [--ipmi] [-mpre IPMI_INTERFACE_IP_PREFIX] [-pre INTERFACE_IP_PREFIX] [-q] [-ww WINDOW_WIDTH] [-wh WINDOW_HEIGHT]
113
+ [-sw] [-eo] [-no] [--no_env] [--env_file ENV_FILE] [-m MAX_CONNECTIONS] [-j] [--success_hosts] [-g] [-su | -nsu]
114
+ [-sh SKIP_HOSTS] [--store_config_file] [--debug] [-ci] [-V]
115
115
  [hosts] [commands ...]
116
116
 
117
117
  Run a command on multiple hosts, Use #HOST# or #HOSTNAME# to replace the host name in the command. Config file: /etc/multiSSH3.config.json
@@ -128,6 +128,10 @@ options:
128
128
  (default: None)
129
129
  -p PASSWORD, --password PASSWORD
130
130
  The password to use to connect to the hosts, (default: )
131
+ -k [KEY], --key [KEY], --identity [KEY]
132
+ The identity file to use to connect to the hosts. Implies --use_key. Specify a folder for program to search for a
133
+ key. Use option without value to use ~/.ssh/ (default: None)
134
+ -uk, --use_key Attempt to use public key file to connect to the hosts. (default: False)
131
135
  -ea EXTRAARGS, --extraargs EXTRAARGS
132
136
  Extra arguments to pass to the ssh / rsync / scp command. Put in one string for multiple arguments.Use "=" ! Ex.
133
137
  -ea="--delete" (default: None)
@@ -165,16 +169,20 @@ options:
165
169
  Max number of connections to use (default: 4 * cpu_count)
166
170
  -j, --json Output in json format. (default: False)
167
171
  --success_hosts Output the hosts that succeeded in summary as wells. (default: False)
168
- -g, --greppable Output in greppable format. (default: False)
172
+ -g, --greppable, --table
173
+ Output in greppable format. (default: False)
169
174
  -su, --skip_unreachable
170
- Skip unreachable hosts while using --repeat. Note: Timedout Hosts are considered unreachable. Note: multiple
171
- command sequence will still auto skip unreachable hosts. (default: False)
175
+ Skip unreachable hosts. Note: Timedout Hosts are considered unreachable. Note: multiple command sequence will
176
+ still auto skip unreachable hosts. (default: False)
177
+ -nsu, --no_skip_unreachable
178
+ Do not skip unreachable hosts. Note: Timedout Hosts are considered unreachable. Note: multiple command sequence
179
+ will still auto skip unreachable hosts. (default: True)
172
180
  -sh SKIP_HOSTS, --skip_hosts SKIP_HOSTS
173
181
  Skip the hosts in the list. (default: None)
174
182
  --store_config_file Store / generate the default config file from command line argument and current config at
175
183
  /etc/multiSSH3.config.json
176
184
  --debug Print debug information
177
- --copy-id Copy the ssh id to the hosts
185
+ -ci, --copy_id Copy the ssh id to the hosts
178
186
  -V, --version show program's version number and exit
179
187
  ```
180
188
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: multiSSH3
3
- Version: 5.25
3
+ Version: 5.30
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
@@ -96,7 +96,7 @@ An example .ssh/config:
96
96
  Host *
97
97
  StrictHostKeyChecking no
98
98
  ControlMaster auto
99
- ControlPath /run/ssh_sockets_%r@%h-%p
99
+ ControlPath /tmp/%u_ssh_sockets_%r@%h-%p
100
100
  ControlPersist 3600
101
101
  ```
102
102
 
@@ -124,10 +124,10 @@ While leaving minimum 40 characters / 1 line for each host display by default. Y
124
124
  Use ```mssh --help``` for more info.
125
125
 
126
126
  ```bash
127
- usage: mssh [-h] [-u USERNAME] [-p PASSWORD] [-ea EXTRAARGS] [-11] [-f FILE] [-fs] [--scp] [-gm] [-t TIMEOUT] [-r REPEAT] [-i INTERVAL]
128
- [--ipmi] [-mpre IPMI_INTERFACE_IP_PREFIX] [-pre INTERFACE_IP_PREFIX] [-q] [-ww WINDOW_WIDTH] [-wh WINDOW_HEIGHT] [-sw] [-eo]
129
- [-no] [--no_env] [--env_file ENV_FILE] [-m MAX_CONNECTIONS] [-j] [--success_hosts] [-g] [-su] [-sh SKIP_HOSTS]
130
- [--store_config_file] [--debug] [--copy-id] [-V]
127
+ usage: mssh [-h] [-u USERNAME] [-p PASSWORD] [-k [KEY]] [-uk] [-ea EXTRAARGS] [-11] [-f FILE] [-fs] [--scp] [-gm] [-t TIMEOUT] [-r REPEAT]
128
+ [-i INTERVAL] [--ipmi] [-mpre IPMI_INTERFACE_IP_PREFIX] [-pre INTERFACE_IP_PREFIX] [-q] [-ww WINDOW_WIDTH] [-wh WINDOW_HEIGHT]
129
+ [-sw] [-eo] [-no] [--no_env] [--env_file ENV_FILE] [-m MAX_CONNECTIONS] [-j] [--success_hosts] [-g] [-su | -nsu]
130
+ [-sh SKIP_HOSTS] [--store_config_file] [--debug] [-ci] [-V]
131
131
  [hosts] [commands ...]
132
132
 
133
133
  Run a command on multiple hosts, Use #HOST# or #HOSTNAME# to replace the host name in the command. Config file: /etc/multiSSH3.config.json
@@ -144,6 +144,10 @@ options:
144
144
  (default: None)
145
145
  -p PASSWORD, --password PASSWORD
146
146
  The password to use to connect to the hosts, (default: )
147
+ -k [KEY], --key [KEY], --identity [KEY]
148
+ The identity file to use to connect to the hosts. Implies --use_key. Specify a folder for program to search for a
149
+ key. Use option without value to use ~/.ssh/ (default: None)
150
+ -uk, --use_key Attempt to use public key file to connect to the hosts. (default: False)
147
151
  -ea EXTRAARGS, --extraargs EXTRAARGS
148
152
  Extra arguments to pass to the ssh / rsync / scp command. Put in one string for multiple arguments.Use "=" ! Ex.
149
153
  -ea="--delete" (default: None)
@@ -181,16 +185,20 @@ options:
181
185
  Max number of connections to use (default: 4 * cpu_count)
182
186
  -j, --json Output in json format. (default: False)
183
187
  --success_hosts Output the hosts that succeeded in summary as wells. (default: False)
184
- -g, --greppable Output in greppable format. (default: False)
188
+ -g, --greppable, --table
189
+ Output in greppable format. (default: False)
185
190
  -su, --skip_unreachable
186
- Skip unreachable hosts while using --repeat. Note: Timedout Hosts are considered unreachable. Note: multiple
187
- command sequence will still auto skip unreachable hosts. (default: False)
191
+ Skip unreachable hosts. Note: Timedout Hosts are considered unreachable. Note: multiple command sequence will
192
+ still auto skip unreachable hosts. (default: False)
193
+ -nsu, --no_skip_unreachable
194
+ Do not skip unreachable hosts. Note: Timedout Hosts are considered unreachable. Note: multiple command sequence
195
+ will still auto skip unreachable hosts. (default: True)
188
196
  -sh SKIP_HOSTS, --skip_hosts SKIP_HOSTS
189
197
  Skip the hosts in the list. (default: None)
190
198
  --store_config_file Store / generate the default config file from command line argument and current config at
191
199
  /etc/multiSSH3.config.json
192
200
  --debug Print debug information
193
- --copy-id Copy the ssh id to the hosts
201
+ -ci, --copy_id Copy the ssh id to the hosts
194
202
  -V, --version show program's version number and exit
195
203
  ```
196
204
 
@@ -24,6 +24,7 @@ import shutil
24
24
  import getpass
25
25
  import uuid
26
26
  import tempfile
27
+ import math
27
28
 
28
29
  try:
29
30
  # Check if functiools.cache is available
@@ -36,7 +37,7 @@ except AttributeError:
36
37
  # If neither is available, use a dummy decorator
37
38
  def cache_decorator(func):
38
39
  return func
39
- version = '5.25'
40
+ version = '5.30'
40
41
  VERSION = version
41
42
 
42
43
  CONFIG_FILE = '/etc/multiSSH3.config.json'
@@ -179,12 +180,17 @@ class Host:
179
180
  self.uuid = uuid
180
181
  self.identity_file = identity_file
181
182
  self.ip = ip if ip else getIP(name)
183
+ self.current_color_pair = [-1, -1, 1]
182
184
 
183
185
  def __iter__(self):
184
186
  return zip(['name', 'command', 'returncode', 'stdout', 'stderr'], [self.name, self.command, self.returncode, self.stdout, self.stderr])
185
187
  def __repr__(self):
186
188
  # return the complete data structure
187
- return f"Host(name={self.name}, command={self.command}, returncode={self.returncode}, stdout={self.stdout}, stderr={self.stderr}, output={self.output}, printedLines={self.printedLines}, files={self.files}, ipmi={self.ipmi}, interface_ip_prefix={self.interface_ip_prefix}, scp={self.scp}, gatherMode={self.gatherMode}, extraargs={self.extraargs}, resolvedName={self.resolvedName}, i={self.i}, uuid={self.uuid}), identity_file={self.identity_file}"
189
+ return f"Host(name={self.name}, command={self.command}, returncode={self.returncode}, stdout={self.stdout}, stderr={self.stderr}, \
190
+ output={self.output}, printedLines={self.printedLines}, files={self.files}, ipmi={self.ipmi}, \
191
+ interface_ip_prefix={self.interface_ip_prefix}, scp={self.scp}, gatherMode={self.gatherMode}, \
192
+ extraargs={self.extraargs}, resolvedName={self.resolvedName}, i={self.i}, uuid={self.uuid}), \
193
+ identity_file={self.identity_file}, ip={self.ip}, current_color_pair={self.current_color_pair}"
188
194
  def __str__(self):
189
195
  return f"Host(name={self.name}, command={self.command}, returncode={self.returncode}, stdout={self.stdout}, stderr={self.stderr})"
190
196
 
@@ -341,7 +347,30 @@ if True:
341
347
  __keyPressesIn = [[]]
342
348
  _emo = False
343
349
  _etc_hosts = __configs_from_file.get('_etc_hosts', __build_in_default_config['_etc_hosts'])
344
-
350
+ __curses_global_color_pairs = {(-1,-1):1}
351
+ __curses_current_color_pair_index = 2 # Start from 1, as 0 is the default color pair
352
+ __curses_color_table = {}
353
+ __curses_current_color_index = 10
354
+
355
+ # Mapping of ANSI 4-bit colors to curses colors
356
+ ANSI_TO_CURSES_COLOR = {
357
+ 30: curses.COLOR_BLACK,
358
+ 31: curses.COLOR_RED,
359
+ 32: curses.COLOR_GREEN,
360
+ 33: curses.COLOR_YELLOW,
361
+ 34: curses.COLOR_BLUE,
362
+ 35: curses.COLOR_MAGENTA,
363
+ 36: curses.COLOR_CYAN,
364
+ 37: curses.COLOR_WHITE,
365
+ 90: curses.COLOR_BLACK, # Bright Black (usually gray)
366
+ 91: curses.COLOR_RED, # Bright Red
367
+ 92: curses.COLOR_GREEN, # Bright Green
368
+ 93: curses.COLOR_YELLOW, # Bright Yellow
369
+ 94: curses.COLOR_BLUE, # Bright Blue
370
+ 95: curses.COLOR_MAGENTA, # Bright Magenta
371
+ 96: curses.COLOR_CYAN, # Bright Cyan
372
+ 97: curses.COLOR_WHITE # Bright White
373
+ }
345
374
  # ------------ Exportable Help Functions ----------------
346
375
  # check if command sshpass is available
347
376
  _binPaths = {}
@@ -758,7 +787,7 @@ def __filterSumDic(sumDic):
758
787
  return newSumDic
759
788
 
760
789
  @cache_decorator
761
- def compact_hostnames(Hostnames):
790
+ def __compact_hostnames(Hostnames):
762
791
  """
763
792
  Compact a list of hostnames.
764
793
  Compact numeric numbers into ranges.
@@ -803,6 +832,43 @@ def compact_hostnames(Hostnames):
803
832
  rtnSet.add(''.join(hostnameList))
804
833
  return frozenset(rtnSet)
805
834
 
835
+ def compact_hostnames(Hostnames):
836
+ """
837
+ Compact a list of hostnames.
838
+ Compact numeric numbers into ranges.
839
+
840
+ Args:
841
+ Hostnames (list): A list of hostnames.
842
+
843
+ Returns:
844
+ list: A list of comapcted hostname list.
845
+
846
+ Example:
847
+ >>> compact_hostnames(['server15', 'server16', 'server17'])
848
+ ['server[15-17]']
849
+ >>> compact_hostnames(['server-1', 'server-2', 'server-3'])
850
+ ['server-[1-3]']
851
+ >>> compact_hostnames(['server-1-2', 'server-1-1', 'server-2-1', 'server-2-2'])
852
+ ['server-[1-2]-[1-2]']
853
+ >>> compact_hostnames(['server-1-2', 'server-1-1', 'server-2-2'])
854
+ ['server-1-[1-2]', 'server-2-2']
855
+ >>> compact_hostnames(['test1-a', 'test2-a'])
856
+ ['test[1-2]-a']
857
+ >>> compact_hostnames(['sub-s1', 'sub-s2'])
858
+ ['sub-s[1-2]']
859
+ """
860
+ global __global_suppress_printout
861
+ if not isinstance(Hostnames, frozenset):
862
+ hostSet = frozenset(Hostnames)
863
+ else:
864
+ hostSet = Hostnames
865
+ compact_hosts = __compact_hostnames(hostSet)
866
+ if set(expand_hostnames(compact_hosts)) != set(expand_hostnames(hostSet)):
867
+ if not __global_suppress_printout:
868
+ eprint(f"Error compacting hostnames: {hostSet} -> {compact_hosts}")
869
+ compact_hosts = hostSet
870
+ return compact_hosts
871
+
806
872
  # ------------ Expanding Hostnames ----------------
807
873
  @cache_decorator
808
874
  def __validate_expand_hostname(hostname):
@@ -823,10 +889,10 @@ def __validate_expand_hostname(hostname):
823
889
  return [hostname]
824
890
  elif not _no_env and hostname in os.environ:
825
891
  # we will expand these hostnames again
826
- return expand_hostnames(frozenset(os.environ[hostname].split(',')))
892
+ return expand_hostnames(os.environ[hostname].split(','))
827
893
  elif hostname in readEnvFromFile():
828
894
  # we will expand these hostnames again
829
- return expand_hostnames(frozenset(readEnvFromFile()[hostname].split(',')))
895
+ return expand_hostnames(readEnvFromFile()[hostname].split(','))
830
896
  elif getIP(hostname,local=False):
831
897
  return [hostname]
832
898
  else:
@@ -940,7 +1006,7 @@ def __expand_hostname(text, validate=True):# -> set:
940
1006
  return expandedhosts
941
1007
 
942
1008
  @cache_decorator
943
- def expand_hostnames(hosts) -> dict:
1009
+ def __expand_hostnames(hosts) -> dict:
944
1010
  '''
945
1011
  Expand the hostnames in the hosts into a dictionary
946
1012
 
@@ -951,8 +1017,6 @@ def expand_hostnames(hosts) -> dict:
951
1017
  dict: A dictionary of expanded hostnames with key: hostname, value: resolved IP address
952
1018
  '''
953
1019
  expandedhosts = {}
954
- if isinstance(hosts, str):
955
- hosts = [hosts]
956
1020
  for host in hosts:
957
1021
  host = host.strip()
958
1022
  if not host:
@@ -986,6 +1050,24 @@ def expand_hostnames(hosts) -> dict:
986
1050
  [expandedhosts.update({host:ip}) for host,ip in zip(hostSetToAdd,iplist)]
987
1051
  return expandedhosts
988
1052
 
1053
+ def expand_hostnames(hosts):
1054
+ '''
1055
+ Expand the hostnames in the hosts into a dictionary
1056
+
1057
+ Args:
1058
+ hosts (list): A list of hostnames
1059
+
1060
+ Returns:
1061
+ dict: A dictionary of expanded hostnames with key: hostname, value: resolved IP address
1062
+ '''
1063
+ if isinstance(hosts, str):
1064
+ hosts = [hosts]
1065
+ # change data type to frozenset if it is not hashable
1066
+ if not isinstance(hosts, frozenset):
1067
+ hosts = frozenset(hosts)
1068
+ return __expand_hostnames(hosts)
1069
+
1070
+
989
1071
  # ------------ Run Command Block ----------------
990
1072
  def __handle_reading_stream(stream,target, host):
991
1073
  '''
@@ -1368,6 +1450,227 @@ def start_run_on_hosts(hosts, timeout=60,password=None,max_connections=4 * os.cp
1368
1450
  return threads
1369
1451
 
1370
1452
  # ------------ Display Block ----------------
1453
+ def __approximate_color_8bit(color):
1454
+ """
1455
+ Approximate an 8-bit color (0-255) to the nearest curses color.
1456
+
1457
+ Args:
1458
+ color: 8-bit color code
1459
+
1460
+ Returns:
1461
+ Curses color code
1462
+ """
1463
+ if color < 8: # Standard and bright colors
1464
+ return ANSI_TO_CURSES_COLOR.get(color % 8 + 30, curses.COLOR_WHITE)
1465
+ elif 8 <= color < 16: # Bright colors
1466
+ return ANSI_TO_CURSES_COLOR.get(color % 8 + 90, curses.COLOR_WHITE)
1467
+ elif 16 <= color <= 231: # Color cube
1468
+ # Convert 216-color cube index to RGB
1469
+ color -= 16
1470
+ r = (color // 36) % 6 * 51
1471
+ g = (color // 6) % 6 * 51
1472
+ b = color % 6 * 51
1473
+ return __approximate_color_24bit(r, g, b) # Map to the closest curses color
1474
+ elif 232 <= color <= 255: # Grayscale
1475
+ gray = (color - 232) * 10 + 8
1476
+ return __approximate_color_24bit(gray, gray, gray)
1477
+ else:
1478
+ return curses.COLOR_WHITE # Fallback to white for unexpected values
1479
+
1480
+ def __approximate_color_24bit(r, g, b):
1481
+ """
1482
+ Approximate a 24-bit RGB color to the nearest curses color.
1483
+ Will initiate a curses color if curses.can_change_color() is True.
1484
+
1485
+ Globals:
1486
+ __curses_color_table: Dictionary of RGB color to curses color code
1487
+ __curses_current_color_index: Current index of the
1488
+
1489
+ Args:
1490
+ r: Red component (0-255)
1491
+ g: Green component (0-255)
1492
+ b: Blue component (0-255)
1493
+
1494
+ Returns:
1495
+ Curses color code
1496
+ """
1497
+ if curses.can_change_color():
1498
+ global __curses_color_table,__curses_current_color_index
1499
+ # Initiate a new color if it does not exist
1500
+ if (r, g, b) not in __curses_color_table:
1501
+ if __curses_current_color_index >= curses.COLORS:
1502
+ eprint("Warning: Maximum number of colors reached. Wrapping around.")
1503
+ __curses_current_color_index = 10
1504
+ curses.init_color(__curses_current_color_index, int(r/255*1000), int(g/255*1000), int(b/255*1000))
1505
+ __curses_color_table[(r, g, b)] = __curses_current_color_index
1506
+ __curses_current_color_index += 1
1507
+ return __curses_color_table[(r, g, b)]
1508
+ # Fallback to 8-bit color approximation
1509
+ colors = {
1510
+ curses.COLOR_BLACK: (0, 0, 0),
1511
+ curses.COLOR_RED: (255, 0, 0),
1512
+ curses.COLOR_GREEN: (0, 255, 0),
1513
+ curses.COLOR_YELLOW: (255, 255, 0),
1514
+ curses.COLOR_BLUE: (0, 0, 255),
1515
+ curses.COLOR_MAGENTA: (255, 0, 255),
1516
+ curses.COLOR_CYAN: (0, 255, 255),
1517
+ curses.COLOR_WHITE: (255, 255, 255),
1518
+ }
1519
+ best_match = curses.COLOR_WHITE
1520
+ min_distance = float("inf")
1521
+ for color, (cr, cg, cb) in colors.items():
1522
+ distance = math.sqrt((r - cr) ** 2 + (g - cg) ** 2 + (b - cb) ** 2)
1523
+ if distance < min_distance:
1524
+ min_distance = distance
1525
+ best_match = color
1526
+ return best_match
1527
+
1528
+ def __parse_ansi_escape_sequence_to_curses_color(escape_code):
1529
+ """
1530
+ Parse ANSI escape codes to extract foreground and background colors.
1531
+
1532
+ Args:
1533
+ escape_code: ANSI escape sequence for color
1534
+
1535
+ Returns:
1536
+ Tuple of (foreground, background) curses color pairs.
1537
+ If the escape code is a reset code, return (-1, -1).
1538
+ None values indicate that the color should not be changed.
1539
+ """
1540
+ if not escape_code:
1541
+ return None, None
1542
+ color_match = re.match(r"\x1b\[(\d+)(?:;(\d+))?(?:;(\d+))?(?:;(\d+);(\d+);(\d+))?m", escape_code)
1543
+ if color_match:
1544
+ params = color_match.groups()
1545
+ if params[0] == "0" and not any(params[1:]): # Reset code
1546
+ return -1, -1
1547
+ if params[0] == "38" and params[1] == "5": # 8-bit foreground
1548
+ return __approximate_color_8bit(int(params[2])), None
1549
+ elif params[0] == "38" and params[1] == "2": # 24-bit foreground
1550
+ return __approximate_color_24bit(int(params[3]), int(params[4]), int(params[5])), None
1551
+ elif params[0] == "48" and params[1] == "5": # 8-bit background
1552
+ return None , __approximate_color_8bit(int(params[2]))
1553
+ elif params[0] == "48" and params[1] == "2": # 24-bit background
1554
+ return None, __approximate_color_24bit(int(params[3]), int(params[4]), int(params[5]))
1555
+ else:
1556
+ fg = None
1557
+ bg = None
1558
+ if params[0] and params[0].isdigit(): # 4-bit color
1559
+ fg = ANSI_TO_CURSES_COLOR.get(int(params[0]), curses.COLOR_WHITE)
1560
+ if params[1] and params[1].isdigit():
1561
+ bg = ANSI_TO_CURSES_COLOR.get(int(params[1]), curses.COLOR_BLACK)
1562
+ return fg, bg
1563
+ return None, None
1564
+
1565
+ def __get_curses_color_pair(fg, bg):
1566
+ """
1567
+ Use curses color int values to create a curses color pair.
1568
+
1569
+ Globals:
1570
+ __curses_global_color_pairs: Dictionary of color pairs
1571
+ __curses_current_color_pair_index: Current index of the color pair
1572
+
1573
+ Args:
1574
+ fg: Foreground color code
1575
+ bg: Background color code
1576
+
1577
+ Returns:
1578
+ Curses color pair code
1579
+ """
1580
+ global __curses_global_color_pairs, __curses_current_color_pair_index
1581
+ if (fg, bg) not in __curses_global_color_pairs:
1582
+ if __curses_current_color_pair_index >= curses.COLOR_PAIRS:
1583
+ print("Warning: Maximum number of color pairs reached, wrapping around.")
1584
+ __curses_current_color_pair_index = 1
1585
+ curses.init_pair(__curses_current_color_pair_index, fg, bg)
1586
+ __curses_global_color_pairs[(fg, bg)] = __curses_current_color_pair_index
1587
+ __curses_current_color_pair_index += 1
1588
+ return curses.color_pair(__curses_global_color_pairs[(fg, bg)])
1589
+
1590
+ def _curses_add_string_to_window(window, line, y = 0, x = 0, number_of_char_to_write = -1, color_pair_list = [-1,-1,1],fill_char=' ',parse_ascii_colors = True,centered = False,lead_str = '', trail_str = '',box_ansi_color = None):
1591
+ """
1592
+ Add a string to a curses window with / without ANSI color escape sequences translated to curses color pairs.
1593
+
1594
+ Args:
1595
+ window: curses window object
1596
+ line: The line to add
1597
+ y: Line position in the window. Use -1 to scroll the window up 1 line and add the line at the bottom
1598
+ x: Column position in the window
1599
+ number_of_char_to_write: Number of characters to write. -1 for all remaining space in line, 0 for no characters, and a positive integer for a specific number of characters.
1600
+ color_pair_list: List of [foreground, background, color_pair] curses color pair values
1601
+ fill_char: Character to fill the remaining space in the line
1602
+ parse_ascii_colors: Parse ASCII color codes
1603
+ centered: Center the text in the window
1604
+ lead_str: Leading string to add to the line
1605
+ trail_str: Trailing string to add to the line
1606
+
1607
+ Returns:
1608
+ None
1609
+ """
1610
+ if window.getmaxyx()[0] == 0 or window.getmaxyx()[1] == 0 or x >= window.getmaxyx()[1]:
1611
+ return
1612
+ if x < 0:
1613
+ x = window.getmaxyx()[1] + x
1614
+ if number_of_char_to_write == -1:
1615
+ numChar = window.getmaxyx()[1] - x -1
1616
+ elif number_of_char_to_write == 0:
1617
+ return
1618
+ elif number_of_char_to_write + x > window.getmaxyx()[1]:
1619
+ numChar = window.getmaxyx()[1] - x -1
1620
+ else:
1621
+ numChar = number_of_char_to_write
1622
+ if numChar < 0:
1623
+ return
1624
+ if y < 0 or y >= window.getmaxyx()[0]:
1625
+ window.move(0, 0)
1626
+ window.deleteln()
1627
+ y = window.getmaxyx()[0] - 1
1628
+ if parse_ascii_colors:
1629
+ segments = re.split(r"(\x1b\[[\d;]*m)", line) # Split line by ANSI escape codes
1630
+ else:
1631
+ segments = [line]
1632
+ charsWritten = 0
1633
+ boxFrontColor, boxBackColor = color_pair_list[0], color_pair_list[1]
1634
+ newBoxFrontColor, newBoxBackColor = __parse_ansi_escape_sequence_to_curses_color(box_ansi_color)
1635
+ if newBoxFrontColor:
1636
+ boxFrontColor = newBoxFrontColor
1637
+ if newBoxBackColor:
1638
+ boxBackColor = newBoxBackColor
1639
+ boxColorPair = __get_curses_color_pair(boxFrontColor, boxBackColor)
1640
+ # first add the lead_str
1641
+ window.addnstr(y, x, lead_str, numChar, boxColorPair)
1642
+ charsWritten = min(len(lead_str), numChar)
1643
+ # process centering
1644
+ if centered:
1645
+ fill_length = numChar - len(lead_str) - len(trail_str) - sum([len(segment) for segment in segments if not segment.startswith("\x1b[")])
1646
+ window.addnstr(y, x + charsWritten, fill_char * (fill_length // 2), numChar - charsWritten, boxColorPair)
1647
+ charsWritten += min(fill_length // 2, numChar - charsWritten)
1648
+ # add the segments
1649
+ for segment in segments:
1650
+ if parse_ascii_colors and segment.startswith("\x1b["):
1651
+ # Parse ANSI escape sequence
1652
+ newFrontColor, newBackColor = __parse_ansi_escape_sequence_to_curses_color(segment)
1653
+ if newFrontColor is not None:
1654
+ color_pair_list[0] = newFrontColor
1655
+ if newBackColor is not None:
1656
+ color_pair_list[1] = newBackColor
1657
+ color_pair_list[2] = __get_curses_color_pair(color_pair_list[0], color_pair_list[1])
1658
+ #window.addnstr(y, x + charsWritten, str(color_pair_list[2]), numChar - charsWritten, color_pair_list[2])
1659
+ #charsWritten += min(len(str(color_pair_list[2])), numChar - charsWritten)
1660
+ else:
1661
+ # Add text with current color
1662
+ if charsWritten < numChar:
1663
+ window.addnstr(y, x + charsWritten, segment, numChar - charsWritten, color_pair_list[2])
1664
+ charsWritten += min(len(segment), numChar - charsWritten)
1665
+ # if we have finished printing segments but we still have space, we will fill it with fill_char
1666
+ if charsWritten + len(trail_str) < numChar:
1667
+ fillStr = fill_char * (numChar - charsWritten - len(trail_str))
1668
+ #fillStr = f'{color_pair_list}'
1669
+ window.addnstr(y, x + charsWritten, fillStr + trail_str, numChar - charsWritten, boxColorPair)
1670
+ charsWritten += numChar - charsWritten
1671
+ else:
1672
+ window.addnstr(y, x + charsWritten, trail_str, numChar - charsWritten, boxColorPair)
1673
+
1371
1674
  def _get_hosts_to_display (hosts, max_num_hosts, hosts_to_display = None):
1372
1675
  '''
1373
1676
  Generate a list for the hosts to be displayed on the screen. This is used to display as much relevant information as possible.
@@ -1463,6 +1766,7 @@ def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min
1463
1766
  stdscr.nodelay(True)
1464
1767
  # we generate a stats window at the top of the screen
1465
1768
  stat_window = curses.newwin(1, max_x, 0, 0)
1769
+ stat_window.leaveok(True)
1466
1770
  # We create a window for each host
1467
1771
  host_windows = []
1468
1772
  for i, host in enumerate(hosts_to_display):
@@ -1473,13 +1777,18 @@ def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min
1473
1777
  #print(f"Creating a window at {y},{x}")
1474
1778
  # We create the window
1475
1779
  host_window = curses.newwin(host_window_height, host_window_width, y, x)
1780
+ host_window.idlok(True)
1781
+ host_window.scrollok(True)
1782
+ host_window.leaveok(True)
1476
1783
  host_windows.append(host_window)
1477
1784
  # If there is space left, we will draw the bottom border
1478
1785
  bottom_border = None
1479
1786
  if y + host_window_height < org_dim[0]:
1480
1787
  bottom_border = curses.newwin(1, max_x, y + host_window_height, 0)
1788
+ bottom_border.leaveok(True)
1481
1789
  #bottom_border.clear()
1482
- bottom_border.addstr(0, 0, '-' * (max_x - 1))
1790
+ #bottom_border.addnstr(0, 0, '-' * (max_x - 1), max_x - 1)
1791
+ _curses_add_string_to_window(window=bottom_border, y=0, line='-' * (max_x - 1),fill_char='-')
1483
1792
  bottom_border.refresh()
1484
1793
  while host_stats['running'] > 0 or host_stats['waiting'] > 0:
1485
1794
  # Check for keypress
@@ -1581,11 +1890,13 @@ def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min
1581
1890
  # encodedLine = encodedLine[:curserPosition] + ' ' + encodedLine[curserPosition:]
1582
1891
  stats = '┍'+ f"Send CMD: {encodedLine}"[:max_x - 2].center(max_x - 2, "━")
1583
1892
  if bottom_border:
1584
- bottom_stats = '└'+ f" Total: {len(hosts)} Running: {host_stats['running']} Failed: {host_stats['failed']} Finished: {host_stats['finished']} Waiting: {host_stats['waiting']} "[:max_x - 2].center(max_x - 2, "─")
1893
+ target_length = max_x - 2 + len('\x1b[33m\x1b[0m\x1b[31m\x1b[0m\x1b[32m\x1b[0m')
1894
+ bottom_stats = '└'+ f" Total: {len(hosts)} Running: \x1b[33m{host_stats['running']}\x1b[0m Failed: \x1b[31m{host_stats['failed']}\x1b[0m Finished: \x1b[32m{host_stats['finished']}\x1b[0m Waiting: {host_stats['waiting']} "[:target_length].center(target_length, "─")
1585
1895
  if bottom_stats != old_bottom_stat:
1586
1896
  old_bottom_stat = bottom_stats
1587
1897
  #bottom_border.clear()
1588
- bottom_border.addstr(0, 0, bottom_stats)
1898
+ #bottom_border.addnstr(0, 0, bottom_stats, max_x - 1)
1899
+ _curses_add_string_to_window(window=bottom_border, y=0, line=bottom_stats)
1589
1900
  bottom_border.refresh()
1590
1901
  if stats != old_stat or curserPosition != old_cursor_position:
1591
1902
  old_stat = stats
@@ -1598,9 +1909,9 @@ def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min
1598
1909
  #stat_window.clear()
1599
1910
  #stat_window.addstr(0, 0, stats)
1600
1911
  # add the line with curser that inverses the color at the curser position
1601
- stat_window.addstr(0, 0, stats[:curserPositionStats], curses.color_pair(1))
1602
- stat_window.addstr(0, curserPositionStats, stats[curserPositionStats], curses.color_pair(2))
1603
- stat_window.addstr(0, curserPositionStats + 1, stats[curserPositionStats + 1:], curses.color_pair(1))
1912
+ stat_window.addstr(0, 0, stats[:curserPositionStats])
1913
+ stat_window.addch(0,curserPositionStats, stats[curserPositionStats], curses.A_REVERSE)
1914
+ stat_window.addnstr(0, curserPositionStats + 1, stats[curserPositionStats + 1:], max_x - 1 - curserPositionStats)
1604
1915
  stat_window.refresh()
1605
1916
  # set the maximum refresh rate to 100 Hz
1606
1917
  if time.perf_counter() - last_refresh_time < 0.01:
@@ -1614,17 +1925,19 @@ def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min
1614
1925
  #host_window.clear()
1615
1926
  # we will try to center the name of the host with ┼ at the beginning and end and ─ in between
1616
1927
  linePrintOut = f'┼{(host.name+":["+host.command+"]")[:host_window_width - 2].center(host_window_width - 1, "─")}'.replace('\n', ' ').replace('\r', ' ').strip()
1617
- host_window.addstr(0, 0, linePrintOut)
1928
+ host_window.addnstr(0, 0, linePrintOut, host_window_width - 1)
1929
+ #_add_line_with_ascii_colors(window=host_window, y=0, x=0, line=linePrintOut, n=host_window_width - 1, color_pair_list = host.current_color_pair)
1618
1930
  # we will display the latest outputs of the host as much as we can
1619
1931
  for i, line in enumerate(host.output[-(host_window_height - 1):]):
1620
1932
  # print(f"Printng a line at {i + 1} with length of {len('│'+line[:host_window_width - 1])}")
1621
1933
  # time.sleep(10)
1622
- linePrintOut = ('│'+line[:host_window_width - 2].replace('\n', ' ').replace('\r', ' ')).strip().ljust(host_window_width - 1, ' ')
1623
- host_window.addstr(i + 1, 0, linePrintOut)
1934
+ linePrintOut = ('│'+line[:host_window_width - 2].replace('\n', ' ').replace('\r', ' ')).strip()
1935
+ #host_window.addnstr(i + 1, 0, linePrintOut, host_window_width - 1)
1936
+ _curses_add_string_to_window(window=host_window, y=i + 1, line=linePrintOut, color_pair_list=host.current_color_pair)
1624
1937
  # we draw the rest of the available lines
1625
1938
  for i in range(len(host.output), host_window_height - 1):
1626
1939
  # print(f"Printng a line at {i + 1} with length of {len('│')}")
1627
- host_window.addstr(i + 1, 0, '│'.ljust(host_window_width - 1, ' '))
1940
+ host_window.addnstr(i + 1, 0, '│'.ljust(host_window_width - 1, ' '), host_window_width - 1)
1628
1941
  host.printedLines = len(host.output)
1629
1942
  host_window.refresh()
1630
1943
  except Exception as e:
@@ -1654,33 +1967,39 @@ def curses_print(stdscr, hosts, threads, min_char_len = DEFAULT_CURSES_MINIMUM_C
1654
1967
  '''
1655
1968
  # We create all the windows we need
1656
1969
  # We initialize the color pair
1657
- curses.start_color()
1658
1970
  curses.curs_set(0)
1659
- curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLACK)
1660
- curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_WHITE)
1661
- curses.init_pair(3, curses.COLOR_RED, curses.COLOR_BLACK)
1662
- curses.init_pair(4, curses.COLOR_GREEN, curses.COLOR_BLACK)
1663
- curses.init_pair(5, curses.COLOR_YELLOW, curses.COLOR_BLACK)
1664
- curses.init_pair(6, curses.COLOR_BLUE, curses.COLOR_BLACK)
1665
- curses.init_pair(7, curses.COLOR_MAGENTA, curses.COLOR_BLACK)
1666
- curses.init_pair(8, curses.COLOR_CYAN, curses.COLOR_BLACK)
1667
- curses.init_pair(9, curses.COLOR_WHITE, curses.COLOR_RED)
1668
- curses.init_pair(10, curses.COLOR_WHITE, curses.COLOR_GREEN)
1669
- curses.init_pair(11, curses.COLOR_WHITE, curses.COLOR_YELLOW)
1670
- curses.init_pair(12, curses.COLOR_WHITE, curses.COLOR_BLUE)
1671
- curses.init_pair(13, curses.COLOR_WHITE, curses.COLOR_MAGENTA)
1672
- curses.init_pair(14, curses.COLOR_WHITE, curses.COLOR_CYAN)
1673
- curses.init_pair(15, curses.COLOR_BLACK, curses.COLOR_RED)
1674
- curses.init_pair(16, curses.COLOR_BLACK, curses.COLOR_GREEN)
1675
- curses.init_pair(17, curses.COLOR_BLACK, curses.COLOR_YELLOW)
1676
- curses.init_pair(18, curses.COLOR_BLACK, curses.COLOR_BLUE)
1677
- curses.init_pair(19, curses.COLOR_BLACK, curses.COLOR_MAGENTA)
1678
- curses.init_pair(20, curses.COLOR_BLACK, curses.COLOR_CYAN)
1679
-
1971
+ curses.start_color()
1972
+ curses.use_default_colors()
1973
+ curses.init_pair(1, -1, -1)
1680
1974
  # do not generate display if the output window have a size of zero
1681
1975
  if stdscr.getmaxyx()[0] < 2 or stdscr.getmaxyx()[1] < 2:
1682
1976
  return
1683
-
1977
+ stdscr.idlok(True)
1978
+ stdscr.scrollok(True)
1979
+ stdscr.leaveok(True)
1980
+ # generate some debug information before display initialization
1981
+ try:
1982
+ stdscr.clear()
1983
+ _curses_add_string_to_window(window=stdscr, y=0, line='Initializing display...', n=stdscr.getmaxyx()[1] - 1)
1984
+ # print the size
1985
+ _curses_add_string_to_window(window=stdscr, y=1, line=f"Terminal size: {stdscr.getmaxyx()}", n=stdscr.getmaxyx()[1] - 1)
1986
+ # print the number of hosts
1987
+ _curses_add_string_to_window(window=stdscr, y=2, line=f"Number of hosts: {len(hosts)}", n=stdscr.getmaxyx()[1] - 1)
1988
+ # print the number of threads
1989
+ _curses_add_string_to_window(window=stdscr, y=3, line=f"Number of threads: {len(threads)}", n=stdscr.getmaxyx()[1] - 1)
1990
+ # print the minimum character length
1991
+ _curses_add_string_to_window(window=stdscr, y=4, line=f"Minimum character length: {min_char_len}", n=stdscr.getmaxyx()[1] - 1)
1992
+ # print the minimum line length
1993
+ _curses_add_string_to_window(window=stdscr, y=5, line=f"Minimum line length: {min_line_len}", n=stdscr.getmaxyx()[1] - 1)
1994
+ # print the single window mode
1995
+ _curses_add_string_to_window(window=stdscr, y=6, line=f"Single window mode: {single_window}", n=stdscr.getmaxyx()[1] - 1)
1996
+ # print COLORS and COLOR_PAIRS count
1997
+ _curses_add_string_to_window(window=stdscr, y=7, line=f"len(COLORS): {curses.COLORS} len(COLOR_PAIRS): {curses.COLOR_PAIRS}", n=stdscr.getmaxyx()[1] - 1)
1998
+ # print if can change color
1999
+ _curses_add_string_to_window(window=stdscr, y=8, line=f"Real color capability: {curses.can_change_color()}", n=stdscr.getmaxyx()[1] - 1)
2000
+ stdscr.refresh()
2001
+ except:
2002
+ pass
1684
2003
  params = (-1,0 , min_char_len, min_line_len, single_window,'new config')
1685
2004
  while params:
1686
2005
  params = __generate_display(stdscr, hosts, *params)
@@ -1763,17 +2082,13 @@ def print_output(hosts,usejson = False,quiet = False,greppable = False):
1763
2082
  outputs.setdefault(hostPrintOut, set()).add(host['name'])
1764
2083
  rtnStr = ''
1765
2084
  for output, hostSet in outputs.items():
1766
- hostSet = frozenset(hostSet)
1767
- compact_hosts = compact_hostnames(hostSet)
1768
- if set(expand_hostnames(compact_hosts)) != set(expand_hostnames(hostSet)):
1769
- eprint(f"Error compacting hostnames: {hostSet} -> {compact_hosts}")
1770
- compact_hosts = hostSet
2085
+ compact_hosts = sorted(compact_hostnames(hostSet))
1771
2086
  if __global_suppress_printout:
1772
2087
  rtnStr += f'Abnormal returncode produced by {",".join(compact_hosts)}:\n'
1773
2088
  rtnStr += output+'\n'
1774
2089
  else:
1775
2090
  rtnStr += '*'*80+'\n'
1776
- rtnStr += f'These hosts: "{",".join(sorted(compact_hosts))}" have a response of:\n'
2091
+ rtnStr += f'These hosts: "{",".join(compact_hosts)}" have a response of:\n'
1777
2092
  rtnStr += output+'\n'
1778
2093
  if not __global_suppress_printout or outputs:
1779
2094
  rtnStr += '*'*80+'\n'
@@ -2065,13 +2380,13 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
2065
2380
  if '@' not in host:
2066
2381
  skipHostStr[i] = userStr + host
2067
2382
  skipHostStr = ','.join(skipHostStr)
2068
- targetHostDic = expand_hostnames(frozenset(hostStr.split(',')))
2383
+ targetHostDic = expand_hostnames(hostStr.split(','))
2069
2384
  if __DEBUG_MODE:
2070
2385
  eprint(f"Target hosts: {targetHostDic!r}")
2071
- skipHostsDic = expand_hostnames(frozenset(skipHostStr.split(',')))
2072
- if skipHostsDic:
2073
- eprint(f"Skipping hosts: {skipHostsDic!r}")
2386
+ skipHostsDic = expand_hostnames(skipHostStr.split(','))
2074
2387
  skipHostSet = set(skipHostsDic).union(skipHostsDic.values())
2388
+ if skipHostSet:
2389
+ eprint(f"Skipping hosts: \"{' '.join(sorted(compact_hostnames(skipHostSet)))}\"")
2075
2390
  if copy_id:
2076
2391
  if 'ssh-copy-id' in _binPaths:
2077
2392
  # we will copy the id to the hosts
@@ -2421,18 +2736,15 @@ def main():
2421
2736
  succeededHosts.add(host.name)
2422
2737
  succeededHosts -= __failedHosts
2423
2738
  # sort the failed hosts and succeeded hosts
2424
- __failedHosts = sorted(__failedHosts)
2425
- succeededHosts = sorted(succeededHosts)
2426
2739
  if __mainReturnCode > 0:
2427
- if not __global_suppress_printout: eprint(f'Complete. Failed hosts (Return Code not 0) count: {__mainReturnCode}')
2428
- # with open('/tmp/bashcmd.stdin','w') as f:
2429
- # f.write(f"export failed_hosts={__failedHosts}\n")
2430
- if not __global_suppress_printout: eprint(f'failed_hosts: {",".join(__failedHosts)}')
2740
+ if not __global_suppress_printout:
2741
+ eprint(f'Complete. Failed hosts (Return Code not 0) count: {__mainReturnCode}')
2742
+ eprint(f'failed_hosts: {",".join(sorted(compact_hostnames(__failedHosts)))}')
2431
2743
  else:
2432
2744
  if not __global_suppress_printout: eprint('Complete. All hosts returned 0.')
2433
2745
 
2434
2746
  if args.success_hosts and not __global_suppress_printout:
2435
- eprint(f'succeeded_hosts: {",".join(succeededHosts)}')
2747
+ eprint(f'succeeded_hosts: {",".join(sorted(compact_hostnames(succeededHosts)))}')
2436
2748
 
2437
2749
  if threading.active_count() > 1:
2438
2750
  if not __global_suppress_printout: eprint(f'Remaining active thread: {threading.active_count()}')
@@ -2,7 +2,7 @@ from setuptools import setup
2
2
 
3
3
  setup(
4
4
  name='multiSSH3',
5
- version='5.25',
5
+ version='5.30',
6
6
  description='Run commands on multiple hosts via SSH',
7
7
  long_description=open('README.md').read(),
8
8
  long_description_content_type='text/markdown',
File without changes
File without changes