Skip to content

Do not omit shadowed entries in ls output #2341

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Sep 16, 2022
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 26 additions & 16 deletions docs/commands/list.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ The `list` command is used to list all the entries in the password store or at a
## Synopsis

```bash
$ gopass ls
$ gopass ls path/to/entries
gopass ls
gopass ls path/to/entries
```

- List all the entries in the password store including the one in mounted stores: `gopass list`
Expand All @@ -19,39 +19,44 @@ Note: `list` will not change anything, nor encrypt or decrypt anything.
Flag | Aliases | Description
---- | ------- | -----------
`--limit value` | `-l value`| Max tree depth (default: -1)
` --flat ` |` -f` | Print a flat list of secrets (default: false)
` --folders` | `-d` | Print a flat list of folders (default: false)
` --strip-prefix` | `-s` | Strip prefix from filtered entries (default: false)
`--flat` |`-f` | Print a flat list of secrets (default: false)
`--folders` | `-d` | Print a flat list of folders (default: false)
`--strip-prefix` | `-s` | Strip prefix from filtered entries (default: false)

The `--flat` and `--folders` flags provide a plaintext list of the entries located at
the given prefix (default prefix being the root `/`). They are notably used to produce the
completion results.
The `--flat` and `--folders` flags provide a plaintext list of the entries located at
the given prefix (default prefix being the root `/`). They are notably used to produce the
completion results.
The `--flat` one will list all entries, one per line, using its full path.
The `--folders` one will display all the folders, one per line, recursively per level.
The `--folders` one will display all the folders, one per line, recursively per level.
For instance an entry `folder/sub/entry` would cause it to list both:

```bash
$ gopass list --folders
folder
folder/sub
```

whereas `gopass list --flat` would have just displayed one line: `folder/sub/entry`.

The `--strip-prefix` flag is meant to be used along with `--flat` or `--folders`.
It will list the relative path from the current prefix, removing the said prefix,
It will list the relative path from the current prefix, removing the said prefix,
instead of listing the relative paths from the root.
For instance on entry `folder/sub/entry`, running `gopass ls -f -s folder` would display
only `sub/entry` instead of `folder/sub/entry`.

The `--limit` flag starts counting its depth from the root store, which means that
The `--limit` flag starts counting its depth from the root store, which means that
a depth of 0 only lists the items in the root gopass store:

```bash
$ gopass list -l 0
gopass
├── bar/
├── foo/
└── test (/home/user/.local/share/gopass/stores/substore1)
```

A value of 1 would list all the items in the root, plus their sub-items but no more:

```bash
$ gopass list -l 1
gopass
Expand All @@ -63,7 +68,9 @@ gopass
└── test (/home/user/.local/share/gopass/stores/substore1)
└── foo
```
A negative value lists all the items without any depth limit.

A negative value lists all the items without any depth limit.

```bash
$ gopass list -l -1
gopass
Expand All @@ -80,6 +87,7 @@ gopass
```

The flags can be used together: `gopass -l 1 -d` will list only the folders up to a depth of 1:

```bash
$ gopass list -l 1 -d
bar/
Expand All @@ -90,18 +98,20 @@ test/foo/
```

## Shadowing

It is possible to have a path that is both an entry and a folder. In that case the list command
will always display the folder and the entry is "shadowed", but it can still be accessed using
will display the folder with a marker of `(shadowed)`, it can still be accessed using
`gopass show path/to/it`, while the content of the folder can be listed using `gopass list path/to/it`.

It should also be noted that the `mount` command can completely "shadow" an entry in a password store,
simply by having the same name and this entry and its subentries will not be visible
simply by having the same name and this entry and its subentries will not be visible
using `ls` anymore until the substore is unmounted.
The entries shadowed by a mount will not show up in a search and cannot be accessed at all without unmounting.

For instance in our example above, maybe there is an entry test/zaz in the root store,
but since the substore is mounted as `test/`, it only displays the content of the substore.
For instance in our example above, maybe there is an entry test/zaz in the root store,
but since the substore is mounted as `test/`, it only displays the content of the substore.
Unmounting it reveals its shadowed entries:

