Repository: twpayne/chezmoi
Pull request #271 (2019-04-28 11:52:24)

Improve documentation and configuration consistency


Patch:
diff --git a/cmd/doctor.go b/cmd/doctor.go
index f2118e338..1a3bc8e2a 100644
--- a/cmd/doctor.go
+++ b/cmd/doctor.go
@@ -127,32 +127,32 @@ func (c *Config) runDoctorCmd(fs vfs.FS, args []string) error {
 		vcsCommandCheck,
 		&doctorBinaryCheck{
 			name:          "1Password CLI",
-			binaryName:    c.Onepassword.Op,
+			binaryName:    c.Onepassword.Command,
 			versionArgs:   []string{"--version"},
 			versionRegexp: regexp.MustCompile(`^(\d+\.\d+\.\d+)`),
 		},
 		&doctorBinaryCheck{
 			name:          "Bitwarden CLI",
-			binaryName:    c.Bitwarden.Bw,
+			binaryName:    c.Bitwarden.Command,
 			versionArgs:   []string{"--version"},
 			versionRegexp: regexp.MustCompile(`^(\d+\.\d+\.\d+)`),
 		},
 		&doctorBinaryCheck{
 			name:          "LastPass CLI",
-			binaryName:    c.Lastpass.Lpass,
+			binaryName:    c.Lastpass.Command,
 			versionArgs:   lastpassVersionArgs,
 			versionRegexp: lastpassVersionRegexp,
 			minVersion:    &lastpassMinVersion,
 		},
 		&doctorBinaryCheck{
 			name:          "pass CLI",
-			binaryName:    c.Pass.Pass,
+			binaryName:    c.Pass.Command,
 			versionArgs:   []string{"version"},
 			versionRegexp: regexp.MustCompile(`(?m)=\s*v(\d+\.\d+\.\d+)`),
 		},
 		&doctorBinaryCheck{
 			name:          "Vault CLI",
-			binaryName:    c.Vault.Vault,
+			binaryName:    c.Vault.Command,
 			versionArgs:   []string{"version"},
 			versionRegexp: regexp.MustCompile(`^Vault\s+v(\d+\.\d+\.\d+)`),
 		},
diff --git a/cmd/helps.gen.go b/cmd/helps.gen.go
index 08f243177..82439d373 100644
--- a/cmd/helps.gen.go
+++ b/cmd/helps.gen.go
@@ -136,7 +136,7 @@ var helps = map[string]help{
 			"Dump the target state in JSON format. If no targets are specified, then the\n" +
 			"entire target state. The \"dump\" command accepts additional arguments:\n" +
 			"\n" +
-			"\"-f\" / \"--format\" format\n" +
+			"\"-f\", \"--format\" format\n" +
 			"\n" +
 			"Print the target state in the given format. The accepted formats are \"json\"\n" +
 			"(JSON) and \"yaml\" (YAML).\n",
@@ -158,7 +158,7 @@ var helps = map[string]help{
 			"Print the difference between the target state and the actual state after\n" +
 			"editing.\n" +
 			"\n" +
-			"\"-p\" / \"--prompt\"\n" +
+			"\"-p\", \"--prompt\"\n" +
 			"\n" +
 			"Prompt before applying each target.\n",
 		example: "" +
@@ -194,7 +194,7 @@ var helps = map[string]help{
 			"\n" +
 			"Set the destination (in the source state) where the archive will be imported.\n" +
 			"\n" +
-			"\"-x\" / \"--exact\"\n" +
+			"\"-x\", \"--exact\"\n" +
 			"\n" +
 			"Set the \"exact\" attribute on all imported directories.\n" +
 			"\n" +
@@ -239,7 +239,26 @@ var helps = map[string]help{
 	},
 	"secret": {
 		long: "" +
-			"Interact with a secret manager. See the \"Secret managers\" section for details.\n",
+			"Run a secret manager's CLI, passing any extra arguments to the secret manager's\n" +
+			"CLI. This is primarily for verifying chezmoi's integration with your secret\n" +
+			"manager. Normally you would use template functions to retrieve secrets. Note\n" +
+			"that if you want to pass flags to the secret manager's CLU you will need to\n" +
+			"separate them with \"--\" to prevent chezmoi from interpreting them.\n" +
+			"\n" +
+			"To get a full list of available commands run:\n" +
+			"\n" +
+			"  chezmoi secret help\n" +
+			"  ",
+		example: "" +
+			"  chezmoi secret bitwarden list items\n" +
+			"  chezmoi secret keyring set --service service --user user\n" +
+			"  chezmoi secret keyring get --service service --user user\n" +
+			"  chezmoi secret lastpass ls\n" +
+			"  chezmoi secret lastpass -- show --format=json id\n" +
+			"  chezmoi secret onepassword list items\n" +
+			"  chezmoi secret onepassword get item id\n" +
+			"  chezmoi secret pass show id\n" +
+			"  chezmoi secret vault -- kv get -format=json id",
 	},
 	"source": {
 		long: "" +
diff --git a/cmd/secretbitwarden.go b/cmd/secretbitwarden.go
index c2871e93b..2578ca8b5 100644
--- a/cmd/secretbitwarden.go
+++ b/cmd/secretbitwarden.go
@@ -19,20 +19,20 @@ var bitwardenCmd = &cobra.Command{
 }
 
 type bitwardenCmdConfig struct {
-	Bw string
+	Command string
 }
 
 var bitwardenCache = make(map[string]interface{})
 
 func init() {
-	config.Bitwarden.Bw = "bw"
+	config.Bitwarden.Command = "bw"
 	config.addTemplateFunc("bitwarden", config.bitwardenFunc)
 
 	secretCmd.AddCommand(bitwardenCmd)
 }
 
 func (c *Config) runBitwardenCmd(fs vfs.FS, args []string) error {
-	return c.exec(append([]string{c.Bitwarden.Bw}, args...))
+	return c.exec(append([]string{c.Bitwarden.Command}, args...))
 }
 
 func (c *Config) bitwardenFunc(args ...string) interface{} {
@@ -40,7 +40,7 @@ func (c *Config) bitwardenFunc(args ...string) interface{} {
 	if data, ok := bitwardenCache[key]; ok {
 		return data
 	}
-	name := c.Bitwarden.Bw
+	name := c.Bitwarden.Command
 	args = append([]string{"get"}, args...)
 	if c.Verbose {
 		fmt.Printf("%s %s\n", name, strings.Join(args, " "))
diff --git a/cmd/secretgeneric.go b/cmd/secretgeneric.go
index 86f79bb47..8d9ebbc05 100644
--- a/cmd/secretgeneric.go
+++ b/cmd/secretgeneric.go
@@ -1,6 +1,7 @@
 package cmd
 
 import (
+	"bytes"
 	"encoding/json"
 	"fmt"
 	"os/exec"
@@ -22,7 +23,10 @@ type genericSecretCmdConfig struct {
 	Command string
 }
 
-var genericSecretCache = make(map[string][]byte)
+var (
+	secretCache     = make(map[string][]byte)
+	secretJSONCache = make(map[string]interface{})
+)
 
 func init() {
 	config.addTemplateFunc("secret", config.secretFunc)
@@ -36,10 +40,9 @@ func (c *Config) runGenericSecretCmd(fs vfs.FS, args []string) error {
 }
 
 func (c *Config) secretFunc(args ...string) interface{} {
-	// FIXME factor out common functionality with secretJSONFunc
 	key := strings.Join(args, "\x00")
-	if output, ok := genericSecretCache[key]; ok {
-		return output
+	if value, ok := secretCache[key]; ok {
+		return value
 	}
 	name := c.GenericSecret.Command
 	if c.Verbose {
@@ -49,15 +52,15 @@ func (c *Config) secretFunc(args ...string) interface{} {
 	if err != nil {
 		chezmoi.ReturnTemplateFuncError(fmt.Errorf("secret: %s %s: %v\n%s", name, strings.Join(args, " "), err, output))
 	}
-	genericSecretCache[key] = output
-	return strings.TrimSpace(string(output))
+	value := bytes.TrimSpace(output)
+	secretCache[key] = value
+	return value
 }
 
 func (c *Config) secretJSONFunc(args ...string) interface{} {
-	// FIXME factor out common functionality with secretFunc
 	key := strings.Join(args, "\x00")
-	if output, ok := genericSecretCache[key]; ok {
-		return output
+	if value, ok := secretJSONCache[key]; ok {
+		return value
 	}
 	name := c.GenericSecret.Command
 	if c.Verbose {
@@ -67,10 +70,10 @@ func (c *Config) secretJSONFunc(args ...string) interface{} {
 	if err != nil {
 		chezmoi.ReturnTemplateFuncError(fmt.Errorf("secretJSON: %s %s: %v\n%s", name, strings.Join(args, " "), err, output))
 	}
-	var data interface{}
-	if err := json.Unmarshal(output, &data); err != nil {
+	var value interface{}
+	if err := json.Unmarshal(output, &value); err != nil {
 		chezmoi.ReturnTemplateFuncError(fmt.Errorf("secretJSON: %s %s: %v\n%s", name, strings.Join(args, " "), err, output))
 	}
-	genericSecretCache[key] = output
-	return data
+	secretJSONCache[key] = value
+	return value
 }
diff --git a/cmd/secretlastpass.go b/cmd/secretlastpass.go
index d2d55eac6..4d0d09dc0 100644
--- a/cmd/secretlastpass.go
+++ b/cmd/secretlastpass.go
@@ -35,25 +35,25 @@ var (
 )
 
 type lastpassCmdConfig struct {
-	Lpass            string
+	Command          string
 	versionCheckOnce sync.Once
 }
 
 var lastPassCache = make(map[string]interface{})
 
 func init() {
-	config.Lastpass.Lpass = "lpass"
+	config.Lastpass.Command = "lpass"
 	config.addTemplateFunc("lastpass", config.lastpassFunc)
 
 	secretCmd.AddCommand(lastpassCmd)
 }
 
 func (c *Config) runLastpassCmd(fs vfs.FS, args []string) error {
-	return c.exec(append([]string{c.Lastpass.Lpass}, args...))
+	return c.exec(append([]string{c.Lastpass.Command}, args...))
 }
 
 func (c *Config) lastpassOutput(args ...string) ([]byte, error) {
-	name := c.Lastpass.Lpass
+	name := c.Lastpass.Command
 	if c.Verbose {
 		fmt.Printf("%s %s\n", name, strings.Join(args, " "))
 	}
diff --git a/cmd/secretonepassword.go b/cmd/secretonepassword.go
index 7b6776160..48f65c421 100644
--- a/cmd/secretonepassword.go
+++ b/cmd/secretonepassword.go
@@ -19,27 +19,27 @@ var onepasswordCmd = &cobra.Command{
 }
 
 type onepasswordCmdConfig struct {
-	Op string
+	Command string
 }
 
 var onepasswordCache = make(map[string]interface{})
 
 func init() {
-	config.Onepassword.Op = "op"
+	config.Onepassword.Command = "op"
 	config.addTemplateFunc("onepassword", config.onepasswordFunc)
 
 	secretCmd.AddCommand(onepasswordCmd)
 }
 
 func (c *Config) runOnepasswordCmd(fs vfs.FS, args []string) error {
-	return c.exec(append([]string{c.Onepassword.Op}, args...))
+	return c.exec(append([]string{c.Onepassword.Command}, args...))
 }
 
 func (c *Config) onepasswordFunc(item string) interface{} {
 	if data, ok := onepasswordCache[item]; ok {
 		return data
 	}
-	name := c.Onepassword.Op
+	name := c.Onepassword.Command
 	args := []string{"get", "item", item}
 	if c.Verbose {
 		fmt.Printf("%s %s\n", name, strings.Join(args, " "))
diff --git a/cmd/secretpass.go b/cmd/secretpass.go
index d6ea1ed84..eda64c03b 100644
--- a/cmd/secretpass.go
+++ b/cmd/secretpass.go
@@ -19,7 +19,7 @@ var passCmd = &cobra.Command{
 }
 
 type passCmdConfig struct {
-	Pass string
+	Command string
 }
 
 var passCache = make(map[string]string)
@@ -27,19 +27,19 @@ var passCache = make(map[string]string)
 func init() {
 	secretCmd.AddCommand(passCmd)
 
-	config.Pass.Pass = "pass"
+	config.Pass.Command = "pass"
 	config.addTemplateFunc("pass", config.passFunc)
 }
 
 func (c *Config) runSecretPassCmd(fs vfs.FS, args []string) error {
-	return c.exec(append([]string{c.Pass.Pass}, args...))
+	return c.exec(append([]string{c.Pass.Command}, args...))
 }
 
 func (c *Config) passFunc(id string) string {
 	if s, ok := passCache[id]; ok {
 		return s
 	}
-	name := c.Pass.Pass
+	name := c.Pass.Command
 	args := []string{"show", id}
 	if c.Verbose {
 		fmt.Printf("%s %s\n", name, strings.Join(args, " "))
diff --git a/cmd/secretvault.go b/cmd/secretvault.go
index f0b154b33..4ba9d9073 100644
--- a/cmd/secretvault.go
+++ b/cmd/secretvault.go
@@ -19,27 +19,27 @@ var vaultCmd = &cobra.Command{
 }
 
 type vaultCmdConfig struct {
-	Vault string
+	Command string
 }
 
 var vaultCache = make(map[string]interface{})
 
 func init() {
-	config.Vault.Vault = "vault"
+	config.Vault.Command = "vault"
 	config.addTemplateFunc("vault", config.vaultFunc)
 
 	secretCmd.AddCommand(vaultCmd)
 }
 
 func (c *Config) runVaultCmd(fs vfs.FS, args []string) error {
-	return c.exec(append([]string{c.Vault.Vault}, args...))
+	return c.exec(append([]string{c.Vault.Command}, args...))
 }
 
 func (c *Config) vaultFunc(key string) interface{} {
 	if data, ok := vaultCache[key]; ok {
 		return data
 	}
-	name := c.Vault.Vault
+	name := c.Vault.Command
 	args := []string{"kv", "get", "-format=json", key}
 	if c.Verbose {
 		fmt.Printf("%s %s\n", name, strings.Join(args, " "))
diff --git a/docs/REFERENCE.md b/docs/REFERENCE.md
index 047c3544a..1b4310d46 100644
--- a/docs/REFERENCE.md
+++ b/docs/REFERENCE.md
@@ -17,73 +17,44 @@ Manage your dotfiles securely across multiple machines.
 * [Attributes](#attributes)
 * [Commands](#commands)
   * [`add` *targets*](#add-targets)
-    * [`-e`, `--empty`](#-e---empty)
-    * [`-x`, `--exact`](#-x---exact)
-    * [`-p`, `--prompt`](#-p---prompt)
-    * [`-r`, `--recursive`](#-r---recursive)
-    * [`-T`, `--template`](#-t---template)
-    * [`add` examples](#add-examples)
   * [`apply` [*targets*]](#apply-targets)
-    * [`apply` examples](#apply-examples)
   * [`archive`](#archive)
-    * [`archive` examples](#archive-examples)
-  * [`cat` [targets]](#cat-targets)
-    * [`cat` examples](#cat-examples)
+  * [`cat` targets](#cat-targets)
   * [`cd`](#cd)
-    * [`cd` examples](#cd-examples)
   * [`chattr` *attributes* *targets*](#chattr-attributes-targets)
-    * [`chattr` examples](#chattr-examples)
   * [`completion` *shell*](#completion-shell)
-    * [`completion` examples](#completion-examples)
   * [`data`](#data)
-    * [`-f`, `--format` *format*](#-f---format-format)
-    * [`data` examples](#data-examples)
-  * [`diff` *targets*](#diff-targets)
-    * [`diff` examples](#diff-examples)
+  * [`diff` [*targets*]](#diff-targets)
   * [`doctor`](#doctor)
-    * [`doctor` examples](#doctor-examples)
-  * [`dump` *targets*](#dump-targets)
-    * [`-f` / `--format` *format*](#-f----format-format)
-    * [`dump` examples](#dump-examples)
+  * [`dump` [*targets*]](#dump-targets)
   * [`edit` *targets*](#edit-targets)
-    * [`-a`, `--apply`](#-a---apply)
-    * [`-d`, `--diff`](#-d---diff)
-    * [`-p` / `--prompt`](#-p----prompt)
-    * [`edit` examples](#edit-examples)
   * [`edit-config`](#edit-config)
-    * [`edit-config` examples](#edit-config-examples)
   * [`forget` *targets*](#forget-targets)
-    * [`forget` examples](#forget-examples)
   * [`help` *command*](#help-command)
   * [`init` [*repo*]](#init-repo)
-    * [`init` examples](#init-examples)
   * [`import` *filename*](#import-filename)
-    * [`--destination` *directory*](#--destination-directory)
-    * [`-x` / `--exact`](#-x----exact)
-    * [`-r`, `--remove-destination`](#-r---remove-destination)
-    * [`--strip-components` *n*](#--strip-components-n)
-    * [`import` examples](#import-examples)
   * [`merge` *targets*](#merge-targets)
-    * [`merge` examples](#merge-examples)
-  * [`remove`, `rm` *targets*](#remove-rm-targets)
+  * [`remove` *targets*](#remove-targets)
   * [`secret`](#secret)
   * [`source` [*args*]](#source-args)
-    * [`source` examples](#source-examples)
   * [`source-path` [*targets*]](#source-path-targets)
-    * [`source-path` examples](#source-path-examples)
   * [`unmanaged`](#unmanaged)
-    * [`unmanaged` examples](#unmanaged-examples)
   * [`update`](#update)
-    * [`update` examples](#update-examples)
   * [`upgrade`](#upgrade)
-    * [`upgrade` examples](#upgrade-examples)
   * [`verify` [*targets*]](#verify-targets)
-    * [`verify` examples](#verify-examples)
 * [Editor configuration](#editor-configuration)
 * [Umask configuration](#umask-configuration)
-* [Templates](#templates)
-  * [Variables](#variables)
-  * [Functions](#functions)
+* [Templates variables](#templates-variables)
+* [Template functions](#template-functions)
+  * [`bitwarden` [*args*]](#bitwarden-args)
+  * [`keyring` *service* *user*](#keyring-service-user)
+  * [`lastpass` *id*](#lastpass-id)
+  * [`onepassword` *uuid*](#onepassword-uuid)
+  * [`pass` *pass-name*](#pass-pass-name)
+  * [`promptString` *prompt*](#promptstring-prompt)
+  * [`secret` [*args*]](#secret-args)
+  * [`secretJSON` [*args*]](#secretjson-args)
+  * [`vault` *key*](#vault-key)
 
 ## Concepts
 
@@ -164,19 +135,25 @@ chezmoi, and the first config file found is used.
 
 The following configuration variables are available:
 
-| Variable           | Type     | Default value             | Description                       |
-| ------------------ | -------- | ------------------------- | --------------------------------- |
-| `color`            | string   | `auto`                    | Colorize diffs                    |
-| `data`             | any      | none                      | Template data                     |
-| `dryRun`           | boolean  | `false`                   | Dry run mode                      |
-| `gpgRecipient`     | string   | none                      | GPG recipient                     |
-| `merge.command`    | string   | `vimdiff`                 | 3-way merge command               |
-| `merge.args`       | []string | none                      | Extra args to 3-way merge command |
-| `sourceDir`        | string   | `~/.config/share/chezmoi` | Source directory                  |
-| `sourceVCSCommand` | string   | `git`                     | Source version control system     |
-| `targetDir`        | string   | `~`                       | Target directory                  |
-| `umask`            | integer  | from system               | Umask                             |
-| `verbose`          | boolean  | `false`                   | Verbose mode                      |
+| Variable                | Type     | Default value             | Description                       |
+| ----------------------- | -------- | ------------------------- | --------------------------------- |
+| `bitwarden.command`     | string   | `bw`                      | Bitwarden CLI command             |
+| `color`                 | string   | `auto`                    | Colorize diffs                    |
+| `data`                  | any      | none                      | Template data                     |
+| `destDir`               | string   | `~`                       | Destination directory             |
+| `dryRun`                | boolean  | `false`                   | Dry run mode                      |
+| `genericSecret.command` | string   | none                      | Generic secret command            |
+| `gpgRecipient`          | string   | none                      | GPG recipient                     |
+| `lastpass.command`      | string   | `lpass`                   | Lastpass CLI command              |
+| `merge.args`            | []string | none                      | Extra args to 3-way merge command |
+| `merge.command`         | string   | `vimdiff`                 | 3-way merge command               |
+| `onepassword.command`   | string   | `op`                      | 1Password CLI command             |
+| `pass.command`          | string   | `pass`                    | Pass CLI command                  |
+| `sourceDir`             | string   | `~/.config/share/chezmoi` | Source directory                  |
+| `sourceVCS.command`     | string   | `git`                     | Source version control system     |
+| `umask`                 | integer  | from system               | Umask                             |
+| `vault.command`         | string   | `vault`                   | Vault CLI command                 |
+| `verbose`               | boolean  | `false`                   | Verbose mode                      |
 
 In addition, a number of secret manager integrations add configuration
 variables. These are documented in the secret manager section.
@@ -271,7 +248,7 @@ to inspect the target state.
 
     chezmoi archive | tar tvf -
 
-### `cat` [targets]
+### `cat` targets
 
 Write the target state of *targets*  to stdout. *targets* must be files or
 symlinks. For files, the target file contents are written. For symlinks, the
@@ -339,7 +316,7 @@ Print the computed template data in the given format. The accepted formats are
     chezmoi data
     chezmoi data --format=yaml
 
-### `diff` *targets*
+### `diff` [*targets*]
 
 Print the approximate shell commands required to ensure that *targets* in the
 destination directory match the target state. If no targets are specifed, print
@@ -359,12 +336,12 @@ Check for potential problems.
 
     chezmoi doctor
 
-### `dump` *targets*
+### `dump` [*targets*]
 
 Dump the target state in JSON format. If no targets are specified, then the
 entire target state. The `dump` command accepts additional arguments:
 
-#### `-f` / `--format` *format*
+#### `-f`, `--format` *format*
 
 Print the target state in the given format. The accepted formats are `json`
 (JSON) and `yaml` (YAML).
@@ -388,7 +365,7 @@ Apply target immediately after editing.
 Print the difference between the target state and the actual state after
 editing.
 
-#### `-p` / `--prompt`
+#### `-p`, `--prompt`
 
 Prompt before applying each target.
 
@@ -445,7 +422,7 @@ The only supported archive format is `.tar.gz`.
 
 Set the destination (in the source state) where the archive will be imported.
 
-#### `-x` / `--exact`
+#### `-x`, `--exact`
 
 Set the `exact` attribute on all imported directories.
 
@@ -475,13 +452,33 @@ cannot be decrypted) a two-way merge is performed instead.
 
     chezmoi merge ~/.bashrc
 
-### `remove`, `rm` *targets*
+### `remove` *targets*
 
 Remove *targets* from both the source state and the destination directory.
 
 ### `secret`
 
-Interact with a secret manager. See the "Secret managers" section for details.
+Run a secret manager's CLI, passing any extra arguments to the secret manager's
+CLI. This is primarily for verifying chezmoi's integration with your secret
+manager. Normally you would use template functions to retrieve secrets. Note
+that if you want to pass flags to the secret manager's CLU you will need to
+separate them with `--` to prevent chezmoi from interpreting them.
+
+To get a full list of available commands run:
+
+    chezmoi secret help
+
+#### `secret` examples
+
+    chezmoi secret bitwarden list items
+    chezmoi secret keyring set --service service --user user
+    chezmoi secret keyring get --service service --user user
+    chezmoi secret lastpass ls
+    chezmoi secret lastpass -- show --format=json id
+    chezmoi secret onepassword list items
+    chezmoi secret onepassword get item id
+    chezmoi secret pass show id
+    chezmoi secret vault -- kv get -format=json id
 
 ### `source` [*args*]
 
@@ -570,9 +567,7 @@ and `0077` respectively.
 For machine-specifc control of umask, set the `umask` configuration variable in
 chezmoi's configuration file.
 
-## Templates
-
-### Variables
+## Templates variables
 
 chezmoi provides the following automatically populated variables:
 
@@ -587,18 +582,119 @@ chezmoi provides the following automatically populated variables:
 | `.chezmoi.osRelease`    | The information from `/etc/os-release`, Linux only, run `chezmoi data` to see its output.                              |
 | `.chezmoi.username`     | The username of the user running chezmoi.                                                                              |
 
-### Functions
+Additional variables can be defined in the config file in the `data` section.
+
+## Template functions
 
 All standard [`text/template`](https://godoc.org/text/template) and [hermetic
 text template functions from `sprig`](http://masterminds.github.io/sprig/) are
-included.
+included. chezmoi provides some additional functions.
+
+### `bitwarden` [*args*]
+
+`bitwarden` returns structured data retrieved from
+[Bitwarden](https://bitwarden.com) using the [Bitwarden
+CLI](https://github.com/bitwarden/cli) (`bw`). *args* are passed to `bw`
+unchanged and the output from `bw` is parsed as JSON. The output from `bw` is
+cached so calling `bitwarden` multitple times with the same arguments will only
+invoke `bw` once.
+
+#### `biwarden` examples
+
+    username = {{ (bitwarden "item" "example.com").login.username }}
+    password = {{ (bitwarden "item" "example.com").login.password }}
+
+### `keyring` *service* *user*
+
+`keyring` retrieves the password associated with *service* and *user* from the
+user's keyring.
+
+| OS    | Keyring       |
+| ----- | ------------- |
+| macOS | Keychain      |
+| Linux | GNOME Keyring |
+
+#### `keyring` examples
+
+    [github]
+      user = "{{ .github.user }}"
+      token = "{{ keyring "github" .github.user }}"
+
+### `lastpass` *id*
+
+`lastpass` returns structured data from [LastPass](https://lastpass.com) using
+the [LastPass CLI](https://lastpass.github.io/lastpass-cli/lpass.1.html)
+(`lpass`). *id* is passed to `lpass show --json <id>` and the output from
+`lpass` is parsed as JSON. In addition, the `note` field, if present, is further
+parsed as JSON. The structured data is an array so typically the `index`
+function is used to extract the first item. The output from `lpass` is cached so
+calling `lastpass` multiple times with the same *id* will only invoke `lpass`
+once.
 
-When generating the initial config file, the function `promptString` is also
-available. The single argument is a string prompted to the user, and the return
-value is the user's response to that prompt.
+#### `lastpass` examples
 
-`.chezmoi.yaml.tmpl` example:
+    githubPassword = "{{ (index (lastpass "Github") 0).password }}"
+    {{ (index (lastpass "SSH") 0).note.privateKey }}
+
+### `onepassword` *uuid*
+
+`onepassword` returns structured data from [1Password](https://1password.com/)
+using the [1Password
+CLI](https://support.1password.com/command-line-getting-started/) (`op`). *uuid*
+is passed to `op get item <uuid>` and the output from `op` is parsed as JSON.
+The output from `op` is cached so calling `onepassword` multiple times with the
+same *uuid* will only invoke `op` once.
+
+#### `onepassword` examples
+
+    {{ (onepassword "<uuid>").details.password }}
+
+### `pass` *pass-name*
+
+`pass` returns passwords stored in [pass](https://www.passwordstore.org/) using
+the pass CLI (`pass`). *pass-name* is passed to `pass show <pass-name>` and
+first line of the output of `pass` is returned with the trailing newline
+stripped. The output from `pass` is cached so calling `pass` multiple times with
+the same *pass-name* will only invoke `pass` once.
+
+#### `pass` examples
+
+    {{ pass "<pass-name>" }}
+
+### `promptString` *prompt*
+
+`promptString` takes a single argument is a string prompted to the user, and the
+return value is the user's response to that prompt. It is only available when
+generating the initial conifig file.
+
+#### `promptString` examples
 
     {{ $email := promptString "email" -}}
     data:
         email: "{{ $email }}"
+
+### `secret` [*args*]
+
+`secret` returns the output of the generic secret command defined by the
+`genericSecret.command` configuration variable with *args* with leading and
+trailing whitespace removed. The output is cached so multitple calls to `secret`
+with the same *args* will only invoke the generic secret command once.
+
+### `secretJSON` [*args*]
+
+`secretJSON` returns structured data from the generic secret command defined by
+the `genericSecret.command` configuration variable with *args*. The output is
+parsed as JSON. The output is cached so multitple calls to `secret` with the
+same *args* will only invoke the generic secret command once.
+
+### `vault` *key*
+
+`vault` returns structured data from [Vault](https://www.vaultproject.io/) using
+the [Vault CLI](https://www.vaultproject.io/docs/commands/) (`vault`). *key* is
+passed to `vault kv get -format=json <key>` and the output from `vault` is
+parsed as JSON. The output from `vault` is cached so calling `vault` multiple
+times with the same *key* will only invoke `vault` once.
+
+#### `vault` examples
+
+    {{ (vault "<key>").data.data.password }}


Test patch:
diff --git a/cmd/secretgeneric_test.go b/cmd/secretgeneric_test.go
new file mode 100644
index 000000000..d0983fa41
--- /dev/null
+++ b/cmd/secretgeneric_test.go
@@ -0,0 +1,40 @@
+package cmd
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestSecretFunc(t *testing.T) {
+	t.Parallel()
+	c := &Config{
+		GenericSecret: genericSecretCmdConfig{
+			Command: "date",
+		},
+	}
+	args := []string{"+%Y-%M-%DT%H:%M:%SZ"}
+	var value interface{}
+	assert.NotPanics(t, func() {
+		value = c.secretFunc(args...)
+	})
+	time.Sleep(1100 * time.Millisecond)
+	assert.Equal(t, value, c.secretFunc(args...))
+}
+
+func TestSecretJSONFunc(t *testing.T) {
+	t.Parallel()
+	c := &Config{
+		GenericSecret: genericSecretCmdConfig{
+			Command: "date",
+		},
+	}
+	args := []string{`+{"date":"%Y-%M-%DT%H:%M:%SZ"}`}
+	var value interface{}
+	assert.NotPanics(t, func() {
+		value = c.secretJSONFunc(args...)
+	})
+	time.Sleep(1100 * time.Millisecond)
+	assert.Equal(t, value, c.secretJSONFunc(args...))
+}