multiSSH3 6.33__tar.gz → 6.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.
Files changed (111) hide show
  1. {multissh3-6.33 → multissh3-6.35}/PKG-INFO +9 -5
  2. multissh3-6.33/multiSSH3.egg-info/PKG-INFO → multissh3-6.35/README.md +8 -30
  3. {multissh3-6.33 → multissh3-6.35}/mssh_complete.py +41 -9
  4. multissh3-6.33/README.md → multissh3-6.35/multiSSH3.egg-info/PKG-INFO +34 -4
  5. {multissh3-6.33 → multissh3-6.35}/multiSSH3.egg-info/SOURCES.txt +3 -0
  6. {multissh3-6.33 → multissh3-6.35}/multiSSH3.py +508 -132
  7. {multissh3-6.33 → multissh3-6.35}/tests/complete_shell_harness.py +18 -2
  8. {multissh3-6.33 → multissh3-6.35}/tests/conftest.py +19 -0
  9. {multissh3-6.33 → multissh3-6.35}/tests/test_cli_option_words.py +102 -0
  10. {multissh3-6.33 → multissh3-6.35}/tests/test_complete_context.py +70 -3
  11. {multissh3-6.33 → multissh3-6.35}/tests/test_complete_diagnose.py +24 -3
  12. {multissh3-6.33 → multissh3-6.35}/tests/test_complete_keys.py +2 -2
  13. {multissh3-6.33 → multissh3-6.35}/tests/test_complete_picker.py +4 -2
  14. {multissh3-6.33 → multissh3-6.35}/tests/test_complete_shells.py +2 -2
  15. {multissh3-6.33 → multissh3-6.35}/tests/test_curses_event_loop.py +256 -0
  16. {multissh3-6.33 → multissh3-6.35}/tests/test_expand_hostnames.py +15 -0
  17. {multissh3-6.33 → multissh3-6.35}/tests/test_options_after_command.py +14 -0
  18. {multissh3-6.33 → multissh3-6.35}/tests/test_runtime_branch_matrix.py +1 -1
  19. multissh3-6.35/tests/test_shared_lines.py +219 -0
  20. multissh3-6.35/tests/test_stopped_commands.py +287 -0
  21. {multissh3-6.33 → multissh3-6.35}/tests/test_stream_reader.py +7 -5
  22. multissh3-6.35/tests/test_streamed_output.py +228 -0
  23. multissh3-6.35/tests/test_waiting.py +244 -0
  24. multissh3-6.33/tests/test_stopped_commands.py +0 -123
  25. {multissh3-6.33 → multissh3-6.35}/LICENSE +0 -0
  26. {multissh3-6.33 → multissh3-6.35}/MANIFEST.in +0 -0
  27. {multissh3-6.33 → multissh3-6.35}/multiSSH3.egg-info/dependency_links.txt +0 -0
  28. {multissh3-6.33 → multissh3-6.35}/multiSSH3.egg-info/entry_points.txt +0 -0
  29. {multissh3-6.33 → multissh3-6.35}/multiSSH3.egg-info/top_level.txt +0 -0
  30. {multissh3-6.33 → multissh3-6.35}/pytest.ini +0 -0
  31. {multissh3-6.33 → multissh3-6.35}/setup.cfg +0 -0
  32. {multissh3-6.33 → multissh3-6.35}/setup.py +0 -0
  33. {multissh3-6.33 → multissh3-6.35}/tests/bench_complete.py +0 -0
  34. {multissh3-6.33 → multissh3-6.35}/tests/data/run_command_golden.json +0 -0
  35. {multissh3-6.33 → multissh3-6.35}/tests/shells/Dockerfile.bash5.1 +0 -0
  36. {multissh3-6.33 → multissh3-6.35}/tests/shells/Dockerfile.bash5.3 +0 -0
  37. {multissh3-6.33 → multissh3-6.35}/tests/shells/Dockerfile.centos7 +0 -0
  38. {multissh3-6.33 → multissh3-6.35}/tests/shells/Dockerfile.current +0 -0
  39. {multissh3-6.33 → multissh3-6.35}/tests/shells/Dockerfile.legacy +0 -0
  40. {multissh3-6.33 → multissh3-6.35}/tests/shells/Dockerfile.modern +0 -0
  41. {multissh3-6.33 → multissh3-6.35}/tests/shells/run_matrix.sh +0 -0
  42. {multissh3-6.33 → multissh3-6.35}/tests/test_cli_branches.py +0 -0
  43. {multissh3-6.33 → multissh3-6.35}/tests/test_cli_processing.py +0 -0
  44. {multissh3-6.33 → multissh3-6.35}/tests/test_closed_output.py +0 -0
  45. {multissh3-6.33 → multissh3-6.35}/tests/test_color_helpers.py +0 -0
  46. {multissh3-6.33 → multissh3-6.35}/tests/test_command_terminal.py +0 -0
  47. {multissh3-6.33 → multissh3-6.35}/tests/test_compact_hostnames.py +0 -0
  48. {multissh3-6.33 → multissh3-6.35}/tests/test_complete_blesh.py +0 -0
  49. {multissh3-6.33 → multissh3-6.35}/tests/test_complete_cli.py +0 -0
  50. {multissh3-6.33 → multissh3-6.35}/tests/test_complete_editor.py +0 -0
  51. {multissh3-6.33 → multissh3-6.35}/tests/test_complete_engine.py +0 -0
  52. {multissh3-6.33 → multissh3-6.35}/tests/test_complete_eval.py +0 -0
  53. {multissh3-6.33 → multissh3-6.35}/tests/test_complete_hosts.py +0 -0
  54. {multissh3-6.33 → multissh3-6.35}/tests/test_complete_hoststate.py +0 -0
  55. {multissh3-6.33 → multissh3-6.35}/tests/test_complete_index.py +0 -0
  56. {multissh3-6.33 → multissh3-6.35}/tests/test_complete_lex.py +0 -0
  57. {multissh3-6.33 → multissh3-6.35}/tests/test_complete_mssh_helpers.py +0 -0
  58. {multissh3-6.33 → multissh3-6.35}/tests/test_complete_password_spellings.py +0 -0
  59. {multissh3-6.33 → multissh3-6.35}/tests/test_complete_probe.py +0 -0
  60. {multissh3-6.33 → multissh3-6.35}/tests/test_complete_start.py +0 -0
  61. {multissh3-6.33 → multissh3-6.35}/tests/test_complete_store.py +0 -0
  62. {multissh3-6.33 → multissh3-6.35}/tests/test_complete_suggest.py +0 -0
  63. {multissh3-6.33 → multissh3-6.35}/tests/test_complete_terminal.py +0 -0
  64. {multissh3-6.33 → multissh3-6.35}/tests/test_complete_untrusted.py +0 -0
  65. {multissh3-6.33 → multissh3-6.35}/tests/test_complete_zsh_options.py +0 -0
  66. {multissh3-6.33 → multissh3-6.35}/tests/test_config_cli.py +0 -0
  67. {multissh3-6.33 → multissh3-6.35}/tests/test_config_validate.py +0 -0
  68. {multissh3-6.33 → multissh3-6.35}/tests/test_config_values.py +0 -0
  69. {multissh3-6.33 → multissh3-6.35}/tests/test_control_master_config.py +0 -0
  70. {multissh3-6.33 → multissh3-6.35}/tests/test_copy_id.py +0 -0
  71. {multissh3-6.33 → multissh3-6.35}/tests/test_curses_branches.py +0 -0
  72. {multissh3-6.33 → multissh3-6.35}/tests/test_curses_tui.py +0 -0
  73. {multissh3-6.33 → multissh3-6.35}/tests/test_curses_wide_chars.py +0 -0
  74. {multissh3-6.33 → multissh3-6.35}/tests/test_file_trust.py +0 -0
  75. {multissh3-6.33 → multissh3-6.35}/tests/test_harness_smoke.py +0 -0
  76. {multissh3-6.33 → multissh3-6.35}/tests/test_helpers_extra.py +0 -0
  77. {multissh3-6.33 → multissh3-6.35}/tests/test_history_replay.py +0 -0
  78. {multissh3-6.33 → multissh3-6.35}/tests/test_host.py +0 -0
  79. {multissh3-6.33 → multissh3-6.35}/tests/test_host_validity.py +0 -0
  80. {multissh3-6.33 → multissh3-6.35}/tests/test_hosts_file.py +0 -0
  81. {multissh3-6.33 → multissh3-6.35}/tests/test_identities_only.py +0 -0
  82. {multissh3-6.33 → multissh3-6.35}/tests/test_ipmi_lockout_wait.py +0 -0
  83. {multissh3-6.33 → multissh3-6.35}/tests/test_ipmi_logins.py +0 -0
  84. {multissh3-6.33 → multissh3-6.35}/tests/test_key_path_home.py +0 -0
  85. {multissh3-6.33 → multissh3-6.35}/tests/test_login_drop_retry.py +0 -0
  86. {multissh3-6.33 → multissh3-6.35}/tests/test_main_lifecycle.py +0 -0
  87. {multissh3-6.33 → multissh3-6.35}/tests/test_narrow_terminal.py +0 -0
  88. {multissh3-6.33 → multissh3-6.35}/tests/test_no_hosts.py +0 -0
  89. {multissh3-6.33 → multissh3-6.35}/tests/test_option_no_effect.py +0 -0
  90. {multissh3-6.33 → multissh3-6.35}/tests/test_output_merge.py +0 -0
  91. {multissh3-6.33 → multissh3-6.35}/tests/test_output_robustness.py +0 -0
  92. {multissh3-6.33 → multissh3-6.35}/tests/test_packaging.py +0 -0
  93. {multissh3-6.33 → multissh3-6.35}/tests/test_password_handling.py +0 -0
  94. {multissh3-6.33 → multissh3-6.35}/tests/test_passwords_out_of_ps.py +0 -0
  95. {multissh3-6.33 → multissh3-6.35}/tests/test_run_command.py +0 -0
  96. {multissh3-6.33 → multissh3-6.35}/tests/test_run_command_branches.py +0 -0
  97. {multissh3-6.33 → multissh3-6.35}/tests/test_run_command_characterization.py +0 -0
  98. {multissh3-6.33 → multissh3-6.35}/tests/test_run_command_exit.py +0 -0
  99. {multissh3-6.33 → multissh3-6.35}/tests/test_run_on_hosts.py +0 -0
  100. {multissh3-6.33 → multissh3-6.35}/tests/test_run_on_hosts_modes.py +0 -0
  101. {multissh3-6.33 → multissh3-6.35}/tests/test_runtime_dir.py +0 -0
  102. {multissh3-6.33 → multissh3-6.35}/tests/test_smoke_optional.py +0 -0
  103. {multissh3-6.33 → multissh3-6.35}/tests/test_ssh_error_text.py +0 -0
  104. {multissh3-6.33 → multissh3-6.35}/tests/test_ssh_timeouts.py +0 -0
  105. {multissh3-6.33 → multissh3-6.35}/tests/test_sshpass_exit.py +0 -0
  106. {multissh3-6.33 → multissh3-6.35}/tests/test_start_run_on_hosts.py +0 -0
  107. {multissh3-6.33 → multissh3-6.35}/tests/test_tui_ssh_notices.py +0 -0
  108. {multissh3-6.33 → multissh3-6.35}/tests/test_unavailable_hosts.py +0 -0
  109. {multissh3-6.33 → multissh3-6.35}/tests/test_underscore_settings.py +0 -0
  110. {multissh3-6.33 → multissh3-6.35}/tests/test_wide_wrap.py +0 -0
  111. {multissh3-6.33 → multissh3-6.35}/tests/vt_grid.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: multiSSH3
