github.com/quantumghost/awgo@v0.15.0/util/scripts.go (about) 1 // 2 // Copyright (c) 2018 Dean Jackson <deanishe@deanishe.net> 3 // 4 // MIT Licence. See http://opensource.org/licenses/MIT 5 // 6 // Created on 2018-02-10 7 // 8 9 package util 10 11 import ( 12 "bytes" 13 "encoding/json" 14 "errors" 15 "log" 16 "os" 17 "os/exec" 18 "path/filepath" 19 "strings" 20 ) 21 22 // ErrUnknownFileType is returned by Run for files it can't identify. 23 var ErrUnknownFileType = errors.New("unknown filetype") 24 25 // Default Runners used by Run to determine how to execute a file. 26 var ( 27 Executable Runner // run executable files directly 28 Script Runner // run script files with commands from Interpreters 29 30 // DefaultInterpreters maps script file extensions to interpreters. 31 // Used by the Script Runner (and by extension Run()) to to 32 // determine how to run files that aren't executable. 33 DefaultInterpreters = map[string][]string{ 34 ".py": []string{"/usr/bin/python"}, 35 ".rb": []string{"/usr/bin/ruby"}, 36 ".sh": []string{"/bin/bash"}, 37 ".zsh": []string{"/bin/zsh"}, 38 ".scpt": []string{"/usr/bin/osascript"}, 39 ".scptd": []string{"/usr/bin/osascript"}, 40 ".applescript": []string{"/usr/bin/osascript"}, 41 ".js": []string{"/usr/bin/osascript", "-l", "JavaScript"}, 42 } 43 ) 44 45 // Available runners in order they should be tried. 46 // Executable and Script are added by init. 47 var runners Runners 48 49 func init() { 50 51 // Default runners 52 Executable = &ExecRunner{} 53 Script = NewScriptRunner(DefaultInterpreters) 54 55 runners = Runners{ 56 Executable, 57 Script, 58 } 59 } 60 61 // Runner knows how to execute a file passed to it. 62 // It is used by Run to determine how to run a file. 63 // 64 // When Run is passed a filepath, it asks each registered Runner 65 // in turn whether it can handle the file. 66 type Runner interface { 67 // Can Runner execute this (type of) file? 68 CanRun(filename string) bool 69 // Cmd that executes file (via Runner's execution mechanism). 70 Cmd(filename string, args ...string) *exec.Cmd 71 } 72 73 // Runners implements Runner over a sequence of Runner objects. 74 type Runners []Runner 75 76 // CanRun returns true if one of the runners can run this file. 77 func (rs Runners) CanRun(filename string) bool { 78 79 for _, r := range rs { 80 if r.CanRun(filename) { 81 return true 82 } 83 } 84 return false 85 } 86 87 // Cmd returns a command to run the (script) file. 88 func (rs Runners) Cmd(filename string, args ...string) *exec.Cmd { 89 90 for _, r := range rs { 91 if r.CanRun(filename) { 92 return r.Cmd(filename, args...) 93 } 94 } 95 96 return nil 97 } 98 99 // Run runs the executable or script at path and returns the output. 100 // If it can't figure out how to run the file (see Runner), it 101 // returns ErrUnknownFileType. 102 func (rs Runners) Run(filename string, args ...string) ([]byte, error) { 103 104 fi, err := os.Stat(filename) 105 if err != nil { 106 return nil, err 107 } 108 if fi.IsDir() { 109 return nil, ErrUnknownFileType 110 } 111 112 // See if a runner will accept file 113 for _, r := range rs { 114 115 if r.CanRun(filename) { 116 117 cmd := r.Cmd(filename, args...) 118 119 return RunCmd(cmd) 120 } 121 } 122 123 return nil, ErrUnknownFileType 124 } 125 126 // Run runs the executable or script at path and returns the output. 127 // If it can't figure out how to run the file (see Runner), it 128 // returns ErrUnknownFileType. 129 func Run(filename string, args ...string) ([]byte, error) { 130 return runners.Run(filename, args...) 131 } 132 133 // RunAS executes AppleScript and returns the output. 134 func RunAS(script string, args ...string) (string, error) { 135 return runOsaScript(script, "AppleScript", args...) 136 } 137 138 // RunJS executes JavaScript (JXA) and returns the output. 139 func RunJS(script string, args ...string) (string, error) { 140 return runOsaScript(script, "JavaScript", args...) 141 } 142 143 // runOsaScript executes a script with /usr/bin/osascript. 144 // It returns the output from STDOUT. 145 func runOsaScript(script, lang string, args ...string) (string, error) { 146 147 argv := []string{"-l", lang, "-e", script} 148 argv = append(argv, args...) 149 150 cmd := exec.Command("/usr/bin/osascript", argv...) 151 data, err := RunCmd(cmd) 152 if err != nil { 153 return "", err 154 } 155 156 s := string(data) 157 158 // Remove trailing newline added by osascript 159 if strings.HasSuffix(s, "\n") { 160 s = s[0 : len(s)-1] 161 } 162 163 return s, nil 164 } 165 166 // RunCmd executes a command and returns its output. 167 // 168 // The main difference to exec.Cmd.Output() is that RunCmd writes all 169 // STDERR output to the log if a command fails. 170 func RunCmd(cmd *exec.Cmd) ([]byte, error) { 171 172 var ( 173 output []byte 174 stdout, stderr bytes.Buffer 175 ) 176 177 cmd.Stdout = &stdout 178 cmd.Stderr = &stderr 179 180 if err := cmd.Run(); err != nil { 181 log.Printf("------------- %v ---------------", cmd.Args) 182 log.Println(stderr.String()) 183 log.Println("----------------------------------------------") 184 return nil, err 185 } 186 187 output = stdout.Bytes() 188 189 return output, nil 190 } 191 192 // QuoteAS quotes a string for insertion into AppleScript code. 193 // It wraps the value in quotation marks, so don't insert additional ones. 194 func QuoteAS(s string) string { 195 196 if s == "" { 197 return `""` 198 } 199 200 if s == `"` { 201 return "quote" 202 } 203 204 chars := []string{} 205 for i, c := range s { 206 if c == '"' { 207 if i == 0 { 208 chars = append(chars, `quote & "`) 209 } else if i == len(s)-1 { 210 chars = append(chars, `" & quote`) 211 } else { 212 chars = append(chars, `" & quote & "`) 213 } 214 continue 215 } 216 if i == 0 { 217 chars = append(chars, `"`) 218 } 219 chars = append(chars, string(c)) 220 if i == len(s)-1 { 221 chars = append(chars, `"`) 222 } 223 } 224 225 return strings.Join(chars, "") 226 } 227 228 // QuoteJS quotes a value for insertion into JavaScript. 229 // It calls json.Marshal(v), and returns an empty string if an error occurs. 230 func QuoteJS(v interface{}) string { 231 232 data, err := json.Marshal(v) 233 if err != nil { 234 log.Printf("couldn't convert %#v to JS: %v", v, err) 235 return "" 236 } 237 238 return string(data) 239 } 240 241 // ExecRunner implements Runner for executable files. 242 type ExecRunner struct{} 243 244 // CanRun returns true if file exists and is executable. 245 func (r ExecRunner) CanRun(filename string) bool { 246 247 fi, err := os.Stat(filename) 248 if err != nil || fi.IsDir() { 249 return false 250 } 251 252 perms := uint32(fi.Mode().Perm()) 253 return perms&0111 != 0 254 } 255 256 // Cmd returns a Cmd to run executable with args. 257 func (r ExecRunner) Cmd(executable string, args ...string) *exec.Cmd { 258 259 executable, err := filepath.Abs(executable) 260 if err != nil { 261 panic(err) 262 } 263 264 return exec.Command(executable, args...) 265 } 266 267 // ScriptRunner implements Runner for the specified file extensions. 268 // It calls the given script with the interpreter command from Interpreters. 269 // 270 // A ScriptRunner (combined with Runners, which implements Run) is a useful 271 // base for adding support for running scripts to your own program. 272 type ScriptRunner struct { 273 // Interpreters is an "extension: command" mapping of file extensions 274 // to commands to invoke interpreters that can run the files. 275 // 276 // Interpreters = map[string][]string{ 277 // ".py": []string{"/usr/bin/python"}, 278 // ".rb": []string{"/usr/bin/ruby"}, 279 // } 280 // 281 Interpreters map[string][]string 282 } 283 284 // NewScriptRunner creates a new ScriptRunner for interpreters. 285 func NewScriptRunner(interpreters map[string][]string) *ScriptRunner { 286 287 if interpreters == nil { 288 interpreters = map[string][]string{} 289 } 290 291 r := &ScriptRunner{ 292 Interpreters: make(map[string][]string, len(interpreters)), 293 } 294 295 // Copy over defaults 296 for k, v := range interpreters { 297 r.Interpreters[k] = v 298 } 299 300 return r 301 } 302 303 // CanRun returns true if file exists and its extension is in Interpreters. 304 func (r ScriptRunner) CanRun(filename string) bool { 305 306 if fi, err := os.Stat(filename); err != nil || fi.IsDir() { 307 return false 308 } 309 ext := strings.ToLower(filepath.Ext(filename)) 310 311 _, ok := r.Interpreters[ext] 312 return ok 313 } 314 315 // Cmd returns a Cmd to run filename with its interpreter. 316 func (r ScriptRunner) Cmd(filename string, args ...string) *exec.Cmd { 317 318 var ( 319 argv []string 320 command string 321 ) 322 323 ext := strings.ToLower(filepath.Ext(filename)) 324 interpreter := DefaultInterpreters[ext] 325 326 command = interpreter[0] 327 328 argv = append(argv, interpreter[1:]...) // any remainder of interpreter command 329 argv = append(argv, filename) // path to script file 330 argv = append(argv, args...) // arguments to script 331 332 return exec.Command(command, argv...) 333 334 }