/
pshell.go
218 lines (180 loc) · 5.91 KB
/
pshell.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
// Copyright (c) 2022 Blacknon. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
package ssh
import (
"fmt"
"log"
"os"
"os/signal"
"strconv"
"strings"
"syscall"
"github.com/blacknon/go-sshlib"
"github.com/blacknon/lssh/output"
"github.com/c-bata/go-prompt"
)
// TODO(blacknon): 接続が切れた場合の再接続処理、および再接続ができなかった場合のsliceからの削除対応の追加(v0.6.1)
// TODO(blacknon): pShellのログ(実行コマンド及び出力結果)をログとしてファイルに記録する機能の追加(v0.6.1)
// TODO(blacknon): グループ化(`()`で囲んだりする)や三項演算子への対応(v0.6.1)
// TODO(blacknon): `サーバ名:command...` で、指定したサーバでのみコマンドを実行させる機能の追加(v0.6.1)
// TODO(blacknon):
// 出力をvim diffに食わせてdiffを得られるようにしたい => 変数かプロセス置換か、なにかしらの方法でローカルコマンド実行時にssh経由で得られた出力を食わせる方法を実装する?
// => 多分、プロセス置換が良いんだと思う(プロセス置換時にssh先でコマンドを実行できるように、かつ実行したデータを個別にファイルとして扱えるようにしたい)
// ```bash
// !vim diff <(cat /etc/passwd)
// => !vim diff host1:/etc/passwd host2:/etc/passwd ....
// ```
// やるなら普通に一時ファイルに書き出すのが良さそう(/tmp 配下とか。一応、ちゃんと権限周り気をつけないといかんね、というのと消さないといかんね、というお気持ち)
// Pshell is Parallel-Shell struct
type pShell struct {
Signal chan os.Signal
Count int
ServerList []string
Connects []*psConnect
PROMPT string
History map[int]map[string]*pShellHistory
HistoryFile string
latestCommand string
CmdComplete []prompt.Suggest
PathComplete []prompt.Suggest
Options pShellOption
}
// pShellOption is optitons pshell.
// TODO(blacknon): つくる。
type pShellOption struct {
// local command実行時の結果をHistoryResultに記録しない(os.Stdoutに直接出す)
LocalCommandNotRecordResult bool
// trueの場合、リモートマシンでパイプライン処理をする際にパイプ経由でもOPROMPTを付与して出力する
// RemoteHeaderWithPipe bool
// trueの場合、リモートマシンにキーインプットを送信しない
// hogehoge
// trueの場合、コマンドの補完処理を無効にする
// DisableCommandComplete bool
// trueの場合、PATHの補完処理を無効にする
// DisableCommandComplete bool
}
// psConnect is pShell connect struct.
type psConnect struct {
Name string
Output *output.Output
*sshlib.Connect
}
// variable
var (
// Default PROMPT
defaultPrompt = "[${COUNT}] <<< "
// Default OPROMPT
defaultOPrompt = "[${SERVER}][${COUNT}] > "
// Default Parallel shell history file
defaultHistoryFile = "~/.lssh_history"
)
func (r *Run) pshell() (err error) {
// print header
fmt.Println("Start parallel-shell...")
r.PrintSelectServer()
// read shell config
config := r.Conf.Shell
// overwrite default value config.Prompt
if config.Prompt == "" {
config.Prompt = defaultPrompt
}
// overwrite default value config.OPrompt
if config.OPrompt == "" {
config.OPrompt = defaultOPrompt
}
// overwrite default parallel shell history file
if config.HistoryFile == "" {
config.HistoryFile = defaultHistoryFile
}
// run pre cmd
execLocalCommand(config.PreCmd)
defer execLocalCommand(config.PostCmd)
// Connect
var cons []*psConnect
for _, server := range r.ServerList {
// Create *sshlib.Connect
con, err := r.CreateSshConnect(server)
if err != nil {
log.Println(err)
continue
}
// TTY enable
con.TTY = true
// Create Output
o := &output.Output{
Templete: config.OPrompt,
ServerList: r.ServerList,
Conf: r.Conf.Server[server],
AutoColor: true,
}
// Create output prompt
o.Create(server)
psCon := &psConnect{
Name: server,
Output: o,
Connect: con,
}
cons = append(cons, psCon)
}
// count sshlib.Connect.
if len(cons) == 0 {
return
}
// create new shell struct
ps := &pShell{
Signal: make(chan os.Signal),
ServerList: r.ServerList,
Connects: cons,
PROMPT: config.Prompt,
History: map[int]map[string]*pShellHistory{},
HistoryFile: config.HistoryFile,
}
// set signal
// TODO: Windows対応
// - 参考: https://cad-san.hatenablog.com/entry/2017/01/09/170213
signal.Notify(ps.Signal, syscall.SIGTERM, syscall.SIGINT, os.Interrupt)
// old history list
var historyCommand []string
oldHistory, err := ps.GetHistoryFromFile()
if err == nil {
for _, h := range oldHistory {
historyCommand = append(historyCommand, h.Command)
}
}
// create complete data
// TODO(blacknon): 定期的に裏で取得するよう処理を加える(v0.6.1)
ps.GetCommandComplete()
// create go-prompt
p := prompt.New(
ps.Executor,
ps.Completer,
prompt.OptionHistory(historyCommand),
prompt.OptionLivePrefix(ps.CreatePrompt),
prompt.OptionInputTextColor(prompt.Green),
prompt.OptionPrefixTextColor(prompt.Blue),
prompt.OptionCompletionWordSeparator("/: \\"), // test
)
// start go-prompt
p.Run()
return
}
// CreatePrompt is create shell prompt.
// default value is `[${COUNT}] <<< `
func (ps *pShell) CreatePrompt() (p string, result bool) {
// set prompt templete (from conf)
p = ps.PROMPT
if p == "" {
p = defaultPrompt
}
// Get env
hostname, _ := os.Hostname()
username := os.Getenv("USER")
pwd := os.Getenv("PWD")
// replace variable value
p = strings.Replace(p, "${COUNT}", strconv.Itoa(ps.Count), -1)
p = strings.Replace(p, "${HOSTNAME}", hostname, -1)
p = strings.Replace(p, "${USER}", username, -1)
p = strings.Replace(p, "${PWD}", pwd, -1)
return p, true
}