multiSSH3 6.34__tar.gz → 6.36__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 (110) hide show
  1. {multissh3-6.34/multiSSH3.egg-info → multissh3-6.36}/PKG-INFO +8 -6
  2. {multissh3-6.34 → multissh3-6.36}/README.md +7 -5
  3. {multissh3-6.34 → multissh3-6.36}/mssh_complete.py +61 -7
  4. {multissh3-6.34 → multissh3-6.36/multiSSH3.egg-info}/PKG-INFO +8 -6
  5. {multissh3-6.34 → multissh3-6.36}/multiSSH3.egg-info/SOURCES.txt +3 -0
  6. {multissh3-6.34 → multissh3-6.36}/multiSSH3.py +444 -120
  7. {multissh3-6.34 → multissh3-6.36}/tests/conftest.py +19 -0
  8. {multissh3-6.34 → multissh3-6.36}/tests/test_cli_option_words.py +102 -0
  9. {multissh3-6.34 → multissh3-6.36}/tests/test_complete_context.py +70 -3
  10. {multissh3-6.34 → multissh3-6.36}/tests/test_complete_diagnose.py +20 -0
  11. {multissh3-6.34 → multissh3-6.36}/tests/test_complete_picker.py +4 -2
  12. {multissh3-6.34 → multissh3-6.36}/tests/test_curses_event_loop.py +228 -0
  13. {multissh3-6.34 → multissh3-6.36}/tests/test_harness_smoke.py +13 -0
  14. {multissh3-6.34 → multissh3-6.36}/tests/test_options_after_command.py +14 -0
  15. {multissh3-6.34 → multissh3-6.36}/tests/test_runtime_branch_matrix.py +1 -1
  16. {multissh3-6.34 → multissh3-6.36}/tests/test_runtime_dir.py +28 -0
  17. multissh3-6.36/tests/test_shared_lines.py +219 -0
  18. {multissh3-6.34 → multissh3-6.36}/tests/test_stream_reader.py +7 -5
  19. multissh3-6.36/tests/test_streamed_output.py +228 -0
  20. multissh3-6.36/tests/test_waiting.py +244 -0
  21. {multissh3-6.34 → multissh3-6.36}/LICENSE +0 -0
  22. {multissh3-6.34 → multissh3-6.36}/MANIFEST.in +0 -0
  23. {multissh3-6.34 → multissh3-6.36}/multiSSH3.egg-info/dependency_links.txt +0 -0
  24. {multissh3-6.34 → multissh3-6.36}/multiSSH3.egg-info/entry_points.txt +0 -0
  25. {multissh3-6.34 → multissh3-6.36}/multiSSH3.egg-info/top_level.txt +0 -0
  26. {multissh3-6.34 → multissh3-6.36}/pytest.ini +0 -0
  27. {multissh3-6.34 → multissh3-6.36}/setup.cfg +0 -0
  28. {multissh3-6.34 → multissh3-6.36}/setup.py +0 -0
  29. {multissh3-6.34 → multissh3-6.36}/tests/bench_complete.py +0 -0
  30. {multissh3-6.34 → multissh3-6.36}/tests/complete_shell_harness.py +0 -0
  31. {multissh3-6.34 → multissh3-6.36}/tests/data/run_command_golden.json +0 -0
  32. {multissh3-6.34 → multissh3-6.36}/tests/shells/Dockerfile.bash5.1 +0 -0
  33. {multissh3-6.34 → multissh3-6.36}/tests/shells/Dockerfile.bash5.3 +0 -0
  34. {multissh3-6.34 → multissh3-6.36}/tests/shells/Dockerfile.centos7 +0 -0
  35. {multissh3-6.34 → multissh3-6.36}/tests/shells/Dockerfile.current +0 -0
  36. {multissh3-6.34 → multissh3-6.36}/tests/shells/Dockerfile.legacy +0 -0
  37. {multissh3-6.34 → multissh3-6.36}/tests/shells/Dockerfile.modern +0 -0
  38. {multissh3-6.34 → multissh3-6.36}/tests/shells/run_matrix.sh +0 -0
  39. {multissh3-6.34 → multissh3-6.36}/tests/test_cli_branches.py +0 -0
  40. {multissh3-6.34 → multissh3-6.36}/tests/test_cli_processing.py +0 -0
  41. {multissh3-6.34 → multissh3-6.36}/tests/test_closed_output.py +0 -0
  42. {multissh3-6.34 → multissh3-6.36}/tests/test_color_helpers.py +0 -0
  43. {multissh3-6.34 → multissh3-6.36}/tests/test_command_terminal.py +0 -0
  44. {multissh3-6.34 → multissh3-6.36}/tests/test_compact_hostnames.py +0 -0
  45. {multissh3-6.34 → multissh3-6.36}/tests/test_complete_blesh.py +0 -0
  46. {multissh3-6.34 → multissh3-6.36}/tests/test_complete_cli.py +0 -0
  47. {multissh3-6.34 → multissh3-6.36}/tests/test_complete_editor.py +0 -0
  48. {multissh3-6.34 → multissh3-6.36}/tests/test_complete_engine.py +0 -0
  49. {multissh3-6.34 → multissh3-6.36}/tests/test_complete_eval.py +0 -0
  50. {multissh3-6.34 → multissh3-6.36}/tests/test_complete_hosts.py +0 -0
  51. {multissh3-6.34 → multissh3-6.36}/tests/test_complete_hoststate.py +0 -0
  52. {multissh3-6.34 → multissh3-6.36}/tests/test_complete_index.py +0 -0
  53. {multissh3-6.34 → multissh3-6.36}/tests/test_complete_keys.py +0 -0
  54. {multissh3-6.34 → multissh3-6.36}/tests/test_complete_lex.py +0 -0
  55. {multissh3-6.34 → multissh3-6.36}/tests/test_complete_mssh_helpers.py +0 -0
  56. {multissh3-6.34 → multissh3-6.36}/tests/test_complete_password_spellings.py +0 -0
  57. {multissh3-6.34 → multissh3-6.36}/tests/test_complete_probe.py +0 -0
  58. {multissh3-6.34 → multissh3-6.36}/tests/test_complete_shells.py +0 -0
  59. {multissh3-6.34 → multissh3-6.36}/tests/test_complete_start.py +0 -0
  60. {multissh3-6.34 → multissh3-6.36}/tests/test_complete_store.py +0 -0
  61. {multissh3-6.34 → multissh3-6.36}/tests/test_complete_suggest.py +0 -0
  62. {multissh3-6.34 → multissh3-6.36}/tests/test_complete_terminal.py +0 -0
  63. {multissh3-6.34 → multissh3-6.36}/tests/test_complete_untrusted.py +0 -0
  64. {multissh3-6.34 → multissh3-6.36}/tests/test_complete_zsh_options.py +0 -0
  65. {multissh3-6.34 → multissh3-6.36}/tests/test_config_cli.py +0 -0
  66. {multissh3-6.34 → multissh3-6.36}/tests/test_config_validate.py +0 -0
  67. {multissh3-6.34 → multissh3-6.36}/tests/test_config_values.py +0 -0
  68. {multissh3-6.34 → multissh3-6.36}/tests/test_control_master_config.py +0 -0
  69. {multissh3-6.34 → multissh3-6.36}/tests/test_copy_id.py +0 -0
  70. {multissh3-6.34 → multissh3-6.36}/tests/test_curses_branches.py +0 -0
  71. {multissh3-6.34 → multissh3-6.36}/tests/test_curses_tui.py +0 -0
  72. {multissh3-6.34 → multissh3-6.36}/tests/test_curses_wide_chars.py +0 -0
  73. {multissh3-6.34 → multissh3-6.36}/tests/test_expand_hostnames.py +0 -0
  74. {multissh3-6.34 → multissh3-6.36}/tests/test_file_trust.py +0 -0
  75. {multissh3-6.34 → multissh3-6.36}/tests/test_helpers_extra.py +0 -0
  76. {multissh3-6.34 → multissh3-6.36}/tests/test_history_replay.py +0 -0
  77. {multissh3-6.34 → multissh3-6.36}/tests/test_host.py +0 -0
  78. {multissh3-6.34 → multissh3-6.36}/tests/test_host_validity.py +0 -0
  79. {multissh3-6.34 → multissh3-6.36}/tests/test_hosts_file.py +0 -0
  80. {multissh3-6.34 → multissh3-6.36}/tests/test_identities_only.py +0 -0
  81. {multissh3-6.34 → multissh3-6.36}/tests/test_ipmi_lockout_wait.py +0 -0
  82. {multissh3-6.34 → multissh3-6.36}/tests/test_ipmi_logins.py +0 -0
  83. {multissh3-6.34 → multissh3-6.36}/tests/test_key_path_home.py +0 -0
  84. {multissh3-6.34 → multissh3-6.36}/tests/test_login_drop_retry.py +0 -0
  85. {multissh3-6.34 → multissh3-6.36}/tests/test_main_lifecycle.py +0 -0
  86. {multissh3-6.34 → multissh3-6.36}/tests/test_narrow_terminal.py +0 -0
  87. {multissh3-6.34 → multissh3-6.36}/tests/test_no_hosts.py +0 -0
  88. {multissh3-6.34 → multissh3-6.36}/tests/test_option_no_effect.py +0 -0
  89. {multissh3-6.34 → multissh3-6.36}/tests/test_output_merge.py +0 -0
  90. {multissh3-6.34 → multissh3-6.36}/tests/test_output_robustness.py +0 -0
  91. {multissh3-6.34 → multissh3-6.36}/tests/test_packaging.py +0 -0
  92. {multissh3-6.34 → multissh3-6.36}/tests/test_password_handling.py +0 -0
  93. {multissh3-6.34 → multissh3-6.36}/tests/test_passwords_out_of_ps.py +0 -0
  94. {multissh3-6.34 → multissh3-6.36}/tests/test_run_command.py +0 -0
  95. {multissh3-6.34 → multissh3-6.36}/tests/test_run_command_branches.py +0 -0
  96. {multissh3-6.34 → multissh3-6.36}/tests/test_run_command_characterization.py +0 -0
  97. {multissh3-6.34 → multissh3-6.36}/tests/test_run_command_exit.py +0 -0
  98. {multissh3-6.34 → multissh3-6.36}/tests/test_run_on_hosts.py +0 -0
  99. {multissh3-6.34 → multissh3-6.36}/tests/test_run_on_hosts_modes.py +0 -0
  100. {multissh3-6.34 → multissh3-6.36}/tests/test_smoke_optional.py +0 -0
  101. {multissh3-6.34 → multissh3-6.36}/tests/test_ssh_error_text.py +0 -0
  102. {multissh3-6.34 → multissh3-6.36}/tests/test_ssh_timeouts.py +0 -0
  103. {multissh3-6.34 → multissh3-6.36}/tests/test_sshpass_exit.py +0 -0
  104. {multissh3-6.34 → multissh3-6.36}/tests/test_start_run_on_hosts.py +0 -0
  105. {multissh3-6.34 → multissh3-6.36}/tests/test_stopped_commands.py +0 -0
  106. {multissh3-6.34 → multissh3-6.36}/tests/test_tui_ssh_notices.py +0 -0
  107. {multissh3-6.34 → multissh3-6.36}/tests/test_unavailable_hosts.py +0 -0
  108. {multissh3-6.34 → multissh3-6.36}/tests/test_underscore_settings.py +0 -0
  109. {multissh3-6.34 → multissh3-6.36}/tests/test_wide_wrap.py +0 -0
  110. {multissh3-6.34 → multissh3-6.36}/tests/vt_grid.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: multiSSH3
