Skip to content

Commit 28fecd1

Browse files
authored
chore: update command usage and examples to reflect new argument parsing behavior (#29)
1 parent 1415948 commit 28fecd1

9 files changed

Lines changed: 152 additions & 54 deletions

File tree

README.md

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -176,31 +176,33 @@ For complete command reference, see [CLI Reference](cli/docs/cli-reference.md).
176176
### Specify Shell
177177

178178
```bash
179-
azd exec ./deploy.ps1 --shell pwsh
179+
azd exec --shell pwsh ./deploy.ps1
180180

181181
# Inline with specific shell
182-
azd exec 'Write-Host $env:AZURE_ENV_NAME' --shell pwsh
182+
azd exec --shell pwsh 'Write-Host $env:AZURE_ENV_NAME'
183183
```
184184

185185
### Pass Arguments
186186

187187
```bash
188-
azd exec ./build.sh -- --verbose --config release
188+
azd exec ./build.sh --verbose --config release
189+
# azd exec flags go before the script; script args go after it
190+
# example with cwd flag: azd exec --cwd /path/to/project ./build.sh --verbose
189191
```
190192

191193
### Set Working Directory
192194

193195
```bash
194-
azd exec ./scripts/setup.sh --cwd /path/to/project
196+
azd exec --cwd /path/to/project ./scripts/setup.sh
195197

196198
# Inline with working directory
197-
azd exec 'echo $(pwd)' --cwd /tmp
199+
azd exec --cwd /tmp 'echo $(pwd)'
198200
```
199201

200202
### Interactive Mode
201203

202204
```bash
203-
azd exec ./interactive-setup.sh --interactive
205+
azd exec --interactive ./interactive-setup.sh
204206
```
205207

206208
---
@@ -248,8 +250,8 @@ Write-Host "Resource Group: $env:AZURE_RESOURCE_GROUP"
248250
**PowerShell Inline**
249251

250252
```bash
251-
azd exec 'Write-Host "Hello from $env:AZURE_ENV_NAME"' --shell pwsh
252-
azd exec 'Get-ChildItem Env: | Where-Object Name -like "AZURE_*"' --shell pwsh
253+
azd exec --shell pwsh 'Write-Host "Hello from $env:AZURE_ENV_NAME"'
254+
azd exec --shell pwsh 'Get-ChildItem Env: | Where-Object Name -like "AZURE_*"'
253255
```
254256

255257
</td>
@@ -364,7 +366,7 @@ If Key Vault resolution fails (e.g., secret not found, no access, vault doesn't
364366
To fail-fast (abort on the first Key Vault resolution error), use:
365367

366368
```bash
367-
azd exec ./script.sh --stop-on-keyvault-error
369+
azd exec --stop-on-keyvault-error ./script.sh
368370
```
369371

370372
### Security Benefits

cli/docs/cli-reference.md

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,11 @@ Execute a script file or inline command with full access to azd environment vari
4545
### Usage
4646

4747
```bash
48-
azd exec [script-file-or-command] [flags] [-- script-args...]
48+
azd exec [flags-before-script] <script-file-or-command> [script-args...]
4949
```
5050

51+
Place azd exec flags (like `--cwd`, `--debug`, `--environment`) before the script. Everything after the script is passed through to the script; `--` remains optional if you prefer an explicit separator.
52+
5153
### Description
5254

5355
Executes scripts and commands with access to:
@@ -68,22 +70,22 @@ azd exec ./deploy.sh
6870
azd exec 'echo "Environment: $AZURE_ENV_NAME"'
6971

7072
# Specify shell explicitly
71-
azd exec ./setup.ps1 --shell pwsh
73+
azd exec --shell pwsh ./setup.ps1
7274

7375
# Run in interactive mode (for scripts with prompts)
74-
azd exec ./interactive-setup.sh --interactive
76+
azd exec --interactive ./interactive-setup.sh
7577

7678
# Pass arguments to the script
77-
azd exec ./build.sh -- --verbose --config release
79+
azd exec ./build.sh --verbose --config release
7880

7981
# Inline PowerShell command
80-
azd exec 'Write-Host "Hello from $env:AZURE_ENV_NAME"' --shell pwsh
82+
azd exec --shell pwsh 'Write-Host "Hello from $env:AZURE_ENV_NAME"'
8183

82-
# Execute with debug logging
83-
azd exec ./deploy.sh --debug
84+
# Execute with debug logging (flags before the script)
85+
azd exec --debug ./deploy.sh
8486

85-
# Use specific environment
86-
azd exec ./deploy.sh --environment production
87+
# Use specific environment (flags before the script)
88+
azd exec --environment production ./deploy.sh
8789
```
8890

8991
### Flags
@@ -108,14 +110,14 @@ azd exec ./deploy.sh --environment production
108110

109111
### Script Arguments
110112

111-
Arguments after `--` are passed directly to your script:
113+
Arguments after the script are passed directly to it—no `--` separator required. You can still use `--` if you prefer to explicitly stop flag parsing.
112114

113115
```bash
114116
# The script receives: --verbose --config release
115-
azd exec ./build.sh -- --verbose --config release
117+
azd exec ./build.sh --verbose --config release
116118

117119
# Inline script with arguments
118-
azd exec './process.sh "$@"' -- file1.txt file2.txt
120+
azd exec './process.sh "$@"' file1.txt file2.txt
119121
```
120122

121123
### File vs Inline Execution
@@ -141,10 +143,10 @@ azd exec 'echo $AZURE_ENV_NAME'
141143
azd exec 'echo "Starting"; echo $AZURE_ENV_NAME; echo "Done"'
142144

143145
# PowerShell inline
144-
azd exec 'Write-Host "Hello"; Get-Date' --shell pwsh
146+
azd exec --shell pwsh 'Write-Host "Hello"; Get-Date'
145147

146148
# Complex inline with arguments
147-
azd exec 'echo "Args: $@"' -- arg1 arg2
149+
azd exec 'echo "Args: $@"' arg1 arg2
148150
```
149151

150152
### Shell Detection
@@ -380,30 +382,30 @@ azd exec ./deploy.sh
380382
azd exec 'echo "Deploying to $AZURE_ENV_NAME in $AZURE_LOCATION"'
381383

382384
# Run tests with arguments
383-
azd exec ./test.sh -- --verbose --coverage
385+
azd exec ./test.sh --verbose --coverage
384386
```
385387

386388
### Multi-Environment Deployment
387389

388390
```bash
389391
# Deploy to development
390-
azd exec ./deploy.sh --environment dev
392+
azd exec --environment dev ./deploy.sh
391393

392394
# Deploy to production (with confirmation)
393-
azd exec ./deploy.sh --environment prod --interactive
395+
azd exec --environment prod --interactive ./deploy.sh
394396
```
395397

396398
### Debugging Scripts
397399

398400
```bash
399401
# Enable debug logging
400-
azd exec ./deploy.sh --debug
402+
azd exec --debug ./deploy.sh
401403

402404
# Check what environment variables are available
403405
azd exec 'env | grep AZURE_' --debug
404406

405407
# PowerShell debugging
406-
azd exec 'Get-ChildItem Env: | Where-Object Name -like "AZURE_*"' --shell pwsh --debug
408+
azd exec --shell pwsh --debug 'Get-ChildItem Env: | Where-Object Name -like "AZURE_*"'
407409
```
408410

409411
### Working with Key Vault Secrets
@@ -449,7 +451,7 @@ azd exec ./deploy.sh
449451

450452
```bash
451453
# Solution: Specify shell explicitly
452-
azd exec ./script --shell bash
454+
azd exec --shell bash ./script
453455

454456
# Or add shebang to script
455457
echo '#!/bin/bash' | cat - script.sh > temp && mv temp script.sh

cli/src/cmd/exec/main.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,12 @@ Examples:
142142
},
143143
}
144144