```bash
$ gopass list test
test/
Expand Down
60 changes: 36 additions & 24 deletions internal/action/list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,53 +123,65 @@ func TestListLimit(t *testing.T) { //nolint:paralleltest
assert.Equal(t, want, buf.String())
buf.Reset()

assert.NoError(t, act.List(gptest.CliCtxWithFlags(ctx, t, map[string]string{"folders": "true", "limit": "0"})))
want = `foo/
t.Run("folders-limit-0", func(t *testing.T) { //nolint:paralleltest
assert.NoError(t, act.List(gptest.CliCtxWithFlags(ctx, t, map[string]string{"folders": "true", "limit": "0"})))
want = `foo/
foo2/
`
assert.Equal(t, want, buf.String())
buf.Reset()
assert.Equal(t, want, buf.String())
buf.Reset()
})

assert.NoError(t, act.List(gptest.CliCtxWithFlags(ctx, t, map[string]string{"folders": "true", "limit": "1"})))
want = `foo/
t.Run("folders-limit-1", func(t *testing.T) { //nolint:paralleltest
assert.NoError(t, act.List(gptest.CliCtxWithFlags(ctx, t, map[string]string{"folders": "true", "limit": "1"})))
want = `foo/
foo/zen/
foo2/
`
assert.Equal(t, want, buf.String())
buf.Reset()
assert.Equal(t, want, buf.String())
buf.Reset()
})

assert.NoError(t, act.List(gptest.CliCtxWithFlags(ctx, t, map[string]string{"folders": "true", "limit": "-1"})))
want = `foo/
t.Run("folders-limit--1", func(t *testing.T) { //nolint:paralleltest
assert.NoError(t, act.List(gptest.CliCtxWithFlags(ctx, t, map[string]string{"folders": "true", "limit": "-1"})))
want = `foo/
foo/zen/
foo/zen/baz/
foo2/
`
assert.Equal(t, want, buf.String())
buf.Reset()
assert.Equal(t, want, buf.String())
buf.Reset()
})

assert.NoError(t, act.List(gptest.CliCtxWithFlags(ctx, t, map[string]string{"flat": "true", "limit": "-1"})))
want = `foo/bar
t.Run("flat-limit--1", func(t *testing.T) { //nolint:paralleltest
assert.NoError(t, act.List(gptest.CliCtxWithFlags(ctx, t, map[string]string{"flat": "true", "limit": "-1"})))
want = `foo/bar
foo/zen/baz/bar
foo2/bar2
`
assert.Equal(t, want, buf.String())
buf.Reset()
assert.Equal(t, want, buf.String())
buf.Reset()
})

assert.NoError(t, act.List(gptest.CliCtxWithFlags(ctx, t, map[string]string{"flat": "true", "limit": "0"})))
want = `foo/
t.Run("folders-limit-0", func(t *testing.T) { //nolint:paralleltest
assert.NoError(t, act.List(gptest.CliCtxWithFlags(ctx, t, map[string]string{"flat": "true", "limit": "0"})))
want = `foo/
foo2/
`
assert.Equal(t, want, buf.String())
buf.Reset()
assert.Equal(t, want, buf.String())
buf.Reset()
})