3
- Version: 6.34
3
+ Version: 6.36
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
@@ -603,10 +603,12 @@ mssh -q -- web1 grep -r TODO /srv
603
603
 
604
604
  ### `-ea, --extraargs EXTRAARGS` | _`DEFAULT_EXTRA_ARGS`_
605
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. )
606
607
  - The string is split into arguments as a shell would: quote an argument that contains spaces.
607
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`.
608
609
  - Example:
609
610
  ```bash
611
+ mssh -ea '-p 2222' allServers uptime
610
612
  mssh -ea="--delete" -f ./data/ allServers /tmp/data/
611
613
  mssh -ea="-o ProxyCommand='ssh -W %h:%p bastion'" allServers uptime
612
614
  ```
@@ -717,7 +719,7 @@ mssh -f ./data/ allServers /tmp/data/
717
719
 
718
720
  ### `-ia, --ipmi_args IPMI_ARGS` | _`DEFAULT_IPMI_ARGS`_
719
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.
720
- - 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'` ).
721
723
 
722
724
  ### `-im, --ipmi_method IPMI_METHOD` | _`DEFAULT_IPMI_METHOD`_
723
725
  - How `--ipmi` reaches a host:
@@ -824,7 +826,7 @@ mssh -f ./data/ allServers /tmp/data/
824
826
  - By default mssh set this to true to speed up operations on large host lists with some unreachable hosts.
