Mercurial > projects > sievemgr
comparison cmd/sievemgr/edit.go @ 8:8caacf702c0d
Add edit subcommand for interactive editing of a script
author | Guido Berhoerster <guido+sievemgr@berhoerster.name> |
---|---|
date | Fri, 13 Nov 2020 10:49:08 +0100 |
parents | |
children | 29769b9e2f09 |
comparison
equal
deleted
inserted
replaced
7:3abc8be485c0 | 8:8caacf702c0d |
---|---|
1 // Copyright (C) 2020 Guido Berhoerster <guido+sievemgr@berhoerster.name> | |
2 // | |
3 // Permission is hereby granted, free of charge, to any person obtaining | |
4 // a copy of this software and associated documentation files (the | |
5 // "Software"), to deal in the Software without restriction, including | |
6 // without limitation the rights to use, copy, modify, merge, publish, | |
7 // distribute, sublicense, and/or sell copies of the Software, and to | |
8 // permit persons to whom the Software is furnished to do so, subject to | |
9 // the following conditions: | |
10 // | |
11 // The above copyright notice and this permission notice shall be included | |
12 // in all copies or substantial portions of the Software. | |
13 // | |
14 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |
15 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | |
16 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. | |
17 // IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY | |
18 // CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, | |
19 // TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE | |
20 // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | |
21 | |
22 package main | |
23 | |
24 import ( | |
25 "fmt" | |
26 "io" | |
27 "io/ioutil" | |
28 "os" | |
29 "os/exec" | |
30 "path/filepath" | |
31 "runtime" | |
32 "strings" | |
33 | |
34 "go.guido-berhoerster.org/managesieve" | |
35 "go.guido-berhoerster.org/sievemgr/cmd/sievemgr/internal/config" | |
36 "golang.org/x/crypto/ssh/terminal" | |
37 ) | |
38 | |
39 func init() { | |
40 cmdEdit.Flag.StringVar(&acctName, "a", "", "Select the account") | |
41 } | |
42 | |
43 var cmdEdit = &command{ | |
44 UsageLine: "edit [options] name", | |
45 Run: runEdit, | |
46 } | |
47 | |
48 func promptYesNo(prompt string) (yesNo bool, err error) { | |
49 var tty *os.File | |
50 var r io.Reader | |
51 var w io.Writer | |
52 if runtime.GOOS == "windows" { | |
53 r = os.Stdin | |
54 w = os.Stdout | |
55 } else { | |
56 tty, err = os.OpenFile("/dev/tty", os.O_RDWR, 0666) | |
57 if err != nil { | |
58 return | |
59 } | |
60 r = tty | |
61 w = tty | |
62 defer tty.Close() | |
63 } | |
64 | |
65 for { | |
66 io.WriteString(w, prompt) | |
67 | |
68 var response string | |
69 _, err = fmt.Fscanln(r, &response) | |
70 if err != nil { | |
71 err = fmt.Errorf("failed to read response: %s", err) | |
72 return | |
73 } | |
74 | |
75 switch strings.ToLower(response) { | |
76 case "y", "yes": | |
77 yesNo = true | |
78 return | |
79 case "n", "no": | |
80 return | |
81 } | |
82 } | |
83 } | |
84 | |
85 func getScript(acct *config.Account, scriptName string) (string, error) { | |
86 c, err := dialPlainAuth(acct) | |
87 if err != nil { | |
88 return "", err | |
89 } | |
90 defer c.Logout() | |
91 | |
92 return c.GetScript(scriptName) | |
93 } | |
94 | |
95 func readScript(filename string) (string, error) { | |
96 f, err := os.Open(filename) | |
97 if err != nil { | |
98 return "", err | |
99 } | |
100 defer f.Close() | |
101 | |
102 return readLimitedString(f, managesieve.ReadLimit) | |
103 } | |
104 | |
105 func putScript(acct *config.Account, scriptName, script string) (string, error) { | |
106 c, err := dialPlainAuth(acct) | |
107 if err != nil { | |
108 return "", err | |
109 } | |
110 defer c.Logout() | |
111 | |
112 return c.PutScript(scriptName, script) | |
113 } | |
114 | |
115 func runEdit(cmd *command, args []string) error { | |
116 if len(args) != 1 { | |
117 return usageError("invalid number of arguments") | |
118 } | |
119 | |
120 scriptName := args[0] | |
121 | |
122 if !terminal.IsTerminal(int(os.Stdin.Fd())) || | |
123 !terminal.IsTerminal(int(os.Stdout.Fd())) { | |
124 return fmt.Errorf("the edit subcommand can only be used interactively\n") | |
125 } | |
126 | |
127 editor := os.Getenv("EDITOR") | |
128 if editor == "" { | |
129 return fmt.Errorf("EDITOR environment variable not set") | |
130 } | |
131 | |
132 acct, err := getAccount(&conf, acctName) | |
133 if err != nil { | |
134 return err | |
135 } | |
136 | |
137 if err := lookupHostPort(acct); err != nil { | |
138 return err | |
139 } | |
140 | |
141 if err := readPassword(acct); err != nil { | |
142 return err | |
143 } | |
144 | |
145 c, err := dialPlainAuth(acct) | |
146 if err != nil { | |
147 return err | |
148 } | |
149 defer c.Logout() | |
150 | |
151 script, err := getScript(acct, scriptName) | |
152 if err != nil { | |
153 return err | |
154 } | |
155 | |
156 tmpDir, err := ioutil.TempDir(os.TempDir(), "sievemgr*") | |
157 if err != nil { | |
158 return fmt.Errorf("failed to create temporary directory: %s", err) | |
159 } | |
160 | |
161 tmpFile := filepath.Join(tmpDir, scriptName) | |
162 if err = ioutil.WriteFile(tmpFile, []byte(script), 0640); err != nil { | |
163 return fmt.Errorf("failed to create script file: %s", err) | |
164 } | |
165 defer func() { | |
166 // show filename if an error has occured and the file is | |
167 // preserved | |
168 if tmpFile != "" { | |
169 fmt.Fprintf(os.Stderr, | |
170 "the script has been preserved as %s\n", | |
171 tmpFile) | |
172 } | |
173 }() | |
174 | |
175 // modification time is used to detect changes | |
176 info, err := os.Stat(tmpFile) | |
177 if err != nil { | |
178 return fmt.Errorf("failed to stat file: %s", err) | |
179 } | |
180 origModTime := info.ModTime() | |
181 | |
182 for { | |
183 cmd := exec.Command(editor, tmpFile) | |
184 cmd.Stdin = os.Stdin | |
185 cmd.Stdout = os.Stdout | |
186 cmd.Stderr = os.Stderr | |
187 if err := cmd.Run(); err != nil { | |
188 return fmt.Errorf("failed to run editor: %s", err) | |
189 } | |
190 | |
191 // quit if the script has not been changed | |
192 if info, err := os.Stat(tmpFile); err != nil { | |
193 return fmt.Errorf("failed to stat file: %s", err) | |
194 } else if info.ModTime() == origModTime { | |
195 fmt.Fprintln(os.Stderr, "aborting, script was not modified") | |
196 break | |
197 } | |
198 | |
199 script, err = readScript(tmpFile) | |
200 if err != nil { | |
201 return fmt.Errorf("failed to read script: %s", err) | |
202 } | |
203 | |
204 warnings, err := putScript(acct, scriptName, script) | |
205 if err != nil { | |
206 // show error and try again if the script was rejected | |
207 // by the server | |
208 fmt.Fprintln(os.Stderr, err) | |
209 yesNo, err := promptYesNo("edit again [y/n]? ") | |
210 if err != nil { | |
211 return err | |
212 } else if !yesNo { | |
213 return fmt.Errorf("script not saved") | |
214 } | |
215 } else if warnings != "" { | |
216 fmt.Fprintln(os.Stderr, warnings) | |
217 break | |
218 } else { | |
219 break | |
220 } | |
221 } | |
222 | |
223 os.RemoveAll(tmpDir) | |
224 tmpFile = "" | |
225 | |
226 return nil | |
227 } |