io

package
v1.202.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 18, 2025 License: Apache-2.0 Imports: 13 Imported by: 0

Documentation

Overview

Example (ComparisonOldVsNew)

Example_comparisonOldVsNew shows the difference between old and new patterns.

package main

import (
	"fmt"

	iolib "github.com/cloudposse/atmos/pkg/io"
	"github.com/cloudposse/atmos/pkg/terminal"
	"github.com/cloudposse/atmos/pkg/ui"
)

func main() {
	ioCtx, _ := iolib.NewContext()
	term := terminal.New()
	formatter := ui.NewFormatter(ioCtx, term)

	// ===== OLD PATTERN (being phased out) =====
	// Mixed concerns - unclear where output goes.
	//
	// out := ui.NewOutput(ioCtx)
	// out.Print("data")           // Where does this go? stdout? stderr?
	// out.Success("done!")        // This goes to stderr, but not obvious.
	// out.Markdown("# Doc")       // Is this data or UI? stdout or stderr?

	// ===== NEW PATTERN (preferred) =====
	// Explicit channels - always clear where output goes.

	// Data → stdout (explicit).
	fmt.Fprintf(ioCtx.Data(), "data\n")

	// UI → stderr (explicit).
	successMsg := formatter.Success("done!")
	fmt.Fprintf(ioCtx.UI(), "%s\n", successMsg)

	// Markdown - developer chooses channel based on context.
	helpMarkdown, _ := formatter.Markdown("# Documentation")

	// Help text → stdout (pipeable).
	fmt.Fprint(ioCtx.Data(), helpMarkdown)

	// Error details → stderr (UI message).
	errorMarkdown, _ := formatter.Markdown("**Error:** Something failed")
	fmt.Fprint(ioCtx.UI(), errorMarkdown)
}
Example (DecisionTree)

Example_decisionTree shows how to decide where to output.

package main

import (
	"fmt"

	iolib "github.com/cloudposse/atmos/pkg/io"
	"github.com/cloudposse/atmos/pkg/terminal"
	"github.com/cloudposse/atmos/pkg/ui"
)

func main() {
	ioCtx, _ := iolib.NewContext()
	term := terminal.New()
	formatter := ui.NewFormatter(ioCtx, term)

	// DECISION TREE:
	// 1. WHERE should it go?
	//    ├─ Pipeable data (JSON, YAML, results)     → ioCtx.Data()
	//    ├─ Human messages (status, errors, help)    → ioCtx.UI()
	//    └─ User input                               → ioCtx.Input()
	//
	// 2. HOW should it look?
	//    ├─ Plain text                               → fmt.Fprintf(channel, text)
	//    ├─ Colored/styled                           → fmt.Fprintf(channel, formatter.Success(text))
	//    └─ Markdown rendered                        → fmt.Fprint(channel, formatter.Markdown(md))
	//
	// 3. WHEN to format?
	//    ├─ Always for UI channel                    → Use formatter.* methods
	//    ├─ Conditionally for data channel           → Check term.IsTTY()
	//    └─ Never for piped output                   → Auto-handled by I/O layer

	// Example: Command help.
	// Help is DATA (can be saved, piped) but uses markdown formatting.
	helpContent := "# atmos terraform apply\n\nApplies Terraform configuration..."
	rendered, _ := formatter.Markdown(helpContent)
	fmt.Fprint(ioCtx.Data(), rendered)

	// Example: Processing status.
	// Status is UI (human-readable only) with formatting.
	status := formatter.Info("⏳ Processing 150 components...")
	fmt.Fprintf(ioCtx.UI(), "%s\n", status)

	// Example: Success message.
	// Success is UI with semantic formatting.
	msg := formatter.Success("✓ Deployment complete!")
	fmt.Fprintf(ioCtx.UI(), "%s\n", msg)

	// Example: JSON output.
	// JSON is DATA (no formatting).
	jsonString := `{"result": "success"}`
	fmt.Fprintf(ioCtx.Data(), "%s\n", jsonString)

	// Example: Error with explanation.
	// Error is UI with markdown for rich explanation.
	errorTitle := formatter.Error("Failed to load stack configuration")
	errorDetails, _ := formatter.Markdown("**Reason:** Invalid YAML syntax\n\n```yaml\nstack: invalid\n```")
	fmt.Fprintf(ioCtx.UI(), "%s\n\n%s\n", errorTitle, errorDetails)
}
Example (KeyPrinciples)