145+
// Allow passthrough flags meant for the invoked command without requiring "--".
146+
rootCmd.FParseErrWhitelist.UnknownFlags = true
147+
// Stop flag parsing after the first script argument so downstream flags are preserved as args.
148+
rootCmd.Flags().SetInterspersed(false)
149+
rootCmd.PersistentFlags().SetInterspersed(false)
150+
145151
// Add extension-specific flags
146152
rootCmd.PersistentFlags().StringVarP(&outputFormat, "output", "o", "default", "Output format: default or json")
147153

cli/src/cmd/exec/main_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"os"
66
"path/filepath"
7+
"reflect"
78
"testing"
89

910
"github.com/jongio/azd-exec/cli/src/internal/executor"
@@ -173,3 +174,40 @@ func TestRunE_DispatchesFileOrInline(t *testing.T) {
173174
}
174175
})
175176
}
177+
178+
func TestRunE_AllowsPassthroughArgsWithoutDoubleDash(t *testing.T) {
179+
oldNew := newScriptExecutor
180+
defer func() { newScriptExecutor = oldNew }()
181+
182+
fake := &fakeExecutor{}
183+
newScriptExecutor = func(cfg executor.Config) scriptExecutor {
184+
fake.args = append([]string{}, cfg.Args...)
185+
return fake
186+
}
187+
188+
// Avoid changing env/cwd during Execute.
189+
debugMode = false
190+
noPrompt = false
191+
cwd = ""
192+
environment = ""
193+
traceLogFile = ""
194+
traceLogURL = ""
195+
shell = ""
196+
interactive = false
197+
198+
cmd := newRootCmd()
199+
cmd.SetArgs([]string{"pnpm", "sync", "--skip-sync"})
200+
201+
if err := cmd.Execute(); err != nil {
202+
t.Fatalf("Execute failed: %v", err)
203+
}
204+
205+
expectedArgs := []string{"sync", "--skip-sync"}
206+
if !reflect.DeepEqual(fake.args, expectedArgs) {
207+
t.Fatalf("expected passthrough args %v, got %v", expectedArgs, fake.args)
208+
}
209+
210+
if fake.inlineContent != "pnpm" {
211+
t.Fatalf("expected inline execution of 'pnpm', got %q", fake.inlineContent)
212+
}
213+
}

