main.go GO 235 lines View on github.com → Search inside
1// SPDX-License-Identifier: MIT23package main45import (6	"errors"7	"fmt"8	"os"9	"slices"10	"strings"1112	"github.com/boyter/scc/v4/processor"13	"github.com/spf13/cobra"14	"github.com/spf13/pflag"15)1617func printShellCompletion(cmd *cobra.Command, command string) error {18	switch command {19	case "bash":20		return cmd.GenBashCompletionV2(os.Stdout, true)21	case "zsh":22		return cmd.GenZshCompletion(os.Stdout)23	case "fish":24		return cmd.GenFishCompletion(os.Stdout, true)25	case "powershell":26		return cmd.GenPowerShellCompletion(os.Stdout)27	default:28		return errors.New("Unknown shell: " + command)29	}30}3132func printFlagSuggestion(flagSet *pflag.FlagSet, unknownFlag string) {33	flags := processor.GetMostSimilarFlags(flagSet, unknownFlag)34	if len(flags) == 0 {35		return36	}3738	if len(flags) > 1 {39		_, _ = fmt.Fprintf(os.Stderr, "The most similar flags of --%s are:\n", unknownFlag)40	} else {41		_, _ = fmt.Fprintf(os.Stderr, "The most similar flag of --%s is:\n", unknownFlag)42	}4344	for _, flag := range flags {45		_, _ = fmt.Fprintf(os.Stderr, "\t--%s\n", flag)46	}47}4849//go:generate go run scripts/include.go50func main() {51	// f, _ := os.Create("scc.pprof")52	// pprof.StartCPUProfile(f)53	// defer pprof.StopCPUProfile()5455	// Handle --mcp flag before cobra to avoid interfering with stdio. Match both56	// the bare boolean form and the explicit --mcp=true form pflag accepts, so the57	// server starts consistently however the flag is spelled.58	if slices.ContainsFunc(os.Args[1:], func(a string) bool {59		return a == "--mcp" || a == "--mcp=true"60	}) {61		startMCPServer()62		return63	}6465	// handle "scc @flags.txt" syntax. The sole-argument trigger is preserved;66	// only the splitter is swapped for the shared tokenizer, which adds comment67	// stripping, quote-aware tokenization and drops the old blank-line empty-arg68	// bug.69	if len(os.Args) == 2 && strings.HasPrefix(os.Args[1], "@") {70		filename := strings.TrimPrefix(os.Args[1], "@")71		b, err := os.ReadFile(filename)72		if err != nil {73			fmt.Fprintf(os.Stderr, "Error reading flags from a file: %s\n", err)74			os.Exit(1)75		}76		os.Args = append([]string{os.Args[0]}, parseConfigArgs(string(b), true)...)77	}7879	// What the user actually specified80	genuineCLI := slices.Clone(os.Args[1:])8182	// Cobra's completion machinery must see the genuine argv. The shell invokes83	// the hidden __complete / __completeNoDesc commands on every TAB, and the84	// user-facing `completion` command generates the scripts; both key on the85	// subcommand sitting in args[0]. Prepending discovered config tokens would86	// shift it and break dynamic completion in any directory containing a ./.sccconfig.87	// Config never influences completion output anyway, so skip discovery for it.88	var globalTokens, projectTokens []string89	if !isCompletionInvocation(os.Args) {90		noConfig, findRoot, explicitPath := preScanConfig(os.Args)91		var discoverErr error92		globalTokens, projectTokens, discoverErr = discoverConfigArgs(noConfig, findRoot, explicitPath)93		if discoverErr != nil {94			processor.PrintError(discoverErr.Error())95			os.Exit(1)96		}97	}9899	// Fast path when no config was discovered: the merged list *is* the genuine100	// CLI, so bind write flags directly to the real vars and parse once, exactly101	// as scc does today. The discard / CLI-only split engages only when config is102	// actually present.103	// Ensures we follow old scc logic without config104	configPresent := len(globalTokens) != 0 || len(projectTokens) != 0105106	// Build the merged argument list N.B. ORDER MATTERS HERE! CLI MUST COME LAST!107	var merged []string108	merged = append(merged, os.Args[0])109	merged = append(merged, globalTokens...)110	merged = append(merged, projectTokens...)111	merged = append(merged, genuineCLI...)112113	// Write-flag bindings for the merged parse114	var discardOutput, discardReport, discardFormatMulti string115	bindings := &flagBindings{116		output:      &processor.FileOutput,117		report:      &processor.ReportOut,118		formatMulti: &processor.FormatMulti,119	}120121	// if we use config, we NEVER allow writing to disk because someone could use that as122	// an attack vector, so we reset these options to ensure this is not a risk123	if configPresent {124		bindings.output = &discardOutput125		bindings.report = &discardReport126		bindings.formatMulti = &discardFormatMulti127	}128129	rootCmd := &cobra.Command{130		Use:   "scc [flags] [files or directories]",131		Short: "scc [files or directories]",132		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\nhttps://github.com/boyter/scc", processor.Version),133		Example: `  Count the current directory:134    scc135136  Count a specific folder or file:137    scc myproject/138    scc main.go139140  Count several paths at once:141    scc src/ docs/ README.md142143  Show a per-file breakdown instead of the per-language summary:144    scc --by-file145146  Output as CSV or JSON (e.g. for further processing):147    scc --format csv148    scc --format json -o counts.json149150  Count an unrecognised extension as a known language:151    scc --count-as jsp:html152153  Count files matching a path pattern as a new category (glob by default):154    scc --count-as-pattern '*_spec.rb:Ruby Spec:Ruby'155156  Generate a self-contained HTML infographic report:157    scc --report158    scc --report=out.html --report-title "myrepo" --report-skip cocomo159160  Use a project config file (./.sccconfig) or a global one (precedence: global < project < CLI):161    export SCC_CONFIG_PATH=~/.sccconfig162    scc --config team.sccconfig163164  Tune the COCOMO cost estimate, or turn it off (see https://en.wikipedia.org/wiki/COCOMO):165    scc --avg-wage 75000 --cocomo-project-type semi-detached166    scc --no-cocomo`,167		Version: processor.Version,168		Run: func(cmd *cobra.Command, args []string) {169			processor.DirFilePaths = args170			processor.ConfigureGc()171			processor.ConfigureLazy(true)172173			// Detect if LOCOMO price/tps flags were explicitly set. Their default174			// is 0, which is ambiguous (unset vs. an explicit 0), so the processor175			// needs the "was it set?" bit to decide between the preset value and the176			// user override. Only pflag's Changed() knows this, hence here not there.177			processor.LocomoInputPriceSet = cmd.PersistentFlags().Changed("locomo-input-price")178			processor.LocomoOutputPriceSet = cmd.PersistentFlags().Changed("locomo-output-price")179			processor.LocomoTPSSet = cmd.PersistentFlags().Changed("locomo-tps")180			processor.LocomoCyclesSet = cmd.PersistentFlags().Changed("locomo-cycles")181182			if v, err := cmd.PersistentFlags().GetBool("no-fold-authors"); err == nil && v {183				processor.FoldAuthors = false184			}185186			// Source the write vars from the genuine CLI alone (file output is a187			// CLI-only capability), then warn if config tried to set one.188			if configPresent {189				cliSet := resolveWriteFlags(genuineCLI)190				warnIfConfigWrote(cmd.PersistentFlags(), cliSet)191			}192193			// Merge the built-in defaults back into the empty-defaulted slice194			// flags, then flush any buffered config trace/debug.195			applySliceDefaults()196			flushConfigTrace()197198			processor.Process()199		},200	}201202	flags := rootCmd.PersistentFlags()203	registerFlags(flags, bindings)204	registerConfigControlFlags(flags)205206	// If invoked in the format of "scc completion --shell [name of shell]", generate command line completions instead.207	// With the --shell option, unintentionally triggering shell completions should be highly unlikely. This reads the208	// genuine os.Args (config tokens are handed to cobra via SetArgs below, never prepended into os.Args), so a ./.sccconfig209	// in the working directory cannot shift args[1] and break completion.210	args := os.Args211	if len(args) == 4 && args[1] == "completion" && args[2] == "--shell" {212		err := printShellCompletion(rootCmd, args[3])213		if err != nil {214			_, _ = fmt.Fprintf(os.Stderr, "Error printing shell completion: %s\n", err)215		}216		return217	}218219	// Hand the merged list to cobra without mutating os.Args.220	rootCmd.SetArgs(merged[1:])221222	if err := rootCmd.Execute(); err != nil {223		// If a flag does not exist and is not a shorthand, it may be a spelling error. Search for and print possible options.224		if notExistError, ok := err.(*pflag.NotExistError); ok && len(notExistError.GetSpecifiedName()) > 1 {225			name := notExistError.GetSpecifiedName()226			// Best-effort: attribute the unknown flag to the config file it came.227			if src := attributeConfigFlag(name); src != "" {228				_, _ = fmt.Fprintf(os.Stderr, "in %s: unknown flag --%s\n", src, name)229			}230			printFlagSuggestion(flags, name)231		}232		os.Exit(1)233	}234}

Code quality findings 13

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)
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = fmt.Fprintf(os.Stderr, "in %s: unknown flag --%s\n", src, name)
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)
Multiple appends without pre-allocation; use make() with capacity when size is known
info performance append-without-prealloc
os.Args = append([]string{os.Args[0]}, parseConfigArgs(string(b), true)...)
Multiple appends without pre-allocation; use make() with capacity when size is known
info performance append-without-prealloc
merged = append(merged, os.Args[0])
Multiple appends without pre-allocation; use make() with capacity when size is known
info performance append-without-prealloc
merged = append(merged, globalTokens...)
Multiple appends without pre-allocation; use make() with capacity when size is known
info performance append-without-prealloc
merged = append(merged, projectTokens...)
Multiple appends without pre-allocation; use make() with capacity when size is known
info performance append-without-prealloc
merged = append(merged, genuineCLI...)

Get this view in your editor

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