3
- Version: 6.33
3
+ Version: 6.35
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
@@ -336,6 +336,8 @@ When hostname need to be resolved, mssh will check in the following order:
336
336
 
337
337
  An alias's value is split into hosts as the hosts argument is (`,`, spaces, `;`, ...). An alias with an empty value (`eu=''`) stands for no hosts.
338
338
 
339
+ A member can name its user: with `dbs="postgres@db1,db2"`, mssh logs in to db1 as postgres, and `-u` changes only db2's user. A user typed with the alias replaces it: `mssh root@dbs` logs in to both as root, as a user on the command line wins over ssh config's `User`.
340
+
339
341
  Inside its own expansion, directly or through other aliases, an alias's name is not expanded again but taken as a host name, as bash does with aliases: `web="web,web-b"` runs on the host `web` and on `web-b`. When no host has that name, mssh reports the loop and runs on the rest: `Error: alias loop through 'web' (web -> app -> web): 'web' is not a valid hostname or IP address!`
340
342
 
341
343
 
@@ -601,10 +603,12 @@ mssh -q -- web1 grep -r TODO /srv
601
603
 
602
604
  ### `-ea, --extraargs EXTRAARGS` | _`DEFAULT_EXTRA_ARGS`_
603
605
  - Extra arguments to pass to the ssh / rsync / scp command. Put in one string for multiple arguments.
