Skip to content

Commit 95a4824

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 <fabian.wienand@9elements.com>
1 parent b81b408 commit 95a4824

File tree

7 files changed

+630
-26
lines changed

7 files changed

+630
-26
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,18 +96,34 @@ 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, cmdName string, runtimeArgs []string) ([][]string, error) {
99-
if len(runtimeArgs) > 0 && cmd.CountInteractive() == 0 {
100-
return nil, fmt.Errorf("arguments provided but command %q 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 %q has no interactive module and no args declaration",
101105
cmdName)
102106
}
103107

108+
// If command declares args, validate count matches
109+
if hasCommandArgs && len(runtimeArgs) != len(cmd.Args) {
110+
return nil, fmt.Errorf("command %q expects %d argument(s) but got %d",
111+
cmdName, len(cmd.Args), len(runtimeArgs))
112+
}
113+
104114
// Prepare args for each module
105115
moduleArgs := make([][]string, len(cmd.Modules))
106-
for i, module := range cmd.Modules {
116+
for idx, module := range cmd.Modules {
107117
if module.Config.Interactive {
108-
moduleArgs[i] = runtimeArgs
118+
moduleArgs[idx] = runtimeArgs
109119
} else {
110-
moduleArgs[i] = module.Config.Args
120+
// Non-interactive: substitute templates in configured args
121+
substitutedArgs, err := cmd.SubstituteArgs(module.Config.Args, runtimeArgs)
122+
if err != nil {
123+
return nil, fmt.Errorf("template substitution failed for module %q: %w", module.Config.Name, err)
124+
}
125+
126+
moduleArgs[idx] = substitutedArgs
111127
}
112128
}
113129

contrib/dutagent-cfg-example.yaml

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ devices:
1616
desc: "Report status"
1717
modules:
1818
- module: dummy-status
19-
interactive: true
19+
args:
20+
- foo
21+
- bar
2022
repeat:
2123
desc: "Repeat input"
2224
modules:
@@ -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
modules:
3643
- module: dummy-status
37-
args:
38-
- foo
39-
- bar
44+
args:
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: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,17 @@
33
The _DUT Agent_ is configured by a YAML configuration file.
44

55
The configuration mainly consists of a list of devices connected to an agent and a list of the _Commands_ available
6-
for those devices. Commands are meant to be the high-level tasks you want to perform on the device, e.g.
6+
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

11-
The DUT Control project offers a collection of Module implementations but also allows for easy integration of [custom modules](./module_guide.md).
11+
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,18 +34,28 @@ could look like.
3434
| Attribute | Type | Default | Description | Mandatory |
3535
|-------------|----------------------|---------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------|
3636
| description | string | | Command description | no |
37-
| 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 arguments to the command are passed to it, and its usage information is used as the command help text. | yes |
37+
| 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 |
38+
| 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 |
39+
40+
### Command Arguments
41+
42+
Command arguments define named parameters that can be passed at runtime and distributed to modules via template syntax.
43+
44+
| Attribute | Type | Default | Description | Mandatory |
45+
|-----------|--------|---------|--------------------------------------|-----------|
46+
| name | string | | Argument name (used in templates) | yes |
47+
| desc | string | | Human-readable argument description | yes |
3848

3949
### Module
4050

4151
| Attribute | Type | Default | Description | Mandatory |
4252
|-----------|----------------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------|
4353
| module | string | | The module's name also serves as its identifier and must be unique. | yes |
44-
| 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 |
45-
| 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 |
54+
| 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 |
55+
| args | []string | nil | Arguments for non-interactive modules. Can contain static values or template references like `${arg-name}` to command-level args. | no |
4656
| 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 |
4757

48-
> [!IMPORTANT]
58+
> [!IMPORTANT]
4959
> Refer to option keys of a module in all-lowercase representation of the modules exported fields.
5060
> See the respective module's documentation for details.
5161

0 commit comments

Comments
 (0)