Skip to content

CLI Reference

pluckit uses a chain-based CLI where a source comes first, followed by one or more operation steps. There are no subcommands for querying, viewing, or editing — everything is expressed as a chain of steps.

pluckit [FLAGS] SOURCE STEP [STEP...] [-- STEP...]
pluckit --json JSON_STRING
pluckit init [--force-reinstall] [--quiet]

Run pluckit --help for a summary of flags and operations.


Sources

The first positional argument is the source — a file path, glob pattern, or a named shortcut:

# Glob pattern
pluckit "src/**/*.py" find ".fn:exported" names

# Explicit file
pluckit src/auth.py find ".fn#validate_token" text

# Named shortcuts
pluckit -c find ".fn:exported" count      # --code
pluckit -d find ".cls" names              # --docs
pluckit -t find ".fn[name^=test_]" names  # --tests

Shortcuts (-c/--code, -d/--docs, -t/--tests) resolve via the [tool.pluckit.sources] table in pyproject.toml. See Configuration below.


Steps

After the source, every token is part of a step chain. Each recognized operation name starts a new step. Positional tokens between operation names are arguments to that step. --key=value tokens become keyword arguments.

pluckit src/**/*.py find ".fn:exported" filter --min-lines=10 text
#       ^^^^^^^^^^^ ^^^^ ^^^^^^^^^^^^^^ ^^^^^^ ^^^^^^^^^^^^^^^ ^^^^
#       source      op   arg            op     kwarg           op

A bare -- is equivalent to the reset operation — it clears the current selection and starts a new find context:

pluckit src/**/*.py find ".fn#foo" rename "bar" -- find ".call#foo" replace "foo" "bar"

Query operations

Operation Arguments Description
find SELECTOR Select nodes matching a CSS selector
filter [--kwargs] Narrow the current selection
not_ Invert the current selection
Operation Arguments Description
parent Move to parent nodes
children Move to direct children
siblings Nodes sharing a parent
ancestor Walk up the AST
next Next sibling
prev Previous sibling
containing TEXT Nodes whose source contains text
at_line N Node at a specific line number
at_lines START END Nodes within a line range

Terminal operations

Operation Arguments Description
count Print the number of matches
names Print one identifier name per line
text Print the source text of each match
attr NAME Print a named attribute of each match
complexity Print cyclomatic complexity
materialize Print matches as JSON

Pagination operations

Operation Arguments Description
limit N Take only the first N matches
offset N Skip the first N matches
page N SIZE Page N (0-indexed), page size SIZE

When any pagination op appears in a chain, the result JSON gains source_chain + page: {offset, limit, total, has_more} fields. total is None by default (lazy); has_more is heuristic until total is computed.

Mutation operations

Operation Arguments Description
addParam PARAM Add a parameter to matched functions
removeParam NAME Remove a parameter by name
addArg EXPR Add an argument to matched calls
removeArg NAME Remove a keyword argument by name
rename NEW Rename the matched definition
prepend TEXT Prepend lines to the body
append TEXT Append lines to the body
insertBefore ANCHOR CODE Insert code before an anchor selector
insertAfter ANCHOR CODE Insert code after an anchor selector
wrap BEFORE AFTER Wrap the matched node
unwrap Remove enclosing context
remove Delete the matched node
clearBody Replace body with pass / {}
replaceWith TEXT Replace the entire matched node
replace OLD NEW String-level replace within the node
patch CONTENT Apply a unified diff or replacement

Plugin operations

Operation Arguments Description
view [QUERY] Render matched nodes as markdown
history Show git history for matched nodes
authors List distinct commit authors
at REV Show the node at a historical revision
diff REV Unified diff against a revision
blame Annotate matched nodes with blame info

Control operations

Operation Arguments Description
reset Clear selection, start fresh (same as bare --)
pop Return to the previous selection

@file Argument Syntax

Any string argument in a chain step can reference a file with the @path prefix. The file's content replaces the argument at evaluation time:

# Read replacement code from a file
pluckit src/**/*.py find ".fn#handler" replaceWith @patches/new_handler.py

# Apply a patch file
pluckit src/**/*.py find ".fn#handler" patch @refactor.patch

# Escape: literal @path (no file read)
pluckit src/**/*.py find ".fn#handler" replaceWith @@not_a_file

In JSON, the same convention applies as a string "@path" or an explicit object {"file": "path"}:

{"op": "replaceWith", "args": ["@patches/new_handler.py"]}
{"op": "replaceWith", "args": [{"file": "patches/new_handler.py"}]}

Paths resolve relative to the current working directory. The @path reference stays as-is in serialized chains (to_dict / to_json / to_argv) — resolution happens only at evaluate() time, keeping chains portable.


Global Flags

Flag Description
--plugin NAME Load a named plugin
--repo DIR Repository root for relative paths (default: cwd)
--dry-run, -n Show what would change without writing
--diff Output mutations as unified diff (no file writes)
--json JSON Run a chain from a JSON string (see JSON I/O)
--to-json Print the chain as JSON instead of executing it
--version Print version and exit
-h, --help Print help and exit

Result Formatting

The terminal operation you choose determines the output format:

Terminal Output
count A single number on stdout
names One identifier name per line
text Source text of each matched node, separated by newlines
view Rendered markdown (requires AstViewer plugin)
materialize JSON array of matched nodes
mutations A summary line printed to stderr
--diff Unified diff of what mutations would change (stdout)

--dry-run prevents file writes and reports a summary to stderr. --diff also prevents file writes, but outputs a unified diff to stdout that can be piped to patch, git apply, or pluckit's own patch operation. When both are set, --diff takes precedence.


pluckit init

The only named subcommand. Eagerly installs and verifies the DuckDB community extensions pluckit depends on. This happens lazily on first use of any other command, but running it explicitly gives clearer diagnostics if anything fails.

pluckit init [--force-reinstall] [--quiet]

Options

Flag Description
--force-reinstall Re-install extensions even if they already load
--quiet Suppress success messages; only print errors

Exit codes

Code Meaning
0 Required extensions are ready (optional ones may be missing)
1 A required extension could not be installed or loaded

Example

$ pluckit init
  sitting_duck (required): loaded
  duck_tails (optional): loaded

pluckit init: all extensions ready.

JSON I/O

Chains can be expressed as JSON for programmatic use — pass them via --json on the command line or pipe them to tools that generate pluckit invocations.

{
  "source": ["src/**/*.py"],
  "plugins": ["AstViewer"],
  "steps": [
    {"op": "find", "args": [".fn:exported"]},
    {"op": "count"}
  ]
}
# From the command line
pluckit --json '{"source": ["src/**/*.py"], "steps": [{"op": "find", "args": [".fn:exported"]}, {"op": "count"}]}'

# Inspect what a CLI invocation would look like as JSON
pluckit --to-json src/**/*.py find ".fn:exported" count

Each step object has:

Field Type Description
op str Operation name (e.g. find, count)
args list[str] Positional arguments (optional)
kwargs dict Keyword arguments (optional)

Top-level fields:

Field Type Description
source list[str] File paths or glob patterns
plugins list[str] Plugin names to load (optional)
steps list[step] Ordered list of step objects

Configuration

pluckit reads configuration from the [tool.pluckit] table in pyproject.toml:

[tool.pluckit]
plugins = ["AstViewer"]
cache = true                      # opt-in persistent AST cache
cache_path = ".pluckit.duckdb"    # custom cache location

[tool.pluckit.sources]
code = ["src/**/*.py"]
docs = ["docs/**/*.md"]
tests = ["tests/**/*.py"]

The plugins list names plugins to load by default. The [tool.pluckit.sources] table defines named shortcuts that the -c, -d, and -t flags (and any custom names) resolve against.