606
+ - The word after `-ea` is its value, also when it starts with `-`: `-ea '-p 2222'`, `-ea -A`. Quote an option together with its value: with `-ea -p 2222` the quotes were forgotten, and mssh stops, as it does for a password option there. ( mssh 6.34 and older need `-ea='-p 2222'`, which still works. )
604
607
  - The string is split into arguments as a shell would: quote an argument that contains spaces.
605
608
  - For a file copy with rsync, ssh's options that take a value go to the ssh that rsync runs, and the other arguments to rsync: the option followed by its value as the next word (`-o ConnectTimeout=5`, `-p 2222`, `-i FILE`, `-J HOST`, `-F FILE`, `-l USER`, ...), or `-oName=value` and `-pPORT`. scp takes ssh's options itself; ssh's `-p PORT` is given to it as `-P PORT`.
606
609
  - Example:
607
610
  ```bash
611
+ mssh -ea '-p 2222' allServers uptime
608
612
  mssh -ea="--delete" -f ./data/ allServers /tmp/data/
609
613
  mssh -ea="-o ProxyCommand='ssh -W %h:%p bastion'" allServers uptime
610
614
  ```
@@ -715,7 +719,7 @@ mssh -f ./data/ allServers /tmp/data/
715
719
 
716
720
  ### `-ia, --ipmi_args IPMI_ARGS` | _`DEFAULT_IPMI_ARGS`_
717
721
  - Extra arguments for ipmitool (or redfishtool) in `--ipmi` mode, in one string, placed as they are before the command. `-ea` arguments only go to ssh / scp / rsync.
718
- - Ex: `mssh -M -ia='-I lanplus' bmc[1-4] 'power status'` runs `ipmitool -H <address> -U <user> -E -I lanplus power status`, with the password in its environment. Use `=`, as the value starts with `-`.
722
+ - Ex: `mssh -M -ia '-I lanplus' bmc[1-4] 'power status'` runs `ipmitool -H <address> -U <user> -E -I lanplus power status`, with the password in its environment. The word after `-ia` is its value, also when it starts with `-`; quote an option together with its value ( mssh 6.34 and older need `-ia='-I lanplus'` ).
719
723
 
