multiSSH3 6.37__tar.gz → 6.39__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 (115) hide show
  1. {multissh3-6.37/multiSSH3.egg-info → multissh3-6.39}/PKG-INFO +61 -22
  2. {multissh3-6.37 → multissh3-6.39}/README.md +60 -21
  3. {multissh3-6.37 → multissh3-6.39}/mssh_complete.py +769 -1707
  4. {multissh3-6.37 → multissh3-6.39/multiSSH3.egg-info}/PKG-INFO +61 -22
  5. {multissh3-6.37 → multissh3-6.39}/multiSSH3.egg-info/SOURCES.txt +1 -0
  6. {multissh3-6.37 → multissh3-6.39}/multiSSH3.py +324 -1018
  7. {multissh3-6.37 → multissh3-6.39}/pytest.ini +0 -1
  8. {multissh3-6.37 → multissh3-6.39}/tests/bench_complete.py +7 -2
  9. {multissh3-6.37 → multissh3-6.39}/tests/complete_shell_harness.py +80 -12
  10. {multissh3-6.37 → multissh3-6.39}/tests/conftest.py +1 -15
  11. multissh3-6.39/tests/replay.py +356 -0
  12. {multissh3-6.37 → multissh3-6.39}/tests/test_cli_branches.py +0 -28
  13. {multissh3-6.37 → multissh3-6.39}/tests/test_cli_processing.py +0 -32
  14. multissh3-6.39/tests/test_compact_hostnames.py +43 -0
  15. {multissh3-6.37 → multissh3-6.39}/tests/test_complete_blesh.py +4 -3
  16. multissh3-6.39/tests/test_complete_cli.py +468 -0
  17. {multissh3-6.37 → multissh3-6.39}/tests/test_complete_diagnose.py +74 -346
  18. {multissh3-6.37 → multissh3-6.39}/tests/test_complete_editor.py +2 -13
  19. {multissh3-6.37 → multissh3-6.39}/tests/test_complete_engine.py +30 -64
  20. {multissh3-6.37 → multissh3-6.39}/tests/test_complete_eval.py +55 -26
  21. {multissh3-6.37 → multissh3-6.39}/tests/test_complete_index.py +80 -38
  22. {multissh3-6.37 → multissh3-6.39}/tests/test_complete_keys.py +77 -16
  23. {multissh3-6.37 → multissh3-6.39}/tests/test_complete_picker.py +76 -223
  24. {multissh3-6.37 → multissh3-6.39}/tests/test_complete_probe.py +15 -54
  25. {multissh3-6.37 → multissh3-6.39}/tests/test_complete_shells.py +746 -140
  26. {multissh3-6.37 → multissh3-6.39}/tests/test_complete_untrusted.py +5 -5
  27. {multissh3-6.37 → multissh3-6.39}/tests/test_config_cli.py +89 -10
  28. {multissh3-6.37 → multissh3-6.39}/tests/test_config_values.py +9 -28
  29. {multissh3-6.37 → multissh3-6.39}/tests/test_control_master_config.py +1 -1
  30. {multissh3-6.37 → multissh3-6.39}/tests/test_copy_id.py +4 -4
  31. {multissh3-6.37 → multissh3-6.39}/tests/test_curses_branches.py +0 -33
  32. {multissh3-6.37 → multissh3-6.39}/tests/test_curses_event_loop.py +1 -77
  33. multissh3-6.39/tests/test_curses_tui.py +39 -0
  34. {multissh3-6.37 → multissh3-6.39}/tests/test_file_trust.py +0 -11
  35. {multissh3-6.37 → multissh3-6.39}/tests/test_harness_smoke.py +4 -3
  36. {multissh3-6.37 → multissh3-6.39}/tests/test_login_drop_retry.py +15 -49
  37. {multissh3-6.37 → multissh3-6.39}/tests/test_main_lifecycle.py +15 -0
  38. {multissh3-6.37 → multissh3-6.39}/tests/test_output_merge.py +37 -0
  39. {multissh3-6.37 → multissh3-6.39}/tests/test_output_robustness.py +25 -6
  40. {multissh3-6.37 → multissh3-6.39}/tests/test_run_command_branches.py +4 -187
  41. {multissh3-6.37 → multissh3-6.39}/tests/test_run_command_characterization.py +5 -2
  42. {multissh3-6.37 → multissh3-6.39}/tests/test_run_command_exit.py +0 -1
  43. {multissh3-6.37 → multissh3-6.39}/tests/test_runtime_branch_matrix.py +0 -56
  44. {multissh3-6.37 → multissh3-6.39}/tests/test_runtime_dir.py +8 -20
  45. {multissh3-6.37 → multissh3-6.39}/tests/test_smoke_optional.py +0 -20
  46. multissh3-6.39/tests/test_ssh_timeouts.py +235 -0
  47. {multissh3-6.37 → multissh3-6.39}/tests/test_stopped_commands.py +1 -148
  48. {multissh3-6.37 → multissh3-6.39}/tests/test_stream_reader.py +25 -36
  49. {multissh3-6.37 → multissh3-6.39}/tests/test_unavailable_hosts.py +4 -2
  50. multissh3-6.37/tests/test_compact_hostnames.py +0 -26
  51. multissh3-6.37/tests/test_complete_cli.py +0 -285
  52. multissh3-6.37/tests/test_curses_tui.py +0 -81
  53. multissh3-6.37/tests/test_ssh_timeouts.py +0 -386
  54. {multissh3-6.37 → multissh3-6.39}/LICENSE +0 -0
  55. {multissh3-6.37 → multissh3-6.39}/MANIFEST.in +0 -0
  56. {multissh3-6.37 → multissh3-6.39}/multiSSH3.egg-info/dependency_links.txt +0 -0
  57. {multissh3-6.37 → multissh3-6.39}/multiSSH3.egg-info/entry_points.txt +0 -0
  58. {multissh3-6.37 → multissh3-6.39}/multiSSH3.egg-info/top_level.txt +0 -0
  59. {multissh3-6.37 → multissh3-6.39}/setup.cfg +0 -0
  60. {multissh3-6.37 → multissh3-6.39}/setup.py +0 -0
  61. {multissh3-6.37 → multissh3-6.39}/tests/data/run_command_golden.json +0 -0
  62. {multissh3-6.37 → multissh3-6.39}/tests/shells/Dockerfile.bash5.1 +0 -0
  63. {multissh3-6.37 → multissh3-6.39}/tests/shells/Dockerfile.bash5.3 +0 -0
  64. {multissh3-6.37 → multissh3-6.39}/tests/shells/Dockerfile.centos7 +0 -0
  65. {multissh3-6.37 → multissh3-6.39}/tests/shells/Dockerfile.current +0 -0
  66. {multissh3-6.37 → multissh3-6.39}/tests/shells/Dockerfile.legacy +0 -0
  67. {multissh3-6.37 → multissh3-6.39}/tests/shells/Dockerfile.modern +0 -0
  68. {multissh3-6.37 → multissh3-6.39}/tests/shells/run_matrix.sh +0 -0
  69. {multissh3-6.37 → multissh3-6.39}/tests/test_cli_option_words.py +0 -0
  70. {multissh3-6.37 → multissh3-6.39}/tests/test_closed_output.py +0 -0
  71. {multissh3-6.37 → multissh3-6.39}/tests/test_color_helpers.py +0 -0
  72. {multissh3-6.37 → multissh3-6.39}/tests/test_command_terminal.py +0 -0
  73. {multissh3-6.37 → multissh3-6.39}/tests/test_complete_context.py +0 -0
  74. {multissh3-6.37 → multissh3-6.39}/tests/test_complete_hosts.py +0 -0
  75. {multissh3-6.37 → multissh3-6.39}/tests/test_complete_hoststate.py +0 -0
  76. {multissh3-6.37 → multissh3-6.39}/tests/test_complete_lex.py +0 -0
  77. {multissh3-6.37 → multissh3-6.39}/tests/test_complete_mssh_helpers.py +0 -0
  78. {multissh3-6.37 → multissh3-6.39}/tests/test_complete_password_spellings.py +0 -0
  79. {multissh3-6.37 → multissh3-6.39}/tests/test_complete_start.py +0 -0
  80. {multissh3-6.37 → multissh3-6.39}/tests/test_complete_store.py +0 -0
  81. {multissh3-6.37 → multissh3-6.39}/tests/test_complete_suggest.py +0 -0
  82. {multissh3-6.37 → multissh3-6.39}/tests/test_complete_terminal.py +0 -0
  83. {multissh3-6.37 → multissh3-6.39}/tests/test_complete_zsh_options.py +0 -0
  84. {multissh3-6.37 → multissh3-6.39}/tests/test_config_validate.py +0 -0
  85. {multissh3-6.37 → multissh3-6.39}/tests/test_curses_wide_chars.py +0 -0
  86. {multissh3-6.37 → multissh3-6.39}/tests/test_expand_hostnames.py +0 -0
  87. {multissh3-6.37 → multissh3-6.39}/tests/test_helpers_extra.py +0 -0
  88. {multissh3-6.37 → multissh3-6.39}/tests/test_history_replay.py +0 -0
  89. {multissh3-6.37 → multissh3-6.39}/tests/test_host.py +0 -0
  90. {multissh3-6.37 → multissh3-6.39}/tests/test_host_validity.py +0 -0
  91. {multissh3-6.37 → multissh3-6.39}/tests/test_hosts_file.py +0 -0
  92. {multissh3-6.37 → multissh3-6.39}/tests/test_identities_only.py +0 -0
  93. {multissh3-6.37 → multissh3-6.39}/tests/test_ipmi_lockout_wait.py +0 -0
  94. {multissh3-6.37 → multissh3-6.39}/tests/test_ipmi_logins.py +0 -0
  95. {multissh3-6.37 → multissh3-6.39}/tests/test_key_path_home.py +0 -0
  96. {multissh3-6.37 → multissh3-6.39}/tests/test_narrow_terminal.py +0 -0
  97. {multissh3-6.37 → multissh3-6.39}/tests/test_no_hosts.py +0 -0
  98. {multissh3-6.37 → multissh3-6.39}/tests/test_option_no_effect.py +0 -0
  99. {multissh3-6.37 → multissh3-6.39}/tests/test_options_after_command.py +0 -0
  100. {multissh3-6.37 → multissh3-6.39}/tests/test_packaging.py +0 -0
  101. {multissh3-6.37 → multissh3-6.39}/tests/test_password_handling.py +0 -0
  102. {multissh3-6.37 → multissh3-6.39}/tests/test_passwords_out_of_ps.py +0 -0
  103. {multissh3-6.37 → multissh3-6.39}/tests/test_run_command.py +0 -0
  104. {multissh3-6.37 → multissh3-6.39}/tests/test_run_on_hosts.py +0 -0
  105. {multissh3-6.37 → multissh3-6.39}/tests/test_run_on_hosts_modes.py +0 -0
  106. {multissh3-6.37 → multissh3-6.39}/tests/test_shared_lines.py +0 -0
  107. {multissh3-6.37 → multissh3-6.39}/tests/test_ssh_error_text.py +0 -0
  108. {multissh3-6.37 → multissh3-6.39}/tests/test_sshpass_exit.py +0 -0
  109. {multissh3-6.37 → multissh3-6.39}/tests/test_start_run_on_hosts.py +0 -0
  110. {multissh3-6.37 → multissh3-6.39}/tests/test_streamed_output.py +0 -0
  111. {multissh3-6.37 → multissh3-6.39}/tests/test_tui_ssh_notices.py +0 -0
  112. {multissh3-6.37 → multissh3-6.39}/tests/test_underscore_settings.py +0 -0
  113. {multissh3-6.37 → multissh3-6.39}/tests/test_waiting.py +0 -0
  114. {multissh3-6.37 → multissh3-6.39}/tests/test_wide_wrap.py +0 -0
  115. {multissh3-6.37 → multissh3-6.39}/tests/vt_grid.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: multiSSH3
