class

Analyzer::Go::Cli

Inherits CliEndpointSupport < Analyzer::Go::GoEngine < Analyzer < FileHelper < Reference < Object

Surfaces the command-line attack surface of Go programs as cli:// endpoints: one endpoint per (sub)command, with named flags/options (param_type "flag"), positional arguments ("argument"), and consumed environment variables ("env"). Covers the stdlib flag package + os.Args + os.Getenv/os.LookupEnv, plus the cobra, urfave/cli, pflag, go-arg, go-flags, kong, kingpin and mitchellh/cli ecosystems.

This is a line-scan analyzer (the house style for non-tree-sitter Go adapters, e.g. fasthttp). Endpoints are merged by URL across files so a root command whose flags are registered in a separate init() block collects them all under a single cli://<binary> entry.

Constants

BUILTIN_ARG_RE = /\bflag\.Arg\s*\(\s*(\d+)\s*\)/
BUILTIN_ARGS_RE = /\bflag\.Args\s*\(\s*\)/
BUILTIN_FLAG_RE = /\bflag\.(?:String|Int|Int64|Uint|Uint64|Bool|Float64|Duration)(?:Var)?\s*\(\s*(?:&?[\w.]+\s*,\s*)?"([^"]+)"/

--- builtin flag / argv ------------------------------------------------- The flag name is always the FIRST quoted string in the call: the *Var forms put the destination pointer (unquoted) first, then the name.

BUILTIN_FLAG_VAR_RE = /\bflag\.Var\s*\(\s*&?[\w.]+\s*,\s*"([^"]+)"/
CLI_PARSE_POINT_RE = Regex.union("github.com/spf13/cobra", "github.com/urfave/cli", "github.com/alexflint/go-arg", "github.com/jessevdk/go-flags", "flag.Parse(")

Five markers OR-ed as a standalone boolean gate for cli_parse_point? below — one precompiled Regex.union (PCRE2 JIT) replaces the separate content.includes? scan each used to make over the same buffer. kong/kingpin/mitchellh are deliberately excluded (see the comment in cli_parse_point?).

COBRA_CMD_DECL_RE = /(\w+)\s*[:=]\s*&?cobra\.Command\s*\{/

--- cobra ---------------------------------------------------------------

COBRA_FLAG_RE = /(\w+)\s*\.\s*(?:Persistent)?Flags\s*\(\s*\)\s*\.\s*[A-Za-z0-9]+\s*\(\s*(?:&?[\w.]+\s*,\s*)?"([^"]+)"/
COBRA_USE_RE = /\bUse\s*:\s*"([^"\s]+)/
FLAG_IMPORT_RE = /"flag"/
FLAG_PARSE_RE = /flag\.Parse\(/
FRAMEWORK_IMPORTS = ["github.com/spf13/cobra", "github.com/urfave/cli", "github.com/alexflint/go-arg", "github.com/jessevdk/go-flags", "github.com/spf13/pflag", "github.com/alecthomas/kong", "github.com/mitchellh/cli", "github.com/alecthomas/kingpin", "gopkg.in/alecthomas/kingpin.v2"]

Any of these in a file means it participates in the CLI surface.

FRAMEWORK_IMPORTS_RE = Regex.union(FRAMEWORK_IMPORTS)

FRAMEWORK_IMPORTS.any? { |m| content.includes?(m) } re-scans the whole file once per marker (used at two call sites below); one precompiled Regex.union (PCRE2 JIT) checks all nine in a single pass.

GOARG_TAG_RE = /`[^`]*\barg:"([^"]*)"[^`]*`/

--- go-arg / go-flags struct tags ---------------------------------------

