Skip to content

Commit b547be2

Browse files
committed
Fix min-score help text
1 parent 9cd96f0 commit b547be2

3 files changed

Lines changed: 140 additions & 84 deletions

File tree

cmd/quickdup/flags.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package main
2+
3+
import "flag"
4+
5+
const minScoreHelpText = "Minimum score to report (strategy-specific score)"
6+
7+
type cliFlags struct {
8+
path *string
9+
filePath *string
10+
ext *string
11+
minOccur *int
12+
minScore *int
13+
minSize *int
14+
maxSize *int
15+
minSimilarity *float64
16+
topN *int
17+
comment *string
18+
noCache *bool
19+
githubAnnotations *bool
20+
githubLevel *string
21+
gitDiff *string
22+
exclude *string
23+
compare *string
24+
strategyName *string
25+
selectRange *string
26+
keepOverlaps *bool
27+
debug *bool
28+
timeoutSeconds *int
29+
}
30+
31+
func registerFlags(fs *flag.FlagSet) cliFlags {
32+
return cliFlags{
33+
path: fs.String("path", ".", "Path to scan"),
34+
filePath: fs.String("file", "", "Scan a single file (overrides --path)"),
35+
ext: fs.String("ext", ".go", "File extension to scan"),
36+
minOccur: fs.Int("min", 2, "Minimum occurrences to report"),
37+
minScore: fs.Int("min-score", 5, minScoreHelpText),
38+
minSize: fs.Int("min-size", 3, "Base pattern size to start growing from"),
39+
maxSize: fs.Int("max-size", 0, "Maximum pattern size to grow to (0 = no limit)"),
40+
minSimilarity: fs.Float64("min-similarity", 0.75, "Minimum token similarity between occurrences (0.0-1.0)"),
41+
topN: fs.Int("top", 10, "Show top N matches by pattern length"),
42+
comment: fs.String("comment", "", "Override comment prefix (auto-detected by extension)"),
43+
noCache: fs.Bool("no-cache", false, "Disable incremental caching, force full re-parse"),
44+
githubAnnotations: fs.Bool("github-annotations", false, "Output GitHub Actions annotations for inline PR comments"),
45+
githubLevel: fs.String("github-level", "warning", "GitHub annotation level: notice, warning, or error"),
46+
gitDiff: fs.String("git-diff", "", "Only annotate files changed vs this git ref (e.g., origin/main)"),
47+
exclude: fs.String("exclude", "", "Exclude files matching patterns (comma-separated, e.g., '*.pb.go,*_gen.go')"),
48+
compare: fs.String("compare", "", "Compare duplicates between two commits (format: base..head)"),
49+
strategyName: fs.String("strategy", "normalized-indent", "Detection strategy: word-indent, normalized-indent, word-only, inlineable"),
50+
selectRange: fs.String("select", "", "Show detailed output for patterns (format: skip..limit, e.g., 0..5)"),
51+
keepOverlaps: fs.Bool("keep-overlaps", false, "Keep overlapping occurrences (don't prune adjacent matches)"),
52+
debug: fs.Bool("debug", false, "Print verbose progress for long-running phases"),
53+
timeoutSeconds: fs.Int("timeout", 20, "Hard timeout in seconds (0 disables)"),
54+
}
55+
}

cmd/quickdup/flags_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package main
2+
3+
import (
4+
"flag"
5+
"testing"
6+
)
7+
8+
func TestMinScoreHelpText(t *testing.T) {
9+
fs := flag.NewFlagSet("quickdup", flag.ContinueOnError)
10+
registerFlags(fs)
11+
12+
minScoreFlag := fs.Lookup("min-score")
13+
if minScoreFlag == nil {
14+
t.Fatal("min-score flag not registered")
15+
}
16+
17+
want := "Minimum score to report (strategy-specific score)"
18+
if minScoreFlag.Usage != want {
19+
t.Fatalf("min-score help text = %q, want %q", minScoreFlag.Usage, want)
20+
}
21+
}

cmd/quickdup/main.go

