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. See Go Installation Guide, Go Ecosystem Overview, Go Best Practices for more context.
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