Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
70 changes: 54 additions & 16 deletions pass/cmd/find.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"

"github.com/mandu/tools/pass/cmd/tree"
Expand All @@ -18,23 +19,19 @@ var findCmd = &cobra.Command{
Long: `Search for passwords containing the given string anywhere in their path.`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
flat, _ := cmd.Flags().GetBool("flat")
noTree, _ := cmd.Flags().GetBool("no-tree")
return findPasswords(args[0], flat || noTree)
// Default to tree view for pass find command, flat view for 'ls' argument
useFlat := args[0] == "ls"
return findPasswords(args[0], useFlat)
},
}

// Flags for find command
var (
ignoreCaseFlag bool
flatFlag bool
noTreeFlag bool
)

func addFindCmd() {
findCmd.Flags().BoolVarP(&ignoreCaseFlag, "ignore-case", "i", false, "Case-insensitive search")
findCmd.Flags().BoolVarP(&flatFlag, "flat", "f", false, "Output flat list instead of tree")
findCmd.Flags().BoolVar(&noTreeFlag, "no-tree", false, "Output flat list instead of tree")
rootCmd.AddCommand(findCmd)
}

Expand Down Expand Up @@ -86,6 +83,53 @@ func findPasswords(searchString string, flat bool) error {
return nil
}

// Special case: "ls" should show all passwords in flat view
if searchString == "ls" {
// Collect all passwords
var results []string
err := filepath.Walk(storeDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}

// Skip .git directory
if info.IsDir() && info.Name() == ".git" {
return filepath.SkipDir
}

// Only process .gpg files
if !info.IsDir() && strings.HasSuffix(info.Name(), ".gpg") {
// Get relative path from store directory
relPath, err := filepath.Rel(storeDir, path)
if err != nil {
return err
}

// Normalize path separators
relPath = filesystem.NormalizePathForDisplay(relPath)

// Strip .gpg extension
passwordPath := strings.TrimSuffix(relPath, ".gpg")
results = append(results, passwordPath)
}

return nil
})

if err != nil {
return fmt.Errorf("pass: failed to walk directory: %v", err)
}

// Sort results
sort.Strings(results)

// Print results in flat view
for _, result := range results {
fmt.Println(result)
}
return nil
}

// Prepare search string
target := searchString
if ignoreCaseFlag {
Expand Down Expand Up @@ -137,22 +181,16 @@ func findPasswords(searchString string, flat bool) error {
}

// Sort results
for i := 0; i < len(results)-1; i++ {
for j := 0; j < len(results)-i-1; j++ {
if results[j] > results[j+1] {
results[j], results[j+1] = results[j+1], results[j]
}
}
}
sort.Strings(results)

// Print results
if flat {
// Original flat list output
// Flat view
for _, result := range results {
fmt.Println(result)
}
} else {
// Tree view output
// Tree view
if len(results) > 0 {
treeRoot := tree.BuildTreeFromPaths(results)
// Render each top-level child with empty prefix
Expand Down
35 changes: 28 additions & 7 deletions pass/cmd/tree/tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,19 @@ import (

// TreeNode represents a node in a hierarchical tree structure.
type TreeNode struct {
Name string
IsDir bool
Children []*TreeNode
Name string
IsDir bool
IsPassword bool // True if this node represents a password (i.e., was the last component of a path)
Children []*TreeNode
}

// NewTreeNode creates a new tree node with the given name and directory flag.
func NewTreeNode(name string, isDir bool) *TreeNode {
return &TreeNode{
Name: name,
IsDir: isDir,
Children: []*TreeNode{},
Name: name,
IsDir: isDir,
IsPassword: false,
Children: []*TreeNode{},
}
}

Expand All @@ -36,9 +38,17 @@ func (n *TreeNode) AddChild(child *TreeNode) {

// FindOrCreateChild finds a child by name or creates it if not found.
// Returns the child node (existing or newly created).
// If the child already exists and isDir is true, the existing node's IsDir flag
// is updated to true to handle the case where a path component is both a file
// and a directory (e.g., "dji-mimo" file and "dji-mimo/subdir" directory).
func (n *TreeNode) FindOrCreateChild(name string, isDir bool) *TreeNode {
for _, child := range n.Children {
if child.Name == name {
// Update IsDir to true if we're adding children under this node
// This handles the case where the same name is both a file and a directory
if isDir {
child.IsDir = true
}
return child
}
}
Expand Down Expand Up @@ -92,15 +102,26 @@ func BuildTreeFromPaths(paths []string) *TreeNode {
// Remove .gpg extension if present
path = strings.TrimSuffix(path, ".gpg")

// Split into path components
// Split into path components and filter out empty ones
components := strings.Split(path, "/")
var nonEmptyComponents []string
for _, comp := range components {
if comp != "" {
nonEmptyComponents = append(nonEmptyComponents, comp)
}
}
components = nonEmptyComponents

// Build tree structure from components
current := root
for i, component := range components {
// Only the last component is a file, others are directories
isDir := i < len(components)-1
current = current.FindOrCreateChild(component, isDir)
// Mark the last component (the password file) as a password
if !isDir {
current.IsPassword = true
}
}
}

Expand Down
Loading
Loading