github.com/theclapp/sh@v2.6.4+incompatible/interp/module.go (about)

     1  // Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>
     2  // See LICENSE for licensing information
     3  
     4  package interp
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"io"
    10  	"os"
    11  	"os/exec"
    12  	"runtime"
    13  	"strings"
    14  	"syscall"
    15  	"time"
    16  
    17  	"mvdan.cc/sh/expand"
    18  )
    19  
    20  // FromModuleContext returns the ModuleCtx value stored in ctx, if any.
    21  func FromModuleContext(ctx context.Context) (ModuleCtx, bool) {
    22  	mc, ok := ctx.Value(moduleCtxKey{}).(ModuleCtx)
    23  	return mc, ok
    24  }
    25  
    26  type moduleCtxKey struct{}
    27  
    28  // ModuleCtx is the data passed to all the module functions via a context value.
    29  // It contains some of the current state of the Runner, as well as some fields
    30  // necessary to implement some of the modules.
    31  type ModuleCtx struct {
    32  	Env         expand.Environ
    33  	Dir         string
    34  	Stdin       io.Reader
    35  	Stdout      io.Writer
    36  	Stderr      io.Writer
    37  	KillTimeout time.Duration
    38  }
    39  
    40  // UnixPath fixes absolute unix paths on Windows, for example converting
    41  // "C:\\CurDir\\dev\\null" to "/dev/null".
    42  func (mc ModuleCtx) UnixPath(path string) string {
    43  	if runtime.GOOS != "windows" {
    44  		return path
    45  	}
    46  	path = strings.TrimPrefix(path, mc.Dir)
    47  	return strings.Replace(path, `\`, `/`, -1)
    48  }
    49  
    50  // ModuleExec is the module responsible for executing a program. It is
    51  // executed for all CallExpr nodes where the first argument is neither a
    52  // declared function nor a builtin.
    53  //
    54  // Note that the name is included as the first argument. If path is an
    55  // empty string, it means that the executable did not exist or was not
    56  // found in $PATH.
    57  //
    58  // Use a return error of type ExitStatus to set the exit status. A nil error has
    59  // the same effect as ExitStatus(0). If the error is of any other type, the
    60  // interpreter will come to a stop.
    61  type ModuleExec func(ctx context.Context, path string, args []string) error
    62  
    63  func (ModuleExec) isModule() {}
    64  
    65  var DefaultExec = ModuleExec(func(ctx context.Context, path string, args []string) error {
    66  	mc, _ := FromModuleContext(ctx)
    67  	if path == "" {
    68  		fmt.Fprintf(mc.Stderr, "%q: executable file not found in $PATH\n", args[0])
    69  		return ExitStatus(127)
    70  	}
    71  	cmd := exec.Cmd{
    72  		Path:   path,
    73  		Args:   args,
    74  		Env:    execEnv(mc.Env),
    75  		Dir:    mc.Dir,
    76  		Stdin:  mc.Stdin,
    77  		Stdout: mc.Stdout,
    78  		Stderr: mc.Stderr,
    79  	}
    80  
    81  	err := cmd.Start()
    82  	if err == nil {
    83  		if done := ctx.Done(); done != nil {
    84  			go func() {
    85  				<-done
    86  
    87  				if mc.KillTimeout <= 0 || runtime.GOOS == "windows" {
    88  					_ = cmd.Process.Signal(os.Kill)
    89  					return
    90  				}
    91  
    92  				// TODO: don't temporarily leak this goroutine
    93  				// if the program stops itself with the
    94  				// interrupt.
    95  				go func() {
    96  					time.Sleep(mc.KillTimeout)
    97  					_ = cmd.Process.Signal(os.Kill)
    98  				}()
    99  				_ = cmd.Process.Signal(os.Interrupt)
   100  			}()
   101  		}
   102  
   103  		err = cmd.Wait()
   104  	}
   105  
   106  	switch x := err.(type) {
   107  	case *exec.ExitError:
   108  		// started, but errored - default to 1 if OS
   109  		// doesn't have exit statuses
   110  		if status, ok := x.Sys().(syscall.WaitStatus); ok {
   111  			if status.Signaled() && ctx.Err() != nil {
   112  				return ctx.Err()
   113  			}
   114  			return ExitStatus(status.ExitStatus())
   115  		}
   116  		return ExitStatus(1)
   117  	case *exec.Error:
   118  		// did not start
   119  		fmt.Fprintf(mc.Stderr, "%v\n", err)
   120  		return ExitStatus(127)
   121  	default:
   122  		return err
   123  	}
   124  })
   125  
   126  // ModuleOpen is the module responsible for opening a file. It is
   127  // executed for all files that are opened directly by the shell, such as
   128  // in redirects. Files opened by executed programs are not included.
   129  //
   130  // The path parameter is absolute and has been cleaned.
   131  //
   132  // Use a return error of type *os.PathError to have the error printed to
   133  // stderr and the exit status set to 1. If the error is of any other type, the
   134  // interpreter will come to a stop.
   135  //
   136  // TODO: What about stat calls? They are used heavily in the builtin
   137  // test expressions, and also when doing a cd. Should they have a
   138  // separate module?
   139  type ModuleOpen func(ctx context.Context, path string, flag int, perm os.FileMode) (io.ReadWriteCloser, error)
   140  
   141  func (ModuleOpen) isModule() {}
   142  
   143  var DefaultOpen = ModuleOpen(func(ctx context.Context, path string, flag int, perm os.FileMode) (io.ReadWriteCloser, error) {
   144  	return os.OpenFile(path, flag, perm)
   145  })
   146  
   147  func OpenDevImpls(next ModuleOpen) ModuleOpen {
   148  	return func(ctx context.Context, path string, flag int, perm os.FileMode) (io.ReadWriteCloser, error) {
   149  		mc, _ := FromModuleContext(ctx)
   150  		switch mc.UnixPath(path) {
   151  		case "/dev/null":
   152  			return devNull{}, nil
   153  		}
   154  		return next(ctx, path, flag, perm)
   155  	}
   156  }
   157  
   158  var _ io.ReadWriteCloser = devNull{}
   159  
   160  type devNull struct{}
   161  
   162  func (devNull) Read(p []byte) (int, error)  { return 0, io.EOF }
   163  func (devNull) Write(p []byte) (int, error) { return len(p), nil }
   164  func (devNull) Close() error                { return nil }