720
724
  ### `-im, --ipmi_method IPMI_METHOD` | _`DEFAULT_IPMI_METHOD`_
721
725
  - How `--ipmi` reaches a host:
@@ -970,7 +974,7 @@ Option names count in full only: `--user` is not `--username` and `-c` is not `-
970
974
 
971
975
  Putting the command after `--` is no substitute for quotes: every word after the hosts is a command of its own, so `mssh web1 -- bash -c 'echo hi'` runs `bash`, `-c` and `echo hi` one after another (when each word is a single word, as in `mssh web1 -- ls -la`, mssh asks whether to join them).
972
976
 
973
- Below is a sample help message output from multiSSH3 6.33 @ 2026-10-14 (no config file, 120 columns; the lines after the options list the programs found on that machine)
977
+ Below is a sample help message output from multiSSH3 6.35 @ 2026-10-16 (no config file, 120 columns; the lines after the options list the programs found on that machine)
974
978
 
975
979
  ```bash
976
980
  $ mssh -h
@@ -1001,7 +1005,7 @@ options:
1001
1005
  The identity file to use to connect to the hosts. Implies --use_key. Specify a folder for program to search for a key. Use option without value to use ~/.ssh/ (default: None)
1002
1006
  -uk, --use_key Attempt to use public key file to connect to the hosts. (default: False)
1003
1007
  -ea, --extraargs EXTRAARGS
1004
- Extra arguments to pass to the ssh / rsync / scp command. (Use -ia for ipmi arguments) Put in one string for multiple arguments.Use "=" ! Ex. -ea="--delete" (default: None)
1008
+ Extra arguments to pass to the ssh / rsync / scp command. (Use -ia for ipmi arguments) Put in one string for multiple arguments. The word after -ea is its value, also when it starts with "-": quote an option with its value. Ex. -ea "-p 2222 -A" (default: None)
1005
1009
  -11, --oneonone Run one corresponding command on each host. (default: False)
1006
1010
  -f, --file FILE The file to be copied to the hosts. Use -f multiple times to copy multiple files
1007
1011
  -s, -fs, --file_sync [FILE_SYNC]
@@ -1026,7 +1030,7 @@ options:
1026
1030
  -ip, --ipmi_password IPMI_PASSWORD
1027
1031
  The password to use to connect to the hosts via ipmi. (default: set, not shown)
1028
1032
  -ia, --ipmi_args IPMI_ARGS
1029
- The arguments passed to ipmitool to use to connect to the hosts via ipmi. (default: )
1033
+ The arguments passed to ipmitool to use to connect to the hosts via ipmi. The word after -ia is its value, also when it starts with "-": quote an option with its value. Ex. -ia "-I lanplus" (default: )
1030
1034
  -im, --ipmi_method IPMI_METHOD
1031
1035
  The method to use to connect to the hosts via ipmi. (default: ipmitool)
1032
1036
  -irlt, --ipmi_retry_lockout_time IPMI_RETRY_LOCKOUT_TIME
@@ -1,29 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: multiSSH3
3
- Version: 6.33
4
- Summary: Run commands on multiple hosts via SSH
5
- Home-page: https://github.com/yufei-pan/multiSSH3
6
- Author: Yufei Pan
7
- Author-email: pan@zopyr.us
8
- License-Expression: GPL-3.0-or-later
9
- Classifier: Programming Language :: Python :: 3
10
- Classifier: Operating System :: POSIX :: Linux
11
- Classifier: Operating System :: MacOS
12
- Classifier: Operating System :: Microsoft :: Windows
13
- Requires-Python: >=3.6
14
- Description-Content-Type: text/markdown
15
- License-File: LICENSE
16
- Dynamic: author
17
- Dynamic: author-email
18
- Dynamic: classifier
19
- Dynamic: description
20
- Dynamic: description-content-type
21
- Dynamic: home-page
22
- Dynamic: license-file
23
- Dynamic: license-expression
24
- Dynamic: requires-python
25
- Dynamic: summary
26
-
27
1
  # multiSSH3
28
2
 
29
3
  ## Introduction
@@ -336,6 +310,8 @@ When hostname need to be resolved, mssh will check in the following order:
336
310
 
337
311
  An alias's value is split into hosts as the hosts argument is (`,`, spaces, `;`, ...). An alias with an empty value (`eu=''`) stands for no hosts.
338
312
 
313
+ A member can name its user: with `dbs="postgres@db1,db2"`, mssh logs in to db1 as postgres, and `-u` changes only db2's user. A user typed with the alias replaces it: `mssh root@dbs` logs in to both as root, as a user on the command line wins over ssh config's `User`.
314
+
339
315
  Inside its own expansion, directly or through other aliases, an alias's name is not expanded again but taken as a host name, as bash does with aliases: `web="web,web-b"` runs on the host `web` and on `web-b`. When no host has that name, mssh reports the loop and runs on the rest: `Error: alias loop through 'web' (web -> app -> web): 'web' is not a valid hostname or IP address!`