Example_keyPrinciples demonstrates the architectural principles.

package main

import (
	"fmt"

	iolib "github.com/cloudposse/atmos/pkg/io"
	"github.com/cloudposse/atmos/pkg/terminal"
	"github.com/cloudposse/atmos/pkg/ui"
)

func main() {
	ioCtx, _ := iolib.NewContext()
	term := terminal.New()
	formatter := ui.NewFormatter(ioCtx, term)

	// KEY PRINCIPLE 1: I/O layer provides CHANNELS and CAPABILITIES.
	// - Channels: Where does output go? (stdout, stderr, stdin).
	// - Capabilities: What can the terminal do? (color, TTY, width).
	// - NO formatting logic in I/O layer.

	// Access channels.
	_ = ioCtx.Data()  // stdout - pipeable data.
	_ = ioCtx.UI()    // stderr - human messages.
	_ = ioCtx.Input() // stdin - user input.

	// Access capabilities.
	_ = term.IsTTY(terminal.Stdout)
	_ = term.ColorProfile()
	_ = term.Width(terminal.Stdout)

	// KEY PRINCIPLE 2: UI layer provides FORMATTING.
	// - Returns formatted strings (pure functions).
	// - NEVER writes to streams directly.
	// - Uses I/O layer for capability detection.

	// Get formatted strings.
	_ = formatter.Success("Success!")    // Returns string.
	_ = formatter.Warning("Warning!")    // Returns string.
	_ = formatter.Error("Error!")        // Returns string.
	_, _ = formatter.Markdown("# Title") // Returns string.

	// KEY PRINCIPLE 3: Application layer COMBINES both.
	// - Gets formatted string from UI layer.
	// - Chooses channel from I/O layer.
	// - Uses fmt.Fprintf to write.

	msg := formatter.Success("Done!")
	fmt.Fprintf(ioCtx.UI(), "%s\n", msg)
}
Example (NewPattern)

ExampleNewPattern demonstrates the new I/O and UI pattern. This shows the clear separation between I/O (channels) and UI (formatting).

package main

import (
	"fmt"

	iolib "github.com/cloudposse/atmos/pkg/io"
	"github.com/cloudposse/atmos/pkg/terminal"
	"github.com/cloudposse/atmos/pkg/ui"
)

func main() {
	// ===== SETUP =====
	// Create I/O context - provides channels and masking.
	ioCtx, _ := iolib.NewContext()

	// Create terminal instance - provides TTY detection and color capabilities.
	term := terminal.New()

	// Create UI formatter - provides formatting functions (returns strings).
	formatter := ui.NewFormatter(ioCtx, term)

	// ===== PATTERN 1: Data Output (stdout) =====
	// Data channel is for pipeable output (JSON, YAML, results).

	// Plain data.
	fmt.Fprintf(ioCtx.Data(), "plain data output\n")

	// Formatted data (markdown help text).
	helpMarkdown, _ := formatter.Markdown("# Help\n\nThis is help text.")
	fmt.Fprint(ioCtx.Data(), helpMarkdown)

	// ===== PATTERN 2: UI Messages (stderr) =====
	// UI channel is for human-readable messages.

	// Plain message (asking for user confirmation).
	fmt.Fprintf(ioCtx.UI(), "Would you like to continue? (y/n): ")

	// Formatted success message.
	successMsg := formatter.Success("✓ Configuration loaded!")
	fmt.Fprintf(ioCtx.UI(), "%s\n", successMsg)

	// Formatted warning.
	warningMsg := formatter.Warning("⚠ Stack is using deprecated settings")
	fmt.Fprintf(ioCtx.UI(), "%s\n", warningMsg)

	// Formatted error.
	errorMsg := formatter.Error("✗ Failed to load stack")
	details := formatter.Muted("  Check your atmos.yaml syntax")
	fmt.Fprintf(ioCtx.UI(), "%s\n%s\n", errorMsg, details)

	// ===== PATTERN 3: Markdown for UI (stderr) =====
	// Error explanation with rich formatting.

	errorMarkdown, _ := formatter.Markdown("**Error:** Invalid configuration\n\n```yaml\nstack: invalid\n```")
	fmt.Fprint(ioCtx.UI(), errorMarkdown)

	// ===== PATTERN 4: Conditional Formatting =====
	// Only show progress if stderr is a TTY.

	if term.IsTTY(terminal.Stderr) {
		progress := formatter.Info("⏳ Processing 150 components...")
		fmt.Fprintf(ioCtx.UI(), "%s\n", progress)
	}

	// ===== PATTERN 5: Using Terminal Capabilities =====
	// Set terminal title (if supported).

	term.SetTitle("Atmos - Processing")
	defer term.RestoreTitle()

	// Check color support.
	if formatter.SupportsColor() {
		coloredMsg := formatter.Success("Color is supported!")
		fmt.Fprintf(ioCtx.UI(), "%s\n", coloredMsg)
	}

	// Get terminal width for adaptive formatting.
	width := term.Width(terminal.Stdout)
	if width > 0 {
		fmt.Fprintf(ioCtx.UI(), "Terminal width: %d columns\n", width)
	}
}

