-
Notifications
You must be signed in to change notification settings - Fork 239
/
install.go
187 lines (167 loc) · 4.68 KB
/
install.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
package complete
import (
"flag"
"fmt"
"io/ioutil"
"os"
"os/user"
"path/filepath"
"regexp"
"strings"
)
//
// This code is largely inspired by the github.com/posener/complete/cmd/install package
//
// + On install, will add COMP_WORDBREAKS if it's not already there
// + On uninstall, remove only what's added and nothing else
// - On uninstall, will not save/backup the unmodified file (FIXME)
// - Less modular/magical, no interfaces etc.
//
type shellSetupType struct {
configFile string
requiredLines []string
}
var shellSetups = map[string]shellSetupType{
"bash": {
configFile: ".bashrc",
requiredLines: []string{
"COMP_WORDBREAKS=${COMP_WORDBREAKS//:}",
"complete -C %binary %command",
},
},
"zsh": {
configFile: ".zshrc",
requiredLines: []string{
// From http://blogs.perl.org/users/perlancar/2014/11/comparing-programmable-tab-completion-in-bash-zsh-tcsh-and-fish.html
`_s5cmd_completer() { read -l; local cl="$REPLY"; read -ln; local cp="$REPLY"; reply=(` + "`" + `COMP_SHELL=zsh COMP_LINE="$cl" COMP_POINT="$cp" %binary` + "`" + `) }`,
`compctl -K _s5cmd_completer s5cmd`,
},
},
}
const (
setupPrefix = "# start s5cmd -- Lines below are added by s5cmd -cmp-install"
setupComment = "# To automatically uninstall, do not remove these comments and run s5cmd -cmp-uninstall"
setupSuffix = "# end s5cmd"
setupRegex = `(?ms)(^\n?# start s5cmd.+?# end s5cmd\n?$)`
)
func configFiles() (ret []string) {
for _, se := range shellSetups {
ret = append(ret, se.configFile)
}
return ret
}
func prepareLine(line, binary string) string {
line = strings.Replace(line, "%binary", binary, -1)
line = strings.Replace(line, "%command", "s5cmd", -1)
return line
}
func setupCompletion(install bool) error {
verb := "install"
if !install {
verb = "uninstall"
}
binPath, err := getBinaryPath()
if err != nil {
return err
}
promptUser := flag.Arg(0) != "assume-yes" // We don't add the flag as a flag, so that it won't clutter the help text
foundOne := false
takenAction := false
for shellName, setup := range shellSetups {
cfgFile := findExistingFileInHomeDir(setup.configFile)
if cfgFile == "" {
fmt.Printf("Could not find %s, skipping for %s\n", setup.configFile, shellName)
continue
}
foundOne = true
var linesMissing []string
for _, line := range setup.requiredLines {
line = prepareLine(line, binPath)
if !lineInFile(cfgFile, line) {
linesMissing = append(linesMissing, line)
}
}
if install && len(linesMissing) == 0 {
fmt.Printf("Already installed in %s\n", setup.configFile)
continue
}
if !install {
if len(linesMissing) == len(setup.requiredLines) {
fmt.Printf("Already not installed in %s\n", setup.configFile)
continue
}
prefixFound := lineInFile(cfgFile, setupPrefix)
suffixFound := lineInFile(cfgFile, setupSuffix)
if !prefixFound || !suffixFound {
fmt.Printf("Could not find automatic comments in %s, will not auto-uninstall for %s\n", setup.configFile, shellName)
continue
}
}
if promptUser {
fmt.Printf("%s shell completion for %s? This will modify your ~/%s [y/N] ", strings.Title(verb), shellName, setup.configFile)
var answer string
fmt.Scanln(&answer)
switch strings.ToLower(answer) {
case "y", "yes":
// no-op
default:
continue
}
}
if install {
linesToAdd := []string{setupPrefix, setupComment}
linesToAdd = append(linesToAdd, linesMissing...)
linesToAdd = append(linesToAdd, setupSuffix)
appendToFile(cfgFile, strings.Join(linesToAdd, "\n"))
fmt.Printf("Installed for %s\n", shellName)
} else {
r := regexp.MustCompile(setupRegex)
contents, err := ioutil.ReadFile(cfgFile)
if err != nil {
return err
}
newContents := r.ReplaceAll(contents, nil)
if len(newContents) == len(contents) {
fmt.Printf("Error processing %s: regex did not match the comments\n", setup.configFile)
continue
}
err = ioutil.WriteFile(cfgFile, newContents, os.ModePerm)
if err != nil {
fmt.Printf("Error processing %s: %s\n", setup.configFile, err.Error())
continue
} else {
fmt.Printf("Uninstalled from %s\n", setup.configFile)
}
}
takenAction = true
}
if !foundOne {
return fmt.Errorf("Could not find %s in home directory", strings.Join(configFiles(), " or "))
}
if !takenAction {
fmt.Println("No action taken")
}
return nil
}
func findExistingFileInHomeDir(name string) string {
u, err := user.Current()
if err != nil {
return ""
}
fn := filepath.Join(u.HomeDir, name)
st, err := os.Stat(fn)
if err != nil {
return ""
}
if st.IsDir() {
return ""
}
return fn
}
func getBinaryPath() (string, error) {
bin, err := os.Executable()
if err != nil {
return "", err
}
return filepath.Abs(bin)
}