4.2.3 Args & Command Lines
ctx.actions.args() is the boundary between "I know what this action needs" and "I can safely spell that need as a command line." It lets a rule author pass files, strings, lists, and depsets to an action without flattening every transitive input during analysis. When the rule calls use_param_file(), it also gives Bazel enough structure to choose inline arguments or a param file at execution time.1 If 4.2.2 Actions taught you that actions are (inputs, command) -> outputs, this item is about making the command part scale.
Adds one value, for example --out then out.
Expands a list or depset to repeated argv items.
Maps each include to argv such as -Ia, -Ib.
The action receives the object as arguments = [args].
Why Lists Stop Scaling
The tempting version of a rule implementation builds a Python-style list:
arguments = ["--out", out.path]
arguments += ["-I%s" % f.short_path for f in headers.to_list()]
That works for a tiny rule. It is the wrong shape for a production ruleset. As covered in 4.1.5 depset vs list, transitive information should travel through providers as depsets, because a depset can share the same dependency subgraph across many targets instead of copying it into a fresh list at every node.2 Flattening that depset with to_list() while building command lines throws away the sharing benefit and can create O(N^2) time or memory cost across a large target graph.3
Args keeps the command line as a structured object. It can hold a depset directly and delay expansion until Bazel is preparing the action for execution.1 If the action is already cached, the expensive "turn this whole transitive closure into strings" step may never be needed.4
Build the Command Line as Data
Create one with ctx.actions.args(), append to it, and pass it as an element of the action's arguments list.5
def _include_arg(file):
return file.short_path
def _impl(ctx):
out = ctx.actions.declare_file(ctx.label.name + ".out")
transitive_headers = depset(
transitive = [dep[HeaderInfo].headers for dep in ctx.attr.deps],
)
inputs = depset(
direct = ctx.files.srcs,
transitive = [transitive_headers],
)
args = ctx.actions.args()
args.add("--out", out)
args.add_all(ctx.files.srcs, before_each = "--src")
args.add_all(
transitive_headers,
format_each = "-I%s",
map_each = _include_arg,
)
ctx.actions.run(
executable = ctx.executable._tool,
arguments = [args],
inputs = inputs,
outputs = [out],
mnemonic = "CompileThing",
)
The example uses add() for one value, add_all() for repeated values, and format_each plus map_each when the tool expects a transformed spelling. Args automatically converts File values to paths, so the rule does not need to write out.path or [src.path for src in ctx.files.srcs] just to make the command line valid.1 Keep the map_each callback as a top-level def: Bazel documents nested closures as disabled by default because they can accidentally retain large analysis-phase objects into the execution phase.6
Use the extra "arg name" form only when you want one flag before a non-empty sequence:
args.add_all("--sources", ctx.files.srcs)
That produces --sources a.cc b.cc when srcs is non-empty, and omits --sources when it is empty by default.7 For repeated flag-value pairs, use before_each or format_each, as in --src a.cc --src b.cc or -Ia -Ib.
add_joined() is for tools that want many values collapsed into one argument, such as a comma-separated list. It follows the same lazy processing model as add_all(), but joins the expanded values with join_with and optionally applies format_joined.8
Param Files Are Part of the Rule Contract
Operating systems limit command-line length, and Windows tends to make long argument vectors fail earlier than Unix-like systems. Args.use_param_file() lets Bazel spill the command line into a param file when needed, replacing the spilled arguments with a pointer such as @path/to/params.9
args = ctx.actions.args()
args.add("--out", out)
args.add_all(transitive_inputs)
args.use_param_file("@%s")
args.set_param_file_format("multiline")
args.set_param_file_path(ctx.label.name + ".params")
The rule author still chooses the format and, when necessary, the filename. Some tools expect @file, some expect --flagfile=file, and some do not support param files at all. Bazel's default param-file format is shell. Available formats also include multiline and flag_per_line, where flag_per_line is designed for flag libraries that expect one --flag=value style entry per line.10 Use set_param_file_path() when the tool expects a fixed filename or when multiple Args objects need coordinated spill files instead of Bazel's default name derived from the action's primary output.10 When debugging, remember that Bazel may avoid materializing the param file in the output tree for efficiency. --materialize_param_files asks Bazel to write it so you can inspect it.9
Prefer run() Over Shell Strings
ctx.actions.run() invokes an executable with a structured argument list. ctx.actions.run_shell() invokes a shell command and then exposes arguments through shell variables such as $1, $2, and $@.11 If an Args object has unknown size, later positional indexes in run_shell() can become unpredictable because the object is flattened into individual arguments before the shell sees them.11
That is one reason to keep ordinary tool execution in run(): less quoting logic, fewer OS-specific shell assumptions, and a clearer action contract.4 Use run_shell() when the shell is genuinely part of what the rule promises, not as a convenience for string concatenation. The broader production version of this decision includes inputs, tools, environment, mnemonics, and scheduling tags in 4.4.3 Action Execution Contract.
For a complete command line, see the mini-ruleset's
GlyphCompile Args construction.
It keeps sources and provider-derived inputs lazy, uses repeated flags, selects
a param-file format, and passes the resulting Args object to run().
Use Args when command-line data can grow with dependencies. Lists are fine for small rule-local constants. Depsets and ctx.actions.args() are the scalable path for transitive files, repeated flags, and param-file fallback.
The command line is not just a string. It is part of the action's public behavior, so choose the spelling your tool accepts, keep transformations lazy, and make param-file support explicit.
Check your understanding · 4 questions
1.Why should a rule use ctx.actions.args() for command lines that include transitive files?
Select one answer
2.Which statements describe correct Args usage?
Select all that apply
3.True or false: command-line construction boundaries for custom rules.
Choose True or False for each sentence
File to Args lets Bazel convert it to a path lazily.depset.to_list() before building every command line preserves depset sharing.--materialize_param_files can help debug a param file that Bazel would otherwise avoid writing to the output tree.4.Match each Args spelling to the command-line shape it is meant to model.
Drag each answer onto the matching prompt, or click an answer and then click a prompt
args.add_all("--sources", ctx.files.srcs)args.add_all(ctx.files.srcs, before_each = "--src")args.add_joined(values, join_with = ",")args.use_param_file("@%s"); args.set_param_file_format("flag_per_line")Footnotes
-
Args — overview of memory-efficient command-line construction and deferred depset expansion. ↩1 ↩2 ↩3
-
Optimizing Performance — depsets share transitive dependency data instead of repeatedly copying lists. ↩
-
Optimizing Performance — warning that flattening depsets with
to_list()can create O(N^2) costs. ↩ -
Writing Bazel rules: moving logic to execution — practical
ctx.actions.run()andArgsuse in rule actions, including lazy depset iteration. ↩1 ↩2 -
actions —
ctx.actions.args()and theargumentsparameter forrun()/run_shell(). ↩ -
Args —
map_eachtop-level function requirement andallow_closurecaveat. ↩ -
Args — alternate arg-name form and
omit_if_emptybehavior foradd_all(). ↩ -
Args —
add_joined()lazy processing and formatting parameters. ↩ -
Args —
use_param_file()behavior, command-line limits, and--materialize_param_files. ↩1 ↩2 -
Args —
set_param_file_format()formats and default.set_param_file_path()overrides Bazel's default param-file name. ↩1 ↩2 -
actions —
run_shell()argument indexing andArgsflattening behavior. ↩1 ↩2