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  }