// SPDX-License-Identifier: MIT package main import ( "errors" "fmt" "os" "runtime" "strings" "github.com/boyter/scc/v3/processor" "github.com/spf13/cobra" "github.com/spf13/pflag" ) func printShellCompletion(cmd *cobra.Command, command string) error { switch command { case "bash": return cmd.GenBashCompletionV2(os.Stdout, true) case "zsh": return cmd.GenZshCompletion(os.Stdout) case "fish": return cmd.GenFishCompletion(os.Stdout, true) case "powershell": return cmd.GenPowerShellCompletion(os.Stdout) default: return errors.New("Unknown shell: " + command) } } func printFlagSuggestion(flagSet *pflag.FlagSet, unknownFlag string) { flags := processor.GetMostSimilarFlags(flagSet, unknownFlag) if len(flags) == 0 { return } if len(flags) > 1 { _, _ = fmt.Fprintf(os.Stderr, "The most similar flags of --%s are:\n", unknownFlag) } else { _, _ = fmt.Fprintf(os.Stderr, "The most similar flag of --%s is:\n", unknownFlag) } for _, flag := range flags { _, _ = fmt.Fprintf(os.Stderr, "\t--%s\n", flag) } } //go:generate go run scripts/include.go func main() { // f, _ := os.Create("scc.pprof") // pprof.StartCPUProfile(f) // defer pprof.StopCPUProfile() if len(os.Args) == 2 && strings.HasPrefix(os.Args[1], "@") { // handle "scc @flags.txt" syntax filepath := strings.TrimPrefix(os.Args[1], "@") b, err := os.ReadFile(filepath) if err != nil { fmt.Printf("Error reading flags from a file: %s\n", err) os.Exit(1) } args := strings.Split(string(b), "\n") newArgs := make([]string, 0, len(args)) for _, x := range args { newArgs = append(newArgs, strings.TrimSpace(x)) } os.Args = append([]string{os.Args[0]}, newArgs...) } rootCmd := &cobra.Command{ Use: "scc [flags] [files or directories]", Short: "scc [files or directories]", Long: fmt.Sprintf("Sloc, Cloc and Code. Count lines of code in a directory with complexity estimation.\nVersion %s\nBen Boyter + Contributors", processor.Version), Version: processor.Version, Run: func(cmd *cobra.Command, args []string) { processor.DirFilePaths = args processor.ConfigureGc() processor.ConfigureLazy(true) // Detect if LOCOMO price/tps flags were explicitly set processor.LocomoInputPriceSet = cmd.PersistentFlags().Changed("locomo-input-price") processor.LocomoOutputPriceSet = cmd.PersistentFlags().Changed("locomo-output-price") processor.LocomoTPSSet = cmd.PersistentFlags().Changed("locomo-tps") processor.LocomoCyclesSet = cmd.PersistentFlags().Changed("locomo-cycles") processor.Process() }, } flags := rootCmd.PersistentFlags() flags.BoolVarP( &processor.MaxMean, "character", "m", false, "calculate max and mean characters per line", ) flags.BoolVarP( &processor.Percent, "percent", "p", false, "include percentage values in output", ) flags.BoolVarP( &processor.UlocMode, "uloc", "u", false, "calculate the number of unique lines of code (ULOC) for the project", ) flags.BoolVarP( &processor.Dryness, "dryness", "a", false, "calculate the DRYness of the project (implies --uloc)", ) flags.BoolVar( &processor.DisableCheckBinary, "binary", false, "disable binary file detection", ) flags.BoolVar( &processor.Files, "by-file", false, "display output for every file", ) flags.BoolVar( &processor.Ci, "ci", false, "enable CI output settings where stdout is ASCII", ) flags.BoolVar( &processor.Ignore, "no-ignore", false, "disables .ignore file logic", ) flags.BoolVar( &processor.SccIgnore, "no-scc-ignore", false, "disables .sccignore file logic", ) flags.BoolVar( &processor.GitIgnore, "no-gitignore", false, "disables .gitignore file logic", ) flags.BoolVar( &processor.GitModuleIgnore, "no-gitmodule", false, "disables .gitmodules file logic", ) flags.BoolVar( &processor.CountIgnore, "count-ignore", false, "set to allow .gitignore and .ignore files to be counted", ) flags.BoolVar( &processor.Debug, "debug", false, "enable debug output", ) flags.StringSliceVar( &processor.PathDenyList, "exclude-dir", []string{".git", ".hg", ".svn"}, "directories to exclude", ) flags.IntVar( &processor.GcFileCount, "file-gc-count", 10000, "number of files to parse before turning the GC on", ) flags.IntVar( &processor.FileListQueueSize, "file-list-queue-size", runtime.NumCPU(), "the size of the queue of files found and ready to be read into memory", ) flags.IntVar( &processor.FileProcessJobWorkers, "file-process-job-workers", runtime.NumCPU(), "number of goroutine workers that process files collecting stats", ) flags.IntVar( &processor.FileSummaryJobQueueSize, "file-summary-job-queue-size", runtime.NumCPU(), "the size of the queue used to hold processed file statistics before formatting", ) flags.IntVar( &processor.DirectoryWalkerJobWorkers, "directory-walker-job-workers", 8, "controls the maximum number of workers which will walk the directory tree", ) flags.StringVarP( &processor.Format, "format", "f", "tabular", "set output format [tabular, wide, json, json2, csv, csv-stream, cloc-yaml, html, html-table, sql, sql-insert, openmetrics]", ) flags.StringSliceVarP( &processor.AllowListExtensions, "include-ext", "i", []string{}, "limit to file extensions [comma separated list: e.g. go,java,js]", ) flags.StringSliceVarP( &processor.ExcludeListExtensions, "exclude-ext", "x", []string{}, "ignore file extensions (overrides include-ext) [comma separated list: e.g. go,java,js]", ) flags.StringSliceVarP( &processor.ExcludeFilename, "exclude-file", "n", []string{"package-lock.json", "Cargo.lock", "yarn.lock", "pubspec.lock", "Podfile.lock", "pnpm-lock.yaml"}, "ignore files with matching names", ) flags.BoolVarP( &processor.Languages, "languages", "l", false, "print supported languages and extensions", ) flags.Int64Var( &processor.AverageWage, "avg-wage", 56286, "average wage value used for basic COCOMO calculation", ) flags.Float64Var( &processor.Overhead, "overhead", 2.4, "set the overhead multiplier for corporate overhead (facilities, equipment, accounting, etc.)", ) flags.Float64Var( &processor.EAF, "eaf", 1.0, "the effort adjustment factor derived from the cost drivers (1.0 if rated nominal)", ) flags.BoolVar( &processor.SLOCCountFormat, "sloccount-format", false, "print a more SLOCCount like COCOMO calculation", ) flags.BoolVar( &processor.Cocomo, "no-cocomo", false, "remove COCOMO calculation output", ) flags.StringVar( &processor.CocomoProjectType, "cocomo-project-type", "organic", "change COCOMO model type [organic, semi-detached, embedded, \"custom,1,1,1,1\"]", ) flags.BoolVar( &processor.Size, "no-size", false, "remove size calculation output", ) flags.BoolVar( &processor.HBorder, "no-hborder", false, "remove horizontal borders between sections", ) flags.StringVar( &processor.SizeUnit, "size-unit", "si", "set size unit [si, binary, mixed, xkcd-kb, xkcd-kelly, xkcd-imaginary, xkcd-intel, xkcd-drive, xkcd-bakers]", ) flags.BoolVarP( &processor.Complexity, "no-complexity", "c", false, "skip calculation of code complexity", ) flags.BoolVarP( &processor.Duplicates, "no-duplicates", "d", false, "remove duplicate files from stats and output", ) flags.BoolVarP( &processor.MinifiedGenerated, "min-gen", "z", false, "identify minified or generated files", ) flags.BoolVarP( &processor.Minified, "min", "", false, "identify minified files", ) flags.BoolVarP( &processor.Generated, "gen", "", false, "identify generated files", ) flags.StringSliceVarP( &processor.GeneratedMarkers, "generated-markers", "", []string{"do not edit", ""}, "string markers in head of generated files", ) flags.BoolVar( &processor.IgnoreMinifiedGenerate, "no-min-gen", false, "ignore minified or generated files in output (implies --min-gen)", ) flags.BoolVar( &processor.IgnoreMinified, "no-min", false, "ignore minified files in output (implies --min)", ) flags.BoolVar( &processor.IgnoreGenerated, "no-gen", false, "ignore generated files in output (implies --gen)", ) flags.IntVar( &processor.MinifiedGeneratedLineByteLength, "min-gen-line-length", 255, "number of bytes per average line for file to be considered minified or generated", ) flags.StringArrayVarP( &processor.Exclude, "not-match", `M`, []string{}, "ignore files and directories matching regular expression", ) flags.StringVarP( &processor.FileOutput, "output", "o", "", "output filename (default stdout)", ) flags.StringVarP( &processor.SortBy, "sort", "s", "files", "column to sort by [files, name, lines, blanks, code, comments, complexity]", ) flags.BoolVarP( &processor.Trace, "trace", "t", false, "enable trace output (not recommended when processing multiple files)", ) flags.BoolVarP( &processor.Verbose, "verbose", "v", false, "verbose output", ) flags.BoolVarP( &processor.More, "wide", "w", false, "wider output with additional statistics (implies --complexity)", ) flags.BoolVar( &processor.NoLarge, "no-large", false, "ignore files over certain byte and line size set by large-line-count and large-byte-count", ) flags.BoolVar( &processor.IncludeSymLinks, "include-symlinks", false, "if set will count symlink files", ) flags.Int64Var( &processor.LargeLineCount, "large-line-count", 40000, "number of lines a file can contain before being removed from output", ) flags.Int64Var( &processor.LargeByteCount, "large-byte-count", 1000000, "number of bytes a file can contain before being removed from output", ) flags.StringVar( &processor.CountAs, "count-as", "", "count extension as language [e.g. jsp:htm,chead:\"C Header\" maps extension jsp to html and chead to C Header]", ) flags.StringVar( &processor.FormatMulti, "format-multi", "", "have multiple format output overriding --format [e.g. tabular:stdout,csv:file.csv,json:file.json]", ) flags.StringVar( &processor.SQLProject, "sql-project", "", "use supplied name as the project identifier for the current run. Only valid with the --format sql or sql-insert option", ) flags.StringVar( &processor.RemapUnknown, "remap-unknown", "", "inspect files of unknown type and remap by checking for a string and remapping the language [e.g. \"-*- C++ -*-\":\"C Header\"]", ) flags.StringVar( &processor.RemapAll, "remap-all", "", "inspect every file and remap by checking for a string and remapping the language [e.g. \"-*- C++ -*-\":\"C Header\"]", ) flags.StringVar( &processor.CurrencySymbol, "currency-symbol", "$", "set currency symbol", ) flags.BoolVar( &processor.Locomo, "locomo", false, "enable LOCOMO (LLM Output COst MOdel) cost estimation", ) flags.BoolVar( &processor.CostComparison, "cost-comparison", false, "show both COCOMO and LOCOMO estimates side by side", ) flags.StringVar( &processor.LocomoPresetName, "locomo-preset", "medium", "LOCOMO model preset [large, medium, small, local]", ) flags.Float64Var( &processor.LocomoReviewMinutesPerLine, "locomo-review", 0.01, "human review minutes per line of code for LOCOMO estimate", ) flags.StringVar( &processor.LocomoConfig, "locomo-config", "", "LOCOMO power-user config \"tokensPerLine,inputPerLine,complexityWeight,iterations,iterationWeight\"", ) flags.Float64Var( &processor.LocomoInputPrice, "locomo-input-price", 0, "LOCOMO cost per 1M input tokens in dollars (overrides preset)", ) flags.Float64Var( &processor.LocomoOutputPrice, "locomo-output-price", 0, "LOCOMO cost per 1M output tokens in dollars (overrides preset)", ) flags.Float64Var( &processor.LocomoTPS, "locomo-tps", 0, "LOCOMO output tokens per second (overrides preset)", ) flags.Float64Var( &processor.LocomoCyclesOverride, "locomo-cycles", 0, "override estimated LLM iteration cycles (default: calculated from complexity)", ) // If invoked in the format of "scc completion --shell [name of shell]", generate command line completions instead. // With the --shell option, unintentionally triggering shell completions should be highly unlikely. args := os.Args if len(args) == 4 && args[1] == "completion" && args[2] == "--shell" { err := printShellCompletion(rootCmd, args[3]) if err != nil { _, _ = fmt.Fprintf(os.Stderr, "Error printing shell completion: %s\n", err) } return } if err := rootCmd.Execute(); err != nil { // If a flag does not exist and is not a shorthand, it may be a spelling error. Search for and print possible options. if notExistError, ok := err.(*pflag.NotExistError); ok && len(notExistError.GetSpecifiedName()) > 1 { printFlagSuggestion(flags, notExistError.GetSpecifiedName()) } os.Exit(1) } }