@@ -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{
9696var commentPrefix string
9797
9898func 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