github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/command/views/view.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package views
     5  
     6  import (
     7  	"github.com/mitchellh/colorstring"
     8  	"github.com/terramate-io/tf/command/arguments"
     9  	"github.com/terramate-io/tf/command/format"
    10  	"github.com/terramate-io/tf/terminal"
    11  	"github.com/terramate-io/tf/tfdiags"
    12  )
    13  
    14  // View is the base layer for command views, encapsulating a set of I/O
    15  // streams, a colorize implementation, and implementing a human friendly view
    16  // for diagnostics.
    17  type View struct {
    18  	streams  *terminal.Streams
    19  	colorize *colorstring.Colorize
    20  
    21  	compactWarnings bool
    22  
    23  	// When this is true it's a hint that Terraform is being run indirectly
    24  	// via a wrapper script or other automation and so we may wish to replace
    25  	// direct examples of commands to run with more conceptual directions.
    26  	// However, we only do this on a best-effort basis, typically prioritizing
    27  	// the messages that users are most likely to see.
    28  	runningInAutomation bool
    29  
    30  	// This unfortunate wart is required to enable rendering of diagnostics which
    31  	// have associated source code in the configuration. This function pointer
    32  	// will be dereferenced as late as possible when rendering diagnostics in
    33  	// order to access the config loader cache.
    34  	configSources func() map[string][]byte
    35  }
    36  
    37  // Initialize a View with the given streams, a disabled colorize object, and a
    38  // no-op configSources callback.
    39  func NewView(streams *terminal.Streams) *View {
    40  	return &View{
    41  		streams: streams,
    42  		colorize: &colorstring.Colorize{
    43  			Colors:  colorstring.DefaultColors,
    44  			Disable: true,
    45  			Reset:   true,
    46  		},
    47  		configSources: func() map[string][]byte { return nil },
    48  	}
    49  }
    50  
    51  // SetRunningInAutomation modifies the view's "running in automation" flag,
    52  // which causes some slight adjustments to certain messages that would normally
    53  // suggest specific Terraform commands to run, to make more conceptual gestures
    54  // instead for situations where the user isn't running Terraform directly.
    55  //
    56  // For convenient use during initialization (in conjunction with NewView),
    57  // SetRunningInAutomation returns the reciever after modifying it.
    58  func (v *View) SetRunningInAutomation(new bool) *View {
    59  	v.runningInAutomation = new
    60  	return v
    61  }
    62  
    63  func (v *View) RunningInAutomation() bool {
    64  	return v.runningInAutomation
    65  }
    66  
    67  // Configure applies the global view configuration flags.
    68  func (v *View) Configure(view *arguments.View) {
    69  	v.colorize.Disable = view.NoColor
    70  	v.compactWarnings = view.CompactWarnings
    71  }
    72  
    73  // SetConfigSources overrides the default no-op callback with a new function
    74  // pointer, and should be called when the config loader is initialized.
    75  func (v *View) SetConfigSources(cb func() map[string][]byte) {
    76  	v.configSources = cb
    77  }
    78  
    79  // Diagnostics renders a set of warnings and errors in human-readable form.
    80  // Warnings are printed to stdout, and errors to stderr.
    81  func (v *View) Diagnostics(diags tfdiags.Diagnostics) {
    82  	diags.Sort()
    83  
    84  	if len(diags) == 0 {
    85  		return
    86  	}
    87  
    88  	diags = diags.ConsolidateWarnings(1)
    89  
    90  	// Since warning messages are generally competing
    91  	if v.compactWarnings {
    92  		// If the user selected compact warnings and all of the diagnostics are
    93  		// warnings then we'll use a more compact representation of the warnings
    94  		// that only includes their summaries.
    95  		// We show full warnings if there are also errors, because a warning
    96  		// can sometimes serve as good context for a subsequent error.
    97  		useCompact := true
    98  		for _, diag := range diags {
    99  			if diag.Severity() != tfdiags.Warning {
   100  				useCompact = false
   101  				break
   102  			}
   103  		}
   104  		if useCompact {
   105  			msg := format.DiagnosticWarningsCompact(diags, v.colorize)
   106  			msg = "\n" + msg + "\nTo see the full warning notes, run Terraform without -compact-warnings.\n"
   107  			v.streams.Print(msg)
   108  			return
   109  		}
   110  	}
   111  
   112  	for _, diag := range diags {
   113  		var msg string
   114  		if v.colorize.Disable {
   115  			msg = format.DiagnosticPlain(diag, v.configSources(), v.streams.Stderr.Columns())
   116  		} else {
   117  			msg = format.Diagnostic(diag, v.configSources(), v.colorize, v.streams.Stderr.Columns())
   118  		}
   119  
   120  		if diag.Severity() == tfdiags.Error {
   121  			v.streams.Eprint(msg)
   122  		} else {
   123  			v.streams.Print(msg)
   124  		}
   125  	}
   126  }
   127  
   128  // HelpPrompt is intended to be called from commands which fail to parse all
   129  // of their CLI arguments successfully. It refers users to the full help output
   130  // rather than rendering it directly, which can be overwhelming and confusing.
   131  func (v *View) HelpPrompt(command string) {
   132  	v.streams.Eprintf(helpPrompt, command)
   133  }
   134  
   135  const helpPrompt = `
   136  For more help on using this command, run:
   137    terraform %s -help
   138  `
   139  
   140  // outputColumns returns the number of text character cells any non-error
   141  // output should be wrapped to.
   142  //
   143  // This is the number of columns to use if you are calling v.streams.Print or
   144  // related functions.
   145  func (v *View) outputColumns() int {
   146  	return v.streams.Stdout.Columns()
   147  }
   148  
   149  // errorColumns returns the number of text character cells any error
   150  // output should be wrapped to.
   151  //
   152  // This is the number of columns to use if you are calling v.streams.Eprint
   153  // or related functions.
   154  func (v *View) errorColumns() int {
   155  	return v.streams.Stderr.Columns()
   156  }
   157  
   158  // outputHorizRule will call v.streams.Println with enough horizontal line
   159  // characters to fill an entire row of output.
   160  //
   161  // If UI color is enabled, the rule will get a dark grey coloring to try to
   162  // visually de-emphasize it.
   163  func (v *View) outputHorizRule() {
   164  	v.streams.Println(format.HorizontalRule(v.colorize, v.outputColumns()))
   165  }