cli/src/internal/executor/command_builder.go

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ var validShells = map[string]bool{
2626
// Script arguments (e.config.Args) are appended after the script specification.
2727
func (e *Executor) buildCommand(shell, scriptOrPath string, isInline bool) *exec.Cmd {
2828
var cmdArgs []string
29+
skipAppendArgs := false
2930

3031
// Normalize shell name to lowercase for comparison
3132
shellLower := strings.ToLower(shell)
@@ -39,7 +40,8 @@ func (e *Executor) buildCommand(shell, scriptOrPath string, isInline bool) *exec
3940
}
4041
case shellPwsh, shellPowerShell:
4142
if isInline {
42-
cmdArgs = []string{shell, "-Command", scriptOrPath}
43+
cmdArgs = []string{shell, "-Command", e.buildPowerShellInlineCommand(scriptOrPath)}
44+
skipAppendArgs = true
4345
} else {
4446
cmdArgs = []string{shell, "-File", scriptOrPath}
4547
}
@@ -54,10 +56,36 @@ func (e *Executor) buildCommand(shell, scriptOrPath string, isInline bool) *exec
5456
}
5557
}
5658

57-
// Append script arguments
58-
if len(e.config.Args) > 0 {
59+
// Append script arguments unless already embedded
60+
if !skipAppendArgs && len(e.config.Args) > 0 {
5961
cmdArgs = append(cmdArgs, e.config.Args...)
6062
}
6163

6264
return exec.Command(cmdArgs[0], cmdArgs[1:]...) // #nosec G204 - cmdArgs are controlled by caller
6365
}
66+
67+
// buildPowerShellInlineCommand joins the inline script with its arguments into a single
68+
// -Command string to avoid PowerShell re-quoting passthrough arguments (e.g., "--flag").
69+
// All arguments are single-quoted with internal quotes escaped to preserve literal values.
70+
func (e *Executor) buildPowerShellInlineCommand(scriptOrPath string) string {
71+
if len(e.config.Args) == 0 {
72+
return scriptOrPath
73+
}
74+
75+
quotedArgs := make([]string, len(e.config.Args))
76+
for i, arg := range e.config.Args {
77+
quotedArgs[i] = quotePowerShellArg(arg)
78+
}
79+
80+
return strings.Join(append([]string{scriptOrPath}, quotedArgs...), " ")
81+
}
82+
83+
// quotePowerShellArg returns a safely single-quoted PowerShell argument.
84+
// Single quotes inside the argument are escaped by doubling them.
85+
func quotePowerShellArg(arg string) string {
86+
if arg == "" {
87+
return "''"
88+
}
89+
90+
return "'" + strings.ReplaceAll(arg, "'", "''") + "'"
91+
}

