How do you declare an Exec task to run an external command, and what are commandLine, args, and workingDir used for?
answer
- Exec forks a real OS process
- commandLine = executable + args list
- executable + args() split style
- workingDir, environment(), stream redirects
- NO shell — pass tokens separately
- isIgnoreExitValue for non-zero exits
basics
~10 sRegister a task of type Exec and set commandLine to the executable plus its arguments. workingDir sets the directory to run in; args lets you set arguments separately from the executable.
solid answer
~40 s`Exec` runs an external process as part of the build. You register it with `tasks.register<Exec>("...")` and configure the process: `commandLine("npm", "run", "build")` sets the full command (executable + args) as a list, or you split it: `executable = "npm"` plus `args("run", "build")`. `workingDir` sets the directory the process runs in (defaults to the project dir). You can also set `environment(...)`, redirect `standardOutput`/`errorOutput` to a stream, and capture the result. By default a non-zero exit code fails the build; set `isIgnoreExitValue = true` to tolerate it and inspect `executionResult` yourself. Pass a *list* of separate arguments rather than one space-joined string, because Gradle does not invoke a shell — arguments are passed directly to the OS process, so quoting and globbing won't be interpreted.
code
kotlin · 6 linestasks.register<Exec>("frontendBuild") {
workingDir = file("frontend")
commandLine("npm", "run", "build")
environment("NODE_ENV", "production")
// isIgnoreExitValue = true // tolerate non-zero exit and inspect executionResult
}go deeper
Know Exec runs an external command and you set commandLine plus workingDir.
Explain commandLine vs executable+args, workingDir/environment/stream redirects, isIgnoreExitValue, and that no shell is involved so tokens must be separate.
Discuss declaring inputs/outputs for up-to-date checking, capturing output streams, and configuration-cache compatibility via ExecOperations instead of config-time exec.
Weigh shelling out vs native Gradle tasks/plugins for reproducibility and cacheability, command-injection risk when interpolating untrusted input into sh -c, and cross-platform portability of external tooling.
## What Exec does `Exec` is a built-in task type that launches an **external operating-system process** during the build — e.g. invoking `npm`, `docker`, a shell script, or a code generator. It does not run inside the JVM; it forks a real process. ## Configuring the command There are two equivalent styles: ```kotlin // 1) one call with executable + all args tasks.register<Exec>("frontendBuild") { workingDir = file("frontend") commandLine("npm", "run", "build") } // 2) split executable and args tasks.register<Exec>("frontendBuild2") { workingDir = file("frontend") executable = "npm" args("run", "build") } ``` Key properties: - **`commandLine`** — a `List<Any>`: the executable followed by its arguments. - **`executable`** + **`args`** — the same thing split in two; `args` appends to the argument list. - **`workingDir`** — the directory the process is launched in (default: the project directory). - **`environment(key, value)`** / `environment(map)` — set/override env vars. - **`standardOutput` / `errorOutput` / `standardInput`** — redirect streams (e.g. to a `ByteArrayOutputStream` to capture output). - **`isIgnoreExitValue`** — if `false` (default) a non-zero exit fails the build; set `true` to handle it yourself via `executionResult.get().exitValue`. ## No shell is involved This is the most common trap. Gradle passes the argument list **directly** to the OS process API — there is **no shell** interpreting it. So: - `commandLine("echo $HOME")` does NOT expand `$HOME`. - Pipes (`|`), redirects (`>`), and globs (`*.txt`) are NOT interpreted. - You must pass each token as a separate list element: `commandLine("git", "rev-parse", "HEAD")`, not `commandLine("git rev-parse HEAD")`. If you genuinely need shell features, invoke the shell explicitly: `commandLine("sh", "-c", "echo $HOME")` (and beware the security implications of injecting untrusted strings there). ## Up-to-date and configuration cache caveats An `Exec` task has no automatic inputs/outputs, so Gradle re-runs it every time unless you declare them (`inputs.dir(...)`, `outputs.dir(...)`). Also, doing process execution at **configuration time** breaks the configuration cache; keep it inside the task action (which `Exec` does by design). For configuration-time needs, prefer the injected `ExecOperations` service.
- Why does commandLine("echo $HOME") not print your home directory?Gradle invokes the OS process directly without a shell, so $HOME is never expanded and "echo $HOME" is treated as one executable name. You'd need commandLine("sh", "-c", "echo $HOME") to get shell expansion.
- How do you let the build continue when the external command returns a non-zero exit code?Set isIgnoreExitValue = true. The build won't fail automatically; you then read executionResult.get().exitValue to decide how to react.
- Why might an Exec task run on every build, and how do you fix it?Exec declares no inputs/outputs by default, so Gradle can't prove it's up-to-date. Declare inputs (inputs.dir/files) and outputs (outputs.dir/file) so Gradle can skip it when nothing changed.
saying these in an interview costs you the question
- Passing the whole command as one space-joined string and expecting a shell to split/interpret it.
- Assuming pipes, redirects, globs, or env-var expansion work without explicitly invoking a shell.
- Running Process/exec at configuration time, breaking the configuration cache.