diff --git a/README.md b/README.md index d59ca36..80e4116 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ The goal of project `gexe` is to make it simple to write code for system operati ## What can you do with `gexe`? * Parse and execute OS plain text commands, as you would in a shell. * Support for variable expansion in command string (i.e. `gexe.Run("echo $HOME")`) +* Support for Go's `fmt.Sprintf` formatting in all string parameters (i.e. `gexe.Run("echo %s", "Hello")`) * Ability to pipe processes: `gexe.Pipe("cat /etc/hosts", "wc -l")` * Run processes concurrently: `gexe.RunConcur('wget https://example.com/files'; "date")` * Get process information (i.e. PID, status, exit code, etc) @@ -41,6 +42,23 @@ if proc.Err() != nil { fmt.Println(proc.Result()) ``` +### String formatting with Go's fmt syntax +`gexe` methods now support Go's `fmt.Sprintf` formatting alongside variable expansion: + +```go +// Using Go formatting with variable expansion +gexe.SetVar("name", "Alice") +gexe.Run("echo Hello %s, your home is ${HOME}", "World") + +// File operations with formatting +gexe.FileWrite("/tmp/log_%s.txt", time.Now().Format("2006-01-02")) + +// Variable setting with formatting +gexe.SetVar("message", "User %s logged in at %s", username, timestamp) +``` + +The formatting is applied intelligently - if no format verbs are detected in the string, the arguments are ignored, maintaining backward compatibility. + ## Examples Find more examples [here](./examples/)! diff --git a/echo.go b/echo.go index 12f3715..ff107b5 100644 --- a/echo.go +++ b/echo.go @@ -44,7 +44,8 @@ func (e *Session) AddExecPath(execPath string) { } // ProgAvail returns the full path of the program if found on exec PATH -func (e *Session) ProgAvail(progName string) string { +func (e *Session) ProgAvail(progName string, args ...interface{}) string { + progName = applySprintfIfNeeded(progName, args...) path, err := exec.LookPath(e.Eval(progName)) if err != nil { return "" diff --git a/examples/sprintf_demo/README.md b/examples/sprintf_demo/README.md new file mode 100644 index 0000000..d99fb21 --- /dev/null +++ b/examples/sprintf_demo/README.md @@ -0,0 +1,25 @@ +# Sprintf Demo + +This example demonstrates the new `fmt.Sprintf` functionality in gexe, which allows you to use Go's string formatting alongside variable expansion. + +## Features Demonstrated + +1. **Basic string formatting**: Using `%s`, `%d`, `%t` format verbs +2. **Multiple arguments**: Passing multiple values for formatting +3. **Combined functionality**: Using both sprintf and variable expansion together +4. **File operations**: Creating files with formatted paths +5. **Variable setting**: Setting variables with formatted values +6. **Backward compatibility**: Showing that unused args are ignored when no format verbs exist + +## Run the Example + +```bash +go run main.go +``` + +## Key Benefits + +- **Cleaner code**: No need for manual string concatenation or `fmt.Sprintf` calls +- **Type safety**: Go's type checking for format arguments +- **Flexibility**: Combine with existing variable expansion features +- **Backward compatible**: Existing code continues to work unchanged \ No newline at end of file diff --git a/examples/sprintf_demo/main.go b/examples/sprintf_demo/main.go new file mode 100644 index 0000000..a0f3f62 --- /dev/null +++ b/examples/sprintf_demo/main.go @@ -0,0 +1,74 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "time" + + "github.com/vladimirvivien/gexe" +) + +func main() { + fmt.Println("=== Gexe Sprintf Functionality Demo ===") + + // 1. Basic string formatting + fmt.Println("\n1. Basic string formatting:") + result := gexe.Run("echo Hello %s!", "World") + fmt.Printf(" Result: %s\n", result) + + // 2. Multiple arguments + fmt.Println("\n2. Multiple arguments:") + result = gexe.Run("echo User: %s, ID: %d, Active: %t", "Alice", 42, true) + fmt.Printf(" Result: %s\n", result) + + // 3. Combined with variable expansion + fmt.Println("\n3. Combined with variable expansion:") + gexe.SetVar("greeting", "Welcome") + result = gexe.Run("echo ${greeting} %s to our platform!", "Bob") + fmt.Printf(" Result: %s\n", result) + + // 4. File operations with formatting + fmt.Println("\n4. File operations with formatting:") + tempDir := os.TempDir() + filename := fmt.Sprintf("demo_%s.txt", time.Now().Format("20060102_150405")) + fullPath := filepath.Join(tempDir, "%s") + gexe.FileWrite(fullPath, filename).String("This is a demo file created with sprintf formatting") + + if gexe.PathExists(fullPath, filename) { + content := gexe.FileRead(fullPath, filename).String() + fmt.Printf(" Created file: %s\n", filepath.Join(tempDir, filename)) + fmt.Printf(" Content: %s\n", content) + } + + // 5. Variable setting with formatting + fmt.Println("\n5. Variable setting with formatting:") + gexe.SetVar("status", "User %s has %d points", "Charlie", 150) + fmt.Printf(" Variable value: %s\n", gexe.Val("status")) + + // 6. Backward compatibility (no formatting verbs) + fmt.Println("\n6. Backward compatibility:") + result = gexe.Run("echo Hello World", "unused_arg", "another_unused") + fmt.Printf(" Result: %s (args ignored when no format verbs)\n", result) + + // 7. Complex example: log file creation + fmt.Println("\n7. Complex example - log file creation:") + user := "admin" + action := "login" + timestamp := time.Now().Format("2006-01-02 15:04:05") + + logDir := filepath.Join(tempDir, "logs") + gexe.SetVar("log_dir", logDir) + gexe.MkDir("${log_dir}", 0755) + + logEntry := fmt.Sprintf("[%s] User %s performed %s", timestamp, user, action) + gexe.FileWrite("${log_dir}/app_%s.log", time.Now().Format("20060102")).String(logEntry) + + fmt.Printf(" Log entry created: %s\n", logEntry) + + // Cleanup + gexe.Run("rm -f %s/demo_*.txt", tempDir) + gexe.Run("rm -rf %s", logDir) + + fmt.Println("\n=== Demo Complete ===") +} diff --git a/filesys.go b/filesys.go index 28a7201..4773750 100644 --- a/filesys.go +++ b/filesys.go @@ -9,56 +9,66 @@ import ( // PathExists returns true if path exists. // All errors causes to return false. -func (e *Session) PathExists(path string) bool { +func (e *Session) PathExists(path string, args ...interface{}) bool { + path = applySprintfIfNeeded(path, args...) 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 *Session) MkDir(path string, mode os.FileMode) *fs.FSInfo { +func (e *Session) MkDir(path string, mode os.FileMode, args ...interface{}) *fs.FSInfo { + path = applySprintfIfNeeded(path, args...) p := fs.PathWithVars(path, e.vars) return p.MkDir(mode) } // RmPath removes specified path (dir or file). // Error is returned FSInfo.Err() -func (e *Session) RmPath(path string) *fs.FSInfo { +func (e *Session) RmPath(path string, args ...interface{}) *fs.FSInfo { + path = applySprintfIfNeeded(path, args...) p := fs.PathWithVars(path, e.vars) return p.Remove() } // PathInfo -func (e *Session) PathInfo(path string) *fs.FSInfo { +func (e *Session) PathInfo(path string, args ...interface{}) *fs.FSInfo { + path = applySprintfIfNeeded(path, args...) return fs.PathWithVars(path, e.vars).Info() } // FileReadWithContext uses specified context to provide methods to read file // content at path. -func (e *Session) FileReadWithContext(ctx context.Context, path string) *fs.FileReader { +func (e *Session) FileReadWithContext(ctx context.Context, path string, args ...interface{}) *fs.FileReader { + path = applySprintfIfNeeded(path, args...) return fs.ReadWithContextVars(ctx, path, e.vars) } // FileRead provides methods to read file content -func (e *Session) FileRead(path string) *fs.FileReader { +func (e *Session) FileRead(path string, args ...interface{}) *fs.FileReader { + path = applySprintfIfNeeded(path, args...) 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 *Session) FileWriteWithContext(ctx context.Context, path string) *fs.FileWriter { +func (e *Session) FileWriteWithContext(ctx context.Context, path string, args ...interface{}) *fs.FileWriter { + path = applySprintfIfNeeded(path, args...) return fs.WriteWithContextVars(ctx, path, e.vars) } // FileWrite creates a fs.FileWriter to write content to provided path -func (e *Session) FileWrite(path string) *fs.FileWriter { +func (e *Session) FileWrite(path string, args ...interface{}) *fs.FileWriter { + path = applySprintfIfNeeded(path, args...) return fs.WriteWithContextVars(context.Background(), path, e.vars) } // FileAppend creates a new fs.FileWriter to append content to provided path -func (e *Session) FileAppendWithContext(ctx context.Context, path string) *fs.FileWriter { +func (e *Session) FileAppendWithContext(ctx context.Context, path string, args ...interface{}) *fs.FileWriter { + path = applySprintfIfNeeded(path, args...) return fs.AppendWithContextVars(ctx, path, e.vars) } // FileAppend creates a new fs.FileWriter to append content to provided path -func (e *Session) FileAppend(path string) *fs.FileWriter { +func (e *Session) FileAppend(path string, args ...interface{}) *fs.FileWriter { + path = applySprintfIfNeeded(path, args...) return fs.AppendWithContextVars(context.Background(), path, e.vars) } diff --git a/functions.go b/functions.go index 1e13416..9c77ae6 100644 --- a/functions.go +++ b/functions.go @@ -29,8 +29,8 @@ func Envs(val ...string) *Session { } // SetEnv sets a process environment variable. -func SetEnv(name, value string) *Session { - return DefaultSession.SetEnv(name, value) +func SetEnv(name, value string, args ...interface{}) *Session { + return DefaultSession.SetEnv(name, value, args...) } // Vars declares multiple session-scope variables using @@ -45,8 +45,8 @@ func Vars(variables ...string) *Session { } // SetVar declares a session variable. -func SetVar(name, value string) *Session { - return DefaultSession.SetVar(name, value) +func SetVar(name, value string, args ...interface{}) *Session { + return DefaultSession.SetVar(name, value, args...) } // Val retrieves a session or environment variable @@ -57,60 +57,60 @@ func Val(name string) string { // Eval returns the string str with its content expanded // with variable values i.e. Eval("I am $HOME") returns // "I am " -func Eval(str string) string { - return DefaultSession.Eval(str) +func Eval(str string, args ...interface{}) string { + return DefaultSession.Eval(str, args...) } // 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 DefaultSession.NewProcWithContext(ctx, cmdStr) +func NewProcWithContext(ctx context.Context, cmdStr string, args ...interface{}) *exec.Proc { + return DefaultSession.NewProcWithContext(ctx, cmdStr, args...) } // 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 DefaultSession.NewProcWithContext(context.Background(), cmdStr) +func NewProc(cmdStr string, args ...interface{}) *exec.Proc { + return DefaultSession.NewProcWithContext(context.Background(), cmdStr, args...) } // 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 DefaultSession.StartProcWithContext(ctx, cmdStr) +func StartProcWithContext(ctx context.Context, cmdStr string, args ...interface{}) *exec.Proc { + return DefaultSession.StartProcWithContext(ctx, cmdStr, args...) } // StartProc executes the command in cmdStr and returns immediately // without waiting. Information about the running process is stored in *exec.Proc. -func StartProc(cmdStr string) *exec.Proc { - return DefaultSession.StartProc(cmdStr) +func StartProc(cmdStr string, args ...interface{}) *exec.Proc { + return DefaultSession.StartProc(cmdStr, args...) } // 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 DefaultSession.RunProc(cmdStr) +func RunProcWithContext(ctx context.Context, cmdStr string, args ...interface{}) *exec.Proc { + return DefaultSession.RunProcWithContext(ctx, cmdStr, args...) } // 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 DefaultSession.RunProc(cmdStr) +func RunProc(cmdStr string, args ...interface{}) *exec.Proc { + return DefaultSession.RunProc(cmdStr, args...) } // 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 DefaultSession.RunWithContext(ctx, cmdStr) +func RunWithContext(ctx context.Context, cmdStr string, args ...interface{}) string { + return DefaultSession.RunWithContext(ctx, cmdStr, args...) } // Run executes cmdStr, waits, and returns the result as a string. -func Run(cmdStr string) string { - return DefaultSession.Run(cmdStr) +func Run(cmdStr string, args ...interface{}) string { + return DefaultSession.Run(cmdStr, args...) } // Runout executes command cmdStr and prints out the result -func Runout(cmdStr string) { - DefaultSession.Runout(cmdStr) +func Runout(cmdStr string, args ...interface{}) { + DefaultSession.Runout(cmdStr, args...) } // Commands returns a *exe.CommandBuilder to build a multi-command execution flow. @@ -150,48 +150,48 @@ func Pipe(cmdStrs ...string) *exec.PipedCommandResult { // PathExists returns true if specified path exists. // Any error will cause it to return false. -func PathExists(path string) bool { - return DefaultSession.PathExists(path) +func PathExists(path string, args ...interface{}) bool { + return DefaultSession.PathExists(path, args...) } // PathInfo returns information for specified path (i.e. size, etc) -func PathInfo(path string) *fs.FSInfo { - return DefaultSession.PathInfo(path) +func PathInfo(path string, args ...interface{}) *fs.FSInfo { + return DefaultSession.PathInfo(path, args...) } // MkDirs creates one or more directories along the specified path -func MkDirs(path string, mode os.FileMode) *fs.FSInfo { - return DefaultSession.MkDir(path, mode) +func MkDirs(path string, mode os.FileMode, args ...interface{}) *fs.FSInfo { + return DefaultSession.MkDir(path, mode, args...) } // MkDir creates a directory with default mode 0744 -func MkDir(path string) *fs.FSInfo { - return DefaultSession.MkDir(path, 0744) +func MkDir(path string, args ...interface{}) *fs.FSInfo { + return DefaultSession.MkDir(path, 0744, args...) } // RmPath removes files or directories along specified path -func RmPath(path string) *fs.FSInfo { - return DefaultSession.RmPath(path) +func RmPath(path string, args ...interface{}) *fs.FSInfo { + return DefaultSession.RmPath(path, args...) } // FileRead uses context ctx to read file content from path -func FileReadWithContext(ctx context.Context, path string) *fs.FileReader { - return DefaultSession.FileReadWithContext(ctx, path) +func FileReadWithContext(ctx context.Context, path string, args ...interface{}) *fs.FileReader { + return DefaultSession.FileReadWithContext(ctx, path, args...) } // FileRead provides methods to read file content from path -func FileRead(path string) *fs.FileReader { - return DefaultSession.FileReadWithContext(context.Background(), path) +func FileRead(path string, args ...interface{}) *fs.FileReader { + return DefaultSession.FileReadWithContext(context.Background(), path, args...) } // FileWriteWithContext uses context ctx to write file content to path -func FileWriteWithContext(ctx context.Context, path string) *fs.FileWriter { - return DefaultSession.FileWriteWithContext(ctx, path) +func FileWriteWithContext(ctx context.Context, path string, args ...interface{}) *fs.FileWriter { + return DefaultSession.FileWriteWithContext(ctx, path, args...) } // FileWrite provides methods to write file content to path -func FileWrite(path string) *fs.FileWriter { - return DefaultSession.FileWriteWithContext(context.Background(), path) +func FileWrite(path string, args ...interface{}) *fs.FileWriter { + return DefaultSession.FileWriteWithContext(context.Background(), path, args...) } // HttpGetWithContext uses context ctx to start an HTTP GET operation to retrieve resource at URL/path @@ -230,8 +230,8 @@ func Prog() *prog.Info { } // ProgAvail returns the full path of the program if available. -func ProgAvail(program string) string { - return DefaultSession.ProgAvail(program) +func ProgAvail(program string, args ...interface{}) string { + return DefaultSession.ProgAvail(program, args...) } // Workdir returns the current program's working directory @@ -244,6 +244,6 @@ func AddExecPath(execPath string) { DefaultSession.AddExecPath(execPath) } -func String(s string) *str.Str { - return DefaultSession.String(s) +func String(s string, args ...interface{}) *str.Str { + return DefaultSession.String(s, args...) } diff --git a/procs.go b/procs.go index 3937182..f723aff 100644 --- a/procs.go +++ b/procs.go @@ -10,54 +10,62 @@ 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 *Session) NewProcWithContext(ctx context.Context, cmdStr string) *exec.Proc { +func (e *Session) NewProcWithContext(ctx context.Context, cmdStr string, args ...interface{}) *exec.Proc { + cmdStr = applySprintfIfNeeded(cmdStr, args...) return exec.NewProcWithContextVars(ctx, cmdStr, e.vars) } // NewProc a convenient function that calls NewProcWithContext with a default contet. -func (e *Session) NewProc(cmdStr string) *exec.Proc { +func (e *Session) NewProc(cmdStr string, args ...interface{}) *exec.Proc { + cmdStr = applySprintfIfNeeded(cmdStr, args...) 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 *Session) StartProcWithContext(ctx context.Context, cmdStr string) *exec.Proc { +func (e *Session) StartProcWithContext(ctx context.Context, cmdStr string, args ...interface{}) *exec.Proc { + cmdStr = applySprintfIfNeeded(cmdStr, args...) 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 *Session) StartProc(cmdStr string) *exec.Proc { +func (e *Session) StartProc(cmdStr string, args ...interface{}) *exec.Proc { + cmdStr = applySprintfIfNeeded(cmdStr, args...) 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 *Session) RunProcWithContext(ctx context.Context, cmdStr string) *exec.Proc { +func (e *Session) RunProcWithContext(ctx context.Context, cmdStr string, args ...interface{}) *exec.Proc { + cmdStr = applySprintfIfNeeded(cmdStr, args...) 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 *Session) RunProc(cmdStr string) *exec.Proc { +func (e *Session) RunProc(cmdStr string, args ...interface{}) *exec.Proc { + cmdStr = applySprintfIfNeeded(cmdStr, args...) return exec.RunProcWithContextVars(context.Background(), cmdStr, e.vars) } // Run executes cmdStr, with given context, and returns the result as a string. -func (e *Session) RunWithContext(ctx context.Context, cmdStr string) string { +func (e *Session) RunWithContext(ctx context.Context, cmdStr string, args ...interface{}) string { + cmdStr = applySprintfIfNeeded(cmdStr, args...) return exec.RunWithContextVars(ctx, cmdStr, e.vars) } // Run executes cmdStr, waits, and returns the result as a string. -func (e *Session) Run(cmdStr string) string { +func (e *Session) Run(cmdStr string, args ...interface{}) string { + cmdStr = applySprintfIfNeeded(cmdStr, args...) return exec.RunWithContextVars(context.Background(), cmdStr, e.vars) } // Runout executes command cmdStr and prints out the result -func (e *Session) Runout(cmdStr string) { - fmt.Print(e.Run(cmdStr)) +func (e *Session) Runout(cmdStr string, args ...interface{}) { + fmt.Print(e.Run(cmdStr, args...)) } // Commands creates a *exe.CommandBuilder, with the specified context, to build a multi-command execution flow. @@ -129,12 +137,13 @@ func (e *Session) Pipe(cmdStrs ...string) *exec.PipedCommandResult { } // ParseCommand parses the string into individual command tokens -func (e *Session) ParseCommand(cmdStr string) (cmdName string, args []string) { +func (e *Session) ParseCommand(cmdStr string, args ...interface{}) (cmdName string, argsList []string) { + cmdStr = applySprintfIfNeeded(cmdStr, args...) result, err := exec.Parse(e.vars.Eval(cmdStr)) if err != nil { e.err = err } cmdName = result[0] - args = result[1:] + argsList = result[1:] return } diff --git a/sprintf_helpers.go b/sprintf_helpers.go new file mode 100644 index 0000000..380597c --- /dev/null +++ b/sprintf_helpers.go @@ -0,0 +1,21 @@ +package gexe + +import "fmt" + +// hasFormatVerbs checks if a string contains format verbs like %s, %d, etc. +func hasFormatVerbs(s string) bool { + for i := 0; i < len(s)-1; i++ { + if s[i] == '%' && s[i+1] != '%' { // %% is escaped % + return true + } + } + return false +} + +// applySprintfIfNeeded applies fmt.Sprintf only if there are format verbs and args +func applySprintfIfNeeded(format string, args ...interface{}) string { + if len(args) > 0 && hasFormatVerbs(format) { + return fmt.Sprintf(format, args...) + } + return format +} diff --git a/sprintf_test.go b/sprintf_test.go new file mode 100644 index 0000000..bcd5140 --- /dev/null +++ b/sprintf_test.go @@ -0,0 +1,349 @@ +package gexe + +import ( + "os" + "path/filepath" + "testing" +) + +func TestSprintfFunctionality(t *testing.T) { + tests := []struct { + name string + testFn func(t *testing.T) + }{ + {"Run with sprintf", testRunSprintf}, + {"SetVar with sprintf", testSetVarSprintf}, + {"SetEnv with sprintf", testSetEnvSprintf}, + {"Eval with sprintf", testEvalSprintf}, + {"FileRead with sprintf", testFileReadSprintf}, + {"FileWrite with sprintf", testFileWriteSprintf}, + {"PathExists with sprintf", testPathExistsSprintf}, + {"MkDir with sprintf", testMkDirSprintf}, + {"String with sprintf", testStringSprintf}, + {"ProgAvail with sprintf", testProgAvailSprintf}, + {"Combined sprintf and variable expansion", testCombinedSprintfAndVarExpansion}, + {"No args backward compatibility", testNoArgsBackwardCompatibility}, + {"Multiple args", testMultipleArgs}, + {"Edge cases", testEdgeCases}, + } + + for _, tt := range tests { + t.Run(tt.name, tt.testFn) + } +} + +func testRunSprintf(t *testing.T) { + g := New() + + // Test with string formatting + result := g.Run("echo %s", "Hello World") + expected := "Hello World" + if result != expected { + t.Errorf("Run with sprintf failed: expected %q, got %q", expected, result) + } + + // Test with multiple args + result = g.Run("echo %s %d", "Number:", 42) + expected = "Number: 42" + if result != expected { + t.Errorf("Run with multiple sprintf args failed: expected %q, got %q", expected, result) + } + + // Test package-level function + result = Run("echo %s", "Package Level") + expected = "Package Level" + if result != expected { + t.Errorf("Package-level Run with sprintf failed: expected %q, got %q", expected, result) + } +} + +func testSetVarSprintf(t *testing.T) { + g := New() + + // Test SetVar with sprintf + g.SetVar("user", "User: %s", "Alice") + value := g.Val("user") + expected := "User: Alice" + if value != expected { + t.Errorf("SetVar with sprintf failed: expected %q, got %q", expected, value) + } + + // Test package-level function + SetVar("count", "Count: %d", 100) + value = Val("count") + expected = "Count: 100" + if value != expected { + t.Errorf("Package-level SetVar with sprintf failed: expected %q, got %q", expected, value) + } +} + +func testSetEnvSprintf(t *testing.T) { + g := New() + + // Test SetEnv with sprintf + g.SetEnv("TEST_VAR", "Value: %s", "formatted") + value := os.Getenv("TEST_VAR") + expected := "Value: formatted" + if value != expected { + t.Errorf("SetEnv with sprintf failed: expected %q, got %q", expected, value) + } + + // Cleanup + os.Unsetenv("TEST_VAR") +} + +func testEvalSprintf(t *testing.T) { + g := New() + g.SetVar("name", "World") + + // Test Eval with sprintf + result := g.Eval("Hello %s: ${name}", "Formatted") + expected := "Hello Formatted: World" + if result != expected { + t.Errorf("Eval with sprintf failed: expected %q, got %q", expected, result) + } + + // Test package-level function (set variable in default session) + SetVar("name", "World") + result = Eval("Hello %s: ${name}", "Package") + expected = "Hello Package: World" + if result != expected { + t.Errorf("Package-level Eval with sprintf failed: expected %q, got %q", expected, result) + } +} + +func testFileReadSprintf(t *testing.T) { + g := New() + + // Create test file + filename := "test_file.txt" + g.FileWrite(filename).String("test content") + defer os.Remove(filename) + + // Test FileRead with sprintf + content := g.FileRead("%s", filename).String() + expected := "test content" + if content != expected { + t.Errorf("FileRead with sprintf failed: expected %q, got %q", expected, content) + } + + // Test package-level function + content = FileRead("%s", filename).String() + if content != expected { + t.Errorf("Package-level FileRead with sprintf failed: expected %q, got %q", expected, content) + } +} + +func testFileWriteSprintf(t *testing.T) { + g := New() + + // Test FileWrite with sprintf + filename := "test_%s.txt" + formattedName := "output" + g.FileWrite(filename, formattedName).String("test content") + + expectedFilename := "test_output.txt" + defer os.Remove(expectedFilename) + + // Verify file was created with correct name + if !g.PathExists(expectedFilename) { + t.Errorf("FileWrite with sprintf failed: file %q was not created", expectedFilename) + } + + // Test package-level function + filename2 := "test_%s_%d.txt" + FileWrite(filename2, "package", 123).String("test content") + expectedFilename2 := "test_package_123.txt" + defer os.Remove(expectedFilename2) + + if !PathExists(expectedFilename2) { + t.Errorf("Package-level FileWrite with sprintf failed: file %q was not created", expectedFilename2) + } +} + +func testPathExistsSprintf(t *testing.T) { + g := New() + + // Use cross-platform temp directory + tempDir := os.TempDir() + + // Test PathExists with sprintf + exists := g.PathExists(tempDir) + if !exists { + t.Errorf("PathExists failed on temp dir %q", tempDir) + } + + // Test with formatting - use current directory which should always exist + currentDir, err := os.Getwd() + if err != nil { + t.Fatalf("Failed to get current directory: %v", err) + } + dirName := filepath.Base(currentDir) + parentDir := filepath.Dir(currentDir) + + exists = g.PathExists(filepath.Join(parentDir, "%s"), dirName) + if !exists { + t.Errorf("PathExists with sprintf failed for path %q", filepath.Join(parentDir, dirName)) + } + + // Test non-existent path with sprintf + exists = g.PathExists(filepath.Join(tempDir, "nonexistent_%s"), "path") + if exists { + t.Error("PathExists with sprintf should return false for non-existent path") + } +} + +func testMkDirSprintf(t *testing.T) { + g := New() + + // Test MkDir with sprintf using cross-platform temp directory + tempDir := os.TempDir() + dirName := filepath.Join(tempDir, "test_%s") + formattedDir := "sprintf" + info := g.MkDir(dirName, 0755, formattedDir) + expectedDir := filepath.Join(tempDir, "test_sprintf") + + if info.Err() != nil { + t.Errorf("MkDir with sprintf failed: %v", info.Err()) + } + + defer os.RemoveAll(expectedDir) + + if !g.PathExists(expectedDir) { + t.Errorf("MkDir with sprintf failed: directory %q was not created", expectedDir) + } +} + +func testStringSprintf(t *testing.T) { + g := New() + + // Test String with sprintf + str := g.String("Hello %s", "World").String() + expected := "Hello World" + if str != expected { + t.Errorf("String with sprintf failed: expected %q, got %q", expected, str) + } + + // Test package-level function + str = String("Count: %d", 42).String() + expected = "Count: 42" + if str != expected { + t.Errorf("Package-level String with sprintf failed: expected %q, got %q", expected, str) + } +} + +func testProgAvailSprintf(t *testing.T) { + g := New() + + // Use a command that should be available on all platforms + // Use go command which should be available since we're running go test + path := g.ProgAvail("%s", "go") + if path == "" { + t.Error("ProgAvail with sprintf failed: go command should be available") + } + + // Test package-level function + path = ProgAvail("%s", "go") + if path == "" { + t.Error("Package-level ProgAvail with sprintf failed: go command should be available") + } +} + +func testCombinedSprintfAndVarExpansion(t *testing.T) { + g := New() + g.SetVar("user", "Alice") + + // Test combination of sprintf and variable expansion + result := g.Run("echo Hello %s, your home is ${HOME}", "formatted") + // Should contain both "Hello formatted" and the HOME value + if !contains(result, "Hello formatted") { + t.Errorf("Combined sprintf and var expansion failed: missing sprintf part in %q", result) + } + + // Test with Eval + result = g.Eval("User: %s, Name: ${user}", "Admin") + expected := "User: Admin, Name: Alice" + if result != expected { + t.Errorf("Combined sprintf and var expansion in Eval failed: expected %q, got %q", expected, result) + } +} + +func testNoArgsBackwardCompatibility(t *testing.T) { + g := New() + + // Test that calling without args works as before + result := g.Run("echo Hello") + expected := "Hello" + if result != expected { + t.Errorf("Backward compatibility failed: expected %q, got %q", expected, result) + } + + // Test with variable expansion + g.SetVar("msg", "World") + result = g.Run("echo Hello ${msg}") + expected = "Hello World" + if result != expected { + t.Errorf("Backward compatibility with var expansion failed: expected %q, got %q", expected, result) + } +} + +func testMultipleArgs(t *testing.T) { + g := New() + + // Test with multiple formatting arguments + result := g.Run("echo %s %d %f %t", "String", 42, 3.14, true) + expected := "String 42 3.140000 true" + if result != expected { + t.Errorf("Multiple args sprintf failed: expected %q, got %q", expected, result) + } + + // Test SetVar with multiple args + g.SetVar("complex", "%s-%d-%s", "prefix", 123, "suffix") + value := g.Val("complex") + expected = "prefix-123-suffix" + if value != expected { + t.Errorf("SetVar with multiple args failed: expected %q, got %q", expected, value) + } +} + +func testEdgeCases(t *testing.T) { + g := New() + + // Test with no formatting verbs but args provided - should ignore args + result := g.Run("echo Hello", "unused") + expected := "Hello" + if result != expected { + t.Errorf("Edge case (no verbs with args) failed: expected %q, got %q", expected, result) + } + + // Test with empty string + result = g.Eval("%s", "") + expected = "" + if result != expected { + t.Errorf("Edge case (empty string) failed: expected %q, got %q", expected, result) + } + + // Test with nil args (empty slice) + result = g.Run("echo Hello") + expected = "Hello" + if result != expected { + t.Errorf("Edge case (no args) failed: expected %q, got %q", expected, result) + } +} + +// Helper function +func contains(s, substr string) bool { + return len(s) >= len(substr) && (s == substr || len(substr) == 0 || + (len(s) > len(substr) && (s[:len(substr)] == substr || + s[len(s)-len(substr):] == substr || + indexOf(s, substr) >= 0))) +} + +func indexOf(s, substr string) int { + for i := 0; i <= len(s)-len(substr); i++ { + if s[i:i+len(substr)] == substr { + return i + } + } + return -1 +} diff --git a/sprintf_unix_test.go b/sprintf_unix_test.go new file mode 100644 index 0000000..d9aca79 --- /dev/null +++ b/sprintf_unix_test.go @@ -0,0 +1,72 @@ +//go:build !windows + +package gexe + +import ( + "testing" +) + +func TestSprintfFunctionalityUnix(t *testing.T) { + tests := []struct { + name string + testFn func(t *testing.T) + }{ + {"Unix ProgAvail with sprintf", testProgAvailSprintfUnix}, + {"Unix Run with sprintf", testRunSprintfUnix}, + {"Unix PathExists with /tmp", testPathExistsSprintfUnix}, + } + + for _, tt := range tests { + t.Run(tt.name, tt.testFn) + } +} + +func testProgAvailSprintfUnix(t *testing.T) { + g := New() + + // Test ProgAvail with sprintf - using Unix-specific commands + path := g.ProgAvail("%s", "ls") + if path == "" { + t.Error("ProgAvail with sprintf failed: ls command should be available on Unix") + } + + // Test package-level function + path = ProgAvail("%s", "echo") + if path == "" { + t.Error("Package-level ProgAvail with sprintf failed: echo command should be available on Unix") + } +} + +func testRunSprintfUnix(t *testing.T) { + g := New() + + // Test with Unix echo command + result := g.Run("echo %s", "Hello World") + expected := "Hello World" + if result != expected { + t.Errorf("Unix Run with sprintf failed: expected %q, got %q", expected, result) + } + + // Test with ls command + result = g.Run("echo %s", "Unix Test") + expected = "Unix Test" + if result != expected { + t.Errorf("Unix echo with sprintf failed: expected %q, got %q", expected, result) + } +} + +func testPathExistsSprintfUnix(t *testing.T) { + g := New() + + // Test PathExists with /tmp on Unix systems + exists := g.PathExists("/tmp") + if !exists { + t.Error("PathExists failed on /tmp") + } + + // Test with formatting + exists = g.PathExists("/%s", "tmp") + if !exists { + t.Error("PathExists with sprintf failed") + } +} diff --git a/sprintf_windows_test.go b/sprintf_windows_test.go new file mode 100644 index 0000000..ed1bc7b --- /dev/null +++ b/sprintf_windows_test.go @@ -0,0 +1,55 @@ +//go:build windows + +package gexe + +import ( + "testing" +) + +func TestSprintfFunctionalityWindows(t *testing.T) { + tests := []struct { + name string + testFn func(t *testing.T) + }{ + {"Windows ProgAvail with sprintf", testProgAvailSprintfWindows}, + {"Windows Run with sprintf", testRunSprintfWindows}, + } + + for _, tt := range tests { + t.Run(tt.name, tt.testFn) + } +} + +func testProgAvailSprintfWindows(t *testing.T) { + g := New() + + // Test ProgAvail with sprintf - using Windows-specific commands + path := g.ProgAvail("%s", "cmd") + if path == "" { + t.Error("ProgAvail with sprintf failed: cmd command should be available on Windows") + } + + // Test package-level function + path = ProgAvail("%s", "powershell") + if path == "" { + t.Skip("PowerShell not available, skipping test") + } +} + +func testRunSprintfWindows(t *testing.T) { + g := New() + + // Test with Windows echo command + result := g.Run("echo %s", "Hello World") + expected := "Hello World" + if result != expected { + t.Errorf("Windows Run with sprintf failed: expected %q, got %q", expected, result) + } + + // Test with dir command (Windows equivalent of ls) + result = g.Run("cmd /c echo %s", "Windows Test") + expected = "Windows Test" + if result != expected { + t.Errorf("Windows cmd with sprintf failed: expected %q, got %q", expected, result) + } +} diff --git a/str.go b/str.go index b1127f4..d203596 100644 --- a/str.go +++ b/str.go @@ -1,8 +1,11 @@ package gexe -import "github.com/vladimirvivien/gexe/str" +import ( + "github.com/vladimirvivien/gexe/str" +) // String creates a new str.Str value with string manipulation methods -func (e *Session) String(s string) *str.Str { +func (e *Session) String(s string, args ...interface{}) *str.Str { + s = applySprintfIfNeeded(s, args...) return str.StringWithVars(s, e.vars) } diff --git a/variables.go b/variables.go index 0db3c92..97495d8 100644 --- a/variables.go +++ b/variables.go @@ -23,7 +23,8 @@ func (e *Session) Envs(variables ...string) *Session { } // SetEnv sets a global process environment variable. -func (e *Session) SetEnv(name, value string) *Session { +func (e *Session) SetEnv(name, value string, args ...interface{}) *Session { + value = applySprintfIfNeeded(value, args...) vars := e.vars.SetEnv(name, value) e.err = vars.Err() return e @@ -43,7 +44,8 @@ func (e *Session) Vars(variables ...string) *Session { } // SetVar declares a session variable. -func (e *Session) SetVar(name, value string) *Session { +func (e *Session) SetVar(name, value string, args ...interface{}) *Session { + value = applySprintfIfNeeded(value, args...) vars := e.vars.SetVar(name, value) e.err = vars.Err() return e @@ -64,6 +66,7 @@ func (e *Session) Val(name string) string { // Eval returns the string str with its content expanded // with variable values i.e. Eval("I am $HOME") returns // "I am " -func (e *Session) Eval(str string) string { +func (e *Session) Eval(str string, args ...interface{}) string { + str = applySprintfIfNeeded(str, args...) return e.vars.Eval(str) }