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  }