class

Analyzer::Ruby::Cli

Inherits CliEndpointSupport < Analyzer::Ruby::RubyEngine < Analyzer < FileHelper < Reference < Object

Surfaces the command-line attack surface of Ruby programs as cli:// endpoints: one endpoint per (sub)command, with named options (param_type "flag"), positional arguments ("argument") and consumed environment variables ("env"). Covers stdlib OptionParser / ARGV plus Thor, GLI, Slop, TTY::Option, the commander gem, Optimist, Clamp and dry-cli.

Line-scan analyzer (house style for non-tree-sitter Ruby adapters), merging endpoints by URL across files.

Constants

ARGV_INDEX = /\bARGV\s*\[\s*(\d+)\s*\]/

builtin argv / env.

BLOCK_COMMAND = /^\s*command\s+:?["']?([A-Za-z0-9][\w-]*)/

GLI / commander gem command blocks + flags.

CLAMP_OPTION_LONG = /^\s*option\s+(?=["'\[])[^\n]*?["'](-{2}[A-Za-z0-9][\w-]*)["']/
CLAMP_OPTION_SHORT = /^\s*option\s+(?=["'\[])[^\n]*?["'](-[A-Za-z0-9])["']/
CLAMP_PARAMETER = /^\s*parameter\s+["']\[?([A-Za-z0-9_]+)/
CLAMP_SUBCLASS = /<\s*Clamp::Command\b/

Clamp: class Foo < Clamp::Command with option/parameter DSL and optional nested subcommand "name", "desc" do ... end blocks. The DSL call's first argument (the switch name or an array of switch names) must appear immediately after option + whitespace — a (?=["'\[]) lookahead — so an unrelated local/instance variable assignment like option = default? ? "--json" : "--text" cannot masquerade as the DSL call just because a dash-prefixed quoted string appears somewhere later on the line. Mirrors CLAMP_PARAMETER, which already requires the quote directly after the keyword.

CLAMP_SUBCOMMAND = /^(\s*)subcommand\s+["']([A-Za-z0-9][\w-]*)["'][^\n]*\bdo\s*$/
CLI_EVIDENCE_MARKERS_RE = Regex.union("OptionParser.new", "GLI::App", "TTY::Option", "Commander::Methods")

cli_evidence? runs once per .rb file and OR-ed four String#includes? scans as a standalone boolean gate. Folded into one precompiled union so the literal-marker half of the gate costs a single PCRE2 match instead of up to four naive substring scans.

CLI_EVIDENCE_RE = Regex.union(THOR_SUBCLASS, CLI_EVIDENCE_MARKERS_RE, SLOP_CALL, ARGV_INDEX, OPTIMIST_CALL, CLAMP_SUBCLASS, DRY_CLI_MARKER)

cli_evidence? is the gate every .rb in the tree passes through, and it OR-ed seven separate whole-file scans. One union is the same predicate in a single pass; the individual patterns are still needed below, where each one sets its own flag.

COMMANDER_OPT = /\bc\.option\s+[^\n]*?["'](-{2}[A-Za-z0-9][\w-]*)/
COMMANDER_PROGRAM = /\bprogram\s+:name\s*,\s*["']([^"']+)["']/

Program-name hints.

DEF_RE = /^\s*def\s+([a-z_]\w*[?!]?)\s*(?:[(\s]|$)/

Instance methods only: def self.exit_on_failure? (standard Thor boilerplate) is a class method, never a command, so the name must be followed by an argument list, whitespace or end-of-line — not ..

DRY_CLI_ARGUMENT = /^\s*argument\s+:([A-Za-z0-9_]+)/
DRY_CLI_CLASS = /^(\s*)class\s+([A-Za-z0-9_]+)\s*<\s*Dry::CLI::Command\b/
DRY_CLI_MARKER = /<\s*Dry::CLI::Command\b/

dry-cli: class Build < Dry::CLI::Command with option/argument DSL; each subclass is its own (sub)command. DRY_CLI_MARKER (unanchored) is for whole-content evidence checks; DRY_CLI_CLASS (line-anchored, to capture indent) is for the per-line scan — ^ in Crystal only matches the very start of a multi-line string, not after every \n.

DRY_CLI_OPTION = /^\s*option\s+:([A-Za-z0-9_]+)/
ENV_FETCH = /\bENV\.fetch\s*\(\s*["']([^"']+)["']/
ENV_INDEX = /\bENV\s*\[\s*["']([^"']+)["']\s*\]/
GLI_FLAG = /^\s*(?:flag|switch)\s+:?["']?([A-Za-z0-9][\w-]*)/
OPTIMIST_CALL = /\bOptimist(?:::|\.)options\b/

Optimist: opt :name, "desc", type: :string — a flat parser (no subcommand DSL), gated on the block-opening call so a bare local variable/method named opt elsewhere doesn't false-positive.

OPTIMIST_OPT = /^\s*opt\s+:([A-Za-z0-9_]+)/
OPTPARSE_BANNER = /banner\s*=\s*["']Usage:\s*(\S+)/
OPTPARSE_LONG = /\.on\s*\(?[^)]*?["'](-{2}[A-Za-z0-9][\w-]*)/

OptionParser: opts.on("-p", "--port PORT") — prefer the long name.

OPTPARSE_SHORT = /\.on\s*\(\s*["'](-[A-Za-z0-9])(["' ]|\))/
SLOP_CALL = /\bSlop\.(?:parse|new)\b/
SLOP_LONG = /\bo\.(?:string|integer|int|float|bool|boolean|array|symbol|null|on)\s+[^\n]*?["'](-{2}[A-Za-z0-9][\w-]*)/

Slop: o.string '-p', '--port'.

THOR_DESC = /^\s*desc\s+["']([^"'\s]+)/
THOR_OPTION = /^\s*(?:method_option|option)\s+:?["']?([A-Za-z0-9][\w-]*)/
THOR_SUBCLASS = /<\s*Thor\b/

Thor DSL.

TTY_ARGUMENT_DECL = /^\s*argument\s+:?(\w+)/
TTY_OPTION_DECL = /^\s*(?:option|flag|keyword)\s+:?(\w+)/

TTY::Option DSL (only when no Thor class is present, to avoid clashing with Thor's option).

WEB_FRAMEWORK_RE = /(?:^|\n)\s*require\s+["'](?:sinatra|rails|action_controller|active_record|grape|hanami|roda|rack|rackup|puma|unicorn|thin)\b|<\s*(?:Sinatra::Base|Grape::API|ApplicationController)\b|Rails\.application/

Web frameworks: their ENV reads are config, not a CLI surface.

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