340
316
 
341
317
 
@@ -601,10 +577,12 @@ mssh -q -- web1 grep -r TODO /srv
601
577
 
602
578
  ### `-ea, --extraargs EXTRAARGS` | _`DEFAULT_EXTRA_ARGS`_
603
579
  - Extra arguments to pass to the ssh / rsync / scp command. Put in one string for multiple arguments.
580
+ - The word after `-ea` is its value, also when it starts with `-`: `-ea '-p 2222'`, `-ea -A`. Quote an option together with its value: with `-ea -p 2222` the quotes were forgotten, and mssh stops, as it does for a password option there. ( mssh 6.34 and older need `-ea='-p 2222'`, which still works. )
604
581
  - The string is split into arguments as a shell would: quote an argument that contains spaces.
605
582
  - For a file copy with rsync, ssh's options that take a value go to the ssh that rsync runs, and the other arguments to rsync: the option followed by its value as the next word (`-o ConnectTimeout=5`, `-p 2222`, `-i FILE`, `-J HOST`, `-F FILE`, `-l USER`, ...), or `-oName=value` and `-pPORT`. scp takes ssh's options itself; ssh's `-p PORT` is given to it as `-P PORT`.
606
583
  - Example:
607
584
  ```bash
585
+ mssh -ea '-p 2222' allServers uptime
608
586
  mssh -ea="--delete" -f ./data/ allServers /tmp/data/
609
587
  mssh -ea="-o ProxyCommand='ssh -W %h:%p bastion'" allServers uptime
610
588
  ```
@@ -715,7 +693,7 @@ mssh -f ./data/ allServers /tmp/data/
715
693
 
716
694
  ### `-ia, --ipmi_args IPMI_ARGS` | _`DEFAULT_IPMI_ARGS`_
717
695
  - Extra arguments for ipmitool (or redfishtool) in `--ipmi` mode, in one string, placed as they are before the command. `-ea` arguments only go to ssh / scp / rsync.
718
- - Ex: `mssh -M -ia='-I lanplus' bmc[1-4] 'power status'` runs `ipmitool -H <address> -U <user> -E -I lanplus power status`, with the password in its environment. Use `=`, as the value starts with `-`.
696
+ - Ex: `mssh -M -ia '-I lanplus' bmc[1-4] 'power status'` runs `ipmitool -H <address> -U <user> -E -I lanplus power status`, with the password in its environment. The word after `-ia` is its value, also when it starts with `-`; quote an option together with its value ( mssh 6.34 and older need `-ia='-I lanplus'` ).
719
697
 
720
698
  ### `-im, --ipmi_method IPMI_METHOD` | _`DEFAULT_IPMI_METHOD`_
721
699
  - How `--ipmi` reaches a host:
@@ -970,7 +948,7 @@ Option names count in full only: `--user` is not `--username` and `-c` is not `-
970
948
 
971
949
  Putting the command after `--` is no substitute for quotes: every word after the hosts is a command of its own, so `mssh web1 -- bash -c 'echo hi'` runs `bash`, `-c` and `echo hi` one after another (when each word is a single word, as in `mssh web1 -- ls -la`, mssh asks whether to join them).
972
950
 
973
- Below is a sample help message output from multiSSH3 6.33 @ 2026-10-14 (no config file, 120 columns; the lines after the options list the programs found on that machine)
951
+ Below is a sample help message output from multiSSH3 6.35 @ 2026-10-16 (no config file, 120 columns; the lines after the options list the programs found on that machine)
974
952
 
975
953
  ```bash
976
954
  $ mssh -h
@@ -1001,7 +979,7 @@ options:
1001
979
  The identity file to use to connect to the hosts. Implies --use_key. Specify a folder for program to search for a key. Use option without value to use ~/.ssh/ (default: None)
1002
980
  -uk, --use_key Attempt to use public key file to connect to the hosts. (default: False)
1003
981
  -ea, --extraargs EXTRAARGS
1004
- Extra arguments to pass to the ssh / rsync / scp command. (Use -ia for ipmi arguments) Put in one string for multiple arguments.Use "=" ! Ex. -ea="--delete" (default: None)
982
+ Extra arguments to pass to the ssh / rsync / scp command. (Use -ia for ipmi arguments) Put in one string for multiple arguments. The word after -ea is its value, also when it starts with "-": quote an option with its value. Ex. -ea "-p 2222 -A" (default: None)
1005
983
  -11, --oneonone Run one corresponding command on each host. (default: False)
1006
984
  -f, --file FILE The file to be copied to the hosts. Use -f multiple times to copy multiple files
1007
985
  -s, -fs, --file_sync [FILE_SYNC]
@@ -1026,7 +1004,7 @@ options:
1026
1004
  -ip, --ipmi_password IPMI_PASSWORD
1027
1005
  The password to use to connect to the hosts via ipmi. (default: set, not shown)
1028
1006
  -ia, --ipmi_args IPMI_ARGS
