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 }