Add support for state push/pull
diff --git a/tfexec/state_pull.go b/tfexec/state_pull.go
new file mode 100644
index 0000000..48a97c1
--- /dev/null
+++ b/tfexec/state_pull.go
@@ -0,0 +1,49 @@
+package tfexec
+
+import (
+	"context"
+	"os/exec"
+
+	tfjson "github.com/hashicorp/terraform-json"
+)
+
+type statePullConfig struct {
+	reattachInfo ReattachInfo
+}
+
+var defaultStatePullConfig = statePullConfig{}
+
+func (tf *Terraform) StatePull(ctx context.Context) (*tfjson.State, error) {
+	c := defaultStatePullConfig
+
+	mergeEnv := map[string]string{}
+	if c.reattachInfo != nil {
+		reattachStr, err := c.reattachInfo.marshalString()
+		if err != nil {
+			return nil, err
+		}
+		mergeEnv[reattachEnvVar] = reattachStr
+	}
+
+	cmd := tf.statePullCmd(ctx)
+
+	var ret tfjson.State
+	ret.UseJSONNumber(true)
+	err := tf.runTerraformCmdJSON(ctx, cmd, &ret)
+	if err != nil {
+		return nil, err
+	}
+
+	err = ret.Validate()
+	if err != nil {
+		return nil, err
+	}
+
+	return &ret, nil
+}
+
+func (tf *Terraform) statePullCmd(ctx context.Context) *exec.Cmd {
+	args := []string{"state", "pull"}
+
+	return tf.buildTerraformCmd(ctx, nil, args...)
+}
diff --git a/tfexec/state_pull_test.go b/tfexec/state_pull_test.go
new file mode 100644
index 0000000..48c347a
--- /dev/null
+++ b/tfexec/state_pull_test.go
@@ -0,0 +1,28 @@
+package tfexec
+
+import (
+	"context"
+	"testing"
+
+	"github.com/hashicorp/terraform-exec/tfexec/internal/testutil"
+)
+
+func TestStatePull(t *testing.T) {
+	td := testTempDir(t)
+
+	tf, err := NewTerraform(td, tfVersion(t, testutil.Latest_v1))
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	tf.SetEnv(map[string]string{})
+
+	t.Run("tfstate", func(t *testing.T) {
+		statePullCmd := tf.statePullCmd(context.Background())
+
+		assertCmd(t, []string{
+			"state",
+			"pull",
+		}, nil, statePullCmd)
+	})
+}
diff --git a/tfexec/state_push.go b/tfexec/state_push.go
new file mode 100644
index 0000000..98f50f1
--- /dev/null
+++ b/tfexec/state_push.go
@@ -0,0 +1,65 @@
+package tfexec
+
+import (
+	"context"
+	"os/exec"
+	"strconv"
+)
+
+type statePushConfig struct {
+	force       bool
+	lock        bool
+	lockTimeout string
+}
+
+var defaultStatePushOptions = statePushConfig{
+	lock:        false,
+	lockTimeout: "0s",
+}
+
+// StatePushCmdOption represents options used in the Refresh method.
+type StatePushCmdOption interface {
+	configureStatePush(*statePushConfig)
+}
+
+func (opt *ForceOption) configureStatePush(conf *statePushConfig) {
+	conf.force = opt.force
+}
+
+func (opt *LockOption) configureStatePush(conf *statePushConfig) {
+	conf.lock = opt.lock
+}
+
+func (opt *LockTimeoutOption) configureStatePush(conf *statePushConfig) {
+	conf.lockTimeout = opt.timeout
+}
+
+func (tf *Terraform) StatePush(ctx context.Context, path string, opts ...StatePushCmdOption) error {
+	cmd, err := tf.statePushCmd(ctx, path, opts...)
+	if err != nil {
+		return err
+	}
+	return tf.runTerraformCmd(ctx, cmd)
+}
+
+func (tf *Terraform) statePushCmd(ctx context.Context, path string, opts ...StatePushCmdOption) (*exec.Cmd, error) {
+	c := defaultStatePushOptions
+
+	for _, o := range opts {
+		o.configureStatePush(&c)
+	}
+
+	args := []string{"state", "push"}
+
+	if c.force {
+		args = append(args, "-force")
+	}
+
+	args = append(args, "-lock="+strconv.FormatBool(c.lock))
+
+	if c.lockTimeout != "" {
+		args = append(args, "-lock-timeout="+c.lockTimeout)
+	}
+
+	return tf.buildTerraformCmd(ctx, nil, args...), nil
+}
diff --git a/tfexec/state_push_test.go b/tfexec/state_push_test.go
new file mode 100644
index 0000000..7967718
--- /dev/null
+++ b/tfexec/state_push_test.go
@@ -0,0 +1,48 @@
+package tfexec
+
+import (
+	"context"
+	"testing"
+
+	"github.com/hashicorp/terraform-exec/tfexec/internal/testutil"
+)
+
+func TestStatePushCmd(t *testing.T) {
+	td := testTempDir(t)
+
+	tf, err := NewTerraform(td, tfVersion(t, testutil.Latest_v1))
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	tf.SetEnv(map[string]string{})
+
+	t.Run("defaults", func(t *testing.T) {
+		statePushCmd, err := tf.statePushCmd(context.Background(), "testpath")
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		assertCmd(t, []string{
+			"state",
+			"push",
+			"-lock=false",
+			"-lock-timeout=0s",
+		}, nil, statePushCmd)
+	})
+
+	t.Run("override all defaults", func(t *testing.T) {
+		statePushCmd, err := tf.statePushCmd(context.Background(), "testpath", Force(true), Lock(true), LockTimeout("10s"))
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		assertCmd(t, []string{
+			"state",
+			"push",
+			"-force",
+			"-lock=true",
+			"-lock-timeout=10s",
+		}, nil, statePushCmd)
+	})
+}