1029
- The arguments passed to ipmitool to use to connect to the hosts via ipmi. (default: )
1007
+ The arguments passed to ipmitool to use to connect to the hosts via ipmi. The word after -ia is its value, also when it starts with "-": quote an option with its value. Ex. -ia "-I lanplus" (default: )
1030
1008
  -im, --ipmi_method IPMI_METHOD
1031
1009
  The method to use to connect to the hosts via ipmi. (default: ipmitool)
1032
1010
  -irlt, --ipmi_retry_lockout_time IPMI_RETRY_LOCKOUT_TIME
@@ -16,7 +16,7 @@ import sys
16
16
  import time
17
17
  from bisect import bisect_left
18
18
 
19
- version = '0.6'
19
+ version = '0.8'
20
20
  __version__ = version
21
21
  PROTOCOL = 'MSSHC1'
22
22
  MSSH_NAMES = ('mssh', 'mssh3', 'multissh', 'multissh3', 'multiSSH3')
@@ -166,6 +166,31 @@ def split_at_cursor(before, after):
166
166
 
167
167
  #%% ------------ Option spec ----------------
168
168
  PASSWORD_DESTS = frozenset(('password', 'ipmi_password'))
169
+ # options passing options on to another program (-ea, -ia): mssh 6.35+ takes the word after them as
170
+ # their value even when it starts with '-' (its _with_passed_on_values), except as takes_passed_on_value says
171
+ PASSED_ON_DESTS = frozenset(('extraargs', 'ipmi_args'))
172
+ # the letters of the options needing a value of their own, by where they pass them: ssh, ipmitool
173
+ PASSED_ON_BARE_VALUE_LETTERS = {
174
+ 'extraargs': frozenset('BbcDEeFIiJLlmOoPpQRSWw'),
175
+ 'ipmi_args': frozenset('AbBCdefHIklLmNoOpPRtTUyz'),
176
+ }
177
+
178
+ def takes_passed_on_value(dest, text, spec):
179
+ '''
180
+ Whether -ea or -ia (dest) takes text, the word after it, as its value, as mssh's _takes_passed_on_value
181
+ reads it: not a bare option needing a value of its own (-ea -p 2222: the quotes were forgotten), nor a
182
+ word giving mssh's password; mssh stops at those.
183
+ '''
184
+ if not text.startswith('-') or ' ' in text:
185
+ return True
186
+ if len(text) == 2 and text[1] in PASSED_ON_BARE_VALUE_LETTERS[dest]:
187
+ return False
188
+ if text.startswith('-ip'):
189
+ return False
190
+ if text[:2] == '-p' and text[2:].isdigit():
191
+ return True
192
+ option = spec.lookup(text)[0]
193
+ return option is None or option.dest not in PASSWORD_DESTS
169
194
  # options naming files mssh reads its hosts and env aliases from, at Enter
170
195
  LOOKUP_DESTS = frozenset(('env_file', 'env_files', 'host_file', 'config_file'))
171
196
  PATH_DESTS = frozenset(('file', 'file_sync', 'env_file', 'env_files', 'config_file', 'store_config_file', 'host_file', 'history_file'))
@@ -506,9 +531,12 @@ def assign_roles(words, spec, current=None):
506
531
  pending = None
507
532
  # the word after a password option is its password, whatever it looks like: argparse stops
508
533
  # at "-p -secret", but the shell's history keeps the line, and a password is never another word.
509
- # argparse takes a dash word as the value when it holds a space and names no option
510
- # (-ea "-o StrictHostKeyChecking=no"); mssh's -11 makes it read negative numbers as options
511
- if not dashed or option.dest in PASSWORD_DESTS or (' ' in text and not spec.names_option(text)):
534
+ # mssh takes the word after -ea and -ia as their value (PASSED_ON_DESTS). argparse takes a dash
535
+ # word as the value when it holds a space and names no option (-u "-o x"); mssh's -11 makes it
536
+ # read negative numbers as options
537
+ # (a word being typed in an open quote will hold more: "-I lanplus")
538
+ passed_on = option.dest in PASSED_ON_DESTS and (takes_passed_on_value(option.dest, text, spec) or (is_current and token.open_quote))
539
+ if not dashed or option.dest in PASSWORD_DESTS or passed_on or (' ' in text and not spec.names_option(text)):
512
540
  roles.append((ROLE_VALUE, option))
513
541
  continue
514
542
  if text == '--' and not is_current and not after_separator:
@@ -2577,12 +2605,9 @@ def _run_name(host, typed_user, line_user, own_user=''):
2577
2605
  The name mssh gives a host in this run: what it records a failure under in unavailable_hosts.csv
2578
2606
  and looks up to skip the host. The user typed with the host, else -u (line_user), else none.
2579
2607
  own_user: the user an alias member is written with. -u does not replace it, a user typed with the