825
827
  - Unreachable hosts will be tried again when their timeout expires.
826
828
  - A skipped host is not run and counts as a failed host: it shows return code 255 ( what ssh returns when it cannot connect ) with a line saying it was skipped, and the summary lists it after `failed_hosts:` as `Skipped N hosts unreachable in a recent run, without trying them: ...`. So mssh does not exit 0 or print `Success` when it skipped a host.
827
- - mssh stores the current run unreachable hosts in memory and if `skip_unreachable` is true, it will store them in `unavailable_hosts.csv` in mssh's private runtime directory: `$XDG_RUNTIME_DIR/multiSSH3`, else `/run/user/<uid>/multiSSH3`, `/dev/shm/multiSSH3-<uid>`, or a `multiSSH3-<uid>` folder in the system temp folder (the first that only you can write to). If none can be used, they are only kept for the current run.
829
+ - mssh stores the current run unreachable hosts in memory and if `skip_unreachable` is true, it will store them in `unavailable_hosts.csv` in mssh's private runtime directory: `$XDG_RUNTIME_DIR/multiSSH3`, else `/run/user/<uid>/multiSSH3`, `/dev/shm/multiSSH3-<uid>`, or a `multiSSH3-<uid>` folder in the system temp folder (the first that only you can write to, or could make). mssh reads them back from that one only, not from a later place an older session left them in. If none can be used, they are only kept for the current run.
828
830
  - To force mssh to not use unreachable hosts from previous runs, you can use `-a, -nsu, --no_skip_unreachable` to set `skip_unreachable` to false.
