@@ -21,7 +21,6 @@ import (
2121 "fmt"
2222 "io/ioutil"
2323 "os"
24- "os/exec"
2524 "path"
2625 "path/filepath"
2726 "strings"
@@ -32,20 +31,19 @@ import (
3231
3332// execute hook as script (priority) or inline shell. Shell is platform-independent, thanks to mvdan.cc/sh.
3433func (h Runnable ) execute (ctx context.Context , state map [string ]interface {}, workDir string , layoutFS string ) error {
35- if h .Script != "" {
36- return h .executeScript (ctx , state , workDir , layoutFS )
37- }
38- return h .executeInline (ctx , state , workDir )
39- }
40-
41- // execute inline (run) shell script.
42- func (h Runnable ) executeInline (ctx context.Context , state map [string ]interface {}, workDir string ) error {
4334 cp , err := h .render (state )
4435 if err != nil {
4536 return fmt .Errorf ("render hook: %w" , err )
4637 }
38+ if cp .Script != "" {
39+ return cp .executeScript (ctx , state , workDir , layoutFS )
40+ }
41+ return cp .executeInline (ctx , state , workDir )
42+ }
4743
48- script , err := syntax .NewParser ().Parse (strings .NewReader (cp .Run ), "" )
44+ // execute inline (run) shell script.
45+ func (h Runnable ) executeInline (ctx context.Context , state map [string ]interface {}, workDir string ) error {
46+ script , err := syntax .NewParser ().Parse (strings .NewReader (h .Run ), "" )
4947 if err != nil {
5048 return fmt .Errorf ("parse script: %w" , err )
5149 }
@@ -59,52 +57,76 @@ func (h Runnable) executeInline(ctx context.Context, state map[string]interface{
5957}
6058
6159// render script to temporary file and execute it. Automatically sets +x (executable) flag to file.
60+ // It CAN support more or less complex shell execution, however, it designed for direct script invocation: <script> [args...]
6261func (h Runnable ) executeScript (ctx context.Context , state map [string ]interface {}, workDir string , layoutFS string ) error {
63- scriptContent , err := ioutil . ReadFile ( filepath . Join ( layoutFS , path . Clean (h .Script )) )
62+ parsedCommand , err := syntax . NewParser (). Parse ( strings . NewReader (h .Script ), "" )
6463 if err != nil {
65- return fmt .Errorf ("read hook script content : %w" , err )
64+ return fmt .Errorf ("parse script invokation : %w" , err )
6665 }
67-
68- newScriptContent , err := render (string (scriptContent ), state )
69- if err != nil {
70- return fmt .Errorf ("render hook script content: %w" , err )
66+ var callExpr * syntax.CallExpr
67+ for _ , stmt := range parsedCommand .Stmts {
68+ if call , ok := stmt .Cmd .(* syntax.CallExpr ); ok && len (call .Args ) > 0 {
69+ callExpr = call
70+ break
71+ }
7172 }
7273
73- f , err := os .CreateTemp ("" , "" )
74- if err != nil {
75- return fmt .Errorf ("create temp file: %w" , err )
76- }
77- defer os .RemoveAll (f .Name ())
78- defer f .Close ()
74+ if callExpr != nil {
75+ // render script content and copy it to temp dir
7976
80- if _ , err := f .WriteString (newScriptContent ); err != nil {
81- return fmt .Errorf ("write rendered hook content: %w" , err )
82- }
77+ scriptContent , err := ioutil .ReadFile (filepath .Join (layoutFS , path .Clean (assemblePathToCommand (callExpr ))))
78+ if err != nil {
79+ return fmt .Errorf ("read hook script content: %w" , err )
80+ }
8381
84- if err := f .Close (); err != nil {
85- return fmt .Errorf ("close script: %w" , err )
86- }
82+ newScriptContent , err := render (string (scriptContent ), state )
83+ if err != nil {
84+ return fmt .Errorf ("render hook script content: %w" , err )
85+ }
86+
87+ f , err := os .CreateTemp ("" , "" )
88+ if err != nil {
89+ return fmt .Errorf ("create temp file: %w" , err )
90+ }
91+ defer os .RemoveAll (f .Name ())
92+ defer f .Close ()
93+
94+ if _ , err := f .WriteString (newScriptContent ); err != nil {
95+ return fmt .Errorf ("write rendered hook content: %w" , err )
96+ }
97+
98+ if err := f .Close (); err != nil {
99+ return fmt .Errorf ("close script: %w" , err )
100+ }
87101
88- if err := os .Chmod (f .Name (), 0700 ); err != nil {
89- return fmt .Errorf ("mark script as executable: %w" , err )
102+ if err := os .Chmod (f .Name (), 0700 ); err != nil {
103+ return fmt .Errorf ("mark script as executable: %w" , err )
104+ }
105+
106+ // mock call in expression
107+ callExpr .Args [0 ].Parts [0 ] = & syntax.Lit {Value : f .Name ()}
90108 }
91109
92- cmd := exec . CommandContext ( ctx , f . Name ( ))
93- cmd . Stdout = os . Stdout
94- cmd . Stderr = os . Stderr
95- cmd . Dir = workDir
110+ runner , err := interp . New ( interp . Dir ( workDir ), interp . StdIO ( nil , os . Stdout , os . Stderr ))
111+ if err != nil {
112+ return fmt . Errorf ( "create script runner: %w" , err )
113+ }
96114
97- return cmd .Run ()
115+ return runner .Run (ctx , parsedCommand )
98116}
99117
100- // render templated variables: run
118+ // render templated variables: run, script
101119func (h Runnable ) render (state map [string ]interface {}) (Runnable , error ) {
102120 if v , err := render (h .Run , state ); err != nil {
103121 return h , fmt .Errorf ("render run: %w" , err )
104122 } else {
105123 h .Run = v
106124 }
107-
125+ if v , err := render (h .Script , state ); err != nil {
126+ return h , fmt .Errorf ("render script: %w" , err )
127+ } else {
128+ h .Script = v
129+ }
108130 return h , nil
109131}
110132
@@ -115,3 +137,16 @@ func (h Runnable) what() string {
115137 }
116138 return h .Run
117139}
140+
141+ func assemblePathToCommand (stmt * syntax.CallExpr ) string {
142+ var ans []string = make ([]string , 0 , len (stmt .Args [0 ].Parts ))
143+ for _ , p := range stmt .Args [0 ].Parts {
144+ switch v := p .(type ) {
145+ case * syntax.SglQuoted :
146+ ans = append (ans , v .Value )
147+ case * syntax.Lit :
148+ ans = append (ans , v .Value )
149+ }
150+ }
151+ return strings .Join (ans , "" )
152+ }
0 commit comments