From 2923f7e22527f222b1ac145a978d0ee0af916f96 Mon Sep 17 00:00:00 2001 From: Miklos Vajna Date: Sun, 21 Dec 2025 22:05:17 +0100 Subject: [PATCH] Add a new 'export' command This is primarily interesting in case the 'cpm search' output would be parsed by some external tool, but you can also use this to an other password manager. --- Makefile | 3 ++ commands/export.go | 71 +++++++++++++++++++++++++++++++++++++++++ commands/export_test.go | 67 ++++++++++++++++++++++++++++++++++++++ commands/root.go | 2 ++ guide/src/news.md | 4 +++ man/cpm-create.1 | 10 ++---- man/cpm-delete.1 | 10 ++---- man/cpm-export.1 | 26 +++++++++++++++ man/cpm-gc.1 | 10 ++---- man/cpm-import.1 | 10 ++---- man/cpm-pull.1 | 10 ++---- man/cpm-search.1 | 10 ++---- man/cpm-update.1 | 10 ++---- man/cpm-version.1 | 10 ++---- man/cpm.1 | 12 ++----- man/generate.go | 2 +- 16 files changed, 193 insertions(+), 74 deletions(-) create mode 100644 commands/export.go create mode 100644 commands/export_test.go create mode 100644 man/cpm-export.1 diff --git a/Makefile b/Makefile index f156633..52bcdca 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,10 @@ GO_OBJECTS = \ commands/create_test.go \ commands/delete.go \ commands/delete_test.go \ + commands/export.go \ + commands/export_test.go \ commands/gc.go \ + commands/gc_test.go \ commands/import.go \ commands/import_test.go \ commands/pull.go \ diff --git a/commands/export.go b/commands/export.go new file mode 100644 index 0000000..2439949 --- /dev/null +++ b/commands/export.go @@ -0,0 +1,71 @@ +// Copyright 2025 Miklos Vajna +// +// SPDX-License-Identifier: MIT + +package commands + +import ( + "database/sql" + "encoding/json" + "fmt" + + "github.com/spf13/cobra" +) + +type passwordRow struct { + ID int + Machine string + Service string + User string + Password string + PasswordType PasswordType + Archived bool + Created string + Modified string +} + +func exportPasswords(db *sql.DB) ([]byte, error) { + var results []passwordRow + rows, err := db.Query("select id, machine, service, user, password, type, archived, created, modified from passwords") + if err != nil { + return nil, fmt.Errorf("db.Query(select) failed: %s", err) + } + + defer rows.Close() + for rows.Next() { + var row passwordRow + err = rows.Scan(&row.ID, &row.Machine, &row.Service, &row.User, &row.Password, &row.PasswordType, &row.Archived, &row.Created, &row.Modified) + if err != nil { + return nil, fmt.Errorf("rows.Scan() failed: %s", err) + } + + results = append(results, row) + } + + j, err := json.Marshal(results) + if err != nil { + return nil, fmt.Errorf("json.Marshal() failed: %s", err) + } + + return j, nil +} + +func newExportCommand(ctx *Context) *cobra.Command { + var cmd = &cobra.Command{ + Use: "export", + Short: "exports passwords as JSON", + RunE: func(cmd *cobra.Command, args []string) error { + j, err := exportPasswords(ctx.Database) + if err != nil { + return fmt.Errorf("readPasswords() failed: %s", err) + } + + fmt.Fprintf(cmd.OutOrStdout(), "%s\n", j) + + ctx.NoWriteBack = true + return nil + }, + } + + return cmd +} diff --git a/commands/export_test.go b/commands/export_test.go new file mode 100644 index 0000000..2f28c80 --- /dev/null +++ b/commands/export_test.go @@ -0,0 +1,67 @@ +// Copyright 2025 Miklos Vajna +// +// SPDX-License-Identifier: MIT + +package commands + +import ( + "bytes" + "encoding/json" + "os" + "testing" +) + +func TestExport(t *testing.T) { + ctx := CreateContextForTesting(t) + _, err := ctx.Database.Exec(`insert into passwords (machine, service, user, password, type) values('mymachine', 'myservice', 'myuser', 'mypassword', 'plain');`) + if err != nil { + t.Fatalf("createPassword() = %q, want nil", err) + } + os.Args = []string{"", "export"} + inBuf := new(bytes.Buffer) + outBuf := new(bytes.Buffer) + + actualRet := Main(inBuf, outBuf) + + expectedRet := 0 + if actualRet != expectedRet { + t.Fatalf("Main() = %q, want %q", actualRet, expectedRet) + } + actualOutput := outBuf.String() + var passwords []passwordRow + err = json.NewDecoder(bytes.NewBufferString(actualOutput)).Decode(&passwords) + if err != nil { + t.Fatalf("json.Decode() = %q, want nil", err) + } + if len(passwords) != 1 { + t.Fatalf("passwords len = %q, want %q", len(passwords), 1) + } + password := passwords[0] + if password.ID != 1 { + t.Fatalf("password.ID = %q, want %q", password.ID, 1) + } + if password.Machine != "mymachine" { + t.Fatalf("password.Machine = %q, want %q", password.Machine, "mymachine") + } + if password.Service != "myservice" { + t.Fatalf("password.Service = %q, want %q", password.Service, "myservice") + } + if password.User != "myuser" { + t.Fatalf("password.User = %q, want %q", password.User, "myuser") + } + if password.Password != "mypassword" { + t.Fatalf("password.Password = %q, want %q", password.Password, "mypassword") + } + if password.PasswordType != "plain" { + t.Fatalf("password.PasswordType = %q, want %q", password.PasswordType, "plain") + } + if password.Archived != false { + t.Fatalf("password.Archived = %t, want %t", password.Archived, false) + } + if password.Created != "" { + t.Fatalf("password.Created = %q, want %q", password.Created, "") + } + if password.Modified != "" { + t.Fatalf("password.Modified = %q, want %q", password.Modified, "") + } +} diff --git a/commands/root.go b/commands/root.go index f5166a0..1d70d03 100644 --- a/commands/root.go +++ b/commands/root.go @@ -58,6 +58,7 @@ func NewRootCommand(ctx *Context) *cobra.Command { cmd.AddCommand(newPullCommand(ctx)) cmd.AddCommand(newVersionCommand(ctx)) cmd.AddCommand(newGcCommand(ctx)) + cmd.AddCommand(newExportCommand(ctx)) return cmd } @@ -77,6 +78,7 @@ func getCommands() []string { "update", "version", "gc", + "export", } } diff --git a/guide/src/news.md b/guide/src/news.md index 64bd631..67ca708 100644 --- a/guide/src/news.md +++ b/guide/src/news.md @@ -1,5 +1,9 @@ # Changelog +## main + +- new `export` command to write the password database as a JSON file + ## 25.8 - new `gc` command to rebuild the database file, repacking it into a minimal amount of disk space diff --git a/man/cpm-create.1 b/man/cpm-create.1 index ba3f6bb..0dddea6 100644 --- a/man/cpm-create.1 +++ b/man/cpm-create.1 @@ -1,23 +1,19 @@ .nh -.TH "CPM" "1" "Jul 2022" "Auto generated by spf13/cobra" "" +.TH "CPM" "1" "Dec 2025" "Auto generated by spf13/cobra" "" .SH NAME -.PP cpm-create - creates a new password .SH SYNOPSIS -.PP \fBcpm create [flags]\fP .SH DESCRIPTION -.PP creates a new password .SH OPTIONS -.PP \fB-n\fP, \fB--dry-run\fP[=false] do everything except actually perform the database action (default: false) @@ -51,10 +47,8 @@ creates a new password .SH SEE ALSO -.PP \fBcpm(1)\fP .SH HISTORY -.PP -22-Jul-2022 Auto generated by spf13/cobra +21-Dec-2025 Auto generated by spf13/cobra diff --git a/man/cpm-delete.1 b/man/cpm-delete.1 index 642706c..07de876 100644 --- a/man/cpm-delete.1 +++ b/man/cpm-delete.1 @@ -1,23 +1,19 @@ .nh -.TH "CPM" "1" "Jul 2022" "Auto generated by spf13/cobra" "" +.TH "CPM" "1" "Dec 2025" "Auto generated by spf13/cobra" "" .SH NAME -.PP cpm-delete - deletes an existing password .SH SYNOPSIS -.PP \fBcpm delete [flags]\fP .SH DESCRIPTION -.PP deletes an existing password .SH OPTIONS -.PP \fB-n\fP, \fB--dry-run\fP[=false] do everything except actually perform the database action (default: false) @@ -31,10 +27,8 @@ deletes an existing password .SH SEE ALSO -.PP \fBcpm(1)\fP .SH HISTORY -.PP -22-Jul-2022 Auto generated by spf13/cobra +21-Dec-2025 Auto generated by spf13/cobra diff --git a/man/cpm-export.1 b/man/cpm-export.1 new file mode 100644 index 0000000..7e53604 --- /dev/null +++ b/man/cpm-export.1 @@ -0,0 +1,26 @@ +.nh +.TH "CPM" "1" "Dec 2025" "Auto generated by spf13/cobra" "" + +.SH NAME +cpm-export - exports passwords as JSON + + +.SH SYNOPSIS +\fBcpm export [flags]\fP + + +.SH DESCRIPTION +exports passwords as JSON + + +.SH OPTIONS +\fB-h\fP, \fB--help\fP[=false] + help for export + + +.SH SEE ALSO +\fBcpm(1)\fP + + +.SH HISTORY +21-Dec-2025 Auto generated by spf13/cobra diff --git a/man/cpm-gc.1 b/man/cpm-gc.1 index 9b4d1c5..9772395 100644 --- a/man/cpm-gc.1 +++ b/man/cpm-gc.1 @@ -1,32 +1,26 @@ .nh -.TH "CPM" "1" "Jul 2022" "Auto generated by spf13/cobra" "" +.TH "CPM" "1" "Dec 2025" "Auto generated by spf13/cobra" "" .SH NAME -.PP cpm-gc - rebuilds the database file, repacking it into a minimal amount of disk space .SH SYNOPSIS -.PP \fBcpm gc [flags]\fP .SH DESCRIPTION -.PP rebuilds the database file, repacking it into a minimal amount of disk space .SH OPTIONS -.PP \fB-h\fP, \fB--help\fP[=false] help for gc .SH SEE ALSO -.PP \fBcpm(1)\fP .SH HISTORY -.PP -22-Jul-2022 Auto generated by spf13/cobra +21-Dec-2025 Auto generated by spf13/cobra diff --git a/man/cpm-import.1 b/man/cpm-import.1 index 312aa96..2d5b64d 100644 --- a/man/cpm-import.1 +++ b/man/cpm-import.1 @@ -1,32 +1,26 @@ .nh -.TH "CPM" "1" "Jul 2022" "Auto generated by spf13/cobra" "" +.TH "CPM" "1" "Dec 2025" "Auto generated by spf13/cobra" "" .SH NAME -.PP cpm-import - imports an old XML database .SH SYNOPSIS -.PP \fBcpm import [flags]\fP .SH DESCRIPTION -.PP imports an old XML database .SH OPTIONS -.PP \fB-h\fP, \fB--help\fP[=false] help for import .SH SEE ALSO -.PP \fBcpm(1)\fP .SH HISTORY -.PP -22-Jul-2022 Auto generated by spf13/cobra +21-Dec-2025 Auto generated by spf13/cobra diff --git a/man/cpm-pull.1 b/man/cpm-pull.1 index b2499a3..d04fef9 100644 --- a/man/cpm-pull.1 +++ b/man/cpm-pull.1 @@ -1,32 +1,26 @@ .nh -.TH "CPM" "1" "Jul 2022" "Auto generated by spf13/cobra" "" +.TH "CPM" "1" "Dec 2025" "Auto generated by spf13/cobra" "" .SH NAME -.PP cpm-pull - copies a remote database to a local one .SH SYNOPSIS -.PP \fBcpm pull [flags]\fP .SH DESCRIPTION -.PP copies a remote database to a local one .SH OPTIONS -.PP \fB-h\fP, \fB--help\fP[=false] help for pull .SH SEE ALSO -.PP \fBcpm(1)\fP .SH HISTORY -.PP -22-Jul-2022 Auto generated by spf13/cobra +21-Dec-2025 Auto generated by spf13/cobra diff --git a/man/cpm-search.1 b/man/cpm-search.1 index 03a643d..91256ff 100644 --- a/man/cpm-search.1 +++ b/man/cpm-search.1 @@ -1,23 +1,19 @@ .nh -.TH "CPM" "1" "Jul 2022" "Auto generated by spf13/cobra" "" +.TH "CPM" "1" "Dec 2025" "Auto generated by spf13/cobra" "" .SH NAME -.PP cpm-search - searches passwords .SH SYNOPSIS -.PP \fBcpm search [flags]\fP .SH DESCRIPTION -.PP searches passwords .SH OPTIONS -.PP \fB-h\fP, \fB--help\fP[=false] help for search @@ -59,10 +55,8 @@ searches passwords .SH SEE ALSO -.PP \fBcpm(1)\fP .SH HISTORY -.PP -22-Jul-2022 Auto generated by spf13/cobra +21-Dec-2025 Auto generated by spf13/cobra diff --git a/man/cpm-update.1 b/man/cpm-update.1 index f659888..9c2c10f 100644 --- a/man/cpm-update.1 +++ b/man/cpm-update.1 @@ -1,23 +1,19 @@ .nh -.TH "CPM" "1" "Jul 2022" "Auto generated by spf13/cobra" "" +.TH "CPM" "1" "Dec 2025" "Auto generated by spf13/cobra" "" .SH NAME -.PP cpm-update - updates an existing password .SH SYNOPSIS -.PP \fBcpm update [flags]\fP .SH DESCRIPTION -.PP updates an existing password .SH OPTIONS -.PP \fB-a\fP, \fB--archived\fP="" new archived value ("true" or "false"; default: keep unchanged) @@ -59,10 +55,8 @@ updates an existing password .SH SEE ALSO -.PP \fBcpm(1)\fP .SH HISTORY -.PP -22-Jul-2022 Auto generated by spf13/cobra +21-Dec-2025 Auto generated by spf13/cobra diff --git a/man/cpm-version.1 b/man/cpm-version.1 index 25331d7..2e726bb 100644 --- a/man/cpm-version.1 +++ b/man/cpm-version.1 @@ -1,32 +1,26 @@ .nh -.TH "CPM" "1" "Jul 2022" "Auto generated by spf13/cobra" "" +.TH "CPM" "1" "Dec 2025" "Auto generated by spf13/cobra" "" .SH NAME -.PP cpm-version - shows version information .SH SYNOPSIS -.PP \fBcpm version [flags]\fP .SH DESCRIPTION -.PP shows version information .SH OPTIONS -.PP \fB-h\fP, \fB--help\fP[=false] help for version .SH SEE ALSO -.PP \fBcpm(1)\fP .SH HISTORY -.PP -22-Jul-2022 Auto generated by spf13/cobra +21-Dec-2025 Auto generated by spf13/cobra diff --git a/man/cpm.1 b/man/cpm.1 index 348a389..dadc9e3 100644 --- a/man/cpm.1 +++ b/man/cpm.1 @@ -1,32 +1,26 @@ .nh -.TH "CPM" "1" "Jul 2022" "Auto generated by spf13/cobra" "" +.TH "CPM" "1" "Dec 2025" "Auto generated by spf13/cobra" "" .SH NAME -.PP cpm - turtle-cpm is a console password manager .SH SYNOPSIS -.PP \fBcpm [flags]\fP .SH DESCRIPTION -.PP turtle-cpm is a console password manager .SH OPTIONS -.PP \fB-h\fP, \fB--help\fP[=false] help for cpm .SH SEE ALSO -.PP -\fBcpm-create(1)\fP, \fBcpm-delete(1)\fP, \fBcpm-gc(1)\fP, \fBcpm-import(1)\fP, \fBcpm-pull(1)\fP, \fBcpm-search(1)\fP, \fBcpm-update(1)\fP, \fBcpm-version(1)\fP +\fBcpm-create(1)\fP, \fBcpm-delete(1)\fP, \fBcpm-export(1)\fP, \fBcpm-gc(1)\fP, \fBcpm-import(1)\fP, \fBcpm-pull(1)\fP, \fBcpm-search(1)\fP, \fBcpm-update(1)\fP, \fBcpm-version(1)\fP .SH HISTORY -.PP -22-Jul-2022 Auto generated by spf13/cobra +21-Dec-2025 Auto generated by spf13/cobra diff --git a/man/generate.go b/man/generate.go index 551764c..0aa2783 100644 --- a/man/generate.go +++ b/man/generate.go @@ -15,7 +15,7 @@ import ( func main() { var ctx commands.Context cmd := commands.NewRootCommand(&ctx) - date := time.Date(2022, 7, 22, 12, 0, 0, 0, time.UTC) + date := time.Date(2025, 12, 21, 12, 0, 0, 0, time.UTC) header := &doc.GenManHeader{ Title: "CPM", Section: "1",