assert.NoError(t, act.List(gptest.CliCtxWithFlags(ctx, t, map[string]string{"flat": "true", "limit": "2"})))
want = `foo/bar
t.Run("folders-limit-2", func(t *testing.T) { //nolint:paralleltest
assert.NoError(t, act.List(gptest.CliCtxWithFlags(ctx, t, map[string]string{"flat": "true", "limit": "2"})))
want = `foo/bar
foo/zen/baz/
foo2/bar2
`

assert.Equal(t, want, buf.String())
buf.Reset()
assert.Equal(t, want, buf.String())
buf.Reset()
})
}

func TestRedirectPager(t *testing.T) { //nolint:paralleltest
Expand Down
4 changes: 4 additions & 0 deletions internal/store/root/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/gopasspw/gopass/internal/out"
"github.com/gopasspw/gopass/internal/store"
"github.com/gopasspw/gopass/internal/tree"
"github.com/gopasspw/gopass/pkg/debug"
)

// List will return a flattened list of all tree entries.
Expand Down Expand Up @@ -61,7 +62,9 @@ func (r *Store) Tree(ctx context.Context) (*tree.Root, error) {
return nil, err
}

debug.Log("[root] adding files: %q", sf)
addFileFunc(sf...)
debug.Log("[root] Tree: %s", root.Format(-1))
addTplFunc(r.store.ListTemplates(ctx, "")...)

mps := r.MountPoints()
Expand All @@ -82,6 +85,7 @@ func (r *Store) Tree(ctx context.Context) (*tree.Root, error) {
return nil, fmt.Errorf("failed to add file: %w", err)
}

debug.Log("[%s] adding files: %q", alias, sf)
addFileFunc(sf...)
addTplFunc(substore.ListTemplates(ctx, alias)...)
}
Expand Down
56 changes: 48 additions & 8 deletions internal/tree/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ package tree

import (
"bytes"

"github.com/gopasspw/gopass/pkg/debug"
)

// Node is a tree node.
type Node struct {
Name string
Type string
Leaf bool
Template bool
Mount bool
Path string
Expand Down Expand Up @@ -40,7 +42,7 @@ func (n Node) Equals(other Node) bool {
return false
}

if n.Type != other.Type {
if n.Leaf != other.Leaf {
return false
}

Expand All @@ -59,6 +61,40 @@ func (n Node) Equals(other Node) bool {
return true
}

func (n Node) Merge(other Node) *Node {
r := Node{
Name: n.Name,
Leaf: n.Leaf,
Template: n.Template,
Mount: n.Mount,
Path: n.Path,
Subtree: n.Subtree,
}

// can't change name
if other.Leaf {
r.Leaf = true
}
if other.Template {
r.Template = true
}
if other.Mount {
r.Mount = true
r.Leaf = false
r.Path = other.Path
r.Template = false
// the subtree from the mount overlays (shadows) the original tree
r.Subtree = other.Subtree
}
// can't change path
if r.Subtree == nil && other.Subtree != nil {
r.Subtree = other.Subtree
}
debug.Log("merged %+v and %+v into %+v", n, other, r)

return &r
}

// format returns a pretty printed string of all nodes in and below
// this node, e.g. `├── baz`.
func (n *Node) format(prefix string, last bool, maxDepth, curDepth int) string {
Expand Down Expand Up @@ -86,7 +122,7 @@ func (n *Node) format(prefix string, last bool, maxDepth, curDepth int) string {
switch {
case n.Mount:
_, _ = out.WriteString(colMount(n.Name + " (" + n.Path + ")"))
case n.Type == "dir":
case n.Subtree != nil:
_, _ = out.WriteString(colDir(n.Name + sep))
default:
_, _ = out.WriteString(n.Name)
Expand All @@ -95,6 +131,10 @@ func (n *Node) format(prefix string, last bool, maxDepth, curDepth int) string {
if n.Template {
_, _ = out.WriteString(" " + colTpl("(template)"))
}
// mark shadowed entries
if n.Leaf && n.Subtree != nil && !n.Mount {
_, _ = out.WriteString(" " + colShadow("(shadowed)"))
}
// finish this output
_, _ = out.WriteString("\n")

Expand All @@ -113,7 +153,7 @@ func (n *Node) format(prefix string, last bool, maxDepth, curDepth int) string {

// Len returns the length of this subtree.
func (n *Node) Len() int {
if n.Type == "file" {
if n.Subtree == nil {
return 1
}

Expand All @@ -137,17 +177,17 @@ func (n *Node) list(prefix string, maxDepth, curDepth int, files bool) []string

prefix += n.Name

out := make([]string, 0, n.Len())
// if it's a file and we are looking for files
if n.Type == "file" && files {
if n.Subtree == nil && files {
// we return the file
return []string{prefix}
} else if curDepth == maxDepth && n.Type != "file" {
out = append(out, prefix)
} else if curDepth == maxDepth && n.Subtree != nil {
// otherwise if we are "at the bottom" and it's not a file
// we return the directory name with a separator at the end
return []string{prefix + sep}
}

out := make([]string, 0, n.Len())
// if we don't have subitems, then it's a leaf and we return
// (notice that this is what ends the recursion when maxDepth is set to -1)
if n.Subtree == nil {
Expand Down
Loading