22//
33// # Package Extraction Framework
44//
5- // This file provides a generic framework for extracting package names from command strings.
6- // The PackageExtractor type can be configured to handle different package managers
7- // (npm, pip, uv, go, etc.) with minimal code duplication.
5+ // This file provides a reusable, configurable framework for extracting package names
6+ // from command strings. The PackageExtractor type eliminates code duplication by
7+ // providing a single abstraction that handles different package managers (npm, pip,
8+ // uv, go, etc.) with minimal configuration.
89//
9- // # Usage Example
10+ // # Purpose and Benefits
11+ //
12+ // The PackageExtractor pattern exists to:
13+ // - Prevent code duplication across package manager extraction functions
14+ // - Provide a consistent interface for command parsing
15+ // - Centralize package name extraction logic
16+ // - Reduce maintenance burden by having a single implementation
17+ //
18+ // # Usage Pattern
19+ //
20+ // Instead of implementing custom extraction logic, configure a PackageExtractor
21+ // with the appropriate settings for your package manager:
1022//
1123// extractor := PackageExtractor{
1224// CommandNames: []string{"pip", "pip3"},
1628// packages := extractor.ExtractPackages("pip install requests")
1729// // Returns: []string{"requests"}
1830//
19- // For package-specific extraction, see npm.go, pip.go, and dependabot.go.
20- // For validation, see validation.go.
31+ // # Package Manager Examples
32+ //
33+ // NPM (npx):
34+ //
35+ // extractor := PackageExtractor{
36+ // CommandNames: []string{"npx"},
37+ // RequiredSubcommand: "", // No subcommand needed
38+ // TrimSuffixes: "&|;",
39+ // }
40+ // packages := extractor.ExtractPackages("npx @playwright/mcp@latest")
41+ // // Returns: []string{"@playwright/mcp@latest"}
42+ //
43+ // Python (pip):
44+ //
45+ // extractor := PackageExtractor{
46+ // CommandNames: []string{"pip", "pip3"},
47+ // RequiredSubcommand: "install", // Must have "install" subcommand
48+ // TrimSuffixes: "&|;",
49+ // }
50+ // packages := extractor.ExtractPackages("pip install requests==2.28.0")
51+ // // Returns: []string{"requests==2.28.0"}
52+ //
53+ // Go:
54+ //
55+ // installExtractor := PackageExtractor{
56+ // CommandNames: []string{"go"},
57+ // RequiredSubcommand: "install",
58+ // TrimSuffixes: "&|;",
59+ // }
60+ // packages := installExtractor.ExtractPackages("go install github.com/user/tool@v1.0.0")
61+ // // Returns: []string{"github.com/user/tool@v1.0.0"}
62+ //
63+ // Python (uv):
64+ //
65+ // extractor := PackageExtractor{
66+ // CommandNames: []string{"uvx"},
67+ // RequiredSubcommand: "",
68+ // TrimSuffixes: "&|;",
69+ // }
70+ // packages := extractor.ExtractPackages("uvx black")
71+ // // Returns: []string{"black"}
72+ //
73+ // # Best Practices
74+ //
75+ // - ALWAYS use PackageExtractor instead of reimplementing extraction logic
76+ // - Configure CommandNames for all variations of the command (e.g., ["pip", "pip3"])
77+ // - Set RequiredSubcommand only when the package manager requires it (e.g., "install" for pip)
78+ // - Include common shell operators in TrimSuffixes (typically "&|;")
79+ // - For special cases, use the exported FindPackageName method to reuse logic
80+ //
81+ // # Configuration Details
82+ //
83+ // - CommandNames: List of command names to match (case-sensitive)
84+ // - RequiredSubcommand: Subcommand that must appear before the package name
85+ // (empty string if package comes directly after command)
86+ // - TrimSuffixes: Characters to remove from the end of package names
87+ // (useful for shell operators like "&", "|", ";")
88+ //
89+ // For package-specific extraction implementations, see:
90+ // - npm.go (npx packages)
91+ // - pip.go (pip and uv packages)
92+ // - dependabot.go (go packages)
93+ //
94+ // For package validation, see validation.go.
2195package workflow
2296
2397import (
@@ -27,42 +101,118 @@ import (
27101// PackageExtractor provides a configurable framework for extracting package names
28102// from command-line strings. It can be configured to handle different package
29103// managers (npm, pip, uv, go) by setting the appropriate command names and options.
104+ //
105+ // This type is the core of the package extraction pattern. Use it instead of
106+ // writing custom parsing logic to avoid code duplication.
107+ //
108+ // Configuration:
109+ // - Set CommandNames to all variants of the command (e.g., ["pip", "pip3"])
110+ // - Set RequiredSubcommand if the package manager requires a subcommand
111+ // (e.g., "install" for pip, "get" for go)
112+ // - Set TrimSuffixes to remove shell operators from package names
113+ // (typically "&|;")
114+ //
115+ // Examples:
116+ //
117+ // // For npx (no subcommand):
118+ // extractor := PackageExtractor{
119+ // CommandNames: []string{"npx"},
120+ // RequiredSubcommand: "",
121+ // TrimSuffixes: "&|;",
122+ // }
123+ //
124+ // // For pip (with "install" subcommand):
125+ // extractor := PackageExtractor{
126+ // CommandNames: []string{"pip", "pip3"},
127+ // RequiredSubcommand: "install",
128+ // TrimSuffixes: "&|;",
129+ // }
30130type PackageExtractor struct {
31- // CommandNames is the list of command names to look for (e.g., ["pip", "pip3"])
131+ // CommandNames is the list of command names to look for.
132+ // Include all variations of the command (e.g., ["pip", "pip3"]).
133+ // Matching is case-sensitive and exact.
134+ //
135+ // Examples:
136+ // - ["npx"] for npm packages
137+ // - ["pip", "pip3"] for Python packages
138+ // - ["go"] for Go packages
139+ // - ["uvx"] for uv tool packages
32140 CommandNames []string
33141
34142 // RequiredSubcommand is the subcommand that must follow the command name
35- // (e.g., "install" for pip). If empty, the package name is expected immediately
36- // after the command name (e.g., "npx <package>").
143+ // before the package name appears. Set to empty string if the package name
144+ // comes directly after the command.
145+ //
146+ // Examples:
147+ // - "install" for pip (pip install <package>)
148+ // - "get" for go (go get <package>)
149+ // - "" for npx (npx <package>)
37150 RequiredSubcommand string
38151
39- // TrimSuffixes is a string of characters to trim from the end of package names
40- // (e.g., "&|;" for shell operators)
152+ // TrimSuffixes is a string of characters to trim from the end of package names.
153+ // This is useful for removing shell operators that may appear after package names
154+ // in command strings.
155+ //
156+ // Recommended value: "&|;" (covers common shell operators)
157+ //
158+ // Examples:
159+ // - "pip install requests;" → extracts "requests" (trims ";")
160+ // - "npx playwright&" → extracts "playwright" (trims "&")
41161 TrimSuffixes string
42162}
43163
44164// ExtractPackages extracts package names from command strings using the configured
45165// extraction rules. It processes multi-line command strings and returns all found
46166// package names.
47167//
168+ // This is the main entry point for package extraction. Call this method with your
169+ // command string(s) after configuring the PackageExtractor.
170+ //
48171// The extraction process:
49172// 1. Split commands by newlines
50173// 2. Split each line into words
51- // 3. Find command name matches
174+ // 3. Find command name matches (from CommandNames)
52175// 4. If RequiredSubcommand is set, look for that subcommand
53176// 5. Skip flags (words starting with -)
54177// 6. Extract package name and trim configured suffixes
55178// 7. Return first package found per command invocation
56179//
57- // Example usage:
180+ // Multi-line commands are supported:
181+ //
182+ // commands := `pip install requests
183+ // pip install numpy`
184+ // packages := extractor.ExtractPackages(commands)
185+ // // Returns: []string{"requests", "numpy"}
186+ //
187+ // Flags are automatically skipped:
188+ //
189+ // packages := extractor.ExtractPackages("pip install --upgrade requests")
190+ // // Returns: []string{"requests"}
191+ //
192+ // Shell operators are automatically trimmed:
193+ //
194+ // packages := extractor.ExtractPackages("npx playwright;")
195+ // // Returns: []string{"playwright"}
196+ //
197+ // Example usage with pip:
58198//
59199// extractor := PackageExtractor{
60- // CommandNames: []string{"pip", "pip3"},
200+ // CommandNames: []string{"pip", "pip3"},
61201// RequiredSubcommand: "install",
62- // TrimSuffixes: "&|;",
202+ // TrimSuffixes: "&|;",
63203// }
64204// packages := extractor.ExtractPackages("pip install requests==2.28.0")
65205// // Returns: []string{"requests==2.28.0"}
206+ //
207+ // Example usage with npx:
208+ //
209+ // extractor := PackageExtractor{
210+ // CommandNames: []string{"npx"},
211+ // RequiredSubcommand: "",
212+ // TrimSuffixes: "&|;",
213+ // }
214+ // packages := extractor.ExtractPackages("npx @playwright/mcp@latest")
215+ // // Returns: []string{"@playwright/mcp@latest"}
66216func (pe * PackageExtractor ) ExtractPackages (commands string ) []string {
67217 var packages []string
68218 lines := strings .Split (commands , "\n " )
0 commit comments