Skip to content

Support for Go fmt-formatted strings #76

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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/)!

Expand Down
3 changes: 2 additions & 1 deletion echo.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 ""
Expand Down
25 changes: 25 additions & 0 deletions examples/sprintf_demo/README.md
Original file line number Diff line number Diff line change
@@ -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
74 changes: 74 additions & 0 deletions examples/sprintf_demo/main.go
Original file line number Diff line number Diff line change
@@ -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 ===")
}
30 changes: 20 additions & 10 deletions filesys.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Loading
Loading