command/output: Raw output mode
So far the output command has had a default output format intended for human consumption and a JSON output format intended for machine consumption. However, until Terraform v0.14 the default output format for primitive types happened to be _almost_ a raw string representation of the value, and so users started using that as a more convenient way to access primitive-typed output values from shell scripts, avoiding the need to also use a tool like "jq" to decode the JSON. Recognizing that primitive-typed output values are common and that processing them with shell scripts is common, this commit introduces a new -raw mode which is explicitly intended for that use-case, guaranteeing that the result will always be the direct result of a string conversion of the output value, or an error if no such conversion is possible. Our policy elsewhere in Terraform is that we always use JSON for machine-readable output. We adopted that policy because our other machine-readable output has typically been complex data structures rather than single primitive values. A special mode seems justified for output values because it is common for root module output values to be just strings, and so it's pragmatic to offer access to the raw value directly rather than requiring a round-trip through JSON.
This commit is contained in:
parent
7acc483110
commit
3268a7eaba
|
@ -5,6 +5,8 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/zclconf/go-cty/cty"
|
||||||
|
"github.com/zclconf/go-cty/cty/convert"
|
||||||
ctyjson "github.com/zclconf/go-cty/cty/json"
|
ctyjson "github.com/zclconf/go-cty/cty/json"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/addrs"
|
"github.com/hashicorp/terraform/addrs"
|
||||||
|
@ -17,14 +19,19 @@ import (
|
||||||
// from a Terraform state and prints it.
|
// from a Terraform state and prints it.
|
||||||
type OutputCommand struct {
|
type OutputCommand struct {
|
||||||
Meta
|
Meta
|
||||||
|
|
||||||
|
// Unit tests may set rawPrint to capture the output from the -raw
|
||||||
|
// option, which would normally go to stdout directly.
|
||||||
|
rawPrint func(string)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *OutputCommand) Run(args []string) int {
|
func (c *OutputCommand) Run(args []string) int {
|
||||||
args = c.Meta.process(args)
|
args = c.Meta.process(args)
|
||||||
var module, statePath string
|
var module, statePath string
|
||||||
var jsonOutput bool
|
var jsonOutput, rawOutput bool
|
||||||
cmdFlags := c.Meta.defaultFlagSet("output")
|
cmdFlags := c.Meta.defaultFlagSet("output")
|
||||||
cmdFlags.BoolVar(&jsonOutput, "json", false, "json")
|
cmdFlags.BoolVar(&jsonOutput, "json", false, "json")
|
||||||
|
cmdFlags.BoolVar(&rawOutput, "raw", false, "raw")
|
||||||
cmdFlags.StringVar(&statePath, "state", "", "path")
|
cmdFlags.StringVar(&statePath, "state", "", "path")
|
||||||
cmdFlags.StringVar(&module, "module", "", "module")
|
cmdFlags.StringVar(&module, "module", "", "module")
|
||||||
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
||||||
|
@ -42,6 +49,18 @@ func (c *OutputCommand) Run(args []string) int {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if jsonOutput && rawOutput {
|
||||||
|
c.Ui.Error("The -raw and -json options are mutually-exclusive.\n")
|
||||||
|
cmdFlags.Usage()
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if rawOutput && len(args) == 0 {
|
||||||
|
c.Ui.Error("You must give the name of a single output value when using the -raw option.\n")
|
||||||
|
cmdFlags.Usage()
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
name := ""
|
name := ""
|
||||||
if len(args) > 0 {
|
if len(args) > 0 {
|
||||||
name = args[0]
|
name = args[0]
|
||||||
|
@ -187,14 +206,65 @@ func (c *OutputCommand) Run(args []string) int {
|
||||||
}
|
}
|
||||||
v := os.Value
|
v := os.Value
|
||||||
|
|
||||||
if jsonOutput {
|
switch {
|
||||||
|
case jsonOutput:
|
||||||
jsonOutput, err := ctyjson.Marshal(v, v.Type())
|
jsonOutput, err := ctyjson.Marshal(v, v.Type())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Ui.Output(string(jsonOutput))
|
c.Ui.Output(string(jsonOutput))
|
||||||
} else {
|
case rawOutput:
|
||||||
|
strV, err := convert.Convert(v, cty.String)
|
||||||
|
if err != nil {
|
||||||
|
diags = diags.Append(tfdiags.Sourceless(
|
||||||
|
tfdiags.Error,
|
||||||
|
"Unsupported value for raw output",
|
||||||
|
fmt.Sprintf(
|
||||||
|
"The -raw option only supports strings, numbers, and boolean values, but output value %q is %s.\n\nUse the -json option for machine-readable representations of output values that have complex types.",
|
||||||
|
name, v.Type().FriendlyName(),
|
||||||
|
),
|
||||||
|
))
|
||||||
|
c.showDiagnostics(diags)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if strV.IsNull() {
|
||||||
|
diags = diags.Append(tfdiags.Sourceless(
|
||||||
|
tfdiags.Error,
|
||||||
|
"Unsupported value for raw output",
|
||||||
|
fmt.Sprintf(
|
||||||
|
"The value for output value %q is null, so -raw mode cannot print it.",
|
||||||
|
name,
|
||||||
|
),
|
||||||
|
))
|
||||||
|
c.showDiagnostics(diags)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if !strV.IsKnown() {
|
||||||
|
// Since we're working with values from the state it would be very
|
||||||
|
// odd to end up in here, but we'll handle it anyway to avoid a
|
||||||
|
// panic in case our rules somehow change in future.
|
||||||
|
diags = diags.Append(tfdiags.Sourceless(
|
||||||
|
tfdiags.Error,
|
||||||
|
"Unsupported value for raw output",
|
||||||
|
fmt.Sprintf(
|
||||||
|
"The value for output value %q won't be known until after a successful terraform apply, so -raw mode cannot print it.",
|
||||||
|
name,
|
||||||
|
),
|
||||||
|
))
|
||||||
|
c.showDiagnostics(diags)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
// If we get out here then we should have a valid string to print.
|
||||||
|
// We're writing it directly to the output here so that a shell caller
|
||||||
|
// will get exactly the value and no extra whitespace.
|
||||||
|
str := strV.AsString()
|
||||||
|
if c.rawPrint != nil {
|
||||||
|
c.rawPrint(str)
|
||||||
|
} else {
|
||||||
|
fmt.Print(str)
|
||||||
|
}
|
||||||
|
default:
|
||||||
result := repl.FormatValue(v, 0)
|
result := repl.FormatValue(v, 0)
|
||||||
c.Ui.Output(result)
|
c.Ui.Output(result)
|
||||||
}
|
}
|
||||||
|
@ -219,8 +289,12 @@ Options:
|
||||||
-no-color If specified, output won't contain any color.
|
-no-color If specified, output won't contain any color.
|
||||||
|
|
||||||
-json If specified, machine readable output will be
|
-json If specified, machine readable output will be
|
||||||
printed in JSON format
|
printed in JSON format.
|
||||||
|
|
||||||
|
-raw For value types that can be automatically
|
||||||
|
converted to a string, will print the raw
|
||||||
|
string directly, rather than a human-oriented
|
||||||
|
representation of the value.
|
||||||
`
|
`
|
||||||
return strings.TrimSpace(helpText)
|
return strings.TrimSpace(helpText)
|
||||||
}
|
}
|
||||||
|
|
|
@ -130,6 +130,92 @@ func TestOutput_json(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestOutput_raw(t *testing.T) {
|
||||||
|
originalState := states.BuildState(func(s *states.SyncState) {
|
||||||
|
s.SetOutputValue(
|
||||||
|
addrs.OutputValue{Name: "str"}.Absolute(addrs.RootModuleInstance),
|
||||||
|
cty.StringVal("bar"),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
s.SetOutputValue(
|
||||||
|
addrs.OutputValue{Name: "multistr"}.Absolute(addrs.RootModuleInstance),
|
||||||
|
cty.StringVal("bar\nbaz"),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
s.SetOutputValue(
|
||||||
|
addrs.OutputValue{Name: "num"}.Absolute(addrs.RootModuleInstance),
|
||||||
|
cty.NumberIntVal(2),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
s.SetOutputValue(
|
||||||
|
addrs.OutputValue{Name: "bool"}.Absolute(addrs.RootModuleInstance),
|
||||||
|
cty.True,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
s.SetOutputValue(
|
||||||
|
addrs.OutputValue{Name: "obj"}.Absolute(addrs.RootModuleInstance),
|
||||||
|
cty.EmptyObjectVal,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
s.SetOutputValue(
|
||||||
|
addrs.OutputValue{Name: "null"}.Absolute(addrs.RootModuleInstance),
|
||||||
|
cty.NullVal(cty.String),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
statePath := testStateFile(t, originalState)
|
||||||
|
|
||||||
|
tests := map[string]struct {
|
||||||
|
WantOutput string
|
||||||
|
WantErr bool
|
||||||
|
}{
|
||||||
|
"str": {WantOutput: "bar"},
|
||||||
|
"multistr": {WantOutput: "bar\nbaz"},
|
||||||
|
"num": {WantOutput: "2"},
|
||||||
|
"bool": {WantOutput: "true"},
|
||||||
|
"obj": {WantErr: true},
|
||||||
|
"null": {WantErr: true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, test := range tests {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
var printed string
|
||||||
|
ui := cli.NewMockUi()
|
||||||
|
c := &OutputCommand{
|
||||||
|
Meta: Meta{
|
||||||
|
testingOverrides: metaOverridesForProvider(testProvider()),
|
||||||
|
Ui: ui,
|
||||||
|
},
|
||||||
|
rawPrint: func(s string) {
|
||||||
|
printed = s
|
||||||
|
},
|
||||||
|
}
|
||||||
|
args := []string{
|
||||||
|
"-state", statePath,
|
||||||
|
"-raw",
|
||||||
|
name,
|
||||||
|
}
|
||||||
|
code := c.Run(args)
|
||||||
|
|
||||||
|
if code != 0 {
|
||||||
|
if !test.WantErr {
|
||||||
|
t.Errorf("unexpected failure\n%s", ui.ErrorWriter.String())
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if test.WantErr {
|
||||||
|
t.Fatalf("succeeded, but want error")
|
||||||
|
}
|
||||||
|
|
||||||
|
if got, want := printed, test.WantOutput; got != want {
|
||||||
|
t.Errorf("wrong result\ngot: %q\nwant: %q", got, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestOutput_emptyOutputs(t *testing.T) {
|
func TestOutput_emptyOutputs(t *testing.T) {
|
||||||
originalState := states.NewState()
|
originalState := states.NewState()
|
||||||
statePath := testStateFile(t, originalState)
|
statePath := testStateFile(t, originalState)
|
||||||
|
|
|
@ -24,6 +24,11 @@ The command-line flags are all optional. The list of available flags are:
|
||||||
* `-json` - If specified, the outputs are formatted as a JSON object, with
|
* `-json` - If specified, the outputs are formatted as a JSON object, with
|
||||||
a key per output. If `NAME` is specified, only the output specified will be
|
a key per output. If `NAME` is specified, only the output specified will be
|
||||||
returned. This can be piped into tools such as `jq` for further processing.
|
returned. This can be piped into tools such as `jq` for further processing.
|
||||||
|
* `-raw` - If specified, Terraform will convert the specified output value to a
|
||||||
|
string and print that string directly to the output, without any special
|
||||||
|
formatting. This can be convenient when working with shell scripts, but
|
||||||
|
it only supports string, number, and boolean values. Use `-json` instead
|
||||||
|
for processing complex data types.
|
||||||
* `-no-color` - If specified, output won't contain any color.
|
* `-no-color` - If specified, output won't contain any color.
|
||||||
* `-state=path` - Path to the state file. Defaults to "terraform.tfstate".
|
* `-state=path` - Path to the state file. Defaults to "terraform.tfstate".
|
||||||
Ignored when [remote state](/docs/state/remote.html) is used.
|
Ignored when [remote state](/docs/state/remote.html) is used.
|
||||||
|
@ -88,21 +93,31 @@ instance_ips = [
|
||||||
## Use in automation
|
## Use in automation
|
||||||
|
|
||||||
The `terraform output` command by default displays in a human-readable format,
|
The `terraform output` command by default displays in a human-readable format,
|
||||||
which can change over time to improve clarity. For use in automation, use
|
which can change over time to improve clarity.
|
||||||
`-json` to output the stable JSON format. You can parse the output using a JSON
|
|
||||||
command-line parser such as [jq](https://stedolan.github.io/jq/).
|
|
||||||
|
|
||||||
For string outputs, you can remove quotes using `jq -r`:
|
For scripting and automation, use `-json` to produce the stable JSON format.
|
||||||
|
You can parse the output using a JSON command-line parser such as
|
||||||
|
[jq](https://stedolan.github.io/jq/):
|
||||||
|
|
||||||
```shellsession
|
```shellsession
|
||||||
$ terraform output -json lb_address | jq -r .
|
$ terraform output -json instance_ips | jq -r '.[0]'
|
||||||
|
54.43.114.12
|
||||||
|
```
|
||||||
|
|
||||||
|
For the common case of directly using a string value in a shell script, you
|
||||||
|
can use `-raw` instead, which will print the string directly with no extra
|
||||||
|
escaping or whitespace.
|
||||||
|
|
||||||
|
```shellsession
|
||||||
|
$ terraform output -raw lb_address
|
||||||
my-app-alb-1657023003.us-east-1.elb.amazonaws.com
|
my-app-alb-1657023003.us-east-1.elb.amazonaws.com
|
||||||
```
|
```
|
||||||
|
|
||||||
To query for a particular value in a list, use `jq` with an index filter. For
|
The `-raw` option works only with values that Terraform can automatically
|
||||||
example, to query for the first instance's IP address:
|
convert to strings. Use `-json` instead, possibly combined with `jq`, to
|
||||||
|
work with complex-typed values such as objects.
|
||||||
|
|
||||||
```shellsession
|
Terraform strings are sequences of Unicode characters rather than raw bytes,
|
||||||
$ terraform output -json instance_ips | jq '.[0]'
|
so the `-raw` output will be UTF-8 encoded when it contains non-ASCII
|
||||||
"54.43.114.12"
|
characters. If you need a different character encoding, use a separate command
|
||||||
```
|
such as `iconv` to transcode Terraform's raw output.
|
||||||
|
|
Loading…
Reference in New Issue