829
831
 
830
832
  ### `-a, -nsu, --no_skip_unreachable` | not _`DEFAULT_SKIP_UNREACHABLE`_
@@ -972,7 +974,7 @@ Option names count in full only: `--user` is not `--username` and `-c` is not `-
972
974
 
973
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).
974
976
 
975
- Below is a sample help message output from multiSSH3 6.34 @ 2026-10-16 (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.36 @ 2026-10-16 (no config file, 120 columns; the lines after the options list the programs found on that machine)
976
978
 
977
979
  ```bash
978
980
  $ mssh -h
@@ -1003,7 +1005,7 @@ options:
1003
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)
1004
1006
  -uk, --use_key Attempt to use public key file to connect to the hosts. (default: False)
1005
1007
  -ea, --extraargs EXTRAARGS
1006
- 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)
1007
1009
  -11, --oneonone Run one corresponding command on each host. (default: False)
1008
1010
  -f, --file FILE The file to be copied to the hosts. Use -f multiple times to copy multiple files
1009
1011
  -s, -fs, --file_sync [FILE_SYNC]
@@ -1028,7 +1030,7 @@ options:
1028
1030
  -ip, --ipmi_password IPMI_PASSWORD
1029
1031
  The password to use to connect to the hosts via ipmi. (default: set, not shown)
1030
1032
  -ia, --ipmi_args IPMI_ARGS
1031
- 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: )
1032
1034
  -im, --ipmi_method IPMI_METHOD
1033
1035
  The method to use to connect to the hosts via ipmi. (default: ipmitool)
1034
1036
  -irlt, --ipmi_retry_lockout_time IPMI_RETRY_LOCKOUT_TIME
@@ -577,10 +577,12 @@ mssh -q -- web1 grep -r TODO /srv
577
577
 
578
578
  ### `-ea, --extraargs EXTRAARGS` | _`DEFAULT_EXTRA_ARGS`_
579
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. )
580
581
  - The string is split into arguments as a shell would: quote an argument that contains spaces.
