Skip to content

Commit 3d7047e

Browse files
committed
updated readme.md
1 parent a715c70 commit 3d7047e

8 files changed

Lines changed: 80 additions & 80 deletions

File tree

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,6 @@ jobs:
3838
run: make test
3939

4040
- name: Run GoLand CI linters
41-
uses: golangci/golangci-lint-action@v4
41+
uses: golangci/golangci-lint-action@v8
4242
with:
43-
args: --out-format=colored-line-number
43+
version: latest

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ all: test lint build
99
build:
1010
@echo "Building $(BINARY_NAME)..."
1111
@mkdir -p $(BUILD_DIR)
12-
@go build -mod=mod -trimpath -ldflags="-s -w" -o $(BUILD_DIR)/$(BINARY_NAME) ./cmd
12+
@CGO_ENABLED=0 go build -mod=mod -trimpath -ldflags="-s -w -extldflags '-static'" -o $(BUILD_DIR)/$(BINARY_NAME) ./cmd
1313

1414
test:
1515
@echo "Running tests..."

cmd/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import (
66
"log"
77
"os"
88

9-
"github.com/innogames/gosh/pkg"
9+
"github.com/brainexe/gosh/pkg"
1010
"github.com/spf13/pflag"
1111
)
1212

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
module github.com/innogames/gosh
1+
module github.com/brainexe/gosh
22

33
go 1.25.0
44

go.sum

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,15 @@
1+
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
12
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
23
github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI=
34
github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
5+
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
46
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
5-
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
6-
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
7-
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
8-
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
9-
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
10-
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
11-
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
12-
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
13-
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
147
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
158
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
16-
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
17-
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
18-
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
19-
golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY=
20-
golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
219
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
2210
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
2311
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
24-
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
25-
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
26-
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
2712
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
2813
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
29-
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
30-
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
31-
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
32-
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
33-
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
14+
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
15+
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=

pkg/interactive.go

Lines changed: 7 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"bufio"
55
"context"
66
"fmt"
7-
"log"
87
"os"
98
"os/exec"
109
"strings"
@@ -48,12 +47,8 @@ func (c *customCompleter) Do(line []rune, pos int) ([][]rune, int) {
4847
// Extract the current word being completed
4948
currentWord := string(line[wordStart:pos])
5049

51-
// Debug logging
52-
log.Printf("[DEBUG] Tab completion: line='%s', pos=%d, currentWord='%s', wordStart=%d", lineStr, pos, currentWord, wordStart)
53-
5450
// Get completions using our existing logic
5551
completions := Completer(lineStr, c.hosts, c.user)
56-
log.Printf("[DEBUG] Got %d raw completions: %v", len(completions), completions)
5752

5853
// Filter and process completions
5954
var filtered []string
@@ -64,23 +59,13 @@ func (c *customCompleter) Do(line []rune, pos int) ([][]rune, int) {
6459
}
6560

6661
// For file completions, we need to handle the path properly
67-
if strings.Contains(currentWord, "/") {
68-
// For paths like "/v", we expect completions like "/var", "/vagrant", etc.
69-
// readline expects only the suffix that completes the current word
70-
if strings.HasPrefix(completion, currentWord) {
71-
// Extract only the part that should be appended
72-
// For currentWord="/v" and completion="/var", we want "ar"
73-
suffix := completion[len(currentWord):]
74-
filtered = append(filtered, suffix)
75-
}
76-
} else {
77-
// For simple completions (like command names), extract suffix
78-
if strings.HasPrefix(completion, currentWord) {
79-
// Extract only the part that should be appended
80-
// For currentWord="l" and completion="ls", we want "s"
81-
suffix := completion[len(currentWord):]
82-
filtered = append(filtered, suffix)
83-
}
62+
// Both path and simple completions use the same suffix extraction logic
63+
if strings.HasPrefix(completion, currentWord) {
64+
// Extract only the part that should be appended
65+
// For currentWord="/v" and completion="/var", we want "ar"
66+
// For currentWord="l" and completion="ls", we want "s"
67+
suffix := completion[len(currentWord):]
68+
filtered = append(filtered, suffix)
8469
}
8570
}
8671

@@ -90,7 +75,6 @@ func (c *customCompleter) Do(line []rune, pos int) ([][]rune, int) {
9075
result[i] = []rune(completion)
9176
}
9277

93-
log.Printf("[DEBUG] Returning %d filtered completions: %v, wordStart=%d", len(filtered), filtered, wordStart)
9478
// Return completions and the position where the word starts
9579
return result, wordStart
9680
}
@@ -103,7 +87,6 @@ func getSSHCompletions(line string, hosts []string, user string) []string {
10387

10488
// Use the first host for completion
10589
host := hosts[0]
106-
log.Printf("[DEBUG] Using host '%s' for completion (first of %d hosts)", host, len(hosts))
10790

10891
// Extract the word being completed
10992
words := strings.Fields(line)
@@ -143,8 +126,6 @@ func getSSHCompletions(line string, hosts []string, user string) []string {
143126
}
144127
}
145128

