Shell Integration and Scripting in Go
Introduction
Go applications often need to integrate with shell environments and execute shell scripts. This guide covers shell integration, script generation, and cross-platform compatibility.
Proper shell integration enables your Go applications to work seamlessly with existing shell scripts and environments.
Shell Command Execution
Basic Shell Execution
package main
import (
"fmt"
"os"
"os/exec"
"runtime"
)
// ExecuteShellCommand executes a shell command
func ExecuteShellCommand(command string) error {
var cmd *exec.Cmd
if runtime.GOOS == "windows" {
cmd = exec.Command("cmd", "/C", command)
} else {
cmd = exec.Command("sh", "-c", command)
}
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin
return cmd.Run()
}
// ExecuteShellCommandWithOutput executes shell command and captures output
func ExecuteShellCommandWithOutput(command string) (string, error) {
var cmd *exec.Cmd
if runtime.GOOS == "windows" {
cmd = exec.Command("cmd", "/C", command)
} else {
cmd = exec.Command("sh", "-c", command)
}
output, err := cmd.CombinedOutput()
return string(output), err
}
// Example usage
func ShellExample() {
// Execute shell command
if err := ExecuteShellCommand("echo 'Hello from shell'"); err != nil {
fmt.Println("Error:", err)
}
// Execute with output
output, err := ExecuteShellCommandWithOutput("ls -la")
if err != nil {
fmt.Println("Error:", err)
}
fmt.Println(output)
}
Good: Proper Shell Integration Implementation
package main
import (
"bytes"
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"time"
)
// ShellExecutor handles shell command execution
type ShellExecutor struct {
shell string
timeout time.Duration
workDir string
env map[string]string
}
// NewShellExecutor creates a new shell executor
func NewShellExecutor() *ShellExecutor {
shell := "sh"
if runtime.GOOS == "windows" {
shell = "cmd"
}
return &ShellExecutor{
shell: shell,
timeout: 30 * time.Second,
env: make(map[string]string),
}
}
// SetTimeout sets command timeout
func (se *ShellExecutor) SetTimeout(timeout time.Duration) {
se.timeout = timeout
}
// SetWorkDir sets working directory
func (se *ShellExecutor) SetWorkDir(dir string) {
se.workDir = dir
}
// SetEnv sets environment variable
func (se *ShellExecutor) SetEnv(key, value string) {
se.env[key] = value
}
// Execute executes a shell command
func (se *ShellExecutor) Execute(command string) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), se.timeout)
defer cancel()
var cmd *exec.Cmd
if runtime.GOOS == "windows" {
cmd = exec.CommandContext(ctx, "cmd", "/C", command)
} else {
cmd = exec.CommandContext(ctx, "sh", "-c", command)
}
// Set working directory
if se.workDir != "" {
cmd.Dir = se.workDir
}
// Set environment variables
cmd.Env = os.Environ()
for key, value := range se.env {
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", key, value))
}
// Capture output
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
return "", fmt.Errorf("command failed: %s\nstderr: %s", err, stderr.String())
}
return stdout.String(), nil
}
// ExecuteScript executes a shell script file
func (se *ShellExecutor) ExecuteScript(scriptPath string, args ...string) (string, error) {
// Validate script exists
if _, err := os.Stat(scriptPath); err != nil {
return "", fmt.Errorf("script not found: %w", err)
}
// Make script executable on Unix
if runtime.GOOS != "windows" {
os.Chmod(scriptPath, 0755)
}
ctx, cancel := context.WithTimeout(context.Background(), se.timeout)
defer cancel()
cmd := exec.CommandContext(ctx, scriptPath, args...)
if se.workDir != "" {
cmd.Dir = se.workDir
}
cmd.Env = os.Environ()
for key, value := range se.env {
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", key, value))
}
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
return "", fmt.Errorf("script failed: %s\nstderr: %s", err, stderr.String())
}
return stdout.String(), nil
}
// ExecutePipe executes piped commands
func (se *ShellExecutor) ExecutePipe(commands ...string) (string, error) {
if len(commands) == 0 {
return "", fmt.Errorf("no commands provided")
}
// Join commands with pipe
command := strings.Join(commands, " | ")
return se.Execute(command)
}
Script Generation
Dynamic Script Generation
package main
import (
"fmt"
"os"
"path/filepath"
"runtime"
"text/template"
)
// ScriptGenerator generates shell scripts
type ScriptGenerator struct {
isWindows bool
}
// NewScriptGenerator creates a new script generator
func NewScriptGenerator() *ScriptGenerator {
return &ScriptGenerator{
isWindows: runtime.GOOS == "windows",
}
}
// GenerateBackupScript generates a backup script
func (sg *ScriptGenerator) GenerateBackupScript(source, destination string) string {
if sg.isWindows {
return fmt.Sprintf(`@echo off
xcopy "%s" "%s" /E /I /Y
echo Backup completed
`, source, destination)
}
return fmt.Sprintf(`#!/bin/bash
cp -r "%s" "%s"
echo "Backup completed"
`, source, destination)
}
// GenerateDeployScript generates a deployment script
func (sg *ScriptGenerator) GenerateDeployScript(appName, version string) string {
if sg.isWindows {
return fmt.Sprintf(`@echo off
echo Deploying %s version %s
REM Add deployment commands here
echo Deployment completed
`, appName, version)
}
return fmt.Sprintf(`#!/bin/bash
echo "Deploying %s version %s"
# Add deployment commands here
echo "Deployment completed"
`, appName, version)
}
// SaveScript saves script to file
func (sg *ScriptGenerator) SaveScript(scriptPath, content string) error {
if err := os.WriteFile(scriptPath, []byte(content), 0755); err != nil {
return fmt.Errorf("failed to save script: %w", err)
}
// Make executable on Unix
if !sg.isWindows {
os.Chmod(scriptPath, 0755)
}
return nil
}
// GenerateFromTemplate generates script from template
func (sg *ScriptGenerator) GenerateFromTemplate(tmpl string, data interface{}) (string, error) {
t, err := template.New("script").Parse(tmpl)
if err != nil {
return "", err
}
var result bytes.Buffer
if err := t.Execute(&result, data); err != nil {
return "", err
}
return result.String(), nil
}
Cross-Platform Compatibility
package main
import (
"fmt"
"os"
"path/filepath"
"runtime"
)
// CrossPlatformPath converts path to platform-specific format
func CrossPlatformPath(path string) string {
return filepath.FromSlash(path)
}
// GetShellPath returns platform-specific shell path
func GetShellPath() string {
if runtime.GOOS == "windows" {
return "cmd.exe"
}
return "/bin/sh"
}
// GetPathSeparator returns platform-specific path separator
func GetPathSeparator() string {
return string(os.PathSeparator)
}
// GetLineEnding returns platform-specific line ending
func GetLineEnding() string {
if runtime.GOOS == "windows" {
return "\r\n"
}
return "\n"
}
// PlatformSpecificCommand returns platform-specific command
func PlatformSpecificCommand(unixCmd, windowsCmd string) string {
if runtime.GOOS == "windows" {
return windowsCmd
}
return unixCmd
}
// Example usage
func CrossPlatformExample() {
// Get appropriate shell
shell := GetShellPath()
fmt.Printf("Shell: %s\n", shell)
// Convert paths
path := CrossPlatformPath("path/to/file")
fmt.Printf("Path: %s\n", path)
// Get line ending
ending := GetLineEnding()
fmt.Printf("Line ending: %q\n", ending)
// Platform-specific command
cmd := PlatformSpecificCommand("ls -la", "dir")
fmt.Printf("Command: %s\n", cmd)
}
Environment Integration
package main
import (
"fmt"
"os"
"strings"
)
// ShellEnvironment manages shell environment
type ShellEnvironment struct {
vars map[string]string
}
// NewShellEnvironment creates a new shell environment
func NewShellEnvironment() *ShellEnvironment {
return &ShellEnvironment{
vars: make(map[string]string),
}
}
// LoadFromOS loads environment from OS
func (se *ShellEnvironment) LoadFromOS() {
for _, env := range os.Environ() {
parts := strings.SplitN(env, "=", 2)
if len(parts) == 2 {
se.vars[parts[0]] = parts[1]
}
}
}
// Set sets an environment variable
func (se *ShellEnvironment) Set(key, value string) {
se.vars[key] = value
}
// Get gets an environment variable
func (se *ShellEnvironment) Get(key string) string {
return se.vars[key]
}
// GetOrDefault gets environment variable with default
func (se *ShellEnvironment) GetOrDefault(key, defaultValue string) string {
if value, exists := se.vars[key]; exists {
return value
}
return defaultValue
}
// ToEnvSlice converts to environment slice
func (se *ShellEnvironment) ToEnvSlice() []string {
var env []string
for key, value := range se.vars {
env = append(env, fmt.Sprintf("%s=%s", key, value))
}
return env
}
Best Practices
1. Validate Commands
// Validate command before execution
if strings.Contains(command, ";") {
return fmt.Errorf("invalid command")
}
2. Use Context for Timeout
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
3. Capture Output
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
4. Handle Platform Differences
if runtime.GOOS == "windows" {
// Windows-specific code
} else {
// Unix-specific code
}
Common Pitfalls
1. Shell Injection
Always validate and escape user input.
2. Platform Assumptions
Don’t assume Unix-like environment.
3. No Timeout
Always set timeouts for shell commands.
4. Ignoring Errors
Always check and handle errors.
Resources
Summary
Shell integration requires careful handling. Key takeaways:
- Use context for timeout management
- Validate and escape user input
- Handle platform differences
- Capture output for debugging
- Set appropriate permissions
- Use environment variables properly
- Test on multiple platforms
By mastering shell integration, you can build powerful CLI tools.
Comments