581
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`.
582
583
  - Example:
583
584
  ```bash
585
+ mssh -ea '-p 2222' allServers uptime
584
586
  mssh -ea="--delete" -f ./data/ allServers /tmp/data/
585
587
  mssh -ea="-o ProxyCommand='ssh -W %h:%p bastion'" allServers uptime
586
588
  ```
@@ -691,7 +693,7 @@ mssh -f ./data/ allServers /tmp/data/
691
693
 
692
694
  ### `-ia, --ipmi_args IPMI_ARGS` | _`DEFAULT_IPMI_ARGS`_
693
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.
694
- - 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'` ).
695
697
 
696
698
  ### `-im, --ipmi_method IPMI_METHOD` | _`DEFAULT_IPMI_METHOD`_
697
699
  - How `--ipmi` reaches a host:
@@ -798,7 +800,7 @@ mssh -f ./data/ allServers /tmp/data/
798
800
  - By default mssh set this to true to speed up operations on large host lists with some unreachable hosts.
799
801
  - Unreachable hosts will be tried again when their timeout expires.
800
802
  - A skipped host is not run and counts as a failed host: it shows return code 255 ( what ssh returns when it cannot connect ) with a line saying it was skipped, and the summary lists it after `failed_hosts:` as `Skipped N hosts unreachable in a recent run, without trying them: ...`. So mssh does not exit 0 or print `Success` when it skipped a host.
801
- - mssh stores the current run unreachable hosts in memory and if `skip_unreachable` is true, it will store them in `unavailable_hosts.csv` in mssh's private runtime directory: `$XDG_RUNTIME_DIR/multiSSH3`, else `/run/user/<uid>/multiSSH3`, `/dev/shm/multiSSH3-<uid>`, or a `multiSSH3-<uid>` folder in the system temp folder (the first that only you can write to). If none can be used, they are only kept for the current run.
803
+ - mssh stores the current run unreachable hosts in memory and if `skip_unreachable` is true, it will store them in `unavailable_hosts.csv` in mssh's private runtime directory: `$XDG_RUNTIME_DIR/multiSSH3`, else `/run/user/<uid>/multiSSH3`, `/dev/shm/multiSSH3-<uid>`, or a `multiSSH3-<uid>` folder in the system temp folder (the first that only you can write to, or could make). mssh reads them back from that one only, not from a later place an older session left them in. If none can be used, they are only kept for the current run.
802
804
  - To force mssh to not use unreachable hosts from previous runs, you can use `-a, -nsu, --no_skip_unreachable` to set `skip_unreachable` to false.
803
805
 
804
806
  ### `-a, -nsu, --no_skip_unreachable` | not _`DEFAULT_SKIP_UNREACHABLE`_
@@ -946,7 +948,7 @@ Option names count in full only: `--user` is not `--username` and `-c` is not `-
946
948
 
947
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).
948
950
 
949
- Below is a sample help message output from multiSSH3 6.34 @ 2026-10-16 (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.36 @ 2026-10-16 (no config file, 120 columns; the lines after the options list the programs found on that machine)
950
952
 
951
953
  ```bash
952
954
  $ mssh -h
@@ -977,7 +979,7 @@ options:
977
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)
978
980
  -uk, --use_key Attempt to use public key file to connect to the hosts. (default: False)
979
981
  -ea, --extraargs EXTRAARGS
980
- 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)
981
983
  -11, --oneonone Run one corresponding command on each host. (default: False)
982
984
  -f, --file FILE The file to be copied to the hosts. Use -f multiple times to copy multiple files
983
985
  -s, -fs, --file_sync [FILE_SYNC]
@@ -1002,7 +1004,7 @@ options:
1002
1004
  -ip, --ipmi_password IPMI_PASSWORD
1003
1005
  The password to use to connect to the hosts via ipmi. (default: set, not shown)
1004
1006
  -ia, --ipmi_args IPMI_ARGS
1005
- 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: )
1006
1008
  -im, --ipmi_method IPMI_METHOD
1007
1009
  The method to use to connect to the hosts via ipmi. (default: ipmitool)
1008
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.7'
19
+ version = '0.9'
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:
@@ -1422,6 +1450,21 @@ def _private_dir(path, create=True):
1422
1450
  return False