Set cache = true to enable the persistent AST cache — pluckit materializes read_ast output into per-pattern tables in a .pluckit.duckdb file and re-parses only files whose mtime has changed. Override the file location with cache_path.


Examples

# Count all exported functions
pluckit src/**/*.py find ".fn:exported" count

# List function names in a file
pluckit src/auth.py find ".fn" names

# View a specific function as markdown
pluckit src/auth.py find ".fn#validate_token" view

# Dry-run a rename
pluckit -n src/*.py find ".fn#old_name" rename "new_name"

# Chain multiple operations with reset
pluckit src/**/*.py find ".fn#foo" rename "bar" -- find ".call#foo" replace "foo" "bar"

# Navigate: find classes then their methods
pluckit src/**/*.py find ".cls:exported" children names

# Use a named source shortcut
pluckit -c find ".fn:exported" count

# Add a parameter to every exported function
pluckit src/**/*.py find ".fn:exported" addParam "timeout: int = 30"

# Get the source text of a function at a historical revision
pluckit src/auth.py find ".fn#validate_token" at v0.1.0

# View complexity of matched functions
pluckit src/**/*.py find ".fn" complexity

# JSON round-trip
pluckit --to-json src/**/*.py find ".fn:exported" count | pluckit --json "$(cat -)"

# Preview a rename as a unified diff
pluckit src/**/*.py find ".fn#old_api" rename "new_api" --diff

# Save diff to file, then apply
pluckit src/**/*.py find ".fn#old_api" rename "new_api" --diff > refactor.patch
pluckit src/**/*.py find ".fn#old_api" patch @refactor.patch

# Replace a function body from a file
pluckit src/api.py find ".fn#handler" replaceWith @patches/new_handler.py

Migration Guide

The old CLI had separate view, find, and edit subcommands. These are gone. Everything is now a chain where the source comes first, followed by operations.

view subcommand

# Old
pluckit view ".fn#main" src/**/*.py
pluckit view ".fn#main { show: signature; }" src/**/*.py

# New
pluckit src/**/*.py find ".fn#main" view
pluckit src/**/*.py find ".fn#main" view ".fn#main { show: signature; }"

find subcommand

# Old
pluckit find ".fn:exported" --format names src/**/*.py
pluckit find ".fn:exported" --count src/**/*.py
pluckit find ".cls" --format json src/models.py

# New
pluckit src/**/*.py find ".fn:exported" names
pluckit src/**/*.py find ".fn:exported" count
pluckit src/models.py find ".cls" materialize

edit subcommand

# Old
pluckit edit ".fn#foo" --add-param "x: int" src/*.py
pluckit edit ".fn#deprecated_helper" --remove src/*.py
pluckit edit --dry-run ".fn#top_level_fn" --rename renamed src/sample.py
pluckit edit ".cls#Foo .fn#__init__" --add-param "foo: int = 30" \
                                     --append-lines "self.foo = foo" \
    -- ".call#Foo" --add-arg "foo=10" src/**/*.py

# New
pluckit src/*.py find ".fn#foo" addParam "x: int"
pluckit src/*.py find ".fn#deprecated_helper" remove
pluckit -n src/sample.py find ".fn#top_level_fn" rename "renamed"
pluckit src/**/*.py find ".cls#Foo .fn#__init__" addParam "foo: int = 30" \
    append "self.foo = foo" -- find ".call#Foo" addArg "foo=10"

Key differences

Old CLI New CLI
pluckit view QUERY PATHS pluckit PATHS find SELECTOR view
pluckit find SEL PATHS pluckit PATHS find SEL terminal
pluckit edit SEL --op PATHS pluckit PATHS find SEL op
--format names names (terminal operation)
--format json materialize (terminal operation)
--count count (terminal operation)
--add-param addParam (mutation operation)
--remove remove (mutation operation)
-- SELECTOR --op -- find SELECTOR op or reset find ...
Paths at the end Source at the beginning