Skip to content

Commit 7890c9c

Browse files
authored
Merge pull request #155 from HQJaTu/improved-show
feat: Added displaying of all fields of a record into list and show
2 parents 8e331b6 + 6076cb1 commit 7890c9c

2 files changed

Lines changed: 225 additions & 50 deletions

File tree

cmd/enpasscli/main.go

Lines changed: 188 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ type Args struct {
6868
pinEnable *bool
6969
sort *bool
7070
trashed *bool
71+
detailed *bool
7172
and *bool
7273
clipboardPrimary *bool
7374
// write command flags
@@ -91,6 +92,7 @@ func (args *Args) parse() {
9192
args.and = flag.Bool("and", false, "Combines filters with AND instead of default OR.")
9293
args.sort = flag.Bool("sort", false, "Sort the output by title and username of the 'list' and 'show' command.")
9394
args.trashed = flag.Bool("trashed", false, "Show trashed items in the 'list' and 'show' command.")
95+
args.detailed = flag.Bool("detailed", false, "Show every field of each entry in 'list' and 'show'. Without this flag, only the original summary fields (title, login, category, label, type) are displayed.")
9496
args.clipboardPrimary = flag.Bool("clipboardPrimary", false, "Use primary X selection instead of clipboard for the 'copy' command.")
9597
// write command flags
9698
args.title = flag.String("title", "", "Entry title (for create/edit).")
@@ -154,85 +156,221 @@ func sortEntries(cards []enpass.Card) {
154156
}
155157

156158
func listEntries(logger *logrus.Logger, vault *enpass.Vault, args *Args) {
157-
cards, err := vault.GetEntries(*args.cardType, args.filters)
159+
entries, err := collectEntries(vault, args, false)
158160
if err != nil {
159-
logger.WithError(err).Fatal("could not retrieve cards")
160-
}
161-
if *args.sort {
162-
sortEntries(cards)
161+
logger.WithError(err).Fatal(err.Error())
163162
}
163+
outputEntriesOrLog(logger, entries, args)
164+
}
164165

165-
data, err := prepareCardData(cards, false, args)
166+
func showEntries(logger *logrus.Logger, vault *enpass.Vault, args *Args) {
167+
entries, err := collectEntries(vault, args, true)
166168
if err != nil {
167169
logger.WithError(err).Fatal(err.Error())
168170
}
171+
outputEntriesOrLog(logger, entries, args)
172+
}
169173

170-
outputDataOrLog(logger, data, args)
174+
// entryView is one Enpass item with all of its fields grouped together.
175+
type entryView struct {
176+
UUID string `json:"uuid"`
177+
Title string `json:"title"`
178+
Subtitle string `json:"subtitle,omitempty"`
179+
Category string `json:"category,omitempty"`
180+
Trashed bool `json:"trashed,omitempty"`
181+
Fields []fieldView `json:"fields"`
171182
}
172183

173-
func showEntries(logger *logrus.Logger, vault *enpass.Vault, args *Args) {
174-
cards, err := vault.GetEntries(*args.cardType, args.filters)
184+
// fieldView is a single field of an entry (username, email, password, ...).
185+
// Value is empty when the field is sensitive and the caller didn't ask for
186+
// decrypted output (list mode).
187+
type fieldView struct {
188+
Type string `json:"type"`
189+
Label string `json:"label,omitempty"`
190+
Sensitive bool `json:"sensitive,omitempty"`
191+
Value string `json:"value,omitempty"`
192+
}
193+
194+
// collectEntries fetches every field for matching entries and groups them by
195+
// item UUID. When includeSensitive is false, values of sensitive fields
196+
// (passwords) are omitted while non-sensitive fields like username/email are
197+
// still populated — this is what powers the "list shows usernames and emails
198+
// but not passwords" behavior.
199+
func collectEntries(vault *enpass.Vault, args *Args, includeSensitive bool) ([]entryView, error) {
200+
// The -type flag defaults to "password" for the copy/pass commands. For
201+
// list/show we want every field type, so treat the default as "no filter".
202+
// Any other explicit value still filters server-side.
203+
typeFilter := *args.cardType
204+
if typeFilter == "password" {
205+
typeFilter = ""
206+
}
207+
208+
cards, err := vault.GetAllFields(typeFilter, args.filters)
175209
if err != nil {
176-
logger.WithError(err).Fatal("could not retrieve cards")
177-
}
178-
if *args.sort {
179-
sortEntries(cards)
210+
return nil, fmt.Errorf("could not retrieve cards: %w", err)
180211
}
181212

182-
data, err := prepareCardData(cards, true, args)
183-
if err != nil {
184-
logger.WithError(err).Fatal(err.Error())
213+
order := make([]string, 0)
214+
groups := make(map[string]*entryView)
215+
for _, c := range cards {
216+
if c.IsDeleted() {
217+
continue
218+
}
219+
if c.IsTrashed() && !*args.trashed {
220+
continue
221+
}
222+
g, ok := groups[c.UUID]
223+
if !ok {
224+
g = &entryView{
225+
UUID: c.UUID,
226+
Title: c.Title,
227+
Subtitle: c.Subtitle,
228+
Category: c.Category,
229+
Trashed: c.IsTrashed(),
230+
}
231+
groups[c.UUID] = g
232+
order = append(order, c.UUID)
233+
}
234+
f := fieldView{
235+
Type: c.Type,
236+
Label: c.Label,
237+
Sensitive: c.Sensitive,
238+
}
239+
// Non-password field values are stored in cleartext; Decrypt() returns
240+
// them as-is. For password fields, Decrypt() actually decrypts.
241+
value, derr := c.Decrypt()
242+
if derr != nil {
243+
return nil, fmt.Errorf("could not decrypt %s/%s: %w", c.Title, c.Label, derr)
244+
}
245+
if includeSensitive || !c.Sensitive {
246+
f.Value = value
247+
}
248+
g.Fields = append(g.Fields, f)
185249
}
186250

187-
outputDataOrLog(logger, data, args)
251+
entries := make([]entryView, 0, len(order))
252+
for _, uuid := range order {
253+
entries = append(entries, *groups[uuid])
254+
}
255+
if *args.sort {
256+
sort.SliceStable(entries, func(i, j int) bool {
257+
return strings.ToLower(entries[i].Title) < strings.ToLower(entries[j].Title)
258+
})
259+
}
260+
return entries, nil
188261
}
189262

190-
func prepareCardData(cards []enpass.Card, includeDecrypted bool, args *Args) ([]map[string]string, error) {
191-
data := make([]map[string]string, 0)
192-
for _, card := range cards {
193-
if card.IsTrashed() && !*args.trashed {
194-
continue
195-
}
263+
func outputEntriesOrLog(logger *logrus.Logger, entries []entryView, args *Args) {
264+
if *args.detailed {
265+
outputDetailed(logger, entries, args)
266+
return
267+
}
268+
outputCompact(logger, entries, args)
269+
}
196270

197-
cardMap := map[string]string{
198-
"title": card.Title,
199-
"login": card.Subtitle,
200-
"category": card.Category,
201-
"label": card.Label,
202-
"type": card.Type,
271+
// outputCompact reproduces the original list/show output: one row per entry
272+
// with the summary fields title, login, category, label, type — plus password
273+
// when present (show mode).
274+
func outputCompact(logger *logrus.Logger, entries []entryView, args *Args) {
275+
type compactRow struct {
276+
Title string `json:"title"`
277+
Login string `json:"login"`
278+
Category string `json:"category"`
279+
Label string `json:"label"`
280+
Type string `json:"type"`
281+
Password string `json:"password,omitempty"`
282+
}
283+
284+
rows := make([]compactRow, 0, len(entries))
285+
for _, e := range entries {
286+
anchor := anchorField(e.Fields)
287+
row := compactRow{
288+
Title: e.Title,
289+
Login: e.Subtitle,
290+
Category: e.Category,
203291
}
204-
205-
if includeDecrypted {
206-
decrypted, err := card.Decrypt()
207-
if err != nil {
208-
return nil, fmt.Errorf("could not decrypt %s: %w", card.Title, err)
292+
if anchor != nil {
293+
row.Label = anchor.Label
294+
row.Type = anchor.Type
295+
if anchor.Sensitive {
296+
row.Password = anchor.Value
209297
}
210-
cardMap["password"] = decrypted
211298
}
299+
rows = append(rows, row)
300+
}
212301

213-
data = append(data, cardMap)
302+
if *args.jsonOutput {
303+
jsonData, err := json.Marshal(rows)
304+
if err != nil {
305+
logger.WithError(err).Fatal("could not marshal JSON data")
306+
}
307+
fmt.Println(string(jsonData))
308+
return
309+
}
310+
for _, r := range rows {
311+
format := "> title: %s login: %s cat.: %s label: %s type: %s"
312+
vals := []any{r.Title, r.Login, r.Category, r.Label, r.Type}
313+
if r.Password != "" {
314+
format += " password: %s"
315+
vals = append(vals, r.Password)
316+
}
317+
logger.Printf(format, vals...)
214318
}
215-
return data, nil
216319
}
217320

218-
func outputDataOrLog(logger *logrus.Logger, data []map[string]string, args *Args) {
321+
// outputDetailed emits the grouped per-field view: one header line per entry
322+
// followed by an indented line per field.
323+
func outputDetailed(logger *logrus.Logger, entries []entryView, args *Args) {
219324
if *args.jsonOutput {
220-
jsonData, jsonErr := json.Marshal(data)
221-
if jsonErr != nil {
222-
logger.WithError(jsonErr).Fatal("could not marshal JSON data")
325+
jsonData, err := json.Marshal(entries)
326+
if err != nil {
327+
logger.WithError(err).Fatal("could not marshal JSON data")
223328
}
224329
fmt.Println(string(jsonData))
225-
} else {
226-
for _, card := range data {
227-
logger.Printf(
228-
"> title: %s login: %s cat.: %s label: %s",
229-
card["title"],
230-
card["login"],
231-
card["category"],
232-
card["label"],
233-
)
330+
return
331+
}
332+
for _, e := range entries {
333+
header := "> " + e.Title
334+
if e.Subtitle != "" {
335+
header += " (" + e.Subtitle + ")"
336+
}
337+
if e.Category != "" {
338+
header += " cat.: " + e.Category
234339
}
340+
if e.Trashed {
341+
header += " [trashed]"
342+
}
343+
logger.Print(header)
344+
for _, f := range e.Fields {
345+
name := f.Label
346+
if name == "" {
347+
name = f.Type
348+
}
349+
switch {
350+
case f.Sensitive && f.Value == "":
351+
logger.Printf(" %s (%s): ********", name, f.Type)
352+
case f.Value != "":
353+
logger.Printf(" %s (%s): %s", name, f.Type, f.Value)
354+
default:
355+
logger.Printf(" %s (%s)", name, f.Type)
356+
}
357+
}
358+
}
359+
}
360+
361+
// anchorField picks the field that represents the entry in compact mode.
362+
// Mirrors the original GetEntries dedup: prefer the sensitive (password)
363+
// field, fall back to the first field.
364+
func anchorField(fields []fieldView) *fieldView {
365+
for i := range fields {
366+
if fields[i].Sensitive {
367+
return &fields[i]
368+
}
369+
}
370+
if len(fields) > 0 {
371+
return &fields[0]
235372
}
373+
return nil
236374
}
237375

238376
func copyEntry(logger *logrus.Logger, vault *enpass.Vault, args *Args) {

pkg/enpass/vault.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,43 @@ func (v *Vault) GetEntries(cardType string, filters []string) ([]Card, error) {
268268
return cards, nil
269269
}
270270

271+
// GetAllFields returns every itemfield row matching the filters, without
272+
// deduplicating by item UUID. Each returned Card represents a single field
273+
// (e.g. username, email, password) belonging to an entry. Use this when the
274+
// caller wants to display or operate on multiple fields per entry; use
275+
// GetEntries when the caller wants one Card per entry.
276+
func (v *Vault) GetAllFields(cardType string, filters []string) ([]Card, error) {
277+
if v.db == nil || v.vaultInfo.VaultName == "" {
278+
return nil, errors.New("vault is not initialized")
279+
}
280+
281+
rows, err := v.executeEntryQuery(cardType, filters)
282+
if err != nil {
283+
return nil, errors.Wrap(err, "could not retrieve cards from database")
284+
}
285+
defer rows.Close()
286+
287+
cards := make([]Card, 0)
288+
for rows.Next() {
289+
var card Card
290+
if err := rows.Scan(
291+
&card.UUID, &card.Type, &card.CreatedAt, &card.UpdatedAt, &card.Title,
292+
&card.Subtitle, &card.Note, &card.Trashed, &card.Deleted, &card.Category,
293+
&card.Label, &card.value, &card.itemKey, &card.LastUsed, &card.Sensitive, &card.Icon,
294+
); err != nil {
295+
return nil, errors.Wrap(err, "could not read card from database")
296+
}
297+
card.RawValue = card.value
298+
cards = append(cards, card)
299+
}
300+
301+
if err := rows.Err(); err != nil {
302+
return nil, errors.Wrap(err, "error iterating database rows")
303+
}
304+
305+
return cards, nil
306+
}
307+
271308
func (v *Vault) GetEntry(cardType string, filters []string, unique bool) (*Card, error) {
272309
cards, err := v.GetEntries(cardType, filters)
273310
if err != nil {

0 commit comments

Comments
 (0)