1423
1451
  return stat.S_ISDIR(st.st_mode) and st.st_uid == os.getuid() and not st.st_mode & 0o077
1424
1452
 
1453
+ def _could_be_private_dir(path):
1454
+ '''Whether _private_dir(path, create=True) would take path, without changing anything (as multiSSH3's).'''
1455
+ import stat
1456
+ try:
1457
+ st = os.lstat(path)
1458
+ except FileNotFoundError:
1459
+ return os.access(os.path.dirname(path), os.W_OK | os.X_OK)
1460
+ except OSError:
1461
+ return False
1462
+ if not stat.S_ISDIR(st.st_mode):
1463
+ return False
1464
+ if not hasattr(os, 'getuid'):
1465
+ return True
1466
+ return st.st_uid == os.getuid() and not st.st_mode & 0o022
1467
+
1425
1468
  def _owned_and_not_writable_by_others(path):
1426
1469
  if not hasattr(os, 'getuid'):
1427
1470
  return True
@@ -1440,7 +1483,8 @@ def cache_dir(create=True):
1440
1483
  mssh's private runtime directory, where unavailable_hosts.csv and the index live, or None.
1441
1484
 
1442
1485
  Exactly as multiSSH3.runtime_dir picks it: $XDG_RUNTIME_DIR/multiSSH3 when that runtime dir
