diff --git a/exec/builder.go b/exec/builder.go index a805c94..c5395c8 100644 --- a/exec/builder.go +++ b/exec/builder.go @@ -1,6 +1,7 @@ package exec import ( + "context" "fmt" "io" "sync" @@ -112,24 +113,32 @@ type CommandBuilder struct { stderr io.Writer } -// Commands creates a *CommandBuilder used to collect -// command strings to be executed. -func Commands(cmds ...string) *CommandBuilder { +// CommandsWithContextVars creates a *CommandBuilder with the specified context and session variables. +// The resulting *CommandBuilder is used to execute command strings. +func CommandsWithContextVars(ctx context.Context, variables *vars.Variables, cmds ...string) *CommandBuilder { cb := new(CommandBuilder) - cb.vars = &vars.Variables{} + cb.vars = variables for _, cmd := range cmds { - cb.procs = append(cb.procs, NewProc(cmd)) + cb.procs = append(cb.procs, NewProcWithContextVars(ctx, cmd, variables)) } return cb } +// CommandsWithContext creates a *CommandBuilder, with specified context, used to collect +// command strings to be executed. +func CommandsWithContext(ctx context.Context, cmds ...string) *CommandBuilder { + return CommandsWithContextVars(ctx, &vars.Variables{}, cmds...) +} + +// Commands creates a *CommandBuilder used to collect +// command strings to be executed. +func Commands(cmds ...string) *CommandBuilder { + return CommandsWithContext(context.Background(), cmds...) +} + // CommandsWithVars creates a new CommandBuilder and sets session varialbes for it func CommandsWithVars(variables *vars.Variables, cmds ...string) *CommandBuilder { - cb := &CommandBuilder{vars: variables} - for _, cmd := range cmds { - cb.procs = append(cb.procs, NewProc(variables.Eval(cmd))) - } - return cb + return CommandsWithContextVars(context.Background(), variables, cmds...) } // WithPolicy sets one or more command policy mask values, i.e. (CmdOnErrContinue | CmdExecConcurrent) diff --git a/exec/proc.go b/exec/proc.go index 98152ad..f9b6a52 100644 --- a/exec/proc.go +++ b/exec/proc.go @@ -2,6 +2,7 @@ package exec import ( "bytes" + "context" "fmt" "io" "os" @@ -31,36 +32,51 @@ type Proc struct { vars *vars.Variables } -// NewProc sets up command string to be started as an OS process, however -// does not start the process. The process must be started using a subsequent call to +// NewProcWithContext sets up command string to be started as an OS process using the specified context. +// However, it does not start the process. The process must be started using a subsequent call to // Proc.StartXXX() or Proc.RunXXX() method. -func NewProc(cmdStr string) *Proc { +func NewProcWithContext(ctx context.Context, cmdStr string) *Proc { words, err := parse(cmdStr) if err != nil { return &Proc{err: err} } - command := osexec.Command(words[0], words[1:]...) + command := osexec.CommandContext(ctx, words[0], words[1:]...) return &Proc{ cmd: command, result: new(bytes.Buffer), vars: &vars.Variables{}, } + +} + +// NewProc sets up command string to be started as an OS process, however +// does not start the process. The process must be started using a subsequent call to +// Proc.StartXXX() or Proc.RunXXX() method. +func NewProc(cmdStr string) *Proc { + return NewProcWithContext(context.Background(), cmdStr) } // NewProcWithVars sets up new command string and session variables for a new proc func NewProcWithVars(cmdStr string, variables *vars.Variables) *Proc { - p := NewProc(variables.Eval(cmdStr)) + p := NewProcWithContext(context.Background(), variables.Eval(cmdStr)) p.vars = variables return p } -// StartProc creates and starts an OS process (with combined stdout/stderr) and does not wait for -// it to complete. You must follow this with proc.Wait() to wait for result directly. Then, -// call proc.Out() or proc.Result() to access the process' result. -func StartProc(cmdStr string) *Proc { - proc := NewProc(cmdStr) +// NewProcWithContextVars is a convenient function to create new Proc with context and variables. +func NewProcWithContextVars(ctx context.Context, cmdStr string, variables *vars.Variables) *Proc { + proc := NewProcWithContext(ctx, variables.Eval(cmdStr)) + proc.vars = variables + return proc +} + +// StartProcWithContext creates and starts an OS process (with combined stdout/stderr) using the specified context. +// The function does not wait for the process to complete and must be followed by proc.Wait() to wait for process completion. +// Then, call proc.Out() or proc.Result() to access the process' result. +func StartProcWithContext(ctx context.Context, cmdStr string) *Proc { + proc := NewProcWithContext(ctx, cmdStr) proc.cmd.Stdout = proc.result proc.cmd.Stderr = proc.result @@ -70,48 +86,82 @@ func StartProc(cmdStr string) *Proc { return proc.Start() } +// StartProc creates and starts an OS process using StartProcWithContext using a default context. +func StartProc(cmdStr string) *Proc { + return StartProcWithContext(context.Background(), cmdStr) +} + // StartProcWithVars sets session variables and calls StartProc to create and start a process. func StartProcWithVars(cmdStr string, variables *vars.Variables) *Proc { - proc := StartProc(variables.Eval(cmdStr)) + proc := StartProcWithContext(context.Background(), variables.Eval(cmdStr)) proc.vars = variables return proc } -// RunProc creates, starts, and wait for a new process (with combined stdout/stderr) to complete. -// Use Proc.Out() to access the command's output as an io.Reader (combining stdout and stderr). -// Or, use Proc.Result() to access the commands output as a string. -func RunProc(cmdStr string) *Proc { - proc := StartProc(cmdStr) - if procErr := proc.Err(); procErr != nil { - proc.err = procErr +// StartProcWithContextVars is a convenient function that creates and starts a process with a context and variables. +func StartProcWithContextVars(ctx context.Context, cmdStr string, variables *vars.Variables) *Proc { + proc := StartProcWithContext(ctx, variables.Eval(cmdStr)) + proc.vars = variables + return proc +} + +// RunProcWithContext creates, starts, and runs an OS process using the specified context. +// It then waits for a new process (with combined stdout/stderr) to complete. +// Use Proc.Out() to access the command's output as an io.Reader, or use Proc.Result() +// to access the commands output as a string. +func RunProcWithContext(ctx context.Context, cmdStr string) *Proc { + proc := StartProcWithContext(ctx, cmdStr) + if err := proc.Err(); err != nil { return proc } if err := proc.Wait().Err(); err != nil { proc.err = err return proc } - return proc } +// RunProc creates, starts, and runs for a new process using RunProcWithContext with a default context. +func RunProc(cmdStr string) *Proc { + return RunProcWithContext(context.Background(), cmdStr) +} + // RunProcWithVars sets session variables and calls RunProc func RunProcWithVars(cmdStr string, variables *vars.Variables) *Proc { - proc := RunProc(variables.Eval(cmdStr)) + proc := RunProcWithContext(context.Background(), variables.Eval(cmdStr)) proc.vars = variables return proc } -// Run creates and runs a process and waits for its result (combined stdin,stderr) returned as a string value. -// This is equivalent to calling Proc.RunProc() followed by Proc.Result(). +// RunProcWithContextVars runs a process with a context and session variables +func RunProcWithContextVars(ctx context.Context, cmdStr string, variables *vars.Variables) *Proc { + proc := RunProcWithContext(ctx, variables.Eval(cmdStr)) + proc.vars = variables + return proc +} + +// RunWithContext creates and runs a new process using the specified context. +// It waits for its result (combined stdin,stderr) and makes it availble as a string value. +// This is equivalent to calling Proc.RunProcWithContext() followed by Proc.Result(). +func RunWithContext(ctx context.Context, cmdStr string) string { + return RunProcWithContext(ctx, cmdStr).Result() +} + +// Runs is a convenient shortcut to calling RunWithContext with a default context func Run(cmdStr string) (result string) { - return RunProc(cmdStr).Result() + return RunProcWithContext(context.Background(), cmdStr).Result() } -// RunWithVars sets session variables and call Run +// RunWithVars creates and runs a new process with a specified session variables. func RunWithVars(cmdStr string, variables *vars.Variables) string { return RunProcWithVars(cmdStr, variables).Result() } +// RunWithContextVars creates and runs a new process with a specified context and session variables. +func RunWithContextVars(ctx context.Context, cmdStr string, variables *vars.Variables) string { + return RunProcWithContextVars(ctx, cmdStr, variables).Result() +} + // Start starts the associated command as an OS process and does not wait for its result. // This call should follow a process creation using NewProc. // If you don't want to use the internal combined output streams, make sure to configure access diff --git a/filesys.go b/filesys.go index ca1cde7..bc68392 100644 --- a/filesys.go +++ b/filesys.go @@ -1,6 +1,7 @@ package gexe import ( + "context" "os" "github.com/vladimirvivien/gexe/fs" @@ -31,23 +32,33 @@ func (e *Echo) PathInfo(path string) *fs.FSInfo { return fs.PathWithVars(path, e.vars).Info() } +// FileReadWithContext uses specified context to provide methods to read file +// content at path. +func (e *Echo) FileReadWithContext(ctx context.Context, path string) *fs.FileReader { + return fs.ReadWithContextVars(ctx, path, e.vars) +} + // FileRead provides methods to read file content -// -// FileRead(path).Lines() func (e *Echo) FileRead(path string) *fs.FileReader { - return fs.PathWithVars(path, e.vars).Read() + return fs.ReadWithContextVars(context.Background(), path, e.vars) +} + +// FileWriteWithContext uses context ctx to create a fs.FileWriter to write content to provided path +func (e *Echo) FileWriteWithContext(ctx context.Context, path string) *fs.FileWriter { + return fs.WriteWithContextVars(ctx, path, e.vars) } -// FileWrite provides methods to write content to provided path -// -// FileWrite(path).String("hello world") +// FileWrite creates a fs.FileWriter to write content to provided path func (e *Echo) FileWrite(path string) *fs.FileWriter { - return fs.PathWithVars(path, e.vars).Write() + return fs.WriteWithContextVars(context.Background(), path, e.vars) +} + +// FileAppend creates a new fs.FileWriter to append content to provided path +func (e *Echo) FileAppendWithContext(ctx context.Context, path string) *fs.FileWriter { + return fs.AppendWithContextVars(ctx, path, e.vars) } -// FileAppend provides methods to append content to provided path -// -// FileAppend(path).String("hello world") +// FileAppend creates a new fs.FileWriter to append content to provided path func (e *Echo) FileAppend(path string) *fs.FileWriter { - return fs.PathWithVars(path, e.vars).Append() + return fs.AppendWithContextVars(context.Background(), path, e.vars) } diff --git a/fs/file_reader.go b/fs/file_reader.go index c425b51..1e75785 100644 --- a/fs/file_reader.go +++ b/fs/file_reader.go @@ -3,6 +3,7 @@ package fs import ( "bufio" "bytes" + "context" "io" "os" @@ -16,34 +17,49 @@ type FileReader struct { mode os.FileMode vars *vars.Variables content *bytes.Buffer + ctx context.Context } -// Read reads the file at path and creates new FileReader. -// Access file content using FileReader methods. -func Read(path string) *FileReader { - info, err := os.Stat(path) +// ReadWithContextVars uses specified context and session variables to read the file at path +// and returns a *FileReader to access its content +func ReadWithContextVars(ctx context.Context, path string, variables *vars.Variables) *FileReader { + if variables == nil { + variables = &vars.Variables{} + } + filePath := variables.Eval(path) + + if err := ctx.Err(); err != nil { + return &FileReader{err: err, path: filePath} + } + + info, err := os.Stat(filePath) if err != nil { - return &FileReader{err: err, path: path} + return &FileReader{err: err, path: filePath} } - fileData, err := os.ReadFile(path) + fileData, err := os.ReadFile(filePath) if err != nil { - return &FileReader{err: err, path: path} + return &FileReader{err: err, path: filePath} } return &FileReader{ - path: path, + path: filePath, info: info, mode: info.Mode(), content: bytes.NewBuffer(fileData), + vars: variables, + ctx: ctx, } } -// ReadWithVars creates a new FileReader and sets the reader's session variables +// ReadWithVars uses session variables to create a new FileReader func ReadWithVars(path string, variables *vars.Variables) *FileReader { - reader := Read(variables.Eval(path)) - reader.vars = variables - return reader + return ReadWithContextVars(context.Background(), path, variables) +} + +// Read reads the file at path and returns FileReader to access its content +func Read(path string) *FileReader { + return ReadWithContextVars(context.Background(), path, &vars.Variables{}) } // SetVars sets the FileReader's session variables @@ -52,6 +68,12 @@ func (fr *FileReader) SetVars(variables *vars.Variables) *FileReader { return fr } +// SetContext sets the context for the FileReader operations +func (fr *FileReader) SetContext(ctx context.Context) *FileReader { + fr.ctx = ctx + return fr +} + // Err returns an operation error during file read. func (fr *FileReader) Err() error { return fr.err @@ -73,10 +95,19 @@ func (fr *FileReader) Lines() []string { return []string{} } + if err := fr.ctx.Err(); err != nil { + fr.err = err + return []string{} + } + var lines []string scnr := bufio.NewScanner(fr.content) for scnr.Scan() { + if err := fr.ctx.Err(); err != nil { + fr.err = err + break + } lines = append(lines, scnr.Text()) } @@ -95,6 +126,11 @@ func (fr *FileReader) Bytes() []byte { return []byte{} } + if err := fr.ctx.Err(); err != nil { + fr.err = err + return []byte{} + } + return fr.content.Bytes() } @@ -105,6 +141,11 @@ func (fr *FileReader) Into(w io.Writer) *FileReader { return fr } + if err := fr.ctx.Err(); err != nil { + fr.err = err + return fr + } + if _, err := io.Copy(w, fr.content); err != nil { fr.err = err } diff --git a/fs/file_writer.go b/fs/file_writer.go index 9c469bd..0629100 100644 --- a/fs/file_writer.go +++ b/fs/file_writer.go @@ -1,6 +1,7 @@ package fs import ( + "context" "io" "os" @@ -14,11 +15,27 @@ type FileWriter struct { mode os.FileMode flags int vars *vars.Variables + ctx context.Context } -// Write creates a new FileWriter with flags os.O_CREATE | os.O_TRUNC | os.O_WRONLY and mode 0644. -func Write(path string) *FileWriter { - fw := &FileWriter{path: path, flags: os.O_CREATE | os.O_TRUNC | os.O_WRONLY, mode: 0644, vars: &vars.Variables{}} +// WriteWithVars uses the specified context and session variables to create a new FileWriter +// that can be used to write content to file at path. The file will be created with: +// +// os.O_CREATE | os.O_TRUNC | os.O_WRONLY +func WriteWithContextVars(ctx context.Context, path string, variables *vars.Variables) *FileWriter { + if variables == nil { + variables = &vars.Variables{} + } + filePath := variables.Eval(path) + + fw := &FileWriter{ + path: filePath, + flags: os.O_CREATE | os.O_TRUNC | os.O_WRONLY, + mode: 0644, + vars: variables, + ctx: ctx, + } + info, err := os.Stat(fw.path) if err == nil { fw.finfo = info @@ -26,29 +43,54 @@ func Write(path string) *FileWriter { return fw } -// WriteWithVars creates a new FileWriter and sets sessions variables +// WriteWithVars uses sesison variables to create a new FileWriter to write content to file func WriteWithVars(path string, variables *vars.Variables) *FileWriter { - fw := Write(variables.Eval(path)) - fw.vars = variables - return fw + return WriteWithContextVars(context.Background(), path, variables) } -// Append creates a new FileWriter with flags os.O_CREATE | os.O_APPEND | os.O_WRONLY and mode 0644 -func Append(path string) *FileWriter { - fw := &FileWriter{path: path, flags: os.O_CREATE | os.O_APPEND | os.O_WRONLY, mode: 0644} - info, err := os.Stat(fw.path) - if err == nil { - fw.finfo = info +// Write creates a new FileWriter to write file content +func Write(path string) *FileWriter { + return WriteWithContextVars(context.Background(), path, &vars.Variables{}) +} + +// AppendWithContextVars uses the specified context and session variables to create a new FileWriter +// that can be used to append content existing file at path. The file will be open with: +// +// os.O_CREATE | os.O_APPEND | os.O_WRONLY +// +// and mode 0644 +func AppendWithContextVars(ctx context.Context, path string, variables *vars.Variables) *FileWriter { + if variables == nil { + variables = &vars.Variables{} + } + filePath := variables.Eval(path) + + fw := &FileWriter{ + path: filePath, + flags: os.O_CREATE | os.O_APPEND | os.O_WRONLY, + mode: 0644, + vars: variables, + ctx: ctx, } + info, err := os.Stat(fw.path) + if err != nil { + fw.err = err + return fw + } + fw.finfo = info return fw } -// AppendWithVars creates a new FileWriter and sets session variables +// AppendWithVars uses the specified session variables to create a FileWriter +// to write content to file at path. func AppendWitVars(path string, variables *vars.Variables) *FileWriter { - fw := Append(variables.Eval(path)) - fw.vars = variables - return fw + return AppendWithContextVars(context.Background(), path, variables) +} + +// Append creates FileWriter to write content to file at path +func Append(path string) *FileWriter { + return AppendWithContextVars(context.Background(), path, &vars.Variables{}) } // SetVars sets session variables for FileWriter @@ -64,6 +106,12 @@ func (fw *FileWriter) WithMode(mode os.FileMode) *FileWriter { return fw } +// WithContext sets an execution context for the FileWriter operations +func (fw *FileWriter) WithContext(ctx context.Context) *FileWriter { + fw.ctx = ctx + return fw +} + // Err returns FileWriter error during execution func (fw *FileWriter) Err() error { return fw.err @@ -102,6 +150,12 @@ func (fw *FileWriter) Lines(lines []string) *FileWriter { if fw.err != nil { return fw } + + if err := fw.ctx.Err(); err != nil { + fw.err = err + return fw + } + file, err := os.OpenFile(fw.path, fw.flags, fw.mode) if err != nil { fw.err = err @@ -114,6 +168,11 @@ func (fw *FileWriter) Lines(lines []string) *FileWriter { len := len(lines) for i, line := range lines { + if err := fw.ctx.Err(); err != nil { + fw.err = err + break + } + if _, err := file.WriteString(line); err != nil { fw.err = err return fw @@ -134,6 +193,12 @@ func (fw *FileWriter) Bytes(data []byte) *FileWriter { if fw.err != nil { return fw } + + if err := fw.ctx.Err(); err != nil { + fw.err = err + return fw + } + file, err := os.OpenFile(fw.path, fw.flags, fw.mode) if err != nil { fw.err = err @@ -157,6 +222,12 @@ func (fw *FileWriter) From(r io.Reader) *FileWriter { if fw.err != nil { return fw } + + if err := fw.ctx.Err(); err != nil { + fw.err = err + return fw + } + file, err := os.OpenFile(fw.path, fw.flags, fw.mode) if err != nil { fw.err = err diff --git a/functions.go b/functions.go index cf1563d..a8616fa 100644 --- a/functions.go +++ b/functions.go @@ -1,6 +1,7 @@ package gexe import ( + "context" "os" "github.com/vladimirvivien/gexe/exec" @@ -60,10 +61,22 @@ func Eval(str string) string { return DefaultEcho.Eval(str) } +// NewProcWithContext setups a new process with specified context and command cmdStr and returns immediately +// without starting. Information about the running process is stored in *exec.Proc. +func NewProcWithContext(ctx context.Context, cmdStr string) *exec.Proc { + return DefaultEcho.NewProcWithContext(ctx, cmdStr) +} + // NewProc setups a new process with specified command cmdStr and returns immediately // without starting. Information about the running process is stored in *exec.Proc. func NewProc(cmdStr string) *exec.Proc { - return DefaultEcho.NewProc(cmdStr) + return DefaultEcho.NewProcWithContext(context.Background(), cmdStr) +} + +// StartProcWith executes the command in cmdStr with the specified contex and returns immediately +// without waiting. Information about the running process is stored in *exec.Proc. +func StartProcWithContext(ctx context.Context, cmdStr string) *exec.Proc { + return DefaultEcho.StartProcWithContext(ctx, cmdStr) } // StartProc executes the command in cmdStr and returns immediately @@ -72,12 +85,24 @@ func StartProc(cmdStr string) *exec.Proc { return DefaultEcho.StartProc(cmdStr) } +// RunProcWithContext executes command in cmdStr, with specified ctx, and waits for the result. +// It returns a *Proc with information about the executed process. +func RunProcWithContext(ctx context.Context, cmdStr string) *exec.Proc { + return DefaultEcho.RunProc(cmdStr) +} + // RunProc executes command in cmdStr and waits for the result. // It returns a *Proc with information about the executed process. func RunProc(cmdStr string) *exec.Proc { return DefaultEcho.RunProc(cmdStr) } +// RunWithContext executes cmdStr, with specified context, and waits for completion. +// It returns the result as a string. +func RunWithContext(ctx context.Context, cmdStr string) string { + return DefaultEcho.RunWithContext(ctx, cmdStr) +} + // Run executes cmdStr, waits, and returns the result as a string. func Run(cmdStr string) string { return DefaultEcho.Run(cmdStr) @@ -149,19 +174,34 @@ func RmPath(path string) *fs.FSInfo { return DefaultEcho.RmPath(path) } +// FileRead uses context ctx to read file content from path +func FileReadWithContext(ctx context.Context, path string) *fs.FileReader { + return DefaultEcho.FileReadWithContext(ctx, path) +} + // FileRead provides methods to read file content from path func FileRead(path string) *fs.FileReader { - return DefaultEcho.FileRead(path) + return DefaultEcho.FileReadWithContext(context.Background(), path) +} + +// FileWriteWithContext uses context ctx to write file content to path +func FileWriteWithContext(ctx context.Context, path string) *fs.FileWriter { + return DefaultEcho.FileWriteWithContext(ctx, path) } // FileWrite provides methods to write file content to path func FileWrite(path string) *fs.FileWriter { - return DefaultEcho.FileWrite(path) + return DefaultEcho.FileWriteWithContext(context.Background(), path) +} + +// HttpGetWithContext uses context ctx to start an HTTP GET operation to retrieve resource at URL/path +func HttpGetWithContext(ctx context.Context, url string, paths ...string) *http.ResourceReader { + return DefaultEcho.HttpGetWithContext(ctx, url, paths...) } // HttpGet starts an HTTP GET operation to retrieve resource at URL/path func HttpGet(url string, paths ...string) *http.ResourceReader { - return DefaultEcho.HttpGet(url, paths...) + return DefaultEcho.HttpGetWithContext(context.Background(), url, paths...) } // Get is a convenient alias for HttpGet that retrieves specified resource at given URL/path @@ -169,9 +209,14 @@ func Get(url string, paths ...string) *http.Response { return DefaultEcho.Get(url, paths...) } +// HttpPostWithContext uses context ctx to start an HTTP POST operation to post resource to URL/path +func HttpPostWithContext(ctx context.Context, url string, paths ...string) *http.ResourceWriter { + return DefaultEcho.HttpPostWithContext(ctx, url, paths...) +} + // HttpPost starts an HTTP POST operation to post resource to URL/path func HttpPost(url string, paths ...string) *http.ResourceWriter { - return DefaultEcho.HttpPost(url, paths...) + return DefaultEcho.HttpPostWithContext(context.Background(), url, paths...) } // Post is a convenient alias for HttpPost to post data at specified URL diff --git a/http.go b/http.go index 2a4622d..71c6e0b 100644 --- a/http.go +++ b/http.go @@ -1,29 +1,40 @@ package gexe import ( + "context" "strings" "github.com/vladimirvivien/gexe/http" ) -// HttpGet starts an HTTP GET operation to retrieve server resource from given URL/paths. -func (e *Echo) HttpGet(url string, paths ...string) *http.ResourceReader { +// HttpGetWithContext uses context ctx to start an HTTP GET operation to retrieve server resource from given URL/paths. +func (e *Echo) HttpGetWithContext(ctx context.Context, url string, paths ...string) *http.ResourceReader { var exapandedUrl strings.Builder exapandedUrl.WriteString(e.vars.Eval(url)) for _, path := range paths { exapandedUrl.WriteString(e.vars.Eval(path)) } - return http.GetWithVars(exapandedUrl.String(), e.vars) + return http.GetWithContextVars(ctx, exapandedUrl.String(), e.vars) } -// HttpPost starts an HTTP POST operation to post resource to a server at given URL/path. -func (e *Echo) HttpPost(url string, paths ...string) *http.ResourceWriter { +// HttpGetWithContext starts an HTTP GET operation to retrieve server resource from given URL/paths. +func (e *Echo) HttpGet(url string, paths ...string) *http.ResourceReader { + return e.HttpGetWithContext(context.Background(), url, paths...) +} + +// HttpPostWithContext uses context ctx to start an HTTP POST operation to post resource to a server at given URL/path. +func (e *Echo) HttpPostWithContext(ctx context.Context, url string, paths ...string) *http.ResourceWriter { var exapandedUrl strings.Builder exapandedUrl.WriteString(e.vars.Eval(url)) for _, path := range paths { exapandedUrl.WriteString(e.vars.Eval(path)) } - return http.PostWithVars(exapandedUrl.String(), e.vars) + return http.PostWithContextVars(ctx, exapandedUrl.String(), e.vars) +} + +// HttpPost starts an HTTP POST operation to post resource to a server at given URL/path. +func (e *Echo) HttpPost(url string, paths ...string) *http.ResourceWriter { + return e.HttpPostWithContext(context.Background(), url, paths...) } // Get is convenient alias for HttpGet to retrieve a resource at given URL/path diff --git a/http/http_reader.go b/http/http_reader.go index d67cb2f..1dc380f 100644 --- a/http/http_reader.go +++ b/http/http_reader.go @@ -1,7 +1,11 @@ package http import ( + "bytes" + "context" + "io" "net/http" + "strings" "time" "github.com/vladimirvivien/gexe/vars" @@ -9,22 +13,38 @@ import ( // ResourceReader provides types and methods to read content of resources from a server using HTTP type ResourceReader struct { - client *http.Client - err error - url string - vars *vars.Variables + client *http.Client + err error + url string + vars *vars.Variables + ctx context.Context + data io.Reader + headers http.Header } -// Get initiates a "GET" operation for the specified resource -func Get(url string) *ResourceReader { - return &ResourceReader{url: url, client: &http.Client{}, vars: &vars.Variables{}} +// GetWithContextVars uses context ctx and session variables to initiate +// a "GET" operation for the specified resource +func GetWithContextVars(ctx context.Context, url string, variables *vars.Variables) *ResourceReader { + if variables == nil { + variables = &vars.Variables{} + } + + return &ResourceReader{ + ctx: ctx, + url: variables.Eval(url), + client: &http.Client{}, + vars: &vars.Variables{}, + } } -// Get initiates a "GET" operation and sets session variables +// GetWithVars uses session vars to initiate a "GET" operation func GetWithVars(url string, variables *vars.Variables) *ResourceReader { - r := Get(variables.Eval(url)) - r.vars = variables - return r + return GetWithContextVars(context.Background(), url, variables) +} + +// Get initiates a "GET" operation for the specified resource +func Get(url string) *ResourceReader { + return GetWithContextVars(context.Background(), url, &vars.Variables{}) } // SetVars sets session variables for ResourceReader @@ -44,12 +64,60 @@ func (r *ResourceReader) WithTimeout(to time.Duration) *ResourceReader { return r } +// WithContext sets the context for the HTTP request +func (r *ResourceReader) WithContext(ctx context.Context) *ResourceReader { + r.ctx = ctx + return r +} + +// WithHeaders sets all HTTP headers for GET request +func (r *ResourceReader) WithHeaders(h http.Header) *ResourceReader { + r.headers = h + return r +} + +// AddHeader convenience method to add request header +func (r *ResourceReader) AddHeader(key, value string) *ResourceReader { + r.headers.Add(r.vars.Eval(key), r.vars.Eval(value)) + return r +} + +// SetHeader convenience method to set a specific header +func (r *ResourceReader) SetHeader(key, value string) *ResourceReader { + r.headers.Set(r.vars.Eval(key), r.vars.Eval(value)) + return r +} + +// RequestString sets GET request data as string +func (r *ResourceReader) String(val string) *ResourceReader { + r.data = strings.NewReader(r.vars.Eval(val)) + return r +} + +// RequestBytes sets GET request data as byte slice +func (r *ResourceReader) Bytes(data []byte) *ResourceReader { + r.data = bytes.NewReader(data) + return r +} + +// RequestBody sets GET request content as io.Reader +func (r *ResourceReader) Body(body io.Reader) *ResourceReader { + r.data = body + return r +} + // Do is a terminal method that actually retrieves the HTTP resource from the server. // It returns a gexe/http/*Response instance that can be used to access the result. func (r *ResourceReader) Do() *Response { - res, err := r.client.Get(r.url) + req, err := http.NewRequestWithContext(r.ctx, "GET", r.url, r.data) if err != nil { return &Response{err: err} } + + res, err := r.client.Do(req) + if err != nil { + return &Response{err: err} + } + return &Response{stat: res.Status, statCode: res.StatusCode, body: res.Body} } diff --git a/http/http_writer.go b/http/http_writer.go index 7c2af13..fa077f9 100644 --- a/http/http_writer.go +++ b/http/http_writer.go @@ -2,6 +2,7 @@ package http import ( "bytes" + "context" "io" "net/http" "net/url" @@ -19,18 +20,33 @@ type ResourceWriter struct { headers http.Header data io.Reader vars *vars.Variables + ctx context.Context } -// Post starts an HTTP "POST" operation using the provided URL. -func Post(resource string) *ResourceWriter { - return &ResourceWriter{url: resource, client: &http.Client{}, headers: make(http.Header), vars: &vars.Variables{}} +// PostWithContextVars uses the specified context ctx and session variable to +// start an HTTP "POST" operation +func PostWithContextVars(ctx context.Context, resource string, variables *vars.Variables) *ResourceWriter { + if variables == nil { + variables = &vars.Variables{} + } + + return &ResourceWriter{ + ctx: ctx, + url: variables.Eval(resource), + client: &http.Client{}, + headers: make(http.Header), + vars: variables, + } } -// PostWithVars starts an HTTP "POST" operation and sets the provided gexe session variables +// PostWithVars uses session variables to start an HTTP "POST" operation func PostWithVars(resource string, variables *vars.Variables) *ResourceWriter { - w := Post(variables.Eval(resource)) - w.vars = variables - return w + return PostWithContextVars(context.Background(), resource, variables) +} + +// Post starts an HTTP "POST" operation using the provided URL. +func Post(resource string) *ResourceWriter { + return PostWithContextVars(context.Background(), resource, &vars.Variables{}) } // SetVars sets gexe session variables to be used by the post operation @@ -45,6 +61,12 @@ func (w *ResourceWriter) WithTimeout(to time.Duration) *ResourceWriter { return w } +// WithContext sets the context to be used for the HTTP request +func (w *ResourceWriter) WithContext(ctx context.Context) *ResourceWriter { + w.ctx = ctx + return w +} + // Err returns the last error generated by the ResourceWriter func (w *ResourceWriter) Err() error { return w.err @@ -98,7 +120,7 @@ func (w *ResourceWriter) FormData(val map[string][]string) *ResourceWriter { // Do is a terminal method that actually posts the HTTP request to the server. // It returns a gexe/http/*Response instance that can be used to access post result. func (w *ResourceWriter) Do() *Response { - req, err := http.NewRequest("POST", w.url, w.data) + req, err := http.NewRequestWithContext(w.ctx, "POST", w.url, w.data) if err != nil { return &Response{err: err} } diff --git a/procs.go b/procs.go index ed23067..34f6fcf 100644 --- a/procs.go +++ b/procs.go @@ -1,6 +1,7 @@ package gexe import ( + "context" "fmt" "github.com/vladimirvivien/gexe/exec" @@ -9,26 +10,49 @@ import ( // NewProc setups a new process with specified command cmdStr and returns immediately // without starting. Use Proc.Wait to wait for exection and then retrieve process result. // Information about the running process is stored in *exec.Proc. +func (e *Echo) NewProcWithContext(ctx context.Context, cmdStr string) *exec.Proc { + return exec.NewProcWithContextVars(ctx, cmdStr, e.vars) +} + +// NewProc a convenient function that calls NewProcWithContext with a default contet. func (e *Echo) NewProc(cmdStr string) *exec.Proc { - return exec.NewProcWithVars(cmdStr, e.vars) + return exec.NewProcWithContextVars(context.Background(), cmdStr, e.vars) +} + +// StartProc executes the command in cmdStr, with the specified context, and returns immediately +// without waiting. Use Proc.Wait to wait for exection and then retrieve process result. +// Information about the running process is stored in *Proc. +func (e *Echo) StartProcWithContext(ctx context.Context, cmdStr string) *exec.Proc { + return exec.StartProcWithContextVars(ctx, cmdStr, e.vars) } // StartProc executes the command in cmdStr and returns immediately // without waiting. Use Proc.Wait to wait for exection and then retrieve process result. // Information about the running process is stored in *Proc. func (e *Echo) StartProc(cmdStr string) *exec.Proc { - return exec.StartProcWithVars(cmdStr, e.vars) + return exec.StartProcWithContextVars(context.Background(), cmdStr, e.vars) +} + +// RunProcWithContext executes command in cmdStr, with given context, and waits for the result. +// It returns a *Proc with information about the executed process. +func (e *Echo) RunProcWithContext(ctx context.Context, cmdStr string) *exec.Proc { + return exec.RunProcWithContextVars(ctx, cmdStr, e.vars) } // RunProc executes command in cmdStr and waits for the result. // It returns a *Proc with information about the executed process. func (e *Echo) RunProc(cmdStr string) *exec.Proc { - return exec.RunProcWithVars(cmdStr, e.vars) + return exec.RunProcWithContextVars(context.Background(), cmdStr, e.vars) +} + +// Run executes cmdStr, with given context, and returns the result as a string. +func (e *Echo) RunWithContext(ctx context.Context, cmdStr string) string { + return exec.RunWithContextVars(ctx, cmdStr, e.vars) } // Run executes cmdStr, waits, and returns the result as a string. func (e *Echo) Run(cmdStr string) string { - return exec.RunWithVars(cmdStr, e.vars) + return exec.RunWithContextVars(context.Background(), cmdStr, e.vars) } // Runout executes command cmdStr and prints out the result @@ -36,38 +60,72 @@ func (e *Echo) Runout(cmdStr string) { fmt.Print(e.Run(cmdStr)) } +// Commands creates a *exe.CommandBuilder, with the specified context, to build a multi-command execution flow. +func (e *Echo) CommandsWithContext(ctx context.Context, cmdStrs ...string) *exec.CommandBuilder { + return exec.CommandsWithContextVars(ctx, e.vars, cmdStrs...) +} + // Commands returns a *exe.CommandBuilder to build a multi-command execution flow. func (e *Echo) Commands(cmdStrs ...string) *exec.CommandBuilder { - return exec.CommandsWithVars(e.vars, cmdStrs...) + return exec.CommandsWithContextVars(context.Background(), e.vars, cmdStrs...) +} + +// StartAllWithContext uses the specified ctx to start sequential execution of each command, in cmdStrs, and does not +// wait for their completion. +func (e *Echo) StartAllWithContext(ctx context.Context, cmdStrs ...string) *exec.CommandResult { + return exec.CommandsWithContextVars(ctx, e.vars, cmdStrs...).Start() } // StartAll starts the sequential execution of each command, in cmdStrs, and does not // wait for their completion. func (e *Echo) StartAll(cmdStrs ...string) *exec.CommandResult { - return exec.CommandsWithVars(e.vars, cmdStrs...).Start() + return exec.CommandsWithContextVars(context.Background(), e.vars, cmdStrs...).Start() +} + +// RunAllWithContext executes each command sequentially, in cmdStrs, and wait for their completion. +func (e *Echo) RunAllWithContext(ctx context.Context, cmdStrs ...string) *exec.CommandResult { + return exec.CommandsWithContextVars(ctx, e.vars, cmdStrs...).Run() } // RunAll executes each command sequentially, in cmdStrs, and wait for their completion. func (e *Echo) RunAll(cmdStrs ...string) *exec.CommandResult { - return exec.CommandsWithVars(e.vars, cmdStrs...).Run() + return exec.CommandsWithContextVars(context.Background(), e.vars, cmdStrs...).Run() +} + +// StartConcurWithContext uses specified context to start the concurrent execution of each command, in cmdStrs, and does not +// wait for their completion. +func (e *Echo) StartConcurWithContext(ctx context.Context, cmdStrs ...string) *exec.CommandResult { + return exec.CommandsWithContextVars(ctx, e.vars, cmdStrs...).Concurr() } // StartConcur starts the concurrent execution of each command, in cmdStrs, and does not // wait for their completion. func (e *Echo) StartConcur(cmdStrs ...string) *exec.CommandResult { - return exec.CommandsWithVars(e.vars, cmdStrs...).Concurr() + return exec.CommandsWithContextVars(context.Background(), e.vars, cmdStrs...).Concurr() +} + +// RunConcurWithContext uses context to execute each command concurrently, in cmdStrs, and waits +// their completion. +func (e *Echo) RunConcurWithContext(ctx context.Context, cmdStrs ...string) *exec.CommandResult { + return exec.CommandsWithContextVars(ctx, e.vars, cmdStrs...).Concurr().Wait() } // RunConcur executes each command concurrently, in cmdStrs, and waits // their completion. func (e *Echo) RunConcur(cmdStrs ...string) *exec.CommandResult { - return exec.CommandsWithVars(e.vars, cmdStrs...).Concurr().Wait() + return exec.CommandsWithContextVars(context.Background(), e.vars, cmdStrs...).Concurr().Wait() +} + +// Pipe uses specified context to execute each command, in cmdStrs, by piping the result +// of the previous command as input to the next command until done. +func (e *Echo) PipeWithContext(ctx context.Context, cmdStrs ...string) *exec.PipedCommandResult { + return exec.CommandsWithContextVars(ctx, e.vars, cmdStrs...).Pipe() } // Pipe executes each command, in cmdStrs, by piping the result // of the previous command as input to the next command until done. func (e *Echo) Pipe(cmdStrs ...string) *exec.PipedCommandResult { - return exec.CommandsWithVars(e.vars, cmdStrs...).Pipe() + return exec.CommandsWithContextVars(context.Background(), e.vars, cmdStrs...).Pipe() } // ParseCommand parses the string into individual command tokens