multiSSH3 6.38__tar.gz → 6.40__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 (117) hide show
  1. {multissh3-6.38/multiSSH3.egg-info → multissh3-6.40}/PKG-INFO +98 -22
  2. {multissh3-6.38 → multissh3-6.40}/README.md +97 -21
  3. {multissh3-6.38 → multissh3-6.40}/mssh_complete.py +554 -1880
  4. {multissh3-6.38 → multissh3-6.40/multiSSH3.egg-info}/PKG-INFO +98 -22
  5. {multissh3-6.38 → multissh3-6.40}/multiSSH3.egg-info/SOURCES.txt +2 -0
  6. {multissh3-6.38 → multissh3-6.40}/multiSSH3.py +405 -1031
  7. {multissh3-6.38 → multissh3-6.40}/pytest.ini +0 -1
  8. {multissh3-6.38 → multissh3-6.40}/tests/bench_complete.py +3 -1
  9. {multissh3-6.38 → multissh3-6.40}/tests/complete_shell_harness.py +15 -4
  10. {multissh3-6.38 → multissh3-6.40}/tests/conftest.py +4 -14
  11. multissh3-6.40/tests/replay.py +356 -0
  12. multissh3-6.40/tests/shells/Dockerfile.el9 +19 -0
  13. {multissh3-6.38 → multissh3-6.40}/tests/shells/run_matrix.sh +10 -7
  14. {multissh3-6.38 → multissh3-6.40}/tests/test_cli_branches.py +0 -28
  15. {multissh3-6.38 → multissh3-6.40}/tests/test_cli_processing.py +0 -32
  16. multissh3-6.40/tests/test_compact_hostnames.py +43 -0
  17. multissh3-6.40/tests/test_complete_blesh.py +205 -0
  18. {multissh3-6.38 → multissh3-6.40}/tests/test_complete_cli.py +70 -111
  19. {multissh3-6.38 → multissh3-6.40}/tests/test_complete_diagnose.py +52 -398
  20. {multissh3-6.38 → multissh3-6.40}/tests/test_complete_editor.py +2 -13
  21. {multissh3-6.38 → multissh3-6.40}/tests/test_complete_engine.py +28 -61
  22. {multissh3-6.38 → multissh3-6.40}/tests/test_complete_eval.py +55 -26
  23. {multissh3-6.38 → multissh3-6.40}/tests/test_complete_index.py +31 -38
  24. {multissh3-6.38 → multissh3-6.40}/tests/test_complete_keys.py +12 -11
  25. {multissh3-6.38 → multissh3-6.40}/tests/test_complete_picker.py +45 -279
  26. {multissh3-6.38 → multissh3-6.40}/tests/test_complete_probe.py +15 -54
  27. {multissh3-6.38 → multissh3-6.40}/tests/test_complete_shells.py +440 -372
  28. {multissh3-6.38 → multissh3-6.40}/tests/test_complete_untrusted.py +5 -5
  29. multissh3-6.40/tests/test_config_cli.py +623 -0
  30. {multissh3-6.38 → multissh3-6.40}/tests/test_config_values.py +9 -28
  31. {multissh3-6.38 → multissh3-6.40}/tests/test_control_master_config.py +94 -1
  32. {multissh3-6.38 → multissh3-6.40}/tests/test_copy_id.py +4 -4
  33. {multissh3-6.38 → multissh3-6.40}/tests/test_curses_branches.py +0 -33
  34. {multissh3-6.38 → multissh3-6.40}/tests/test_curses_event_loop.py +1 -77
  35. multissh3-6.40/tests/test_curses_tui.py +39 -0
  36. {multissh3-6.38 → multissh3-6.40}/tests/test_file_trust.py +0 -11
  37. {multissh3-6.38 → multissh3-6.40}/tests/test_harness_smoke.py +4 -3
  38. {multissh3-6.38 → multissh3-6.40}/tests/test_login_drop_retry.py +15 -49
  39. {multissh3-6.38 → multissh3-6.40}/tests/test_main_lifecycle.py +15 -0
  40. {multissh3-6.38 → multissh3-6.40}/tests/test_output_merge.py +37 -0
  41. {multissh3-6.38 → multissh3-6.40}/tests/test_output_robustness.py +25 -6
  42. {multissh3-6.38 → multissh3-6.40}/tests/test_run_command_branches.py +4 -187
  43. {multissh3-6.38 → multissh3-6.40}/tests/test_run_command_characterization.py +5 -2
  44. {multissh3-6.38 → multissh3-6.40}/tests/test_run_command_exit.py +0 -1
  45. {multissh3-6.38 → multissh3-6.40}/tests/test_runtime_branch_matrix.py +0 -56
  46. {multissh3-6.38 → multissh3-6.40}/tests/test_runtime_dir.py +8 -20
  47. {multissh3-6.38 → multissh3-6.40}/tests/test_smoke_optional.py +0 -20
  48. multissh3-6.40/tests/test_ssh_timeouts.py +235 -0
  49. {multissh3-6.38 → multissh3-6.40}/tests/test_stopped_commands.py +1 -148
  50. {multissh3-6.38 → multissh3-6.40}/tests/test_stream_reader.py +25 -36
  51. {multissh3-6.38 → multissh3-6.40}/tests/test_unavailable_hosts.py +4 -2
  52. multissh3-6.38/tests/test_compact_hostnames.py +0 -26
  53. multissh3-6.38/tests/test_complete_blesh.py +0 -121
  54. multissh3-6.38/tests/test_config_cli.py +0 -312
  55. multissh3-6.38/tests/test_curses_tui.py +0 -81
  56. multissh3-6.38/tests/test_ssh_timeouts.py +0 -386
  57. {multissh3-6.38 → multissh3-6.40}/LICENSE +0 -0
  58. {multissh3-6.38 → multissh3-6.40}/MANIFEST.in +0 -0
  59. {multissh3-6.38 → multissh3-6.40}/multiSSH3.egg-info/dependency_links.txt +0 -0
  60. {multissh3-6.38 → multissh3-6.40}/multiSSH3.egg-info/entry_points.txt +0 -0
  61. {multissh3-6.38 → multissh3-6.40}/multiSSH3.egg-info/top_level.txt +0 -0
  62. {multissh3-6.38 → multissh3-6.40}/setup.cfg +0 -0
  63. {multissh3-6.38 → multissh3-6.40}/setup.py +0 -0
  64. {multissh3-6.38 → multissh3-6.40}/tests/data/run_command_golden.json +0 -0
  65. {multissh3-6.38 → multissh3-6.40}/tests/shells/Dockerfile.bash5.1 +0 -0
  66. {multissh3-6.38 → multissh3-6.40}/tests/shells/Dockerfile.bash5.3 +0 -0
  67. {multissh3-6.38 → multissh3-6.40}/tests/shells/Dockerfile.centos7 +0 -0
  68. {multissh3-6.38 → multissh3-6.40}/tests/shells/Dockerfile.current +0 -0
  69. {multissh3-6.38 → multissh3-6.40}/tests/shells/Dockerfile.legacy +0 -0
  70. {multissh3-6.38 → multissh3-6.40}/tests/shells/Dockerfile.modern +0 -0
  71. {multissh3-6.38 → multissh3-6.40}/tests/test_cli_option_words.py +0 -0
  72. {multissh3-6.38 → multissh3-6.40}/tests/test_closed_output.py +0 -0
  73. {multissh3-6.38 → multissh3-6.40}/tests/test_color_helpers.py +0 -0
  74. {multissh3-6.38 → multissh3-6.40}/tests/test_command_terminal.py +0 -0
  75. {multissh3-6.38 → multissh3-6.40}/tests/test_complete_context.py +0 -0
  76. {multissh3-6.38 → multissh3-6.40}/tests/test_complete_hosts.py +0 -0
  77. {multissh3-6.38 → multissh3-6.40}/tests/test_complete_hoststate.py +0 -0
  78. {multissh3-6.38 → multissh3-6.40}/tests/test_complete_lex.py +0 -0
  79. {multissh3-6.38 → multissh3-6.40}/tests/test_complete_mssh_helpers.py +0 -0
  80. {multissh3-6.38 → multissh3-6.40}/tests/test_complete_password_spellings.py +0 -0
  81. {multissh3-6.38 → multissh3-6.40}/tests/test_complete_start.py +0 -0
  82. {multissh3-6.38 → multissh3-6.40}/tests/test_complete_store.py +0 -0
  83. {multissh3-6.38 → multissh3-6.40}/tests/test_complete_suggest.py +0 -0
  84. {multissh3-6.38 → multissh3-6.40}/tests/test_complete_terminal.py +0 -0
  85. {multissh3-6.38 → multissh3-6.40}/tests/test_complete_zsh_options.py +0 -0
  86. {multissh3-6.38 → multissh3-6.40}/tests/test_config_validate.py +0 -0
  87. {multissh3-6.38 → multissh3-6.40}/tests/test_curses_wide_chars.py +0 -0
  88. {multissh3-6.38 → multissh3-6.40}/tests/test_expand_hostnames.py +0 -0
  89. {multissh3-6.38 → multissh3-6.40}/tests/test_helpers_extra.py +0 -0
  90. {multissh3-6.38 → multissh3-6.40}/tests/test_history_replay.py +0 -0
  91. {multissh3-6.38 → multissh3-6.40}/tests/test_host.py +0 -0
  92. {multissh3-6.38 → multissh3-6.40}/tests/test_host_validity.py +0 -0
  93. {multissh3-6.38 → multissh3-6.40}/tests/test_hosts_file.py +0 -0
  94. {multissh3-6.38 → multissh3-6.40}/tests/test_identities_only.py +0 -0
  95. {multissh3-6.38 → multissh3-6.40}/tests/test_ipmi_lockout_wait.py +0 -0
  96. {multissh3-6.38 → multissh3-6.40}/tests/test_ipmi_logins.py +0 -0
  97. {multissh3-6.38 → multissh3-6.40}/tests/test_key_path_home.py +0 -0
  98. {multissh3-6.38 → multissh3-6.40}/tests/test_narrow_terminal.py +0 -0
  99. {multissh3-6.38 → multissh3-6.40}/tests/test_no_hosts.py +0 -0
  100. {multissh3-6.38 → multissh3-6.40}/tests/test_option_no_effect.py +0 -0
  101. {multissh3-6.38 → multissh3-6.40}/tests/test_options_after_command.py +0 -0
  102. {multissh3-6.38 → multissh3-6.40}/tests/test_packaging.py +0 -0
  103. {multissh3-6.38 → multissh3-6.40}/tests/test_password_handling.py +0 -0
  104. {multissh3-6.38 → multissh3-6.40}/tests/test_passwords_out_of_ps.py +0 -0
  105. {multissh3-6.38 → multissh3-6.40}/tests/test_run_command.py +0 -0
  106. {multissh3-6.38 → multissh3-6.40}/tests/test_run_on_hosts.py +0 -0
  107. {multissh3-6.38 → multissh3-6.40}/tests/test_run_on_hosts_modes.py +0 -0
  108. {multissh3-6.38 → multissh3-6.40}/tests/test_shared_lines.py +0 -0
  109. {multissh3-6.38 → multissh3-6.40}/tests/test_ssh_error_text.py +0 -0
  110. {multissh3-6.38 → multissh3-6.40}/tests/test_sshpass_exit.py +0 -0
  111. {multissh3-6.38 → multissh3-6.40}/tests/test_start_run_on_hosts.py +0 -0
  112. {multissh3-6.38 → multissh3-6.40}/tests/test_streamed_output.py +0 -0
  113. {multissh3-6.38 → multissh3-6.40}/tests/test_tui_ssh_notices.py +0 -0
  114. {multissh3-6.38 → multissh3-6.40}/tests/test_underscore_settings.py +0 -0
  115. {multissh3-6.38 → multissh3-6.40}/tests/test_waiting.py +0 -0
  116. {multissh3-6.38 → multissh3-6.40}/tests/test_wide_wrap.py +0 -0
  117. {multissh3-6.38 → multissh3-6.40}/tests/vt_grid.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: multiSSH3