1443
- is ours, /run/user/<uid>/multiSSH3, /dev/shm/multiSSH3-<uid>, then <tempdir>/multiSSH3-<uid>.
1486
+ is ours, /run/user/<uid>/multiSSH3, /dev/shm/multiSSH3-<uid>, then <tempdir>/multiSSH3-<uid>;
1487
+ without create, the one a write would use if it is there already, else None.
1444
1488
  '''
1445
1489
  import tempfile
1446
1490
  if hasattr(os, 'getuid'):
@@ -1460,8 +1504,11 @@ def cache_dir(create=True):
1460
1504
  candidates.append(os.path.join(_SHM_DIR, 'multiSSH3-{}'.format(user_id)))
1461
1505
  candidates.append(os.path.join(tempfile.gettempdir(), 'multiSSH3-{}'.format(user_id)))
1462
1506
  for directory in candidates:
1463
- if _private_dir(directory, create):
1464
- return directory
1507
+ if create:
1508
+ if _private_dir(directory):
1509
+ return directory
1510
+ elif _could_be_private_dir(directory):
1511
+ return directory if _private_dir(directory, create=False) else None
1465
1512
  return None
1466
1513
 
1467
1514
  def _debug(message):
@@ -3616,10 +3663,16 @@ class Engine(object):
3616
3663
  have_hosts = False
3617
3664
  run = None # where the unquoted remote command starts in argv
3618
3665
  pending = None # nargs of an option still taking the words that follow as values
3666
+ passed_on = None # -ea's or -ia's dest: the word may be its value even when it starts with '-'
3619
3667
  paths = False
3620
3668
  read = []
3621
3669
  stop = len(argv)
3622
3670
  for index, token in enumerate(argv):
3671
+ if passed_on:
3672
+ dest, passed_on = passed_on, None
3673
+ if takes_passed_on_value(dest, token.text, self.spec):
3674
+ pending = None
3675
+ continue
3623
3676
  if token.text == '--':
3624
3677
  stop = index
3625
3678
  break
@@ -3642,6 +3695,7 @@ class Engine(object):
3642
3695
  option, attached, _ = readings[-1]
3643
3696
  if attached is None and option.nargs != '0':
3644
3697
  pending = option.nargs
3698
+ passed_on = option.dest if option.dest in PASSED_ON_DESTS else None
3645
3699
  if run is None and any(option.dest in ('file', 'file_sync', 'gather_mode') for option, _, _ in readings):
3646
3700
  paths = True
3647
3701
  if run is not None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: multiSSH3
3
- Version: 6.34
3
+ Version: 6.36
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
@@ -603,10 +603,12 @@ mssh -q -- web1 grep -r TODO /srv
603
603
 
604
604
  ### `-ea, --extraargs EXTRAARGS` | _`DEFAULT_EXTRA_ARGS`_
605
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. )
606
607
  - The string is split into arguments as a shell would: quote an argument that contains spaces.
607
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`.
608
609
  - Example:
609
610
  ```bash
611
+ mssh -ea '-p 2222' allServers uptime
610
612
  mssh -ea="--delete" -f ./data/ allServers /tmp/data/
611
613
  mssh -ea="-o ProxyCommand='ssh -W %h:%p bastion'" allServers uptime
612
614
  ```
@@ -717,7 +719,7 @@ mssh -f ./data/ allServers /tmp/data/
717
719
 
718
720
  ### `-ia, --ipmi_args IPMI_ARGS` | _`DEFAULT_IPMI_ARGS`_
719
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.
720
- - 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'` ).
721
723
 
722
724
  ### `-im, --ipmi_method IPMI_METHOD` | _`DEFAULT_IPMI_METHOD`_
723
725
  - How `--ipmi` reaches a host:
@@ -824,7 +826,7 @@ mssh -f ./data/ allServers /tmp/data/
824
826
  - By default mssh set this to true to speed up operations on large host lists with some unreachable hosts.
825
827
  - Unreachable hosts will be tried again when their timeout expires.
826
828
  - A skipped host is not run and counts as a failed host: it shows return code 255 ( what ssh returns when it cannot connect ) with a line saying it was skipped, and the summary lists it after `failed_hosts:` as `Skipped N hosts unreachable in a recent run, without trying them: ...`. So mssh does not exit 0 or print `Success` when it skipped a host.
827
- - mssh stores the current run unreachable hosts in memory and if `skip_unreachable` is true, it will store them in `unavailable_hosts.csv` in mssh's private runtime directory: `$XDG_RUNTIME_DIR/multiSSH3`, else `/run/user/<uid>/multiSSH3`, `/dev/shm/multiSSH3-<uid>`, or a `multiSSH3-<uid>` folder in the system temp folder (the first that only you can write to). If none can be used, they are only kept for the current run.
829
+ - mssh stores the current run unreachable hosts in memory and if `skip_unreachable` is true, it will store them in `unavailable_hosts.csv` in mssh's private runtime directory: `$XDG_RUNTIME_DIR/multiSSH3`, else `/run/user/<uid>/multiSSH3`, `/dev/shm/multiSSH3-<uid>`, or a `multiSSH3-<uid>` folder in the system temp folder (the first that only you can write to, or could make). mssh reads them back from that one only, not from a later place an older session left them in. If none can be used, they are only kept for the current run.
828
830
  - To force mssh to not use unreachable hosts from previous runs, you can use `-a, -nsu, --no_skip_unreachable` to set `skip_unreachable` to false.
829
831
 
830
832
  ### `-a, -nsu, --no_skip_unreachable` | not _`DEFAULT_SKIP_UNREACHABLE`_
@@ -972,7 +974,7 @@ Option names count in full only: `--user` is not `--username` and `-c` is not `-
972
974
 
973
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).
974
976
 
975
- Below is a sample help message output from multiSSH3 6.34 @ 2026-10-16 (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.36 @ 2026-10-16 (no config file, 120 columns; the lines after the options list the programs found on that machine)
976
978
 
977
979
  ```bash
978
980
  $ mssh -h
@@ -1003,7 +1005,7 @@ options:
1003
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)
1004
1006
  -uk, --use_key Attempt to use public key file to connect to the hosts. (default: False)
1005
1007
  -ea, --extraargs EXTRAARGS
1006
- 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)
1007
1009
  -11, --oneonone Run one corresponding command on each host. (default: False)
1008
1010
  -f, --file FILE The file to be copied to the hosts. Use -f multiple times to copy multiple files
1009
1011
  -s, -fs, --file_sync [FILE_SYNC]
@@ -1028,7 +1030,7 @@ options:
1028
1030
  -ip, --ipmi_password IPMI_PASSWORD
1029
1031
  The password to use to connect to the hosts via ipmi. (default: set, not shown)
1030
1032
  -ia, --ipmi_args IPMI_ARGS
1031
- 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: )
1032
1034
  -im, --ipmi_method IPMI_METHOD
1033
1035
  The method to use to connect to the hosts via ipmi. (default: ipmitool)
1034
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