diff --git a/backend/local/backend_plan.go b/backend/local/backend_plan.go index d579efd04..348d9a14b 100644 --- a/backend/local/backend_plan.go +++ b/backend/local/backend_plan.go @@ -32,6 +32,8 @@ func (b *Local) opPlan( var diags tfdiags.Diagnostics + outputColumns := b.outputColumns() + if op.PlanFile != nil { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, @@ -150,6 +152,7 @@ func (b *Local) opPlan( if runningOp.PlanEmpty { b.CLI.Output("\n" + b.Colorize().Color(strings.TrimSpace(planNoChanges))) + b.CLI.Output("\n" + strings.TrimSpace(format.WordWrap(planNoChangesDetail, outputColumns))) // Even if there are no changes, there still could be some warnings b.ShowDiagnostics(diags) return @@ -166,15 +169,15 @@ func (b *Local) opPlan( // tool which is presumed to provide its own UI for further actions. if !b.RunningInAutomation { - b.CLI.Output("\n------------------------------------------------------------------------") + b.outputHorizRule() if path := op.PlanOutPath; path == "" { b.CLI.Output(fmt.Sprintf( - "\n" + strings.TrimSpace(planHeaderNoOutput) + "\n", + "\n" + strings.TrimSpace(format.WordWrap(planHeaderNoOutput, outputColumns)) + "\n", )) } else { b.CLI.Output(fmt.Sprintf( - "\n"+strings.TrimSpace(planHeaderYesOutput)+"\n", + "\n"+strings.TrimSpace(format.WordWrap(planHeaderYesOutput, outputColumns))+"\n", path, path, )) } @@ -183,7 +186,7 @@ func (b *Local) opPlan( } func (b *Local) renderPlan(plan *plans.Plan, baseState *states.State, schemas *terraform.Schemas) { - RenderPlan(plan, baseState, schemas, b.CLI, b.Colorize()) + RenderPlan(plan, baseState, schemas, b.CLI, b.Colorize(), b.outputColumns()) } // RenderPlan renders the given plan to the given UI. @@ -206,7 +209,7 @@ func (b *Local) renderPlan(plan *plans.Plan, baseState *states.State, schemas *t // output values will not currently be rendered because their prior values // are currently stored only in the prior state. (see the docstring for // func planHasSideEffects for why this is and when that might change) -func RenderPlan(plan *plans.Plan, baseState *states.State, schemas *terraform.Schemas, ui cli.Ui, colorize *colorstring.Colorize) { +func RenderPlan(plan *plans.Plan, baseState *states.State, schemas *terraform.Schemas, ui cli.Ui, colorize *colorstring.Colorize, width int) { counts := map[plans.Action]int{} var rChanges []*plans.ResourceInstanceChangeSrc for _, change := range plan.Changes.Resources { @@ -220,7 +223,7 @@ func RenderPlan(plan *plans.Plan, baseState *states.State, schemas *terraform.Sc } headerBuf := &bytes.Buffer{} - fmt.Fprintf(headerBuf, "\n%s\n", strings.TrimSpace(planHeaderIntro)) + fmt.Fprintf(headerBuf, "\n%s\n", strings.TrimSpace(format.WordWrap(planHeaderIntro, width))) if counts[plans.Create] > 0 { fmt.Fprintf(headerBuf, "%s create\n", format.DiffActionSymbol(plans.Create)) } @@ -330,18 +333,15 @@ func RenderPlan(plan *plans.Plan, baseState *states.State, schemas *terraform.Sc } const planHeaderIntro = ` -An execution plan has been generated and is shown below. -Resource actions are indicated with the following symbols: +Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: ` const planHeaderNoOutput = ` -Note: You didn't specify an "-out" parameter to save this plan, so Terraform -can't guarantee that exactly these actions will be performed if -"terraform apply" is subsequently run. +Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now. ` const planHeaderYesOutput = ` -This plan was saved to: %s +Saved the plan to: %s To perform exactly these actions, run the following command to apply: terraform apply %q @@ -349,8 +349,8 @@ To perform exactly these actions, run the following command to apply: const planNoChanges = ` [reset][bold][green]No changes. Infrastructure is up-to-date.[reset][green] - -This means that Terraform did not detect any differences between your -configuration and real physical resources that exist. As a result, no -actions need to be performed. +` + +const planNoChangesDetail = ` +That Terraform did not detect any differences between your configuration and the remote system(s). As a result, there are no actions to take. ` diff --git a/backend/local/backend_plan_test.go b/backend/local/backend_plan_test.go index 3f72168f1..0b0b14172 100644 --- a/backend/local/backend_plan_test.go +++ b/backend/local/backend_plan_test.go @@ -51,7 +51,7 @@ func TestLocal_planInAutomation(t *testing.T) { defer cleanup() TestLocalProvider(t, b, "test", planFixtureSchema()) - const msg = `You didn't specify an "-out" parameter` + const msg = `You didn't use the -out option` // When we're "in automation" we omit certain text from the // plan output. However, testing for the absense of text is @@ -77,7 +77,7 @@ func TestLocal_planInAutomation(t *testing.T) { output := b.CLI.(*cli.MockUi).OutputWriter.String() if !strings.Contains(output, msg) { - t.Fatalf("missing next-steps message when not in automation") + t.Fatalf("missing next-steps message when not in automation\nwant: %s\noutput:\n%s", msg, output) } } @@ -331,8 +331,8 @@ func TestLocal_planTainted(t *testing.T) { t.Fatal("plan should not be empty") } - expectedOutput := `An execution plan has been generated and is shown below. -Resource actions are indicated with the following symbols: + expectedOutput := `Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: -/+ destroy and then create replacement Terraform will perform the following actions: @@ -433,8 +433,8 @@ func TestLocal_planDeposedOnly(t *testing.T) { // it's also possible for there to be _multiple_ deposed objects, in the // unlikely event that create_before_destroy _keeps_ crashing across // subsequent runs. - expectedOutput := `An execution plan has been generated and is shown below. -Resource actions are indicated with the following symbols: + expectedOutput := `Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: + create - destroy @@ -507,8 +507,8 @@ func TestLocal_planTainted_createBeforeDestroy(t *testing.T) { t.Fatal("plan should not be empty") } - expectedOutput := `An execution plan has been generated and is shown below. -Resource actions are indicated with the following symbols: + expectedOutput := `Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: +/- create replacement and then destroy Terraform will perform the following actions: diff --git a/backend/local/cli.go b/backend/local/cli.go index f395fbff6..432ffc383 100644 --- a/backend/local/cli.go +++ b/backend/local/cli.go @@ -4,6 +4,7 @@ import ( "log" "github.com/hashicorp/terraform/backend" + "github.com/hashicorp/terraform/command/format" ) // backend.CLI impl. @@ -63,3 +64,17 @@ func (b *Local) errorColumns() int { } return b.Streams.Stderr.Columns() } + +// outputHorizRule will call b.CLI.Output with enough horizontal line +// characters to fill an entire row of output. +// +// This function does nothing if the backend doesn't have a CLI attached. +// +// If UI color is enabled, the rule will get a dark grey coloring to try to +// visually de-emphasize it. +func (b *Local) outputHorizRule() { + if b.CLI == nil { + return + } + b.CLI.Output(format.HorizontalRule(b.CLIColor, b.outputColumns())) +} diff --git a/command/e2etest/primary_test.go b/command/e2etest/primary_test.go index a16b2c277..304ddedc9 100644 --- a/command/e2etest/primary_test.go +++ b/command/e2etest/primary_test.go @@ -59,8 +59,8 @@ func TestPrimarySeparatePlan(t *testing.T) { t.Errorf("incorrect plan tally; want 1 to add:\n%s", stdout) } - if !strings.Contains(stdout, "This plan was saved to: tfplan") { - t.Errorf("missing \"This plan was saved to...\" message in plan output\n%s", stdout) + if !strings.Contains(stdout, "Saved the plan to: tfplan") { + t.Errorf("missing \"Saved the plan to...\" message in plan output\n%s", stdout) } if !strings.Contains(stdout, "terraform apply \"tfplan\"") { t.Errorf("missing next-step instruction in plan output\n%s", stdout) @@ -169,8 +169,8 @@ func TestPrimaryChdirOption(t *testing.T) { t.Errorf("incorrect plan tally; want 0 to add:\n%s", stdout) } - if !strings.Contains(stdout, "This plan was saved to: tfplan") { - t.Errorf("missing \"This plan was saved to...\" message in plan output\n%s", stdout) + if !strings.Contains(stdout, "Saved the plan to: tfplan") { + t.Errorf("missing \"Saved the plan to...\" message in plan output\n%s", stdout) } if !strings.Contains(stdout, "terraform apply \"tfplan\"") { t.Errorf("missing next-step instruction in plan output\n%s", stdout) diff --git a/command/format/trivia.go b/command/format/trivia.go new file mode 100644 index 000000000..3e05faa55 --- /dev/null +++ b/command/format/trivia.go @@ -0,0 +1,55 @@ +package format + +import ( + "strings" + + "github.com/mitchellh/colorstring" + wordwrap "github.com/mitchellh/go-wordwrap" +) + +// HorizontalRule returns a newline character followed by a number of +// horizontal line characters to fill the given width. +// +// If the given colorize has colors enabled, the rule will also be given a +// dark grey color to attempt to visually de-emphasize it for sighted users. +// +// This is intended for printing to the UI via mitchellh/cli.UI.Output, or +// similar, which will automatically append a trailing newline too. +func HorizontalRule(color *colorstring.Colorize, width int) string { + rule := strings.Repeat("─", width) + if color == nil { // sometimes unit tests don't populate this properly + return "\n" + rule + } + return color.Color("[dark_gray]\n" + rule) +} + +// WordWrap takes a string containing unbroken lines of text and inserts +// newline characters to try to make the text fit within the given width. +// +// The string can already contain newline characters, for example if you are +// trying to render multiple paragraphs of text. (In that case, our usual +// style would be to have _two_ newline characters as the paragraph separator.) +// +// As a special case, any line that begins with at least one space will be left +// unbroken. This allows including literal segments in the output, such as +// code snippets or filenames, where word wrapping would be confusing. +func WordWrap(str string, width int) string { + if width == 0 { + // Silly edge case. We'll just return the original string to avoid + // panicking or doing other weird stuff. + return str + } + + var buf strings.Builder + lines := strings.Split(str, "\n") + for i, line := range lines { + if !strings.HasPrefix(line, " ") { + line = wordwrap.WrapString(line, uint(width)) + } + if i > 0 { + buf.WriteByte('\n') // reintroduce the newlines we skipped in Scan + } + buf.WriteString(line) + } + return buf.String() +} diff --git a/command/meta.go b/command/meta.go index 6b77c3a68..14f9d9c51 100644 --- a/command/meta.go +++ b/command/meta.go @@ -629,6 +629,8 @@ func (m *Meta) showDiagnostics(vals ...interface{}) { return } + outputWidth := m.ErrorColumns() + diags = diags.ConsolidateWarnings(1) // Since warning messages are generally competing @@ -654,11 +656,7 @@ func (m *Meta) showDiagnostics(vals ...interface{}) { } for _, diag := range diags { - // TODO: Actually measure the terminal width and pass it here. - // For now, we don't have easy access to the writer that - // ui.Error (etc) are writing to and thus can't interrogate - // to see if it's a terminal and what size it is. - msg := format.Diagnostic(diag, m.configSources(), m.Colorize(), 78) + msg := format.Diagnostic(diag, m.configSources(), m.Colorize(), outputWidth) switch diag.Severity() { case tfdiags.Error: m.Ui.Error(msg) diff --git a/command/show.go b/command/show.go index 852999542..db5efee10 100644 --- a/command/show.go +++ b/command/show.go @@ -166,7 +166,7 @@ func (c *ShowCommand) Run(args []string) int { // package rather than in the backends themselves, but for now we're // accepting this oddity because "terraform show" is a less commonly // used way to render a plan than "terraform plan" is. - localBackend.RenderPlan(plan, stateFile.State, schemas, c.Ui, c.Colorize()) + localBackend.RenderPlan(plan, stateFile.State, schemas, c.Ui, c.Colorize(), c.OutputColumns()) return 0 }