3
- Version: 6.37
3
+ Version: 6.39
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
@@ -180,7 +180,9 @@ Defaults are read from the following chain map stored as json files, top ones ov
180
180
  >
181
181
  > `--store_config_file` writes the file readable by you only (`0600`), as it can hold passwords. The `/etc` locations above are written `0644` so every user can read them. An existing file's permissions are only ever tightened, and a file whose permissions cannot be restricted is left untouched (the config is printed instead).
182
182
  >
183
- > A config file that is a symlink (e.g. into a dotfiles repository) is written through the link; its backup (`.bak`, the default answer when the file exists) is a copy next to the link.
183
+ > A config file that is a symlink (e.g. into a dotfiles repository) is written through the link; its backup (`.bak`, the default answer when the file exists) is a copy next to the link. Under `sudo`, root follows only its own links: with the default `b` the link is moved to the `.bak` and replaced by a regular file; with `o` the config is printed and the link is kept ([limitations](docs/limitations.md#under-sudo---store_config_file-replaces-a-symlinked-config-instead-of-writing-through-it)).
184
+ >
185
+ > The file is replaced in one step: a temporary file beside it, with the old file's owner and group where they can be kept, is renamed over it, so with `o` a full disk leaves the old one; with the default `b` the old one is the `.bak`. With `o` (overwrite), and for a config reached through a symlink, a file of another user is written only when its owner can be kept (as root); otherwise the config is printed instead, also when you may write the file through an ACL ([limitations](docs/limitations.md#another-users-config-shared-through-an-acl-is-not-written)). With the default `b`, a config that is not a symlink is moved to the `.bak` and the new one keeps its owner and group where that can be kept (the owner only as root; the group as anyone, for a file of yours); otherwise it is yours.
184
186
  >
185
187
  > Each value is checked against the type of the setting's default. Values whose meaning is clear are converted (`"true"` / `"false"`, `"yes"` / `"no"` for true / false settings, numbers written as strings, a list of host names for `DEFAULT_HOSTS`); anything else, such as `"DEFAULT_TIMEOUT": "fifty"` or an encoding Python does not know, is ignored with a warning naming the file and the key, and the setting keeps its value from the rest of the chain (no warning when a more specific file sets that key anyway). `null` for a string setting means its default (`DEFAULT_HOSTS: null` is `all`). A file that is not a JSON object is skipped the same way. `--config_file` applies a file exactly as the chain does, `_sshPath`, `DEFAULT_ENV_FILE` and `ERROR_MESSAGES_TO_IGNORE` included.
186
188
 
@@ -418,7 +420,7 @@ The output is printed when the commands finish, so it can be piped like any comm
418
420
  ```bash
419
421
  mssh all 'dmesg' | less
420
422
  ```
421
- If the reader exits before reading all of it (`| head`, or `q` in `less`), mssh stops quietly as other command line tools do: no further commands or `--repeat` runs start, and the exit code still counts the hosts that failed in what ran. If that happens before anything has run (the output of `-h`, `-V`, a usage error or `--generate_config_file`), the exit code is 141, as for other tools SIGPIPE ends.
423
+ If the reader exits before reading all of it (`| head`, or `q` in `less`), mssh stops quietly as other command line tools do: no further commands or `--repeat` runs start, and the exit code still counts the hosts that failed in what ran. If that happens before anything has run (the output of `-h`, `-V`, a usage error or `--generate_config_file`), SIGPIPE ends mssh as it ends other tools: the shell reports 141, a program that starts mssh with Python's subprocess sees -13, and on Windows the exit code is 120 ([limitations](docs/limitations.md#output-before-a-run-into-a-closed-pipe-ends-mssh-by-sigpipe)).
422
424
 
423
425
  ### Curses Window Size Control
424
426
 
@@ -444,7 +446,7 @@ While leaving minimum 40 characters / 1 line for each host display by default. Y
444
446
  > Home/End: Jump cursor
445
447
  > Esc : Clear / hide view
446
448
  > ```
447
- > `Esc` clears the input line. On an empty input line it hides the live view; the commands keep running and their output is printed when they finish.
449
+ > `Esc` clears the input line. On an empty input line it hides the live view; the commands keep running and their output is printed when they finish. While the terminal is under 3 rows the display shows "Terminal too small" and Esc waits until it is bigger ([limitations](docs/limitations.md#esc-does-nothing-while-the-terminal-is-under-3-rows)).
448
450
  > The live view leaves out ssh's own notices, as the output at the end does ( `ERROR_MESSAGES_TO_IGNORE` ): mssh does not keep the hosts' keys, so ssh says `Warning: Permanently added ...` on every connection, and a quiet command's window showed only that.
449
451
  > You can also toggle this help in curses by pressing `?` or `F1`.
450
452
  > The one-character keys (`?`, `_`, `+`, `{`, `}`, `<`, `>`, `|`) act only on an empty input line; after you have typed something they are typed like any other character, so a `?` can still reach the hosts. `F1` toggles the help at any time.
@@ -476,9 +478,7 @@ mssh has ssh, scp, rsync's ssh and ssh-copy-id give up on a host that takes the
476
478
 
477
479
  Change the values with `DEFAULT_SSH_CONNECT_TIMEOUT`, `DEFAULT_SSH_SERVER_ALIVE_INTERVAL` and `DEFAULT_SSH_SERVER_ALIVE_COUNT_MAX` in the config file; 0 leaves the option to ssh (without a keepalive interval, mssh does not pass its count either).
478
480
 
479
- Your own values win. ssh uses the first value it gets and a command-line `-o` beats the ssh config, so mssh leaves an option out when you give it:
480
- - with `-ea` for ssh or scp, e.g. `-ea '-o ConnectTimeout=120'` (with `-ea '-F FILE'`, mssh reads that ssh config instead of the two below);
481
- - in your ssh config, `~/.ssh/config` or `/etc/ssh/ssh_config` and the files they `Include`, also for single hosts (`Host slow*` / `ConnectTimeout 120`). Only when these files mention one of the three options does mssh ask `ssh -G` about each host whether it sets it, which runs the config's `Match exec` lines once more. `ConnectTimeout 0` there means no timeout for those hosts; `ServerAliveInterval 0` cannot be told from not setting it.
481
+ Your own `-ea` values win: ssh uses the first value it gets, and mssh puts its options after the ones in `-ea`, so `-ea '-o ConnectTimeout=120'` sets 120. An `-ea` keepalive interval still gets mssh's count (`-ea '-o ServerAliveInterval=60'`: 4 x 60 s). Options in your ssh config (`~/.ssh/config`, `/etc/ssh/ssh_config`, or a file given with `-ea '-F FILE'`) do not win: a command-line `-o` beats the ssh config, and mssh's are command-line options. Set the `DEFAULT_SSH_*` values instead, or 0 to leave an option to your ssh config ([limitations](docs/limitations.md#msshs-ssh-timeouts-override-the-same-options-in-the-ssh-config)).
482
482
 
483
483
  A shared connection (`ControlMaster`, see [SSH ControlMaster Configuration](#ssh-controlmaster-configuration)) does the keepalive of the ssh that opened it. One mssh opened gives up on a host that went away, and the next ssh connects anew (where `ConnectTimeout` applies). One that a plain `ssh` without `ServerAliveInterval` in your ssh config opened does not notice: commands through it wait until the kernel gives up on the connection, which takes about 15 minutes on Linux. Setting `ServerAliveInterval` in your ssh config covers those too.
484
484
 
@@ -498,7 +498,7 @@ A new block is appended to your rc file; changing or removing it replaces the fi
498
498
 
499
499
  - **Hosts:** host strings you typed before, env file aliases, `/etc/hosts`, ssh config `Host` entries, `known_hosts` and the ARP table. Large clusters are offered as ranges (`node[0001-2000]`) until what you typed narrows them down. Hosts mssh recently found unreachable come last (with the user this line gives them: `root@node05` and `node05` are told apart).
500
500
  - **Options and values:** option names, and values you used before (`-u`, `-t`, ...). Paths complete as files. Passwords are never completed or shown, the ones in your config file included.
501
- - **Commands:** commands from your mssh history, the rest of a command from this machine's own completion (`mssh grp systemctl rest<TAB>`), and the magic strings (`#HOST#`, ...). A command typed as separate words gets the next words of commands you ran, with the quotes they ran with (`'ssh*'` comes as `\'ssh\*\'`, so the remote shell gets the same command), never a word holding a space: mssh runs each word as its own command when one of them holds a space, so type such a command in quotes (`mssh grp "grep 'connection refused' /var/log/syslog"`).
501
+ - **Commands:** commands from your mssh history, the rest of a command from this machine's own completion (`mssh grp systemctl rest<TAB>`; in bash as bash-completion completes `sudo CMD`, with a few gaps in [limitations](docs/limitations.md#autocomplete)), and the magic strings (`#HOST#`, ...). A command typed as separate words gets the next words of commands you ran, with the quotes they ran with (`'ssh*'` comes as `\'ssh\*\'`, so the remote shell gets the same command), never a word holding a space: mssh runs each word as its own command when one of them holds a space, so type such a command in quotes (`mssh grp "grep 'connection refused' /var/log/syslog"`).
502
502
 
503
503
  Ranking uses the rest of the line on both sides of the cursor: after `mssh gpu[01-16] "` the commands you run on those hosts come first, and in the hosts of `mssh ▮ "systemctl restart slurmd"` the hosts you restarted slurmd on come first.
504
504
 
@@ -508,7 +508,7 @@ In bash with readline's `completion-ignore-case` on (`set completion-ignore-case
508
508
 
509
509
  In zsh, TAB needs zsh's completion system: `compinit` has to run when zsh starts, before or after the mssh block in `~/.zshrc` (oh-my-zsh, prezto and the like run it, and so does Ubuntu's `/etc/zsh/zshrc`). `mssh --completion install` says so when `~/.zshrc` does not seem to start it. Grey suggestions, warnings and Ctrl-R work without it.
510
510
 
511
- It learns from an index of your history and hosts, kept in mssh's private runtime directory (`$XDG_RUNTIME_DIR/multiSSH3`, else `/run/user/<uid>/multiSSH3`, `/dev/shm/multiSSH3-<uid>` or `multiSSH3-<uid>` in the temp dir) and rebuilt in the background when its sources change. `mssh --completion install` builds it right away. A reboot clears it: the first TAB after one starts the build and, until the index is ready (about a second for a long history), offers options and what your shell history holds. `mssh-complete rebuild` rebuilds it now; `mssh-complete eval` replays your history and shows how much typing completion saves.
511
+ It learns from an index of your history and hosts, kept in mssh's private runtime directory (`$XDG_RUNTIME_DIR/multiSSH3`, else `/run/user/<uid>/multiSSH3`, `/dev/shm/multiSSH3-<uid>` or `multiSSH3-<uid>` in the temp dir) and rebuilt in the background when its sources change. `mssh --completion install` builds it right away. A reboot clears it, and an upgrade that changes its format (6.39) makes it unreadable: the first TAB after either starts the build and, until the index is ready (about a second for a long history), offers options, the remote command's own completion and what your shell history holds. `mssh-complete rebuild` rebuilds it now. From a source checkout, `python3 tests/replay.py` replays your history and shows how much typing completion saves.
512
512
 
513
513
  ### Grey suggestions
514
514
 
@@ -528,29 +528,29 @@ While you type an mssh line, the most likely rest of it shows in grey after the
528
528
 
529
529
  ### Ctrl-R
530
530
 
531
- Ctrl-R on an mssh line opens a menu below the prompt with the candidates for the word under the cursor (hosts, commands for those hosts, option values), how often and how long ago you used them; Tab switches to whole past invocations. Type or paste to filter, ↑/↓ to move, Enter to insert, Esc to leave the line as it was. On any other line Ctrl-R does what it did before (readline's search, fzf, atuin, ...).
531
+ Ctrl-R on an mssh line opens a menu below the prompt with the candidates for the word under the cursor (hosts, commands for those hosts, option values), how often and how long ago you used them; Tab switches to whole past invocations, the ones mssh's history holds ([limitations](docs/limitations.md#the-ctrl-r-history-tab-holds-msshs-history-only)). Type or paste to filter, ↑/↓ to move, Enter to insert, Esc to leave the line as it was. On any other line Ctrl-R does what it did before (readline's search, fzf, atuin, ...).
532
532
 
533
533
  A paste only filters, nothing in it runs, whether or not the terminal marks it as pasted (bracketed paste). Unmarked pastes are told by timing: a line break with another one less than 50 ms behind it was pasted. So Enter pressed twice within 50 ms, or Enter and the next line's keys arriving together over a laggy link, filter instead of choosing (press Enter again). Other keys typed right after Enter follow the choice, as typed.
534
534
 
535
- In plain bash, mssh takes over Ctrl-R only when it can call the old binding on other lines. It leaves Ctrl-R alone when Ctrl-R is unbound, and on bash 4.2 when Ctrl-R runs a shell command bound with `bind -x` (fzf's key bindings do this), as bash 4.2 cannot list such bindings. Ctrl-R then does what it did before on every line, mssh lines included: there is no menu, and without the line editor (bash 4.4 or later) the warnings are not shown either. TAB completion works as usual. On bash 4.2, Ctrl-R bound to readline's search or to a macro is taken over as on newer versions.
535
+ In plain bash, mssh takes over Ctrl-R only when it can call the old binding on other lines. It leaves Ctrl-R alone when Ctrl-R is unbound, and on bash 4.2 when Ctrl-R runs a shell command bound with `bind -x` (fzf's key bindings do this), as bash 4.2 cannot list such bindings. Ctrl-R then does what it did before on every line, mssh lines included: there is no menu, and without the line editor (bash 4.4 or later) the warnings are not shown either. TAB completion works as usual. On bash 4.2, Ctrl-R bound to readline's search or to a macro is taken over as on newer versions. On bash 4.2 and 4.3 the menu is drawn without your prompt beside it ([limitations](docs/limitations.md#no-prompt-beside-the-ctrl-r-menu-on-bash-42-and-43)).
536
536
 
537
537
  ### Warnings before Enter
538
538
 
539
539
  While you type an mssh line, words already typed that mssh would trip over are marked, and one line under the prompt says why. Nothing is ever blocked: Enter runs what you typed.
540
540
 
541
541
  - **Unknown host:** a name mssh would reject (no IP, alias or hosts-file entry, not in DNS, not mapped by your ssh config), with the host you probably meant: `gpu017: unknown host — did you mean gpu17?`
542
- - **Range surprises:** a range or alias with members mssh would reject: `node[1-16]: 9 of 16 hosts unknown (node1, node2, node3, …) — did you mean node[01-16]?`. Braces the shell expands make several words of the hosts, and mssh runs the second as the command: `node{1,2}: the shell makes 2 words of this; mssh would run node2 as the command — did you mean node1,node2?`
542
+ - **Range surprises:** a range or alias with members mssh would reject: `node[1-16]: 9 of 16 hosts unknown (node1, node2, node3, …) — did you mean node[01-16]?`. Past 4,096 names, a name a range makes twice is counted twice ([limitations](docs/limitations.md#large-ranges-whose-names-repeat-are-counted-with-their-repeats)). Braces the shell expands make several words of the hosts, and mssh runs the second as the command: `node{1,2}: the shell makes 2 words of this; mssh would run node2 as the command — did you mean node1,node2?`
543
543
  - **Unreachable host:** one that failed in your recent mssh runs (`will be skipped` with `-su`), or that answered ping recently and stopped. Like mssh, the warning tells `root@node05` (typed so, or with `-u root`) apart from `node05`.
544
544
  - **Conflicting options:** two options mssh refuses together, an option given twice, or one that does nothing on this line (`-iu does nothing: only used with --ipmi`).
545
545
  - **Options inside an unquoted remote command:** mssh reads its options anywhere on the line, so a word of an unquoted remote command that names an mssh option is that option, and any other word starting with `-` stops mssh with an error: `-u is mssh's --username here — quote the command: 'journalctl -u slurmd'`. Options after a quoted command (`mssh web1 'uptime' -q`) are mssh's, as meant, and are not marked. Unlike other warnings, this one also marks the word you are typing when it is the line's last word and already an mssh option (`mssh all df -h`), as Enter would run the line as it stands.
546
546
 
547
- Hosts the shell rewrites before mssh runs (`$NODES`, `$(cat hosts.txt)`, `` `...` ``, `~`, `!$`) are not checked: mssh gets what the shell makes of them. In single quotes or after a backslash they reach mssh as typed and are checked as mssh reads them.
547
+ A hosts word the shell may rewrite before mssh runs is not checked, the hosts beside the rewritten part included (`$NODES`, `$NODES,gpu017`, `$USER@gpu017`, `$(cat hosts.txt)`, `` `...` ``, `~`, `!$`, fish's `(...)`, braces in a word with quotes, and such words in single quotes too): mssh gets what the shell makes of them, or something the check cannot tell. The brace warning covers lists and number sequences; letter sequences (`n{a..c}`) and steps (`n{1..10..3}`) get none ([limitations](docs/limitations.md#a-hosts-word-holding-a-shell-expansion-is-not-checked)).
548
548
 
549
549
  zsh, bash with ble.sh and the plain-bash line editor mark the words and show the reason; fish 4 prints the reason under the line after a space; fish 3 and plain bash without the editor list the warnings at the top of the Ctrl-R menu.
550
550
 
551
551
  With [zsh-syntax-highlighting](https://github.com/zsh-users/zsh-syntax-highlighting) before zsh 5.9 (or before its 0.8), source it last in `.zshrc`, as it asks: loaded later (a plugin manager's deferred loading), it removes the marks and the grey text's colour at every key.
552
552
 
553
- Hosts are checked in the background by `mssh-complete probe`: DNS, `ssh -G` (which prints what your ssh config says about the name) and ping. It never connects or logs in to a host itself, so it cannot trip fail2ban. One exception comes from your ssh config: `ssh -G` runs the commands of `Match exec` lines, so a `Match exec` command that contacts the host (such as `nc -z %h 22` to choose a jump host) runs for every host name you type, before Enter. Check `~/.ssh/config` and `/etc/ssh/ssh_config` (and the files they `Include`) if fail2ban matters. The prober runs at low priority, as many checks at once as mssh's own `max_connections` allows. What it learns lives in mssh's runtime directory (`$XDG_RUNTIME_DIR/multiSSH3`, else `/run/user/<uid>/multiSSH3`, `/dev/shm/multiSSH3-<uid>` or the temp dir) and is gone after a reboot. It remembers when up to 10 000 hosts last answered ping, for a day (longer when `MSSH_COMPLETE_PING_TTL` or mssh's unreachable host expiry is longer); `-uhe` above that on the line finds no older answers. `mssh-complete eval --diagnostics` replays your history and lists the hosts mssh would reject.
553
+ Hosts are checked in the background by `mssh-complete probe`: DNS, `ssh -G` (which prints what your ssh config says about the name) and ping. It never connects or logs in to a host itself, so it cannot trip fail2ban. One exception comes from your ssh config: `ssh -G` runs the commands of `Match exec` lines, so a `Match exec` command that contacts the host (such as `nc -z %h 22` to choose a jump host) runs for every host name you type, before Enter. Check `~/.ssh/config` and `/etc/ssh/ssh_config` (and the files they `Include`) if fail2ban matters. The prober runs at low priority, as many checks at once as mssh's own `max_connections` allows. What it learns lives in mssh's runtime directory (`$XDG_RUNTIME_DIR/multiSSH3`, else `/run/user/<uid>/multiSSH3`, `/dev/shm/multiSSH3-<uid>` or the temp dir) and is gone after a reboot. It remembers when up to 10 000 hosts last answered ping, for a day (longer when `MSSH_COMPLETE_PING_TTL` or mssh's unreachable host expiry is longer); `-uhe` above that on the line finds no older answers. From a source checkout, `python3 tests/replay.py --diagnostics` replays your history and lists the hosts mssh would reject.
554
554
 
555
555
  | Environment variable | Effect |
556
556
  | --- | --- |
@@ -562,14 +562,16 @@ Hosts are checked in the background by `mssh-complete probe`: DNS, `ssh -G` (whi
562
562
  | `MSSH_COMPLETE_PING_TTL` | Seconds a host that answered ping is expected to keep answering (default: mssh's unreachable host expiry, 600) |
563
563
  | `MSSH_COMPLETE_DEBUG` | `1` logs errors to `complete.log` in the cache directory |
564
564
 
565
- > Tested with bash 4.2 to 5.3, zsh 5.5 to 5.9 (with `compinit` loaded; zsh-autosuggestions 0.7.1; zsh-syntax-highlighting 0.7.1 and 0.8.0), fish 3.3 to 4.0 and the 2026-09-08 ble.sh nightly. Since 6.28 mssh records hosts in its history as typed; older history lines hold them expanded.
565
+ > Tested with bash 4.2 to 5.3, zsh 5.5 to 5.9 (with `compinit` loaded; zsh-autosuggestions 0.7.1; zsh-syntax-highlighting 0.7.1 and 0.8.0), fish 3.3 to 4.0 and the 2026-09-08 ble.sh nightly. On fish 2, install succeeds but the glue registers nothing ([limitations](docs/limitations.md#install-on-fish-2-succeeds-and-does-nothing)). Since 6.28 mssh records hosts in its history as typed; older history lines hold them expanded.
566
566
 
567
- > After upgrading mssh, run `mssh --completion install` again to get the new shell glue. Glue from an older mssh keeps working, without the newer features.
567
+ > After upgrading mssh, run `mssh --completion install` again to get the new shell glue. Glue from an older mssh keeps working, without the newer features; glue from before 6.37 (bash) or 6.38 (zsh, fish) does not tell the completer your locale, so the Ctrl-R menu and the warnings are drawn in ASCII until you do ([limitations](docs/limitations.md#older-glue-files-draw-the-ctrl-r-menu-and-warnings-in-ascii)).
568
568
 
569
569
  > The plain-bash editor binds the space key, so every space typed in bash runs a tiny shell function; Ctrl-R becomes a key macro (the binding it replaced is called for other lines). Uninstall restores both in new shells. Such a function also fires bash's DEBUG trap, which some tools take for the start of a command: a command timer kept with the DEBUG trap (as some prompts keep theirs) counts from the first space typed instead of from Enter, and the preexec hooks of [bash-preexec](https://github.com/rcaloras/bash-preexec) (used by atuin and iTerm2's shell integration) may run at that space, before the line is complete.
570
570
 
571
571
  > In plain bash (without ble.sh), Ctrl-R and the line editor take their keys at the first prompt, through `PROMPT_COMMAND`. Where `PROMPT_COMMAND` is read-only (some audit setups on shared clusters make it so), they are left out without a message; TAB completion still works.
572
572
 
573
+ > The bash glue runs with `nocasematch` off and gives you your setting back when it returns; with `shopt -s nocasematch` on, a Ctrl-C during a TAB can leave it off ([limitations](docs/limitations.md#with-nocasematch-on-in-bash-a-ctrl-c-during-a-tab-can-leave-it-off)). Under `bind TAB:menu-complete`, mssh's words beside the words of a command like git (which asks for no space) go in without a space ([limitations](docs/limitations.md#under-menu-complete-msshs-words-beside-a-nospace-commands-mixed-words-get-no-space)).
574
+
573
575
  ## Options
574
576
 
575
577
  Here details the options for multiSSH3 6.31 @ 2026-10-13
@@ -631,7 +633,7 @@ mssh -ea="-o ProxyCommand='ssh -W %h:%p bastion'" allServers uptime
631
633
  - rsync connects with the same ssh program and options as mssh's other commands, `-k` identity file included. A host given as an IPv6 address is written in brackets for rsync and scp (`root@[fd00::5]:/dest/`).
632
634
  - As with rsync, a directory given with a trailing slash (`./data/`) copies its contents into the destination, one without (`./data`) copies the directory itself into it.
633
635
  - A source that is a symbolic link to a file is copied as that file, under the link's name, as scp does (rsync's `--copy-links`). Links inside a directory stay links, so a link is copied as a link, with a warning before the copy, when it points to a directory or to nothing, or when a directory is copied in the same run (`--copy-links` would follow the links inside it too).
634
- - A host where rsync could not run (rsync is not installed there or could not start, an rsync turned down mssh's options as macOS's rsync 2.6.9 does, or ssh failed to connect) is retried with scp, laying the files out as rsync would have: scp copies the entries of a `./data/` directory, and a lone directory goes inside the destination directory (scp fails if that directory does not exist, instead of creating it). Not retried: an empty `./data/` (scp has nothing to copy), a `./data/` with more entries than scp's command line holds (about 1 MB of names) and, in gather mode, a remote `dir/` source going into a local directory that exists or with other sources (scp would copy the directory itself into it).
636
+ - A host where rsync could not run (rsync is not installed there or could not start, an rsync turned down mssh's options as macOS's rsync 2.6.9 does, or ssh failed to connect) is retried with scp, laying the files out as rsync would have: scp copies the entries of a `./data/` directory, and a lone directory goes inside the destination directory (scp fails if that directory does not exist, instead of creating it). Not retried: an empty `./data/` (scp has nothing to copy), a `./data/` with more entries than scp's command line holds (about 1 MB of names) and, in gather mode, a remote `dir/` source going into a local directory that exists or with other sources (scp would copy the directory itself into it). A gather retried with scp of a remote directory named without a slash, into a local destination that does not exist, puts its files in the destination itself, where rsync puts the directory inside it ([limitations](docs/limitations.md#a-gather-retried-with-scp-puts-a-directory-named-without-a-slash-one-level-up)).
635
637
  - rsync keeps extended attributes (`-X`). Where the destination cannot keep them (NFSv3, vfat, ...), rsync copies the files and fails with 23; mssh then runs rsync once more without `-X`, a quick pass that ends with the copy's own result, and warns that the attributes were not copied.
636
638
  - Any other rsync failure, such as 23 (some files were not transferred) or 24 (some files vanished), is the host's result, with rsync's return code.
637
639
  - rsync copies files as they are: permissions, times, owner and group (`-a`). Owner and group go by name: on a host that knows the same user name, the file belongs to that user whatever its uid there; only for a name the host does not know is the number kept, so a file of uid 1000 here belongs to whoever is uid 1000 there. Owners are only set when the receiving side is root: when mssh logs in to the hosts as root (with `-G`, when mssh runs as root here). Logged in as another user, the files belong to that user, keeping only groups that user is in. scp (`-W`, and the retry above) keeps no owners: new files belong to the user it logs in as.
@@ -801,7 +803,7 @@ mssh -f ./data/ allServers /tmp/data/
801
803
  - Note: mssh runs the hosts on at most this many threads, however many hosts there are (with `--ipmi`, one thread per host: a host waiting out an IPMI lockout runs nothing). Each connection also needs its ssh process and two threads reading its output, about 4 processes / threads in all. Under a process / thread limit (`ulimit -u`, a container's pids limit) too low for that, mssh starts fewer threads, and a host that cannot start its ssh or threads waits for the running commands to end (`Warning: Too many processes or threads. retrying...`): the hosts run fewer at a time.
802
804
  - You will observe `Warning: The system allows no more threads (ulimit -u, pids limit), running the hosts at most N at a time.` when it does.
803
805
  - Behind a bastion (`ssh -J`, or `ProxyJump` in `~/.ssh/config`), every connection goes through the bastion's sshd, and its `MaxStartups` (default `10:30:100`) turns away connections that have not logged in yet once more than 10 are opening: 30% of new ones, rising to all of them at 100. mssh opens up to `-m` connections at once, so with the default some hosts fail with `kex_exchange_identification: read: Connection reset by peer` / `Connection closed by UNKNOWN port 65535` (exit code 255). The same can happen on a host many clients connect to at once.
804
- - mssh tries such a host again, up to 3 times, 0.2 to 3 s later, when ssh's lines show the connection was dropped before login: they name the address ssh connected to, or `UNKNOWN port 65535` through a `ProxyJump` / `ProxyCommand`, with nothing else printed. A command that may have started is never run again. Older OpenSSH, such as 8.0 on RHEL 8, names no address, so its ssh and scp runs are not tried again; rsync runs are, as they received nothing and only sync again.
806
+ - mssh tries such a host again, up to 3 times, 0.2 to 3 s later, when ssh's lines show the connection was dropped before login (`kex_exchange_identification: ...`, `Connection closed by ... port ...`; a reset counts only after the `kex_exchange_identification` line, as a reset reads the same after login) and nothing else was printed, on stdout or stderr. OpenSSH 8.0 (RHEL 8) is covered. A command that may have started is not run again, unless it printed nothing and its own ssh to another host was turned away the same way ([limitations](docs/limitations.md#a-remote-commands-own-ssh-turned-away-before-login-is-run-again)). rsync runs are tried again when they received nothing, as they only sync again.
805
807
  - When hosts still fail this way, a hint after the summary says how many and why. Use `-m 10` (or fewer) for runs through a bastion, or raise `MaxStartups` on it.
806
808
 
807
809
  ### `-j, --json` | _`DEFAULT_JSON_OUTPUT`_
@@ -828,7 +830,7 @@ mssh -f ./data/ allServers /tmp/data/
828
830
  - By default mssh set this to true to speed up operations on large host lists with some unreachable hosts.
829
831
  - Unreachable hosts will be tried again when their timeout expires.
830
832
  - 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.
831
- - 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.
833
+ - 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. Reading sets that directory up as writing does (mode 0700, created when missing), so the two cannot disagree ([limitations](docs/limitations.md#reading-msshs-runtime-state-may-create-its-directory)). If none can be used, they are only kept for the current run.
832
834
  - 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.
833
835
 
834
836
  ### `-a, -nsu, --no_skip_unreachable` | not _`DEFAULT_SKIP_UNREACHABLE`_
@@ -869,7 +871,7 @@ mssh -f ./data/ allServers /tmp/data/
869
871
  ### `-ci, --copy_id` | False
870
872
  - `copy_id` mode, use `ssh-copy-id` to copy public key to the hosts.
871
873
  - Will use the identity file if specified in `-k, --key, --identity [KEY]` as the key to install.
872
- - Will respect `-u, --username` and `-p, --password` options for username and password. With `-p` and `sshpass`, the hosts get the key at once; without them, `ssh-copy-id` asks for the password on the terminal, one host after the other, and Ctrl-C stops before the next host.
874
+ - Will respect `-u, --username` and `-p, --password` options for username and password. With `-p` and `sshpass`, the hosts get the key at once; without them, `ssh-copy-id` asks for the password on the terminal, one host after the other, and Ctrl-C stops before the next host. With `-p`, a Ctrl-C while ssh still connects can leave an empty `~/.ssh/ssh-copy-id.XXXXXXXXXX` behind (`rmdir ~/.ssh/ssh-copy-id.*`; [limitations](docs/limitations.md#an-interrupted---copy_id--p-can-leave-an-empty-sshssh-copy-idxxxxxxxxxx)).
873
875
  - `ssh-copy-id` checks host keys as every ssh mssh runs does: unless `SSH_STRICT_HOST_KEY_CHECKING` is true, it gets `-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null`, so new nodes, not yet in `known_hosts`, need no answer to a host key question (with `-p`, sshpass cannot give one). With `SSH_STRICT_HOST_KEY_CHECKING` true, it checks them as your ssh config says.
874
876
  - From `-ea, --extraargs` it gets what every `ssh-copy-id` takes (that of OpenSSH 7.4 on CentOS 7 and later): `-o` and `-p`, and ssh's `-J` as `-o ProxyJump=`. The other words are left out: ssh's `-i` (the `-i` of `ssh-copy-id` names the key to install), `-F` (the `ssh-copy-id` of OpenSSH 8.0 and older fails on it) and rsync's options.
875
877
  - A host `ssh-copy-id` failed on is a failed host: it counts towards the exit code and is listed in `failed_hosts`, also when a command runs after the copy. Without a command, mssh exits with 127 if it finds no `ssh-copy-id`.
@@ -976,7 +978,7 @@ Option names count in full only: `--user` is not `--username` and `-c` is not `-
976
978
 
977
979
  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).
978
980
 
979
- Below is a sample help message output from multiSSH3 6.37 @ 2026-10-21 (no config file, 120 columns; the lines after the options list the programs found on that machine)
981
+ Below is a sample help message output from multiSSH3 6.39 @ 2026-10-29 (no config file, 120 columns; the lines after the options list the programs found on that machine)
980
982
 
981
983
  ```bash
982
984
  $ mssh -h
@@ -1259,7 +1261,7 @@ DEFAULT_FILE_SYNC = False
1259
1261
  DEFAULT_TIMEOUT = 50
1260
1262
  DEFAULT_CLI_TIMEOUT = 0
1261
1263
  # seconds after which ssh gives up on a host that does not answer (ConnectTimeout) or stops answering
1262
- # (ServerAliveInterval x ServerAliveCountMax), unless -ea or the ssh config set them; 0 leaves them to ssh
1264
+ # (ServerAliveInterval x ServerAliveCountMax), after -ea's options so an -o there wins; 0 leaves one to ssh
1263
1265
  DEFAULT_SSH_CONNECT_TIMEOUT = 30
1264
1266
  DEFAULT_SSH_SERVER_ALIVE_INTERVAL = 15
1265
1267
  DEFAULT_SSH_SERVER_ALIVE_COUNT_MAX = 4
@@ -1387,3 +1389,40 @@ __DEBUG_MODE = False
1387
1389
  > When ssh cannot connect to a host (it exits with 255 after `ssh: connect to host HOST port PORT: REASON`), the default output shows the line as `SSH not reachable!`, `SSH connection timed out!` or `Cannot find host!` followed by the port and ssh's reason, e.g. `SSH connection timed out! (port 22: Connection timed out)`. Without the host name, unreachable hosts merge into one block. `--json` and `--greppable` output keep ssh's line, and a remote program's error that ends the same way is shown as printed.
1388
1390
 
1389
1391
  > `__DEBUG_MODE` is used to control whether to print host thread execution debug information to its stderr.
1392
+
1393
+ ## Upgrading to 6.39 from 6.38
1394
+
1395
+ A simplification pass: multiSSH3.py is about 740 lines shorter, mssh_complete.py about 1,470 (the history replay moved to `tests/replay.py`). What you may notice:
1396
+
1397
+ - ssh, scp, rsync and ssh-copy-id always get mssh's `ConnectTimeout`, `ServerAliveInterval` and `ServerAliveCountMax` (the `DEFAULT_SSH_*` settings), after the options of `-ea`, so an `-o` in `-ea` still wins. mssh no longer reads your ssh config for them: set the `DEFAULT_SSH_*` settings, or 0 to leave one to the ssh config.
1398
+ - A host dropped before login (sshd's `MaxStartups`) is tried again whatever address ssh's lines name, so OpenSSH 8.0 (RHEL 8) and its scp are now tried again too.
1399
+ - Output lines end only at `\n`. A line holding a form feed, a vertical tab or another Unicode line break stays one line; 6.38 split it there only when it reached 100, 200, ... bytes.
1400
+ - Hosts whose names do not compact into one range are listed in natural order (`db2,db10`), not sorted as text (`db10,db2`), in output headers, merged groups and `Skipping hosts:`.
1401
+ - Fix: a message mssh cannot write to stderr is dropped. 6.38 wrote it to stdout, so with `PYTHONUNBUFFERED=1` `mssh ... 2>&1 | true` exited 1 without running anything, and `-j` output could hold a stray line.
1402
+ - `--store_config_file`, its `.bak` and `--add_control_master_config` write through one atomic writer: root writing a user's config keeps its owner and cannot leave it half written, and the new config keeps the old one's group with the default answer `b` too, where it can be kept (6.38 gave it yours).
1403
+ - Under `-11` with `--error_only`, `--script` or `-Q`, the `Skipping unavailable host` line is no longer printed. A `__multiSSH3_UNAVAILABLE_HOSTS` key in an env file (written by 4.89) is no longer read.
1404
+ - Autocomplete: the bash completion of a remote command works as bash-completion completes `sudo CMD`; warnings skip hosts words the shell rewrites; history rows written by 6.26 and older are inserted with their commands in single quotes, so the local shell expands no `!`, `$` or backtick in them; the debug log (`MSSH_COMPLETE_DEBUG=1`) no longer holds the line typed in the editor.
1405
+ - Fix (zsh): `mssh HOSTS CMD ...<TAB>` no longer adds local file names beside the remote command's words when python-argcomplete (`#compdef -P *`) is installed.
1406
+ - New known limitations, each in [docs/limitations.md](docs/limitations.md):
1407
+ - timeouts set in your ssh config no longer apply to mssh's ssh; an `-ea` keepalive interval gets mssh's count;
1408
+ - a silent remote command whose own ssh is turned away before login runs again (up to 3 times);
1409
+ - a gather retried with scp, of a remote directory named without a slash into a new destination, lands one level up;
1410
+ - an interrupted `--copy_id -p` can leave empty `~/.ssh/ssh-copy-id.*` directories (`rmdir` them);
1411
+ - output before a run into a closed pipe ends by SIGPIPE: -13 for a Python subprocess caller (6.38: 141), also for a program calling `main()` itself, 120 on Windows;
1412
+ - another user's config, writable to you only through an ACL, is not written when you answer `o` or it is reached through a symlink (the config is printed);
1413
+ - reading mssh's runtime state may create its 0700 directory;
1414
+ - Esc waits while the terminal is under 3 rows;
1415
+ - glue files from before 6.37 (bash) or 6.38 (zsh, fish) draw the Ctrl-R menu and warnings in ASCII until reinstalled; fish with a UTF-8 locale that is named but not installed draws Unicode;
1416
+ - the Ctrl-R history tab holds mssh's history only (no `-I`, `--script` or `-hf` runs);
1417
+ - no prompt beside the Ctrl-R menu on bash 4.2 / 4.3;
1418
+ - ranges of more than 4,096 names count a repeated name twice;
1419
+ - a hosts word holding a shell expansion is not checked;
1420
+ - bash: a remote command's `-X`, `-P` and `-S` apply only to the words compgen makes; under `menu-complete`, mssh's words beside a nospace command's mixed words get no space; with `nocasematch` on, a Ctrl-C during a TAB can leave it off;
1421
+ - `mssh --completion install fish` on fish 2 succeeds and does nothing.
1422
+ - Changed for programs and scripts:
1423
+ - `mssh-complete eval` moved to the source tree: run `python3 tests/replay.py` (same options) from a checkout. `mssh-complete eval` now says so and exits 2.
1424
+ - The completion index is format 3. An older index is ignored and rebuilt on the first TAB; until the build is done, TAB offers options, delegation and shell history only; `mssh --completion install` builds it at once.
1425
+ - Run `mssh --completion install` again to get the new shell glue.
1426
+ - The index rebuild and the host prober hold a kernel lock (`flock`) on `complete-index.lock` and `probe.lock` while they run; the files stay as empty files and no lock is ever stale. A failed rebuild leaves `complete-index.failed`, and the next starts 60 s later. `mssh-complete rebuild --lock X` and `probe --lock X` are accepted and ignored.
1427
+ - Accepted and ignored, for callers written for 6.38: `hosts_only` of `compact_hostnames` / `expand_hostnames`, `create` of `runtime_dir` / `_unavailable_hosts_file_path`, and `mssh-complete edit --expand`.
1428
+ - `mssh_complete.expand_ranges` gives back whole, not half expanded, a text that mixes number and letter groups (`n[1-2]x[a-b]`), one with malformed brackets (`n[01-12][,-10]`) and one of several groups that expands to more than `limit` names (`a[1-3]b[1-9999]` with `limit=100`).
@@ -154,7 +154,9 @@ Defaults are read from the following chain map stored as json files, top ones ov
154
154
  >
155
155
  > `--store_config_file` writes the file readable by you only (`0600`), as it can hold passwords. The `/etc` locations above are written `0644` so every user can read them. An existing file's permissions are only ever tightened, and a file whose permissions cannot be restricted is left untouched (the config is printed instead).
156
156
  >
157
- > A config file that is a symlink (e.g. into a dotfiles repository) is written through the link; its backup (`.bak`, the default answer when the file exists) is a copy next to the link.
157
+ > A config file that is a symlink (e.g. into a dotfiles repository) is written through the link; its backup (`.bak`, the default answer when the file exists) is a copy next to the link. Under `sudo`, root follows only its own links: with the default `b` the link is moved to the `.bak` and replaced by a regular file; with `o` the config is printed and the link is kept ([limitations](docs/limitations.md#under-sudo---store_config_file-replaces-a-symlinked-config-instead-of-writing-through-it)).
158
+ >
159
+ > The file is replaced in one step: a temporary file beside it, with the old file's owner and group where they can be kept, is renamed over it, so with `o` a full disk leaves the old one; with the default `b` the old one is the `.bak`. With `o` (overwrite), and for a config reached through a symlink, a file of another user is written only when its owner can be kept (as root); otherwise the config is printed instead, also when you may write the file through an ACL ([limitations](docs/limitations.md#another-users-config-shared-through-an-acl-is-not-written)). With the default `b`, a config that is not a symlink is moved to the `.bak` and the new one keeps its owner and group where that can be kept (the owner only as root; the group as anyone, for a file of yours); otherwise it is yours.
158
160
  >
159
161
  > Each value is checked against the type of the setting's default. Values whose meaning is clear are converted (`"true"` / `"false"`, `"yes"` / `"no"` for true / false settings, numbers written as strings, a list of host names for `DEFAULT_HOSTS`); anything else, such as `"DEFAULT_TIMEOUT": "fifty"` or an encoding Python does not know, is ignored with a warning naming the file and the key, and the setting keeps its value from the rest of the chain (no warning when a more specific file sets that key anyway). `null` for a string setting means its default (`DEFAULT_HOSTS: null` is `all`). A file that is not a JSON object is skipped the same way. `--config_file` applies a file exactly as the chain does, `_sshPath`, `DEFAULT_ENV_FILE` and `ERROR_MESSAGES_TO_IGNORE` included.
160
162
 
@@ -392,7 +394,7 @@ The output is printed when the commands finish, so it can be piped like any comm
392
394
  ```bash
393
395
  mssh all 'dmesg' | less
394
396
  ```
395
- If the reader exits before reading all of it (`| head`, or `q` in `less`), mssh stops quietly as other command line tools do: no further commands or `--repeat` runs start, and the exit code still counts the hosts that failed in what ran. If that happens before anything has run (the output of `-h`, `-V`, a usage error or `--generate_config_file`), the exit code is 141, as for other tools SIGPIPE ends.
397
+ If the reader exits before reading all of it (`| head`, or `q` in `less`), mssh stops quietly as other command line tools do: no further commands or `--repeat` runs start, and the exit code still counts the hosts that failed in what ran. If that happens before anything has run (the output of `-h`, `-V`, a usage error or `--generate_config_file`), SIGPIPE ends mssh as it ends other tools: the shell reports 141, a program that starts mssh with Python's subprocess sees -13, and on Windows the exit code is 120 ([limitations](docs/limitations.md#output-before-a-run-into-a-closed-pipe-ends-mssh-by-sigpipe)).
396
398
 
397
399
  ### Curses Window Size Control
398
400
 
@@ -418,7 +420,7 @@ While leaving minimum 40 characters / 1 line for each host display by default. Y
418
420
  > Home/End: Jump cursor
419
421
  > Esc : Clear / hide view
420
422
  > ```
421
- > `Esc` clears the input line. On an empty input line it hides the live view; the commands keep running and their output is printed when they finish.
423
+ > `Esc` clears the input line. On an empty input line it hides the live view; the commands keep running and their output is printed when they finish. While the terminal is under 3 rows the display shows "Terminal too small" and Esc waits until it is bigger ([limitations](docs/limitations.md#esc-does-nothing-while-the-terminal-is-under-3-rows)).
422
424
  > The live view leaves out ssh's own notices, as the output at the end does ( `ERROR_MESSAGES_TO_IGNORE` ): mssh does not keep the hosts' keys, so ssh says `Warning: Permanently added ...` on every connection, and a quiet command's window showed only that.
423
425
  > You can also toggle this help in curses by pressing `?` or `F1`.
424
426
  > The one-character keys (`?`, `_`, `+`, `{`, `}`, `<`, `>`, `|`) act only on an empty input line; after you have typed something they are typed like any other character, so a `?` can still reach the hosts. `F1` toggles the help at any time.
@@ -450,9 +452,7 @@ mssh has ssh, scp, rsync's ssh and ssh-copy-id give up on a host that takes the
450
452
 
451
453
  Change the values with `DEFAULT_SSH_CONNECT_TIMEOUT`, `DEFAULT_SSH_SERVER_ALIVE_INTERVAL` and `DEFAULT_SSH_SERVER_ALIVE_COUNT_MAX` in the config file; 0 leaves the option to ssh (without a keepalive interval, mssh does not pass its count either).
452
454
 
453
- Your own values win. ssh uses the first value it gets and a command-line `-o` beats the ssh config, so mssh leaves an option out when you give it:
454
- - with `-ea` for ssh or scp, e.g. `-ea '-o ConnectTimeout=120'` (with `-ea '-F FILE'`, mssh reads that ssh config instead of the two below);
455
- - in your ssh config, `~/.ssh/config` or `/etc/ssh/ssh_config` and the files they `Include`, also for single hosts (`Host slow*` / `ConnectTimeout 120`). Only when these files mention one of the three options does mssh ask `ssh -G` about each host whether it sets it, which runs the config's `Match exec` lines once more. `ConnectTimeout 0` there means no timeout for those hosts; `ServerAliveInterval 0` cannot be told from not setting it.
455
+ Your own `-ea` values win: ssh uses the first value it gets, and mssh puts its options after the ones in `-ea`, so `-ea '-o ConnectTimeout=120'` sets 120. An `-ea` keepalive interval still gets mssh's count (`-ea '-o ServerAliveInterval=60'`: 4 x 60 s). Options in your ssh config (`~/.ssh/config`, `/etc/ssh/ssh_config`, or a file given with `-ea '-F FILE'`) do not win: a command-line `-o` beats the ssh config, and mssh's are command-line options. Set the `DEFAULT_SSH_*` values instead, or 0 to leave an option to your ssh config ([limitations](docs/limitations.md#msshs-ssh-timeouts-override-the-same-options-in-the-ssh-config)).
456
456
 
457
457
  A shared connection (`ControlMaster`, see [SSH ControlMaster Configuration](#ssh-controlmaster-configuration)) does the keepalive of the ssh that opened it. One mssh opened gives up on a host that went away, and the next ssh connects anew (where `ConnectTimeout` applies). One that a plain `ssh` without `ServerAliveInterval` in your ssh config opened does not notice: commands through it wait until the kernel gives up on the connection, which takes about 15 minutes on Linux. Setting `ServerAliveInterval` in your ssh config covers those too.
458
458
 
@@ -472,7 +472,7 @@ A new block is appended to your rc file; changing or removing it replaces the fi
472
472
 
473
473
  - **Hosts:** host strings you typed before, env file aliases, `/etc/hosts`, ssh config `Host` entries, `known_hosts` and the ARP table. Large clusters are offered as ranges (`node[0001-2000]`) until what you typed narrows them down. Hosts mssh recently found unreachable come last (with the user this line gives them: `root@node05` and `node05` are told apart).
474
474
  - **Options and values:** option names, and values you used before (`-u`, `-t`, ...). Paths complete as files. Passwords are never completed or shown, the ones in your config file included.
475
- - **Commands:** commands from your mssh history, the rest of a command from this machine's own completion (`mssh grp systemctl rest<TAB>`), and the magic strings (`#HOST#`, ...). A command typed as separate words gets the next words of commands you ran, with the quotes they ran with (`'ssh*'` comes as `\'ssh\*\'`, so the remote shell gets the same command), never a word holding a space: mssh runs each word as its own command when one of them holds a space, so type such a command in quotes (`mssh grp "grep 'connection refused' /var/log/syslog"`).
475
+ - **Commands:** commands from your mssh history, the rest of a command from this machine's own completion (`mssh grp systemctl rest<TAB>`; in bash as bash-completion completes `sudo CMD`, with a few gaps in [limitations](docs/limitations.md#autocomplete)), and the magic strings (`#HOST#`, ...). A command typed as separate words gets the next words of commands you ran, with the quotes they ran with (`'ssh*'` comes as `\'ssh\*\'`, so the remote shell gets the same command), never a word holding a space: mssh runs each word as its own command when one of them holds a space, so type such a command in quotes (`mssh grp "grep 'connection refused' /var/log/syslog"`).
476
476
 
477
477
  Ranking uses the rest of the line on both sides of the cursor: after `mssh gpu[01-16] "` the commands you run on those hosts come first, and in the hosts of `mssh ▮ "systemctl restart slurmd"` the hosts you restarted slurmd on come first.
478
478
 
@@ -482,7 +482,7 @@ In bash with readline's `completion-ignore-case` on (`set completion-ignore-case
482
482
 
483
483
  In zsh, TAB needs zsh's completion system: `compinit` has to run when zsh starts, before or after the mssh block in `~/.zshrc` (oh-my-zsh, prezto and the like run it, and so does Ubuntu's `/etc/zsh/zshrc`). `mssh --completion install` says so when `~/.zshrc` does not seem to start it. Grey suggestions, warnings and Ctrl-R work without it.
484
484
 
485
- It learns from an index of your history and hosts, kept in mssh's private runtime directory (`$XDG_RUNTIME_DIR/multiSSH3`, else `/run/user/<uid>/multiSSH3`, `/dev/shm/multiSSH3-<uid>` or `multiSSH3-<uid>` in the temp dir) and rebuilt in the background when its sources change. `mssh --completion install` builds it right away. A reboot clears it: the first TAB after one starts the build and, until the index is ready (about a second for a long history), offers options and what your shell history holds. `mssh-complete rebuild` rebuilds it now; `mssh-complete eval` replays your history and shows how much typing completion saves.
485
+ It learns from an index of your history and hosts, kept in mssh's private runtime directory (`$XDG_RUNTIME_DIR/multiSSH3`, else `/run/user/<uid>/multiSSH3`, `/dev/shm/multiSSH3-<uid>` or `multiSSH3-<uid>` in the temp dir) and rebuilt in the background when its sources change. `mssh --completion install` builds it right away. A reboot clears it, and an upgrade that changes its format (6.39) makes it unreadable: the first TAB after either starts the build and, until the index is ready (about a second for a long history), offers options, the remote command's own completion and what your shell history holds. `mssh-complete rebuild` rebuilds it now. From a source checkout, `python3 tests/replay.py` replays your history and shows how much typing completion saves.
486
486
 
487
487
  ### Grey suggestions
488
488
 
@@ -502,29 +502,29 @@ While you type an mssh line, the most likely rest of it shows in grey after the
502
502
 
503
503
  ### Ctrl-R
504
504
 
505
- Ctrl-R on an mssh line opens a menu below the prompt with the candidates for the word under the cursor (hosts, commands for those hosts, option values), how often and how long ago you used them; Tab switches to whole past invocations. Type or paste to filter, ↑/↓ to move, Enter to insert, Esc to leave the line as it was. On any other line Ctrl-R does what it did before (readline's search, fzf, atuin, ...).
505
+ Ctrl-R on an mssh line opens a menu below the prompt with the candidates for the word under the cursor (hosts, commands for those hosts, option values), how often and how long ago you used them; Tab switches to whole past invocations, the ones mssh's history holds ([limitations](docs/limitations.md#the-ctrl-r-history-tab-holds-msshs-history-only)). Type or paste to filter, ↑/↓ to move, Enter to insert, Esc to leave the line as it was. On any other line Ctrl-R does what it did before (readline's search, fzf, atuin, ...).
506
506
 
507
507
  A paste only filters, nothing in it runs, whether or not the terminal marks it as pasted (bracketed paste). Unmarked pastes are told by timing: a line break with another one less than 50 ms behind it was pasted. So Enter pressed twice within 50 ms, or Enter and the next line's keys arriving together over a laggy link, filter instead of choosing (press Enter again). Other keys typed right after Enter follow the choice, as typed.
508
508
 
509
- In plain bash, mssh takes over Ctrl-R only when it can call the old binding on other lines. It leaves Ctrl-R alone when Ctrl-R is unbound, and on bash 4.2 when Ctrl-R runs a shell command bound with `bind -x` (fzf's key bindings do this), as bash 4.2 cannot list such bindings. Ctrl-R then does what it did before on every line, mssh lines included: there is no menu, and without the line editor (bash 4.4 or later) the warnings are not shown either. TAB completion works as usual. On bash 4.2, Ctrl-R bound to readline's search or to a macro is taken over as on newer versions.
509
+ In plain bash, mssh takes over Ctrl-R only when it can call the old binding on other lines. It leaves Ctrl-R alone when Ctrl-R is unbound, and on bash 4.2 when Ctrl-R runs a shell command bound with `bind -x` (fzf's key bindings do this), as bash 4.2 cannot list such bindings. Ctrl-R then does what it did before on every line, mssh lines included: there is no menu, and without the line editor (bash 4.4 or later) the warnings are not shown either. TAB completion works as usual. On bash 4.2, Ctrl-R bound to readline's search or to a macro is taken over as on newer versions. On bash 4.2 and 4.3 the menu is drawn without your prompt beside it ([limitations](docs/limitations.md#no-prompt-beside-the-ctrl-r-menu-on-bash-42-and-43)).
510
510
 
511
511
  ### Warnings before Enter
512
512
 
513
513
  While you type an mssh line, words already typed that mssh would trip over are marked, and one line under the prompt says why. Nothing is ever blocked: Enter runs what you typed.
514
514
 
515
515
  - **Unknown host:** a name mssh would reject (no IP, alias or hosts-file entry, not in DNS, not mapped by your ssh config), with the host you probably meant: `gpu017: unknown host — did you mean gpu17?`
516
- - **Range surprises:** a range or alias with members mssh would reject: `node[1-16]: 9 of 16 hosts unknown (node1, node2, node3, …) — did you mean node[01-16]?`. Braces the shell expands make several words of the hosts, and mssh runs the second as the command: `node{1,2}: the shell makes 2 words of this; mssh would run node2 as the command — did you mean node1,node2?`
516
+ - **Range surprises:** a range or alias with members mssh would reject: `node[1-16]: 9 of 16 hosts unknown (node1, node2, node3, …) — did you mean node[01-16]?`. Past 4,096 names, a name a range makes twice is counted twice ([limitations](docs/limitations.md#large-ranges-whose-names-repeat-are-counted-with-their-repeats)). Braces the shell expands make several words of the hosts, and mssh runs the second as the command: `node{1,2}: the shell makes 2 words of this; mssh would run node2 as the command — did you mean node1,node2?`
517
517
  - **Unreachable host:** one that failed in your recent mssh runs (`will be skipped` with `-su`), or that answered ping recently and stopped. Like mssh, the warning tells `root@node05` (typed so, or with `-u root`) apart from `node05`.
518
518
  - **Conflicting options:** two options mssh refuses together, an option given twice, or one that does nothing on this line (`-iu does nothing: only used with --ipmi`).
519
519
  - **Options inside an unquoted remote command:** mssh reads its options anywhere on the line, so a word of an unquoted remote command that names an mssh option is that option, and any other word starting with `-` stops mssh with an error: `-u is mssh's --username here — quote the command: 'journalctl -u slurmd'`. Options after a quoted command (`mssh web1 'uptime' -q`) are mssh's, as meant, and are not marked. Unlike other warnings, this one also marks the word you are typing when it is the line's last word and already an mssh option (`mssh all df -h`), as Enter would run the line as it stands.
520
520
 
521
- Hosts the shell rewrites before mssh runs (`$NODES`, `$(cat hosts.txt)`, `` `...` ``, `~`, `!$`) are not checked: mssh gets what the shell makes of them. In single quotes or after a backslash they reach mssh as typed and are checked as mssh reads them.
521
+ A hosts word the shell may rewrite before mssh runs is not checked, the hosts beside the rewritten part included (`$NODES`, `$NODES,gpu017`, `$USER@gpu017`, `$(cat hosts.txt)`, `` `...` ``, `~`, `!$`, fish's `(...)`, braces in a word with quotes, and such words in single quotes too): mssh gets what the shell makes of them, or something the check cannot tell. The brace warning covers lists and number sequences; letter sequences (`n{a..c}`) and steps (`n{1..10..3}`) get none ([limitations](docs/limitations.md#a-hosts-word-holding-a-shell-expansion-is-not-checked)).
522
522
 
523
523
  zsh, bash with ble.sh and the plain-bash line editor mark the words and show the reason; fish 4 prints the reason under the line after a space; fish 3 and plain bash without the editor list the warnings at the top of the Ctrl-R menu.
524
524
 
525
525
  With [zsh-syntax-highlighting](https://github.com/zsh-users/zsh-syntax-highlighting) before zsh 5.9 (or before its 0.8), source it last in `.zshrc`, as it asks: loaded later (a plugin manager's deferred loading), it removes the marks and the grey text's colour at every key.
526
526
 
527
- Hosts are checked in the background by `mssh-complete probe`: DNS, `ssh -G` (which prints what your ssh config says about the name) and ping. It never connects or logs in to a host itself, so it cannot trip fail2ban. One exception comes from your ssh config: `ssh -G` runs the commands of `Match exec` lines, so a `Match exec` command that contacts the host (such as `nc -z %h 22` to choose a jump host) runs for every host name you type, before Enter. Check `~/.ssh/config` and `/etc/ssh/ssh_config` (and the files they `Include`) if fail2ban matters. The prober runs at low priority, as many checks at once as mssh's own `max_connections` allows. What it learns lives in mssh's runtime directory (`$XDG_RUNTIME_DIR/multiSSH3`, else `/run/user/<uid>/multiSSH3`, `/dev/shm/multiSSH3-<uid>` or the temp dir) and is gone after a reboot. It remembers when up to 10 000 hosts last answered ping, for a day (longer when `MSSH_COMPLETE_PING_TTL` or mssh's unreachable host expiry is longer); `-uhe` above that on the line finds no older answers. `mssh-complete eval --diagnostics` replays your history and lists the hosts mssh would reject.
527
+ Hosts are checked in the background by `mssh-complete probe`: DNS, `ssh -G` (which prints what your ssh config says about the name) and ping. It never connects or logs in to a host itself, so it cannot trip fail2ban. One exception comes from your ssh config: `ssh -G` runs the commands of `Match exec` lines, so a `Match exec` command that contacts the host (such as `nc -z %h 22` to choose a jump host) runs for every host name you type, before Enter. Check `~/.ssh/config` and `/etc/ssh/ssh_config` (and the files they `Include`) if fail2ban matters. The prober runs at low priority, as many checks at once as mssh's own `max_connections` allows. What it learns lives in mssh's runtime directory (`$XDG_RUNTIME_DIR/multiSSH3`, else `/run/user/<uid>/multiSSH3`, `/dev/shm/multiSSH3-<uid>` or the temp dir) and is gone after a reboot. It remembers when up to 10 000 hosts last answered ping, for a day (longer when `MSSH_COMPLETE_PING_TTL` or mssh's unreachable host expiry is longer); `-uhe` above that on the line finds no older answers. From a source checkout, `python3 tests/replay.py --diagnostics` replays your history and lists the hosts mssh would reject.
528
528
 
529
529
  | Environment variable | Effect |
530
530
  | --- | --- |
@@ -536,14 +536,16 @@ Hosts are checked in the background by `mssh-complete probe`: DNS, `ssh -G` (whi
536
536
  | `MSSH_COMPLETE_PING_TTL` | Seconds a host that answered ping is expected to keep answering (default: mssh's unreachable host expiry, 600) |
537
537
  | `MSSH_COMPLETE_DEBUG` | `1` logs errors to `complete.log` in the cache directory |
538
538
 
539
- > Tested with bash 4.2 to 5.3, zsh 5.5 to 5.9 (with `compinit` loaded; zsh-autosuggestions 0.7.1; zsh-syntax-highlighting 0.7.1 and 0.8.0), fish 3.3 to 4.0 and the 2026-09-08 ble.sh nightly. Since 6.28 mssh records hosts in its history as typed; older history lines hold them expanded.
539
+ > Tested with bash 4.2 to 5.3, zsh 5.5 to 5.9 (with `compinit` loaded; zsh-autosuggestions 0.7.1; zsh-syntax-highlighting 0.7.1 and 0.8.0), fish 3.3 to 4.0 and the 2026-09-08 ble.sh nightly. On fish 2, install succeeds but the glue registers nothing ([limitations](docs/limitations.md#install-on-fish-2-succeeds-and-does-nothing)). Since 6.28 mssh records hosts in its history as typed; older history lines hold them expanded.
540
540
 
541
- > After upgrading mssh, run `mssh --completion install` again to get the new shell glue. Glue from an older mssh keeps working, without the newer features.
541
+ > After upgrading mssh, run `mssh --completion install` again to get the new shell glue. Glue from an older mssh keeps working, without the newer features; glue from before 6.37 (bash) or 6.38 (zsh, fish) does not tell the completer your locale, so the Ctrl-R menu and the warnings are drawn in ASCII until you do ([limitations](docs/limitations.md#older-glue-files-draw-the-ctrl-r-menu-and-warnings-in-ascii)).
542
542
 
543
543
  > The plain-bash editor binds the space key, so every space typed in bash runs a tiny shell function; Ctrl-R becomes a key macro (the binding it replaced is called for other lines). Uninstall restores both in new shells. Such a function also fires bash's DEBUG trap, which some tools take for the start of a command: a command timer kept with the DEBUG trap (as some prompts keep theirs) counts from the first space typed instead of from Enter, and the preexec hooks of [bash-preexec](https://github.com/rcaloras/bash-preexec) (used by atuin and iTerm2's shell integration) may run at that space, before the line is complete.
544
544
 
545
545
  > In plain bash (without ble.sh), Ctrl-R and the line editor take their keys at the first prompt, through `PROMPT_COMMAND`. Where `PROMPT_COMMAND` is read-only (some audit setups on shared clusters make it so), they are left out without a message; TAB completion still works.
546
546
 
547
+ > The bash glue runs with `nocasematch` off and gives you your setting back when it returns; with `shopt -s nocasematch` on, a Ctrl-C during a TAB can leave it off ([limitations](docs/limitations.md#with-nocasematch-on-in-bash-a-ctrl-c-during-a-tab-can-leave-it-off)). Under `bind TAB:menu-complete`, mssh's words beside the words of a command like git (which asks for no space) go in without a space ([limitations](docs/limitations.md#under-menu-complete-msshs-words-beside-a-nospace-commands-mixed-words-get-no-space)).
548
+
547
549
  ## Options
548
550
 
549
551
  Here details the options for multiSSH3 6.31 @ 2026-10-13
@@ -605,7 +607,7 @@ mssh -ea="-o ProxyCommand='ssh -W %h:%p bastion'" allServers uptime
605
607
  - rsync connects with the same ssh program and options as mssh's other commands, `-k` identity file included. A host given as an IPv6 address is written in brackets for rsync and scp (`root@[fd00::5]:/dest/`).
606
608
  - As with rsync, a directory given with a trailing slash (`./data/`) copies its contents into the destination, one without (`./data`) copies the directory itself into it.
607
609
  - A source that is a symbolic link to a file is copied as that file, under the link's name, as scp does (rsync's `--copy-links`). Links inside a directory stay links, so a link is copied as a link, with a warning before the copy, when it points to a directory or to nothing, or when a directory is copied in the same run (`--copy-links` would follow the links inside it too).
608
- - A host where rsync could not run (rsync is not installed there or could not start, an rsync turned down mssh's options as macOS's rsync 2.6.9 does, or ssh failed to connect) is retried with scp, laying the files out as rsync would have: scp copies the entries of a `./data/` directory, and a lone directory goes inside the destination directory (scp fails if that directory does not exist, instead of creating it). Not retried: an empty `./data/` (scp has nothing to copy), a `./data/` with more entries than scp's command line holds (about 1 MB of names) and, in gather mode, a remote `dir/` source going into a local directory that exists or with other sources (scp would copy the directory itself into it).
610
+ - A host where rsync could not run (rsync is not installed there or could not start, an rsync turned down mssh's options as macOS's rsync 2.6.9 does, or ssh failed to connect) is retried with scp, laying the files out as rsync would have: scp copies the entries of a `./data/` directory, and a lone directory goes inside the destination directory (scp fails if that directory does not exist, instead of creating it). Not retried: an empty `./data/` (scp has nothing to copy), a `./data/` with more entries than scp's command line holds (about 1 MB of names) and, in gather mode, a remote `dir/` source going into a local directory that exists or with other sources (scp would copy the directory itself into it). A gather retried with scp of a remote directory named without a slash, into a local destination that does not exist, puts its files in the destination itself, where rsync puts the directory inside it ([limitations](docs/limitations.md#a-gather-retried-with-scp-puts-a-directory-named-without-a-slash-one-level-up)).
609
611
  - rsync keeps extended attributes (`-X`). Where the destination cannot keep them (NFSv3, vfat, ...), rsync copies the files and fails with 23; mssh then runs rsync once more without `-X`, a quick pass that ends with the copy's own result, and warns that the attributes were not copied.
610
612
  - Any other rsync failure, such as 23 (some files were not transferred) or 24 (some files vanished), is the host's result, with rsync's return code.
611
613
  - rsync copies files as they are: permissions, times, owner and group (`-a`). Owner and group go by name: on a host that knows the same user name, the file belongs to that user whatever its uid there; only for a name the host does not know is the number kept, so a file of uid 1000 here belongs to whoever is uid 1000 there. Owners are only set when the receiving side is root: when mssh logs in to the hosts as root (with `-G`, when mssh runs as root here). Logged in as another user, the files belong to that user, keeping only groups that user is in. scp (`-W`, and the retry above) keeps no owners: new files belong to the user it logs in as.
@@ -775,7 +777,7 @@ mssh -f ./data/ allServers /tmp/data/
775
777
  - Note: mssh runs the hosts on at most this many threads, however many hosts there are (with `--ipmi`, one thread per host: a host waiting out an IPMI lockout runs nothing). Each connection also needs its ssh process and two threads reading its output, about 4 processes / threads in all. Under a process / thread limit (`ulimit -u`, a container's pids limit) too low for that, mssh starts fewer threads, and a host that cannot start its ssh or threads waits for the running commands to end (`Warning: Too many processes or threads. retrying...`): the hosts run fewer at a time.
776
778
  - You will observe `Warning: The system allows no more threads (ulimit -u, pids limit), running the hosts at most N at a time.` when it does.
777
779
  - Behind a bastion (`ssh -J`, or `ProxyJump` in `~/.ssh/config`), every connection goes through the bastion's sshd, and its `MaxStartups` (default `10:30:100`) turns away connections that have not logged in yet once more than 10 are opening: 30% of new ones, rising to all of them at 100. mssh opens up to `-m` connections at once, so with the default some hosts fail with `kex_exchange_identification: read: Connection reset by peer` / `Connection closed by UNKNOWN port 65535` (exit code 255). The same can happen on a host many clients connect to at once.
778
- - mssh tries such a host again, up to 3 times, 0.2 to 3 s later, when ssh's lines show the connection was dropped before login: they name the address ssh connected to, or `UNKNOWN port 65535` through a `ProxyJump` / `ProxyCommand`, with nothing else printed. A command that may have started is never run again. Older OpenSSH, such as 8.0 on RHEL 8, names no address, so its ssh and scp runs are not tried again; rsync runs are, as they received nothing and only sync again.
780
+ - mssh tries such a host again, up to 3 times, 0.2 to 3 s later, when ssh's lines show the connection was dropped before login (`kex_exchange_identification: ...`, `Connection closed by ... port ...`; a reset counts only after the `kex_exchange_identification` line, as a reset reads the same after login) and nothing else was printed, on stdout or stderr. OpenSSH 8.0 (RHEL 8) is covered. A command that may have started is not run again, unless it printed nothing and its own ssh to another host was turned away the same way ([limitations](docs/limitations.md#a-remote-commands-own-ssh-turned-away-before-login-is-run-again)). rsync runs are tried again when they received nothing, as they only sync again.
779
781
  - When hosts still fail this way, a hint after the summary says how many and why. Use `-m 10` (or fewer) for runs through a bastion, or raise `MaxStartups` on it.
780
782
 
781
783
  ### `-j, --json` | _`DEFAULT_JSON_OUTPUT`_
@@ -802,7 +804,7 @@ mssh -f ./data/ allServers /tmp/data/
802
804
  - By default mssh set this to true to speed up operations on large host lists with some unreachable hosts.
803
805
  - Unreachable hosts will be tried again when their timeout expires.
804
806
  - 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.
805
- - 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.
807
+ - 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. Reading sets that directory up as writing does (mode 0700, created when missing), so the two cannot disagree ([limitations](docs/limitations.md#reading-msshs-runtime-state-may-create-its-directory)). If none can be used, they are only kept for the current run.
806
808
  - 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.
807
809
 
808
810
  ### `-a, -nsu, --no_skip_unreachable` | not _`DEFAULT_SKIP_UNREACHABLE`_
@@ -843,7 +845,7 @@ mssh -f ./data/ allServers /tmp/data/
843
845
  ### `-ci, --copy_id` | False
844
846
  - `copy_id` mode, use `ssh-copy-id` to copy public key to the hosts.
845
847
  - Will use the identity file if specified in `-k, --key, --identity [KEY]` as the key to install.
846
- - Will respect `-u, --username` and `-p, --password` options for username and password. With `-p` and `sshpass`, the hosts get the key at once; without them, `ssh-copy-id` asks for the password on the terminal, one host after the other, and Ctrl-C stops before the next host.
848
+ - Will respect `-u, --username` and `-p, --password` options for username and password. With `-p` and `sshpass`, the hosts get the key at once; without them, `ssh-copy-id` asks for the password on the terminal, one host after the other, and Ctrl-C stops before the next host. With `-p`, a Ctrl-C while ssh still connects can leave an empty `~/.ssh/ssh-copy-id.XXXXXXXXXX` behind (`rmdir ~/.ssh/ssh-copy-id.*`; [limitations](docs/limitations.md#an-interrupted---copy_id--p-can-leave-an-empty-sshssh-copy-idxxxxxxxxxx)).
847
849
  - `ssh-copy-id` checks host keys as every ssh mssh runs does: unless `SSH_STRICT_HOST_KEY_CHECKING` is true, it gets `-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null`, so new nodes, not yet in `known_hosts`, need no answer to a host key question (with `-p`, sshpass cannot give one). With `SSH_STRICT_HOST_KEY_CHECKING` true, it checks them as your ssh config says.
848
850
  - From `-ea, --extraargs` it gets what every `ssh-copy-id` takes (that of OpenSSH 7.4 on CentOS 7 and later): `-o` and `-p`, and ssh's `-J` as `-o ProxyJump=`. The other words are left out: ssh's `-i` (the `-i` of `ssh-copy-id` names the key to install), `-F` (the `ssh-copy-id` of OpenSSH 8.0 and older fails on it) and rsync's options.
849
851
  - A host `ssh-copy-id` failed on is a failed host: it counts towards the exit code and is listed in `failed_hosts`, also when a command runs after the copy. Without a command, mssh exits with 127 if it finds no `ssh-copy-id`.
@@ -950,7 +952,7 @@ Option names count in full only: `--user` is not `--username` and `-c` is not `-
950
952
 
951
953
  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).
952
954
 
953
- Below is a sample help message output from multiSSH3 6.37 @ 2026-10-21 (no config file, 120 columns; the lines after the options list the programs found on that machine)
955
+ Below is a sample help message output from multiSSH3 6.39 @ 2026-10-29 (no config file, 120 columns; the lines after the options list the programs found on that machine)
954
956
 
955
957
  ```bash
956
958
  $ mssh -h
@@ -1233,7 +1235,7 @@ DEFAULT_FILE_SYNC = False
1233
1235
  DEFAULT_TIMEOUT = 50
1234
1236
  DEFAULT_CLI_TIMEOUT = 0
1235
1237
  # seconds after which ssh gives up on a host that does not answer (ConnectTimeout) or stops answering
1236
- # (ServerAliveInterval x ServerAliveCountMax), unless -ea or the ssh config set them; 0 leaves them to ssh
1238
+ # (ServerAliveInterval x ServerAliveCountMax), after -ea's options so an -o there wins; 0 leaves one to ssh
1237
1239
  DEFAULT_SSH_CONNECT_TIMEOUT = 30
1238
1240
  DEFAULT_SSH_SERVER_ALIVE_INTERVAL = 15
1239
1241
  DEFAULT_SSH_SERVER_ALIVE_COUNT_MAX = 4
@@ -1361,3 +1363,40 @@ __DEBUG_MODE = False
1361
1363
  > When ssh cannot connect to a host (it exits with 255 after `ssh: connect to host HOST port PORT: REASON`), the default output shows the line as `SSH not reachable!`, `SSH connection timed out!` or `Cannot find host!` followed by the port and ssh's reason, e.g. `SSH connection timed out! (port 22: Connection timed out)`. Without the host name, unreachable hosts merge into one block. `--json` and `--greppable` output keep ssh's line, and a remote program's error that ends the same way is shown as printed.
1362
1364
 
1363
1365
  > `__DEBUG_MODE` is used to control whether to print host thread execution debug information to its stderr.
1366
+
1367
+ ## Upgrading to 6.39 from 6.38
1368
+
1369
+ A simplification pass: multiSSH3.py is about 740 lines shorter, mssh_complete.py about 1,470 (the history replay moved to `tests/replay.py`). What you may notice:
1370
+
1371
+ - ssh, scp, rsync and ssh-copy-id always get mssh's `ConnectTimeout`, `ServerAliveInterval` and `ServerAliveCountMax` (the `DEFAULT_SSH_*` settings), after the options of `-ea`, so an `-o` in `-ea` still wins. mssh no longer reads your ssh config for them: set the `DEFAULT_SSH_*` settings, or 0 to leave one to the ssh config.
1372
+ - A host dropped before login (sshd's `MaxStartups`) is tried again whatever address ssh's lines name, so OpenSSH 8.0 (RHEL 8) and its scp are now tried again too.
1373
+ - Output lines end only at `\n`. A line holding a form feed, a vertical tab or another Unicode line break stays one line; 6.38 split it there only when it reached 100, 200, ... bytes.
1374
+ - Hosts whose names do not compact into one range are listed in natural order (`db2,db10`), not sorted as text (`db10,db2`), in output headers, merged groups and `Skipping hosts:`.
1375
+ - Fix: a message mssh cannot write to stderr is dropped. 6.38 wrote it to stdout, so with `PYTHONUNBUFFERED=1` `mssh ... 2>&1 | true` exited 1 without running anything, and `-j` output could hold a stray line.
1376
+ - `--store_config_file`, its `.bak` and `--add_control_master_config` write through one atomic writer: root writing a user's config keeps its owner and cannot leave it half written, and the new config keeps the old one's group with the default answer `b` too, where it can be kept (6.38 gave it yours).
1377
+ - Under `-11` with `--error_only`, `--script` or `-Q`, the `Skipping unavailable host` line is no longer printed. A `__multiSSH3_UNAVAILABLE_HOSTS` key in an env file (written by 4.89) is no longer read.
1378
+ - Autocomplete: the bash completion of a remote command works as bash-completion completes `sudo CMD`; warnings skip hosts words the shell rewrites; history rows written by 6.26 and older are inserted with their commands in single quotes, so the local shell expands no `!`, `$` or backtick in them; the debug log (`MSSH_COMPLETE_DEBUG=1`) no longer holds the line typed in the editor.
1379
+ - Fix (zsh): `mssh HOSTS CMD ...<TAB>` no longer adds local file names beside the remote command's words when python-argcomplete (`#compdef -P *`) is installed.
1380
+ - New known limitations, each in [docs/limitations.md](docs/limitations.md):
1381
+ - timeouts set in your ssh config no longer apply to mssh's ssh; an `-ea` keepalive interval gets mssh's count;
1382
+ - a silent remote command whose own ssh is turned away before login runs again (up to 3 times);
1383
+ - a gather retried with scp, of a remote directory named without a slash into a new destination, lands one level up;
1384
+ - an interrupted `--copy_id -p` can leave empty `~/.ssh/ssh-copy-id.*` directories (`rmdir` them);
1385
+ - output before a run into a closed pipe ends by SIGPIPE: -13 for a Python subprocess caller (6.38: 141), also for a program calling `main()` itself, 120 on Windows;
1386
+ - another user's config, writable to you only through an ACL, is not written when you answer `o` or it is reached through a symlink (the config is printed);
1387
+ - reading mssh's runtime state may create its 0700 directory;
1388
+ - Esc waits while the terminal is under 3 rows;
1389
+ - glue files from before 6.37 (bash) or 6.38 (zsh, fish) draw the Ctrl-R menu and warnings in ASCII until reinstalled; fish with a UTF-8 locale that is named but not installed draws Unicode;
1390
+ - the Ctrl-R history tab holds mssh's history only (no `-I`, `--script` or `-hf` runs);
1391
+ - no prompt beside the Ctrl-R menu on bash 4.2 / 4.3;
1392
+ - ranges of more than 4,096 names count a repeated name twice;
1393
+ - a hosts word holding a shell expansion is not checked;
1394
+ - bash: a remote command's `-X`, `-P` and `-S` apply only to the words compgen makes; under `menu-complete`, mssh's words beside a nospace command's mixed words get no space; with `nocasematch` on, a Ctrl-C during a TAB can leave it off;
1395
+ - `mssh --completion install fish` on fish 2 succeeds and does nothing.
1396
+ - Changed for programs and scripts:
1397
+ - `mssh-complete eval` moved to the source tree: run `python3 tests/replay.py` (same options) from a checkout. `mssh-complete eval` now says so and exits 2.
1398
+ - The completion index is format 3. An older index is ignored and rebuilt on the first TAB; until the build is done, TAB offers options, delegation and shell history only; `mssh --completion install` builds it at once.
1399
+ - Run `mssh --completion install` again to get the new shell glue.
1400
+ - The index rebuild and the host prober hold a kernel lock (`flock`) on `complete-index.lock` and `probe.lock` while they run; the files stay as empty files and no lock is ever stale. A failed rebuild leaves `complete-index.failed`, and the next starts 60 s later. `mssh-complete rebuild --lock X` and `probe --lock X` are accepted and ignored.
1401
+ - Accepted and ignored, for callers written for 6.38: `hosts_only` of `compact_hostnames` / `expand_hostnames`, `create` of `runtime_dir` / `_unavailable_hosts_file_path`, and `mssh-complete edit --expand`.
1402
+ - `mssh_complete.expand_ranges` gives back whole, not half expanded, a text that mixes number and letter groups (`n[1-2]x[a-b]`), one with malformed brackets (`n[01-12][,-10]`) and one of several groups that expands to more than `limit` names (`a[1-3]b[1-9999]` with `limit=100`).