github.com/hairyhenderson/gomplate/v3@v3.11.7/plugins.go (about) 1 package gomplate 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "io" 8 "os" 9 "os/exec" 10 "os/signal" 11 "path/filepath" 12 "runtime" 13 "text/template" 14 "time" 15 16 "github.com/hairyhenderson/gomplate/v3/conv" 17 "github.com/hairyhenderson/gomplate/v3/internal/config" 18 ) 19 20 // bindPlugins creates custom plugin functions for each plugin specified by 21 // the config, and adds them to the given funcMap. Uses the configuration's 22 // PluginTimeout as the default plugin Timeout. Errors if a function name is 23 // duplicated. 24 func bindPlugins(ctx context.Context, cfg *config.Config, funcMap template.FuncMap) error { 25 for k, v := range cfg.Plugins { 26 if _, ok := funcMap[k]; ok { 27 return fmt.Errorf("function %q is already bound, and can not be overridden", k) 28 } 29 30 // default the timeout to the one in the config 31 timeout := cfg.PluginTimeout 32 if v.Timeout != 0 { 33 timeout = v.Timeout 34 } 35 36 funcMap[k] = PluginFunc(ctx, v.Cmd, PluginOpts{ 37 Timeout: timeout, 38 Pipe: v.Pipe, 39 Stderr: cfg.Stderr, 40 }) 41 } 42 43 return nil 44 } 45 46 // PluginOpts are options for controlling plugin function execution 47 type PluginOpts struct { 48 // Stderr can be set to redirect the plugin's stderr to a custom writer. 49 // Defaults to os.Stderr. 50 Stderr io.Writer 51 52 // Timeout is the maximum amount of time to wait for the plugin to complete. 53 // Defaults to 5 seconds. 54 Timeout time.Duration 55 56 // Pipe indicates whether the last argument should be piped to the plugin's 57 // stdin (true) or processed as a commandline argument (false) 58 Pipe bool 59 } 60 61 // PluginFunc creates a template function that runs an external process - either 62 // a shell script or commandline executable. 63 func PluginFunc(ctx context.Context, cmd string, opts PluginOpts) func(...interface{}) (interface{}, error) { 64 timeout := opts.Timeout 65 if timeout == 0 { 66 timeout = 5 * time.Second 67 } 68 69 stderr := opts.Stderr 70 if stderr == nil { 71 stderr = os.Stderr 72 } 73 74 plugin := &plugin{ 75 ctx: ctx, 76 path: cmd, 77 timeout: timeout, 78 pipe: opts.Pipe, 79 stderr: stderr, 80 } 81 82 return plugin.run 83 } 84 85 // plugin represents a custom function that binds to an external process to be executed 86 type plugin struct { 87 ctx context.Context 88 stderr io.Writer 89 path string 90 timeout time.Duration 91 pipe bool 92 } 93 94 // builds a command that's appropriate for running scripts 95 func (p *plugin) buildCommand(a []string) (name string, args []string) { 96 switch filepath.Ext(p.path) { 97 case ".ps1": 98 a = append([]string{"-File", p.path}, a...) 99 return findPowershell(), a 100 case ".cmd", ".bat": 101 a = append([]string{"/c", p.path}, a...) 102 return "cmd.exe", a 103 default: 104 return p.path, a 105 } 106 } 107 108 // finds the appropriate powershell command for the platform - prefers 109 // PowerShell Core (`pwsh`), but on Windows if it's not found falls back to 110 // Windows PowerShell (`powershell`). 111 func findPowershell() string { 112 if runtime.GOOS != "windows" { 113 return "pwsh" 114 } 115 116 _, err := exec.LookPath("pwsh") 117 if err != nil { 118 return "powershell" 119 } 120 return "pwsh" 121 } 122 123 func (p *plugin) run(args ...interface{}) (interface{}, error) { 124 a := conv.ToStrings(args...) 125 126 name, a := p.buildCommand(a) 127 128 ctx, cancel := context.WithTimeout(p.ctx, p.timeout) 129 defer cancel() 130 131 var stdin *bytes.Buffer 132 if p.pipe && len(a) > 0 { 133 stdin = bytes.NewBufferString(a[len(a)-1]) 134 a = a[:len(a)-1] 135 } 136 137 c := exec.CommandContext(ctx, name, a...) 138 if stdin != nil { 139 c.Stdin = stdin 140 } 141 142 c.Stderr = p.stderr 143 outBuf := &bytes.Buffer{} 144 c.Stdout = outBuf 145 146 start := time.Now() 147 err := c.Start() 148 if err != nil { 149 return nil, err 150 } 151 152 // make sure all signals are propagated 153 sigs := make(chan os.Signal, 1) 154 signal.Notify(sigs) 155 go func() { 156 select { 157 case sig := <-sigs: 158 // Pass signals to the sub-process 159 if c.Process != nil { 160 _ = c.Process.Signal(sig) 161 } 162 case <-ctx.Done(): 163 } 164 }() 165 166 err = c.Wait() 167 elapsed := time.Since(start) 168 169 if ctx.Err() != nil { 170 err = fmt.Errorf("plugin timed out after %v: %w", elapsed, ctx.Err()) 171 } 172 173 return outBuf.String(), err 174 }