Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 11 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,31 +176,33 @@ For complete command reference, see [CLI Reference](cli/docs/cli-reference.md).
### Specify Shell

```bash
azd exec ./deploy.ps1 --shell pwsh
azd exec --shell pwsh ./deploy.ps1

# Inline with specific shell
azd exec 'Write-Host $env:AZURE_ENV_NAME' --shell pwsh
azd exec --shell pwsh 'Write-Host $env:AZURE_ENV_NAME'
```

### Pass Arguments

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

### Set Working Directory

```bash
azd exec ./scripts/setup.sh --cwd /path/to/project
azd exec --cwd /path/to/project ./scripts/setup.sh

# Inline with working directory
azd exec 'echo $(pwd)' --cwd /tmp
azd exec --cwd /tmp 'echo $(pwd)'
```

### Interactive Mode

```bash
azd exec ./interactive-setup.sh --interactive
azd exec --interactive ./interactive-setup.sh
```

---
Expand Down Expand Up @@ -248,8 +250,8 @@ Write-Host "Resource Group: $env:AZURE_RESOURCE_GROUP"
**PowerShell Inline**

```bash
azd exec 'Write-Host "Hello from $env:AZURE_ENV_NAME"' --shell pwsh
azd exec 'Get-ChildItem Env: | Where-Object Name -like "AZURE_*"' --shell pwsh
azd exec --shell pwsh 'Write-Host "Hello from $env:AZURE_ENV_NAME"'
azd exec --shell pwsh 'Get-ChildItem Env: | Where-Object Name -like "AZURE_*"'
```

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

```bash
azd exec ./script.sh --stop-on-keyvault-error
azd exec --stop-on-keyvault-error ./script.sh
```

### Security Benefits
Expand Down
42 changes: 22 additions & 20 deletions cli/docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,11 @@ Execute a script file or inline command with full access to azd environment vari
### Usage

```bash
azd exec [script-file-or-command] [flags] [-- script-args...]
azd exec [flags-before-script] <script-file-or-command> [script-args...]
```

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.

### Description

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

# Specify shell explicitly
azd exec ./setup.ps1 --shell pwsh
azd exec --shell pwsh ./setup.ps1

# Run in interactive mode (for scripts with prompts)
azd exec ./interactive-setup.sh --interactive
azd exec --interactive ./interactive-setup.sh

# Pass arguments to the script
azd exec ./build.sh -- --verbose --config release
azd exec ./build.sh --verbose --config release

# Inline PowerShell command
azd exec 'Write-Host "Hello from $env:AZURE_ENV_NAME"' --shell pwsh
azd exec --shell pwsh 'Write-Host "Hello from $env:AZURE_ENV_NAME"'

# Execute with debug logging
azd exec ./deploy.sh --debug
# Execute with debug logging (flags before the script)
azd exec --debug ./deploy.sh

# Use specific environment
azd exec ./deploy.sh --environment production
# Use specific environment (flags before the script)
azd exec --environment production ./deploy.sh
```

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

### Script Arguments

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

```bash
# The script receives: --verbose --config release
azd exec ./build.sh -- --verbose --config release
azd exec ./build.sh --verbose --config release

# Inline script with arguments
azd exec './process.sh "$@"' -- file1.txt file2.txt
azd exec './process.sh "$@"' file1.txt file2.txt
```

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

# PowerShell inline
azd exec 'Write-Host "Hello"; Get-Date' --shell pwsh
azd exec --shell pwsh 'Write-Host "Hello"; Get-Date'

# Complex inline with arguments
azd exec 'echo "Args: $@"' -- arg1 arg2
azd exec 'echo "Args: $@"' arg1 arg2
```

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

# Run tests with arguments
azd exec ./test.sh -- --verbose --coverage
azd exec ./test.sh --verbose --coverage
```

### Multi-Environment Deployment

```bash
# Deploy to development
azd exec ./deploy.sh --environment dev
azd exec --environment dev ./deploy.sh

# Deploy to production (with confirmation)
azd exec ./deploy.sh --environment prod --interactive
azd exec --environment prod --interactive ./deploy.sh
```

### Debugging Scripts

```bash
# Enable debug logging
azd exec ./deploy.sh --debug
azd exec --debug ./deploy.sh

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

# PowerShell debugging
azd exec 'Get-ChildItem Env: | Where-Object Name -like "AZURE_*"' --shell pwsh --debug
azd exec --shell pwsh --debug 'Get-ChildItem Env: | Where-Object Name -like "AZURE_*"'
```

### Working with Key Vault Secrets
Expand Down Expand Up @@ -449,7 +451,7 @@ azd exec ./deploy.sh

```bash
# Solution: Specify shell explicitly
azd exec ./script --shell bash
azd exec --shell bash ./script

# Or add shebang to script
echo '#!/bin/bash' | cat - script.sh > temp && mv temp script.sh
Expand Down
6 changes: 6 additions & 0 deletions cli/src/cmd/exec/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,12 @@ Examples:
},
}

// Allow passthrough flags meant for the invoked command without requiring "--".
rootCmd.FParseErrWhitelist.UnknownFlags = true
// Stop flag parsing after the first script argument so downstream flags are preserved as args.
rootCmd.Flags().SetInterspersed(false)
rootCmd.PersistentFlags().SetInterspersed(false)

// Add extension-specific flags
rootCmd.PersistentFlags().StringVarP(&outputFormat, "output", "o", "default", "Output format: default or json")

Expand Down
38 changes: 38 additions & 0 deletions cli/src/cmd/exec/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"os"
"path/filepath"
"reflect"
"testing"

