Skip to content

Commit 44a2072

Browse files
committed
feat: add command-level argument templating
Commands can now declare named arguments and distribute them to modules using ${argname} template syntax. This enables sharing arguments across multiple modules in a command. Signed-off-by: Fabian Wienand <[email protected]>
1 parent fa9dff2 commit 44a2072

File tree

7 files changed

+640
-19
lines changed

7 files changed

+640
-19
lines changed

cmds/dutagent/rpc.go

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,6 @@ func (a *rpcService) Commands(
7575
}
7676

7777
// Details is the handler for the Details RPC.
78-
//
79-
//nolint:funlen
8078
func (a *rpcService) Details(
8179
_ context.Context,
8280
req *connect.Request[pb.DetailsRequest],
@@ -113,6 +111,19 @@ func (a *rpcService) Details(
113111
return nil, e
114112
}
115113

114+
helpStr := buildCommandHelp(cmd)
115+
116+
res := connect.NewResponse(&pb.DetailsResponse{
117+
Details: helpStr,
118+
})
119+
120+
log.Print("Details-RPC finished")
121+
122+
return res, nil
123+
}
124+
125+
// buildCommandHelp constructs help text for a command based on its configuration.
126+
func buildCommandHelp(cmd dut.Command) string {
116127
var helpStr string
117128

118129
// Find help text: prefer interactive module's help, otherwise describe all modules
@@ -135,13 +146,15 @@ func (a *rpcService) Details(
135146
len(cmd.Modules), strings.Join(moduleNames, ", "))
136147
}
137148

138-
res := connect.NewResponse(&pb.DetailsResponse{
139-
Details: helpStr,
140-
})
141-
142-
log.Print("Details-RPC finished")
149+
// Append command args documentation if declared
150+
if len(cmd.Args) > 0 {
151+
helpStr += "\n\nArguments:\n"
152+
for _, arg := range cmd.Args {
153+
helpStr += fmt.Sprintf(" %s: %s\n", arg.Name, arg.Desc)
154+
}
155+
}
143156

144-
return res, nil
157+
return helpStr
145158
}
146159

147160
// streamAdapter decouples a connect.BidiStream to the dutagent.Stream interface.