146-
log.Printf("[DEBUG] Parsed path: dir='%s', pattern='%s' from lastWord='%s'", dir, pattern, lastWord)
147-
148129
// Build the correct completion command
149130
if pattern == "" {
150131
compCommand = fmt.Sprintf("ls -1a '%s'/ 2>/dev/null | head -20", dir)
@@ -160,22 +141,16 @@ func getSSHCompletions(line string, hosts []string, user string) []string {
160141

161142
args = append(args, host, compCommand)
162143

163-
log.Printf("[DEBUG] SSH completion command: ssh %v", args)
164-
165144
// Create context with timeout
166145
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
167146
defer cancel()
168147

169148
cmd := exec.CommandContext(ctx, "ssh", args...)
170149
output, err := cmd.CombinedOutput()
171-
172150
if err != nil {
173-
log.Printf("[DEBUG] SSH completion failed: %v", err)
174151
return []string{}
175152
}
176153

177-
// log.Printf("[DEBUG] SSH completion output: %s", string(output))
178-
179154
// Parse the output and return as suggestions
180155
completions := []string{}
181156
lines := strings.Split(strings.TrimSpace(string(output)), "\n")

pkg/main_test.go

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ func TestFormatHostColorCycling(t *testing.T) {
7676
noColor := false
7777

7878
results := make([]string, len(Colors))
79-
for i := 0; i < len(Colors); i++ {
79+
for i := range Colors {
8080
results[i] = FormatHost(host, i, maxLen, noColor)
8181
}
8282

@@ -322,7 +322,7 @@ func TestExecuteCommand(t *testing.T) {
322322
}
323323

324324
for _, test := range tests {
325-
t.Run(test.name, func(t *testing.T) {
325+
t.Run(test.name, func(_ *testing.T) {
326326
// This test verifies the function doesn't panic and completes
327327
// Actual SSH execution is tested in integration tests
328328
ExecuteCommand(test.hosts, test.command, test.user, test.noColor)
@@ -334,7 +334,7 @@ func TestUploadFile(t *testing.T) {
334334
// Create a temporary test file
335335
tempFile := "/tmp/gosh_test_file.txt"
336336
testContent := "test file content"
337-
err := os.WriteFile(tempFile, []byte(testContent), 0644)
337+
err := os.WriteFile(tempFile, []byte(testContent), 0o600)
338338
if err != nil {
339339
t.Fatalf("Failed to create test file: %v", err)
340340
}
@@ -354,7 +354,7 @@ func TestUploadFile(t *testing.T) {
354354
}
355355

356356
for _, test := range tests {
357-
t.Run(test.name, func(t *testing.T) {
357+
t.Run(test.name, func(_ *testing.T) {
358358
// This test verifies the function doesn't panic and handles file existence
359359
// Actual SCP execution is tested in integration tests
360360
UploadFile(test.hosts, test.filepath, test.user, test.noColor)
@@ -365,18 +365,17 @@ func TestUploadFile(t *testing.T) {
365365
func TestRunCmdWithSeparateOutput(t *testing.T) {
366366
tests := []struct {
367367
name string
368-
command string
369-
args []string
368+
setup func() *exec.Cmd
370369
wantErr bool
371370
}{
372-
{"echo success", "echo", []string{"hello"}, false},
373-
{"false command", "false", []string{}, true},
374-
{"nonexistent command", "nonexistent_command_12345", []string{}, true},
371+
{"echo success", func() *exec.Cmd { return exec.CommandContext(context.Background(), "echo", "hello") }, false},
372+
{"false command", func() *exec.Cmd { return exec.CommandContext(context.Background(), "false") }, true},
373+
{"nonexistent command", func() *exec.Cmd { return exec.CommandContext(context.Background(), "nonexistent_command_12345") }, true},
375374
}
376375

377376
for _, test := range tests {
378377
t.Run(test.name, func(t *testing.T) {
379-
cmd := exec.Command(test.command, test.args...)
378+
cmd := test.setup()
380379
stdout, stderr, err := runCmdWithSeparateOutput(cmd)
381380

382381
if test.wantErr && err == nil {
@@ -537,7 +536,7 @@ func TestCompleter(t *testing.T) {
537536
func TestGetLocalFileCompletions(t *testing.T) {
538537
// Create temporary files for testing
539538
tempDir := "/tmp/gosh_test_completions"
540-
err := os.MkdirAll(tempDir, 0755)
539+
err := os.MkdirAll(tempDir, 0o755)
541540
if err != nil {
542541
t.Fatalf("Failed to create temp dir: %v", err)
543542
}
@@ -551,11 +550,11 @@ func TestGetLocalFileCompletions(t *testing.T) {
551550
// Create test files
552551
testFiles := []string{"test1.txt", "test2.log", "script.sh", "data.csv"}
553552
for _, file := range testFiles {
554-
os.WriteFile(file, []byte("test"), 0644)
553+
os.WriteFile(file, []byte("test"), 0o600)
555554
}
556555

557556
// Create test directory
558-
os.Mkdir("testdir", 0755)
557+
os.Mkdir("testdir", 0o755)
559558

560559
tests := []struct {
561560
name string

readme.md

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,17 @@ Go-based multi SSH session manager for parallel command execution.
55
## Features
66

77
- Execute commands on multiple hosts simultaneously
8-
- Interactive mode with command history
8+
- Interactive mode with tab completion and command history
99
- Colored output for easy host identification
1010
- SSH connection timeouts and error handling
11+
- File upload capability (`:upload` command)
12+
- Connection testing and verbose output
13+
- Support for dynamic host discovery
1114

1215
## Installation
1316

1417
```sh
15-
go install github.com/innogames/gosh@latest
18+
go install github.com/brainexe/gosh@latest
1619
# or build from source
1720
make build
1821
```
@@ -21,16 +24,42 @@ make build
2124

2225
**Single command execution:**
2326
```bash
24-
gosh -c "uptime" server1 server2 server3
27+
gosh -c "uptime" server{1..3}
28+
gosh -c "df -h" $(cat hosts.txt)
2529
gosh -u user -c "df -h" web01 web02 db01
2630
```
2731

2832
**Interactive mode:**
2933
```bash
30-
gosh server1 server2
31-
gosh> uptime
32-
gosh> ps aux | grep nginx
33-
gosh> exit
34+
gosh server{1..3}
35+
🖥️ [3]> uptime
36+
web01: 14:23:15 up 45 days, 3:45, 1 user, load average: 0.15, 0.08, 0.06
37+
web02: 14:23:15 up 45 days, 3:45, 1 user, load average: 0.12, 0.07, 0.05
38+
web03: 14:23:15 up 45 days, 3:45, 1 user, load average: 0.18, 0.09, 0.07
39+
🖥️ [3]> ps aux | grep nginx
40+
web01: root 1234 0.0 0.1 45678 2345 ? Ss 10:30 0:00 nginx: master process /usr/sbin/nginx
41+
web01: www-data 1235 0.0 0.0 45678 1234 ? S 10:30 0:00 nginx: worker process
42+
web02: root 1234 0.0 0.1 45678 2345 ? Ss 10:30 0:00 nginx: master process /usr/sbin/nginx
43+
web02: www-data 1235 0.0 0.0 45678 1234 ? S 10:30 0:00 nginx: worker process
44+
web03: root 1234 0.0 0.1 45678 2345 ? Ss 10:30 0:00 nginx: master process /usr/sbin/nginx
45+
web03: www-data 1235 0.0 0.0 45678 1234 ? S 10:30 0:00 nginx: worker process
46+
🖥️ [3]> :upload deploy.sh
47+
web01: ✅ Upload successful: deploy.sh
48+
web02: ✅ Upload successful: deploy.sh
49+
web03: ✅ Upload successful: deploy.sh
50+
🖥️ [3]> exit
51+
```
52+
53+
**Dynamic host discovery:**
54+
```bash
55+
# Using command substitution for dynamic host lists
56+
./build/gosh $(cat hosts.txt)
57+
🖥️ [5]> hostname
58+
web01.local: web01.local
59+
web02..local: web02.local
60+
web03.local: web03.local
61+
web04.local: web04.local
62+
web05.local: web05.local
3463
```
3564

3665
**Common examples:**
@@ -46,11 +75,26 @@ gosh -c "tail -f /var/log/app.log" app{01..03}
4675

4776
# System health check
4877
gosh --no-color -c "uptime && free -h" $(cat hosts.txt)
78+
79+
# Upload and execute scripts
80+
gosh web01 web02
81+
🖥️ [2]> :upload backup.sh
82+
🖥️ [2]> chmod +x backup.sh && ./backup.sh
83+
84+
# Check system load and memory
85+
gosh -v -c "uptime && free -h" prod{01..10}
4986
```
5087

88+
## Interactive Commands
89+
90+
- `help` - Show available commands
91+
- `exit`/`quit` - Exit interactive mode
92+
- `:upload <file>` - Upload file to all connected hosts
93+
- `<command>` - Execute any command on all hosts
94+
5195
## Options
5296

5397
- `-c, --command` - Command to execute on all hosts
5498
- `-u, --user` - SSH username (default: current user)
5599
- `--no-color` - Disable colored output
56-
- `-v, --verbose` - Enable verbose logging
100+
- `-v, --verbose` - Enable verbose logging and connection testing

0 commit comments

Comments
 (0)