GOFLAGS_ENV_RE = /`[^`]*\benv:"([^"]+)"/
GOFLAGS_LONG_RE = /`[^`]*\blong:"([^"]+)"/
HTTP_LISTEN_RE = /\b(?:http|fasthttp)\.ListenAndServe(?:TLS)?\s*\(|\.(?:ListenAndServe|RunTLS)\s*\(/

An HTTP listen call in the same file means env reads are most likely server config, not a CLI surface — suppress raw env there.

KINGPIN_ARG_RE = /(\w+)\s*\.\s*Arg\s*\(\s*"([^"]+)"\s*,/
KINGPIN_COMMAND_RE = /(\w+)\s*:?=\s*(\w+)\s*\.\s*Command\s*\(\s*"([^"]+)"/
KINGPIN_ENVAR_RE = /\.\s*Envar\s*\(\s*"([^"]+)"\s*\)/
KINGPIN_FLAG_RE = /(\w+)\s*\.\s*Flag\s*\(\s*"([^"]+)"\s*,/
KINGPIN_IMPORT_RE = Regex.union("github.com/alecthomas/kingpin", "gopkg.in/alecthomas/kingpin.v2")
KINGPIN_NEW_RE = /(\w+)\s*:?=\s*kingpin\.New\s*\(/

--- kingpin (fluent Flag/Arg/Command builder) --------------------------- app := kingpin.New(...) seeds the root receiver; cmd := app.Command(...) (or sub := cmd.Command(...) for nesting) maps a new receiver var onto a command URL, and .Flag(...)/.Arg(...) calls on a known receiver attribute a param to that command — never to a sticky "current" command.

KONG_ARG_TAG_RE = /\barg\s*:\s*"[^"]*"/
KONG_CMD_TAG_RE = /\bcmd\s*:\s*"[^"]*"/
KONG_ENV_TAG_RE = /\benv\s*:\s*"([^"]+)"/
KONG_FIELD_RE = /^\s*([A-Z]\w*)\s+([\w.\[\]*]+)\s+`([^`]*)`/
KONG_IMPORT_RE = Regex.union("github.com/alecthomas/kong")

Per-framework matchers for the scan dispatch below, which asks the same questions again once cli_evidence? has let a file through.

KONG_NAME_TAG_RE = /\bname\s*:\s*"([^"]+)"/
KONG_TAG_KEY_RE = /\b(?:cmd|arg|env|default|help)\s*:\s*"/
KONG_TYPE_DECL_RE = /^type\s+(\w+)\s+struct\s*\{/

--- kong (struct-tag CLI) ------------------------------------------------ Root/subcommand fields are declared as struct fields; a cmd:"" tag marks a field as a subcommand whose own flags/args live in that field's named struct type, an arg:"" tag marks a positional, and an env:"" tag additionally binds the field to an environment variable.

MITCHELLH_FLAGVAR_RE = /\b\w+\.(?:StringVar|IntVar|Int64Var|BoolVar|Float64Var|DurationVar)\s*\(\s*&?[\w.]+\s*,\s*"([^"]+)"/
MITCHELLH_IMPORT_RE = Regex.union("github.com/mitchellh/cli")
MITCHELLH_KEY_RE = /^\s*"([^"]+)"\s*:\s*func\s*\(\s*\)\s*\(cli\.Command,\s*error\)\s*\{/

--- mitchellh/cli (Commands map of factories) ---------------------------- c.Commands = map[string]cli.CommandFactory{"name": func() (cli.Command, error) { return &FooCommand{}, nil }} maps each map key to the concrete command type it instantiates; that type's own Run method is then scanned (scoped to its own body) for flag.FlagSet-style *Var registrations and raw env reads.

MITCHELLH_RETURN_RE = /return\s+&(\w+)\{/
MITCHELLH_RETURN_VAR_RE = /return\s+(\w+)\s*,/
MITCHELLH_RUN_FUNC_RE = /^func\s*\(\s*\w+\s+\*?(\w+)\)\s*Run\s*\(/
MITCHELLH_VAR_ASSIGN_RE = /(\w+)\s*:=\s*&(\w+)\{/

cmd := &DeployCommand{} followed later (within the SAME closure) by return cmd, nil — the idiomatic form used whenever the command needs field initialization, as common as the single-expression return &X{}.

OS_ARG_INDEX_RE = /\bos\.Args\s*\[\s*(\d+)\s*\]/
OS_ARGS_RE = /os\.Args/
OS_GETENV_RE = /\bos\.(?:Getenv|LookupEnv)\s*\(\s*"([^"]+)"\s*\)/

--- env -----------------------------------------------------------------

PACKAGE_MAIN_RE = /^\s*package\s+main\b/m

A Go module can hold many commands — cmd/foo, services/bar/cmd — and each package main directory is a separate binary. Naming every one of them after the module merged unrelated programs' flags into a single cli://<module> endpoint. Library packages keep the module name, so the cross-file merge this analyzer is built around still works for the helper files a command pulls its flags from.

PFLAG_RE = /\bpflag\.(?:String|Int|Int64|Uint|Bool|Float64|Duration|StringSlice|StringArray)(?:Var)?(?:P)?\s*\(\s*(?:&?[\w.]+\s*,\s*)?"([^"]+)"/

--- pflag (when used without cobra) -------------------------------------

RAW_CLI_EVIDENCE_RE = Regex.union(FRAMEWORK_IMPORTS + ["\"flag\"", "os.Args"])

Cheap pre-gate applied to the RAW file, before the comment strip.

GoEngine.strip_comments materialises an Array(Char) of the whole file and rebuilds it character by character; running it on every .go file in a monorepo — 15k of them in kubernetes, of which a couple of hundred have any CLI surface — was the whole cost of this analyzer. Stripping only ever replaces characters with ' ' or '\n' and never changes the character count, so a literal with no whitespace in it can appear in the stripped text only if it already appears, unchanged, in the raw text. Every branch of cli_evidence? requires one of these literals verbatim (FLAG_IMPORT_RE is the literal "flag"; OS_ARG_INDEX_RE needs an adjacent os.Args), so a file that misses all of them cannot pass cli_evidence? after the strip either. Files that do match still go through the full, comment-aware gate below.

STRUCT_TAG_IMPORT_RE = Regex.union("alexflint/go-arg", "jessevdk/go-flags")
TOPLEVEL_BRACE_CLOSE_RE = /^\}\s*$/

A bare unindented } closes the top-level struct/func block it belongs to (the gofmt convention this line-scan relies on for kong's type X struct {} and mitchellh/cli's func (...) Run(...) {} scoping below).

URFAVE_APP_RE = /(?:&)?cli\.App\s*\{/

--- urfave/cli ----------------------------------------------------------

URFAVE_CMD_RE = /(?:&)?cli\.Command\s*\{/
URFAVE_ENVVAR_RE = /\bEnvVar\s*:\s*"([^"]+)"/
URFAVE_ENVVARS_RE = /\bEnvVars\s*:\s*\[\]string\s*\{([^}]*)\}/
URFAVE_FLAG_RE = /(?:&)?cli\.[A-Za-z0-9]*Flag\s*\{/
URFAVE_NAME_RE = /\bName\s*:\s*"([^"]+)"/
VIPER_BINDENV_RE = /\bviper\.BindEnv\s*\(\s*"([^"]+)"(?:\s*,\s*"([^"]+)")?/

Class methods

tech_name
Source

Instance methods

analyze
Source
tech

Instance-side view of the same declaration. The per-file rescues live on this base class, which has no way to name the analyzer that is running inside them, so a skipped file could not be attributed to a tech. Deriving it from analyzer_for keeps the name written exactly once.

Source