diff --git a/go.mod b/go.mod
index 319179b1912309261dbf3e4f3de219101e241780..6cdb1b409cfdcf24349b8d122a55d7ad5f87658e 100644
--- a/go.mod
+++ b/go.mod
@@ -21,7 +21,7 @@ require (
 	github.com/stretchr/testify v1.9.0
 	github.com/vishvananda/netlink v1.1.0
 	github.com/vishvananda/netns v0.0.4
-	github.com/vladimirvivien/gexe v0.2.0
+	github.com/vladimirvivien/gexe v0.3.0
 	github.com/vmware/go-ipfix v0.9.0
 	golang.org/x/sys v0.21.0
 	google.golang.org/grpc v1.63.0
diff --git a/go.sum b/go.sum
index dec66e64d3089f709e885da738cf3a0fdf4eb83a..3c61fbae3f4b66855bebf863b9e53397519def25 100644
--- a/go.sum
+++ b/go.sum
@@ -856,8 +856,8 @@ github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYp
 github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
 github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
 github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
-github.com/vladimirvivien/gexe v0.2.0 h1:nbdAQ6vbZ+ZNsolCgSVb9Fno60kzSuvtzVh6Ytqi/xY=
-github.com/vladimirvivien/gexe v0.2.0/go.mod h1:LHQL00w/7gDUKIak24n801ABp8C+ni6eBht9vGVst8w=
+github.com/vladimirvivien/gexe v0.3.0 h1:4xwiOwGrDob5OMR6E92B9olDXYDglXdHhzR1ggYtWJM=
+github.com/vladimirvivien/gexe v0.3.0/go.mod h1:fp7cy60ON1xjhtEI/+bfSEIXX35qgmI+iRYlGOqbBFM=
 github.com/vmware/go-ipfix v0.9.0 h1:4/N5eFliqULEaCUQV0lafOpN/1bItPE9OTAPGhrIXus=
 github.com/vmware/go-ipfix v0.9.0/go.mod h1:MYEdL6Uel2ufOZyVCKvIAaw9hwnewK8aPr7rnwRbxMY=
 github.com/willf/bitset v1.1.3/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
diff --git a/vendor/github.com/vladimirvivien/gexe/TODO.md b/vendor/github.com/vladimirvivien/gexe/TODO.md
index 0d9bb6c477e57ff2bd8897aaddf6bdfaed6d1e44..022f44d11571137ea6775e0b9e4c7c7d6448629e 100644
--- a/vendor/github.com/vladimirvivien/gexe/TODO.md
+++ b/vendor/github.com/vladimirvivien/gexe/TODO.md
@@ -3,7 +3,7 @@ The following are high-level tasks that will be considered for upcoming release
 
 * [ ] Support and test on Windows platform (#27)
 * [ ] Ability to map and access program flags (#20)
-* [ ] Provide a simple API to submit HTTP requests and retrieve HTTP documents (think of wget/curl)
+* [x] Provide a simple API to submit HTTP requests and retrieve HTTP documents (think of wget/curl)
 * [x] Support for scatter/gather exec commands
 * [x] Support for concurrent exec of os commands
 * [x] Piping/chaining OS exec commands (#29)
diff --git a/vendor/github.com/vladimirvivien/gexe/config.go b/vendor/github.com/vladimirvivien/gexe/config.go
deleted file mode 100644
index 932f9b875a9d80118a53fc19aae747be25394d6a..0000000000000000000000000000000000000000
--- a/vendor/github.com/vladimirvivien/gexe/config.go
+++ /dev/null
@@ -1,41 +0,0 @@
-package gexe
-
-// Config stores configuration
-type Config struct {
-	panicOnErr bool
-	verbose    bool
-	escapeChar rune
-}
-
-// SetPanicOnErr panics program on any error
-func (c *Config) SetPanicOnErr(val bool) *Config {
-	c.panicOnErr = val
-	return c
-}
-
-// IsPanicOnErr returns panic-on-error flag
-func (c *Config) IsPanicOnErr() bool {
-	return c.panicOnErr
-}
-
-// SetVerbose sets verbosity
-func (c *Config) SetVerbose(val bool) *Config {
-	c.verbose = val
-	return c
-}
-
-// IsVerbose returns verbosity flag
-func (c *Config) IsVerbose() bool {
-	return c.verbose
-}
-
-// SetEscapeChar sets the escape char for command-line parsing
-func (c *Config) SetEscapeChar(r rune) *Config {
-	c.escapeChar = r
-	return c
-}
-
-// GetEscapeChar returns the escape char set for command-line parsing
-func (c *Config) GetEscapeChar() rune {
-	return c.escapeChar
-}
diff --git a/vendor/github.com/vladimirvivien/gexe/echo.go b/vendor/github.com/vladimirvivien/gexe/echo.go
index 53156ca2bbae1f2c6e91234a05fb55778a2fe61a..70eac5eb06c9ddbaae98f2f9683f28f5c7eb27ba 100644
--- a/vendor/github.com/vladimirvivien/gexe/echo.go
+++ b/vendor/github.com/vladimirvivien/gexe/echo.go
@@ -1,6 +1,10 @@
 package gexe
 
 import (
+	"fmt"
+	"os"
+	"os/exec"
+
 	"github.com/vladimirvivien/gexe/prog"
 	"github.com/vladimirvivien/gexe/vars"
 )
@@ -16,7 +20,6 @@ type Echo struct {
 	err  error
 	vars *vars.Variables // session vars
 	prog *prog.Info
-	Conf *Config // session config
 }
 
 // New creates a new Echo session
@@ -24,7 +27,21 @@ func New() *Echo {
 	e := &Echo{
 		vars: vars.New(),
 		prog: prog.Prog(),
-		Conf: &Config{escapeChar: '\\'},
 	}
 	return e
 }
+
+// AddExecPath adds an executable path to PATH
+func (e *Echo) AddExecPath(execPath string) {
+	oldPath := os.Getenv("PATH")
+	os.Setenv("PATH", fmt.Sprintf("%s%c%s", oldPath, os.PathListSeparator, e.Eval(execPath)))
+}
+
+// ProgAvail returns the full path of the program if found on exec PATH
+func (e *Echo) ProgAvail(progName string) string {
+	path, err := exec.LookPath(e.Eval(progName))
+	if err != nil {
+		return ""
+	}
+	return path
+}
diff --git a/vendor/github.com/vladimirvivien/gexe/exec/builder.go b/vendor/github.com/vladimirvivien/gexe/exec/builder.go
index 93709dcde757fded71c80f94fb14fa56fb97d695..c1b41f90503329e3ff774c1b93213f06a3fc6e8d 100644
--- a/vendor/github.com/vladimirvivien/gexe/exec/builder.go
+++ b/vendor/github.com/vladimirvivien/gexe/exec/builder.go
@@ -1,7 +1,10 @@
 package exec
 
 import (
+	"fmt"
 	"sync"
+
+	"github.com/vladimirvivien/gexe/vars"
 )
 
 type CommandPolicy byte
@@ -19,29 +22,75 @@ type CommandResult struct {
 	errProcs []*Proc
 }
 
+// Procs return all executed processes
 func (cr *CommandResult) Procs() []*Proc {
 	cr.mu.RLock()
 	defer cr.mu.RUnlock()
 	return cr.procs
 }
+
+// ErrProcs returns errored processes
 func (cr *CommandResult) ErrProcs() []*Proc {
 	cr.mu.RLock()
 	defer cr.mu.RUnlock()
 	return cr.errProcs
 }
 
+// Errs returns all errors
+func (cr *CommandResult) Errs() (errs []error) {
+	cr.mu.RLock()
+	defer cr.mu.RUnlock()
+
+	for _, proc := range cr.errProcs {
+		errs = append(errs, fmt.Errorf("%s: %s", proc.Err(), proc.Result()))
+	}
+	return
+}
+
+// ErrStrings returns errors as []string
+func (cr *CommandResult) ErrStrings() (errStrings []string) {
+	errs := cr.Errs()
+	for _, err := range errs {
+		errStrings = append(errStrings, err.Error())
+	}
+	return
+}
+
+// PipedCommandResult stores results of piped commands
 type PipedCommandResult struct {
 	procs    []*Proc
 	errProcs []*Proc
 	lastProc *Proc
 }
 
+// Procs return all executed processes in pipe
 func (cr *PipedCommandResult) Procs() []*Proc {
 	return cr.procs
 }
+
+// ErrProcs returns errored piped processes
 func (cr *PipedCommandResult) ErrProcs() []*Proc {
 	return cr.errProcs
 }
+
+// Errs returns all errors
+func (cr *PipedCommandResult) Errs() (errs []error) {
+	for _, proc := range cr.errProcs {
+		errs = append(errs, fmt.Errorf("%s: %s", proc.Err(), proc.Result()))
+	}
+	return
+}
+
+// ErrStrings returns errors as []string
+func (cr *PipedCommandResult) ErrStrings() (errStrings []string) {
+	errs := cr.Errs()
+	for _, err := range errs {
+		errStrings = append(errStrings, err.Error())
+	}
+	return
+}
+
+// LastProc executes last executed process
 func (cr *PipedCommandResult) LastProc() *Proc {
 	procLen := len(cr.procs)
 	if procLen == 0 {
@@ -50,21 +99,34 @@ func (cr *PipedCommandResult) LastProc() *Proc {
 	return cr.procs[procLen-1]
 }
 
+// CommandBuilder is a batch command builder that
+// can execute commands using different execution policies (i.e. serial, piped, concurrent)
 type CommandBuilder struct {
 	cmdPolicy CommandPolicy
 	procs     []*Proc
+	vars      *vars.Variables
 }
 
 // Commands creates a *CommandBuilder used to collect
 // command strings to be executed.
 func Commands(cmds ...string) *CommandBuilder {
 	cb := new(CommandBuilder)
+	cb.vars = &vars.Variables{}
 	for _, cmd := range cmds {
 		cb.procs = append(cb.procs, NewProc(cmd))
 	}
 	return cb
 }
 
+// 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
+}
+
 // WithPolicy sets one or more command policy mask values, i.e. (CmdOnErrContinue | CmdExecConcurrent)
 func (cb *CommandBuilder) WithPolicy(policyMask CommandPolicy) *CommandBuilder {
 	cb.cmdPolicy = policyMask
@@ -74,7 +136,7 @@ func (cb *CommandBuilder) WithPolicy(policyMask CommandPolicy) *CommandBuilder {
 // Add adds a new command string to the builder
 func (cb *CommandBuilder) Add(cmds ...string) *CommandBuilder {
 	for _, cmd := range cmds {
-		cb.procs = append(cb.procs, NewProc(cmd))
+		cb.procs = append(cb.procs, NewProc(cb.vars.Eval(cmd)))
 	}
 	return cb
 }
diff --git a/vendor/github.com/vladimirvivien/gexe/exec/proc.go b/vendor/github.com/vladimirvivien/gexe/exec/proc.go
index 00899a43fdc3e10d379c827fa23bf782dd296c56..a91db489ddec451347f355c58901b81d60e67950 100644
--- a/vendor/github.com/vladimirvivien/gexe/exec/proc.go
+++ b/vendor/github.com/vladimirvivien/gexe/exec/proc.go
@@ -8,6 +8,8 @@ import (
 	osexec "os/exec"
 	"strings"
 	"time"
+
+	"github.com/vladimirvivien/gexe/vars"
 )
 
 // Proc stores process info when running a process
@@ -21,6 +23,7 @@ type Proc struct {
 	inputPipe  io.WriteCloser
 	cmd        *osexec.Cmd
 	process    *os.Process
+	vars       *vars.Variables
 }
 
 // NewProc sets up command string to be started as an OS process, however
@@ -34,7 +37,6 @@ func NewProc(cmdStr string) *Proc {
 	command := osexec.Command(words[0], words[1:]...)
 	pipeout, outerr := command.StdoutPipe()
 	pipeerr, errerr := command.StderrPipe()
-	//output := io.MultiReader(pipeout, pipeerr)
 
 	if outerr != nil || errerr != nil {
 		return &Proc{err: fmt.Errorf("combinedOutput pipe: %s; %s", outerr, errerr)}
@@ -51,9 +53,17 @@ func NewProc(cmdStr string) *Proc {
 		errorPipe:  pipeerr,
 		inputPipe:  pipein,
 		result:     new(bytes.Buffer),
+		vars:       &vars.Variables{},
 	}
 }
 
+// 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.vars = variables
+	return p
+}
+
 // StartProc starts an OS process (setup a combined output of stdout, stderr) and does not wait for
 // it to complete. You must follow this with either proc.Wait() to wait for result directly. Otherwise,
 // call proc.Out() or proc.Result() which automatically waits and gather result.
@@ -67,6 +77,13 @@ func StartProc(cmdStr string) *Proc {
 	return proc.Start()
 }
 
+// StartProcWithVars sets session variables and calls StartProc
+func StartProcWithVars(cmdStr string, variables *vars.Variables) *Proc {
+	proc := StartProc(variables.Eval(cmdStr))
+	proc.vars = variables
+	return proc
+}
+
 // RunProc starts a new process and waits for its completion. Use Proc.Out() or Proc.Result()
 // to access the combined result from stdout and stderr.
 func RunProc(cmdStr string) *Proc {
@@ -78,12 +95,30 @@ func RunProc(cmdStr string) *Proc {
 	return proc
 }
 
+// RunProcWithVars sets session variables and calls RunProc
+func RunProcWithVars(cmdStr string, variables *vars.Variables) *Proc {
+	proc := RunProc(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().
 func Run(cmdStr string) (result string) {
 	return RunProc(cmdStr).Result()
 }
 
+// RunWithVars sets session variables and call Run
+func RunWithVars(cmdStr string, variables *vars.Variables) string {
+	return RunProcWithVars(cmdStr, variables).Result()
+}
+
+// SetVars sets session variables for Proc
+func (p *Proc) SetVars(variables *vars.Variables) *Proc {
+	p.vars = variables
+	return p
+}
+
 // Start starts the associated command as an OS process and does not wait for its result.
 // Ensure proper access to the process' input/output (stdin,stdout,stderr) has been
 // setup prior to calling p.Start().
@@ -280,3 +315,8 @@ func (p *Proc) GetErrorPipe() io.Reader {
 func (p *Proc) hasStarted() bool {
 	return (p.cmd.Process != nil && p.cmd.Process.Pid != 0)
 }
+
+// Parse parses the command string and returns its tokens
+func Parse(cmd string) ([]string, error) {
+	return parse(cmd)
+}
diff --git a/vendor/github.com/vladimirvivien/gexe/filesys.go b/vendor/github.com/vladimirvivien/gexe/filesys.go
index bc0d91c5aa0ae3317a074fa1a95cdd2682339588..ca1cde7f74314bb1df65b47ed13c6a82ff9ca7c2 100644
--- a/vendor/github.com/vladimirvivien/gexe/filesys.go
+++ b/vendor/github.com/vladimirvivien/gexe/filesys.go
@@ -1,15 +1,53 @@
 package gexe
 
 import (
+	"os"
+
 	"github.com/vladimirvivien/gexe/fs"
 )
 
-// Read creates an fs.FileReader using the provided path
-func (e *Echo) Read(path string) fs.FileReader {
-	return fs.Read(e.Eval(path))
+// PathExists returns true if path exists.
+// All errors causes to return false.
+func (e *Echo) PathExists(path string) bool {
+	return fs.PathWithVars(path, e.vars).Exists()
+}
+
+// MkDir creates a directory at specified path with mode value.
+// FSInfo contains information about the path or error if occured
+func (e *Echo) MkDir(path string, mode os.FileMode) *fs.FSInfo {
+	p := fs.PathWithVars(path, e.vars)
+	return p.MkDir(mode)
+}
+
+// RmPath removes specified path (dir or file).
+// Error is returned FSInfo.Err()
+func (e *Echo) RmPath(path string) *fs.FSInfo {
+	p := fs.PathWithVars(path, e.vars)
+	return p.Remove()
+}
+
+// PathInfo
+func (e *Echo) PathInfo(path string) *fs.FSInfo {
+	return fs.PathWithVars(path, e.vars).Info()
+}
+
+// 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()
+}
+
+// FileWrite provides methods to write content to provided path
+//
+// FileWrite(path).String("hello world")
+func (e *Echo) FileWrite(path string) *fs.FileWriter {
+	return fs.PathWithVars(path, e.vars).Write()
 }
 
-// Write creates an fs.FileWriter using the provided path
-func (e *Echo) Write(path string) fs.FileWriter {
-	return fs.Write(e.Eval(path))
+// FileAppend provides methods to append content to provided path
+//
+// FileAppend(path).String("hello world")
+func (e *Echo) FileAppend(path string) *fs.FileWriter {
+	return fs.PathWithVars(path, e.vars).Append()
 }
diff --git a/vendor/github.com/vladimirvivien/gexe/fs/file_reader.go b/vendor/github.com/vladimirvivien/gexe/fs/file_reader.go
index 061700498795327b2b099619dcc9182e2f609007..8e5be7f4cd2e93a6a2a065c2c8ee19b55c229273 100644
--- a/vendor/github.com/vladimirvivien/gexe/fs/file_reader.go
+++ b/vendor/github.com/vladimirvivien/gexe/fs/file_reader.go
@@ -5,40 +5,54 @@ import (
 	"bytes"
 	"io"
 	"os"
+
+	"github.com/vladimirvivien/gexe/vars"
 )
 
-type fileReader struct {
-	err   error
-	path  string
-	finfo os.FileInfo
+type FileReader struct {
+	err  error
+	path string
+	info os.FileInfo
+	mode os.FileMode
+	vars *vars.Variables
 }
 
-// Read creates a FileReader using the provided path.
+// Read creates a new FileReader using the provided path.
 // A non-nil FileReader.Err() is returned if file does not exist
 // or another error is generated.
-func Read(path string) FileReader {
-	fr := &fileReader{path: path}
-	info, err := os.Stat(fr.path)
+func Read(path string) *FileReader {
+	info, err := os.Stat(path)
 	if err != nil {
-		fr.err = err
-		return fr
+		return &FileReader{err: err, path: path}
 	}
-	fr.finfo = info
+	return &FileReader{path: path, info: info, mode: info.Mode()}
+}
+
+// ReadWithVars creates a new FileReader and sets the reader's session variables
+func ReadWithVars(path string, variables *vars.Variables) *FileReader {
+	reader := Read(variables.Eval(path))
+	reader.vars = variables
+	return reader
+}
+
+// SetVars sets the FileReader's session variables
+func (fr *FileReader) SetVars(variables *vars.Variables) *FileReader {
+	fr.vars = variables
 	return fr
 }
 
 // Err returns an operation error during file read.
-func (fr *fileReader) Err() error {
+func (fr *FileReader) Err() error {
 	return fr.err
 }
 
 // Info surfaces the os.FileInfo for the associated file
-func (fr *fileReader) Info() os.FileInfo {
-	return fr.finfo
+func (fr *FileReader) Info() os.FileInfo {
+	return fr.info
 }
 
 // String returns the content of the file as a string value
-func (fr *fileReader) String() string {
+func (fr *FileReader) String() string {
 	file, err := os.Open(fr.path)
 	if err != nil {
 		fr.err = err
@@ -56,7 +70,7 @@ func (fr *fileReader) String() string {
 }
 
 // Lines returns the content of the file as slice of string
-func (fr *fileReader) Lines() []string {
+func (fr *FileReader) Lines() []string {
 	file, err := os.Open(fr.path)
 	if err != nil {
 		fr.err = err
@@ -78,7 +92,7 @@ func (fr *fileReader) Lines() []string {
 }
 
 // Bytes returns the content of the file as []byte
-func (fr *fileReader) Bytes() []byte {
+func (fr *FileReader) Bytes() []byte {
 	file, err := os.Open(fr.path)
 	if err != nil {
 		fr.err = err
@@ -98,7 +112,7 @@ func (fr *fileReader) Bytes() []byte {
 
 // Into reads the content of the file and writes
 // it into the specified Writer
-func (fr *fileReader) Into(w io.Writer) FileReader {
+func (fr *FileReader) Into(w io.Writer) *FileReader {
 	file, err := os.Open(fr.path)
 	if err != nil {
 		fr.err = err
diff --git a/vendor/github.com/vladimirvivien/gexe/fs/file_writer.go b/vendor/github.com/vladimirvivien/gexe/fs/file_writer.go
index 5b4d3683c577fa3aebd1b56e829ec8ab2ed77fc8..3360669d74f2a83043fc93942cf1bf17dded5831 100644
--- a/vendor/github.com/vladimirvivien/gexe/fs/file_writer.go
+++ b/vendor/github.com/vladimirvivien/gexe/fs/file_writer.go
@@ -3,21 +3,22 @@ package fs
 import (
 	"io"
 	"os"
+
+	"github.com/vladimirvivien/gexe/vars"
 )
 
-type fileWriter struct {
+type FileWriter struct {
 	path  string
 	err   error
 	finfo os.FileInfo
 	mode  os.FileMode
 	flags int
+	vars  *vars.Variables
 }
 
-// Write creates a new file,or truncates an existing one,
-// using the path provided and sets it up for write operations.
-// Operation error is returned by FileWriter.Err().
-func Write(path string) FileWriter {
-	fw := &fileWriter{path: path, flags: os.O_CREATE | os.O_TRUNC | os.O_WRONLY, mode: 0644}
+// 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{}}
 	info, err := os.Stat(fw.path)
 	if err == nil {
 		fw.finfo = info
@@ -25,11 +26,16 @@ func Write(path string) FileWriter {
 	return fw
 }
 
-// Append creates a new file, or append to an existing one,
-// using the path provided and sets it up for write operation only.
-// Any error generated is returned by FileWriter.Err().
-func Append(path string) FileWriter {
-	fw := &fileWriter{path: path, flags: os.O_CREATE | os.O_APPEND | os.O_WRONLY, mode: 0644}
+// WriteWithVars creates a new FileWriter and sets sessions variables
+func WriteWithVars(path string, variables *vars.Variables) *FileWriter {
+	fw := Write(variables.Eval(path))
+	fw.vars = variables
+	return fw
+}
+
+// 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
@@ -38,19 +44,39 @@ func Append(path string) FileWriter {
 	return fw
 }
 
-// Err returns an error during execution
-func (fw *fileWriter) Err() error {
+// AppendWithVars creates a new FileWriter and sets session variables
+func AppendWitVars(path string, variables *vars.Variables) *FileWriter {
+	fw := Append(variables.Eval(path))
+	fw.vars = variables
+	return fw
+}
+
+// SetVars sets session variables for FileWriter
+func (fw *FileWriter) SetVars(variables *vars.Variables) *FileWriter {
+	if variables != nil {
+		fw.vars = variables
+	}
+	return fw
+}
+
+func (fw *FileWriter) WithMode(mode os.FileMode) *FileWriter {
+	fw.mode = mode
+	return fw
+}
+
+// Err returns FileWriter error during execution
+func (fw *FileWriter) Err() error {
 	return fw.err
 }
 
 // Info returns the os.FileInfo for the associated file
-func (fw *fileWriter) Info() os.FileInfo {
+func (fw *FileWriter) Info() os.FileInfo {
 	return fw.finfo
 }
 
 // String writes the provided str into the file. Any
 // error that occurs can be accessed with FileWriter.Err().
-func (fw *fileWriter) String(str string) FileWriter {
+func (fw *FileWriter) String(str string) *FileWriter {
 	file, err := os.OpenFile(fw.path, fw.flags, fw.mode)
 	if err != nil {
 		fw.err = err
@@ -69,7 +95,7 @@ func (fw *fileWriter) String(str string) FileWriter {
 
 // Lines writes the slice of strings into the file.
 // Any error will be captured and returned via FileWriter.Err().
-func (fw *fileWriter) Lines(lines []string) FileWriter {
+func (fw *FileWriter) Lines(lines []string) *FileWriter {
 	file, err := os.OpenFile(fw.path, fw.flags, fw.mode)
 	if err != nil {
 		fw.err = err
@@ -98,7 +124,7 @@ func (fw *fileWriter) Lines(lines []string) FileWriter {
 
 // Bytes writes the []bytre provided into the file.
 // Any error can be accessed using FileWriter.Err().
-func (fw *fileWriter) Bytes(data []byte) FileWriter {
+func (fw *FileWriter) Bytes(data []byte) *FileWriter {
 	file, err := os.OpenFile(fw.path, fw.flags, fw.mode)
 	if err != nil {
 		fw.err = err
@@ -118,7 +144,7 @@ func (fw *fileWriter) Bytes(data []byte) FileWriter {
 // From streams bytes from the provided io.Reader r and
 // writes them to the file. Any error will be captured
 // and returned by fw.Err().
-func (fw *fileWriter) From(r io.Reader) FileWriter {
+func (fw *FileWriter) From(r io.Reader) *FileWriter {
 	file, err := os.OpenFile(fw.path, fw.flags, fw.mode)
 	if err != nil {
 		fw.err = err
diff --git a/vendor/github.com/vladimirvivien/gexe/fs/fsinfo.go b/vendor/github.com/vladimirvivien/gexe/fs/fsinfo.go
new file mode 100644
index 0000000000000000000000000000000000000000..b51e4470b3a4ec460004e4022634e427112571b1
--- /dev/null
+++ b/vendor/github.com/vladimirvivien/gexe/fs/fsinfo.go
@@ -0,0 +1,51 @@
+package fs
+
+import (
+	"io/fs"
+	"os"
+	"time"
+
+	"github.com/vladimirvivien/gexe/vars"
+)
+
+type FSInfo struct {
+	err  error
+	path string
+	mode os.FileMode
+	info os.FileInfo
+	vars *vars.Variables
+}
+
+// Err returns last opertion error on the directory
+func (i *FSInfo) Err() error {
+	return i.err
+}
+
+// Path is the original path for the directory
+func (i *FSInfo) Path() string {
+	return i.path
+}
+
+func (i *FSInfo) Name() string {
+	return i.info.Name()
+}
+
+// Mode returns the fs.FileMode for the directory
+func (i *FSInfo) Mode() fs.FileMode {
+	return i.mode
+}
+
+// Size returns the directory size or -1 if not known or error
+func (i *FSInfo) Size() int64 {
+	return i.info.Size()
+}
+
+// IsDir returns true if path points to a directory
+func (i *FSInfo) IsDir() bool {
+	return i.info.IsDir()
+}
+
+// ModTime returns the last know modification time.
+func (i *FSInfo) ModTime() time.Time {
+	return i.info.ModTime()
+}
diff --git a/vendor/github.com/vladimirvivien/gexe/fs/fspath.go b/vendor/github.com/vladimirvivien/gexe/fs/fspath.go
new file mode 100644
index 0000000000000000000000000000000000000000..4215fd4c7690deb160da8bfa8e19e0d20c7c9859
--- /dev/null
+++ b/vendor/github.com/vladimirvivien/gexe/fs/fspath.go
@@ -0,0 +1,115 @@
+package fs
+
+import (
+	"errors"
+	"io/fs"
+	"os"
+
+	"github.com/vladimirvivien/gexe/vars"
+)
+
+type FSPath struct {
+	err  error
+	path string
+	vars *vars.Variables
+}
+
+// Path points to a node path
+func Path(path string) *FSPath {
+	return &FSPath{path: path}
+}
+
+// PathWithVars points to a path and applies variables to the path value
+func PathWithVars(path string, variables *vars.Variables) *FSPath {
+	p := Path(variables.Eval(path))
+	p.vars = variables
+	return p
+}
+
+// Info returns information about the specified path
+func (p *FSPath) Info() *FSInfo {
+	info, err := os.Stat(p.path)
+	if err != nil {
+		return &FSInfo{err: err, path: p.path}
+	}
+	return &FSInfo{path: p.path, info: info, mode: info.Mode()}
+}
+
+// Exists returns true only if os.Stat nil error.
+// Any other scenarios will return false.
+func (p *FSPath) Exists() bool {
+	if _, err := os.Stat(p.path); err != nil {
+		return false
+	}
+	return true
+}
+
+// MkDir creates a directory with file mode at specified
+func (p *FSPath) MkDir(mode fs.FileMode) *FSInfo {
+	if err := os.MkdirAll(p.path, mode); err != nil {
+		if !errors.Is(err, os.ErrExist) {
+			return &FSInfo{err: err, path: p.path}
+		}
+	}
+	info, err := os.Stat(p.path)
+	if err != nil {
+		return &FSInfo{err: err, path: p.path}
+	}
+	return &FSInfo{path: p.path, info: info, mode: info.Mode(), vars: p.vars}
+}
+
+// Remove removes entry at path
+func (p *FSPath) Remove() *FSInfo {
+	info, err := os.Stat(p.path)
+	if err != nil {
+		if os.IsNotExist(err) {
+			return &FSInfo{path: p.path}
+		}
+		return &FSInfo{err: err, path: p.path}
+	}
+	if err := os.RemoveAll(p.path); err != nil {
+		return &FSInfo{err: err, path: p.path, info: info}
+	}
+	return &FSInfo{path: p.path, info: info, mode: info.Mode()}
+}
+
+// Read wraps call to create a new *FileReader instance
+func (p *FSPath) Read() *FileReader {
+	if p.vars != nil {
+		return ReadWithVars(p.path, p.vars)
+	}
+	return Read(p.path)
+}
+
+// Write wraps call to create a new *FileWriter instance
+func (p *FSPath) Write() *FileWriter {
+	if p.vars != nil {
+		return WriteWithVars(p.path, p.vars)
+	}
+	return Write(p.path)
+}
+
+// Append wraps call to create a new *FileWriter instance for file append operations
+func (p *FSPath) Append() *FileWriter {
+	if p.vars != nil {
+		return AppendWitVars(p.path, p.vars)
+	}
+	return Append(p.path)
+}
+
+// Dirs returns info about dirs in path
+func (p *FSPath) Dirs() (infos []*FSInfo) {
+	entries, err := os.ReadDir(p.path)
+	if err != nil {
+		p.err = err
+		return nil
+	}
+	for _, entry := range entries {
+		info, err := entry.Info()
+		if err != nil {
+			infos = append(infos, &FSInfo{err: err})
+		}
+		infos = append(infos, &FSInfo{info: info, mode: info.Mode()})
+	}
+	return
+}
diff --git a/vendor/github.com/vladimirvivien/gexe/fs/types.go b/vendor/github.com/vladimirvivien/gexe/fs/types.go
deleted file mode 100644
index 8ef18d1048ee86d0a480a8bec39012399303ca6b..0000000000000000000000000000000000000000
--- a/vendor/github.com/vladimirvivien/gexe/fs/types.go
+++ /dev/null
@@ -1,33 +0,0 @@
-package fs
-
-import (
-	"io"
-	"os"
-)
-
-// FileReader aggregates read operations from
-// diverse sources into a file
-type FileReader interface {
-	Err() error
-	Info() os.FileInfo
-	String() string
-	Lines() []string
-	Bytes() []byte
-	Into(io.Writer) FileReader
-}
-
-// FileWriter aggregates several file-writing operations from
-// diverse sources into a provided file.
-type FileWriter interface {
-	Err() error
-	Info() os.FileInfo
-	String(string) FileWriter
-	Lines([]string) FileWriter
-	Bytes([]byte) FileWriter
-	From(io.Reader) FileWriter
-}
-
-// FileAppender is FileWriter with append behavior
-type FileAppender interface {
-	FileWriter
-}
diff --git a/vendor/github.com/vladimirvivien/gexe/functions.go b/vendor/github.com/vladimirvivien/gexe/functions.go
index 7f76889c089aec3e0e78564f1a11815654ed0144..22555a8b219c96d4e53ab13b20a17c5ffe0908c5 100644
--- a/vendor/github.com/vladimirvivien/gexe/functions.go
+++ b/vendor/github.com/vladimirvivien/gexe/functions.go
@@ -1,10 +1,13 @@
 package gexe
 
 import (
+	"os"
+
 	"github.com/vladimirvivien/gexe/exec"
 	"github.com/vladimirvivien/gexe/fs"
 	"github.com/vladimirvivien/gexe/http"
 	"github.com/vladimirvivien/gexe/prog"
+	"github.com/vladimirvivien/gexe/str"
 	"github.com/vladimirvivien/gexe/vars"
 )
 
@@ -123,16 +126,40 @@ func Pipe(cmdStrs ...string) *exec.PipedCommandResult {
 	return DefaultEcho.Pipe(cmdStrs...)
 }
 
-// Read creates an fs.FileReader that
-// can be used to read content from files.
-func Read(path string) fs.FileReader {
-	return DefaultEcho.Read(path)
+// PathExists returns true if specified path exists.
+// Any error will cause it to return false.
+func PathExists(path string) bool {
+	return DefaultEcho.PathExists(path)
+}
+
+// PathInfo returns information for specified path (i.e. size, etc)
+func PathInfo(path string) *fs.FSInfo {
+	return DefaultEcho.PathInfo(path)
+}
+
+// MkDirs creates one or more directories along the specified path
+func MkDirs(path string, mode os.FileMode) *fs.FSInfo {
+	return DefaultEcho.MkDir(path, mode)
 }
 
-// Write creates an fs.FileWriter that
-// can be used to write content to files
-func Write(path string) fs.FileWriter {
-	return DefaultEcho.Write(path)
+// MkDir creates a directory with default mode 0744
+func MkDir(path string) *fs.FSInfo {
+	return DefaultEcho.MkDir(path, 0744)
+}
+
+// RmPath removes files or directories along specified path
+func RmPath(path string) *fs.FSInfo {
+	return DefaultEcho.RmPath(path)
+}
+
+// FileRead provides methods to read file content from path
+func FileRead(path string) *fs.FileReader {
+	return DefaultEcho.FileRead(path)
+}
+
+// FileWrite provides methods to write file content to path
+func FileWrite(path string) *fs.FileWriter {
+	return DefaultEcho.FileWrite(path)
 }
 
 // GetUrl creates a *http.ResourceReader to retrieve HTTP content
@@ -149,3 +176,22 @@ func PostUrl(url string) *http.ResourceWriter {
 func Prog() *prog.Info {
 	return DefaultEcho.Prog()
 }
+
+// ProgAvail returns the full path of the program if available.
+func ProgAvail(program string) string {
+	return DefaultEcho.ProgAvail(program)
+}
+
+// Workdir returns the current program's working directory
+func Workdir() string {
+	return DefaultEcho.Workdir()
+}
+
+// AddExecPath adds an executable path to PATH
+func AddExecPath(execPath string) {
+	DefaultEcho.AddExecPath(execPath)
+}
+
+func String(s string) *str.Str {
+	return DefaultEcho.String(s)
+}
diff --git a/vendor/github.com/vladimirvivien/gexe/http.go b/vendor/github.com/vladimirvivien/gexe/http.go
index a22406ee32b461a0a2e9af6977d2f034b921c758..95c657cbaa3fe2ee5dd847b0ad82002c6ec3f147 100644
--- a/vendor/github.com/vladimirvivien/gexe/http.go
+++ b/vendor/github.com/vladimirvivien/gexe/http.go
@@ -1,13 +1,27 @@
 package gexe
 
-import "github.com/vladimirvivien/gexe/http"
+import (
+	"strings"
+
+	"github.com/vladimirvivien/gexe/http"
+)
 
 // Get creates a *http.ResourceReader to read resource content from HTTP server
-func (e *Echo) Get(url string) *http.ResourceReader {
-	return http.Get(url)
+func (e *Echo) Get(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)
 }
 
 // Post creates a *http.ResourceWriter to write content to an HTTP server
-func (e *Echo) Post(url string) *http.ResourceWriter {
-	return http.Post(url)
+func (e *Echo) Post(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)
 }
diff --git a/vendor/github.com/vladimirvivien/gexe/http/http_reader.go b/vendor/github.com/vladimirvivien/gexe/http/http_reader.go
index 16653bdf1171821b784a03d0bb759ab2b07e3b4c..60db91d03cbd6e3986d1ab8ada3ac7c86e29a2f9 100644
--- a/vendor/github.com/vladimirvivien/gexe/http/http_reader.go
+++ b/vendor/github.com/vladimirvivien/gexe/http/http_reader.go
@@ -3,6 +3,9 @@ package http
 import (
 	"io"
 	"net/http"
+	"time"
+
+	"github.com/vladimirvivien/gexe/vars"
 )
 
 // ResourceReader provides types and methods to read content of resources from a server using HTTP
@@ -10,12 +13,25 @@ type ResourceReader struct {
 	client *http.Client
 	err    error
 	url    string
-	res    *Response
+	vars   *vars.Variables
 }
 
 // Get initiates a "GET" operation for the specified resource
 func Get(url string) *ResourceReader {
-	return &ResourceReader{url: url, client: &http.Client{}}
+	return &ResourceReader{url: url, client: &http.Client{}, vars: &vars.Variables{}}
+}
+
+// Get initiates a "GET" operation and sets session variables
+func GetWithVars(url string, variables *vars.Variables) *ResourceReader {
+	r := Get(variables.Eval(url))
+	r.vars = variables
+	return r
+}
+
+// SetVars sets session variables for ResourceReader
+func (r *ResourceReader) SetVars(variables *vars.Variables) *ResourceReader {
+	r.vars = variables
+	return r
 }
 
 // Err returns the last known error
@@ -23,62 +39,52 @@ func (r *ResourceReader) Err() error {
 	return r.err
 }
 
-// Response returns the server's response info
-func (r *ResourceReader) Response() *Response {
-	return r.res
+// WithTimeout sets the HTTP reader's timeout
+func (r *ResourceReader) WithTimeout(to time.Duration) *ResourceReader {
+	r.client.Timeout = to
+	return r
+}
+
+// Do invokes the client.Get to "GET" the content from server
+// Use Response.Err() to access server response errors
+func (r *ResourceReader) Do() *Response {
+	res, err := r.client.Get(r.url)
+	if err != nil {
+		return &Response{err: err}
+	}
+	return &Response{stat: res.Status, statCode: res.StatusCode, body: res.Body}
 }
 
 // Bytes returns the server response as a []byte
-func (b *ResourceReader) Bytes() []byte {
-	if err := b.Do().Err(); err != nil {
-		b.err = err
+// This is a shorthad for ResourceReader.Do().Bytes()
+func (r *ResourceReader) Bytes() []byte {
+	resp := r.Do()
+	if resp.Err() != nil {
+		r.err = resp.Err()
 		return nil
 	}
-	return b.read()
+	return resp.Bytes()
 }
 
-// String returns the server response as a string
-func (b *ResourceReader) String() string {
-	if err := b.Do().Err(); err != nil {
-		b.err = err
+// String returns the server response as a string.
+// It is a shorthad for ResourceReader.Do().String()
+func (r *ResourceReader) String() string {
+	resp := r.Do()
+	if resp.Err() != nil {
+		r.err = resp.Err()
 		return ""
 	}
-	return string(b.read())
+	return resp.String()
 }
 
-// Body returns an io.ReadCloser to stream the server response.
+// Body returns the server response body (as io.ReadCloser).
+// It is a shorthand for ResourceReader().Do().Body()
 // NOTE: ensure to close the stream when finished.
 func (r *ResourceReader) Body() io.ReadCloser {
-	if err := r.Do().Err(); err != nil {
-		r.err = err
-		return nil
-	}
-	return r.res.body
-}
-
-// Do invokes the client.Get to "GET" the content from server
-func (r *ResourceReader) Do() *ResourceReader {
-	res, err := r.client.Get(r.url)
-	if err != nil {
-		r.err = err
-		r.res = &Response{}
-		return r
-	}
-	r.res = &Response{stat: res.Status, statCode: res.StatusCode, body: res.Body}
-	return r
-}
-
-// read reads the content of the response body and returns a []byte
-func (r *ResourceReader) read() []byte {
-	if r.res.body == nil {
-		return nil
-	}
-
-	data, err := io.ReadAll(r.res.body)
-	defer r.res.body.Close()
-	if err != nil {
-		r.err = err
+	resp := r.Do()
+	if resp.Err() != nil {
+		r.err = resp.Err()
 		return nil
 	}
-	return data
+	return resp.Body()
 }
diff --git a/vendor/github.com/vladimirvivien/gexe/http/http_response.go b/vendor/github.com/vladimirvivien/gexe/http/http_response.go
index adf2de977e86d4c566b2e391867056d367d48e1c..6ed82450ef8488a2a8b403cd68eb19a240ac772a 100644
--- a/vendor/github.com/vladimirvivien/gexe/http/http_response.go
+++ b/vendor/github.com/vladimirvivien/gexe/http/http_response.go
@@ -7,6 +7,7 @@ type Response struct {
 	stat     string
 	statCode int
 	body     io.ReadCloser
+	err      error
 }
 
 // Status returns the standard lib http.Response.Status value from the server
@@ -24,3 +25,33 @@ func (res *Response) StatusCode() int {
 func (res *Response) Body() io.ReadCloser {
 	return res.body
 }
+
+// Err returns the response known error
+func (r *Response) Err() error {
+	return r.err
+}
+
+// Bytes returns the server response as a []byte
+func (r *Response) Bytes() []byte {
+	return r.read()
+}
+
+// String returns the server response as a string
+func (r *Response) String() string {
+	return string(r.read())
+}
+
+// read reads the content of the response body and returns as []byte
+func (r *Response) read() []byte {
+	if r.body == nil {
+		return nil
+	}
+
+	data, err := io.ReadAll(r.body)
+	defer r.body.Close()
+	if err != nil {
+		r.err = err
+		return nil
+	}
+	return data
+}
diff --git a/vendor/github.com/vladimirvivien/gexe/http/http_writer.go b/vendor/github.com/vladimirvivien/gexe/http/http_writer.go
index c3ea4acc298b0a712e6c3b0cdde17c644f56ec96..3aa91637cd0bf13b922c75adb8c8f765e01686e3 100644
--- a/vendor/github.com/vladimirvivien/gexe/http/http_writer.go
+++ b/vendor/github.com/vladimirvivien/gexe/http/http_writer.go
@@ -6,6 +6,9 @@ import (
 	"net/http"
 	"net/url"
 	"strings"
+	"time"
+
+	"github.com/vladimirvivien/gexe/vars"
 )
 
 // ResourceWriter represents types and methods used to post resource data to an HTTP server
@@ -16,11 +19,31 @@ type ResourceWriter struct {
 	headers http.Header
 	data    io.Reader
 	res     *Response
+	vars    *vars.Variables
 }
 
 // Post starts a "POST" HTTP operation to the provided resource.
 func Post(resource string) *ResourceWriter {
-	return &ResourceWriter{url: resource, client: &http.Client{}, headers: make(http.Header)}
+	return &ResourceWriter{url: resource, client: &http.Client{}, headers: make(http.Header), vars: &vars.Variables{}}
+}
+
+// PostWithVars sets up a "POST" operation and sets its session variables
+func PostWithVars(resource string, variables *vars.Variables) *ResourceWriter {
+	w := Post(variables.Eval(resource))
+	w.vars = variables
+	return w
+}
+
+// SetVars sets session variables for the ResourceWriter
+func (w *ResourceWriter) SetVars(variables *vars.Variables) *ResourceWriter {
+	w.vars = variables
+	return w
+}
+
+// WithTimeout sets the HTTP client's timeout
+func (w *ResourceWriter) WithTimeout(to time.Duration) *ResourceWriter {
+	w.client.Timeout = to
+	return w
 }
 
 // Err returns the last known error for the post operation
@@ -61,19 +84,19 @@ func (w *ResourceWriter) WithHeaders(h http.Header) *ResourceWriter {
 
 // AddHeader is a convenience method to add a single header
 func (w *ResourceWriter) AddHeader(key, value string) *ResourceWriter {
-	w.headers.Add(key, value)
+	w.headers.Add(w.vars.Eval(key), w.vars.Eval(value))
 	return w
 }
 
 // SetHeader is a convenience method to sets a specific header
 func (w *ResourceWriter) SetHeader(key, value string) *ResourceWriter {
-	w.headers.Set(key, value)
+	w.headers.Set(w.vars.Eval(key), w.vars.Eval(value))
 	return w
 }
 
 // String posts the string value as content to the server
 func (w *ResourceWriter) String(val string) *ResourceWriter {
-	w.data = strings.NewReader(val)
+	w.data = strings.NewReader(w.vars.Eval(val))
 	return w.Do()
 }
 
diff --git a/vendor/github.com/vladimirvivien/gexe/net.go b/vendor/github.com/vladimirvivien/gexe/net.go
new file mode 100644
index 0000000000000000000000000000000000000000..641b72667c3762b6f13fa7716a1b12d866293f65
--- /dev/null
+++ b/vendor/github.com/vladimirvivien/gexe/net.go
@@ -0,0 +1,9 @@
+package gexe
+
+import (
+	"github.com/vladimirvivien/gexe/net"
+)
+
+func (e *Echo) AddressUsable(addr string) error {
+	return net.AddrUsable(e.Eval(addr))
+}
diff --git a/vendor/github.com/vladimirvivien/gexe/net/listener.go b/vendor/github.com/vladimirvivien/gexe/net/listener.go
new file mode 100644
index 0000000000000000000000000000000000000000..71a17b59ad5dba15076f01d4e1ea2157c6b88a08
--- /dev/null
+++ b/vendor/github.com/vladimirvivien/gexe/net/listener.go
@@ -0,0 +1,29 @@
+package net
+
+import (
+	"errors"
+	"fmt"
+	"net"
+	"os"
+	"syscall"
+)
+
+func AddrUsable(address string) error {
+	addr, err := net.ResolveTCPAddr("tcp", address)
+	if err != nil {
+		return fmt.Errorf("net: address parsing: %s", err)
+	}
+
+	lsnr, err := net.Listen(addr.Network(), addr.String())
+
+	if err != nil {
+		sysErr, ok := err.(*os.SyscallError)
+		if ok && errors.Is(sysErr.Err, syscall.EADDRINUSE) {
+			return fmt.Errorf("net: addr in use: %s", sysErr.Err)
+		}
+		return fmt.Errorf("net: %s", err)
+	}
+
+	defer lsnr.Close()
+	return nil
+}
diff --git a/vendor/github.com/vladimirvivien/gexe/procs.go b/vendor/github.com/vladimirvivien/gexe/procs.go
index 251e579ff27af4214fd0461a3cdf929dd62c76b7..ed230675bb149369002dba354e4932aecf410d02 100644
--- a/vendor/github.com/vladimirvivien/gexe/procs.go
+++ b/vendor/github.com/vladimirvivien/gexe/procs.go
@@ -10,25 +10,25 @@ import (
 // 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) NewProc(cmdStr string) *exec.Proc {
-	return exec.NewProc(cmdStr)
+	return exec.NewProcWithVars(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.StartProc(e.Eval(cmdStr))
+	return exec.StartProcWithVars(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.RunProc(e.Eval(cmdStr))
+	return exec.RunProcWithVars(cmdStr, e.vars)
 }
 
 // Run executes cmdStr, waits, and returns the result as a string.
 func (e *Echo) Run(cmdStr string) string {
-	return exec.Run(e.Eval(cmdStr))
+	return exec.RunWithVars(cmdStr, e.vars)
 }
 
 // Runout executes command cmdStr and prints out the result
@@ -38,52 +38,45 @@ func (e *Echo) Runout(cmdStr string) {
 
 // Commands returns a *exe.CommandBuilder to build a multi-command execution flow.
 func (e *Echo) Commands(cmdStrs ...string) *exec.CommandBuilder {
-	for i, cmd := range cmdStrs {
-		cmdStrs[i] = e.Eval(cmd)
-	}
-	return exec.Commands(cmdStrs...)
+	return exec.CommandsWithVars(e.vars, cmdStrs...)
 }
 
 // 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 {
-	for i, cmd := range cmdStrs {
-		cmdStrs[i] = e.Eval(cmd)
-	}
-	return exec.Commands(cmdStrs...).Start()
+	return exec.CommandsWithVars(e.vars, cmdStrs...).Start()
 }
 
 // RunAll executes each command sequentially, in cmdStrs, and wait for their completion.
 func (e *Echo) RunAll(cmdStrs ...string) *exec.CommandResult {
-	for i, cmd := range cmdStrs {
-		cmdStrs[i] = e.Eval(cmd)
-	}
-	return exec.Commands(cmdStrs...).Run()
+	return exec.CommandsWithVars(e.vars, cmdStrs...).Run()
 }
 
 // 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 {
-	for i, cmd := range cmdStrs {
-		cmdStrs[i] = e.Eval(cmd)
-	}
-	return exec.Commands(cmdStrs...).Concurr()
+	return exec.CommandsWithVars(e.vars, cmdStrs...).Concurr()
 }
 
 // RunConcur executes each command concurrently, in cmdStrs, and waits
 // their completion.
 func (e *Echo) RunConcur(cmdStrs ...string) *exec.CommandResult {
-	for i, cmd := range cmdStrs {
-		cmdStrs[i] = e.Eval(cmd)
-	}
-	return exec.Commands(cmdStrs...).Concurr().Wait()
+	return exec.CommandsWithVars(e.vars, cmdStrs...).Concurr().Wait()
 }
 
 // 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 {
-	for i, cmd := range cmdStrs {
-		cmdStrs[i] = e.Eval(cmd)
+	return exec.CommandsWithVars(e.vars, cmdStrs...).Pipe()
+}
+
+// ParseCommand parses the string into individual command tokens
+func (e *Echo) ParseCommand(cmdStr string) (cmdName string, args []string) {
+	result, err := exec.Parse(e.vars.Eval(cmdStr))
+	if err != nil {
+		e.err = err
 	}
-	return exec.Commands(cmdStrs...).Pipe()
+	cmdName = result[0]
+	args = result[1:]
+	return
 }
diff --git a/vendor/github.com/vladimirvivien/gexe/prog.go b/vendor/github.com/vladimirvivien/gexe/prog.go
index 2f342ce88032fb4213300029c398b0a4bc83f76c..e353a74d6b756b92076791c24e980733afd6927a 100644
--- a/vendor/github.com/vladimirvivien/gexe/prog.go
+++ b/vendor/github.com/vladimirvivien/gexe/prog.go
@@ -4,8 +4,12 @@ import (
 	"github.com/vladimirvivien/gexe/prog"
 )
 
-// Prog creates a new prog.Info to get information
-// about the running program
+// Prog makes info available about currently executing program
 func (e *Echo) Prog() *prog.Info {
 	return e.prog
 }
+
+// Workdir returns the current program's working directory
+func (e *Echo) Workdir() string {
+	return e.Prog().Workdir()
+}
diff --git a/vendor/github.com/vladimirvivien/gexe/prog/prog.go b/vendor/github.com/vladimirvivien/gexe/prog/prog.go
index 908e0c6db35fbce3da3809ee22f5e26ea38568cc..c7697ca9b491630e20116e1af1c1b208a8f5d50f 100644
--- a/vendor/github.com/vladimirvivien/gexe/prog/prog.go
+++ b/vendor/github.com/vladimirvivien/gexe/prog/prog.go
@@ -62,6 +62,7 @@ func (p *Info) Name() string {
 }
 
 // Avail returns full path of binary name if available
+// Deprecated: use echo.ProgAvail or gexe.ProgAvail
 func (p *Info) Avail(progName string) string {
 	path, err := exec.LookPath(progName)
 	if err != nil {
diff --git a/vendor/github.com/vladimirvivien/gexe/str.go b/vendor/github.com/vladimirvivien/gexe/str.go
new file mode 100644
index 0000000000000000000000000000000000000000..251dfffc654dc09e92d7497bb6b9d59c4dbd31df
--- /dev/null
+++ b/vendor/github.com/vladimirvivien/gexe/str.go
@@ -0,0 +1,8 @@
+package gexe
+
+import "github.com/vladimirvivien/gexe/str"
+
+// String creates a new str.Str value with string manipulation methods
+func (e *Echo) String(s string) *str.Str {
+	return str.StringWithVars(s, e.vars)
+}
diff --git a/vendor/github.com/vladimirvivien/gexe/str/functions.go b/vendor/github.com/vladimirvivien/gexe/str/functions.go
new file mode 100644
index 0000000000000000000000000000000000000000..75c779bcd9f5f3513f0d5ca9e8a0bc5eb8cc76bb
--- /dev/null
+++ b/vendor/github.com/vladimirvivien/gexe/str/functions.go
@@ -0,0 +1,53 @@
+package str
+
+import (
+	"fmt"
+)
+
+// IsEmpty tests for str == ""
+func IsEmpty(str string) bool {
+	return String(str).IsEmpty()
+}
+
+// SplitLines splits each line from str into []string
+func SplitLines(str string) []string {
+	return String(str).SplitLines()
+}
+
+// SplitSpaces splits str by blank chars (space,\t,\n)
+func SplitSpaces(str string) []string {
+	return String(str).SplitSpaces()
+}
+
+// Bool returns the bool equivalent of str ("true" = true, etc)
+// A parsing error will cause a program panic.
+func Bool(str string) bool {
+	s := String(str)
+	result := s.Bool()
+	if s.Err() != nil {
+		panic(fmt.Sprintf("%s", s.Err()))
+	}
+	return result
+}
+
+// Int returns the int representation of str.
+// A parsing error will cause a program panic.
+func Int(str string) int {
+	s := String(str)
+	result := s.Int()
+	if s.Err() != nil {
+		panic(fmt.Sprintf("%s", s.Err()))
+	}
+	return result
+}
+
+// Float64 returns the float64 representation of str.
+// A parsing error will cause a program panic.
+func Float64(str string) float64 {
+	s := String(str)
+	result := s.Float64()
+	if s.Err() != nil {
+		panic(fmt.Sprintf("%s", s.Err()))
+	}
+	return result
+}
diff --git a/vendor/github.com/vladimirvivien/gexe/str/strings.go b/vendor/github.com/vladimirvivien/gexe/str/strings.go
new file mode 100644
index 0000000000000000000000000000000000000000..53e2157df48d3784b06e1b41144131d42ae30582
--- /dev/null
+++ b/vendor/github.com/vladimirvivien/gexe/str/strings.go
@@ -0,0 +1,185 @@
+package str
+
+import (
+	"bytes"
+	"io"
+	"regexp"
+	"strconv"
+	"strings"
+
+	"github.com/vladimirvivien/gexe/vars"
+)
+
+var (
+	notSpaceRegex = regexp.MustCompile(`\S`)
+)
+
+// Str represents a string value
+type Str struct {
+	val  string
+	err  error
+	vars *vars.Variables
+}
+
+// String is constructor function that returns *Str
+func String(str string) *Str {
+	return &Str{val: str, vars: &vars.Variables{}}
+}
+
+// StringWithVars sets session variables and calls func String
+func StringWithVars(str string, variables *vars.Variables) *Str {
+	s := String(variables.Eval(str))
+	s.vars = variables
+	return s
+}
+
+// String returns the string value
+func (s *Str) String() string {
+	return s.val
+}
+
+// Err returns any captured error
+func (s *Str) Err() error {
+	return s.err
+}
+
+// IsEmpty returns true if len(s) == 0
+func (s *Str) IsEmpty() bool {
+	return s.val == ""
+}
+
+// Eq returns true if both strings are equal
+func (s *Str) Eq(val1 string) bool {
+	return strings.EqualFold(s.val, val1)
+}
+
+// Split s.val using the sep as delimiter
+func (s *Str) Split(sep string) []string {
+	return strings.Split(s.val, sep)
+}
+
+// SplitLines splits s.val using \n as delimiter
+func (s *Str) SplitLines() []string {
+	return strings.Split(s.val, "\n")
+}
+
+// SplitSpaces properly splits s.val into []elements
+// separated by one or more Unicode.IsSpace characters
+// i.e. SplitSpaces("ab   cd e\tf\ng") returns 5 elements
+func (s *Str) SplitSpaces() []string {
+	return notSpaceRegex.Split(s.val, -1)
+}
+
+// SplitRegex uses regular expression exp to split s.val
+func (s *Str) SplitRegex(exp string) []string {
+	return regexp.MustCompile(exp).Split(s.val, -1)
+}
+
+// Bytes returns []byte(s.val)
+func (s *Str) Bytes() []byte {
+	return []byte(s.val)
+}
+
+// Bool converts s.val from string to a bool representation
+// Check s.Err() for parsing errors
+func (s *Str) Bool() bool {
+	val, err := strconv.ParseBool(s.val)
+	if err != nil {
+		s.err = err
+	}
+	return val
+}
+
+// Int converts s.val from string to a int representation
+// Check s.Err() for parsing errors
+func (s *Str) Int() int {
+	val, err := strconv.Atoi(s.val)
+	if err != nil {
+		s.err = err
+	}
+	return val
+}
+
+// Float64 converts s.val from string to a float64 representation
+// Check s.Error() for parsing errors
+func (s *Str) Float64() float64 {
+	val, err := strconv.ParseFloat(s.val, 64)
+	if err != nil {
+		s.err = err
+	}
+	return val
+}
+
+// Reader returns an io.Reader to access the content.
+func (s *Str) Reader() io.Reader {
+	return bytes.NewReader([]byte(s.val))
+}
+
+// ToLower returns val as lower case
+func (s *Str) ToLower() *Str {
+	s.val = strings.ToLower(s.val)
+	return s
+}
+
+// ToUpper returns val as upper case
+func (s *Str) ToUpper() *Str {
+	s.val = strings.ToUpper(s.val)
+	return s
+}
+
+// ToTitle returns strings.ToTitle for s.val
+func (s *Str) ToTitle() *Str {
+	s.val = strings.ToTitle(s.val)
+	return s
+}
+
+// TrimSpaces removes spaces around a val
+func (s *Str) TrimSpaces() *Str {
+	s.val = strings.TrimSpace(s.val)
+	return s
+}
+
+// TrimLeft removes each character in cutset at the
+// start of s.val
+func (s *Str) TrimLeft(cutset string) *Str {
+	s.val = strings.TrimLeft(s.val, cutset)
+	return s
+}
+
+// TrimRight removes each character in cutset removed at the
+// start of s.val
+func (s *Str) TrimRight(cutset string) *Str {
+	s.val = strings.TrimRight(s.val, cutset)
+	return s
+}
+
+// Trim removes each character in cutset from around s.val
+func (s *Str) Trim(cutset string) *Str {
+	s.val = strings.Trim(s.val, cutset)
+	return s
+}
+
+// ReplaceAll replaces all occurrences of old with new in s.val
+func (s *Str) ReplaceAll(old, new string) *Str {
+	s.val = strings.ReplaceAll(s.val, old, new)
+	return s
+}
+
+// Concat concatenates val1 to s.val
+func (s *Str) Concat(vals ...string) *Str {
+	evals := []string{s.val}
+	for _, val := range vals {
+		evals = append(evals, s.vars.Eval(val))
+	}
+	s.val = strings.Join(evals, "")
+	return s
+}
+
+// CopyTo copies s.val unto dest
+// Check s.Error() for copy error.
+func (s *Str) CopyTo(dest io.Writer) *Str {
+	if _, err := io.Copy(dest, bytes.NewBufferString(s.val)); err != nil {
+		s.err = err
+	}
+	return s
+}
diff --git a/vendor/github.com/vladimirvivien/gexe/variables.go b/vendor/github.com/vladimirvivien/gexe/variables.go
index 54230ee0ad729684d8f26048ca79d07d2cd9143e..657d6d58ed6c76edccbe067cbd4f8c17bd5f69ec 100644
--- a/vendor/github.com/vladimirvivien/gexe/variables.go
+++ b/vendor/github.com/vladimirvivien/gexe/variables.go
@@ -52,6 +52,13 @@ func (e *Echo) SetVar(name, value string) *Echo {
 	return e
 }
 
+// UnsetVar removes a session variable.
+func (e *Echo) UnsetVar(name string) *Echo {
+	vars := e.vars.UnsetVar(name)
+	e.err = vars.Err()
+	return e
+}
+
 // Val retrieves a session or environment variable
 func (e *Echo) Val(name string) string {
 	return e.vars.Val(name)
diff --git a/vendor/github.com/vladimirvivien/gexe/vars/variables.go b/vendor/github.com/vladimirvivien/gexe/vars/variables.go
index ae6cdaf659dc9f53578b1e75ce2f4a56c8f97dfb..4d32de8c0b6e8ae227594696b22156e39dd1968c 100644
--- a/vendor/github.com/vladimirvivien/gexe/vars/variables.go
+++ b/vendor/github.com/vladimirvivien/gexe/vars/variables.go
@@ -95,6 +95,14 @@ func (v *Variables) SetVar(name, value string) *Variables {
 	return v
 }
 
+// UnsetVar removes a previously set local variable.
+func (v *Variables) UnsetVar(name string) *Variables {
+	v.Lock()
+	defer v.Unlock()
+	delete(v.vars, name)
+	return v
+}
+
 // Val searches for a Var with provided key, if not found
 // searches for environment var, for running process, with same key
 func (v *Variables) Val(name string) string {
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 746e2044824a60fbef91ba934685f252a8ae3532..66cb1e556d0a538c549cbc8aec2b06818f80bbad 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -456,13 +456,15 @@ github.com/vishvananda/netlink/nl
 # github.com/vishvananda/netns v0.0.4
 ## explicit; go 1.17
 github.com/vishvananda/netns
-# github.com/vladimirvivien/gexe v0.2.0
-## explicit; go 1.16
+# github.com/vladimirvivien/gexe v0.3.0
+## explicit; go 1.22
 github.com/vladimirvivien/gexe
 github.com/vladimirvivien/gexe/exec
 github.com/vladimirvivien/gexe/fs
 github.com/vladimirvivien/gexe/http
+github.com/vladimirvivien/gexe/net
 github.com/vladimirvivien/gexe/prog
+github.com/vladimirvivien/gexe/str
 github.com/vladimirvivien/gexe/vars
 # github.com/vmware/go-ipfix v0.9.0
 ## explicit; go 1.21