Skip to content

Commit

Permalink
Add context-awareness to gexe packages (#63)
Browse files Browse the repository at this point in the history
* Context support to exec package

* Add context awareness to top level functions

* Add context awareness to fs package

* Add context awareness to http package
  • Loading branch information
vladimirvivien authored Nov 25, 2024
1 parent a87a71f commit 718e158
Show file tree
Hide file tree
Showing 10 changed files with 501 additions and 115 deletions.
29 changes: 19 additions & 10 deletions exec/builder.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package exec

import (
"context"
"fmt"
"io"
"sync"
Expand Down Expand Up @@ -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)
Expand Down
98 changes: 74 additions & 24 deletions exec/proc.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package exec

import (
"bytes"
"context"
"fmt"
"io"
"os"
Expand Down Expand Up @@ -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

Expand All @@ -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
Expand Down
33 changes: 22 additions & 11 deletions filesys.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package gexe

import (
"context"
"os"

"github.com/vladimirvivien/gexe/fs"
Expand Down Expand Up @@ -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)
}
65 changes: 53 additions & 12 deletions fs/file_reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package fs
import (
"bufio"
"bytes"
"context"
"io"
"os"

Expand All @@ -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
Expand All @@ -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
Expand All @@ -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())
}

Expand All @@ -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()
}

Expand All @@ -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
}
Expand Down
Loading

0 comments on commit 718e158

Please sign in to comment.