Index

Examples

Constants

View Source
const (
	// MaskReplacement is the string used to replace masked values.
	MaskReplacement = "***MASKED***"
)

Variables

View Source
var (
	// Data is the global writer for machine-readable output (stdout).
	// All writes are automatically masked based on registered secrets.
	// Use this for JSON, YAML, or any output meant for piping/automation.
	//
	// Example:
	//   fmt.Fprintf(io.Data, `{"version": "%s"}\n`, version)
	//
	// Safe default: Falls back to os.Stdout until Initialize() is called.
	Data stdio.Writer = os.Stdout

	// UI is the global writer for human-readable output (stderr).
	// All writes are automatically masked based on registered secrets.
	// Use this for messages, progress, logs meant for terminal display.
	//
	// Example:
	//   fmt.Fprintf(io.UI, "Processing...\n")
	//   logger := log.New(io.UI)
	//
	// Safe default: Falls back to os.Stderr until Initialize() is called.
	UI stdio.Writer = os.Stderr
)

Functions

func Initialize

func Initialize() error

Initialize sets up the global I/O writers with automatic masking. This should be called once in cmd/root.go PersistentPreRun. If not called explicitly, it will be called automatically on first use.

func MaskWriter

func MaskWriter(w stdio.Writer) stdio.Writer

MaskWriter wraps any io.Writer with automatic masking. Use this when you need to write to custom file handles with masking enabled.

Example:

f, _ := os.Create("output.log")
maskedFile := io.MaskWriter(f)
fmt.Fprintf(maskedFile, "SECRET=%s\n", secret)  // Automatically masked

func RegisterPattern

func RegisterPattern(pattern string) error

RegisterPattern registers a regex pattern for masking. Use this to mask values matching a specific pattern.

Example:

io.RegisterPattern(`api_key=[A-Za-z0-9]+`)

func RegisterSecret

func RegisterSecret(secret string)

RegisterSecret registers a secret value for masking. The secret and its common encodings (base64, URL, JSON) will be masked. This adds to the global masker used by io.Data and io.UI.

Example:

apiToken := getToken()
io.RegisterSecret(apiToken)
fmt.Fprintf(io.UI, "Token: %s\n", apiToken)  // Automatically masked

func RegisterValue

func RegisterValue(value string)

RegisterValue registers a literal value for masking. Use this for values that don't need encoding variants.

Example:

io.RegisterValue(sessionID)

Types

type Config

type Config struct {
	// From global flags
	RedirectStderr string
	DisableMasking bool // --disable-masking flag for debugging

	// From atmos.yaml
	AtmosConfig schema.AtmosConfiguration
}

Config holds I/O configuration for channels and masking.

type Context

type Context interface {
	// Primary output method - ALL writes should go through this for masking
	Write(stream Stream, content string) error

	// Channel access - explicit and clear (deprecated - use Write() instead)
	Data() stdio.Writer  // stdout - for pipeable data (JSON, YAML, results)
	UI() stdio.Writer    // stderr - for human messages (status, errors, prompts)
	Input() stdio.Reader // stdin - for user input

	// Raw channels (unmasked - requires justification)
	RawData() stdio.Writer // Unmasked stdout
	RawUI() stdio.Writer   // Unmasked stderr

	// Configuration
	Config() *Config

	// Output masking
	Masker() Masker

	// Legacy compatibility (deprecated - use Data()/UI() instead)
	Streams() Streams
}