2580
- alias goes in front of it (for root@dbs with dbs=postgres@db1 mssh runs root@postgres@db1).
2608
+ alias does (for root@dbs with dbs=postgres@db1 mssh runs root@db1).
2581
2609
  '''
2582
- if own_user:
2583
- host = own_user + '@' + host
2584
- line_user = ''
2585
- user = typed_user or line_user
2610
+ user = typed_user or own_user or line_user
2586
2611
  return user + '@' + host if user else host
2587
2612
 
2588
2613
  # characters that can start an expansion the shell makes before mssh sees the word
@@ -3619,10 +3644,16 @@ class Engine(object):
3619
3644
  have_hosts = False
3620
3645
  run = None # where the unquoted remote command starts in argv
3621
3646
  pending = None # nargs of an option still taking the words that follow as values
3647
+ passed_on = None # -ea's or -ia's dest: the word may be its value even when it starts with '-'
3622
3648
  paths = False
3623
3649
  read = []
3624
3650
  stop = len(argv)
3625
3651
  for index, token in enumerate(argv):
3652
+ if passed_on:
3653
+ dest, passed_on = passed_on, None
3654
+ if takes_passed_on_value(dest, token.text, self.spec):
3655
+ pending = None
3656
+ continue
3626
3657
  if token.text == '--':
3627
3658
  stop = index
3628
3659
  break
@@ -3645,6 +3676,7 @@ class Engine(object):
3645
3676
  option, attached, _ = readings[-1]
3646
3677
  if attached is None and option.nargs != '0':
3647
3678
  pending = option.nargs
3679
+ passed_on = option.dest if option.dest in PASSED_ON_DESTS else None
3648
3680
  if run is None and any(option.dest in ('file', 'file_sync', 'gather_mode') for option, _, _ in readings):
3649
3681
  paths = True
3650
3682
  if run is not None:
@@ -1,3 +1,29 @@
1
+ Metadata-Version: 2.4
2
+ Name: multiSSH3
3
+ Version: 6.35
4
+ Summary: Run commands on multiple hosts via SSH
5
+ Home-page: https://github.com/yufei-pan/multiSSH3
6
+ Author: Yufei Pan
7
+ Author-email: pan@zopyr.us
8
+ License-Expression: GPL-3.0-or-later
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Operating System :: POSIX :: Linux
11
+ Classifier: Operating System :: MacOS
12
+ Classifier: Operating System :: Microsoft :: Windows
13
+ Requires-Python: >=3.6
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Dynamic: author
17
+ Dynamic: author-email
18
+ Dynamic: classifier
19
+ Dynamic: description
20
+ Dynamic: description-content-type
21
+ Dynamic: home-page
22
+ Dynamic: license-file
23
+ Dynamic: license-expression
24
+ Dynamic: requires-python
25
+ Dynamic: summary
26
+
1
27
  # multiSSH3
2
28
 
3
29
  ## Introduction
@@ -310,6 +336,8 @@ When hostname need to be resolved, mssh will check in the following order:
310
336
 
311
337
  An alias's value is split into hosts as the hosts argument is (`,`, spaces, `;`, ...). An alias with an empty value (`eu=''`) stands for no hosts.
312
338
 
339
+ A member can name its user: with `dbs="postgres@db1,db2"`, mssh logs in to db1 as postgres, and `-u` changes only db2's user. A user typed with the alias replaces it: `mssh root@dbs` logs in to both as root, as a user on the command line wins over ssh config's `User`.
340
+
313
341
  Inside its own expansion, directly or through other aliases, an alias's name is not expanded again but taken as a host name, as bash does with aliases: `web="web,web-b"` runs on the host `web` and on `web-b`. When no host has that name, mssh reports the loop and runs on the rest: `Error: alias loop through 'web' (web -> app -> web): 'web' is not a valid hostname or IP address!`
314
342
 
315
343
 
@@ -575,10 +603,12 @@ mssh -q -- web1 grep -r TODO /srv
575
603
 
576
604
  ### `-ea, --extraargs EXTRAARGS` | _`DEFAULT_EXTRA_ARGS`_
577
605
  - Extra arguments to pass to the ssh / rsync / scp command. Put in one string for multiple arguments.
606
+ - The word after `-ea` is its value, also when it starts with `-`: `-ea '-p 2222'`, `-ea -A`. Quote an option together with its value: with `-ea -p 2222` the quotes were forgotten, and mssh stops, as it does for a password option there. ( mssh 6.34 and older need `-ea='-p 2222'`, which still works. )
578
607
  - The string is split into arguments as a shell would: quote an argument that contains spaces.
579
608
  - For a file copy with rsync, ssh's options that take a value go to the ssh that rsync runs, and the other arguments to rsync: the option followed by its value as the next word (`-o ConnectTimeout=5`, `-p 2222`, `-i FILE`, `-J HOST`, `-F FILE`, `-l USER`, ...), or `-oName=value` and `-pPORT`. scp takes ssh's options itself; ssh's `-p PORT` is given to it as `-P PORT`.
580
609
  - Example:
581
610
  ```bash
611
+ mssh -ea '-p 2222' allServers uptime
582
612
  mssh -ea="--delete" -f ./data/ allServers /tmp/data/
583
613
  mssh -ea="-o ProxyCommand='ssh -W %h:%p bastion'" allServers uptime
584
614
  ```
@@ -689,7 +719,7 @@ mssh -f ./data/ allServers /tmp/data/
689
719
 
690
720
  ### `-ia, --ipmi_args IPMI_ARGS` | _`DEFAULT_IPMI_ARGS`_
691
721
  - Extra arguments for ipmitool (or redfishtool) in `--ipmi` mode, in one string, placed as they are before the command. `-ea` arguments only go to ssh / scp / rsync.
692
- - Ex: `mssh -M -ia='-I lanplus' bmc[1-4] 'power status'` runs `ipmitool -H <address> -U <user> -E -I lanplus power status`, with the password in its environment. Use `=`, as the value starts with `-`.
722
+ - Ex: `mssh -M -ia '-I lanplus' bmc[1-4] 'power status'` runs `ipmitool -H <address> -U <user> -E -I lanplus power status`, with the password in its environment. The word after `-ia` is its value, also when it starts with `-`; quote an option together with its value ( mssh 6.34 and older need `-ia='-I lanplus'` ).
693
723
 
694
724
  ### `-im, --ipmi_method IPMI_METHOD` | _`DEFAULT_IPMI_METHOD`_
695
725
  - How `--ipmi` reaches a host:
@@ -944,7 +974,7 @@ Option names count in full only: `--user` is not `--username` and `-c` is not `-
944
974
 
945
975
  Putting the command after `--` is no substitute for quotes: every word after the hosts is a command of its own, so `mssh web1 -- bash -c 'echo hi'` runs `bash`, `-c` and `echo hi` one after another (when each word is a single word, as in `mssh web1 -- ls -la`, mssh asks whether to join them).
946
976
 
947
- Below is a sample help message output from multiSSH3 6.33 @ 2026-10-14 (no config file, 120 columns; the lines after the options list the programs found on that machine)
977
+ Below is a sample help message output from multiSSH3 6.35 @ 2026-10-16 (no config file, 120 columns; the lines after the options list the programs found on that machine)
948
978
 
949
979
  ```bash
950
980
  $ mssh -h
@@ -975,7 +1005,7 @@ options:
975
1005
  The identity file to use to connect to the hosts. Implies --use_key. Specify a folder for program to search for a key. Use option without value to use ~/.ssh/ (default: None)
976
1006
  -uk, --use_key Attempt to use public key file to connect to the hosts. (default: False)
977
1007
  -ea, --extraargs EXTRAARGS
978
- Extra arguments to pass to the ssh / rsync / scp command. (Use -ia for ipmi arguments) Put in one string for multiple arguments.Use "=" ! Ex. -ea="--delete" (default: None)
1008
+ Extra arguments to pass to the ssh / rsync / scp command. (Use -ia for ipmi arguments) Put in one string for multiple arguments. The word after -ea is its value, also when it starts with "-": quote an option with its value. Ex. -ea "-p 2222 -A" (default: None)
979
1009
  -11, --oneonone Run one corresponding command on each host. (default: False)
980
1010
  -f, --file FILE The file to be copied to the hosts. Use -f multiple times to copy multiple files
981
1011
  -s, -fs, --file_sync [FILE_SYNC]
@@ -1000,7 +1030,7 @@ options:
1000
1030
  -ip, --ipmi_password IPMI_PASSWORD
1001
1031
  The password to use to connect to the hosts via ipmi. (default: set, not shown)
1002
1032
  -ia, --ipmi_args IPMI_ARGS
1003
- The arguments passed to ipmitool to use to connect to the hosts via ipmi. (default: )
1033
+ The arguments passed to ipmitool to use to connect to the hosts via ipmi. The word after -ia is its value, also when it starts with "-": quote an option with its value. Ex. -ia "-I lanplus" (default: )
1004
1034
  -im, --ipmi_method IPMI_METHOD
1005
1035
  The method to use to connect to the hosts via ipmi. (default: ipmitool)
1006
1036
  -irlt, --ipmi_retry_lockout_time IPMI_RETRY_LOCKOUT_TIME
@@ -83,6 +83,7 @@ tests/test_run_on_hosts.py
83
83
  tests/test_run_on_hosts_modes.py
84
84
  tests/test_runtime_branch_matrix.py
85
85
  tests/test_runtime_dir.py
86
+ tests/test_shared_lines.py
86
87
  tests/test_smoke_optional.py
87
88
  tests/test_ssh_error_text.py
88
89
  tests/test_ssh_timeouts.py
@@ -90,9 +91,11 @@ tests/test_sshpass_exit.py
90
91
  tests/test_start_run_on_hosts.py
91
92
  tests/test_stopped_commands.py
92
93
  tests/test_stream_reader.py
94
+ tests/test_streamed_output.py
93
95
  tests/test_tui_ssh_notices.py
94
96
  tests/test_unavailable_hosts.py
95
97
  tests/test_underscore_settings.py
98
+ tests/test_waiting.py
96
99
  tests/test_wide_wrap.py
97
100
  tests/vt_grid.py
98
101
  tests/data/run_command_golden.json