Repository: twpayne/chezmoi
Pull request #80 (2018-12-18 18:00:54)

Automatically parse Lastpass note values


Patch:
diff --git a/README.md b/README.md
index 57f30a398..20fdb0ed7 100644
--- a/README.md
+++ b/README.md
@@ -254,6 +254,14 @@ the field you want. For example, to extract the `password` field from first the
 
     githubPassword = {{ (index (lastpass "Github") 0).password }}
 
+`chezmoi` automatically parses the `note` value of the Lastpass entry, so, for
+example, you can extract a private SSH key like this:
+
+    {{ (index (lastpass "SSH") 0).note.privateKey }}
+
+Keys in the `note` section written as `CamelCase Words` are converted to
+`camelCaseWords`.
+
 ### Using keyring
 
 `chezmoi` includes support for Keychain (on macOS), GNOME Keyring (on Linux),
diff --git a/cmd/lastpass.go b/cmd/lastpass.go
index 3782aacf6..60d7216dc 100644
--- a/cmd/lastpass.go
+++ b/cmd/lastpass.go
@@ -1,10 +1,14 @@
 package cmd
 
 import (
+	"bufio"
+	"bytes"
 	"encoding/json"
 	"fmt"
 	"os/exec"
+	"regexp"
 	"strings"
+	"unicode"
 
 	"github.com/spf13/cobra"
 	"github.com/twpayne/chezmoi/lib/chezmoi"
@@ -17,6 +21,8 @@ var lastpassCommand = &cobra.Command{
 	RunE:  makeRunE(config.runLastPassCommand),
 }
 
+var lastpassParseNoteRegexp = regexp.MustCompile(`\A([ A-Za-z]*):(.*)\z`)
+
 // A LastPassCommandConfig is a configuration for the lastpass command.
 type LastPassCommandConfig struct {
 	Lpass string
@@ -51,6 +57,33 @@ func (c *Config) lastpassFunc(id string) interface{} {
 	if err := json.Unmarshal(output, &data); err != nil {
 		chezmoi.ReturnTemplateFuncError(fmt.Errorf("lastpass %q: %s show -j %q: %v", id, name, id, err))
 	}
+	for _, d := range data {
+		if note, ok := d["note"].(string); ok {
+			d["note"] = lastpassParseNote(note)
+		}
+	}
 	lastPassCache[id] = data
 	return data
 }
+
+func lastpassParseNote(note string) map[string]string {
+	result := make(map[string]string)
+	s := bufio.NewScanner(bytes.NewBufferString(note))
+	key := ""
+	for s.Scan() {
+		if m := lastpassParseNoteRegexp.FindStringSubmatch(s.Text()); m != nil {
+			keyComponents := strings.Split(m[1], " ")
+			firstComponentRunes := []rune(keyComponents[0])
+			firstComponentRunes[0] = unicode.ToLower(firstComponentRunes[0])
+			keyComponents[0] = string(firstComponentRunes)
+			key = strings.Join(keyComponents, "")
+			result[key] = m[2] + "\n"
+		} else {
+			result[key] += s.Text() + "\n"
+		}
+	}
+	if err := s.Err(); err != nil {
+		chezmoi.ReturnTemplateFuncError(fmt.Errorf("lastpassParseNote: %v", err))
+	}
+	return result
+}


Test patch:
diff --git a/cmd/lastpass_test.go b/cmd/lastpass_test.go
new file mode 100644
index 000000000..c7b4cae24
--- /dev/null
+++ b/cmd/lastpass_test.go
@@ -0,0 +1,47 @@
+package cmd
+
+import (
+	"testing"
+
+	"github.com/d4l3k/messagediff"
+)
+
+func Test_lastpassParseNote(t *testing.T) {
+	for _, tc := range []struct {
+		note string
+		want map[string]string
+	}{
+		{
+			note: "Foo:bar\n",
+			want: map[string]string{
+				"foo": "bar\n",
+			},
+		},
+		{
+			note: "Foo:bar\nbaz\n",
+			want: map[string]string{
+				"foo": "bar\nbaz\n",
+			},
+		},
+		{
+			note: "NoteType:SSH Key\nLanguage:en-US\nBit Strength:2048\nFormat:RSA\nPassphrase:Passphrase\nPrivate Key:-----BEGIN OPENSSH PRIVATE KEY-----\n-----END OPENSSH PRIVATE KEY-----\nPublic Key:ssh-rsa public-key user@host\nHostname:Hostname\nDate:Date\nNotes:",
+			want: map[string]string{
+				"noteType":    "SSH Key\n",
+				"language":    "en-US\n",
+				"bitStrength": "2048\n",
+				"format":      "RSA\n",
+				"passphrase":  "Passphrase\n",
+				"privateKey":  "-----BEGIN OPENSSH PRIVATE KEY-----\n-----END OPENSSH PRIVATE KEY-----\n",
+				"publicKey":   "ssh-rsa public-key user@host\n",
+				"hostname":    "Hostname\n",
+				"date":        "Date\n",
+				"notes":       "\n",
+			},
+		},
+	} {
+		got := lastpassParseNote(tc.note)
+		if diff, equal := messagediff.PrettyDiff(tc.want, got); !equal {
+			t.Errorf("lastpassParseNote(%q) == %+v, want %+v\n%s", tc.note, got, tc.want, diff)
+		}
+	}
+}