"github.com/jongio/azd-exec/cli/src/internal/executor"
Expand Down Expand Up @@ -173,3 +174,40 @@ func TestRunE_DispatchesFileOrInline(t *testing.T) {
}
})
}

func TestRunE_AllowsPassthroughArgsWithoutDoubleDash(t *testing.T) {
oldNew := newScriptExecutor
defer func() { newScriptExecutor = oldNew }()

fake := &fakeExecutor{}
newScriptExecutor = func(cfg executor.Config) scriptExecutor {
fake.args = append([]string{}, cfg.Args...)
return fake
}

// Avoid changing env/cwd during Execute.
debugMode = false
noPrompt = false
cwd = ""
environment = ""
traceLogFile = ""
traceLogURL = ""
shell = ""
interactive = false

cmd := newRootCmd()
cmd.SetArgs([]string{"pnpm", "sync", "--skip-sync"})

if err := cmd.Execute(); err != nil {
t.Fatalf("Execute failed: %v", err)
}

expectedArgs := []string{"sync", "--skip-sync"}
if !reflect.DeepEqual(fake.args, expectedArgs) {
t.Fatalf("expected passthrough args %v, got %v", expectedArgs, fake.args)
}

if fake.inlineContent != "pnpm" {
t.Fatalf("expected inline execution of 'pnpm', got %q", fake.inlineContent)
}
}
34 changes: 31 additions & 3 deletions cli/src/internal/executor/command_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ var validShells = map[string]bool{
// Script arguments (e.config.Args) are appended after the script specification.
func (e *Executor) buildCommand(shell, scriptOrPath string, isInline bool) *exec.Cmd {
var cmdArgs []string
skipAppendArgs := false

// Normalize shell name to lowercase for comparison
shellLower := strings.ToLower(shell)
Expand All @@ -39,7 +40,8 @@ func (e *Executor) buildCommand(shell, scriptOrPath string, isInline bool) *exec
}
case shellPwsh, shellPowerShell:
if isInline {
cmdArgs = []string{shell, "-Command", scriptOrPath}
cmdArgs = []string{shell, "-Command", e.buildPowerShellInlineCommand(scriptOrPath)}
skipAppendArgs = true
} else {
cmdArgs = []string{shell, "-File", scriptOrPath}
}
Expand All @@ -54,10 +56,36 @@ func (e *Executor) buildCommand(shell, scriptOrPath string, isInline bool) *exec
}
}

// Append script arguments
if len(e.config.Args) > 0 {
// Append script arguments unless already embedded
if !skipAppendArgs && len(e.config.Args) > 0 {
cmdArgs = append(cmdArgs, e.config.Args...)
}

return exec.Command(cmdArgs[0], cmdArgs[1:]...) // #nosec G204 - cmdArgs are controlled by caller
}

// buildPowerShellInlineCommand joins the inline script with its arguments into a single
// -Command string to avoid PowerShell re-quoting passthrough arguments (e.g., "--flag").
// All arguments are single-quoted with internal quotes escaped to preserve literal values.
func (e *Executor) buildPowerShellInlineCommand(scriptOrPath string) string {
if len(e.config.Args) == 0 {
return scriptOrPath
}

quotedArgs := make([]string, len(e.config.Args))
for i, arg := range e.config.Args {
quotedArgs[i] = quotePowerShellArg(arg)
}

return strings.Join(append([]string{scriptOrPath}, quotedArgs...), " ")
}

// quotePowerShellArg returns a safely single-quoted PowerShell argument.
// Single quotes inside the argument are escaped by doubling them.
func quotePowerShellArg(arg string) string {
if arg == "" {
return "''"
}

return "'" + strings.ReplaceAll(arg, "'", "''") + "'"
}
21 changes: 21 additions & 0 deletions cli/src/internal/executor/command_shell_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,27 @@ func TestBuildCommand_PowerShell(t *testing.T) {
})
}

func TestBuildCommand_PowerShellInlineArgsAreEmbedded(t *testing.T) {
exec := New(Config{
Args: []string{"sync", "--", "--skip-sync"},
})

cmd := exec.buildCommand("pwsh", "pnpm", true)

expected := "pnpm 'sync' '--' '--skip-sync'"
if cmd == nil {
t.Fatal("buildCommand returned nil")
}

if len(cmd.Args) != 3 {
t.Fatalf("expected 3 args for inline PowerShell command, got %d: %v", len(cmd.Args), cmd.Args)
}

if cmd.Args[2] != expected {
t.Fatalf("unexpected inline PowerShell command: got %q, want %q", cmd.Args[2], expected)
}
}

func TestBuildCommand_Cmd(t *testing.T) {
exec := New(Config{})

Expand Down
4 changes: 2 additions & 2 deletions web/src/pages/examples.astro
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ echo "Environment: $AZURE_ENV_NAME"
# Use --stop-on-keyvault-error to fail fast

# Run the deployment
azd exec ./deploy-critical.sh --stop-on-keyvault-error
azd exec --stop-on-keyvault-error ./deploy-critical.sh

# If we get here, all Key Vault secrets were successfully resolved
echo "Deployment completed with all secrets verified!"`} language="bash" />
Expand Down Expand Up @@ -336,7 +336,7 @@ echo ""
echo "Resource created successfully!"`} language="bash" />

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

<section class="example">
Expand Down
10 changes: 5 additions & 5 deletions web/src/pages/getting-started.astro
Original file line number Diff line number Diff line change
Expand Up @@ -104,16 +104,16 @@ azd exec ./test-script.ps1`} language="powershell" />
<CodeBlock code={`azd exec ./my-script.sh`} language="bash" />

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

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

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

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

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

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