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