command: Use the new terminal.Streams object

Here we propagate in the initialized terminal.Streams from package main,
and then onwards to backends running in CLI mode.

This also replaces our use of helper/wrappedstreams to determine whether
stdin is a terminal or a pipe. helper/wrappedstreams returns incorrect
file descriptors on Windows, causing StdinPiped to always return false on
that platform and thus causing one of the odd behaviors discussed in

Finally, this includes some wrappers around the ability to look up the
number of columns in the terminal in preparation for use elsewhere. These
wrappers deal with the fact that our unit tests typically won't populate
meta.Streams.
This commit is contained in:
Martin Atkins 2021-01-11 18:20:58 -08:00
parent 15c0645bd5
commit d2c3403ab6
8 changed files with 88 additions and 10 deletions

View File

@ -1,9 +1,11 @@
package backend
import (
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/cli"
"github.com/mitchellh/colorstring"
"github.com/hashicorp/terraform/internal/terminal"
"github.com/hashicorp/terraform/terraform"
)
// CLI is an optional interface that can be implemented to be initialized
@ -48,6 +50,12 @@ type CLIOpts struct {
CLI cli.Ui
CLIColor *colorstring.Colorize
// Streams describes the low-level streams for Stdout, Stderr and Stdin,
// including some metadata about whether they are terminals. Most output
// should go via the object in field CLI above, but Streams can be useful
// for tailoring the output to fit the attached terminal, for example.
Streams *terminal.Streams
// ShowDiagnostics is a function that will format and print diagnostic
// messages to the UI.
ShowDiagnostics func(vals ...interface{})

View File

@ -14,6 +14,7 @@ import (
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/command/clistate"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/internal/terminal"
"github.com/hashicorp/terraform/states/statemgr"
"github.com/hashicorp/terraform/terraform"
"github.com/hashicorp/terraform/tfdiags"
@ -38,6 +39,10 @@ type Local struct {
CLI cli.Ui
CLIColor *colorstring.Colorize
// If CLI is set then Streams might also be set, to describe the physical
// input/output handles that CLI is connected to.
Streams *terminal.Streams
// ShowDiagnostics prints diagnostic messages to the UI.
ShowDiagnostics func(vals ...interface{})

View File

@ -10,6 +10,7 @@ import (
func (b *Local) CLIInit(opts *backend.CLIOpts) error {
b.CLI = opts.CLI
b.CLIColor = opts.CLIColor
b.Streams = opts.Streams
b.ShowDiagnostics = opts.ShowDiagnostics
b.ContextOpts = opts.ContextOpts
b.OpInput = opts.Input
@ -34,3 +35,31 @@ func (b *Local) CLIInit(opts *backend.CLIOpts) error {
return nil
}
// outputColumns returns the number of text character cells any non-error
// output should be wrapped to.
//
// This is the number of columns to use if you are calling b.CLI.Output or
// b.CLI.Info.
func (b *Local) outputColumns() int {
if b.Streams == nil {
// We can potentially get here in tests, if they don't populate the
// CLIOpts fully.
return 78 // placeholder just so we don't panic
}
return b.Streams.Stdout.Columns()
}
// errorColumns returns the number of text character cells any error
// output should be wrapped to.
//
// This is the number of columns to use if you are calling b.CLI.Error or
// b.CLI.Warn.
func (b *Local) errorColumns() int {
if b.Streams == nil {
// We can potentially get here in tests, if they don't populate the
// CLIOpts fully.
return 78 // placeholder just so we don't panic
}
return b.Streams.Stderr.Columns()
}

View File

@ -22,8 +22,8 @@ import (
"github.com/hashicorp/terraform/command/format"
"github.com/hashicorp/terraform/command/webbrowser"
"github.com/hashicorp/terraform/configs/configload"
"github.com/hashicorp/terraform/helper/wrappedstreams"
"github.com/hashicorp/terraform/internal/getproviders"
"github.com/hashicorp/terraform/internal/terminal"
"github.com/hashicorp/terraform/providers"
"github.com/hashicorp/terraform/provisioners"
"github.com/hashicorp/terraform/terraform"
@ -51,6 +51,15 @@ type Meta struct {
// for some reason.
OriginalWorkingDir string
// Streams tracks the raw Stdout, Stderr, and Stdin handles along with
// some basic metadata about them, such as whether each is connected to
// a terminal, how wide the possible terminal is, etc.
//
// For historical reasons this might not be set in unit test code, and
// so functions working with this field must check if it's nil and
// do some default behavior instead if so, rather than panicking.
Streams *terminal.Streams
Color bool // True if output should be colored
GlobalPluginDirs []string // Additional paths to search for plugins
Ui cli.Ui // Ui for output
@ -288,15 +297,42 @@ func (m *Meta) UIInput() terraform.UIInput {
}
}
// OutputColumns returns the number of columns that normal (non-error) UI
// output should be wrapped to fill.
//
// This is the column count to use if you'll be printing your message via
// the Output or Info methods of m.Ui.
func (m *Meta) OutputColumns() int {
if m.Streams == nil {
// A default for unit tests that don't populate Meta fully.
return 78
}
return m.Streams.Stdout.Columns()
}
// ErrorColumns returns the number of columns that error UI output should be
// wrapped to fill.
//
// This is the column count to use if you'll be printing your message via
// the Error or Warn methods of m.Ui.
func (m *Meta) ErrorColumns() int {
if m.Streams == nil {
// A default for unit tests that don't populate Meta fully.
return 78
}
return m.Streams.Stderr.Columns()
}
// StdinPiped returns true if the input is piped.
func (m *Meta) StdinPiped() bool {
fi, err := wrappedstreams.Stdin().Stat()
if err != nil {
// If there is an error, let's just say its not piped
if m.Streams == nil {
// If we don't have m.Streams populated then we're presumably in a unit
// test that doesn't properly populate Meta, so we'll just say the
// output _isn't_ piped because that's the common case and so most likely
// to be useful to a unit test.
return false
}
return fi.Mode()&os.ModeNamedPipe != 0
return !m.Streams.Stdin.IsTerminal()
}
// InterruptibleContext returns a context.Context that will be cancelled

View File

@ -308,6 +308,7 @@ func (m *Meta) backendCLIOpts() (*backend.CLIOpts, error) {
return &backend.CLIOpts{
CLI: m.Ui,
CLIColor: m.Colorize(),
Streams: m.Streams,
ShowDiagnostics: m.showDiagnostics,
StatePath: m.statePath,
StateOutPath: m.stateOutPath,

View File

@ -80,6 +80,7 @@ func initCommands(
meta := command.Meta{
OriginalWorkingDir: originalWorkingDir,
Streams: streams,
Color: true,
GlobalPluginDirs: globalPluginDirs(),

1
go.mod
View File

@ -80,7 +80,6 @@ require (
github.com/lusis/go-artifactory v0.0.0-20160115162124-7e4ce345df82
github.com/masterzen/simplexml v0.0.0-20190410153822-31eea3082786 // indirect
github.com/masterzen/winrm v0.0.0-20200615185753-c42b5136ff88
github.com/mattn/go-colorable v0.1.8 // indirect
github.com/mattn/go-isatty v0.0.12
github.com/mattn/go-shellwords v1.0.4
github.com/miekg/dns v1.0.8 // indirect

3
go.sum
View File

@ -420,9 +420,8 @@ github.com/masterzen/simplexml v0.0.0-20190410153822-31eea3082786/go.mod h1:kCEb
github.com/masterzen/winrm v0.0.0-20200615185753-c42b5136ff88 h1:cxuVcCvCLD9yYDbRCWw0jSgh1oT6P6mv3aJDKK5o7X4=
github.com/masterzen/winrm v0.0.0-20200615185753-c42b5136ff88/go.mod h1:a2HXwefeat3evJHxFXSayvRHpYEPJYtErl4uIzfaUqY=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=