Lines changed: 64 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -43,28 +43,28 @@ var commentPrefixes = map[string]string{
4343
".v": "//",
4444
".zig": "//",
4545
// Hash-style
46-
".py": "#",
47-
".rb": "#",
48-
".sh": "#",
49-
".bash": "#",
50-
".zsh": "#",
51-
".pl": "#",
52-
".pm": "#",
53-
".r": "#",
54-
".R": "#",
55-
".yaml": "#",
56-
".yml": "#",
57-
".toml": "#",
58-
".tf": "#",
59-
".cmake": "#",
60-
".make": "#",
61-
".mk": "#",
62-
".ps1": "#",
63-
".nim": "#",
64-
".jl": "#",
65-
".ex": "#",
66-
".exs": "#",
67-
".cr": "#",
46+
".py": "#",
47+
".rb": "#",
48+
".sh": "#",
49+
".bash": "#",
50+
".zsh": "#",
51+
".pl": "#",
52+
".pm": "#",
53+
".r": "#",
54+
".R": "#",
55+
".yaml": "#",
56+
".yml": "#",
57+
".toml": "#",
58+
".tf": "#",
59+
".cmake": "#",
60+
".make": "#",
61+
".mk": "#",
62+
".ps1": "#",
63+
".nim": "#",
64+
".jl": "#",
65+
".ex": "#",
66+
".exs": "#",
67+
".cr": "#",
6868
// Double-dash style
6969
".sql": "--",
7070
".lua": "--",
@@ -96,38 +96,18 @@ var commentPrefixes = map[string]string{
9696
var commentPrefix string
9797

9898
func main() {
99-
path := flag.String("path", ".", "Path to scan")
100-
filePath := flag.String("file", "", "Scan a single file (overrides --path)")
101-
ext := flag.String("ext", ".go", "File extension to scan")
102-
minOccur := flag.Int("min", 2, "Minimum occurrences to report")
103-
minScore := flag.Int("min-score", 5, "Minimum score to report (uniqueWords × adjusted similarity)")
104-
minSize := flag.Int("min-size", 3, "Base pattern size to start growing from")
105-
maxSize := flag.Int("max-size", 0, "Maximum pattern size to grow to (0 = no limit)")
106-
minSimilarity := flag.Float64("min-similarity", 0.75, "Minimum token similarity between occurrences (0.0-1.0)")
107-
topN := flag.Int("top", 10, "Show top N matches by pattern length")
108-
comment := flag.String("comment", "", "Override comment prefix (auto-detected by extension)")
109-
noCache := flag.Bool("no-cache", false, "Disable incremental caching, force full re-parse")
110-
githubAnnotations := flag.Bool("github-annotations", false, "Output GitHub Actions annotations for inline PR comments")
111-
githubLevel := flag.String("github-level", "warning", "GitHub annotation level: notice, warning, or error")
112-
gitDiff := flag.String("git-diff", "", "Only annotate files changed vs this git ref (e.g., origin/main)")
113-
exclude := flag.String("exclude", "", "Exclude files matching patterns (comma-separated, e.g., '*.pb.go,*_gen.go')")
114-
compare := flag.String("compare", "", "Compare duplicates between two commits (format: base..head)")
115-
strategyName := flag.String("strategy", "normalized-indent", "Detection strategy: word-indent, normalized-indent, word-only, inlineable")
116-
selectRange := flag.String("select", "", "Show detailed output for patterns (format: skip..limit, e.g., 0..5)")
117-
keepOverlaps := flag.Bool("keep-overlaps", false, "Keep overlapping occurrences (don't prune adjacent matches)")
118-
debug := flag.Bool("debug", false, "Print verbose progress for long-running phases")
119-
timeoutSeconds := flag.Int("timeout", 20, "Hard timeout in seconds (0 disables)")
99+
flags := registerFlags(flag.CommandLine)
120100
flag.Parse()
121-
debugEnabled = *debug
122-
if *timeoutSeconds > 0 {
123-
timeout := time.Duration(*timeoutSeconds) * time.Second
101+
debugEnabled = *flags.debug
102+
if *flags.timeoutSeconds > 0 {
103+
timeout := time.Duration(*flags.timeoutSeconds) * time.Second
124104
go func() {
125105
time.Sleep(timeout)
126106
fmt.Fprintf(os.Stderr, "Error: timed out after %s\n", timeout)
127107
os.Exit(1)
128108
}()
129109
}
130-
if *maxSize > 0 && *maxSize < *minSize {
110+
if *flags.maxSize > 0 && *flags.maxSize < *flags.minSize {
131111
fmt.Fprintf(os.Stderr, "Error: --max-size must be >= --min-size\n")
132112
os.Exit(1)
133113
}
@@ -139,34 +119,34 @@ func main() {
139119
"word-only": &WordOnlyStrategy{},
140120
"inlineable": &InlineableStrategy{},
141121
}
142-
if s, ok := strategies[*strategyName]; ok {
122+
if s, ok := strategies[*flags.strategyName]; ok {
143123
activeStrategy = s
144124
} else {
145-
fmt.Fprintf(os.Stderr, "Unknown strategy: %s\n", *strategyName)
125+
fmt.Fprintf(os.Stderr, "Unknown strategy: %s\n", *flags.strategyName)
146126
os.Exit(1)
147127
}
148128

149129
// Handle compare mode
150-
if *compare != "" {
151-
parts := strings.Split(*compare, "..")
130+
if *flags.compare != "" {
131+
parts := strings.Split(*flags.compare, "..")
152132
if len(parts) != 2 {
153133
fmt.Fprintf(os.Stderr, "Error: --compare requires format 'base..head'\n")
154134
os.Exit(1)
155135
}
156136
baseRef, headRef := parts[0], parts[1]
157137
// Extract subdir from path if it's not "."
158138
subdir := ""
159-
if *path != "." {
160-
subdir = *path
139+
if *flags.path != "." {
140+
subdir = *flags.path
161141
}
162-
runCompare(baseRef, headRef, subdir, *ext, *exclude, *minOccur, *minScore, *minSize, *maxSize, *minSimilarity, *strategyName)
142+
runCompare(baseRef, headRef, subdir, *flags.ext, *flags.exclude, *flags.minOccur, *flags.minScore, *flags.minSize, *flags.maxSize, *flags.minSimilarity, *flags.strategyName)
163143
return
164144
}
165145

166146
// Parse exclude patterns
167147
var excludePatterns []string
168-
if *exclude != "" {
169-
for _, p := range strings.Split(*exclude, ",") {
148+
if *flags.exclude != "" {
149+
for _, p := range strings.Split(*flags.exclude, ",") {
170150
p = strings.TrimSpace(p)
171151
if p != "" {
172152
excludePatterns = append(excludePatterns, p)
@@ -176,8 +156,8 @@ func main() {
176156

177157
// Build set of changed files if --git-diff is specified
178158
changedFiles := make(map[string]bool)
179-
if *gitDiff != "" {
180-
cmd := exec.Command("git", "diff", "--name-only", *gitDiff)
159+
if *flags.gitDiff != "" {
160+
cmd := exec.Command("git", "diff", "--name-only", *flags.gitDiff)
181161
output, err := cmd.Output()
182162
if err == nil {
183163
for _, line := range strings.Split(strings.TrimSpace(string(output)), "\n") {
@@ -190,13 +170,13 @@ func main() {
190170

191171
startTime := time.Now()
192172

193-
folder := *path
194-
extension := *ext
173+
folder := *flags.path
174+
extension := *flags.ext
195175
singleFile := ""
196-
if *filePath != "" {
197-
singleFile = *filePath
198-
} else if info, err := os.Stat(*path); err == nil && !info.IsDir() {
199-
singleFile = *path
176+
if *flags.filePath != "" {
177+
singleFile = *flags.filePath
178+
} else if info, err := os.Stat(*flags.path); err == nil && !info.IsDir() {
179+
singleFile = *flags.path
200180
}
201181
if singleFile != "" {
202182
info, err := os.Stat(singleFile)
@@ -211,22 +191,22 @@ func main() {
211191
folder = filepath.Dir(singleFile)
212192
extension = filepath.Ext(singleFile)
213193
if extension == "" {
214-
extension = *ext
194+
extension = *flags.ext
215195
}
216196
}
217197
extension = strings.ToLower(extension)
218198

219199
// Auto-detect comment prefix from extension, allow override
220-
if *comment != "" {
221-
commentPrefix = *comment
200+
if *flags.comment != "" {
201+
commentPrefix = *flags.comment
222202
} else if prefix, ok := commentPrefixes[extension]; ok {
223203
commentPrefix = prefix
224204
} else {
225205
commentPrefix = "//" // fallback default
226206
}
227207

228208
// Load user-ignored hashes from ignore.json
229-
userIgnored := LoadIgnoredHashes(folder, *strategyName)
209+
userIgnored := LoadIgnoredHashes(folder, *flags.strategyName)
230210
PrintIgnoredPatterns(len(userIgnored))
231211

232212
// First pass: count files
@@ -278,15 +258,15 @@ func main() {
278258

279259
parseStart := time.Now()
280260
var cache *FileCache
281-
if !*noCache {
282-
cache = loadCache(folder, *strategyName)
261+
if !*flags.noCache {
262+
cache = loadCache(folder, *flags.strategyName)
283263
}
284264

285265
fileData, cacheHits, cacheMisses := parseFilesWithCache(files, cache)
286266

287267
// Save updated cache
288-
if !*noCache && cacheMisses > 0 {
289-
saveCache(folder, *strategyName, files, fileData)
268+
if !*flags.noCache && cacheMisses > 0 {
269+
saveCache(folder, *flags.strategyName, files, fileData)
290270
}
291271
parseTime := time.Since(parseStart)
292272

@@ -301,51 +281,51 @@ func main() {
301281
// Phase 2: Pattern detection with growth
302282
detectStart := time.Now()
303283
PrintDetectStart()
304-
patterns := detectPatterns(fileData, len(fileData), *minOccur, *minSize, *maxSize, *keepOverlaps)
284+
patterns := detectPatterns(fileData, len(fileData), *flags.minOccur, *flags.minSize, *flags.maxSize, *flags.keepOverlaps)
305285
detectTime := time.Since(detectStart)
306286
PrintDetectComplete(detectTime)
307287

308288
// Filter and score matches
309289
filterStart := time.Now()
310290
matches, filterStats := FilterPatterns(patterns, FilterConfig{
311-
MinOccur: *minOccur,
312-
MinScore: *minScore,
313-
MinSimilarity: *minSimilarity,
291+
MinOccur: *flags.minOccur,
292+
MinScore: *flags.minScore,
293+
MinSimilarity: *flags.minSimilarity,
314294
UserIgnored: userIgnored,
315295
})
316296
filterTime := time.Since(filterStart)
317297

318298
// Report results
319-
PrintFilterComplete(filterTime, filterStats.SkippedBlocked, filterStats.SkippedLowScore, filterStats.SkippedLowSimilarity, *minScore, *minSimilarity)
299+
PrintFilterComplete(filterTime, filterStats.SkippedBlocked, filterStats.SkippedLowScore, filterStats.SkippedLowSimilarity, *flags.minScore, *flags.minSimilarity)
320300

321-
top := TopN(matches, *topN)
301+
top := TopN(matches, *flags.topN)
322302

323-
if *githubAnnotations {
324-
PrintGitHubAnnotations(top, len(top), *githubLevel, *gitDiff, changedFiles)
303+
if *flags.githubAnnotations {
304+
PrintGitHubAnnotations(top, len(top), *flags.githubLevel, *flags.gitDiff, changedFiles)
325305
}
326306

327307
PrintHotspots(matches)
328308

329-
if *githubAnnotations {
309+
if *flags.githubAnnotations {
330310
elapsed := time.Since(startTime)
331311
PrintTotalSummary(len(matches), len(fileData), totalLines, elapsed)
332312
return
333313
}
334314

335-
outputPath := filepath.Join(folder, ".quickdup", *strategyName+"-results.json")
315+
outputPath := filepath.Join(folder, ".quickdup", *flags.strategyName+"-results.json")
336316
if err := WriteJSONResults(matches, outputPath); err != nil {
337317
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
338318
os.Exit(1)
339319
}
340320

341321
// If --select was provided, show detailed output from the JSON
342-
if *selectRange != "" {
322+
if *flags.selectRange != "" {
343323
patterns, err := ReadJSONResults(outputPath)
344324
if err != nil {
345325
fmt.Fprintf(os.Stderr, "Error reading results: %v\n", err)
346326
os.Exit(1)
347327
}
348-
skip, limit, err := parseSelectRange(*selectRange)
328+
skip, limit, err := parseSelectRange(*flags.selectRange)
349329
if err != nil {
350330
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
351331
os.Exit(1)

0 commit comments

Comments
 (0)