cli/src/internal/executor/command_shell_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,27 @@ func TestBuildCommand_PowerShell(t *testing.T) {
7575
})
7676
}
7777

78+
func TestBuildCommand_PowerShellInlineArgsAreEmbedded(t *testing.T) {
79+
exec := New(Config{
80+
Args: []string{"sync", "--", "--skip-sync"},
81+
})
82+
83+
cmd := exec.buildCommand("pwsh", "pnpm", true)
84+
85+
expected := "pnpm 'sync' '--' '--skip-sync'"
86+
if cmd == nil {
87+
t.Fatal("buildCommand returned nil")
88+
}
89+
90+
if len(cmd.Args) != 3 {
91+
t.Fatalf("expected 3 args for inline PowerShell command, got %d: %v", len(cmd.Args), cmd.Args)
92+
}
93+
94+
if cmd.Args[2] != expected {
95+
t.Fatalf("unexpected inline PowerShell command: got %q, want %q", cmd.Args[2], expected)
96+
}
97+
}
98+
7899
func TestBuildCommand_Cmd(t *testing.T) {
79100
exec := New(Config{})
80101

web/src/pages/examples.astro

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ echo "Environment: $AZURE_ENV_NAME"
263263
# Use --stop-on-keyvault-error to fail fast
264264
265265
# Run the deployment
266-
azd exec ./deploy-critical.sh --stop-on-keyvault-error
266+
azd exec --stop-on-keyvault-error ./deploy-critical.sh
267267
268268
# If we get here, all Key Vault secrets were successfully resolved
269269
echo "Deployment completed with all secrets verified!"`} language="bash" />
@@ -336,7 +336,7 @@ echo ""
336336
echo "Resource created successfully!"`} language="bash" />
337337

338338
<h3>Usage</h3>
339-
<CodeBlock code={`azd exec ./setup-wizard.sh --interactive`} language="bash" />
339+
<CodeBlock code={`azd exec --interactive ./setup-wizard.sh`} language="bash" />
340340
</section>
341341

342342
<section class="example">

web/src/pages/getting-started.astro

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -104,16 +104,16 @@ azd exec ./test-script.ps1`} language="powershell" />
104104
<CodeBlock code={`azd exec ./my-script.sh`} language="bash" />
105105

106106
<h3>Pass Arguments</h3>
107-
<CodeBlock code={`azd exec ./build.sh -- --verbose --config release`} language="bash" />
107+
<CodeBlock code={`azd exec ./build.sh --verbose --config release`} language="bash" />
108108

109109
<h3>Specify Shell</h3>
110-
<CodeBlock code={`azd exec ./deploy.ps1 --shell pwsh`} language="bash" />
110+
<CodeBlock code={`azd exec --shell pwsh ./deploy.ps1`} language="bash" />
111111

112112
<h3>Set Working Directory</h3>
113-
<CodeBlock code={`azd exec ./setup.sh --cwd /path/to/project`} language="bash" />
113+
<CodeBlock code={`azd exec --cwd /path/to/project ./setup.sh`} language="bash" />
114114

115115
<h3>Interactive Mode</h3>
116-
<CodeBlock code={`azd exec ./interactive-setup.sh --interactive`} language="bash" />
116+
<CodeBlock code={`azd exec --interactive ./interactive-setup.sh`} language="bash" />
117117
</section>
118118

119119
<section class="section">
@@ -169,7 +169,7 @@ mysql -u admin -p"$DATABASE_PASSWORD" -h myserver.mysql.database.azure.com`} lan
169169
By default, if Key Vault resolution fails (e.g., secret not found, no access), azd exec displays a warning but continues.
170170
To fail-fast on Key Vault errors, use the <code>--stop-on-keyvault-error</code> flag:
171171
</p>
172-
<CodeBlock code={`azd exec ./script.sh --stop-on-keyvault-error`} language="bash" />
172+
<CodeBlock code={`azd exec --stop-on-keyvault-error ./script.sh`} language="bash" />
173173

174174
<p>For more examples, see the <a href={`${base}examples#keyvault`}>Key Vault examples section</a>.</p>
175175
</section>

0 commit comments

Comments
 (0)