Repository: getsops/sops (language: python)

Issue:
Make encrypt, decrypt, rotate, set, and edit proper commands
Right now there are a few explicit commands (`exec-env`, `exec-file`, `publish`, `keyservice`, `groups`, `updatekeys`, `help`), and several implicit ones (`--encrypt`, `--decrypt`, `--rotate`, `--set`, edit), some pseudo-commands which only work with other commands (`--extract` with `--decrypt`, `--rm-*` and `--add-*` with `--rotate`).

This is pretty confusing, both from the `sops --help` output and also from the code (it is not trivial to figure out which of the flags are commands), *and* we have a long list of top-level flags which are only used in *some* of the commands, or even just one (like `--extract`, `--rm-*`, `--add-*`).

I think it would be better long term to migrate all "flag commands" (`--encrypt`, `--decrypt`, `--rotate`, `--set`, and the fallback 'edit') to proper subcommands. I would do this in three steps:
1. Add proper commands (additionally to the existing flag commands).
2. Eventually deprecate the use of global flags outside of the (flag) commands where they are really needed.
3. Eventually deprecate the flag commands themselves.
4. Eventually remove the flag commands, and remove global flags for command-specific options.

1\. should be done soon, 2. better sooner than later (this also makes the situation less confusing by telling users that the option they provided isn't actually used by the command they try to use), and 3. should only happen in a longer time (some years from now), and 4. even further in the future (say, at least 4-5 years?).

What do you think?
Add separate subcommands for encryption, decryption, rotating, editing, and setting values
This is a Work in Progress for exploring how to implement #1333. Right now the new subcommands are basically a stripped-down copy'n'paste of the current "main" command.

Fixes #1333
Move extraction of encryption and rotation options to separate functions
Preparation for a rewrite of https://github.com/getsops/sops/pull/1343, which aims at implementing https://github.com/getsops/sops/issues/1333 (Make encrypt, decrypt, rotate, set, and edit proper commands).

This simplifies the main command handler to a more manageable ~180 lines of code, and heavily reduces the copy'n'paste that would happen in an implementation like https://github.com/getsops/sops/pull/1343.

This is #1387 without #974. Once merged I'll update #1387 to contain a version of #974 rebased to the then current `main` branch.

Make encrypt, decrypt, rotate, set, and edit proper commands
Right now there are a few explicit commands (`exec-env`, `exec-file`, `publish`, `keyservice`, `groups`, `updatekeys`, `help`), and several implicit ones (`--encrypt`, `--decrypt`, `--rotate`, `--set`, edit), some pseudo-commands which only work with other commands (`--extract` with `--decrypt`, `--rm-*` and `--add-*` with `--rotate`).

This is pretty confusing, both from the `sops --help` output and also from the code (it is not trivial to figure out which of the flags are commands), *and* we have a long list of top-level flags which are only used in *some* of the commands, or even just one (like `--extract`, `--rm-*`, `--add-*`).

I think it would be better long term to migrate all "flag commands" (`--encrypt`, `--decrypt`, `--rotate`, `--set`, and the fallback 'edit') to proper subcommands. I would do this in three steps:
1. Add proper commands (additionally to the existing flag commands).
2. Eventually deprecate the use of global flags outside of the (flag) commands where they are really needed.
3. Eventually deprecate the flag commands themselves.
4. Eventually remove the flag commands, and remove global flags for command-specific options.

1\. should be done soon, 2. better sooner than later (this also makes the situation less confusing by telling users that the option they provided isn't actually used by the command they try to use), and 3. should only happen in a longer time (some years from now), and 4. even further in the future (say, at least 4-5 years?).

What do you think?
Move extraction of encryption and rotation options to separate functions
Preparation for a rewrite of https://github.com/getsops/sops/pull/1343, which aims at implementing https://github.com/getsops/sops/issues/1333 (Make encrypt, decrypt, rotate, set, and edit proper commands).

This simplifies the main command handler to a more manageable ~180 lines of code, and heavily reduces the copy'n'paste that would happen in an implementation like https://github.com/getsops/sops/pull/1343.

This is #1387 without #974. Once merged I'll update #1387 to contain a version of #974 rebased to the then current `main` branch.

Improve integration tests: reformat test code, make sure exit codes are always checked



Discussion / hints:

Fix patch:
diff --git a/README.rst b/README.rst
index 3c808f84e..d88c9b804 100644
--- a/README.rst
+++ b/README.rst
@@ -96,12 +96,12 @@ separated, in the **SOPS_PGP_FP** env variable.
 
 Note: you can use both PGP and KMS simultaneously.
 
-Then simply call ``sops`` with a file path as argument. It will handle the
+Then simply call ``sops edit`` with a file path as argument. It will handle the
 encryption/decryption transparently and open the cleartext file in an editor
 
 .. code:: sh
 
-    $ sops mynewtestfile.yaml
+    $ sops edit mynewtestfile.yaml
     mynewtestfile.yaml doesn't exist, creating it.
     please wait while an encryption key is being generated and stored in a secure fashion
     file written to mynewtestfile.yaml
@@ -154,7 +154,7 @@ To decrypt a file in a ``cat`` fashion, use the ``-d`` flag:
 
 .. code:: sh
 
-    $ sops -d mynewtestfile.yaml
+    $ sops decrypt mynewtestfile.yaml
 
 SOPS encrypted files contain the necessary information to decrypt their content.
 All a user of SOPS needs is valid AWS credentials and the necessary
@@ -164,7 +164,7 @@ Given that, the only command a SOPS user needs is:
 
 .. code:: sh
 
-    $ sops <file>
+    $ sops edit <file>
 
 `<file>` will be opened, decrypted, passed to a text editor (vim by default),
 encrypted if modified, and saved back to its original location. All of these
@@ -184,7 +184,7 @@ the example files and pgp key provided with the repository::
     $ git clone https://github.com/getsops/sops.git
     $ cd sops
     $ gpg --import pgp/sops_functional_tests_key.asc
-    $ sops example.yaml
+    $ sops edit example.yaml
 
 This last step will decrypt ``example.yaml`` using the test private key.
 
@@ -200,7 +200,7 @@ the ``--age`` option or the **SOPS_AGE_RECIPIENTS** environment variable:
 
 .. code:: sh
 
-    $ sops --encrypt --age age1yt3tfqlfrwdwx0z0ynwplcr6qxcxfaqycuprpmy89nr83ltx74tqdpszlw test.yaml > test.enc.yaml
+    $ sops encrypt --age age1yt3tfqlfrwdwx0z0ynwplcr6qxcxfaqycuprpmy89nr83ltx74tqdpszlw test.yaml > test.enc.yaml
 
 When decrypting a file with the corresponding identity, SOPS will look for a
 text file name ``keys.txt`` located in a ``sops`` subdirectory of your user
@@ -250,11 +250,11 @@ sdk:
 
 Now you can encrypt a file using::
 
-    $ sops --encrypt --gcp-kms projects/my-project/locations/global/keyRings/sops/cryptoKeys/sops-key test.yaml > test.enc.yaml
+    $ sops encrypt --gcp-kms projects/my-project/locations/global/keyRings/sops/cryptoKeys/sops-key test.yaml > test.enc.yaml
 
 And decrypt it using::
 
-     $ sops --decrypt test.enc.yaml
+     $ sops decrypt test.enc.yaml
 
 Encrypting using Azure Key Vault
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -324,11 +324,11 @@ from the commandline:
 
 Now you can encrypt a file using::
 
-    $ sops --encrypt --azure-kv https://sops.vault.azure.net/keys/sops-key/some-string test.yaml > test.enc.yaml
+    $ sops encrypt --azure-kv https://sops.vault.azure.net/keys/sops-key/some-string test.yaml > test.enc.yaml
 
 And decrypt it using::
 
-    $ sops --decrypt test.enc.yaml
+    $ sops decrypt test.enc.yaml
 
 
 Encrypting and decrypting from other programs
@@ -344,7 +344,7 @@ To decrypt data, you can simply do:
 
 .. code:: sh
 
-	$ cat encrypted-data | sops --decrypt /dev/stdin > decrypted-data
+	$ cat encrypted-data | sops decrypt /dev/stdin > decrypted-data
 
 To control the input and output format, pass ``--input-type`` and ``--output-type`` as appropriate. By default,
 ``sops`` determines the input and output format from the provided filename, which is ``/dev/stdin`` here, and
@@ -354,7 +354,7 @@ For example, to decrypt YAML data and obtain the decrypted result as YAML, use:
 
 .. code:: sh
 
-	$ cat encrypted-data | sops --input-type yaml --output-type yaml --decrypt /dev/stdin > decrypted-data
+	$ cat encrypted-data | sops decrypt --input-type yaml --output-type yaml /dev/stdin > decrypted-data
 
 To encrypt, it is important to note that SOPS also uses the filename to look up the correct creation rule from
 ``.sops.yaml``. Likely ``/dev/stdin`` will not match a creation rule, or only match the fallback rule without
@@ -363,7 +363,7 @@ parameter which allows you to tell SOPS which filename to use to match creation
 
 .. code:: sh
 
-	$ echo 'foo: bar' | sops --filename-override path/filename.sops.yaml --encrypt /dev/stdin > encrypted-data
+	$ echo 'foo: bar' | sops encrypt --filename-override path/filename.sops.yaml /dev/stdin > encrypted-data
 
 SOPS will find a matching creation rule for ``path/filename.sops.yaml`` in ``.sops.yaml`` and use that one to
 encrypt the data from stdin. This filename will also be used to determine the input and output store. As always,
@@ -372,7 +372,7 @@ the input store type can be adjusted by passing ``--input-type``, and the output
 
 .. code:: sh
 
-	$ echo foo=bar | sops --filename-override path/filename.sops.yaml --input-type dotenv --encrypt /dev/stdin > encrypted-data
+	$ echo foo=bar | sops encrypt --filename-override path/filename.sops.yaml --input-type dotenv /dev/stdin > encrypted-data
 
 
 Encrypting using Hashicorp Vault
@@ -423,7 +423,7 @@ To easily deploy Vault locally: (DO NOT DO THIS FOR PRODUCTION!!!)
     $ vault write sops/keys/thirdkey type=chacha20-poly1305
     Success! Data written to: sops/keys/thirdkey
 
-    $ sops --encrypt --hc-vault-transit $VAULT_ADDR/v1/sops/keys/firstkey vault_example.yml
+    $ sops encrypt --hc-vault-transit $VAULT_ADDR/v1/sops/keys/firstkey vault_example.yml
 
     $ cat <<EOF > .sops.yaml
     creation_rules:
@@ -433,7 +433,7 @@ To easily deploy Vault locally: (DO NOT DO THIS FOR PRODUCTION!!!)
           hc_vault_transit_uri: "$VAULT_ADDR/v1/sops/keys/thirdkey"
     EOF
 
-    $ sops --verbose -e prod/raw.yaml > prod/encrypted.yaml
+    $ sops encrypt --verbose prod/raw.yaml > prod/encrypted.yaml
 
 Adding and removing keys
 ~~~~~~~~~~~~~~~~~~~~~~~~
@@ -480,35 +480,33 @@ separated list.
 SOPS will prompt you with the changes to be made. This interactivity can be
 disabled by supplying the ``-y`` flag.
 
-Command Line
-************
+``rotate`` command
+******************
 
-Command line flag ``--add-kms``, ``--add-pgp``, ``--add-gcp-kms``, ``--add-azure-kv``,
-``--rm-kms``, ``--rm-pgp``, ``--rm-gcp-kms`` and ``--rm-azure-kv`` can be used to add
-and remove keys from a file.
-These flags use the comma separated syntax as the ``--kms``, ``--pgp``, ``--gcp-kms``
-and ``--azure-kv`` arguments when creating new files.
+The ``rotate`` command generates a new data encryption key and reencrypt all values
+with the new key. At te same time, the command line flag ``--add-kms``, ``--add-pgp``,
+``--add-gcp-kms``, ``--add-azure-kv``, ``--rm-kms``, ``--rm-pgp``, ``--rm-gcp-kms``
+and ``--rm-azure-kv`` can be used to add and remove keys from a file. These flags use
+the comma separated syntax as the ``--kms``, ``--pgp``, ``--gcp-kms`` and ``--azure-kv``
+arguments when creating new files.
 
-Note that ``-r`` or ``--rotate`` is mandatory in this mode. Not specifying
-rotate will ignore the ``--add-*`` options. Use ``updatekeys`` if you want to
-add a key without rotating the data key.
+Use ``updatekeys`` if you want to add a key without rotating the data key.
 
 .. code:: sh
 
     # add a new pgp key to the file and rotate the data key
-    $ sops -r -i --add-pgp 85D77543B3D624B63CEA9E6DBC17301B491B3F21 example.yaml
+    $ sops rotate -i --add-pgp 85D77543B3D624B63CEA9E6DBC17301B491B3F21 example.yaml
 
     # remove a pgp key from the file and rotate the data key
-    $ sops -r -i --rm-pgp 85D77543B3D624B63CEA9E6DBC17301B491B3F21 example.yaml
+    $ sops rotate -i --rm-pgp 85D77543B3D624B63CEA9E6DBC17301B491B3F21 example.yaml
 
 
 Direct Editing
 **************
 
-Alternatively, invoking ``sops`` with the flag **-s** will display the master keys
+Alternatively, invoking ``sops edit`` with the flag **-s** will display the master keys
 while editing. This method can be used to add or remove ``kms`` or ``pgp`` keys under the
-``sops`` section. Invoking ``sops`` with the **-i** flag will perform an in-place edit
-instead of redirecting output to ``stdout``.
+``sops`` section.
 
 For example, to add a KMS master key to a file, add the following entry while
 editing:
@@ -620,7 +618,7 @@ When creating a new file, you can specify the encryption context in the
 
 .. code:: sh
 
-    $ sops --encryption-context Environment:production,Role:web-server test.dev.yaml
+    $ sops edit --encryption-context Environment:production,Role:web-server test.dev.yaml
 
 The format of the Encrypt Context string is ``<EncryptionContext Key>:<EncryptionContext Value>,<EncryptionContext Key>:<EncryptionContext Value>,...``
 
@@ -651,13 +649,16 @@ Key Rotation
 ~~~~~~~~~~~~
 
 It is recommended to renew the data key on a regular basis. ``sops`` supports key
-rotation via the ``-r`` flag. Invoking it on an existing file causes ``sops`` to
-reencrypt the file with a new data key, which is then encrypted with the various
+rotation via the ``rotate`` command. Invoking it on an existing file causes ``sops``
+to reencrypt the file with a new data key, which is then encrypted with the various
 KMS and PGP master keys defined in the file.
 
+Add the ``-i`` option to write the rotated file back, instead of printing it to
+stdout.
+
 .. code:: sh
 
-    $ sops -r example.yaml
+    $ sops rotate example.yaml
 
 Using .sops.yaml conf to select KMS, PGP and age for new files
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -741,7 +742,7 @@ Creating a new file with the right keys is now as simple as
 
 .. code:: sh
 
-    $ sops <newfile>.prod.yaml
+    $ sops edit <newfile>.prod.yaml
 
 Note that the configuration file is ignored when KMS or PGP parameters are
 passed on the SOPS command line or in environment variables.
@@ -847,7 +848,7 @@ For example:
 
 .. code:: sh
 
-    $ sops --shamir-secret-sharing-threshold 2 example.json
+    $ sops edit --shamir-secret-sharing-threshold 2 example.json
 
 Alternatively, you can configure the Shamir threshold for each creation rule in the ``.sops.yaml`` config
 with ``shamir_threshold``:
@@ -880,7 +881,7 @@ with ``shamir_threshold``:
               - pgp:
                     - fingerprint5
 
-And then run ``sops example.json``.
+And then run ``sops edit example.json``.
 
 The threshold (``shamir_threshold``) is set to 2, so this configuration will require
 master keys from two of the three different key groups in order to decrypt the file.
@@ -888,7 +889,7 @@ You can then decrypt the file the same way as with any other SOPS file:
 
 .. code:: sh
 
-    $ sops -d example.json
+    $ sops decrypt example.json
 
 Key service
 ~~~~~~~~~~~
@@ -928,14 +929,14 @@ service exposed on the unix socket located in ``/tmp/sops.sock``, you can run:
 
 .. code:: sh
 
-    $ sops --keyservice unix:///tmp/sops.sock -d file.yaml`
+    $ sops decrypt --keyservice unix:///tmp/sops.sock file.yaml`
 
 And if you only want to use the key service exposed on the unix socket located
 in ``/tmp/sops.sock`` and not the local key service, you can run:
 
 .. code:: sh
 
-    $ sops --enable-local-keyservice=false --keyservice unix:///tmp/sops.sock -d file.yaml
+    $ sops decrypt --enable-local-keyservice=false --keyservice unix:///tmp/sops.sock file.yaml
 
 Auditing
 ~~~~~~~~
@@ -1002,7 +1003,7 @@ written to disk.
 .. code:: sh
 
     # print secrets to stdout to confirm values
-    $ sops -d out.json
+    $ sops decrypt out.json
     {
             "database_password": "jf48t9wfw094gf4nhdf023r",
             "AWS_ACCESS_KEY_ID": "AKIAIOSFODNN7EXAMPLE",
@@ -1152,7 +1153,7 @@ Below is an example of publishing to Vault (using token auth with a local dev in
 
     $ export VAULT_TOKEN=...
     $ export VAULT_ADDR='http://127.0.0.1:8200'
-    $ sops -d vault/test.yaml
+    $ sops decrypt vault/test.yaml
     example_string: bar
     example_number: 42
     example_map:
@@ -1193,23 +1194,23 @@ extension after encrypting a file. For example:
 
 .. code:: sh
 
-    $ sops -e -i myfile.json
-    $ sops -d myfile.json
+    $ sops encrypt -i myfile.json
+    $ sops decrypt myfile.json
 
 If you want to change the extension of the file once encrypted, you need to provide
 ``sops`` with the ``--input-type`` flag upon decryption. For example:
 
 .. code:: sh
 
-    $ sops -e myfile.json > myfile.json.enc
+    $ sops encrypt myfile.json > myfile.json.enc
 
-    $ sops -d --input-type json myfile.json.enc
+    $ sops decrypt --input-type json myfile.json.enc
 
 When operating on stdin, use the ``--input-type`` and ``--output-type`` flags as follows:
 
 .. code:: sh
 
-    $ cat myfile.json | sops --input-type json --output-type json -d /dev/stdin
+    $ cat myfile.json | sops decrypt --input-type json --output-type json /dev/stdin
 
 JSON and JSON_binary indentation
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -1348,7 +1349,7 @@ The command below creates a new file with a data key encrypted by KMS and PGP.
 
 .. code:: sh
 
-    $ sops --kms "arn:aws:kms:us-west-2:927034868273:key/fe86dd69-4132-404c-ab86-4269956b4500" --pgp C9CAB0AF1165060DB58D6D6B2653B624D620786D /path/to/new/file.yaml
+    $ sops edit --kms "arn:aws:kms:us-west-2:927034868273:key/fe86dd69-4132-404c-ab86-4269956b4500" --pgp C9CAB0AF1165060DB58D6D6B2653B624D620786D /path/to/new/file.yaml
 
 Encrypting an existing file
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -1361,13 +1362,13 @@ encrypt the file, and redirect the output to a destination file.
 
     $ export SOPS_KMS_ARN="arn:aws:kms:us-west-2:927034868273:key/fe86dd69-4132-404c-ab86-4269956b4500"
     $ export SOPS_PGP_FP="C9CAB0AF1165060DB58D6D6B2653B624D620786D"
-    $ sops -e /path/to/existing/file.yaml > /path/to/new/encrypted/file.yaml
+    $ sops encrypt /path/to/existing/file.yaml > /path/to/new/encrypted/file.yaml
 
 Decrypt the file with ``-d``.
 
 .. code:: sh
 
-    $ sops -d /path/to/new/encrypted/file.yaml
+    $ sops decrypt /path/to/new/encrypted/file.yaml
 
 Encrypt or decrypt a file in place
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -1378,9 +1379,9 @@ original file after encrypting or decrypting it.
 .. code:: sh
 
     # file.yaml is in cleartext
-    $ sops -e -i /path/to/existing/file.yaml
+    $ sops encrypt -i /path/to/existing/file.yaml
     # file.yaml is now encrypted
-    $ sops -d -i /path/to/existing/file.yaml
+    $ sops decrypt -i /path/to/existing/file.yaml
     # file.yaml is back in cleartext
 
 Encrypting binary files
@@ -1407,10 +1408,10 @@ In-place encryption/decryption also works on binary files.
     $ sha512sum /tmp/somerandom
     9589bb20280e9d381f7a192000498c994e921b3cdb11d2ef5a986578dc2239a340b25ef30691bac72bdb14028270828dad7e8bd31e274af9828c40d216e60cbe /tmp/somerandom
 
-    $ sops -e -i /tmp/somerandom
+    $ sops encrypt -i /tmp/somerandom
     please wait while a data encryption key is being generated and stored securely
 
-    $ sops -d -i /tmp/somerandom
+    $ sops decrypt -i /tmp/somerandom
 
     $ sha512sum /tmp/somerandom
     9589bb20280e9d381f7a192000498c994e921b3cdb11d2ef5a986578dc2239a340b25ef30691bac72bdb14028270828dad7e8bd31e274af9828c40d216e60cbe /tmp/somerandom
@@ -1424,7 +1425,7 @@ values, like keys, without needing an extra parser.
 
 .. code:: sh
 
-    $ sops -d --extract '["app2"]["key"]' ~/git/svc/sops/example.yaml
+    $ sops decrypt --extract '["app2"]["key"]' ~/git/svc/sops/example.yaml
     -----BEGIN RSA PRIVATE KEY-----
     MIIBPAIBAAJBAPTMNIyHuZtpLYc7VsHQtwOkWYobkUblmHWRmbXzlAX6K8tMf3Wf
     ImcbNkqAKnELzFAPSBeEMhrBN0PyOC9lYlMCAwEAAQJBALXD4sjuBn1E7Y9aGiMz
@@ -1441,19 +1442,19 @@ them.
 
 .. code:: sh
 
-    $ sops -d --extract '["an_array"][1]' ~/git/svc/sops/example.yaml
+    $ sops decrypt --extract '["an_array"][1]' ~/git/svc/sops/example.yaml
     secretuser2
 
 Set a sub-part in a document tree
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 SOPS can set a specific part of a YAML or JSON document, by providing
-the path and value in the ``--set`` command line flag. This is useful to
-set specific values, like keys, without needing an editor.
+the path and value in the ``set`` command. This is useful to set specific
+values, like keys, without needing an editor.
 
 .. code:: sh
 
-    $ sops --set '["app2"]["key"] "app2keystringvalue"'  ~/git/svc/sops/example.yaml
+    $ sops set ~/git/svc/sops/example.yaml '["app2"]["key"]' '"app2keystringvalue"'
 
 The tree path syntax uses regular python dictionary syntax, without the
 variable name. Set to keys by naming them, and array elements by
@@ -1461,13 +1462,13 @@ numbering them.
 
 .. code:: sh
 
-    $ sops --set '["an_array"][1] "secretuser2"' ~/git/svc/sops/example.yaml
+    $ sops set ~/git/svc/sops/example.yaml '["an_array"][1]' '"secretuser2"'
 
 The value must be formatted as json.
 
 .. code:: sh
 
-    $ sops --set '["an_array"][1] {"uid1":null,"uid2":1000,"uid3":["bob"]}' ~/git/svc/sops/example.yaml
+    $ sops set ~/git/svc/sops/example.yaml '["an_array"][1]' '{"uid1":null,"uid2":1000,"uid3":["bob"]}'
 
 Showing diffs in cleartext in git
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -1488,11 +1489,11 @@ to a SOPS command in the git configuration file of the repository.
 
 .. code:: sh
 
-    $ git config diff.sopsdiffer.textconv "sops -d"
+    $ git config diff.sopsdiffer.textconv "sops decrypt"
 
     $ grep -A 1 sopsdiffer .git/config
     [diff "sopsdiffer"]
-        textconv = "sops -d"
+        textconv = "sops decrypt"
 
 With this in place, calls to ``git diff`` will decrypt both previous and current
 versions of the target file prior to displaying the diff. And it even works with
@@ -1527,7 +1528,7 @@ keys that match the supplied regular expression.  For example, this command:
 
 .. code:: sh
 
-    $ sops --encrypt --encrypted-regex '^(data|stringData)$' k8s-secrets.yaml
+    $ sops encrypt --encrypted-regex '^(data|stringData)$' k8s-secrets.yaml
 
 will encrypt the values under the ``data`` and ``stringData`` keys in a YAML file
 containing kubernetes secrets.  It will not encrypt other values that help you to
@@ -1539,7 +1540,7 @@ that match the supplied regular expression. For example, this command:
 
 .. code:: sh
 
-    $ sops --encrypt --unencrypted-regex '^(description|metadata)$' k8s-secrets.yaml
+    $ sops encrypt --unencrypted-regex '^(description|metadata)$' k8s-secrets.yaml
 
 will not encrypt the values under the ``description`` and ``metadata`` keys in a YAML file
 containing kubernetes secrets, while encrypting everything else.
@@ -1742,7 +1743,7 @@ when creating a new file:
 
 .. code:: sh
 
-    $ sops --pgp "E60892BB9BD89A69F759A1A0A3D652173B763E8F,84050F1D61AF7C230A12217687DF65059EF093D3,85D77543B3D624B63CEA9E6DBC17301B491B3F21" mynewfile.yaml
+    $ sops edit --pgp "E60892BB9BD89A69F759A1A0A3D652173B763E8F,84050F1D61AF7C230A12217687DF65059EF093D3,85D77543B3D624B63CEA9E6DBC17301B491B3F21" mynewfile.yaml
 
 Threat Model
 ------------
diff --git a/cmd/sops/main.go b/cmd/sops/main.go
index 0138abda8..220e73c85 100644
--- a/cmd/sops/main.go
+++ b/cmd/sops/main.go
@@ -128,7 +128,7 @@ func main() {
 
    The -p, -k, --gcp-kms, --hc-vault-transit, and --azure-kv flags are only
    used to encrypt new documents. Editing or decrypting existing documents
-   can be done with "sops file" or "sops -d file" respectively. The KMS and
+   can be done with "sops file" or "sops decrypt file" respectively. The KMS and
    PGP keys listed in the encrypted documents are used then. To manage master
    keys in existing documents, use the "add-{kms,pgp,gcp-kms,azure-kv,hc-vault-transit}"
    and "rm-{kms,pgp,gcp-kms,azure-kv,hc-vault-transit}" flags with --rotate
@@ -622,6 +622,682 @@ func main() {
 				return nil
 			},
 		},
+		{
+			Name:      "decrypt",
+			Usage:     "decrypt a file, and output the results to stdout",
+			ArgsUsage: `file`,
+			Flags: append([]cli.Flag{
+				cli.BoolFlag{
+					Name:  "in-place, i",
+					Usage: "write output back to the same file instead of stdout",
+				},
+				cli.StringFlag{
+					Name:  "extract",
+					Usage: "extract a specific key or branch from the input document. Example: --extract '[\"somekey\"][0]'",
+				},
+				cli.StringFlag{
+					Name:  "output",
+					Usage: "Save the output after decryption to the file specified",
+				},
+				cli.StringFlag{
+					Name:  "input-type",
+					Usage: "currently json, yaml, dotenv and binary are supported. If not set, sops will use the file's extension to determine the type",
+				},
+				cli.StringFlag{
+					Name:  "output-type",
+					Usage: "currently json, yaml, dotenv and binary are supported. If not set, sops will use the input file's extension to determine the output format",
+				},
+				cli.BoolFlag{
+					Name:  "ignore-mac",
+					Usage: "ignore Message Authentication Code during decryption",
+				},
+				cli.StringFlag{
+					Name:  "filename-override",
+					Usage: "Use this filename instead of the provided argument for loading configuration, and for determining input type and output type",
+				},
+				cli.StringFlag{
+					Name:   "decryption-order",
+					Usage:  "comma separated list of decryption key types",
+					EnvVar: "SOPS_DECRYPTION_ORDER",
+				},
+			}, keyserviceFlags...),
+			Action: func(c *cli.Context) error {
+				if c.Bool("verbose") {
+					logging.SetLevel(logrus.DebugLevel)
+				}
+				if c.NArg() < 1 {
+					return common.NewExitError("Error: no file specified", codes.NoFileSpecified)
+				}
+				warnMoreThanOnePositionalArgument(c)
+				if c.Bool("in-place") && c.String("output") != "" {
+					return common.NewExitError("Error: cannot operate on both --output and --in-place", codes.ErrorConflictingParameters)
+				}
+				fileName, err := filepath.Abs(c.Args()[0])
+				if err != nil {
+					return toExitError(err)
+				}
+				if _, err := os.Stat(fileName); os.IsNotExist(err) {
+					return common.NewExitError("Error: cannot operate on non-existent file", codes.NoFileSpecified)
+				}
+				fileNameOverride := c.String("filename-override")
+				if fileNameOverride == "" {
+					fileNameOverride = fileName
+				}
+
+				inputStore := inputStore(c, fileNameOverride)
+				outputStore := outputStore(c, fileNameOverride)
+				svcs := keyservices(c)
+
+				order, err := decryptionOrder(c.String("decryption-order"))
+				if err != nil {
+					return toExitError(err)
+				}
+
+				var extract []interface{}
+				extract, err = parseTreePath(c.String("extract"))
+				if err != nil {
+					return common.NewExitError(fmt.Errorf("error parsing --extract path: %s", err), codes.InvalidTreePathFormat)
+				}
+				output, err := decrypt(decryptOpts{
+					OutputStore:     outputStore,
+					InputStore:      inputStore,
+					InputPath:       fileName,
+					Cipher:          aes.NewCipher(),
+					Extract:         extract,
+					KeyServices:     svcs,
+					DecryptionOrder: order,
+					IgnoreMAC:       c.Bool("ignore-mac"),
+				})
+				if err != nil {
+					return toExitError(err)
+				}
+
+				// We open the file *after* the operations on the tree have been
+				// executed to avoid truncating it when there's errors
+				if c.Bool("in-place") {
+					file, err := os.Create(fileName)
+					if err != nil {
+						return common.NewExitError(fmt.Sprintf("Could not open in-place file for writing: %s", err), codes.CouldNotWriteOutputFile)
+					}
+					defer file.Close()
+					_, err = file.Write(output)
+					if err != nil {
+						return toExitError(err)
+					}
+					log.Info("File written successfully")
+					return nil
+				}
+
+				outputFile := os.Stdout
+				if c.String("output") != "" {
+					file, err := os.Create(c.String("output"))
+					if err != nil {
+						return common.NewExitError(fmt.Sprintf("Could not open output file for writing: %s", err), codes.CouldNotWriteOutputFile)
+					}
+					defer file.Close()
+					outputFile = file
+				}
+				_, err = outputFile.Write(output)
+				return toExitError(err)
+			},
+		},
+		{
+			Name:      "encrypt",
+			Usage:     "encrypt a file, and output the results to stdout",
+			ArgsUsage: `file`,
+			Flags: append([]cli.Flag{
+				cli.BoolFlag{
+					Name:  "in-place, i",
+					Usage: "write output back to the same file instead of stdout",
+				},
+				cli.StringFlag{
+					Name:  "output",
+					Usage: "Save the output after decryption to the file specified",
+				},
+				cli.StringFlag{
+					Name:   "kms, k",
+					Usage:  "comma separated list of KMS ARNs",
+					EnvVar: "SOPS_KMS_ARN",
+				},
+				cli.StringFlag{
+					Name:  "aws-profile",
+					Usage: "The AWS profile to use for requests to AWS",
+				},
+				cli.StringFlag{
+					Name:   "gcp-kms",
+					Usage:  "comma separated list of GCP KMS resource IDs",
+					EnvVar: "SOPS_GCP_KMS_IDS",
+				},
+				cli.StringFlag{
+					Name:   "azure-kv",
+					Usage:  "comma separated list of Azure Key Vault URLs",
+					EnvVar: "SOPS_AZURE_KEYVAULT_URLS",
+				},
+				cli.StringFlag{
+					Name:   "hc-vault-transit",
+					Usage:  "comma separated list of vault's key URI (e.g. 'https://vault.example.org:8200/v1/transit/keys/dev')",
+					EnvVar: "SOPS_VAULT_URIS",
+				},
+				cli.StringFlag{
+					Name:   "pgp, p",
+					Usage:  "comma separated list of PGP fingerprints",
+					EnvVar: "SOPS_PGP_FP",
+				},
+				cli.StringFlag{
+					Name:   "age, a",
+					Usage:  "comma separated list of age recipients",
+					EnvVar: "SOPS_AGE_RECIPIENTS",
+				},
+				cli.StringFlag{
+					Name:  "input-type",
+					Usage: "currently json, yaml, dotenv and binary are supported. If not set, sops will use the file's extension to determine the type",
+				},
+				cli.StringFlag{
+					Name:  "output-type",
+					Usage: "currently json, yaml, dotenv and binary are supported. If not set, sops will use the input file's extension to determine the output format",
+				},
+				cli.StringFlag{
+					Name:  "unencrypted-suffix",
+					Usage: "override the unencrypted key suffix.",
+				},
+				cli.StringFlag{
+					Name:  "encrypted-suffix",
+					Usage: "override the encrypted key suffix. When empty, all keys will be encrypted, unless otherwise marked with unencrypted-suffix.",
+				},
+				cli.StringFlag{
+					Name:  "unencrypted-regex",
+					Usage: "set the unencrypted key regex. When specified, only keys matching the regex will be left unencrypted.",
+				},
+				cli.StringFlag{
+					Name:  "encrypted-regex",
+					Usage: "set the encrypted key regex. When specified, only keys matching the regex will be encrypted.",
+				},
+				cli.StringFlag{
+					Name:  "encryption-context",
+					Usage: "comma separated list of KMS encryption context key:value pairs",
+				},
+				cli.IntFlag{
+					Name:  "shamir-secret-sharing-threshold",
+					Usage: "the number of master keys required to retrieve the data key with shamir",
+				},
+				cli.StringFlag{
+					Name:  "filename-override",
+					Usage: "Use this filename instead of the provided argument for loading configuration, and for determining input type and output type",
+				},
+			}, keyserviceFlags...),
+			Action: func(c *cli.Context) error {
+				if c.Bool("verbose") {
+					logging.SetLevel(logrus.DebugLevel)
+				}
+				if c.NArg() < 1 {
+					return common.NewExitError("Error: no file specified", codes.NoFileSpecified)
+				}
+				warnMoreThanOnePositionalArgument(c)
+				if c.Bool("in-place") && c.String("output") != "" {
+					return common.NewExitError("Error: cannot operate on both --output and --in-place", codes.ErrorConflictingParameters)
+				}
+				fileName, err := filepath.Abs(c.Args()[0])
+				if err != nil {
+					return toExitError(err)
+				}
+				if _, err := os.Stat(fileName); os.IsNotExist(err) {
+					return common.NewExitError("Error: cannot operate on non-existent file", codes.NoFileSpecified)
+				}
+				fileNameOverride := c.String("filename-override")
+				if fileNameOverride == "" {
+					fileNameOverride = fileName
+				}
+
+				inputStore := inputStore(c, fileNameOverride)
+				outputStore := outputStore(c, fileNameOverride)
+				svcs := keyservices(c)
+
+				encConfig, err := getEncryptConfig(c, fileNameOverride)
+				if err != nil {
+					return toExitError(err)
+				}
+				output, err := encrypt(encryptOpts{
+					OutputStore:   outputStore,
+					InputStore:    inputStore,
+					InputPath:     fileName,
+					Cipher:        aes.NewCipher(),
+					KeyServices:   svcs,
+					encryptConfig: encConfig,
+				})
+
+				if err != nil {
+					return toExitError(err)
+				}
+
+				// We open the file *after* the operations on the tree have been
+				// executed to avoid truncating it when there's errors
+				if c.Bool("in-place") {
+					file, err := os.Create(fileName)
+					if err != nil {
+						return common.NewExitError(fmt.Sprintf("Could not open in-place file for writing: %s", err), codes.CouldNotWriteOutputFile)
+					}
+					defer file.Close()
+					_, err = file.Write(output)
+					if err != nil {
+						return toExitError(err)
+					}
+					log.Info("File written successfully")
+					return nil
+				}
+
+				outputFile := os.Stdout
+				if c.String("output") != "" {
+					file, err := os.Create(c.String("output"))
+					if err != nil {
+						return common.NewExitError(fmt.Sprintf("Could not open output file for writing: %s", err), codes.CouldNotWriteOutputFile)
+					}
+					defer file.Close()
+					outputFile = file
+				}
+				_, err = outputFile.Write(output)
+				return toExitError(err)
+			},
+		},
+		{
+			Name:      "rotate",
+			Usage:     "generate a new data encryption key and reencrypt all values with the new key",
+			ArgsUsage: `file`,
+			Flags: append([]cli.Flag{
+				cli.BoolFlag{
+					Name:  "in-place, i",
+					Usage: "write output back to the same file instead of stdout",
+				},
+				cli.StringFlag{
+					Name:  "output",
+					Usage: "Save the output after decryption to the file specified",
+				},
+				cli.StringFlag{
+					Name:  "input-type",
+					Usage: "currently json, yaml, dotenv and binary are supported. If not set, sops will use the file's extension to determine the type",
+				},
+				cli.StringFlag{
+					Name:  "output-type",
+					Usage: "currently json, yaml, dotenv and binary are supported. If not set, sops will use the input file's extension to determine the output format",
+				},
+				cli.StringFlag{
+					Name:  "encryption-context",
+					Usage: "comma separated list of KMS encryption context key:value pairs",
+				},
+				cli.StringFlag{
+					Name:  "add-gcp-kms",
+					Usage: "add the provided comma-separated list of GCP KMS key resource IDs to the list of master keys on the given file",
+				},
+				cli.StringFlag{
+					Name:  "rm-gcp-kms",
+					Usage: "remove the provided comma-separated list of GCP KMS key resource IDs from the list of master keys on the given file",
+				},
+				cli.StringFlag{
+					Name:  "add-azure-kv",
+					Usage: "add the provided comma-separated list of Azure Key Vault key URLs to the list of master keys on the given file",
+				},
+				cli.StringFlag{
+					Name:  "rm-azure-kv",
+					Usage: "remove the provided comma-separated list of Azure Key Vault key URLs from the list of master keys on the given file",
+				},
+				cli.StringFlag{
+					Name:  "add-kms",
+					Usage: "add the provided comma-separated list of KMS ARNs to the list of master keys on the given file",
+				},
+				cli.StringFlag{
+					Name:  "rm-kms",
+					Usage: "remove the provided comma-separated list of KMS ARNs from the list of master keys on the given file",
+				},
+				cli.StringFlag{
+					Name:  "add-hc-vault-transit",
+					Usage: "add the provided comma-separated list of Vault's URI key to the list of master keys on the given file ( eg. https://vault.example.org:8200/v1/transit/keys/dev)",
+				},
+				cli.StringFlag{
+					Name:  "rm-hc-vault-transit",
+					Usage: "remove the provided comma-separated list of Vault's URI key from the list of master keys on the given file ( eg. https://vault.example.org:8200/v1/transit/keys/dev)",
+				},
+				cli.StringFlag{
+					Name:  "add-age",
+					Usage: "add the provided comma-separated list of age recipients fingerprints to the list of master keys on the given file",
+				},
+				cli.StringFlag{
+					Name:  "rm-age",
+					Usage: "remove the provided comma-separated list of age recipients from the list of master keys on the given file",
+				},
+				cli.StringFlag{
+					Name:  "add-pgp",
+					Usage: "add the provided comma-separated list of PGP fingerprints to the list of master keys on the given file",
+				},
+				cli.StringFlag{
+					Name:  "rm-pgp",
+					Usage: "remove the provided comma-separated list of PGP fingerprints from the list of master keys on the given file",
+				},
+				cli.StringFlag{
+					Name:  "filename-override",
+					Usage: "Use this filename instead of the provided argument for loading configuration, and for determining input type and output type",
+				},
+				cli.StringFlag{
+					Name:   "decryption-order",
+					Usage:  "comma separated list of decryption key types",
+					EnvVar: "SOPS_DECRYPTION_ORDER",
+				},
+			}, keyserviceFlags...),
+			Action: func(c *cli.Context) error {
+				if c.Bool("verbose") {
+					logging.SetLevel(logrus.DebugLevel)
+				}
+				if c.NArg() < 1 {
+					return common.NewExitError("Error: no file specified", codes.NoFileSpecified)
+				}
+				warnMoreThanOnePositionalArgument(c)
+				if c.Bool("in-place") && c.String("output") != "" {
+					return common.NewExitError("Error: cannot operate on both --output and --in-place", codes.ErrorConflictingParameters)
+				}
+				fileName, err := filepath.Abs(c.Args()[0])
+				if err != nil {
+					return toExitError(err)
+				}
+				if _, err := os.Stat(fileName); os.IsNotExist(err) {
+					if c.String("add-kms") != "" || c.String("add-pgp") != "" || c.String("add-gcp-kms") != "" || c.String("add-hc-vault-transit") != "" || c.String("add-azure-kv") != "" || c.String("add-age") != "" ||
+						c.String("rm-kms") != "" || c.String("rm-pgp") != "" || c.String("rm-gcp-kms") != "" || c.String("rm-hc-vault-transit") != "" || c.String("rm-azure-kv") != "" || c.String("rm-age") != "" {
+						return common.NewExitError("Error: cannot add or remove keys on non-existent files, use the `edit` subcommand instead.", codes.CannotChangeKeysFromNonExistentFile)
+					}
+				}
+				fileNameOverride := c.String("filename-override")
+				if fileNameOverride == "" {
+					fileNameOverride = fileName
+				}
+
+				inputStore := inputStore(c, fileNameOverride)
+				outputStore := outputStore(c, fileNameOverride)
+				svcs := keyservices(c)
+
+				order, err := decryptionOrder(c.String("decryption-order"))
+				if err != nil {
+					return toExitError(err)
+				}
+
+				rotateOpts, err := getRotateOpts(c, fileName, inputStore, outputStore, svcs, order)
+				if err != nil {
+					return toExitError(err)
+				}
+				output, err := rotate(rotateOpts)
+				if err != nil {
+					return toExitError(err)
+				}
+
+				// We open the file *after* the operations on the tree have been
+				// executed to avoid truncating it when there's errors
+				if c.Bool("in-place") {
+					file, err := os.Create(fileName)
+					if err != nil {
+						return common.NewExitError(fmt.Sprintf("Could not open in-place file for writing: %s", err), codes.CouldNotWriteOutputFile)
+					}
+					defer file.Close()
+					_, err = file.Write(output)
+					if err != nil {
+						return toExitError(err)
+					}
+					log.Info("File written successfully")
+					return nil
+				}
+
+				outputFile := os.Stdout
+				if c.String("output") != "" {
+					file, err := os.Create(c.String("output"))
+					if err != nil {
+						return common.NewExitError(fmt.Sprintf("Could not open output file for writing: %s", err), codes.CouldNotWriteOutputFile)
+					}
+					defer file.Close()
+					outputFile = file
+				}
+				_, err = outputFile.Write(output)
+				return toExitError(err)
+			},
+		},
+		{
+			Name:      "edit",
+			Usage:     "edit an encrypted file",
+			ArgsUsage: `file`,
+			Flags: append([]cli.Flag{
+				cli.StringFlag{
+					Name:   "kms, k",
+					Usage:  "comma separated list of KMS ARNs",
+					EnvVar: "SOPS_KMS_ARN",
+				},
+				cli.StringFlag{
+					Name:  "aws-profile",
+					Usage: "The AWS profile to use for requests to AWS",
+				},
+				cli.StringFlag{
+					Name:   "gcp-kms",
+					Usage:  "comma separated list of GCP KMS resource IDs",
+					EnvVar: "SOPS_GCP_KMS_IDS",
+				},
+				cli.StringFlag{
+					Name:   "azure-kv",
+					Usage:  "comma separated list of Azure Key Vault URLs",
+					EnvVar: "SOPS_AZURE_KEYVAULT_URLS",
+				},
+				cli.StringFlag{
+					Name:   "hc-vault-transit",
+					Usage:  "comma separated list of vault's key URI (e.g. 'https://vault.example.org:8200/v1/transit/keys/dev')",
+					EnvVar: "SOPS_VAULT_URIS",
+				},
+				cli.StringFlag{
+					Name:   "pgp, p",
+					Usage:  "comma separated list of PGP fingerprints",
+					EnvVar: "SOPS_PGP_FP",
+				},
+				cli.StringFlag{
+					Name:   "age, a",
+					Usage:  "comma separated list of age recipients",
+					EnvVar: "SOPS_AGE_RECIPIENTS",
+				},
+				cli.StringFlag{
+					Name:  "input-type",
+					Usage: "currently json, yaml, dotenv and binary are supported. If not set, sops will use the file's extension to determine the type",
+				},
+				cli.StringFlag{
+					Name:  "output-type",
+					Usage: "currently json, yaml, dotenv and binary are supported. If not set, sops will use the input file's extension to determine the output format",
+				},
+				cli.StringFlag{
+					Name:  "unencrypted-suffix",
+					Usage: "override the unencrypted key suffix.",
+				},
+				cli.StringFlag{
+					Name:  "encrypted-suffix",
+					Usage: "override the encrypted key suffix. When empty, all keys will be encrypted, unless otherwise marked with unencrypted-suffix.",
+				},
+				cli.StringFlag{
+					Name:  "unencrypted-regex",
+					Usage: "set the unencrypted key regex. When specified, only keys matching the regex will be left unencrypted.",
+				},
+				cli.StringFlag{
+					Name:  "encrypted-regex",
+					Usage: "set the encrypted key regex. When specified, only keys matching the regex will be encrypted.",
+				},
+				cli.StringFlag{
+					Name:  "encryption-context",
+					Usage: "comma separated list of KMS encryption context key:value pairs",
+				},
+				cli.IntFlag{
+					Name:  "shamir-secret-sharing-threshold",
+					Usage: "the number of master keys required to retrieve the data key with shamir",
+				},
+				cli.BoolFlag{
+					Name:  "show-master-keys, s",
+					Usage: "display master encryption keys in the file during editing",
+				},
+				cli.BoolFlag{
+					Name:  "ignore-mac",
+					Usage: "ignore Message Authentication Code during decryption",
+				},
+				cli.StringFlag{
+					Name:   "decryption-order",
+					Usage:  "comma separated list of decryption key types",
+					EnvVar: "SOPS_DECRYPTION_ORDER",
+				},
+			}, keyserviceFlags...),
+			Action: func(c *cli.Context) error {
+				if c.Bool("verbose") {
+					logging.SetLevel(logrus.DebugLevel)
+				}
+				if c.NArg() < 1 {
+					return common.NewExitError("Error: no file specified", codes.NoFileSpecified)
+				}
+				warnMoreThanOnePositionalArgument(c)
+				fileName, err := filepath.Abs(c.Args()[0])
+				if err != nil {
+					return toExitError(err)
+				}
+				if _, err := os.Stat(fileName); os.IsNotExist(err) {
+					return common.NewExitError("Error: cannot operate on non-existent file", codes.NoFileSpecified)
+				}
+
+				inputStore := inputStore(c, fileName)
+				outputStore := outputStore(c, fileName)
+				svcs := keyservices(c)
+
+				order, err := decryptionOrder(c.String("decryption-order"))
+				if err != nil {
+					return toExitError(err)
+				}
+				var output []byte
+				_, statErr := os.Stat(fileName)
+				fileExists := statErr == nil
+				opts := editOpts{
+					OutputStore:     outputStore,
+					InputStore:      inputStore,
+					InputPath:       fileName,
+					Cipher:          aes.NewCipher(),
+					KeyServices:     svcs,
+					DecryptionOrder: order,
+					IgnoreMAC:       c.Bool("ignore-mac"),
+					ShowMasterKeys:  c.Bool("show-master-keys"),
+				}
+				if fileExists {
+					output, err = edit(opts)
+					if err != nil {
+						return toExitError(err)
+					}
+				} else {
+					// File doesn't exist, edit the example file instead
+					encConfig, err := getEncryptConfig(c, fileName)
+					if err != nil {
+						return toExitError(err)
+					}
+					output, err = editExample(editExampleOpts{
+						editOpts:      opts,
+						encryptConfig: encConfig,
+					})
+					if err != nil {
+						return toExitError(err)
+					}
+				}
+
+				// We open the file *after* the operations on the tree have been
+				// executed to avoid truncating it when there's errors
+				file, err := os.Create(fileName)
+				if err != nil {
+					return common.NewExitError(fmt.Sprintf("Could not open in-place file for writing: %s", err), codes.CouldNotWriteOutputFile)
+				}
+				defer file.Close()
+				_, err = file.Write(output)
+				if err != nil {
+					return toExitError(err)
+				}
+				log.Info("File written successfully")
+				return nil
+			},
+		},
+		{
+			Name:      "set",
+			Usage:     `set a specific key or branch in the input document. value must be a json encoded string. eg. '/path/to/file ["somekey"][0] {"somevalue":true}'`,
+			ArgsUsage: `file index value`,
+			Flags: append([]cli.Flag{
+				cli.StringFlag{
+					Name:  "input-type",
+					Usage: "currently json, yaml, dotenv and binary are supported. If not set, sops will use the file's extension to determine the type",
+				},
+				cli.StringFlag{
+					Name:  "output-type",
+					Usage: "currently json, yaml, dotenv and binary are supported. If not set, sops will use the input file's extension to determine the output format",
+				},
+				cli.IntFlag{
+					Name:  "shamir-secret-sharing-threshold",
+					Usage: "the number of master keys required to retrieve the data key with shamir",
+				},
+				cli.BoolFlag{
+					Name:  "ignore-mac",
+					Usage: "ignore Message Authentication Code during decryption",
+				},
+				cli.StringFlag{
+					Name:   "decryption-order",
+					Usage:  "comma separated list of decryption key types",
+					EnvVar: "SOPS_DECRYPTION_ORDER",
+				},
+			}, keyserviceFlags...),
+			Action: func(c *cli.Context) error {
+				if c.Bool("verbose") {
+					logging.SetLevel(logrus.DebugLevel)
+				}
+				if c.NArg() != 3 {
+					return common.NewExitError("Error: no file specified, or index and value are missing", codes.NoFileSpecified)
+				}
+				fileName, err := filepath.Abs(c.Args()[0])
+				if err != nil {
+					return toExitError(err)
+				}
+
+				inputStore := inputStore(c, fileName)
+				outputStore := outputStore(c, fileName)
+				svcs := keyservices(c)
+
+				path, err := parseTreePath(c.Args()[1])
+				if err != nil {
+					return common.NewExitError("Invalid set index format", codes.ErrorInvalidSetFormat)
+				}
+
+				value, err := jsonValueToTreeInsertableValue(c.Args()[2])
+				if err != nil {
+					return toExitError(err)
+				}
+
+				order, err := decryptionOrder(c.String("decryption-order"))
+				if err != nil {
+					return toExitError(err)
+				}
+				output, err := set(setOpts{
+					OutputStore:     outputStore,
+					InputStore:      inputStore,
+					InputPath:       fileName,
+					Cipher:          aes.NewCipher(),
+					KeyServices:     svcs,
+					DecryptionOrder: order,
+					IgnoreMAC:       c.Bool("ignore-mac"),
+					Value:           value,
+					TreePath:        path,
+				})
+				if err != nil {
+					return toExitError(err)
+				}
+
+				// We open the file *after* the operations on the tree have been
+				// executed to avoid truncating it when there's errors
+				file, err := os.Create(fileName)
+				if err != nil {
+					return common.NewExitError(fmt.Sprintf("Could not open in-place file for writing: %s", err), codes.CouldNotWriteOutputFile)
+				}
+				defer file.Close()
+				_, err = file.Write(output)
+				if err != nil {
+					return toExitError(err)
+				}
+				log.Info("File written successfully")
+				return nil
+			},
+		},
 	}
 	app.Flags = append([]cli.Flag{
 		cli.BoolFlag{
diff --git a/examples/all_in_one/README.rst b/examples/all_in_one/README.rst
index d6b66343b..d82937ea5 100644
--- a/examples/all_in_one/README.rst
+++ b/examples/all_in_one/README.rst
@@ -42,7 +42,7 @@ In both development and production, we will be storing the secrets file unencryp
 
 As peace of mind, think about this:
 
-- Unencrypted on disk is fine because if the attacker ever gains access to the server, then they can run ``sops --decrypt`` as well.
+- Unencrypted on disk is fine because if the attacker ever gains access to the server, then they can run ``sops decrypt`` as well.
 
 Files
 -----
@@ -69,7 +69,7 @@ For testing in a public CI, we can copy ``secret.enc.json`` to ``secret.json``.
 
 ..
 
-    For convenience, we can run ``CONFIG_COPY_ONLY=TRUE bin/decrypt-config.sh`` which will use ``cp`` rather than ``sops --decrypt``.
+    For convenience, we can run ``CONFIG_COPY_ONLY=TRUE bin/decrypt-config.sh`` which will use ``cp`` rather than ``sops decrypt``.
 
 For testing in a private CI where we need private information, see the `Production instructions <#production>`_.
 
diff --git a/examples/all_in_one/bin/decrypt-config.sh b/examples/all_in_one/bin/decrypt-config.sh
index e53965bec..aa00444c4 100755
--- a/examples/all_in_one/bin/decrypt-config.sh
+++ b/examples/all_in_one/bin/decrypt-config.sh
@@ -17,6 +17,6 @@ for file in $secret_files; do
     cp "$src_file" "$target_file"
   # Otherwise, decrypt it
   else
-    sops --decrypt "$src_file" > "$target_file"
+    sops decrypt "$src_file" > "$target_file"
   fi
 done
diff --git a/examples/per_file/README.rst b/examples/per_file/README.rst
index bc198a208..8540fb74e 100644
--- a/examples/per_file/README.rst
+++ b/examples/per_file/README.rst
@@ -47,7 +47,7 @@ In both development and production, we will be storing the secrets file unencryp
 
 As peace of mind, think about this:
 
-- Unencrypted on disk is fine because if the attacker ever gains access to the server, then they can run ``sops --decrypt`` as well.
+- Unencrypted on disk is fine because if the attacker ever gains access to the server, then they can run ``sops decrypt`` as well.
 
 Files
 -----
@@ -78,7 +78,7 @@ For testing in a public CI, we can copy ``config.enc`` to ``config``. The secret
 
 ..
 
-    For convenience, we can run ``CONFIG_COPY_ONLY=TRUE bin/decrypt-config.sh`` which will use ``ln -s`` rather than ``sops --decrypt``.
+    For convenience, we can run ``CONFIG_COPY_ONLY=TRUE bin/decrypt-config.sh`` which will use ``ln -s`` rather than ``sops decrypt``.
 
 For testing in a private CI where we need private information, see the `Production instructions <#production>`_.
 
diff --git a/examples/per_file/bin/decrypt-config.sh b/examples/per_file/bin/decrypt-config.sh
index 43197e41f..77a0f5e2e 100755
--- a/examples/per_file/bin/decrypt-config.sh
+++ b/examples/per_file/bin/decrypt-config.sh
@@ -25,7 +25,7 @@ for src_file in config.enc/*; do
   # If the file is our secret, then decrypt it
   if echo "$src_filename" | grep -E "${secret_ext}$" &&
       test "$CONFIG_COPY_ONLY" != "TRUE"; then
-    sops --decrypt "$src_file" > "$target_file"
+    sops decrypt "$src_file" > "$target_file"
   # Otherwise, symlink to the original file
   else
     ln -s "../$src_file" "$target_file"


Test patch:
diff --git a/functional-tests/src/lib.rs b/functional-tests/src/lib.rs
index e37e2dfa2..a8ddbc060 100644
--- a/functional-tests/src/lib.rs
+++ b/functional-tests/src/lib.rs
@@ -60,7 +60,7 @@ mod tests {
 }",
         );
         let output = Command::new(SOPS_BINARY_PATH)
-            .arg("-e")
+            .arg("encrypt")
             .arg(file_path.clone())
             .output()
             .expect("Error running sops");
@@ -92,7 +92,7 @@ mod tests {
         );
         assert!(
             Command::new(SOPS_BINARY_PATH)
-                .arg("-e")
+                .arg("encrypt")
                 .arg("-i")
                 .arg(file_path.clone())
                 .output()
@@ -127,7 +127,7 @@ mod tests {
         );
         assert!(
             Command::new(SOPS_BINARY_PATH)
-                .arg("-e")
+                .arg("encrypt")
                 .arg("-i")
                 .arg(file_path.clone())
                 .output()
@@ -162,7 +162,7 @@ mod tests {
         );
         assert!(
             Command::new(SOPS_BINARY_PATH)
-                .arg("-e")
+                .arg("encrypt")
                 .arg("-i")
                 .arg(file_path.clone())
                 .output()
@@ -201,9 +201,9 @@ mod tests {
         );
 
         let output = Command::new(SOPS_BINARY_PATH)
+            .arg("encrypt")
             .arg("--kms")
             .arg(kms_arn)
-            .arg("-e")
             .arg(file_path.clone())
             .output()
             .expect("Error running sops");
@@ -231,7 +231,7 @@ mod tests {
 bar: baz",
         );
         let output = Command::new(SOPS_BINARY_PATH)
-            .arg("-e")
+            .arg("encrypt")
             .arg(file_path.clone())
             .output()
             .expect("Error running sops");
@@ -257,7 +257,7 @@ bar: baz",
             prepare_temp_file("test_set_update.json", r#"{"a": 2, "b": "ba"}"#.as_bytes());
         assert!(
             Command::new(SOPS_BINARY_PATH)
-                .arg("-e")
+                .arg("encrypt")
                 .arg("-i")
                 .arg(file_path.clone())
                 .output()
@@ -267,9 +267,10 @@ bar: baz",
             "sops didn't exit successfully"
         );
         let output = Command::new(SOPS_BINARY_PATH)
-            .arg("--set")
-            .arg(r#"["a"] {"aa": "aaa"}"#)
+            .arg("set")
             .arg(file_path.clone())
+            .arg(r#"["a"]"#)
+            .arg(r#"{"aa": "aaa"}"#)
             .output()
             .expect("Error running sops");
         assert!(output.status.success(), "sops didn't exit successfully");
@@ -300,7 +301,7 @@ bar: baz",
             prepare_temp_file("test_set_insert.json", r#"{"a": 2, "b": "ba"}"#.as_bytes());
         assert!(
             Command::new(SOPS_BINARY_PATH)
-                .arg("-e")
+                .arg("encrypt")
                 .arg("-i")
                 .arg(file_path.clone())
                 .output()
@@ -310,9 +311,10 @@ bar: baz",
             "sops didn't exit successfully"
         );
         let output = Command::new(SOPS_BINARY_PATH)
-            .arg("--set")
-            .arg(r#"["c"] {"cc": "ccc"}"#)
+            .arg("set")
             .arg(file_path.clone())
+            .arg(r#"["c"]"#)
+            .arg(r#"{"cc": "ccc"}"#)
             .output()
             .expect("Error running sops");
         assert!(output.status.success(), "sops didn't exit successfully");
@@ -347,7 +349,7 @@ b: ba"#
         );
         assert!(
             Command::new(SOPS_BINARY_PATH)
-                .arg("-e")
+                .arg("encrypt")
                 .arg("-i")
                 .arg(file_path.clone())
                 .output()
@@ -357,9 +359,10 @@ b: ba"#
             "sops didn't exit successfully"
         );
         let output = Command::new(SOPS_BINARY_PATH)
-            .arg("--set")
-            .arg(r#"["a"] {"aa": "aaa"}"#)
+            .arg("set")
             .arg(file_path.clone())
+            .arg(r#"["a"]"#)
+            .arg(r#"{"aa": "aaa"}"#)
             .output()
             .expect("Error running sops");
         assert!(output.status.success(), "sops didn't exit successfully");
@@ -394,7 +397,7 @@ b: ba"#
         );
         assert!(
             Command::new(SOPS_BINARY_PATH)
-                .arg("-e")
+                .arg("encrypt")
                 .arg("-i")
                 .arg(file_path.clone())
                 .output()
@@ -404,9 +407,10 @@ b: ba"#
             "sops didn't exit successfully"
         );
         let output = Command::new(SOPS_BINARY_PATH)
-            .arg("--set")
-            .arg(r#"["c"] {"cc": "ccc"}"#)
+            .arg("set")
             .arg(file_path.clone())
+            .arg(r#"["c"]"#)
+            .arg(r#"{"cc": "ccc"}"#)
             .output()
             .expect("Error running sops");
         assert!(output.status.success(), "sops didn't exit successfully");
@@ -436,6 +440,63 @@ b: ba"#
         let file_path = prepare_temp_file(
             "test_set_string.yaml",
             r#"a: 2
+b: ba"#
+                .as_bytes(),
+        );
+        assert!(
+            Command::new(SOPS_BINARY_PATH)
+                .arg("encrypt")
+                .arg("-i")
+                .arg(file_path.clone())
+                .output()
+                .expect("Error running sops")
+                .status
+                .success(),
+            "sops didn't exit successfully"
+        );
+        assert!(
+            Command::new(SOPS_BINARY_PATH)
+                .arg("set")
+                .arg(file_path.clone())
+                .arg(r#"["a"]"#)
+                .arg(r#""aaa""#)
+                .output()
+                .expect("Error running sops")
+                .status
+                .success(),
+            "sops didn't exit successfully"
+        );
+        let output = Command::new(SOPS_BINARY_PATH)
+            .arg("decrypt")
+            .arg("-i")
+            .arg(file_path.clone())
+            .output()
+            .expect("Error running sops");
+        assert!(output.status.success(), "sops didn't exit successfully");
+        println!(
+            "stdout: {}, stderr: {}",
+            String::from_utf8_lossy(&output.stdout),
+            String::from_utf8_lossy(&output.stderr)
+        );
+        let mut s = String::new();
+        File::open(file_path)
+            .unwrap()
+            .read_to_string(&mut s)
+            .unwrap();
+        let data: Value = serde_yaml::from_str(&s).expect("Error parsing sops's YAML output");
+        if let Value::Mapping(data) = data {
+            let a = data.get(&Value::String("a".to_owned())).unwrap();
+            assert_eq!(a, &Value::String("aaa".to_owned()));
+        } else {
+            panic!("Output JSON does not have the expected structure");
+        }
+    }
+
+    #[test]
+    fn set_yaml_file_string_compat() {
+        let file_path = prepare_temp_file(
+            "test_set_string_compat.yaml",
+            r#"a: 2
 b: ba"#
                 .as_bytes(),
         );
@@ -496,7 +557,7 @@ b: ba"#
         );
         assert!(
             !Command::new(SOPS_BINARY_PATH)
-                .arg("-d")
+                .arg("decrypt")
                 .arg(file_path.clone())
                 .output()
                 .expect("Error running sops")
@@ -507,7 +568,7 @@ b: ba"#
 
         assert!(
             Command::new(SOPS_BINARY_PATH)
-                .arg("-d")
+                .arg("decrypt")
                 .arg("--ignore-mac")
                 .arg(file_path.clone())
                 .output()
@@ -522,7 +583,7 @@ b: ba"#
     fn encrypt_comments() {
         let file_path = "res/comments.yaml";
         let output = Command::new(SOPS_BINARY_PATH)
-            .arg("-e")
+            .arg("encrypt")
             .arg(file_path.clone())
             .output()
             .expect("Error running sops");
@@ -541,7 +602,7 @@ b: ba"#
     fn encrypt_comments_list() {
         let file_path = "res/comments_list.yaml";
         let output = Command::new(SOPS_BINARY_PATH)
-            .arg("-e")
+            .arg("encrypt")
             .arg(file_path.clone())
             .output()
             .expect("Error running sops");
@@ -560,7 +621,7 @@ b: ba"#
     fn decrypt_comments() {
         let file_path = "res/comments.enc.yaml";
         let output = Command::new(SOPS_BINARY_PATH)
-            .arg("-d")
+            .arg("decrypt")
             .arg(file_path.clone())
             .output()
             .expect("Error running sops");
@@ -579,7 +640,7 @@ b: ba"#
     fn decrypt_comments_unencrypted_comments() {
         let file_path = "res/comments_unencrypted_comments.yaml";
         let output = Command::new(SOPS_BINARY_PATH)
-            .arg("-d")
+            .arg("decrypt")
             .arg(file_path.clone())
             .output()
             .expect("Error running sops");
@@ -599,8 +660,8 @@ b: ba"#
         // The .sops.yaml file ensures this file is encrypted with two key groups, each with one GPG key
         let file_path = prepare_temp_file("test_roundtrip_keygroups.yaml", "a: secret".as_bytes());
         let output = Command::new(SOPS_BINARY_PATH)
+            .arg("encrypt")
             .arg("-i")
-            .arg("-e")
             .arg(file_path.clone())
             .output()
             .expect("Error running sops");
@@ -609,7 +670,7 @@ b: ba"#
             "SOPS failed to encrypt a file with Shamir Secret Sharing"
         );
         let output = Command::new(SOPS_BINARY_PATH)
-            .arg("-d")
+            .arg("decrypt")
             .arg(file_path.clone())
             .output()
             .expect("Error running sops");
@@ -629,8 +690,8 @@ b: ba"#
             "a: secret".as_bytes(),
         );
         let output = Command::new(SOPS_BINARY_PATH)
+            .arg("encrypt")
             .arg("-i")
-            .arg("-e")
             .arg(file_path.clone())
             .output()
             .expect("Error running sops");
@@ -639,7 +700,7 @@ b: ba"#
             "SOPS failed to encrypt a file with Shamir Secret Sharing"
         );
         let output = Command::new(SOPS_BINARY_PATH)
-            .arg("-d")
+            .arg("decrypt")
             .arg(file_path.clone())
             .output()
             .expect("Error running sops");
@@ -656,7 +717,7 @@ b: ba"#
             include_bytes!("../res/multiple_keys.yaml"),
         );
         let output = Command::new(SOPS_BINARY_PATH)
-            .arg("-d")
+            .arg("decrypt")
             .arg(file_path.clone())
             .output()
             .expect("Error running sops");
@@ -673,16 +734,16 @@ b: ba"#
             "multiline: |\n  multi\n  line".as_bytes(),
         );
         let output = Command::new(SOPS_BINARY_PATH)
+            .arg("encrypt")
             .arg("-i")
-            .arg("-e")
             .arg(file_path.clone())
             .output()
             .expect("Error running sops");
         assert!(output.status.success(), "SOPS failed to encrypt a file");
         let output = Command::new(SOPS_BINARY_PATH)
+            .arg("decrypt")
             .arg("--extract")
             .arg("[\"multiline\"]")
-            .arg("-d")
             .arg(file_path.clone())
             .output()
             .expect("Error running sops");
@@ -696,8 +757,8 @@ b: ba"#
         let data = b"\"\"{}this_is_binary_data";
         let file_path = prepare_temp_file("test.binary", data);
         let output = Command::new(SOPS_BINARY_PATH)
+            .arg("encrypt")
             .arg("-i")
-            .arg("-e")
             .arg(file_path.clone())
             .output()
             .expect("Error running sops");
@@ -706,7 +767,7 @@ b: ba"#
             "SOPS failed to encrypt a binary file"
         );
         let output = Command::new(SOPS_BINARY_PATH)
-            .arg("-d")
+            .arg("decrypt")
             .arg(file_path.clone())
             .output()
             .expect("Error running sops");
@@ -732,19 +793,19 @@ b: ba"#
         );
 
         let output = Command::new(SOPS_BINARY_PATH)
+            .arg("encrypt")
             .arg("--kms")
             .arg(kms_arn)
             .arg("--encryption-context")
             .arg("foo:bar,one:two")
             .arg("-i")
-            .arg("-e")
             .arg(file_path.clone())
             .output()
             .expect("Error running sops");
         assert!(output.status.success(), "sops didn't exit successfully");
 
         let output = Command::new(SOPS_BINARY_PATH)
-            .arg("-d")
+            .arg("decrypt")
             .arg(file_path.clone())
             .output()
             .expect("Error running sops");
@@ -761,9 +822,9 @@ b: ba"#
         let input_path = prepare_temp_file("test_output_flag.binary", b"foo");
         let output_path = Path::join(TMP_DIR.path(), "output_flag.txt");
         let output = Command::new(SOPS_BINARY_PATH)
+            .arg("encrypt")
             .arg("--output")
             .arg(&output_path)
-            .arg("-e")
             .arg(input_path.clone())
             .output()
             .expect("Error running sops");