|
| 1 | +[](https://goreportcard.com/report/github.com/ncode/pretty) |
| 2 | +[](https://opensource.org/licenses/Apache-2.0) |
| 3 | +[](https://codecov.io/gh/ncode/pretty) |
| 4 | + |
1 | 5 | # pretty |
2 | | -Parallel remote execution tty - (Yet another parallel ssh/shell) |
3 | 6 |
|
4 | | -## Installation: |
5 | | -go get -u github.com/ncode/pretty |
| 7 | +`Parallel remote execution tty` - (Yet another parallel ssh/shell) |
| 8 | + |
| 9 | +- Run commands across many hosts with colored, prefixed output. |
| 10 | +- Keep an interactive prompt with a per-host shell session. |
| 11 | +- Run async jobs in separate SSH sessions and track status. |
| 12 | +- Load hosts from args, config groups, or a hosts file. |
6 | 13 |
|
7 | | -## Config: |
8 | | -By default it lives in ~/.pretty.yaml |
| 14 | +## Installation |
| 15 | +Requires Go 1.25. |
9 | 16 |
|
10 | 17 | ``` |
11 | | -username: ncode |
12 | | -history_file: ~/.pretty.history |
13 | | -ssh_private_key: ~/.ssh/id_rsa |
14 | | -groups: |
15 | | - hosts: |
16 | | - - host1 |
17 | | - - host2 |
18 | | - - host3 |
19 | | - - host4 |
| 18 | +go install github.com/ncode/pretty@latest |
20 | 19 | ``` |
21 | 20 |
|
22 | | -## Usage: |
| 21 | +## Quick start |
| 22 | +``` |
| 23 | +pretty host1 host2 host3 |
| 24 | +pretty -G prod |
| 25 | +pretty -H /tmp/hosts.txt |
23 | 26 | ``` |
24 | | -Parallel remote execution tty - (Yet another parallel ssh/shell) |
25 | 27 |
|
26 | | -usage: |
27 | | - pretty <host1> <host2> <host3>... |
| 28 | +## Configuration |
| 29 | +`pretty` looks for a config file named `.pretty` in your home directory with a supported extension: |
| 30 | +`$HOME/.pretty.yaml`, `$HOME/.pretty.yml`, `$HOME/.pretty.json`, or `$HOME/.pretty.toml`. |
| 31 | +Use `--config` to point at an explicit path. |
28 | 32 |
|
29 | | -Usage: |
30 | | - pretty [flags] |
| 33 | +Optional keys: |
| 34 | +- `username`: SSH username override (falls back to SSH config, then current shell user). |
| 35 | +- `known_hosts`: path to a known_hosts file for host key verification. |
| 36 | +- `groups.<name>`: host groups as wrapper objects with `hosts` and optional `user`. |
| 37 | +- `prompt`: interactive prompt string (UTF-8 supported). `--prompt` overrides config. |
31 | 38 |
|
32 | | -Flags: |
33 | | - --config string config file (default is $HOME/.pretty.yaml) |
34 | | - -h, --help help for pretty |
35 | | - -G, --hostGroup string group of hosts to be loaded from the config file |
36 | | - -H, --hostsFile string hosts file to be used instead of the args via stdout (one host per line) |
| 39 | +Example: |
37 | 40 | ``` |
| 41 | +known_hosts: /Users/me/.ssh/known_hosts |
| 42 | +prompt: "pretty> " |
| 43 | +groups: |
| 44 | + web: |
| 45 | + user: deploy |
| 46 | + hosts: |
| 47 | + - web1.example.com |
| 48 | + - web2.example.com:2222 |
| 49 | +``` |
| 50 | + |
| 51 | +Host key verification: |
| 52 | +- If `known_hosts` is set and loads successfully, it is used. |
| 53 | +- Otherwise `~/.ssh/known_hosts` is used if it loads successfully. |
| 54 | +- If neither can be loaded, host keys are not verified. |
| 55 | +- A loaded known_hosts file must contain each host key or connections will fail. |
38 | 56 |
|
39 | | -Connecting to hosts: |
| 57 | +Notes: |
| 58 | +- Group entries must use the wrapper schema with a `hosts` list. |
| 59 | +- Auth uses your SSH agent (`SSH_AUTH_SOCK`) and IdentityFile entries from SSH config. Load keys with `ssh-add`. |
| 60 | + |
| 61 | +## Host specs |
| 62 | +Accepted formats: |
| 63 | +- `host` (defaults to port 22) |
| 64 | +- `host:port` |
| 65 | +- `user@host` |
| 66 | +- `user@host:port` |
| 67 | +- `[ipv6]:port` (required to specify a port with IPv6) |
| 68 | +- `user@[ipv6]:port` |
| 69 | + |
| 70 | +Hosts files (`-H`) accept one entry per line in the same formats. Blank lines are ignored. |
| 71 | + |
| 72 | +## Flags |
| 73 | +- `--config <path>`: config file path. |
| 74 | +- `--prompt <string>`: prompt to display in the interactive shell. |
| 75 | +- `-G`, `--hostGroup <name>`: load `groups.<name>` from config. |
| 76 | +- `-H`, `--hostsFile <path>`: read hosts from a file (one host per line). |
| 77 | +- `-h`, `--help`: help for pretty. |
| 78 | + |
| 79 | +Host selection behavior: |
| 80 | +- At least one of positional hosts, `--hostGroup`, or `--hostsFile` is required. |
| 81 | +- With no positional hosts, `--hostGroup` loads only the group. |
| 82 | +- With more than one positional host, `--hostGroup` appends the group. |
| 83 | +- With exactly one positional host, `--hostGroup` is currently ignored. |
| 84 | +- `--hostsFile` always appends its hosts. |
| 85 | + |
| 86 | +## Interactive commands |
40 | 87 | ``` |
41 | | -pretty host1 host2 host3 host4 |
42 | | -pretty(2)>> |
43 | | -Error connection to host host3: Failed to dial: dial tcp: lookup host3: no such host |
44 | | -Error connection to host host4: Failed to dial: dial tcp: lookup host4: no such host |
| 88 | +:help |
| 89 | +:list |
| 90 | +:status [id] |
| 91 | +:async <command> |
| 92 | +:scroll |
| 93 | +:bye |
| 94 | +exit |
45 | 95 | ``` |
46 | 96 |
|
47 | | -Connecting to hostGroups: |
| 97 | +Notes: |
| 98 | +- `:list` shows connection status per host. |
| 99 | +- `:status` shows the last normal job plus the last two async jobs; `:status <id>` targets a single job. |
| 100 | +- `:async` runs a command in a new SSH session per host and returns to the prompt immediately. |
| 101 | +- `:scroll` enters scroll mode for the output viewport (output scrolling is disabled otherwise); press `esc` to return to the prompt. |
| 102 | +- Use Up/Down arrows to navigate command history (persisted in `history_file`). |
| 103 | +- `Ctrl+C` forwards to remote sessions; press twice within 500ms to quit locally. |
| 104 | +- `Ctrl+Z` forwards to remote sessions (suspend). |
| 105 | + |
| 106 | +## How it works |
| 107 | +- Starts one persistent SSH shell session per host for interactive commands. |
| 108 | +- Wraps each command with a sentinel to capture per-host exit codes. |
| 109 | +- Runs async commands in fresh SSH sessions and updates job status as they finish. |
| 110 | +- Prefixes output with `host:port` and assigns a stable color per host. |
| 111 | +- Keeps the last 10,000 output lines in the UI buffer. |
| 112 | + |
| 113 | +## Local SSHD testbed |
| 114 | +Use the local SSHD testbed to exercise `pretty` against three localhost targets. |
| 115 | + |
| 116 | +Generate keys, password, and a ready-to-use config file: |
48 | 117 | ``` |
49 | | -pretty -G hosts |
50 | | -pretty(2)>> |
51 | | -Error connection to host host3: Failed to dial: dial tcp: lookup host3: no such host |
52 | | -Error connection to host host4: Failed to dial: dial tcp: lookup host4: no such host |
| 118 | +export PRETTY_AUTHORIZED_KEY="$(ssh-add -L | grep 'my-key' | head -n1)" |
| 119 | +./scripts/ssh-testbed-setup.sh |
53 | 120 | ``` |
54 | 121 |
|
55 | | -Connecting to hostsFile: |
| 122 | +Start the testbed: |
56 | 123 | ``` |
57 | | -pretty -H /tmp/hosts.txt |
58 | | -pretty(2)>> |
59 | | -Error connection to host host3: Failed to dial: dial tcp: lookup host3: no such host |
60 | | -Error connection to host host4: Failed to dial: dial tcp: lookup host4: no such host |
| 124 | +docker compose -f docker-compose.sshd.yml up -d --build |
61 | 125 | ``` |
62 | 126 |
|
63 | | -List connection status: |
| 127 | +Re-run setup after the containers are running to populate `.pretty-test/known_hosts`: |
64 | 128 | ``` |
65 | | -pretty(2)>> :status |
66 | | -Connected hosts (2) |
67 | | -Failed hosts (2) |
| 129 | +./scripts/ssh-testbed-setup.sh |
68 | 130 | ``` |
69 | 131 |
|
70 | | -List hosts: |
| 132 | +If you want to use the generated test key instead of an existing agent key: |
71 | 133 | ``` |
72 | | -pretty(2)>> :list |
73 | | -host1: Connected(true) |
74 | | -host2: Connected(true) |
75 | | -host3: Connected(false) |
76 | | -host4: Connected(false) |
| 134 | +ssh-add .pretty-test/id_ed25519 |
77 | 135 | ``` |
78 | 136 |
|
79 | | -Running commands: |
| 137 | +Example run: |
80 | 138 | ``` |
81 | | -pretty(2)>> whoami |
82 | | -host1: ncode |
83 | | -host2: ncode |
| 139 | +pretty --config .pretty-test/pretty.yaml -G testbed |
84 | 140 | ``` |
| 141 | +Then run `whoami` to confirm each host responds as `pretty`. |
| 142 | + |
| 143 | +The generated password is stored at `.pretty-test/password.txt` for manual `ssh` testing if needed. |
85 | 144 |
|
86 | | -## Why do I need it? |
87 | | -pretty is a tool to control interactive shells across multiple hosts from |
88 | | -a single point. |
| 145 | +## Why pretty? |
| 146 | +`pretty` is a tool to control interactive shells across multiple hosts from a single point. |
89 | 147 |
|
90 | 148 | ### Motivation |
91 | | -After using [polysh](http://guichaz.free.fr/polysh) for a long time. It came with |
92 | | -the motivation to try to write my own parallel shell in GO. In the end the tool worked |
| 149 | +After using [polysh](http://guichaz.free.fr/polysh) for a long time, it came with |
| 150 | +the motivation to try to write my own parallel shell in Go. In the end the tool worked |
93 | 151 | so well and I decided to open source the code. |
94 | 152 |
|
95 | | -### TODO: |
96 | | -Forward Control+C and Control+Z requests to the destination terminal |
97 | | -Support for encrypted ssh keys |
| 153 | +## Limitations |
| 154 | +- SSH authentication uses the local agent and SSH config IdentityFile entries; there is no keyfile flag. |
0 commit comments