3
- Version: 6.38
3
+ Version: 6.40
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`, a config of the user who ran sudo (their file, their symlink, or a new one in a directory of theirs) is looked at, backed up and written with that user's rights: their link is written through where they may write the file it points to, the `.bak` and a new config are theirs, and a home on NFS that squashes root works. Where that user may not write, the config is printed instead. A link of anyone else (but root) is not followed. Under `sudo -E`, mssh runs as root and does not load your own `~/.multiSSH3.config.json` (a file of another user), so `--store_config_file` writes the system configs' settings plus the command line, and your earlier settings are left in the `.bak` ([limitations](docs/limitations.md#under-sudo--e-mssh-ignores-the-users-own-config)).
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
 
@@ -259,6 +261,8 @@ The block applies to every ssh you run, not only mssh's. ssh fails outright (exi
259
261
 
260
262
  The sockets never go in your home directory: on clusters it is often NFS, GPFS or Lustre, where unix sockets may not work. The checks run in `/bin/sh` whatever your login shell (csh and fish too) and work with OpenSSH 7.4 and later. ssh runs each `Match exec` with your `$SHELL -c`, twice per ssh: that adds about 2 ms with bash, more with shells that read startup files for `-c` (tcsh reads `.cshrc` and `/etc/profile.d/*.csh`, fish its `config.fish`), and with a `$SHELL` that is not an executable every ssh fails. The checks also run when the autocomplete's background checker calls `ssh -G`; they only look at local directories (and may create `/dev/shm/multiSSH3-<uid>`).
261
263
 
264
+ Under `sudo`, a `~/.ssh` and `~/.ssh/config` of the user who ran sudo are written with their rights, and a new `~/.ssh` is theirs. A `~/.ssh/config` that is a symlink of anyone else (but root, or that user under sudo) is not followed: mssh prints `Error adding ControlMaster configuration: ... is a symlink owned by someone else, not writing through it` and changes nothing.
265
+
262
266
  Up to multiSSH3 6.31 the block was `Host *` with `ControlPath /run/user/%i/ssh_sockets_%C`, which makes every ssh fail where `/run/user/<uid>` is missing. Run `mssh --add_control_master_config` again: it replaces that block, and only it, keeping the rest of the file and its permissions as they were. Running it once more changes nothing.
263
267
 
264
268
  ### IPMI Logins
@@ -418,7 +422,7 @@ The output is printed when the commands finish, so it can be piped like any comm
418
422
  ```bash
419
423
  mssh all 'dmesg' | less
420
424
  ```
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.
425
+ 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
426
 
423
427
  ### Curses Window Size Control
424
428
 
@@ -444,7 +448,7 @@ While leaving minimum 40 characters / 1 line for each host display by default. Y
444
448
  > Home/End: Jump cursor
445
449
  > Esc : Clear / hide view
446
450
  > ```
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.
451
+ > `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
452
  > 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
453
  > You can also toggle this help in curses by pressing `?` or `F1`.
450
454
  > 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 +480,7 @@ mssh has ssh, scp, rsync's ssh and ssh-copy-id give up on a host that takes the
476
480
 
477
481
  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
482
 
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.
483
+ 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
484
 
483
485
  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
486
 
@@ -498,7 +500,7 @@ A new block is appended to your rc file; changing or removing it replaces the fi
498
500
 
499
501
  - **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
502
  - **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"`).
503
+ - **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"`). A local file name holding whitespace or a shell character goes in quoted for this shell, which is wrong for the remote command (`my\ file` runs `my file` as a command of its own, `a\&b` makes the remote shell run `b`), and TAB inside a quoted argument (`mssh grp cat 'my f<TAB>`) offers nothing useful; both are to be fixed after 6.40 ([limitations](docs/limitations.md#local-file-names-with-whitespace-or-shell-characters-are-wrong-in-a-remote-command), [limitations](docs/limitations.md#inside-a-quoted-argument-the-engine-completes-a-quoted-command)).
502
504
 
503
505
  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
506
 
@@ -508,7 +510,7 @@ In bash with readline's `completion-ignore-case` on (`set completion-ignore-case
508
510
 
509
511
  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
512
 
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.
513
+ 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
514
 
513
515
  ### Grey suggestions
514
516
 
@@ -528,29 +530,29 @@ While you type an mssh line, the most likely rest of it shows in grey after the
528
530
 
529
531
  ### Ctrl-R
530
532
 
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, ...).
533
+ 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
534
 
533
535
  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
536
 
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.
537
+ 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
538
 
537
539
  ### Warnings before Enter
538
540
 
539
541
  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
542
 
541
543
  - **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?`
544
+ - **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
545
  - **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
546
  - **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
547
  - **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
548
 
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.
549
+ 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
550
 
549
551
  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
552
 
551
553
  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
554
 
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.
555
+ 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
556
 
555
557
  | Environment variable | Effect |
556
558
  | --- | --- |
@@ -562,14 +564,20 @@ Hosts are checked in the background by `mssh-complete probe`: DNS, `ssh -G` (whi
562
564
  | `MSSH_COMPLETE_PING_TTL` | Seconds a host that answered ping is expected to keep answering (default: mssh's unreachable host expiry, 600) |
563
565
  | `MSSH_COMPLETE_DEBUG` | `1` logs errors to `complete.log` in the cache directory |
564
566
 
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.
567
+ > 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
568
 
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.
569
+ > 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
570
 
569
571
  > 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
572
 
571
573
  > 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
574
 
575
+ > 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`, and picked from ble.sh's menu, mssh's words beside the words of a command like git (which asks for no space, `--format=` beside `--oneline `) go in without a space ([limitations](docs/limitations.md#under-menu-complete-or-bleshs-menu-msshs-words-beside-a-nospace-commands-mixed-words-get-no-space)). Where python-argcomplete's global completer (`complete -D`) is set, a remote command with no completion file completes local file names, as with bash-completion alone: mssh never runs a local program named like the remote command ([limitations](docs/limitations.md#bash-a-remote-command-with-no-completion-file-completes-local-file-names-where-python-argcompletes-global-completer-is-set)).
576
+
577
+ > With ble.sh, one of mssh's remote words (from its history) that names a directory on this machine goes in with `/` and no space ([limitations](docs/limitations.md#blesh-a-remote-word-of-msshs-that-names-a-local-directory-gets-a--and-no-space)), and a remote `~/...` path from mssh's history goes in with a bare `~`, which this shell expands to your local home ([limitations](docs/limitations.md#blesh-a-remote--path-from-msshs-history-goes-in-with-a-bare-)); plain bash keeps the space and inserts `\~`.
578
+
579
+ > With ble.sh, a word that ble.sh splits at an `=` or `:` after a quote, a backslash or an expansion (`"a"=b`, `$V:x`) is read as in 6.39; a `~` after such a split comes back as this machine's home ([limitations](docs/limitations.md#blesh-a-word-split-at-an--or--after-a-quote-backslash-or-expansion-is-read-as-in-639)). On bash 4.3 and 4.4, a remote command's `-o noquote` is not seen, so the local file names completed for it are quoted ([limitations](docs/limitations.md#bash-43-and-44-a-remote-commands--o-noquote-is-not-seen)). With zsh before 5.8, a Ctrl-C during a completion query now and then reaches the query instead of the shell, as in 6.39; a fix is planned for the change after 6.40 ([limitations](docs/limitations.md#zsh-before-58-a-ctrl-c-during-a-completion-query-can-reach-the-query)).
580
+
573
581
  ## Options
574
582
 
575
583
  Here details the options for multiSSH3 6.31 @ 2026-10-13
@@ -631,7 +639,7 @@ mssh -ea="-o ProxyCommand='ssh -W %h:%p bastion'" allServers uptime
631
639
  - 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
640
  - 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
641
  - 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).
642
+ - 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
643
  - 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
644
  - 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
645
  - 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 +809,7 @@ mssh -f ./data/ allServers /tmp/data/
801
809
  - 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
810
  - 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
811
  - 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.
812
+ - 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
813
  - 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
814
 
807
815
  ### `-j, --json` | _`DEFAULT_JSON_OUTPUT`_
@@ -828,7 +836,7 @@ mssh -f ./data/ allServers /tmp/data/
828
836
  - By default mssh set this to true to speed up operations on large host lists with some unreachable hosts.
829
837
  - Unreachable hosts will be tried again when their timeout expires.
830
838
  - 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.
839
+ - 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
840
  - 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
841
 
834
842
  ### `-a, -nsu, --no_skip_unreachable` | not _`DEFAULT_SKIP_UNREACHABLE`_
@@ -869,7 +877,7 @@ mssh -f ./data/ allServers /tmp/data/
869
877
  ### `-ci, --copy_id` | False
870
878
  - `copy_id` mode, use `ssh-copy-id` to copy public key to the hosts.
871
879
  - 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.
880
+ - 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
881
  - `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
882
  - 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
883
  - 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 +984,7 @@ Option names count in full only: `--user` is not `--username` and `-c` is not `-
976
984
 
977
985
  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
986
 
979
- Below is a sample help message output from multiSSH3 6.38 @ 2026-10-22 (no config file, 120 columns; the lines after the options list the programs found on that machine)
987
+ Below is a sample help message output from multiSSH3 6.40 @ 2026-11-01 (no config file, 120 columns; the lines after the options list the programs found on that machine)
980
988
 
981
989
  ```bash
982
990
  $ mssh -h
@@ -1259,7 +1267,7 @@ DEFAULT_FILE_SYNC = False
1259
1267
  DEFAULT_TIMEOUT = 50
1260
1268
  DEFAULT_CLI_TIMEOUT = 0
1261
1269
  # 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
1270
+ # (ServerAliveInterval x ServerAliveCountMax), after -ea's options so an -o there wins; 0 leaves one to ssh
1263
1271
  DEFAULT_SSH_CONNECT_TIMEOUT = 30
1264
1272
  DEFAULT_SSH_SERVER_ALIVE_INTERVAL = 15
1265
1273
  DEFAULT_SSH_SERVER_ALIVE_COUNT_MAX = 4
@@ -1378,6 +1386,8 @@ __DEBUG_MODE = False
1378
1386
 
1379
1387
  > `run_command_on_hosts` reads `_DEFAULT_CALLED`, `_DEFAULT_RETURN_UNFINISHED`, `_DEFAULT_UPDATE_UNREACHABLE_HOSTS` and `_DEFAULT_NO_START` each time it is called, so setting them after `import multiSSH3`, or in a file given with `--config_file`, works. The `mssh` command always starts its hosts and waits for them: of these four, only `_DEFAULT_UPDATE_UNREACHABLE_HOSTS` changes what it does.
1380
1388
 
1389
+ > Under sudo, `main()` with `--store_config_file` or `--add_control_master_config` switches the whole process's effective user to the user who ran sudo while it writes their config, so a program's other threads run as that user for that moment ([limitations](docs/limitations.md#under-sudo-a-program-calling-main-runs-its-threads-as-the-user-while-a-config-is-written)).
1390
+
1381
1391
  > `_etc_hosts` is a custom dict to use instead of reading /etc/hosts file for hostname resolution.
1382
1392
 
1383
1393
  > `ERROR_MESSAGES_TO_IGNORE` lists regexes for ssh's own notices, which mssh leaves out of each host's stderr (also in `--json` and `--greppable` output). Each regex is searched anywhere in every stderr line, so anchor yours with `^` and `$` to match whole lines (in the JSON config file, write a regex backslash as `\\`). The defaults are anchored to the lines OpenSSH prints, so a remote program's error that uses the same words, such as curl's `Recv failure: Connection reset by peer`, is kept. Connection resets are not hidden: `kex_exchange_identification: read: Connection reset by peer` is how sshd's `MaxStartups` turns away connections opened at once. Config files that `--store_config_file` or `--generate_config_file` wrote before 6.20 hold a copy of the old, unanchored defaults; mssh ignores such an unchanged copy and uses the current defaults.
@@ -1387,3 +1397,69 @@ __DEBUG_MODE = False
1387
1397
  > 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
1398
 
1389
1399
  > `__DEBUG_MODE` is used to control whether to print host thread execution debug information to its stderr.
1400
+
1401
+ ## Upgrading to 6.40 from 6.39
1402
+
1403
+ Fixes for the six items deferred from 6.39 (bash and ble.sh completion, `--store_config_file` under sudo) and for three found beside them. Run `mssh --completion install` again in bash, with or without ble.sh, to get the new glue: the completion fixes below are in it (zsh's and fish's glues did not change). What you may notice:
1404
+
1405
+ - Fix (bash): `mssh h CMD --opt=<TAB>`, where CMD's completion offers `--opt=`, no longer adds a space after the `=`; beside other words the part they share goes in (`--opt=a`).
1406
+ - Fix (bash): a remote command's `-W` list, `-G` pattern and actions (`-u`, `-d`, `-f`, ...) complete the word after a `:` or `=` as bash does, when the whole word matches nothing: `mssh h CMD --user=roo<TAB>` with `complete -u CMD` gives `--user=root`, `x=zz` with `complete -d` gives `x=zzsub/`, and `--x=al` with `-W "alpha beta"` gives `--x=alpha`. A file name or a `-W` word holding a `:` or `=` still completes as one word.
1407
+ - Fix (bash): local file names completed for a remote command (its command word, `-f`, `-d`, `-G`, a command with no completion file) are quoted and marked as bash does: `zzmy\ file`, `zzamp\&b`, `zzdir/`, not `zzmy file` (two words here), `zzamp&b` (this shell ran `b`) or `zzdir `. With mssh's own words beside them, also beside bash-completion's `_filedir` (which `cat`, `ls`, `cp`, ... run), a remote word that names a local directory keeps its space, and a remote `~/...` from mssh's history keeps its `\~`; the list then shows the names with their directories (`/etc/sysctl.d/`), not just their last part. A spec with `-o nospace -o filenames` that offers nothing now leaves mssh's own word its space.
1408
+ - Fix (ble.sh): TAB inside a quoted remote command completes, from the command's completion and from mssh's history (`mssh h "systemctl rest<TAB>`, also with a `=` in it); `mssh h cat ~/zzn<TAB>` offers the path from mssh's history, with the `~` as typed; an earlier argument holding a backslash (`zz\ a`) no longer makes the word being typed a command name.
1409
+ - Fix (ble.sh): beside a git-style command (which asks for no space) TAB in the middle of a line leaves one space, not two; mssh's words get their space beside a command whose words all end in one (git's branches, scp's file names); a word both the command and mssh's history offer is listed once; scp's escaped file names go in as they do natively. Beside mixed words (`--format=` with `--oneline `), mssh's words picked from the menu still get no space, and a word of mssh's and one of the command's that differ only in case are both listed, as readline lists them (6.39 folded case there).
1410
+ - Under sudo (root with `SUDO_UID`), the config of the user who ran sudo is written with their rights: `sudo -E mssh --store_config_file` on a config that is their symlink into a dotfiles repository writes through it and keeps the link (6.39 moved the link to the `.bak` and put a regular file in its place); the `.bak` and a new config are theirs (6.39 made a new config root's `0600` file, which they could not read), and an NFS home that squashes root works. Where that user may not write (the file the link points to, the directory), the write fails and the config is printed, as when they run mssh themselves. A config of anyone else, and every config of root without sudo, are as in 6.39.
1411
+ - `--add_control_master_config` follows the same rules for `~/.ssh/config`: under sudo the user's `~/.ssh` and its config are written with their rights, and a new `~/.ssh` or config is theirs (6.39 made a new `~/.ssh` root's `0700`, which they cannot enter, so their ssh silently ignored the config in it; a root-owned config inside their own `~/.ssh` is accepted by ssh); a `~/.ssh/config` that is a symlink of anyone else (but root) is not followed, with a one-line error and no traceback (6.39, as root, wrote the block wherever such a link pointed).
1412
+ - Fix: with the default answer `b`, a config that is someone else's symlink (one planted in `/tmp`) is moved to the `.bak` and the new file written in its place is the writer's, `0600`; 6.39, as root, gave it the owner of the file the link pointed to, though it can hold passwords.
1413
+ - Fix: root writing a file in a directory another user owns (sudo keeping HOME, or `--store_config_file PATH` there) gives the temporary file its owner on the open file, so that user can no longer swap it for a link between the write and the `chown` and have root give them a file of their choice.
1414
+ - After `--store_config_file`, the config shown is what mssh wrote, from memory, not read back from the file (root may not read it on an NFS home that squashes root).
1415
+ - New known limitations and deferrals, each in [docs/limitations.md](docs/limitations.md):
1416
+ - ble.sh: mssh's words picked from ble.sh's menu beside a nospace command's mixed words get no space (the menu-complete entry, extended);
1417
+ - bash: where python-argcomplete's global completer (`complete -D`) is set, a remote command with no completion file completes local file names;
1418
+ - ble.sh: a remote word of mssh's that names a local directory gets `/` and no space;
1419
+ - ble.sh: a remote `~/...` from mssh's history goes in with a bare `~`, which this shell expands;
1420
+ - under `sudo -E`, mssh ignores your own config, so `--store_config_file` there keeps your earlier settings only in the `.bak`;
1421
+ - ble.sh: a word split at an `=` or `:` after a quote, a backslash or an expansion is read as in 6.39, and a `~` after such a split comes back as this machine's home;
1422
+ - bash 4.3 and 4.4: a remote command's `-o noquote` is not seen;
1423
+ - under sudo, a program calling `main()` with `--store_config_file` or `--add_control_master_config` runs its other threads as the user while the config is written;
1424
+ - deferred to the change after 6.40: local file names with whitespace or shell characters in a remote command (all shells), TAB inside a quoted argument (`mssh h cat 'my f<TAB>`), and, with zsh before 5.8, a Ctrl-C during a completion query that now and then reaches the query (as in 6.39).
1425
+ - Changed for programs and scripts:
1426
+ - The bash glue under ble.sh sends the word as typed, and the engine reads the word of a `bash` request with `ble=1` as typed (lexed, as zsh's and fish's); a 6.39 glue's word is read right too.
1427
+ - The bash glue no longer reads a `C` record's fourth field (the unescaped text); the engine still sends it for older glues.
1428
+ - Under sudo, `_own_symlink` and `_replace_file` compare with the effective uid, not the real one.
1429
+
1430
+ ## Upgrading to 6.39 from 6.38
1431
+
1432
+ 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:
1433
+
1434
+ - 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.
1435
+ - 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.
1436
+ - 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.
1437
+ - 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:`.
1438
+ - 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.
1439
+ - `--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).
1440
+ - Under `-11` with `--error_only`, `--script` or `-Q`, the `Skipping unavailable host` line is no longer printed. A `__multiSSH3_UNAVAILABLE_HOSTS` line in an env file, which 6.38 read when it had no unavailable-hosts cache yet, is no longer read.
1441
+ - 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.
1442
+ - 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.
1443
+ - New known limitations, each in [docs/limitations.md](docs/limitations.md):
1444
+ - timeouts set in your ssh config no longer apply to mssh's ssh; an `-ea` keepalive interval gets mssh's count;
1445
+ - a silent remote command whose own ssh is turned away before login runs again (up to 3 times);
1446
+ - a gather retried with scp, of a remote directory named without a slash into a new destination, lands one level up;
1447
+ - an interrupted `--copy_id -p` can leave empty `~/.ssh/ssh-copy-id.*` directories (`rmdir` them);
1448
+ - 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;
1449
+ - 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);
1450
+ - reading mssh's runtime state may create its 0700 directory;
1451
+ - Esc waits while the terminal is under 3 rows;
1452
+ - 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;
1453
+ - the Ctrl-R history tab holds mssh's history only (no `-I`, `--script` or `-hf` runs);
1454
+ - no prompt beside the Ctrl-R menu on bash 4.2 / 4.3;
1455
+ - ranges of more than 4,096 names count a repeated name twice;
1456
+ - a hosts word holding a shell expansion is not checked;
1457
+ - 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;
1458
+ - `mssh --completion install fish` on fish 2 succeeds and does nothing.
1459
+ - Changed for programs and scripts:
1460
+ - `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.
1461
+ - 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.
1462
+ - Run `mssh --completion install` again to get the new shell glue.
1463
+ - 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.
1464
+ - 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`.
1465
+ - `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`).