github.com/clusterize-io/tusk@v0.6.3-0.20211001020217-cfe8a8cd0d4a/appcli/install_completion.go (about) 1 package appcli 2 3 import ( 4 "bufio" 5 "fmt" 6 "io/ioutil" 7 "os" 8 "path/filepath" 9 "regexp" 10 11 // Embed completion scripts. 12 _ "embed" 13 14 "github.com/clusterize-io/tusk/runner" 15 "github.com/clusterize-io/tusk/ui" 16 ) 17 18 //go:embed completion/tusk-completion.bash 19 var rawBashCompletion string 20 21 //go:embed completion/tusk.fish 22 var rawFishCompletion string 23 24 //go:embed completion/_tusk 25 var rawZshCompletion string 26 27 const ( 28 bashCompletionFile = "tusk-completion.bash" 29 fishCompletionFile = "tusk.fish" 30 zshCompletionFile = "_tusk" 31 zshInstallDir = "/usr/local/share/zsh/site-functions" 32 ) 33 34 var bashRCFiles = []string{".bashrc", ".bash_profile", ".profile"} 35 36 // InstallCompletion installs command line tab completion for a given shell. 37 func InstallCompletion(meta *runner.Metadata) error { 38 shell := meta.InstallCompletion 39 switch shell { 40 case "bash": 41 return installBashCompletion(meta.Logger) 42 case "fish": 43 return installFishCompletion(meta.Logger) 44 case "zsh": 45 return installZshCompletion(meta.Logger, zshInstallDir) 46 default: 47 return fmt.Errorf("tab completion for %q is not supported", shell) 48 } 49 } 50 51 // UninstallCompletion uninstalls command line tab completion for a given shell. 52 func UninstallCompletion(meta *runner.Metadata) error { 53 shell := meta.UninstallCompletion 54 switch shell { 55 case "bash": 56 return uninstallBashCompletion() 57 case "fish": 58 return uninstallFishCompletion() 59 case "zsh": 60 return uninstallZshCompletion(zshInstallDir) 61 default: 62 return fmt.Errorf("tab completion for %q is not supported", shell) 63 } 64 } 65 66 func installBashCompletion(logger *ui.Logger) error { 67 dir, err := getDataDir() 68 if err != nil { 69 return err 70 } 71 72 err = installFileInDir(logger, dir, bashCompletionFile, []byte(rawBashCompletion)) 73 if err != nil { 74 return err 75 } 76 77 rcfile, err := getBashRCFile() 78 if err != nil { 79 return err 80 } 81 82 slashPath := filepath.ToSlash(filepath.Join(dir, bashCompletionFile)) 83 command := fmt.Sprintf("source %q", slashPath) 84 return appendIfAbsent(rcfile, command) 85 } 86 87 func getBashRCFile() (string, error) { 88 homedir, err := os.UserHomeDir() 89 if err != nil { 90 return "", err 91 } 92 93 for _, rcfile := range bashRCFiles { 94 path := filepath.Join(homedir, rcfile) 95 if _, err := os.Stat(path); err != nil { 96 if os.IsNotExist(err) { 97 continue 98 } 99 100 return "", err 101 } 102 103 return path, nil 104 } 105 106 return filepath.Join(homedir, ".bashrc"), nil 107 } 108 109 func appendIfAbsent(path, text string) error { 110 // nolint: gosec 111 f, err := os.OpenFile(path, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0644) 112 if err != nil { 113 return err 114 } 115 defer f.Close() // nolint: errcheck 116 117 scanner := bufio.NewScanner(f) 118 119 prependNewline := false 120 for scanner.Scan() { 121 switch scanner.Text() { 122 case text: 123 return nil 124 case "": 125 prependNewline = false 126 default: 127 prependNewline = true 128 } 129 } 130 if serr := scanner.Err(); serr != nil { 131 return serr 132 } 133 134 if prependNewline { 135 text = "\n" + text 136 } 137 138 _, err = fmt.Fprintln(f, text) 139 return err 140 } 141 142 func uninstallBashCompletion() error { 143 dir, err := getDataDir() 144 if err != nil { 145 return err 146 } 147 148 err = uninstallFileFromDir(dir, bashCompletionFile) 149 if err != nil { 150 return err 151 } 152 153 rcfile, err := getBashRCFile() 154 if err != nil { 155 return err 156 } 157 158 re := regexp.MustCompile(fmt.Sprintf(`source ".*/%s"`, bashCompletionFile)) 159 return removeLineInFile(rcfile, re) 160 } 161 162 func removeLineInFile(path string, re *regexp.Regexp) error { 163 rf, err := os.OpenFile(path, os.O_RDONLY, 0644) // nolint: gosec 164 if err != nil { 165 return err 166 } 167 defer rf.Close() // nolint: errcheck 168 169 wf, err := ioutil.TempFile("", ".profile.tusk.bkp") 170 if err != nil { 171 return err 172 } 173 defer wf.Close() // nolint: errcheck 174 175 scanner := bufio.NewScanner(rf) 176 177 buf := "" 178 for scanner.Scan() { 179 line := scanner.Text() 180 switch { 181 case re.MatchString(line): 182 continue 183 case line == "": 184 buf += "\n" 185 continue 186 } 187 188 _, err := fmt.Fprintln(wf, buf+line) 189 if err != nil { 190 return err 191 } 192 193 buf = "" 194 } 195 if serr := scanner.Err(); serr != nil { 196 return serr 197 } 198 199 rf.Close() // nolint: errcheck 200 wf.Close() // nolint: errcheck 201 return os.Rename(wf.Name(), path) 202 } 203 204 func installFishCompletion(logger *ui.Logger) error { 205 dir, err := getFishCompletionsDir() 206 if err != nil { 207 return err 208 } 209 210 return installFileInDir(logger, dir, fishCompletionFile, []byte(rawFishCompletion)) 211 } 212 213 func uninstallFishCompletion() error { 214 dir, err := getFishCompletionsDir() 215 if err != nil { 216 return err 217 } 218 219 return uninstallFileFromDir(dir, fishCompletionFile) 220 } 221 222 func getDataDir() (string, error) { 223 if xdgHome := os.Getenv("XDG_DATA_HOME"); xdgHome != "" { 224 return filepath.Join(xdgHome, "tusk"), nil 225 } 226 227 homedir, err := os.UserHomeDir() 228 if err != nil { 229 return "", err 230 } 231 232 return filepath.Join(homedir, ".local", "share", "tusk"), nil 233 } 234 235 // getFishCompletionsDir gets the directory to place completions in, adhering 236 // to the XDG base directory. 237 func getFishCompletionsDir() (string, error) { 238 if xdgHome := os.Getenv("XDG_CONFIG_HOME"); xdgHome != "" { 239 return filepath.Join(xdgHome, "fish", "completions"), nil 240 } 241 242 homedir, err := os.UserHomeDir() 243 if err != nil { 244 return "", err 245 } 246 247 return filepath.Join(homedir, ".config", "fish", "completions"), nil 248 } 249 250 func installZshCompletion(logger *ui.Logger, dir string) error { 251 return installFileInDir(logger, dir, zshCompletionFile, []byte(rawZshCompletion)) 252 } 253 254 func installFileInDir(logger *ui.Logger, dir, file string, content []byte) error { 255 // nolint: gosec 256 if err := os.MkdirAll(dir, 0755); err != nil { 257 return err 258 } 259 260 target := filepath.Join(dir, file) 261 262 // nolint: gosec 263 if err := ioutil.WriteFile(target, content, 0644); err != nil { 264 return err 265 } 266 267 logger.Info("Tab completion successfully installed", target) 268 logger.Info("You may need to restart your shell for completion to take effect") 269 return nil 270 } 271 272 func uninstallZshCompletion(dir string) error { 273 return uninstallFileFromDir(dir, zshCompletionFile) 274 } 275 276 func uninstallFileFromDir(dir, file string) error { 277 err := os.Remove(filepath.Join(dir, file)) 278 if !os.IsNotExist(err) { 279 return err 280 } 281 282 return nil 283 }