main.go GO 657 lines View on github.com → Search inside
1// SPDX-License-Identifier: MIT23package main45import (6	"errors"7	"fmt"8	"os"9	"runtime"10	"slices"11	"strings"1213	"github.com/boyter/scc/v3/processor"14	"github.com/spf13/cobra"15	"github.com/spf13/pflag"16)1718func printShellCompletion(cmd *cobra.Command, command string) error {19	switch command {20	case "bash":21		return cmd.GenBashCompletionV2(os.Stdout, true)22	case "zsh":23		return cmd.GenZshCompletion(os.Stdout)24	case "fish":25		return cmd.GenFishCompletion(os.Stdout, true)26	case "powershell":27		return cmd.GenPowerShellCompletion(os.Stdout)28	default:29		return errors.New("Unknown shell: " + command)30	}31}3233func printFlagSuggestion(flagSet *pflag.FlagSet, unknownFlag string) {34	flags := processor.GetMostSimilarFlags(flagSet, unknownFlag)35	if len(flags) == 0 {36		return37	}3839	if len(flags) > 1 {40		_, _ = fmt.Fprintf(os.Stderr, "The most similar flags of --%s are:\n", unknownFlag)41	} else {42		_, _ = fmt.Fprintf(os.Stderr, "The most similar flag of --%s is:\n", unknownFlag)43	}4445	for _, flag := range flags {46		_, _ = fmt.Fprintf(os.Stderr, "\t--%s\n", flag)47	}48}4950//go:generate go run scripts/include.go51func main() {52	// f, _ := os.Create("scc.pprof")53	// pprof.StartCPUProfile(f)54	// defer pprof.StopCPUProfile()5556	// Handle --mcp flag before cobra to avoid interfering with stdio57	if slices.Contains(os.Args[1:], "--mcp") {58		startMCPServer()59		return60	}6162	if len(os.Args) == 2 && strings.HasPrefix(os.Args[1], "@") {63		// handle "scc @flags.txt" syntax64		filepath := strings.TrimPrefix(os.Args[1], "@")65		b, err := os.ReadFile(filepath)66		if err != nil {67			fmt.Printf("Error reading flags from a file: %s\n", err)68			os.Exit(1)69		}7071		sb := string(b)72		newArgs := make([]string, 0, strings.Count(sb, "\n")+1)73		for x := range strings.SplitSeq(sb, "\n") {74			newArgs = append(newArgs, strings.TrimSpace(x))75		}76		os.Args = append([]string{os.Args[0]}, newArgs...)77	}7879	rootCmd := &cobra.Command{80		Use:   "scc [flags] [files or directories]",81		Short: "scc [files or directories]",82		Long:  fmt.Sprintf("Sloc, Cloc and Code. Count lines of code in a directory with complexity estimation.\nVersion %s\nBen Boyter <ben@boyter.org> + Contributors", processor.Version),83		Example: `  Count the current directory:84    scc8586  Count a specific folder or file:87    scc myproject/88    scc main.go8990  Count several paths at once:91    scc src/ docs/ README.md9293  Show a per-file breakdown instead of the per-language summary:94    scc --by-file9596  Output as CSV or JSON (e.g. for further processing):97    scc --format csv98    scc --format json -o counts.json99100  Count an unrecognised extension as a known language:101    scc --count-as jsp:html102103  Count files matching a path pattern as a new category (glob by default):104    scc --count-as-pattern '*_spec.rb:Ruby Spec:Ruby'105106  Generate a self-contained HTML infographic report:107    scc --report108    scc --report=out.html --report-title "myrepo" --report-skip cocomo`,109		Version: processor.Version,110		Run: func(cmd *cobra.Command, args []string) {111			processor.DirFilePaths = args112			processor.ConfigureGc()113			processor.ConfigureLazy(true)114115			// Detect if LOCOMO price/tps flags were explicitly set116			processor.LocomoInputPriceSet = cmd.PersistentFlags().Changed("locomo-input-price")117			processor.LocomoOutputPriceSet = cmd.PersistentFlags().Changed("locomo-output-price")118			processor.LocomoTPSSet = cmd.PersistentFlags().Changed("locomo-tps")119			processor.LocomoCyclesSet = cmd.PersistentFlags().Changed("locomo-cycles")120121			if v, err := cmd.PersistentFlags().GetBool("no-fold-authors"); err == nil && v {122				processor.FoldAuthors = false123			}124125			processor.Process()126		},127	}128129	flags := rootCmd.PersistentFlags()130131	flags.BoolVarP(132		&processor.MaxMean,133		"character",134		"m",135		false,136		"calculate max and mean characters per line",137	)138	flags.BoolVarP(139		&processor.Percent,140		"percent",141		"p",142		false,143		"include percentage values in output",144	)145	flags.BoolVarP(146		&processor.UlocMode,147		"uloc",148		"u",149		false,150		"calculate the number of unique lines of code (ULOC) for the project",151	)152	flags.BoolVarP(153		&processor.Dryness,154		"dryness",155		"a",156		false,157		"calculate the DRYness of the project (implies --uloc)",158	)159	flags.BoolVar(160		&processor.DisableCheckBinary,161		"binary",162		false,163		"disable binary file detection",164	)165	flags.BoolVar(166		&processor.Files,167		"by-file",168		false,169		"display output for every file",170	)171	flags.BoolVar(172		&processor.Ci,173		"ci",174		false,175		"enable CI output settings where stdout is ASCII",176	)177	flags.BoolVar(178		&processor.Ignore,179		"no-ignore",180		false,181		"disables .ignore file logic",182	)183	flags.BoolVar(184		&processor.SccIgnore,185		"no-scc-ignore",186		false,187		"disables .sccignore file logic",188	)189	flags.BoolVar(190		&processor.GitIgnore,191		"no-gitignore",192		false,193		"disables .gitignore file logic",194	)195	flags.BoolVar(196		&processor.GitModuleIgnore,197		"no-gitmodule",198		false,199		"disables .gitmodules file logic",200	)201	flags.BoolVar(202		&processor.CountIgnore,203		"count-ignore",204		false,205		"set to allow .gitignore and .ignore files to be counted",206	)207	flags.BoolVar(208		&processor.Debug,209		"debug",210		false,211		"enable debug output",212	)213	flags.StringSliceVar(214		&processor.PathDenyList,215		"exclude-dir",216		[]string{".git", ".hg", ".svn"},217		"directories to exclude",218	)219	flags.IntVar(220		&processor.GcFileCount,221		"file-gc-count",222		10000,223		"number of files to parse before turning the GC on",224	)225	flags.IntVar(226		&processor.FileListQueueSize,227		"file-list-queue-size",228		runtime.NumCPU(),229		"the size of the queue of files found and ready to be read into memory",230	)231	flags.IntVar(232		&processor.FileProcessJobWorkers,233		"file-process-job-workers",234		runtime.NumCPU(),235		"number of goroutine workers that process files collecting stats",236	)237	flags.IntVar(238		&processor.FileSummaryJobQueueSize,239		"file-summary-job-queue-size",240		runtime.NumCPU(),241		"the size of the queue used to hold processed file statistics before formatting",242	)243	flags.IntVar(244		&processor.DirectoryWalkerJobWorkers,245		"directory-walker-job-workers",246		8,247		"controls the maximum number of workers which will walk the directory tree",248	)249	flags.StringVarP(250		&processor.Format,251		"format",252		"f",253		"tabular",254		"set output format [tabular, wide, json, json2, csv, csv-stream, cloc-yaml, html, html-table, sql, sql-insert, openmetrics]",255	)256	flags.StringVar(257		&processor.ReportOut,258		"report",259		"",260		"write a self-contained HTML report; bare flag writes scc-report.html and prompts before overwriting, --report=path/out.html overwrites silently",261	)262	// NoOptDefVal makes a bare `--report` work (no `=value`). runReport263	// compares ReportOut to processor.DefaultReportName to tell "bare264	// flag" apart from an explicit path, so the two must stay in sync.265	flags.Lookup("report").NoOptDefVal = processor.DefaultReportName266	flags.StringVar(267		&processor.ReportSkip,268		"report-skip",269		"",270		"comma-separated sections to omit (cocomo,locomo,hotspots,authors,timeline,files,uloc,linelength,card)",271	)272	flags.StringVar(273		&processor.ReportTitle,274		"report-title",275		"",276		"override the repo name shown in the report banner",277	)278	flags.StringSliceVarP(279		&processor.AllowListExtensions,280		"include-ext",281		"i",282		[]string{},283		"limit to file extensions [comma separated list: e.g. go,java,js]",284	)285	flags.StringSliceVarP(286		&processor.ExcludeListExtensions,287		"exclude-ext",288		"x",289		[]string{},290		"ignore file extensions (overrides include-ext) [comma separated list: e.g. go,java,js]",291	)292	flags.StringSliceVarP(293		&processor.ExcludeFilename,294		"exclude-file",295		"n",296		[]string{"package-lock.json", "Cargo.lock", "yarn.lock", "pubspec.lock", "Podfile.lock", "pnpm-lock.yaml"},297		"ignore files with matching names",298	)299	flags.BoolVarP(300		&processor.Languages,301		"languages",302		"l",303		false,304		"print supported languages and extensions",305	)306	flags.Int64Var(307		&processor.AverageWage,308		"avg-wage",309		56286,310		"average wage value used for basic COCOMO calculation",311	)312	flags.Float64Var(313		&processor.Overhead,314		"overhead",315		2.4,316		"set the overhead multiplier for corporate overhead (facilities, equipment, accounting, etc.)",317	)318	flags.Float64Var(319		&processor.EAF,320		"eaf",321		1.0,322		"the effort adjustment factor derived from the cost drivers (1.0 if rated nominal)",323	)324	flags.BoolVar(325		&processor.SLOCCountFormat,326		"sloccount-format",327		false,328		"print a more SLOCCount like COCOMO calculation",329	)330	flags.BoolVar(331		&processor.Cocomo,332		"no-cocomo",333		false,334		"remove COCOMO calculation output",335	)336	flags.StringVar(337		&processor.CocomoProjectType,338		"cocomo-project-type",339		"organic",340		"change COCOMO model type [organic, semi-detached, embedded, \"custom,1,1,1,1\"]",341	)342	flags.BoolVar(343		&processor.Size,344		"no-size",345		false,346		"remove size calculation output",347	)348	flags.BoolVar(349		&processor.HBorder,350		"no-hborder",351		false,352		"remove horizontal borders between sections",353	)354	flags.StringVar(355		&processor.SizeUnit,356		"size-unit",357		"si",358		"set size unit [si, binary, mixed, xkcd-kb, xkcd-kelly, xkcd-imaginary, xkcd-intel, xkcd-drive, xkcd-bakers]",359	)360	flags.BoolVarP(361		&processor.Complexity,362		"no-complexity",363		"c",364		false,365		"skip calculation of code complexity",366	)367	flags.BoolVarP(368		&processor.Duplicates,369		"no-duplicates",370		"d",371		false,372		"remove duplicate files from stats and output",373	)374	flags.BoolVarP(375		&processor.MinifiedGenerated,376		"min-gen",377		"z",378		false,379		"identify minified or generated files",380	)381	flags.BoolVarP(382		&processor.Minified,383		"min",384		"",385		false,386		"identify minified files",387	)388	flags.BoolVarP(389		&processor.Generated,390		"gen",391		"",392		false,393		"identify generated files",394	)395	flags.StringSliceVarP(396		&processor.GeneratedMarkers,397		"generated-markers",398		"",399		[]string{"do not edit", "<auto-generated />"},400		"string markers in head of generated files",401	)402	flags.BoolVar(403		&processor.IgnoreMinifiedGenerate,404		"no-min-gen",405		false,406		"ignore minified or generated files in output (implies --min-gen)",407	)408	flags.BoolVar(409		&processor.IgnoreMinified,410		"no-min",411		false,412		"ignore minified files in output (implies --min)",413	)414	flags.BoolVar(415		&processor.IgnoreGenerated,416		"no-gen",417		false,418		"ignore generated files in output (implies --gen)",419	)420	flags.IntVar(421		&processor.MinifiedGeneratedLineByteLength,422		"min-gen-line-length",423		255,424		"number of bytes per average line for file to be considered minified or generated",425	)426	flags.StringArrayVarP(427		&processor.Exclude,428		"not-match",429		`M`,430		[]string{},431		"ignore files and directories matching regular expression",432	)433	flags.StringVarP(434		&processor.FileOutput,435		"output",436		"o",437		"",438		"output filename (default stdout)",439	)440	flags.StringVarP(441		&processor.SortBy,442		"sort",443		"s",444		"files",445		"column to sort by [files, name, lines, blanks, code, comments, complexity]",446	)447	flags.BoolVarP(448		&processor.Trace,449		"trace",450		"t",451		false,452		"enable trace output (not recommended when processing multiple files)",453	)454	flags.BoolVarP(455		&processor.Verbose,456		"verbose",457		"v",458		false,459		"verbose output",460	)461	flags.BoolVarP(462		&processor.More,463		"wide",464		"w",465		false,466		"wider output with additional statistics (implies --complexity)",467	)468	flags.BoolVar(469		&processor.NoLarge,470		"no-large",471		false,472		"ignore files over certain byte and line size set by large-line-count and large-byte-count",473	)474	flags.BoolVar(475		&processor.IncludeSymLinks,476		"include-symlinks",477		false,478		"if set will count symlink files",479	)480	flags.Int64Var(481		&processor.LargeLineCount,482		"large-line-count",483		40000,484		"number of lines a file can contain before being removed from output",485	)486	flags.Int64Var(487		&processor.LargeByteCount,488		"large-byte-count",489		1000000,490		"number of bytes a file can contain before being removed from output",491	)492	flags.StringVar(493		&processor.CountAs,494		"count-as",495		"",496		"count extension as language [e.g. jsp:htm,chead:\"C Header\" maps extension jsp to html and chead to C Header]",497	)498	flags.StringArrayVar(499		&processor.CountAsPattern,500		"count-as-pattern",501		nil,502		"count files matching a path pattern as a new named category backed by a base language "+503			"[repeatable; pattern is glob by default, prefix with re: for regex; "+504			"e.g. *_spec.rb:\"Ruby Spec\":Ruby or re:\\.test\\.js$:\"JavaScript Tests\":JavaScript]",505	)506	flags.StringVar(507		&processor.FormatMulti,508		"format-multi",509		"",510		"have multiple format output overriding --format [e.g. tabular:stdout,csv:file.csv,json:file.json]",511	)512	flags.StringVar(513		&processor.SQLProject,514		"sql-project",515		"",516		"use supplied name as the project identifier for the current run. Only valid with the --format sql or sql-insert option",517	)518	flags.StringVar(519		&processor.RemapUnknown,520		"remap-unknown",521		"",522		"inspect files of unknown type and remap by checking for a string and remapping the language [e.g. \"-*- C++ -*-\":\"C Header\"]",523	)524	flags.StringVar(525		&processor.RemapAll,526		"remap-all",527		"",528		"inspect every file and remap by checking for a string and remapping the language [e.g. \"-*- C++ -*-\":\"C Header\"]",529	)530	flags.StringVar(531		&processor.CurrencySymbol,532		"currency-symbol",533		"$",534		"set currency symbol",535	)536	flags.BoolVar(537		&processor.Locomo,538		"locomo",539		false,540		"enable LOCOMO (LLM Output COst MOdel) cost estimation",541	)542	flags.BoolVar(543		&processor.CostComparison,544		"cost-comparison",545		false,546		"show both COCOMO and LOCOMO estimates side by side",547	)548	flags.StringVar(549		&processor.LocomoPresetName,550		"locomo-preset",551		"medium",552		"LOCOMO model preset [large, medium, small, local]",553	)554	flags.Float64Var(555		&processor.LocomoReviewMinutesPerLine,556		"locomo-review",557		0.01,558		"human review minutes per line of code for LOCOMO estimate",559	)560	flags.StringVar(561		&processor.LocomoConfig,562		"locomo-config",563		"",564		"LOCOMO power-user config \"tokensPerLine,inputPerLine,complexityWeight,iterations,iterationWeight\"",565	)566	flags.Float64Var(567		&processor.LocomoInputPrice,568		"locomo-input-price",569		0,570		"LOCOMO cost per 1M input tokens in dollars (overrides preset)",571	)572	flags.Float64Var(573		&processor.LocomoOutputPrice,574		"locomo-output-price",575		0,576		"LOCOMO cost per 1M output tokens in dollars (overrides preset)",577	)578	flags.Float64Var(579		&processor.LocomoTPS,580		"locomo-tps",581		0,582		"LOCOMO output tokens per second (overrides preset)",583	)584	flags.Float64Var(585		&processor.LocomoCyclesOverride,586		"locomo-cycles",587		0,588		"override estimated LLM iteration cycles (default: calculated from complexity)",589	)590591	flags.BoolVar(592		&processor.Hotspots,593		"hotspots",594		false,595		"render the hotspots report (files ranked by complexity × change frequency over recent git history)",596	)597	flags.BoolVar(598		&processor.ByAuthor,599		"by-author",600		false,601		"render the author rollup report (bus factor and last-toucher attribution over recent git history)",602	)603	flags.IntVar(604		&processor.HistoryDepth,605		"depth",606		1000,607		"commit window size for git history reports; 0 means entire history (large repos may be slow)",608	)609	flags.BoolVar(610		&processor.Timeline,611		"timeline",612		false,613		"render an over-time view of recent git history; with --by-author runs the author timeline, alone runs the languages timeline",614	)615	flags.IntVar(616		&processor.HistoryBuckets,617		"buckets",618		60,619		"time-bucket resolution for the git timeline reports (default 60)",620	)621	var noFoldAuthors bool622	flags.BoolVar(623		&noFoldAuthors,624		"no-fold-authors",625		false,626		"disable the name+email-domain identity folding fallback for git author reports (mailmap still applied)",627	)628629	// --mcp is intercepted before cobra runs, but we register it here so it appears in --help630	var mcpDummy bool631	flags.BoolVar(632		&mcpDummy,633		"mcp",634		false,635		"start as an MCP (Model Context Protocol) server over stdio",636	)637638	// If invoked in the format of "scc completion --shell [name of shell]", generate command line completions instead.639	// With the --shell option, unintentionally triggering shell completions should be highly unlikely.640	args := os.Args641	if len(args) == 4 && args[1] == "completion" && args[2] == "--shell" {642		err := printShellCompletion(rootCmd, args[3])643		if err != nil {644			_, _ = fmt.Fprintf(os.Stderr, "Error printing shell completion: %s\n", err)645		}646		return647	}648649	if err := rootCmd.Execute(); err != nil {650		// If a flag does not exist and is not a shorthand, it may be a spelling error. Search for and print possible options.651		if notExistError, ok := err.(*pflag.NotExistError); ok && len(notExistError.GetSpecifiedName()) > 1 {652			printFlagSuggestion(flags, notExistError.GetSpecifiedName())653		}654		os.Exit(1)655	}656}

Code quality findings 8

Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = fmt.Fprintf(os.Stderr, "The most similar flags of --%s are:\n", unknownFlag)
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = fmt.Fprintf(os.Stderr, "The most similar flag of --%s is:\n", unknownFlag)
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = fmt.Fprintf(os.Stderr, "\t--%s\n", flag)
Defer inside loop; deferred calls accumulate until the function returns, not until the loop iteration ends. This can cause resource leaks
warning correctness defer-in-loop
// defer pprof.StopCPUProfile()
Ensure errors are handled or logged
warning correctness unhandled-error
if err != nil {
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = fmt.Fprintf(os.Stderr, "Error printing shell completion: %s\n", err)
Error string starts with uppercase; per Go convention error strings should not be capitalized or end with punctuation
info maintainability error-string-format
return errors.New("Unknown shell: " + command)
Formatted output to console; prefer structured logging for consistency
info correctness fmt-printf
fmt.Printf("Error reading flags from a file: %s\n", err)

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.