Context provides access to I/O channels and masking. This is the main entry point for I/O operations.

Key Principle: I/O layer provides CHANNELS and MASKING, not formatting or terminal detection. - Channels: Where does output go? (stdout, stderr, stdin) - Masking: Secret redaction for security

ALL output must flow through Write() for automatic masking. The UI layer (pkg/ui/) handles formatting and rendering. The terminal layer (pkg/terminal/) handles TTY detection and capabilities.

func GetContext

func GetContext() Context

GetContext returns the global I/O context for advanced usage. Most code should use io.Data and io.UI instead.

func NewContext

func NewContext(opts ...ContextOption) (Context, error)

NewContext creates a new I/O context with default configuration.

type ContextOption

type ContextOption func(*context)

ContextOption configures Context behavior.

func WithMasker

func WithMasker(masker Masker) ContextOption

WithMasker sets a custom masker (for testing).

func WithStreams

func WithStreams(streams Streams) ContextOption

WithStreams sets custom streams (for testing).

type Masker

type Masker interface {
	// RegisterValue registers a literal value to be masked.
	// The value will be replaced with ***MASKED*** in all output.
	RegisterValue(value string)

	// RegisterSecret registers a secret with encoding variations.
	// Automatically registers base64, URL, and JSON encoded versions.
	RegisterSecret(secret string)

	// RegisterPattern registers a regex pattern to mask.
	// Returns error if pattern is invalid.
	RegisterPattern(pattern string) error

	// RegisterRegex registers a compiled regex pattern to mask.
	RegisterRegex(pattern *regexp.Regexp)

	// RegisterAWSAccessKey registers an AWS access key and attempts to mask the paired secret key.
	RegisterAWSAccessKey(accessKeyID string)

	// Mask applies all registered masks to the input string.
	Mask(input string) string

	// Clear removes all registered masks.
	Clear()

	// Count returns the number of registered masks.
	Count() int

	// Enabled returns whether masking is enabled.
	Enabled() bool
}

Masker handles automatic masking of sensitive data in output.

type Stream

type Stream int

Stream identifies an I/O stream for writing output. Used with Context.Write(stream, content) for centralized masking.

const (
	DataStream Stream = iota // stdout - for pipeable data (JSON, YAML, results)
	UIStream                 // stderr - for human messages (status, errors, prompts)
)

func (Stream) String

func (s Stream) String() string

String returns the string representation of the stream.

type Streams

type Streams interface {
	// Input returns the input stream (typically stdin).
	Input() stdio.Reader

	// Output returns the output stream (typically stdout) with automatic masking.
	// Deprecated: Use Context.Data() instead.
	Output() stdio.Writer

	// Error returns the error stream (typically stderr) with automatic masking.
	// Deprecated: Use Context.UI() instead.
	Error() stdio.Writer

	// RawOutput returns the unmasked output stream.
	// Use only when absolutely necessary (e.g., binary output).
	// Requires explicit justification in code review.
	// Deprecated: Use Context.RawData() instead.
	RawOutput() stdio.Writer

	// RawError returns the unmasked error stream.
	// Use only when absolutely necessary.
	// Requires explicit justification in code review.
	// Deprecated: Use Context.RawUI() instead.
	RawError() stdio.Writer
}

Streams provides access to input/output streams with automatic masking. Deprecated: Use Context.Data()/UI() directly instead. This interface exists for backward compatibility during migration.

type TerminalWriter

type TerminalWriter struct {
	// contains filtered or unexported fields
}

TerminalWriter adapts io.Context to satisfy terminal.IOWriter interface. This avoids circular dependency while allowing terminal to write through I/O layer.

func NewTerminalWriter

func NewTerminalWriter(ctx Context) *TerminalWriter

NewTerminalWriter creates a terminal-compatible writer from io.Context.

func (*TerminalWriter) Write

func (tw *TerminalWriter) Write(stream int, content string) error

Write implements terminal.IOWriter interface. It accepts int stream values (0=Data, 1=UI) and converts to io.Stream.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL