Parse version -json output and fall back to plaintext (#138)

diff --git a/tfexec/version.go b/tfexec/version.go
index 9e05b05..b6496ab 100644
--- a/tfexec/version.go
+++ b/tfexec/version.go
@@ -3,12 +3,14 @@
 import (
 	"bytes"
 	"context"
+	"encoding/json"
 	"fmt"
 	"regexp"
 	"strconv"
 	"strings"
 
 	"github.com/hashicorp/go-version"
+	"github.com/hashicorp/terraform-json"
 )
 
 var (
@@ -37,9 +39,52 @@
 
 // version does not use the locking on the Terraform instance and should probably not be used directly, prefer Version.
 func (tf *Terraform) version(ctx context.Context) (*version.Version, map[string]*version.Version, error) {
-	// TODO: 0.13.0-beta2? and above supports a `-json` on the version command, should add support
-	// for that here and fallback to string parsing
+	versionCmd := tf.buildTerraformCmd(ctx, nil, "version", "-json")
 
+	var outBuf bytes.Buffer
+	versionCmd.Stdout = &outBuf
+
+	err := tf.runTerraformCmd(ctx, versionCmd)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	tfVersion, providerVersions, err := parseJsonVersionOutput(outBuf.Bytes())
+	if err != nil {
+		if _, ok := err.(*json.SyntaxError); ok {
+			return tf.versionFromPlaintext(ctx)
+		}
+	}
+
+	return tfVersion, providerVersions, err
+}
+
+func parseJsonVersionOutput(stdout []byte) (*version.Version, map[string]*version.Version, error) {
+	var out tfjson.VersionOutput
+	err := json.Unmarshal(stdout, &out)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	tfVersion, err := version.NewVersion(out.Version)
+	if err != nil {
+		return nil, nil, fmt.Errorf("unable to parse version %q: %w", out.Version, err)
+	}
+
+	providerVersions := make(map[string]*version.Version, 0)
+	for provider, versionStr := range out.ProviderSelections {
+		v, err := version.NewVersion(versionStr)
+		if err != nil {
+			return nil, nil, fmt.Errorf("unable to parse %q version %q: %w",
+				provider, versionStr, err)
+		}
+		providerVersions[provider] = v
+	}
+
+	return tfVersion, providerVersions, nil
+}
+
+func (tf *Terraform) versionFromPlaintext(ctx context.Context) (*version.Version, map[string]*version.Version, error) {
 	versionCmd := tf.buildTerraformCmd(ctx, nil, "version")
 
 	var outBuf bytes.Buffer
@@ -50,7 +95,7 @@
 		return nil, nil, err
 	}
 
-	tfVersion, providerVersions, err := parseVersionOutput(outBuf.String())
+	tfVersion, providerVersions, err := parsePlaintextVersionOutput(outBuf.String())
 	if err != nil {
 		return nil, nil, fmt.Errorf("unable to parse version: %w", err)
 	}
@@ -65,7 +110,7 @@
 	providerVersionOutputRe = regexp.MustCompile(`(\n\+ provider[\. ](?P<name>\S+) ` + simpleVersionRe + `)`)
 )
 
-func parseVersionOutput(stdout string) (*version.Version, map[string]*version.Version, error) {
+func parsePlaintextVersionOutput(stdout string) (*version.Version, map[string]*version.Version, error) {
 	stdout = strings.TrimSpace(stdout)
 
 	submatches := versionOutputRe.FindStringSubmatch(stdout)
diff --git a/tfexec/version_test.go b/tfexec/version_test.go
index 15405f0..17edb94 100644
--- a/tfexec/version_test.go
+++ b/tfexec/version_test.go
@@ -7,19 +7,20 @@
 	"path/filepath"
 	"testing"
 
+	"github.com/google/go-cmp/cmp"
 	"github.com/hashicorp/go-version"
 	"github.com/hashicorp/terraform-exec/tfinstall"
 )
 
-func TestParseVersionOutput(t *testing.T) {
-	var mustVer = func(s string) *version.Version {
-		v, err := version.NewVersion(s)
-		if err != nil {
-			t.Fatal(err)
-		}
-		return v
+func mustVersion(t *testing.T, s string) *version.Version {
+	v, err := version.NewVersion(s)
+	if err != nil {
+		t.Fatal(err)
 	}
+	return v
+}
 
+func TestParsePlaintextVersionOutput(t *testing.T) {
 	for i, c := range []struct {
 		expectedV         *version.Version
 		expectedProviders map[string]*version.Version
@@ -28,29 +29,29 @@
 	}{
 		// 0.13 tests
 		{
-			mustVer("0.13.0-dev"), nil, `
+			mustVersion(t, "0.13.0-dev"), nil, `
 Terraform v0.13.0-dev`,
 		},
 		{
-			mustVer("0.13.0-dev"), map[string]*version.Version{
-				"registry.terraform.io/hashicorp/null": mustVer("2.1.2"),
-				"registry.terraform.io/paultyng/null":  mustVer("0.1.0"),
+			mustVersion(t, "0.13.0-dev"), map[string]*version.Version{
+				"registry.terraform.io/hashicorp/null": mustVersion(t, "2.1.2"),
+				"registry.terraform.io/paultyng/null":  mustVersion(t, "0.1.0"),
 			}, `
 Terraform v0.13.0-dev
 + provider registry.terraform.io/hashicorp/null v2.1.2
 + provider registry.terraform.io/paultyng/null v0.1.0`,
 		},
 		{
-			mustVer("0.13.0-dev"), nil, `
+			mustVersion(t, "0.13.0-dev"), nil, `
 Terraform v0.13.0-dev
 
 Your version of Terraform is out of date! The latest version
 is 0.13.1. You can update by downloading from https://www.terraform.io/downloads.html`,
 		},
 		{
-			mustVer("0.13.0-dev"), map[string]*version.Version{
-				"registry.terraform.io/hashicorp/null": mustVer("2.1.2"),
-				"registry.terraform.io/paultyng/null":  mustVer("0.1.0"),
+			mustVersion(t, "0.13.0-dev"), map[string]*version.Version{
+				"registry.terraform.io/hashicorp/null": mustVersion(t, "2.1.2"),
+				"registry.terraform.io/paultyng/null":  mustVersion(t, "0.1.0"),
 			}, `
 Terraform v0.13.0-dev
 + provider registry.terraform.io/hashicorp/null v2.1.2
@@ -62,20 +63,20 @@
 
 		// 0.12 tests
 		{
-			mustVer("0.12.26"), nil, `
+			mustVersion(t, "0.12.26"), nil, `
 Terraform v0.12.26
 `,
 		},
 		{
-			mustVer("0.12.26"), map[string]*version.Version{
-				"null": mustVer("2.1.2"),
+			mustVersion(t, "0.12.26"), map[string]*version.Version{
+				"null": mustVersion(t, "2.1.2"),
 			}, `
 Terraform v0.12.26
 + provider.null v2.1.2
 `,
 		},
 		{
-			mustVer("0.12.18"), nil, `
+			mustVersion(t, "0.12.18"), nil, `
 Terraform v0.12.18
 
 Your version of Terraform is out of date! The latest version
@@ -83,8 +84,8 @@
 `,
 		},
 		{
-			mustVer("0.12.18"), map[string]*version.Version{
-				"null": mustVer("2.1.2"),
+			mustVersion(t, "0.12.18"), map[string]*version.Version{
+				"null": mustVersion(t, "2.1.2"),
 			}, `
 Terraform v0.12.18
 + provider.null v2.1.2
@@ -95,7 +96,7 @@
 		},
 	} {
 		t.Run(fmt.Sprintf("%d %s", i, c.expectedV), func(t *testing.T) {
-			actualV, actualProv, err := parseVersionOutput(c.stdout)
+			actualV, actualProv, err := parsePlaintextVersionOutput(c.stdout)
 			if err != nil {
 				t.Fatal(err)
 			}
@@ -117,6 +118,37 @@
 	}
 }
 
+func TestParseJsonVersionOutput(t *testing.T) {
+	testStdout := []byte(`{
+  "terraform_version": "0.15.0-beta1",
+  "platform": "darwin_amd64",
+  "provider_selections": {
+    "registry.terraform.io/hashicorp/aws": "3.31.0",
+    "registry.terraform.io/hashicorp/google": "3.58.0"
+  },
+  "terraform_outdated": false
+}
+`)
+	tfVersion, pvs, err := parseJsonVersionOutput(testStdout)
+	if err != nil {
+		t.Fatal(err)
+	}
+	expectedTfVer := mustVersion(t, "0.15.0-beta1")
+
+	if !expectedTfVer.Equal(tfVersion) {
+		t.Fatalf("version doesn't match (%q != %q)",
+			expectedTfVer.String(), tfVersion.String())
+	}
+
+	expectedPvs := map[string]*version.Version{
+		"registry.terraform.io/hashicorp/aws":    mustVersion(t, "3.31.0"),
+		"registry.terraform.io/hashicorp/google": mustVersion(t, "3.58.0"),
+	}
+	if diff := cmp.Diff(expectedPvs, pvs); diff != "" {
+		t.Fatalf("provider versions don't match: %s", diff)
+	}
+}
+
 func TestVersionInRange(t *testing.T) {
 	for i, c := range []struct {
 		expected bool