cmds/dutagent/states.go

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -96,17 +96,33 @@ func findDUTCmd(_ context.Context, args runCmdArgs) (runCmdArgs, fsm.State[runCm
9696
// It returns a slice where each element contains the args for the corresponding module.
9797
// Returns an error if validation fails.
9898
func prepareModuleArgs(cmd dut.Command, runtimeArgs []string) ([][]string, error) {
99-
if len(runtimeArgs) > 0 && cmd.CountInteractive() == 0 {
100-
return nil, fmt.Errorf("arguments provided but command has no main module to receive them")
99+
// Validate that runtime args are only provided when an interactive module exists OR command declares args
100+
hasInteractive := cmd.CountInteractive() > 0
101+
hasCommandArgs := len(cmd.Args) > 0
102+
103+
if len(runtimeArgs) > 0 && !hasInteractive && !hasCommandArgs {
104+
return nil, fmt.Errorf("arguments provided but command has no interactive module and no args declaration")
105+
}
106+
107+
// If command declares args, validate count matches
108+
if hasCommandArgs && len(runtimeArgs) != len(cmd.Args) {
109+
return nil, fmt.Errorf("command expects %d argument(s) but got %d",
110+
len(cmd.Args), len(runtimeArgs))
101111
}
102112

103113
// Prepare args for each module
104114
moduleArgs := make([][]string, len(cmd.Modules))
105-
for i, module := range cmd.Modules {
115+
for idx, module := range cmd.Modules {
106116
if module.Config.Interactive {
107-
moduleArgs[i] = runtimeArgs
117+
moduleArgs[idx] = runtimeArgs
108118
} else {
109-
moduleArgs[i] = module.Config.Args
119+
// Non-interactive: substitute templates in configured args
120+
substitutedArgs, err := cmd.SubstituteArgs(module.Config.Args, runtimeArgs)
121+
if err != nil {
122+
return nil, fmt.Errorf("template substitution failed for module %q: %w", module.Config.Name, err)
123+
}
124+
125+
moduleArgs[idx] = substitutedArgs
110126
}
111127
}
112128

contrib/dutagent-cfg-example.yaml

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ devices:
1616
desc: "Report status"
1717
uses:
1818
- module: dummy-status
19-
interactive: true
19+
args:
20+
- foo
21+
- bar
2022
repeat:
2123
desc: "Repeat input"
2224
uses:
@@ -32,10 +34,17 @@ devices:
3234
interactive: true
3335
file-transfer:
3436
desc: "Transfer a file"
37+
args:
38+
- name: source-file
39+
desc: "Source file path"
40+
- name: dest-file
41+
desc: "Destination file path"
3542
uses:
3643
- module: dummy-status
3744
args:
38-
- foo
39-
- bar
45+
- transferring
46+
- "${source-file}"
4047
- module: dummy-ft
41-
interactive: true
48+
args:
49+
- "${source-file}"
50+
- "${dest-file}"

docs/command-arg-templating.md

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
# Command Arguments Guide
2+
3+
This guide explains the three ways to handle arguments in dutctl commands: non-interactive, interactive, and templating.
4+
5+
## Three Mutually Exclusive Approaches
6+
7+
Commands support three approaches for arguments. **These approaches are mutually exclusive** - a command must use exactly one:
8+
9+
1. **Non-Interactive Commands** - Static values only, no runtime arguments
10+
2. **Interactive Commands** - Single interactive module receives all runtime arguments
11+
3. **Command-Level Templating** - Named arguments distributed to modules via templates
12+
13+
You cannot mix approaches (e.g., a command cannot have both an interactive module AND command-level args).
14+
15+
## Non-Interactive Commands
16+
17+
For commands without runtime arguments, you specify static values directly in module args:
18+
19+
```yaml
20+
devices:
21+
my-device:
22+
cmds:
23+
power-cycle:
24+
desc: "Power cycle the device"
25+
modules:
26+
- module: gpio-switch
27+
args: ["power-pin", "off"]
28+
- module: time-wait
29+
args: ["2000"]
30+
- module: gpio-switch
31+
args: ["power-pin", "on"]
32+
```
33+
34+
Usage:
35+
36+
```bash
37+
dutctl run my-device power-cycle
38+
```
39+
40+
No runtime arguments needed - all values are configured in the YAML.
41+
42+
## Interactive Commands
43+
44+
Commands with an interactive module pass all runtime arguments directly to that module:
45+
46+
```yaml
47+
devices:
48+
my-device:
49+
cmds:
50+
run-command:
51+
desc: "Run shell command"
52+
modules:
53+
- module: shell
54+
interactive: true
55+
```
56+
57+
Usage:
58+
59+
```bash
60+
dutctl run my-device run-command ls -la /tmp
61+
```
62+
63+
The interactive module receives: `["ls", "-la", "/tmp"]`
64+
65+
All runtime arguments go to the interactive module - you cannot have multiple interactive modules in one command.
66+
67+
## Command-Level Templating
68+
69+
Declare named arguments at the command level and distribute them to modules using `${arg-name}` template syntax. Arguments are mapped positionally in declaration order.
70+
71+
```yaml
72+
flash-firmware:
73+
desc: "Flash firmware to device"
74+
args:
75+
- name: firmware-file
76+
desc: "Path to firmware binary"
77+
- name: backup-path
78+
desc: "Backup location"
79+
modules:
80+
- module: shell
81+
args: ["flashrom", "-r", "${backup-path}"]
82+
- module: file
83+
args: ["${firmware-file}", "/tmp/fw.bin"]
84+
- module: flash
85+
args: ["/tmp/fw.bin"]
86+
```
87+
88+
Usage: `dutctl run device flash-firmware fw.bin /backup/old.bin`
89+
90+
Templates can be embedded in strings (`/configs/${name}.yaml`) and mixed with static values (`["${file}", "static", "${other}"]`).
91+
92+
## Examples
93+
94+
### Flash with Verification
95+
96+
```yaml
97+
flash-verify:
98+
desc: "Flash firmware and verify"
99+
args:
100+
- name: firmware-path
101+
desc: "Path to firmware binary"
102+
modules:
103+
- module: file
104+
args: ["${firmware-path}", "/tmp/fw.bin"]
105+
- module: flash
106+
args: ["/tmp/fw.bin"]
107+
- module: time-wait
108+
args: ["500"]
109+
- module: shell
110+
args: ["flashrom", "-v", "/tmp/fw.bin"]
111+
```
112+
113+
```bash
114+
dutctl run device flash-verify /path/to/firmware.bin
115+
```
116+
117+
### GPIO Control with Parameters
118+
119+
```yaml
120+
gpio-pulse:
121+
desc: "Pulse GPIO pin"
122+
args:
123+
- name: pin-name
124+
desc: "GPIO pin identifier"
125+
- name: duration
126+
desc: "Pulse duration in milliseconds"
127+
modules:
128+
- module: gpio-switch
129+
args: ["${pin-name}", "on"]
130+
- module: time-wait
131+
args: ["${duration}"]
132+
- module: gpio-switch
133+
args: ["${pin-name}", "off"]
134+
```
135+
136+
```bash
137+
dutctl run device gpio-pulse reset-button 100
138+
```

docs/dutagent-config.md

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@ The configuration mainly consists of a list of devices connected to an agent and
66
for those devices. Commands are meant to be the high-level tasks you want to perform on the device, e.g.
77
"Flash the firmware with the given image." To achieve this high-level task, commands can be built up of one or multiple
88
_Modules_. Modules represent the basic operations and represent the actual implementation for the hardware interaction.
9-
The implementation of a Module determines its capabilities and also exposes information on how to use and configure it.
9+
The implementation of a Module determines its capabilities and also exposes information on how to use and configure it.
1010

1111
The DUT Control project offers a collection of Module implementations but also allows for easy integration of [custom modules](./module_guide.md).
1212
Often a _Command_ can consist of only one _Module_ to get the job done, e.g., power cycles the device. But in some cases
1313
like the flash example mentioned earlier, eventually it is mandatory to toggle some GPIOs before doing the actual SPI flash
1414
operation. In this case the command is built up of a Module dealing with GPIO manipulation and a Module performing a
1515
flash writing with a specific programmer. See the second device in the [example](#example-config-file) down below on what this
16-
could look like.
16+
could look like. Commands support three approaches for arguments: non-interactive with static values, interactive modules that receive all runtime arguments directly, and command-level argument templating that distributes named arguments to modules via template syntax (see [Command Argument Templating](./command-arg-templating.md)).
1717

1818
## DUT Agent Configuration Schema
1919

@@ -34,19 +34,42 @@ could look like.
3434
| Attribute | Type | Default | Description | Mandatory |
3535
|-------------|----------------------|---------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------|
3636
| description | string | | Command description | no |
37+
<<<<<<< HEAD
3738
| uses | [] [Module](#Module) | | A command may be composed of multiple steps to achieve its purpose. The list of modules represent these steps. The order of this list is important. At most one module may be set as the interactive module. If an interactive module is present, all arguments to the command are passed to it, and its usage information is used as the command help text. | yes |
39+
=======
40+
| args | [][Argument](#command-arguments) | | Named arguments that can be passed to the command at runtime and distributed to modules via template syntax. Arguments are mapped positionally in declaration order. **Cannot be used with interactive modules** - use one or the other. | no |
41+
| Modules | [] [Module](#Module) | | A command may be composed of multiple steps to achieve its purpose. The list of modules represent these steps. The order of this list is important. At most one module may be set as the interactive module. If an interactive module is present, all runtime arguments are passed to it. If command-level args are declared, they are distributed to non-interactive modules via template substitution. | yes |
42+
43+
### Command Arguments
44+
45+
Command arguments define named parameters that can be passed at runtime and distributed to modules via template syntax.
46+
47+
| Attribute | Type | Default | Description | Mandatory |
48+
|-----------|--------|---------|--------------------------------------|-----------|
49+
| name | string | | Argument name (used in templates) | yes |
50+
| desc | string | | Human-readable argument description | yes |
51+
>>>>>>> f0de77e (feat: add command-level argument templating)
3852
3953
### Module
4054

4155
| Attribute | Type | Default | Description | Mandatory |
4256
|-----------|----------------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------|
4357
| module | string | | The module's name also serves as its identifier and must be unique. | yes |
58+
<<<<<<< HEAD
4459
| interactive | bool | false | Marks this module as the interactive module. All runtime arguments to a command are passed to its interactive module. The interactive module's usage information is also used as the command help text. | 0 or 1 times per command |
4560
| args | []string | nil | If a module is **not** an commands interactive module, it does not get any arguments passed at runtime, instead arguments can be passed here. | no, only applies if `main` is set |
4661
| with | map[string]any | | A module can be configured via key-value pairs. The type of the value is generic and depends on the implementation of the module. | yes |
4762

4863
> [!IMPORTANT]
4964
> Refer to `with` keys of a module in all-lowercase representation of the module's exported fields.
65+
=======
66+
| interactive | bool | false | Marks this module as the interactive module. All runtime arguments to a command are passed to its interactive module. The interactive module's usage information is also used as the command help text. | no |
67+
| args | []string | nil | Arguments for non-interactive modules. Can contain static values or template references like `${arg-name}` to command-level args. | no |
68+
| options | map[string]any | | A module can be configured via key-value pairs. The type of the value is generic and depends on the implementation of the module. | yes |
69+
70+
> [!IMPORTANT]
71+
> Refer to option keys of a module in all-lowercase representation of the modules exported fields.
72+
>>>>>>> f0de77e (feat: add command-level argument templating)
5073
> See the respective module's documentation for details.
5174
5275
### Example config file

0 commit comments

Comments
 (0)