Skip to main content
โšก Calmops

Shell Integration and Scripting in Go

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