Feature request: First-class array input for Feature options (array option type + multiple invocations) [blocked on spec#766]
Feature request: First-class array input for Feature options (array option type + multiple invocations)
Complex feature request — depends on the spec change in devcontainers/spec#766 (RFC). This issue tracks the reference CLI implementation work once the spec RFC is accepted. Related long-standing issues: spec#57 (array option type, open since 2022) and spec#44 (install a feature more than once).
Problem
The dev container spec restricts Feature option values to boolean and string. There is no
array type. As a result, every Feature that needs a list (packages, extensions, tools, versions)
is forced to accept a comma-separated string and split it inside install.sh.
This CLI encodes that limitation directly in its TypeScript types, which is the root cause that blocks any progress:
// src/spec-configuration/containerFeaturesConfiguration.ts
export type FeatureOption = {
type: 'boolean';
default?: boolean;
description?: string;
} | {
type: 'string';
enum?: string[];
default?: string;
description?: string;
} | {
type: 'string';
proposals?: string[];
default?: string;
description?: string;
};
// src/spec-configuration/configuration.ts
export interface DevContainerFeature {
userFeatureId: string;
options: boolean | string | Record<string, boolean | string | undefined>;
}
// ...
features?: Record<string, string | boolean | Record<string, string | boolean>>;
FeatureOption has no 'array' variant. DevContainerFeature.options and the features map
only permit boolean | string for option values — arrays are not representable, so they can
never reach install.sh no matter what a user writes in devcontainer.json.
Real-world impact (shipped Features, today)
| Feature | Option | Today | Symptom |
|---|---|---|---|
ghcr.io/rocker-org/devcontainer-features/r-packages:1 |
packages |
"cli,rlang" |
Comma-joined; values containing commas are unrepresentable. |
ghcr.io/devcontainers/features/github-cli |
extensions | "github/gh-copilot" |
Comma-joined; extension refs/args with commas break. |
mwmahlberg/devcontainer-features npm-packages |
packages |
"typescript,eslint" |
Accepts comma or whitespace or newline — three delimiters, because the spec gives no canonical list form. |
The npm-packages case is the clearest failure signal: with no array type, every Feature author
invents a different delimiter. Consumers cannot reason about a list option without reading each
Feature's install.sh.
This was always meant to be temporary. From spec#57, maintainer @Chuxel (2022):
Right now things in devcontainers/features are using a comma separated string as a near term workaround. Converting this into an array is pretty easy…
That workaround has now been the de facto standard for 3+ years.
Requirements (hard — no alternatives)
This is a required capability, not a nice-to-have. Comma-separated strings are not an acceptable long-term substitute (they are ambiguous, lossy, and force per-Feature delimiter conventions). The CLI must implement:
arrayoption type —devcontainer-feature.jsonmay declare"type": "array"for an option, withdefaultas an array andproposals/enumconstraining elements.- Array option values in
devcontainer.json—"packages": ["curl","git","jq"]must be accepted, validated, and propagated. - Multiple invocations via array of option objects — a Feature value may be an array of option
objects (
"dotnet": [{"version":"3.1"},{"version":"6.0"}]), invokinginstall.shonce per element in order (resolves spec#44). - Option Resolution for arrays — array values are serialized to
devcontainer-features.envas a JSON array string (PACKAGES='["curl","git","jq"]'), the only delimiter-free, unambiguous encoding. - Backward-compatible string→array coercion — if a string is supplied for an
array-typed option: parse as JSON if it looks like a JSON array, else split on commas. Existing comma-separated Features keep working when they migrate totype: "array". - CLI flag support — accept JSON array values in
--override-features, and support repeated flags (--feature-option <feature>.<option> <value>) that append to an array option.
Proposed implementation
1. Type changes
src/spec-configuration/containerFeaturesConfiguration.ts — extend FeatureOption:
export type FeatureOption = {
type: 'boolean';
default?: boolean;
description?: string;
} | {
type: 'string';
enum?: string[];
default?: string;
description?: string;
} | {
type: 'string';
proposals?: string[];
default?: string;
description?: string;
} | {
type: 'array'; // NEW
enum?: string[]; // constrains elements
proposals?: string[]; // suggests elements
default?: (string | boolean | number)[];// array default
description?: string;
};
src/spec-configuration/configuration.ts — widen option value and Feature value types:
export interface DevContainerFeature {
userFeatureId: string;
// allow arrays of primitives as option values, and array-of-option-objects as the Feature value
options: boolean | string | (string | boolean | number)[] | Record<string, boolean | string | (string | boolean | number)[] | undefined> | Record<string, boolean | string | (string | boolean | number)[]>[];
}
// ...
features?: Record<string, string | boolean | (string | boolean | number)[] | Record<string, boolean | string | (string | boolean | number)[]> | Record<string, boolean | string | (string | boolean | number)[]>[]>;
2. Option parsing & normalization
Where feature option values are read and normalized (the getFeatureValueDefaults /
option-resolution path in containerFeaturesConfiguration.ts):
- When
option.type === 'array':- Accept a JSON array value as-is.
- If the supplied value is a string, attempt
JSON.parse; if it yields an array, use it; otherwise split on,and trim each element. Emit a warning recommending the array form. - Validate each element against
enum/proposalswhen present. - Default to
[]when omitted and nodefault.
3. Option Resolution (env serialization)
In the code that writes devcontainer-features.env (<OPTION_NAME>=<value>):
- For an
arrayoption, serialize the value withJSON.stringify(value)so the env var holds the canonical JSON array string. Example output:PACKAGES='["curl","git","jq"]' - This keeps a single, unambiguous source of truth.
install.shparses withjq(already ubiquitous in dev container images):for pkg in $(printf '%s' "$PACKAGES" | jq -r '.[]'); do apt-get install -y "$pkg"; done
4. Multiple invocations (array of option objects)
In the feature install layer (getFeatureLayers / getFeatureInstallWrapperScript and the
surrounding orchestration):
- When a Feature's value is an array of option objects, emit one install layer/wrapper per element,
in array order, each with its own
devcontainer-features.env. All invocations of a given Feature run consecutively at that Feature's position in the install order (do not interleave with other Features).
5. CLI flags
--override-features: already accepts a JSON blob; ensure array option values and array-of-objects Feature values parse and flow through the new types.- Add/confirm a repeated-flag form:
--feature-option <feature>.<option> <value>appends to an array-typed option (and sets/replaces for scalar options).
6. Validation & errors
- Reject non-array values for
array-typed options (after string coercion) with a clear error naming the Feature and option. - Validate
enumelements; report the offending element.
Test plan
-
devcontainer-feature.jsonwithtype: "array"parses and surfacesdefault/enum/proposals. -
devcontainer.jsonwith"packages": ["curl","git"]reachesinstall.shasPACKAGES='["curl","git"]'. - String
"curl,git"for anarrayoption coerces to["curl","git"]with a warning. - String
'["curl","git"]'(JSON) coerces to the array without warning. -
enum-constrained array option rejects an out-of-set element. - Array-of-option-objects Feature value invokes
install.shtwice with distinct env vars, in order. - Repeated
--feature-optionflags append to an array option. - Existing comma-separated Features (e.g.
r-packages) continue to work unchanged after migrating their option totype: "array". - JSON Schema (
devContainerFeature.schema.json) validates the new shapes.
Use cases
- Package lists —
r-packages,npm-packages,github-cliextensions, Homebrew formulae: explicit arrays, values may contain commas. - Multiple runtime versions — install
.NET 3.1and6.0(or Node18+20) in one image via array-of-option-objects, without bespoke per-Feature "multi-version" options. - Multi-select tool bundles — a Feature offering a curated subset of tools; with
enumelements the UX can render a multi-select picker. - Tool-readable config — schema validation, IntelliSense, and diffs work natively on JSON
arrays; comma-strings are opaque to every tool except the splitting
install.sh.
Prior art (alternative stacks)
The dev container spec is the only major dev-environment format lacking a native list type for user-supplied option values:
| Stack | List input | Notes |
|---|---|---|
| Coder | coder_parameter type = "list(string)"; UI multi-select/tag-select; defaults via jsonencode([...]) |
First-class list type. Coder's docs warn that overriding list(string) on the CLI is "tricky" (CSV+JSON quoting) and offer a YAML workaround — exactly the ambiguity this CLI should avoid by defining array semantics up front. |
Nix (mkShell) |
Native lists packages = [ curl git jq ]; |
First-class; no string parsing. |
Gitpod (.gitpod.yml) |
Native YAML arrays (tasks, ports, vscode.extensions) |
First-class. |
| Docker Compose | Native YAML arrays (volumes, ports, environment) |
First-class. |
| Helm | Native YAML arrays in values.yaml, iterated with range |
First-class. |
| Terraform | list(string), list(any) native variable types |
First-class. |
| Dev Containers (this CLI) | ❌ No array option type — comma-separated string only |
Outlier. |
Dependencies & unblocking
- Blocked on spec acceptance: devcontainers/spec#766
(the RFC defining
arrayoption type, Option Resolution for arrays, and array-of-option-objects). - Closes the long-standing workaround: spec#57 (2022) and spec#44.
Once the spec RFC lands, this issue is the implementation tracker for the reference CLI. The type changes in §1 are the minimal unblocking step; everything else follows from the spec's normative requirements.
References
- Spec RFC: https://github.com/devcontainers/spec/issues/766
- Original array-option request: https://github.com/devcontainers/spec/issues/57
- Multiple invocations: https://github.com/devcontainers/spec/issues/44
- Spec option resolution: https://containers.dev/implementors/features/#option-resolution
- Coder
list(string)parameters: https://coder.com/docs/admin/templates/extending-templates/parameters
Source: devcontainers/cli