From 8d8c2d6f16810027f0a4f49c29c9a5a80cc55e74 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 26 Sep 2026 15:12:51 +0000 Subject: [PATCH 01/12] ui: derive var/literal mode from typed values; fix pre-fill priority and stale refresh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Editing a field (and restoring a draft) kept the previous mode, so a typed $RG rendered as '$RG' and a var field edited to "my group" was emitted unquoted. The mode is now derived from the value. - Fields holding only their metadata default were skipped by the draft, bindings, env and Azure-defaults stages, inverting spec §8.5. - Stale cache entries (and the embedded baseline) were never refreshed: Result.Refresh had no caller. It now runs in the background. - undefined-var and escape-error no longer block Done for disabled fields. - Ctrl+G insert spliced by byte offset using a rune cursor. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Dza8X5c3XGHUjejpqGLR8q --- README.md | 16 +-- internal/ui/handlers.go | 2 +- internal/ui/model.go | 53 +++++++++- internal/ui/prefill.go | 79 ++++++++++++-- internal/ui/refresh_test.go | 73 +++++++++++++ internal/ui/typed_value_test.go | 169 ++++++++++++++++++++++++++++++ internal/ui/v_toggle_test.go | 26 +++-- internal/validate/builtin.go | 5 +- internal/validate/builtin_test.go | 7 ++ 9 files changed, 392 insertions(+), 38 deletions(-) create mode 100644 internal/ui/refresh_test.go create mode 100644 internal/ui/typed_value_test.go diff --git a/README.md b/README.md index a7a5d73..860d7f6 100644 --- a/README.md +++ b/README.md @@ -65,10 +65,10 @@ Create a public IP address. ────────────────────────────────────────────────────────────────────────────────────────────────────────── az network public-ip create \ --name $PIP \ - --resource-group '$RG' \ + --resource-group $RG \ --allocation-method Static \ --idle-timeout 15 \ - --location '$LOC' \ + --location $LOC \ --sku StandardV2 \ --tier Regional \ --version IPv4 \ @@ -130,10 +130,10 @@ Create a public IP address. ────────────────────────────────────────────────────────────────────────────────────────────────────────── az network public-ip create \ --name $PIP \ - --resource-group '$RG' \ + --resource-group $RG \ --allocation-method Static \ --idle-timeout 15 \ - --location '$LOC' \ + --location $LOC \ --sku StandardV2 \ --tier Regional \ --version IPv4 \ @@ -170,10 +170,10 @@ Create a public IP address. ────────────────────────────────────────────────────────────────────────────────────────────────────────── az network public-ip create \ --name $PIP \ - --resource-group '$RG' \ + --resource-group $RG \ --allocation-method Static \ --idle-timeout 15 \ - --location '$LOC' \ + --location $LOC \ --sku StandardV2 \ --tier Regional \ --version IPv4 \ @@ -221,10 +221,10 @@ Create a public IP address. ────────────────────────────────────────────────────────────────────────────────────────────────────────── az network public-ip create \ --name $PIP \ - --resource-group '$RG' \ + --resource-group $RG \ --allocation-method Static \ --idle-timeout 15 \ - --location '$LOC' \ + --location $LOC \ --sku StandardV2 \ --tier Regional \ --version IPv4 \ diff --git a/internal/ui/handlers.go b/internal/ui/handlers.go index 9a52bfb..3fbc1e1 100644 --- a/internal/ui/handlers.go +++ b/internal/ui/handlers.go @@ -45,7 +45,7 @@ func (m Form) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { m.mode = FormModeVarPick return m, nil case "enter": - m.fields[m.editIdx].Value = m.textInput.Value() + m.setTypedValue(&m.fields[m.editIdx], m.textInput.Value()) if m.textInput.Value() != "" { m.fields[m.editIdx].Enabled = true } diff --git a/internal/ui/model.go b/internal/ui/model.go index 3c0d6a9..30e7ff4 100644 --- a/internal/ui/model.go +++ b/internal/ui/model.go @@ -58,10 +58,25 @@ type MetadataLoadedMsg struct { Stale bool StaleReason string Health metadata.ParseHealth + + // refresh, when non-nil, re-parses `az --help` and rewrites the cache + // entry. The cache hands it out with every stale result (including the + // embedded baseline) but never runs it itself; without calling it a + // stale entry would stay stale — and keep its banner — forever. + refresh func(context.Context) error } type metadataErrorMsg struct{ err error } +// metadataRefreshedMsg reports the end of a background cache refresh. The +// open form keeps the metadata it was built from; the fresh record is +// picked up on the next invocation. +type metadataRefreshedMsg struct{ err error } + +// metadataRefreshTimeout bounds the background refresh. It must outlive +// metadata.HelpTimeout so the runner's own timeout fires first. +const metadataRefreshTimeout = 30 * time.Second + // Styles var ( headerStyle = lipgloss.NewStyle().Bold(true) @@ -324,10 +339,23 @@ func (m Form) fetchMetadata() tea.Cmd { Stale: result.Stale, StaleReason: result.StaleReason, Health: result.Command.ParseHealth, + refresh: result.Refresh, } } } +// refreshMetadata runs a stale entry's refresh hook in the background so +// the on-disk cache catches up with the installed az (spec §3.4). Errors +// are not surfaced: the form is already usable and the stale banner has +// told the user what they need to know. +func refreshMetadata(refresh func(context.Context) error) tea.Cmd { + return func() tea.Msg { + ctx, cancel := context.WithTimeout(context.Background(), metadataRefreshTimeout) + defer cancel() + return metadataRefreshedMsg{err: refresh(ctx)} + } +} + // Update satisfies tea.Model. Dispatches Bubble Tea messages to the // appropriate handler based on m.mode and the message type. func (m Form) Update(msg tea.Msg) (tea.Model, tea.Cmd) { @@ -429,6 +457,12 @@ func (m Form) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.updateAvailable = msg.Latest return m, nil + case metadataRefreshedMsg: + if msg.err != nil && m.src.Debug != nil { + m.src.Debug.Event("cache.refresh", map[string]any{"command": m.command, "error": msg.err.Error()}) + } + return m, nil + case metadataErrorMsg: m.loadState = LoadStateError m.loadErr = msg.err.Error() @@ -437,6 +471,8 @@ func (m Form) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case EnumSelectedMsg: if m.mode == FormModeEnum { m.fields[m.enumIdx].Value = msg.Value + m.fields[m.enumIdx].VarValue = "" + m.fields[m.enumIdx].Mode = FieldModeLiteral m.fields[m.enumIdx].Enabled = true m.recomputeFindings(nil) m.mode = FormModeList @@ -453,9 +489,15 @@ func (m Form) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // Insert `$NAME` at the saved textinput cursor position. The // textinput stays in edit mode so the user can keep typing. if m.mode == FormModeVarPick { - insert := "$" + msg.Name - m.textInput.SetValue(m.textInput.Value()[:m.varEditCursor] + insert + m.textInput.Value()[m.varEditCursor:]) - m.textInput.SetCursor(m.varEditCursor + len(insert)) + // textinput positions count runes, not bytes: splice on a + // rune slice so non-ASCII text before the cursor neither + // panics nor lands the insert mid-character. + insert := []rune("$" + msg.Name) + value := []rune(m.textInput.Value()) + at := min(max(m.varEditCursor, 0), len(value)) + spliced := append(append(append([]rune(nil), value[:at]...), insert...), value[at:]...) + m.textInput.SetValue(string(spliced)) + m.textInput.SetCursor(at + len(insert)) m.mode = FormModeEdit } return m, nil @@ -626,7 +668,10 @@ func (m *Form) buildFormState(params []metadata.Parameter) *validate.FormState { values[f.Param.Name] = f.Value modes[f.Param.Name] = f.Mode enabled[f.Param.Name] = f.Enabled - if f.Mode == validate.FieldModeVar && len(f.Value) > 1 && f.Value[0] == '$' && f.Value[1] != '(' { + // Only enabled fields reach the command, so only their var refs + // can be undefined at exec time. A disabled field holding e.g. a + // remembered $OLD_RG must not block Done. + if f.Enabled && f.Mode == validate.FieldModeVar && len(f.Value) > 1 && f.Value[0] == '$' && f.Value[1] != '(' { // Walk every whitespace-separated token in the value so multi-var // lists (`$a1 $a2`) report both names to the undefinedVar rule. for _, tok := range strings.Fields(f.Value) { diff --git a/internal/ui/prefill.go b/internal/ui/prefill.go index 538e6de..a1c137a 100644 --- a/internal/ui/prefill.go +++ b/internal/ui/prefill.go @@ -47,7 +47,8 @@ func (m Form) handleMetadataLoaded(msg MetadataLoadedMsg) (tea.Model, tea.Cmd) { m.reqIndices = reqIdx // Spec §8.5 priority order (higher first; each stage only fills fields - // still at FieldSourceNone, so higher-priority sources win): + // that are still fillable — unset or holding just the metadata + // default — so higher-priority sources win): // 1. Parsed buffer (spec 6.8). // 2. Preset (M6, not implemented). // 3. Draft (spec 6.7) — user's last-in-progress state, applied before @@ -69,13 +70,17 @@ func (m Form) handleMetadataLoaded(msg MetadataLoadedMsg) (tea.Model, tea.Cmd) { m.rebuildVisible() m.updateLayout() m.logFieldSources() + var cmds []tea.Cmd + if msg.Stale && msg.refresh != nil { + cmds = append(cmds, refreshMetadata(msg.refresh)) + } // Kick off a lazy fetch for the initially focused field (spec §6.1). if idx := m.fieldAt(m.cursor); idx >= 0 { if cmd := m.maybeFetchField(idx); cmd != nil { - return m, cmd + cmds = append(cmds, cmd) } } - return m, nil + return m, tea.Batch(cmds...) } // logFieldSources emits one debug event per field that has a non-zero @@ -174,7 +179,7 @@ func (m *Form) applyEnvPreFill(params []metadata.Parameter) bool { filled := false for _, mt := range matches { for i := range m.fields { - if m.fields[i].Param.Name == mt.ParamName && m.fields[i].Source == FieldSourceNone { + if m.fields[i].Param.Name == mt.ParamName && fillable(m.fields[i]) { // Closed choice sets (enum/bool) never take var mode: emit the // resolved value as a literal if allowed, else skip the match. if !valueAllowedForParam(m.fields[i].Param, mt.Value) { @@ -213,7 +218,7 @@ func (m *Form) applyAzurePreFill() bool { continue } for i := range m.fields { - if m.fields[i].Source != FieldSourceNone { + if !fillable(m.fields[i]) { continue } if m.fields[i].Param.Name == target { @@ -269,7 +274,7 @@ func normaliseAzureKey(name string) string { } // applyDraftRestore restores the saved draft for this command (priority 3). -// Only fields still at FieldSourceNone are restored. Fields the user +// Only fillable fields (unset or metadata default) are restored. Fields the user // explicitly toggled off before cancelling come back Enabled=false so // a binding-applied value the user removed stays removed across // reopen cycles. @@ -280,8 +285,8 @@ func (m *Form) applyDraftRestore() { } for k, v := range saved { for i := range m.fields { - if m.fields[i].Param.Name == k && m.fields[i].Source == FieldSourceNone { - m.fields[i].Value = v + if m.fields[i].Param.Name == k && fillable(m.fields[i]) { + m.setTypedValue(&m.fields[i], v) m.fields[i].Source = FieldSourceDraft if v != "" { m.fields[i].Enabled = true @@ -314,7 +319,7 @@ func (m *Form) applyRememberedPreFill(params []metadata.Parameter) bool { filled := false for i := range m.fields { f := &m.fields[i] - if f.Param.Name == "" || f.Source != FieldSourceNone { + if f.Param.Name == "" || !fillable(*f) { continue } key := state.BindingKey(m.command, f.Param.Name) @@ -359,6 +364,16 @@ func (m *Form) applyRememberedPreFill(params []metadata.Parameter) bool { return filled } +// fillable reports whether a lower-priority pre-fill stage (draft, +// remembered binding, env heuristic, Azure defaults) may overwrite f. The +// metadata default is the lowest priority of all (spec §8.5, stage 7) but +// is applied first, so a field holding only its default must stay +// fillable; otherwise a draft edit or an AZURE_DEFAULTS_* value for any +// param with a documented default would be silently ignored. +func fillable(f Field) bool { + return f.Source == FieldSourceNone || f.Source == FieldSourceDefault +} + // valueAllowedForParam reports whether value may populate p without breaking // a closed choice set. Params with a known closed set (enum, or bool with // bool-synonym choices) accept only listed values; anything else — including @@ -380,6 +395,52 @@ func valueAllowedForParam(p metadata.Parameter, value string) bool { return false } +// setTypedValue stores a value that carries no mode information of its own +// — text the user typed into the edit input, or a draft restored from disk +// (drafts persist only the raw string) — and derives Mode from its shape. +// +// Mode decides quoting in render.Build: var mode is emitted verbatim, +// literal mode is single-quoted. Keeping the mode a field happened to have +// before the edit breaks both ways: a typed `$RG` in a literal field +// becomes `'$RG'` (az receives the four characters), and a var field +// edited to `my group` is emitted unquoted and word-split by the shell. +// +// A value is var mode when it is a single `$NAME` / `${NAME}` reference, a +// whole `$(…)` command substitution, or — for list-kind params only — a +// run of references (`$a1 $a2`), mirroring what shell.MatchParams accepts +// from the buffer. Closed choice sets never take var mode. +func (m *Form) setTypedValue(f *Field, value string) { + f.Value = value + f.VarValue = "" + f.Mode = FieldModeLiteral + if f.Param.HasSelectChoices() || f.Param.IsSwitch() { + return + } + trimmed := strings.TrimSpace(value) + if strings.HasPrefix(trimmed, "$(") && strings.HasSuffix(trimmed, ")") { + f.Mode = FieldModeVar + return + } + tokens := strings.Fields(trimmed) + if len(tokens) == 0 { + return + } + isList := f.Param.ValueKind == metadata.ValueKindList || f.Param.ValueKind == metadata.ValueKindKeyValue + if len(tokens) > 1 && !isList { + return + } + pp := shell.ParsedParam{} + for _, tok := range tokens { + isVar, name := shell.DetectVarRef(tok) + if !isVar { + return + } + pp.VarNames = append(pp.VarNames, name) + } + f.Mode = FieldModeVar + f.VarValue = resolveBufferVars(pp, m.src.Vars) +} + // resolveBufferVars returns the joined resolved values for a var-mode buffer // param. For single-var refs (pp.VarName set) this returns the matching var's // value. For multi-var lists (pp.VarNames) it returns each var's value joined diff --git a/internal/ui/refresh_test.go b/internal/ui/refresh_test.go new file mode 100644 index 0000000..62eb10e --- /dev/null +++ b/internal/ui/refresh_test.go @@ -0,0 +1,73 @@ +package ui + +import ( + "context" + "testing" + + tea "github.com/charmbracelet/bubbletea" + + "github.com/someson/azform/internal/metadata" +) + +// runCmd executes cmd and, for a batch, every command inside it, returning +// all produced messages. +func runCmd(cmd tea.Cmd) []tea.Msg { + if cmd == nil { + return nil + } + msg := cmd() + if batch, ok := msg.(tea.BatchMsg); ok { + var out []tea.Msg + for _, c := range batch { + out = append(out, runCmd(c)...) + } + return out + } + return []tea.Msg{msg} +} + +// The cache returns stale entries (and the embedded baseline) with a +// Refresh hook but never runs it. Nothing in the form ran it either, so a +// stale entry stayed stale — banner included — forever. +func TestStaleMetadataTriggersBackgroundRefresh(t *testing.T) { + calls := 0 + f := NewForm("group show", "/tmp/out.txt", t.TempDir(), "test", nil) + m, cmd := f.Update(MetadataLoadedMsg{ + Params: []metadata.Parameter{{Name: "--name", TakesValue: true, ValueKind: metadata.ValueKindString}}, + Stale: true, + StaleReason: "az was upgraded since caching", + refresh: func(context.Context) error { + calls++ + return nil + }, + }) + msgs := runCmd(cmd) + if calls != 1 { + t.Fatalf("refresh called %d times, want 1", calls) + } + var got bool + for _, msg := range msgs { + if _, ok := msg.(metadataRefreshedMsg); ok { + got = true + if _, next := m.Update(msg); next != nil { + t.Errorf("metadataRefreshedMsg should not schedule more work") + } + } + } + if !got { + t.Errorf("no metadataRefreshedMsg among %v", msgs) + } +} + +func TestFreshMetadataDoesNotRefresh(t *testing.T) { + calls := 0 + f := NewForm("group show", "/tmp/out.txt", t.TempDir(), "test", nil) + _, cmd := f.Update(MetadataLoadedMsg{ + Params: []metadata.Parameter{{Name: "--name", TakesValue: true, ValueKind: metadata.ValueKindString}}, + refresh: func(context.Context) error { calls++; return nil }, + }) + runCmd(cmd) + if calls != 0 { + t.Errorf("refresh called %d times for a fresh entry, want 0", calls) + } +} diff --git a/internal/ui/typed_value_test.go b/internal/ui/typed_value_test.go new file mode 100644 index 0000000..e74081f --- /dev/null +++ b/internal/ui/typed_value_test.go @@ -0,0 +1,169 @@ +package ui_test + +import ( + "testing" + + tea "github.com/charmbracelet/bubbletea" + + "github.com/someson/azform/internal/metadata" + "github.com/someson/azform/internal/state" + "github.com/someson/azform/internal/ui" + "github.com/someson/azform/internal/validate" + "github.com/someson/azform/internal/vars" +) + +func typedValueParams() []metadata.Parameter { + return []metadata.Parameter{ + {Name: "--name", Required: true, TakesValue: true, ValueKind: metadata.ValueKindString, Group: "Required Parameters"}, + {Name: "--resource-group", Required: true, TakesValue: true, ValueKind: metadata.ValueKindString, Group: "Required Parameters"}, + } +} + +func sendKey(t *testing.T, f ui.Form, k tea.KeyMsg) ui.Form { + t.Helper() + m, _ := f.Update(k) + return m.(ui.Form) +} + +// editField opens the text input on the focused field, replaces its whole +// content with value and commits with Enter. +func editField(t *testing.T, f ui.Form, value string) ui.Form { + t.Helper() + f = sendKey(t, f, tea.KeyMsg{Type: tea.KeyEnter}) + for len(f.TextInputValue()) > 0 { + f = sendKey(t, f, tea.KeyMsg{Type: tea.KeyEnd}) + f = sendKey(t, f, tea.KeyMsg{Type: tea.KeyBackspace}) + } + f = typeInto(t, f, value) + return sendKey(t, f, tea.KeyMsg{Type: tea.KeyEnter}) +} + +func submit(t *testing.T, f ui.Form) ui.Form { + t.Helper() + f = sendKey(t, f, tea.KeyMsg{Type: tea.KeyTab}) + return sendKey(t, f, tea.KeyMsg{Type: tea.KeyEnter}) +} + +// Editing a field used to keep whatever mode it had before, so a typed +// `$RG` in a literal field rendered as `'$RG'` (az got the four +// characters) and a var field edited to `my group` rendered unquoted and +// was word-split by the shell. The mode is now derived from the typed text. +func TestEditDerivesModeFromTypedValue(t *testing.T) { + src := ui.Sources{ + Engine: validate.NewEngine(validate.BuiltinProvider{}), + Vars: []vars.Variable{ + {Name: "RG", Value: "rg-from-shell"}, + {Name: "RESOURCE_GROUP", Value: "rg1"}, + }, + } + f := ui.NewFormWithSources("group show", "/tmp/out.txt", t.TempDir(), "test", nil, src) + m, _ := f.Update(ui.MetadataLoadedMsg{Params: typedValueParams(), Summary: "."}) + f = m.(ui.Form) + + rg := f.Fields()[f.FieldIndex("--resource-group")] + if rg.Mode != ui.FieldModeVar { + t.Fatalf("precondition: --resource-group should be env-filled var mode, got %s", ui.ModeName(rg.Mode)) + } + + // --name is literal and focused first: type a var ref into it. + f = editField(t, f, "$RG") + name := f.Fields()[f.FieldIndex("--name")] + if name.Mode != ui.FieldModeVar || name.VarValue != "rg-from-shell" { + t.Errorf("typed $RG: mode=%s varValue=%q, want var/rg-from-shell", ui.ModeName(name.Mode), name.VarValue) + } + + // --resource-group is var mode: overwrite it with a literal with a space. + f = sendKey(t, f, tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("j")}) + f = editField(t, f, "my group") + rg = f.Fields()[f.FieldIndex("--resource-group")] + if rg.Mode != ui.FieldModeLiteral || rg.VarValue != "" { + t.Errorf("typed literal: mode=%s varValue=%q, want literal/\"\"", ui.ModeName(rg.Mode), rg.VarValue) + } + + f = submit(t, f) + want := "az group show --name $RG --resource-group 'my group'" + if f.Result() != want { + t.Errorf("Result = %q, want %q (errorMsg=%q)", f.Result(), want, f.ErrorMsg()) + } +} + +// A whole-value command substitution is shell syntax, not literal text. +func TestEditCommandSubstitutionIsVarMode(t *testing.T) { + f := ui.NewFormWithSources("group show", "/tmp/out.txt", t.TempDir(), "test", nil, ui.Sources{}) + m, _ := f.Update(ui.MetadataLoadedMsg{Params: typedValueParams(), Summary: "."}) + f = m.(ui.Form) + f = editField(t, f, "$(cat name.txt)") + name := f.Fields()[f.FieldIndex("--name")] + if name.Mode != ui.FieldModeVar { + t.Errorf("$(…) should be var mode, got %s", ui.ModeName(name.Mode)) + } +} + +// The metadata default is the lowest pre-fill priority (spec §8.5), but it +// is applied first; fields holding only their default used to be skipped +// by every later stage, so Azure defaults, env matches and drafts never +// reached any param with a documented default. +func TestDefaultIsOverriddenByLowerStages(t *testing.T) { + def := "eastus" + params := []metadata.Parameter{ + {Name: "--name", Required: true, TakesValue: true, ValueKind: metadata.ValueKindString, Group: "Required Parameters"}, + {Name: "--location", TakesValue: true, ValueKind: metadata.ValueKindString, Default: &def, Group: "Optional Parameters"}, + {Name: "--tags", TakesValue: true, ValueKind: metadata.ValueKindString, Default: &def, Group: "Optional Parameters"}, + } + + dir := t.TempDir() + if err := state.NewDraftStore(dir).Save("thing create", map[string]string{"--tags": "env=dev"}); err != nil { + t.Fatalf("seed draft: %v", err) + } + src := ui.Sources{AzureDefaults: []vars.Variable{{Name: "location", Value: "westeurope"}}} + f := ui.NewFormWithSources("thing create", "/tmp/out.txt", dir, "test", nil, src) + m, _ := f.Update(ui.MetadataLoadedMsg{Params: params, Summary: "."}) + f = m.(ui.Form) + + loc := f.Fields()[f.FieldIndex("--location")] + if loc.Value != "westeurope" || loc.Source != ui.FieldSourceAzure { + t.Errorf("--location = %q (source %s), want westeurope from azure defaults", loc.Value, loc.Source.Name()) + } + tags := f.Fields()[f.FieldIndex("--tags")] + if tags.Value != "env=dev" || tags.Source != ui.FieldSourceDraft { + t.Errorf("--tags = %q (source %s), want env=dev from draft", tags.Value, tags.Source.Name()) + } +} + +// Only enabled fields reach the command; a disabled field referencing an +// unset variable must not block Done. +func TestDisabledUndefinedVarDoesNotBlockDone(t *testing.T) { + params := append(typedValueParams(), metadata.Parameter{ + Name: "--tags", TakesValue: true, ValueKind: metadata.ValueKindString, Group: "Optional Parameters", + }) + dir := t.TempDir() + if err := state.NewDraftStore(dir).SaveWithDisabled("group show", + map[string]string{"--name": "n", "--resource-group": "rg", "--tags": "$GONE"}, + map[string]bool{"--tags": true}); err != nil { + t.Fatalf("seed draft: %v", err) + } + src := ui.Sources{Engine: validate.NewEngine(validate.BuiltinProvider{})} + f := ui.NewFormWithSources("group show", "/tmp/out.txt", dir, "test", nil, src) + m, _ := f.Update(ui.MetadataLoadedMsg{Params: params, Summary: "."}) + f = submit(t, m.(ui.Form)) + if want := "az group show --name n --resource-group rg"; f.Result() != want { + t.Errorf("Result = %q, want %q (errorMsg=%q)", f.Result(), want, f.ErrorMsg()) + } +} + +// Ctrl+G inserts at the textinput cursor, which counts runes; splicing by +// byte offset panicked or split a character after non-ASCII text. +func TestVarPickerInsertAfterNonASCII(t *testing.T) { + src := ui.Sources{Vars: []vars.Variable{{Name: "SUFFIX_X", Value: "x"}}} + f := ui.NewFormWithSources("group show", "/tmp/out.txt", t.TempDir(), "test", nil, src) + m, _ := f.Update(ui.MetadataLoadedMsg{Params: typedValueParams(), Summary: "."}) + f = m.(ui.Form) + f = sendKey(t, f, tea.KeyMsg{Type: tea.KeyEnter}) + f = typeInto(t, f, "é-") + f = sendKey(t, f, tea.KeyMsg{Type: tea.KeyCtrlG}) + m, _ = f.Update(ui.VarPickedMsg{Name: "RG"}) + f = m.(ui.Form) + if got := f.TextInputValue(); got != "é-$RG" { + t.Errorf("input = %q, want %q", got, "é-$RG") + } +} diff --git a/internal/ui/v_toggle_test.go b/internal/ui/v_toggle_test.go index 0ca123e..9141c2e 100644 --- a/internal/ui/v_toggle_test.go +++ b/internal/ui/v_toggle_test.go @@ -145,15 +145,14 @@ func TestVCycleIgnoresUnresolvingVar(t *testing.T) { _ = before } -// Regression for the 2026-09-06 "v doesn't work" bug: a draft-restored -// required field has Value="$RG" as literal text (Mode=Literal, -// VarValue="" — drafts don't preserve var info). The cycle used to -// only fire when Mode==FieldModeVar, so pressing v on such a field -// was a no-op. The rendering now detects "$REF" text independently of -// Mode and honours the cycle for any required field whose value is a -// resolving var ref. Lookup is view-only (m.src.Vars), no field state -// mutation. -func TestVCycleDraftRestoredLiteral(t *testing.T) { +// Regression for the 2026-09-06 "v doesn't work" bug: drafts persist +// only the raw string, so a draft-restored "$RG" used to come back as +// literal text (Mode=Literal, VarValue="") — which both disabled the v +// cycle and rendered the command as `--resource-group '$RG'`, handing az +// the four characters instead of the variable. Draft restore now derives +// the mode from the value, so the field is a resolving var reference and +// the cycle is purely view-side. +func TestVCycleDraftRestoredVar(t *testing.T) { dir := t.TempDir() store := state.NewDraftStore(dir) if err := store.Save("storage account create", map[string]string{ @@ -169,8 +168,8 @@ func TestVCycleDraftRestoredLiteral(t *testing.T) { f = m.(ui.Form) rg := f.Fields()[f.FieldIndex("--resource-group")] - if rg.Mode != ui.FieldModeLiteral || rg.VarValue != "" || rg.Source != ui.FieldSourceDraft { - t.Fatalf("precondition: expected literal draft $RG, got mode=%s varValue=%q source=%s", + if rg.Mode != ui.FieldModeVar || rg.VarValue != "myResourceGroup" || rg.Source != ui.FieldSourceDraft { + t.Fatalf("precondition: expected var-mode draft $RG, got mode=%s varValue=%q source=%s", ui.ModeName(rg.Mode), rg.VarValue, rg.Source.Name()) } @@ -205,10 +204,9 @@ func TestVCycleDraftRestoredLiteral(t *testing.T) { t.Errorf("after 2nd v: want $RG; got: %q", row) } - // Field state must NOT have changed — still draft literal with empty - // VarValue. The cycle is view-only. + // Field state must NOT have changed. The cycle is view-only. rg = f.Fields()[f.FieldIndex("--resource-group")] - if rg.Mode != ui.FieldModeLiteral || rg.VarValue != "" || rg.Value != "$RG" { + if rg.Mode != ui.FieldModeVar || rg.VarValue != "myResourceGroup" || rg.Value != "$RG" { t.Errorf("cycle mutated field state: mode=%s value=%q varValue=%q", ui.ModeName(rg.Mode), rg.Value, rg.VarValue) } diff --git a/internal/validate/builtin.go b/internal/validate/builtin.go index d6fabdf..cdbaf02 100644 --- a/internal/validate/builtin.go +++ b/internal/validate/builtin.go @@ -111,14 +111,15 @@ func usesVarName(value, name string) bool { return false } -// escapeError: literal-mode value with unclosed quote or backtick. +// escapeError: enabled literal-mode value with unclosed quote or backtick. +// Disabled fields are never rendered, so they cannot break the command. type escapeError struct{} func (escapeError) ID() string { return "builtin/escape-error" } func (escapeError) Check(cmd *metadata.Command, st *FormState) []Finding { var out []Finding for name, m := range st.Modes { - if m != FieldModeLiteral { + if m != FieldModeLiteral || !st.Enabled[name] { continue } val := st.Values[name] diff --git a/internal/validate/builtin_test.go b/internal/validate/builtin_test.go index 7456054..f07a671 100644 --- a/internal/validate/builtin_test.go +++ b/internal/validate/builtin_test.go @@ -182,6 +182,13 @@ func TestEscapeError(t *testing.T) { if hasRuleID(fs, "builtin/escape-error") { t.Errorf("bare space should not fire") } + // A disabled field is never rendered, so it must not block Done. + st.Values["--name"] = `unclosed "quote` + st.Enabled["--name"] = false + fs = runRules(t, st) + if hasRuleID(fs, "builtin/escape-error") { + t.Errorf("disabled field should not fire") + } } func TestEnumOutOfRange(t *testing.T) { From 77c887516ccb2cca538fcb47137ccc4d52984da2 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 26 Sep 2026 15:12:51 +0000 Subject: [PATCH 02/12] metadata: rejoin hyphen-wrapped help lines; drop context-dependent switch names az wraps help at hyphens, so 'comma-separated' became 'comma- separated' and 'Values from: az account list-locations' became an unrunnable 'list- locations'. aks create --enable-addons was misread as a switch. --identity, --assign-identity and --service-principal take values on most commands (acr/aks/appgw/postgres/...); login's switch forms are already recognised from their help text. Goldens and baseline regenerated. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Dza8X5c3XGHUjejpqGLR8q --- .../baseline/account_list_locations.json | 2 +- .../baseline/baseline/account_show.json | 2 +- .../baseline/baseline/acr_create.json | 10 ++-- .../metadata/baseline/baseline/acr_login.json | 4 +- .../baseline/baseline/ad_app_create.json | 4 +- .../baseline/ad_sp_create_for_rbac.json | 4 +- internal/metadata/baseline/baseline/aks.json | 2 +- .../baseline/baseline/aks_create.json | 50 ++++++++--------- .../metadata/baseline/baseline/aks_list.json | 2 +- .../metadata/baseline/baseline/aks_scale.json | 2 +- .../metadata/baseline/baseline/aks_show.json | 2 +- .../baseline/baseline/aks_upgrade.json | 4 +- .../baseline/appservice_plan_create.json | 10 ++-- .../metadata/baseline/baseline/configure.json | 2 +- .../baseline/baseline/container_create.json | 4 +- .../baseline/baseline/cosmosdb_create.json | 14 ++--- .../baseline/deployment_group_create.json | 4 +- .../baseline/eventhubs_namespace_create.json | 6 +-- .../baseline/baseline/functionapp_create.json | 18 +++---- .../baseline/baseline/group_create.json | 2 +- .../baseline/baseline/group_delete.json | 2 +- .../baseline/baseline/group_list.json | 2 +- .../baseline/baseline/group_show.json | 2 +- .../baseline/baseline/group_update.json | 2 +- .../metadata/baseline/baseline/keyvault.json | 2 +- .../baseline/baseline/keyvault_create.json | 4 +- .../baseline/baseline/keyvault_list.json | 2 +- .../baseline/keyvault_secret_set.json | 4 +- .../baseline/baseline/keyvault_show.json | 2 +- .../metadata/baseline/baseline/login.json | 2 +- ...onitor_log_analytics_workspace_create.json | 4 +- .../monitor_metrics_alert_create.json | 2 +- .../metadata/baseline/baseline/network.json | 2 +- .../network_application_gateway_create.json | 14 ++--- .../baseline/network_bastion_create.json | 10 ++-- .../baseline/network_bastion_ssh.json | 2 +- .../baseline/network_bastion_tunnel.json | 2 +- .../baseline/baseline/network_lb_create.json | 2 +- .../baseline/baseline/network_nsg_create.json | 4 +- .../baseline/network_public_ip_create.json | 2 +- .../baseline/network_vnet_create.json | 20 +++---- .../baseline/baseline/network_vnet_list.json | 2 +- .../baseline/network_vnet_subnet_create.json | 24 ++++----- .../postgres_flexible_server_create.json | 8 +-- .../baseline/baseline/redis_create.json | 4 +- internal/metadata/baseline/baseline/rest.json | 6 +-- .../baseline/role_assignment_create.json | 4 +- .../baseline/servicebus_queue_create.json | 2 +- .../baseline/baseline/sql_db_create.json | 2 +- .../baseline/baseline/sql_server_create.json | 4 +- .../baseline/baseline/storage_account.json | 2 +- .../baseline/storage_account_check_name.json | 2 +- .../baseline/storage_account_create.json | 24 ++++----- .../baseline/storage_account_delete.json | 2 +- .../baseline/storage_account_keys_list.json | 2 +- .../baseline/storage_account_list.json | 2 +- .../baseline/storage_account_show.json | 2 +- .../baseline/storage_account_update.json | 22 ++++---- .../baseline/storage_blob_upload.json | 2 +- .../baseline/storage_container_create.json | 6 +-- internal/metadata/baseline/baseline/vm.json | 2 +- .../metadata/baseline/baseline/vm_create.json | 54 +++++++++---------- .../metadata/baseline/baseline/vm_delete.json | 4 +- .../metadata/baseline/baseline/vm_list.json | 2 +- .../metadata/baseline/baseline/vm_show.json | 2 +- .../metadata/baseline/baseline/vm_start.json | 4 +- .../metadata/baseline/baseline/vm_stop.json | 2 +- .../baseline/baseline/webapp_create.json | 12 ++--- internal/metadata/parse.go | 31 +++++++++-- testdata/golden/acr-create.json | 4 +- testdata/golden/acr-login.json | 2 +- testdata/golden/ad-app-create.json | 2 +- testdata/golden/ad-sp-create-for-rbac.json | 2 +- testdata/golden/aks-create.json | 44 +++++++-------- testdata/golden/aks-upgrade.json | 2 +- testdata/golden/appservice-plan-create.json | 4 +- testdata/golden/container-create.json | 2 +- testdata/golden/cosmosdb-create.json | 6 +-- .../golden/eventhubs-namespace-create.json | 2 +- testdata/golden/functionapp-create.json | 12 ++--- testdata/golden/keyvault-secret-set.json | 2 +- .../network-application-gateway-create.json | 12 ++--- testdata/golden/network-bastion-create.json | 6 +-- testdata/golden/network-vnet-create.json | 12 ++--- .../golden/network-vnet-subnet-create.json | 16 +++--- .../postgres-flexible-server-create.json | 6 +-- testdata/golden/redis-create.json | 2 +- testdata/golden/rest.json | 4 +- testdata/golden/role-assignment-create.json | 2 +- testdata/golden/sql-db-create.json | 2 +- testdata/golden/sql-server-create.json | 2 +- testdata/golden/storage-account-create.json | 12 ++--- testdata/golden/storage-account-update.json | 10 ++-- testdata/golden/storage-container-create.json | 2 +- testdata/golden/vm-create.json | 44 +++++++-------- testdata/golden/webapp-create.json | 6 +-- 96 files changed, 359 insertions(+), 334 deletions(-) diff --git a/internal/metadata/baseline/baseline/account_list_locations.json b/internal/metadata/baseline/baseline/account_list_locations.json index b73610c..9d74d8c 100644 --- a/internal/metadata/baseline/baseline/account_list_locations.json +++ b/internal/metadata/baseline/baseline/account_list_locations.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 9, "unparsed_lines": 0, diff --git a/internal/metadata/baseline/baseline/account_show.json b/internal/metadata/baseline/baseline/account_show.json index 38e5970..6e4d8ad 100644 --- a/internal/metadata/baseline/baseline/account_show.json +++ b/internal/metadata/baseline/baseline/account_show.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 7, "unparsed_lines": 0, diff --git a/internal/metadata/baseline/baseline/acr_create.json b/internal/metadata/baseline/baseline/acr_create.json index b53d7fa..cfc5d83 100644 --- a/internal/metadata/baseline/baseline/acr_create.json +++ b/internal/metadata/baseline/baseline/acr_create.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 30, "unparsed_lines": 0, @@ -97,7 +97,7 @@ "required": false, "group": "Optional Parameters", "global": false, - "takes_value": true, + "takes_value": false, "value_kind": "bool", "help": "Enable or disable the metadata-search feature for the registry. If not specified, this is set to disabled by default. WARNING: Argument '--allow-metadata-search' is in preview and under development. Reference and support levels: https://aka.ms/CLI_refstatus", "choices": [ @@ -205,8 +205,8 @@ "required": false, "group": "Customer managed key Arguments", "global": false, - "takes_value": false, - "value_kind": "bool", + "takes_value": true, + "value_kind": "string", "help": "Use assigned managed identity resource id or name if in the same resource group.", "choices": null, "default": null, @@ -273,7 +273,7 @@ "required": false, "group": "Network Rule Arguments", "global": false, - "takes_value": true, + "takes_value": false, "value_kind": "bool", "help": "Enable dedicated data endpoint for client firewall configuration.", "choices": [ diff --git a/internal/metadata/baseline/baseline/acr_login.json b/internal/metadata/baseline/baseline/acr_login.json index 5674d91..2d80318 100644 --- a/internal/metadata/baseline/baseline/acr_login.json +++ b/internal/metadata/baseline/baseline/acr_login.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 16, "unparsed_lines": 0, @@ -93,7 +93,7 @@ "global": false, "takes_value": true, "value_kind": "string", - "help": "The tenant suffix in registry login server. You may specify '--suffix tenant' if your registry login server is in the format 'registry- tenant.azurecr.io'. Applicable if you're accessing the registry from a different subscription or you have permission to access images but not the permission to manage the registry resource.", + "help": "The tenant suffix in registry login server. You may specify '--suffix tenant' if your registry login server is in the format 'registry-tenant.azurecr.io'. Applicable if you're accessing the registry from a different subscription or you have permission to access images but not the permission to manage the registry resource.", "choices": null, "default": null, "values_from": null diff --git a/internal/metadata/baseline/baseline/ad_app_create.json b/internal/metadata/baseline/baseline/ad_app_create.json index 7749557..e32dc48 100644 --- a/internal/metadata/baseline/baseline/ad_app_create.json +++ b/internal/metadata/baseline/baseline/ad_app_create.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 28, "unparsed_lines": 0, @@ -132,7 +132,7 @@ "global": false, "takes_value": true, "value_kind": "path", - "help": "Application developers can configure optional claims in their Microsoft Entra applications to specify the claims that are sent to their application by the Microsoft security token service. For more information, see https://learn.microsoft.com/azure/active- directory/develop/active-directory-optional-claims. Should be JSON file path or in-line JSON string. See examples for details.", + "help": "Application developers can configure optional claims in their Microsoft Entra applications to specify the claims that are sent to their application by the Microsoft security token service. For more information, see https://learn.microsoft.com/azure/active-directory/develop/active-directory-optional-claims. Should be JSON file path or in-line JSON string. See examples for details.", "choices": null, "default": null, "values_from": null diff --git a/internal/metadata/baseline/baseline/ad_sp_create_for_rbac.json b/internal/metadata/baseline/baseline/ad_sp_create_for_rbac.json index 6a2bb8c..48982cf 100644 --- a/internal/metadata/baseline/baseline/ad_sp_create_for_rbac.json +++ b/internal/metadata/baseline/baseline/ad_sp_create_for_rbac.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 18, "unparsed_lines": 0, @@ -67,7 +67,7 @@ "global": false, "takes_value": true, "value_kind": "list", - "help": "Space-separated list of scopes the service principal's role assignment applies to. e.g., subscriptions/0b1f6471-1bf0-4dda- aec3-111122223333/resourceGroups/myGroup, /subscriptions/0b1f6471-1bf0-4dda-aec3- 111122223333/resourceGroups/myGroup/providers/Microsoft.Co mpute/virtualMachines/myVM.", + "help": "Space-separated list of scopes the service principal's role assignment applies to. e.g., subscriptions/0b1f6471-1bf0-4dda-aec3-111122223333/resourceGroups/myGroup, /subscriptions/0b1f6471-1bf0-4dda-aec3- 111122223333/resourceGroups/myGroup/providers/Microsoft.Co mpute/virtualMachines/myVM.", "choices": null, "default": null, "values_from": null diff --git a/internal/metadata/baseline/baseline/aks.json b/internal/metadata/baseline/baseline/aks.json index f1832bc..16181e1 100644 --- a/internal/metadata/baseline/baseline/aks.json +++ b/internal/metadata/baseline/baseline/aks.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 35, "unparsed_lines": 0, diff --git a/internal/metadata/baseline/baseline/aks_create.json b/internal/metadata/baseline/baseline/aks_create.json index 43c7aa4..33ec27a 100644 --- a/internal/metadata/baseline/baseline/aks_create.json +++ b/internal/metadata/baseline/baseline/aks_create.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 193, "unparsed_lines": 0, @@ -122,7 +122,7 @@ "global": false, "takes_value": true, "value_kind": "enum", - "help": "Set transit encryption type for ACNS security. Configures pod-to-pod encryption for Cilium-based clusters. Once enabled, all traffic between Cilium managed pods will be encrypted when it leaves the node boundary. Valid values are \"WireGuard\" and \"None\". On cluster creation, this must be used together with \"--enable- acns\".", + "help": "Set transit encryption type for ACNS security. Configures pod-to-pod encryption for Cilium-based clusters. Once enabled, all traffic between Cilium managed pods will be encrypted when it leaves the node boundary. Valid values are \"WireGuard\" and \"None\". On cluster creation, this must be used together with \"--enable-acns\".", "choices": [ "None", "WireGuard" @@ -192,7 +192,7 @@ "global": false, "takes_value": true, "value_kind": "string", - "help": "The ID of a subnet in an existing VNet into which to assign control plane apiserver pods(requires --enable-apiserver-vnet- integration).", + "help": "The ID of a subnet in an existing VNet into which to assign control plane apiserver pods(requires --enable-apiserver-vnet-integration).", "choices": null, "default": null, "values_from": null @@ -223,8 +223,8 @@ "required": false, "group": "Optional Parameters", "global": false, - "takes_value": false, - "value_kind": "bool", + "takes_value": true, + "value_kind": "string", "help": "Specify an existing user assigned identity for control plane's usage in order to manage cluster resource group.", "choices": null, "default": null, @@ -597,7 +597,7 @@ "global": false, "takes_value": true, "value_kind": "string", - "help": "An IP address assigned to the Kubernetes DNS service. This address must be within the Kubernetes service address range specified by \"--service- cidr\". For example, 10.0.0.10.", + "help": "An IP address assigned to the Kubernetes DNS service. This address must be within the Kubernetes service address range specified by \"--service-cidr\". For example, 10.0.0.10.", "choices": null, "default": null, "values_from": null @@ -649,9 +649,9 @@ "required": false, "group": "Optional Parameters", "global": false, - "takes_value": false, - "value_kind": "bool", - "help": "Enable the Kubernetes addons in a comma- separated list. These addons are available: - http_application_routing: configure ingress with automatic public DNS name creation. - monitoring: turn on Log Analytics monitoring. Uses the Log Analytics Default Workspace if it exists, else creates one. Specify \"--workspace-resource-id\" to use an existing workspace. Specify \"--enable-msi-auth-for-monitoring\" to use Managed Identity Auth. Specify \"--enable-syslog\" to enable syslog data collection from nodes. Note MSI must be enabled Specify \"--data-collection-settings\" to configure data collection settings Specify \"--ampls-resource-id\" for private link. Note MSI must be enabled. Specify \"--enable-high-log-scale-mode\" to enable high log scale mode for container logs. Note MSI must be enabled. If monitoring addon is enabled --no-wait argument will have no effect - azure-policy: enable Azure policy. The Azure Policy add-on for AKS enables at-scale enforcements and safeguards on your clusters in a centralized, consistent manner. Learn more at aka.ms/aks/policy. - virtual-node: enable AKS Virtual Node. Requires --aci-subnet-name to provide the name of an existing subnet for the Virtual Node to use. aci-subnet-name must be in the same vnet which is specified by --vnet-subnet-id (required as well). - confcom: enable confcom addon, this will enable SGX device plugin by default. - open-service-mesh: enable Open Service Mesh addon. - azure-keyvault-secrets-provider: enable Azure Keyvault Secrets Provider addon.", + "takes_value": true, + "value_kind": "list", + "help": "Enable the Kubernetes addons in a comma-separated list. These addons are available: - http_application_routing: configure ingress with automatic public DNS name creation. - monitoring: turn on Log Analytics monitoring. Uses the Log Analytics Default Workspace if it exists, else creates one. Specify \"--workspace-resource-id\" to use an existing workspace. Specify \"--enable-msi-auth-for-monitoring\" to use Managed Identity Auth. Specify \"--enable-syslog\" to enable syslog data collection from nodes. Note MSI must be enabled Specify \"--data-collection-settings\" to configure data collection settings Specify \"--ampls-resource-id\" for private link. Note MSI must be enabled. Specify \"--enable-high-log-scale-mode\" to enable high log scale mode for container logs. Note MSI must be enabled. If monitoring addon is enabled --no-wait argument will have no effect - azure-policy: enable Azure policy. The Azure Policy add-on for AKS enables at-scale enforcements and safeguards on your clusters in a centralized, consistent manner. Learn more at aka.ms/aks/policy. - virtual-node: enable AKS Virtual Node. Requires --aci-subnet-name to provide the name of an existing subnet for the Virtual Node to use. aci-subnet-name must be in the same vnet which is specified by --vnet-subnet-id (required as well). - confcom: enable confcom addon, this will enable SGX device plugin by default. - open-service-mesh: enable Open Service Mesh addon. - azure-keyvault-secrets-provider: enable Azure Keyvault Secrets Provider addon.", "choices": null, "default": null, "values_from": null @@ -718,7 +718,7 @@ "global": false, "takes_value": false, "value_kind": "bool", - "help": "Enable Gateway API based ingress on App Routing via Istio without service mesh functionality. This enables an ingress-only version of Istio that reconciles Gateway API resources for App Routing. It does not provide service mesh functionality (e.g. mTLS, traffic management between services). Cannot be used simultaneously with the Istio service mesh add-on (--enable-azure-service- mesh).", + "help": "Enable Gateway API based ingress on App Routing via Istio without service mesh functionality. This enables an ingress-only version of Istio that reconciles Gateway API resources for App Routing. It does not provide service mesh functionality (e.g. mTLS, traffic management between services). Cannot be used simultaneously with the Istio service mesh add-on (--enable-azure-service-mesh).", "choices": null, "default": null, "values_from": null @@ -852,7 +852,7 @@ "global": false, "takes_value": false, "value_kind": "bool", - "help": "Enable collection of Azure Monitor managed Prometheus control plane metrics for managed cluster components (controlplane- apiserver and controlplane-etcd targets by default). Requires Azure Monitor metrics to be enabled (already enabled or via --enable-azure-monitor-metrics).", + "help": "Enable collection of Azure Monitor managed Prometheus control plane metrics for managed cluster components (controlplane-apiserver and controlplane-etcd targets by default). Requires Azure Monitor metrics to be enabled (already enabled or via --enable-azure-monitor-metrics).", "choices": null, "default": null, "values_from": null @@ -865,7 +865,7 @@ "global": false, "takes_value": false, "value_kind": "bool", - "help": "Enable exporting Kubernetes Namespace and Deployment details to the Cost Analysis views in the Azure portal. For more information see aka.ms/aks/docs/cost- analysis.", + "help": "Enable exporting Kubernetes Namespace and Deployment details to the Cost Analysis views in the Azure portal. For more information see aka.ms/aks/docs/cost-analysis.", "choices": null, "default": null, "values_from": null @@ -928,9 +928,9 @@ "required": false, "group": "Optional Parameters", "global": false, - "takes_value": true, + "takes_value": false, "value_kind": "bool", - "help": "Enable High Log Scale Mode for Container Logs. Auto-enabled when --enable-container- network-logs is specified.", + "help": "Enable High Log Scale Mode for Container Logs. Auto-enabled when --enable-container-network-logs is specified.", "choices": [ "false", "true" @@ -1053,7 +1053,7 @@ "global": false, "takes_value": false, "value_kind": "bool", - "help": "Enable secret rotation. Use with azure- keyvault-secrets-provider addon.", + "help": "Enable secret rotation. Use with azure-keyvault-secrets-provider addon.", "choices": null, "default": null, "values_from": null @@ -1103,7 +1103,7 @@ "required": false, "group": "Optional Parameters", "global": false, - "takes_value": true, + "takes_value": false, "value_kind": "bool", "help": "Enable syslog data collection for Monitoring addon.", "choices": [ @@ -1410,7 +1410,7 @@ "global": false, "takes_value": true, "value_kind": "list", - "help": "Comma-separated list of additional Kubernetes label keys that will be used in the resource' labels metric. By default the metric contains only name and namespace labels. To include additional labels provide a list of resource names in their plural form and Kubernetes label keys you would like to allow for them (e.g.'=namespa ces=[k8s-label-1,k8s-label- n,...],pods=[app],...)'. A single '*' can be provided per resource instead to allow any labels, but that has severe performance implications (e.g. '=pods=[*]').", + "help": "Comma-separated list of additional Kubernetes label keys that will be used in the resource' labels metric. By default the metric contains only name and namespace labels. To include additional labels provide a list of resource names in their plural form and Kubernetes label keys you would like to allow for them (e.g.'=namespa ces=[k8s-label-1,k8s-label-n,...],pods=[app],...)'. A single '*' can be provided per resource instead to allow any labels, but that has severe performance implications (e.g. '=pods=[*]').", "choices": null, "default": null, "values_from": null @@ -1423,7 +1423,7 @@ "global": false, "takes_value": true, "value_kind": "list", - "help": "Comma-separated list of additional Kubernetes label keys that will be used in the resource' labels metric. By default the metric contains only name and namespace labels. To include additional labels provide a list of resource names in their plural form and Kubernetes label keys you would like to allow for them (e.g. '=namesp aces=[k8s-label-1,k8s-label- n,...],pods=[app],...)'. A single '*' can be provided per resource instead to allow any labels, but that has severe performance implications (e.g. '=pods=[*]').", + "help": "Comma-separated list of additional Kubernetes label keys that will be used in the resource' labels metric. By default the metric contains only name and namespace labels. To include additional labels provide a list of resource names in their plural form and Kubernetes label keys you would like to allow for them (e.g. '=namesp aces=[k8s-label-1,k8s-label-n,...],pods=[app],...)'. A single '*' can be provided per resource instead to allow any labels, but that has severe performance implications (e.g. '=pods=[*]').", "choices": null, "default": null, "values_from": null @@ -1592,7 +1592,7 @@ "help": "Location. You can configure the default location using `az configure --defaults location=\u003clocation\u003e`.", "choices": null, "default": null, - "values_from": "az account list- locations" + "values_from": "az account list-locations" }, { "name": "--max-count", @@ -1698,7 +1698,7 @@ "global": false, "takes_value": true, "value_kind": "enum", - "help": "The Kubernetes network plugin to use. Specify \"azure\" for highly scalable networking, \"kubenet\" for IP assignment from subnet NAT- based routing, or \"none\" for no networking configured. Defaults to \"azure\".", + "help": "The Kubernetes network plugin to use. Specify \"azure\" for highly scalable networking, \"kubenet\" for IP assignment from subnet NAT-based routing, or \"none\" for no networking configured. Defaults to \"azure\".", "choices": [ "azure", "kubenet", @@ -1947,7 +1947,7 @@ "global": false, "takes_value": true, "value_kind": "list", - "help": "Expose host ports on the node pool. When specified, format should be a space- separated list of ranges with protocol, eg. `80/TCP 443/TCP 4000-5000/TCP`.", + "help": "Expose host ports on the node pool. When specified, format should be a space-separated list of ranges with protocol, eg. `80/TCP 443/TCP 4000-5000/TCP`.", "choices": null, "default": null, "values_from": null @@ -2213,8 +2213,8 @@ "required": false, "group": "Optional Parameters", "global": false, - "takes_value": false, - "value_kind": "bool", + "takes_value": true, + "value_kind": "string", "help": "Service principal used for authentication to Azure APIs.", "choices": null, "default": null, @@ -2402,7 +2402,7 @@ "global": false, "takes_value": true, "value_kind": "list", - "help": "Comma-separated list of VM sizes. Valid for VirtualMachines node pool only. If `--vm- sizes` not specified but `--node-vm-size` specified, value of `--node-vm-size` will be used. If neither of them specified, defaults to Standard_DS2_v2 for Linux or Standard_D2s_v3 for Windows.", + "help": "Comma-separated list of VM sizes. Valid for VirtualMachines node pool only. If `--vm-sizes` not specified but `--node-vm-size` specified, value of `--node-vm-size` will be used. If neither of them specified, defaults to Standard_DS2_v2 for Linux or Standard_D2s_v3 for Windows.", "choices": null, "default": null, "values_from": null @@ -2512,7 +2512,7 @@ "global": false, "takes_value": true, "value_kind": "string", - "help": "Resource Id of an existing Application Gateway to use with AGIC. Use with ingress- azure addon.", + "help": "Resource Id of an existing Application Gateway to use with AGIC. Use with ingress-azure addon.", "choices": null, "default": null, "values_from": null diff --git a/internal/metadata/baseline/baseline/aks_list.json b/internal/metadata/baseline/baseline/aks_list.json index bded0f9..ea5a954 100644 --- a/internal/metadata/baseline/baseline/aks_list.json +++ b/internal/metadata/baseline/baseline/aks_list.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 8, "unparsed_lines": 0, diff --git a/internal/metadata/baseline/baseline/aks_scale.json b/internal/metadata/baseline/baseline/aks_scale.json index cc2fd47..bab304a 100644 --- a/internal/metadata/baseline/baseline/aks_scale.json +++ b/internal/metadata/baseline/baseline/aks_scale.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 14, "unparsed_lines": 0, diff --git a/internal/metadata/baseline/baseline/aks_show.json b/internal/metadata/baseline/baseline/aks_show.json index 64f98fb..0267024 100644 --- a/internal/metadata/baseline/baseline/aks_show.json +++ b/internal/metadata/baseline/baseline/aks_show.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 9, "unparsed_lines": 0, diff --git a/internal/metadata/baseline/baseline/aks_upgrade.json b/internal/metadata/baseline/baseline/aks_upgrade.json index a99084e..53a0e0b 100644 --- a/internal/metadata/baseline/baseline/aks_upgrade.json +++ b/internal/metadata/baseline/baseline/aks_upgrade.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 23, "unparsed_lines": 0, @@ -189,7 +189,7 @@ "global": false, "takes_value": true, "value_kind": "string", - "help": "Until when the cluster upgradeSettings overrides are effective. It needs to be in a valid date-time format that's within the next 30 days. For example, 2023-04-01T13:00:00Z. Note that if --force-upgrade is set to true and --upgrade-override- until is not set, by default it will be set to 3 days from now.", + "help": "Until when the cluster upgradeSettings overrides are effective. It needs to be in a valid date-time format that's within the next 30 days. For example, 2023-04-01T13:00:00Z. Note that if --force-upgrade is set to true and --upgrade-override-until is not set, by default it will be set to 3 days from now.", "choices": null, "default": null, "values_from": null diff --git a/internal/metadata/baseline/baseline/appservice_plan_create.json b/internal/metadata/baseline/baseline/appservice_plan_create.json index 27889ad..e6d9938 100644 --- a/internal/metadata/baseline/baseline/appservice_plan_create.json +++ b/internal/metadata/baseline/baseline/appservice_plan_create.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 33, "unparsed_lines": 0, @@ -52,7 +52,7 @@ "global": false, "takes_value": true, "value_kind": "string", - "help": "Name or ID of the app service environment. If you want to create the app service plan in different subscription than the app service environment, please use the resource ID for --app- service-environment parameter.", + "help": "Name or ID of the app service environment. If you want to create the app service plan in different subscription than the app service environment, please use the resource ID for --app-service-environment parameter.", "choices": null, "default": null, "values_from": null @@ -178,7 +178,7 @@ "required": false, "group": "Optional Parameters", "global": false, - "takes_value": true, + "takes_value": false, "value_kind": "bool", "help": "Enable system-assigned managed identity for this app service plan.", "choices": [ @@ -246,7 +246,7 @@ "required": false, "group": "Optional Parameters", "global": false, - "takes_value": true, + "takes_value": false, "value_kind": "bool", "help": "Enable RDP. Requires is-custom-mode to be true.", "choices": [ @@ -336,7 +336,7 @@ "global": false, "takes_value": true, "value_kind": "string", - "help": "Storage mount configurations. Provide key-value pairs for `name=\u003cname\u003e source=\u003csource\u003e type=\u003ctype\u003e destination- path=\u003cpath\u003e credentials-secret-uri=\u003curi\u003e`.", + "help": "Storage mount configurations. Provide key-value pairs for `name=\u003cname\u003e source=\u003csource\u003e type=\u003ctype\u003e destination-path=\u003cpath\u003e credentials-secret-uri=\u003curi\u003e`.", "choices": null, "default": null, "values_from": null diff --git a/internal/metadata/baseline/baseline/configure.json b/internal/metadata/baseline/baseline/configure.json index abb5ac7..620a1e1 100644 --- a/internal/metadata/baseline/baseline/configure.json +++ b/internal/metadata/baseline/baseline/configure.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 11, "unparsed_lines": 0, diff --git a/internal/metadata/baseline/baseline/container_create.json b/internal/metadata/baseline/baseline/container_create.json index d9ebef8..2f72f1c 100644 --- a/internal/metadata/baseline/baseline/container_create.json +++ b/internal/metadata/baseline/baseline/container_create.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 65, "unparsed_lines": 0, @@ -151,7 +151,7 @@ "help": "Location. You can configure the default location using `az configure --defaults location=\u003clocation\u003e`.", "choices": null, "default": null, - "values_from": "az account list- locations" + "values_from": "az account list-locations" }, { "name": "--memory", diff --git a/internal/metadata/baseline/baseline/cosmosdb_create.json b/internal/metadata/baseline/baseline/cosmosdb_create.json index 4fb1d7c..a7a5393 100644 --- a/internal/metadata/baseline/baseline/cosmosdb_create.json +++ b/internal/metadata/baseline/baseline/cosmosdb_create.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 52, "unparsed_lines": 0, @@ -48,8 +48,8 @@ "required": false, "group": "Optional Parameters", "global": false, - "takes_value": false, - "value_kind": "bool", + "takes_value": true, + "value_kind": "list", "help": "Assign system or user assigned identities separated by spaces. Use '[system]' to refer system assigned identity.", "choices": null, "default": null, @@ -95,7 +95,7 @@ "global": false, "takes_value": true, "value_kind": "string", - "help": "The primary identity to access key vault in CMK related features. e.g. 'FirstPartyIdentity', 'SystemAssignedIdentity' and more. User-assigned identities are specified in format `UserAssignedIdentity=\u003cresource ID of the user- assigned identity\u003e`.", + "help": "The primary identity to access key vault in CMK related features. e.g. 'FirstPartyIdentity', 'SystemAssignedIdentity' and more. User-assigned identities are specified in format `UserAssignedIdentity=\u003cresource ID of the user-assigned identity\u003e`.", "choices": null, "default": null, "values_from": null @@ -122,7 +122,7 @@ "required": false, "group": "Optional Parameters", "global": false, - "takes_value": true, + "takes_value": false, "value_kind": "bool", "help": "Disable write operations on metadata resources (databases, containers, throughput) via account keys.", "choices": [ @@ -138,7 +138,7 @@ "required": false, "group": "Optional Parameters", "global": false, - "takes_value": true, + "takes_value": false, "value_kind": "bool", "help": "Disable key-based authentication on the Cosmos DB account.", "choices": [ @@ -218,7 +218,7 @@ "required": false, "group": "Optional Parameters", "global": false, - "takes_value": true, + "takes_value": false, "value_kind": "bool", "help": "Enable Multiple Write Locations.", "choices": [ diff --git a/internal/metadata/baseline/baseline/deployment_group_create.json b/internal/metadata/baseline/baseline/deployment_group_create.json index 8f79159..8da1e69 100644 --- a/internal/metadata/baseline/baseline/deployment_group_create.json +++ b/internal/metadata/baseline/baseline/deployment_group_create.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 29, "unparsed_lines": 0, @@ -120,7 +120,7 @@ "required": false, "group": "Optional Parameters", "global": false, - "takes_value": true, + "takes_value": false, "value_kind": "bool", "help": "The option to disable the prompt of missing parameters for ARM template. When the value is true, the prompt requiring users to provide missing parameter will be ignored. The default value is false.", "choices": [ diff --git a/internal/metadata/baseline/baseline/eventhubs_namespace_create.json b/internal/metadata/baseline/baseline/eventhubs_namespace_create.json index feee2c8..c63b0a3 100644 --- a/internal/metadata/baseline/baseline/eventhubs_namespace_create.json +++ b/internal/metadata/baseline/baseline/eventhubs_namespace_create.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 30, "unparsed_lines": 0, @@ -186,7 +186,7 @@ "help": "Location. You can configure the default location using `az configure --defaults location=\u003clocation\u003e`.", "choices": null, "default": null, - "values_from": "az account list- locations" + "values_from": "az account list-locations" }, { "name": "--max-lag", @@ -331,7 +331,7 @@ "required": false, "group": "Managed Identity Arguments", "global": false, - "takes_value": true, + "takes_value": false, "value_kind": "bool", "help": "Enable System Assigned Identity.", "choices": [ diff --git a/internal/metadata/baseline/baseline/functionapp_create.json b/internal/metadata/baseline/baseline/functionapp_create.json index 35fc435..17a6610 100644 --- a/internal/metadata/baseline/baseline/functionapp_create.json +++ b/internal/metadata/baseline/baseline/functionapp_create.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 63, "unparsed_lines": 0, @@ -102,8 +102,8 @@ "required": false, "group": "Optional Parameters", "global": false, - "takes_value": false, - "value_kind": "bool", + "takes_value": true, + "value_kind": "list", "help": "Accept system or user assigned identities separated by spaces. Use '[system]' to refer system assigned identity, or a resource id to refer user assigned identity. Check out help for more examples.", "choices": null, "default": null, @@ -119,7 +119,7 @@ "global": false, "takes_value": true, "value_kind": "bool", - "help": "Use this option if you want to configure networking later for an app using network- restricted storage.", + "help": "Use this option if you want to configure networking later for an app using network-restricted storage.", "choices": [ "false", "true" @@ -137,7 +137,7 @@ "global": false, "takes_value": true, "value_kind": "string", - "help": "Geographic location where function app will be hosted. Use `az functionapp list-consumption- locations` to view available locations.", + "help": "Geographic location where function app will be hosted. Use `az functionapp list-consumption-locations` to view available locations.", "choices": null, "default": null, "values_from": null @@ -334,7 +334,7 @@ "global": false, "takes_value": true, "value_kind": "string", - "help": "The deployment storage account authentication value. For the user-assigned managed identity authentication type, this should be the user assigned identity resource id. For the storage account connection string authentication type, this should be the name of the app setting that will contain the storage account connection string. For the system assigned managed- identity authentication type, this parameter is not applicable and should be left empty.", + "help": "The deployment storage account authentication value. For the user-assigned managed identity authentication type, this should be the user assigned identity resource id. For the storage account connection string authentication type, this should be the name of the app setting that will contain the storage account connection string. For the system assigned managed-identity authentication type, this parameter is not applicable and should be left empty.", "choices": null, "default": null, "values_from": null @@ -375,7 +375,7 @@ "required": false, "group": "Optional Parameters", "global": false, - "takes_value": true, + "takes_value": false, "value_kind": "bool", "help": "Disable creating application insights resource during functionapp create. No logs will be available.", "choices": [ @@ -468,7 +468,7 @@ "global": false, "takes_value": true, "value_kind": "string", - "help": "Geographic location where function app will be hosted. Use `az functionapp list- flexconsumption-locations` to view available locations.", + "help": "Geographic location where function app will be hosted. Use `az functionapp list-flexconsumption-locations` to view available locations.", "choices": null, "default": null, "values_from": null @@ -781,7 +781,7 @@ "required": false, "group": "Optional Parameters", "global": false, - "takes_value": true, + "takes_value": false, "value_kind": "bool", "help": "Enable zone redundancy for high availability. Applies to Flex Consumption SKU only.", "choices": [ diff --git a/internal/metadata/baseline/baseline/group_create.json b/internal/metadata/baseline/baseline/group_create.json index ae176bd..89c7884 100644 --- a/internal/metadata/baseline/baseline/group_create.json +++ b/internal/metadata/baseline/baseline/group_create.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 13, "unparsed_lines": 0, diff --git a/internal/metadata/baseline/baseline/group_delete.json b/internal/metadata/baseline/baseline/group_delete.json index a6295fb..efb3268 100644 --- a/internal/metadata/baseline/baseline/group_delete.json +++ b/internal/metadata/baseline/baseline/group_delete.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 13, "unparsed_lines": 0, diff --git a/internal/metadata/baseline/baseline/group_list.json b/internal/metadata/baseline/baseline/group_list.json index 1824645..3d6f490 100644 --- a/internal/metadata/baseline/baseline/group_list.json +++ b/internal/metadata/baseline/baseline/group_list.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 8, "unparsed_lines": 0, diff --git a/internal/metadata/baseline/baseline/group_show.json b/internal/metadata/baseline/baseline/group_show.json index f729f04..fb17f01 100644 --- a/internal/metadata/baseline/baseline/group_show.json +++ b/internal/metadata/baseline/baseline/group_show.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 8, "unparsed_lines": 0, diff --git a/internal/metadata/baseline/baseline/group_update.json b/internal/metadata/baseline/baseline/group_update.json index 5c0cbd4..cf1d5b1 100644 --- a/internal/metadata/baseline/baseline/group_update.json +++ b/internal/metadata/baseline/baseline/group_update.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 13, "unparsed_lines": 0, diff --git a/internal/metadata/baseline/baseline/keyvault.json b/internal/metadata/baseline/baseline/keyvault.json index 0f17f15..8974e4d 100644 --- a/internal/metadata/baseline/baseline/keyvault.json +++ b/internal/metadata/baseline/baseline/keyvault.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 28, "unparsed_lines": 0, diff --git a/internal/metadata/baseline/baseline/keyvault_create.json b/internal/metadata/baseline/baseline/keyvault_create.json index 1b51444..23d96c9 100644 --- a/internal/metadata/baseline/baseline/keyvault_create.json +++ b/internal/metadata/baseline/baseline/keyvault_create.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 31, "unparsed_lines": 0, @@ -182,7 +182,7 @@ "required": false, "group": "Optional Parameters", "global": false, - "takes_value": true, + "takes_value": false, "value_kind": "bool", "help": "[Vault Only] Don't add permissions for the current user/service principal in the new vault.", "choices": [ diff --git a/internal/metadata/baseline/baseline/keyvault_list.json b/internal/metadata/baseline/baseline/keyvault_list.json index 6fa164a..7c7309a 100644 --- a/internal/metadata/baseline/baseline/keyvault_list.json +++ b/internal/metadata/baseline/baseline/keyvault_list.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 9, "unparsed_lines": 0, diff --git a/internal/metadata/baseline/baseline/keyvault_secret_set.json b/internal/metadata/baseline/baseline/keyvault_secret_set.json index 9acb7f5..d2e8311 100644 --- a/internal/metadata/baseline/baseline/keyvault_secret_set.json +++ b/internal/metadata/baseline/baseline/keyvault_secret_set.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 19, "unparsed_lines": 0, @@ -92,7 +92,7 @@ "global": false, "takes_value": true, "value_kind": "enum", - "help": "Source file encoding. The value is saved as a tag (`file- encoding=\u003cval\u003e`) and used during download to automatically encode the resulting file.", + "help": "Source file encoding. The value is saved as a tag (`file-encoding=\u003cval\u003e`) and used during download to automatically encode the resulting file.", "choices": [ "ascii", "base64", diff --git a/internal/metadata/baseline/baseline/keyvault_show.json b/internal/metadata/baseline/baseline/keyvault_show.json index 8595cc9..b018a32 100644 --- a/internal/metadata/baseline/baseline/keyvault_show.json +++ b/internal/metadata/baseline/baseline/keyvault_show.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 10, "unparsed_lines": 0, diff --git a/internal/metadata/baseline/baseline/login.json b/internal/metadata/baseline/baseline/login.json index 86c0cda..ebde9a1 100644 --- a/internal/metadata/baseline/baseline/login.json +++ b/internal/metadata/baseline/baseline/login.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 25, "unparsed_lines": 0, diff --git a/internal/metadata/baseline/baseline/monitor_log_analytics_workspace_create.json b/internal/metadata/baseline/baseline/monitor_log_analytics_workspace_create.json index 3a3aa84..8236aee 100644 --- a/internal/metadata/baseline/baseline/monitor_log_analytics_workspace_create.json +++ b/internal/metadata/baseline/baseline/monitor_log_analytics_workspace_create.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 24, "unparsed_lines": 0, @@ -49,7 +49,7 @@ "required": false, "group": "Optional Parameters", "global": false, - "takes_value": true, + "takes_value": false, "value_kind": "bool", "help": "Do not wait for the long-running operation to finish.", "choices": [ diff --git a/internal/metadata/baseline/baseline/monitor_metrics_alert_create.json b/internal/metadata/baseline/baseline/monitor_metrics_alert_create.json index 9935992..b0c0eb2 100644 --- a/internal/metadata/baseline/baseline/monitor_metrics_alert_create.json +++ b/internal/metadata/baseline/baseline/monitor_metrics_alert_create.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 23, "unparsed_lines": 0, diff --git a/internal/metadata/baseline/baseline/network.json b/internal/metadata/baseline/baseline/network.json index 2241142..2fc1cfb 100644 --- a/internal/metadata/baseline/baseline/network.json +++ b/internal/metadata/baseline/baseline/network.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 37, "unparsed_lines": 0, diff --git a/internal/metadata/baseline/baseline/network_application_gateway_create.json b/internal/metadata/baseline/baseline/network_application_gateway_create.json index b0b178e..08e806a 100644 --- a/internal/metadata/baseline/baseline/network_application_gateway_create.json +++ b/internal/metadata/baseline/baseline/network_application_gateway_create.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 52, "unparsed_lines": 0, @@ -390,7 +390,7 @@ "global": false, "takes_value": true, "value_kind": "string", - "help": "The certificate name. Default will be `\u003capplication- gateway-name\u003eSslCert`.", + "help": "The certificate name. Default will be `\u003capplication-gateway-name\u003eSslCert`.", "choices": null, "default": null, "values_from": null @@ -440,8 +440,8 @@ "required": false, "group": "Identity Arguments", "global": false, - "takes_value": false, - "value_kind": "bool", + "takes_value": true, + "value_kind": "string", "help": "Name or ID of the ManagedIdentity Resource.", "choices": null, "default": null, @@ -455,7 +455,7 @@ "global": false, "takes_value": true, "value_kind": "string", - "help": "The application gateway trusted client certificate. Usage: --trusted-client-certificates name=client1 data=client.cer name: Required. Name of the trusted client certificate that is unique within an Application Gateway data: Required. Certificate public data. Multiple trusted client certificates can be specified by using more than one `--trusted- client-certificates` argument. WARNING: Argument '--trusted-client-cert' is in preview and under development. Reference and support levels: https://aka.ms/CLI_refstatus", + "help": "The application gateway trusted client certificate. Usage: --trusted-client-certificates name=client1 data=client.cer name: Required. Name of the trusted client certificate that is unique within an Application Gateway data: Required. Certificate public data. Multiple trusted client certificates can be specified by using more than one `--trusted-client-certificates` argument. WARNING: Argument '--trusted-client-cert' is in preview and under development. Reference and support levels: https://aka.ms/CLI_refstatus", "choices": null, "default": null, "values_from": null @@ -507,7 +507,7 @@ "global": false, "takes_value": true, "value_kind": "string", - "help": "Name or ID of the subnet. Will create resource if it does not exist. If name specified, also specify --vnet- name. If you want to use an existing subnet in other resource group or subscription, please provide the ID instead of the name of the subnet.", + "help": "Name or ID of the subnet. Will create resource if it does not exist. If name specified, also specify --vnet-name. If you want to use an existing subnet in other resource group or subscription, please provide the ID instead of the name of the subnet.", "choices": null, "default": "default", "values_from": null @@ -627,7 +627,7 @@ "global": false, "takes_value": true, "value_kind": "list", - "help": "The application gateway ssl profiles. Usage: --ssl-profile name=MySslProfile client-auth-configuration=True cipher- suites=TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 policy-type=Custom min-protocol-version=TLSv1_0 name: Required. Name of the SSL profile that is unique within an Application Gateway. polic-name: Name of Ssl Policy. policy-type: Type of Ssl Policy. min-protocol-version: Minimum version of Ssl protocol to be supported on application gateway. cipher-suites: Ssl cipher suites to be enabled in the specified order to application gateway. disabled-ssl-protocols: Space-separated list of protocols to disable. trusted-client-certificates: Array of references to application gateway trusted client certificates. client-auth-configuration: Client authentication configuration of the application gateway resource. Multiple ssl profiles can be specified by using more than one `--ssl-profile` argument. WARNING: Argument '--ssl-profile' is in preview and under development. Reference and support levels: https://aka.ms/CLI_refstatus", + "help": "The application gateway ssl profiles. Usage: --ssl-profile name=MySslProfile client-auth-configuration=True cipher-suites=TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 policy-type=Custom min-protocol-version=TLSv1_0 name: Required. Name of the SSL profile that is unique within an Application Gateway. polic-name: Name of Ssl Policy. policy-type: Type of Ssl Policy. min-protocol-version: Minimum version of Ssl protocol to be supported on application gateway. cipher-suites: Ssl cipher suites to be enabled in the specified order to application gateway. disabled-ssl-protocols: Space-separated list of protocols to disable. trusted-client-certificates: Array of references to application gateway trusted client certificates. client-auth-configuration: Client authentication configuration of the application gateway resource. Multiple ssl profiles can be specified by using more than one `--ssl-profile` argument. WARNING: Argument '--ssl-profile' is in preview and under development. Reference and support levels: https://aka.ms/CLI_refstatus", "choices": null, "default": null, "values_from": null diff --git a/internal/metadata/baseline/baseline/network_bastion_create.json b/internal/metadata/baseline/baseline/network_bastion_create.json index c7ccd2b..ba80034 100644 --- a/internal/metadata/baseline/baseline/network_bastion_create.json +++ b/internal/metadata/baseline/baseline/network_bastion_create.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 27, "unparsed_lines": 0, @@ -61,7 +61,7 @@ "required": false, "group": "Optional Parameters", "global": false, - "takes_value": true, + "takes_value": false, "value_kind": "bool", "help": "Do not wait for the long-running operation to finish.", "choices": [ @@ -159,7 +159,7 @@ "global": false, "takes_value": true, "value_kind": "string", - "help": "Resource tags. Support shorthand-syntax, json-file and yaml- file. Try \"??\" to show more.", + "help": "Resource tags. Support shorthand-syntax, json-file and yaml-file. Try \"??\" to show more.", "choices": null, "default": null, "values_from": null @@ -172,7 +172,7 @@ "global": false, "takes_value": true, "value_kind": "list", - "help": "A list of availability zones denoting where the resource needs to come from. Support shorthand-syntax, json-file and yaml- file. Try \"??\" to show more.", + "help": "A list of availability zones denoting where the resource needs to come from. Support shorthand-syntax, json-file and yaml-file. Try \"??\" to show more.", "choices": null, "default": null, "values_from": null @@ -305,7 +305,7 @@ "global": false, "takes_value": true, "value_kind": "list", - "help": "[Supported in Developer SKU only] Network ACLs IP rules. Space- separated list of IP addresses.", + "help": "[Supported in Developer SKU only] Network ACLs IP rules. Space-separated list of IP addresses.", "choices": null, "default": null, "values_from": null diff --git a/internal/metadata/baseline/baseline/network_bastion_ssh.json b/internal/metadata/baseline/baseline/network_bastion_ssh.json index 4f2815d..c9c84aa 100644 --- a/internal/metadata/baseline/baseline/network_bastion_ssh.json +++ b/internal/metadata/baseline/baseline/network_bastion_ssh.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 19, "unparsed_lines": 0, diff --git a/internal/metadata/baseline/baseline/network_bastion_tunnel.json b/internal/metadata/baseline/baseline/network_bastion_tunnel.json index bc296bf..cd8d5c6 100644 --- a/internal/metadata/baseline/baseline/network_bastion_tunnel.json +++ b/internal/metadata/baseline/baseline/network_bastion_tunnel.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 17, "unparsed_lines": 0, diff --git a/internal/metadata/baseline/baseline/network_lb_create.json b/internal/metadata/baseline/baseline/network_lb_create.json index 8d08c8a..b5caf53 100644 --- a/internal/metadata/baseline/baseline/network_lb_create.json +++ b/internal/metadata/baseline/baseline/network_lb_create.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 30, "unparsed_lines": 0, diff --git a/internal/metadata/baseline/baseline/network_nsg_create.json b/internal/metadata/baseline/baseline/network_nsg_create.json index 815b308..00f97f8 100644 --- a/internal/metadata/baseline/baseline/network_nsg_create.json +++ b/internal/metadata/baseline/baseline/network_nsg_create.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 14, "unparsed_lines": 0, @@ -63,7 +63,7 @@ "required": false, "group": "Optional Parameters", "global": false, - "takes_value": true, + "takes_value": false, "value_kind": "bool", "help": "Do not wait for the long-running operation to finish.", "choices": [ diff --git a/internal/metadata/baseline/baseline/network_public_ip_create.json b/internal/metadata/baseline/baseline/network_public_ip_create.json index 4349ca4..16ed9e3 100644 --- a/internal/metadata/baseline/baseline/network_public_ip_create.json +++ b/internal/metadata/baseline/baseline/network_public_ip_create.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 28, "unparsed_lines": 0, diff --git a/internal/metadata/baseline/baseline/network_vnet_create.json b/internal/metadata/baseline/baseline/network_vnet_create.json index eddf6c8..d91e49e 100644 --- a/internal/metadata/baseline/baseline/network_vnet_create.json +++ b/internal/metadata/baseline/baseline/network_vnet_create.json @@ -1,11 +1,11 @@ { "schema_version": 1, "command": "network vnet create", - "summary": "Create a virtual network. You may also create a subnet at the same time by specifying a subnet name and (optionally) an address prefix. To learn about how to create a virtual network visit https://learn.microsoft.com/azure/virtual-network/manage-virtual-network#create-a-virtual- network.", + "summary": "Create a virtual network. You may also create a subnet at the same time by specifying a subnet name and (optionally) an address prefix. To learn about how to create a virtual network visit https://learn.microsoft.com/azure/virtual-network/manage-virtual-network#create-a-virtual-network.", "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 31, "unparsed_lines": 0, @@ -50,7 +50,7 @@ "global": false, "takes_value": true, "value_kind": "list", - "help": "Space-separated list of IP address prefixes for the VNet. If provided, --ipam- allocations should not be specified. Support shorthand-syntax, json-file and yaml-file. Try \"??\" to show more.", + "help": "Space-separated list of IP address prefixes for the VNet. If provided, --ipam-allocations should not be specified. Support shorthand-syntax, json-file and yaml-file. Try \"??\" to show more.", "choices": null, "default": "10.0.0.0/16", "values_from": null @@ -137,7 +137,7 @@ "required": false, "group": "Optional Parameters", "global": false, - "takes_value": true, + "takes_value": false, "value_kind": "bool", "help": "Enable encryption on the virtual network.", "choices": [ @@ -207,7 +207,7 @@ "required": false, "group": "Optional Parameters", "global": false, - "takes_value": true, + "takes_value": false, "value_kind": "bool", "help": "Do not wait for the long-running operation to finish.", "choices": [ @@ -251,7 +251,7 @@ "global": false, "takes_value": true, "value_kind": "keyvalue", - "help": "Space-separated tags: key[=value] [key[=value]...]. Support shorthand- syntax, json-file and yaml-file. Try \"??\" to show more.", + "help": "Space-separated tags: key[=value] [key[=value]...]. Support shorthand-syntax, json-file and yaml-file. Try \"??\" to show more.", "choices": null, "default": null, "values_from": null @@ -262,7 +262,7 @@ "required": false, "group": "Optional Parameters", "global": false, - "takes_value": true, + "takes_value": false, "value_kind": "bool", "help": "Enable VM protection for all subnets in the VNet.", "choices": [ @@ -331,7 +331,7 @@ "global": false, "takes_value": true, "value_kind": "list", - "help": "A configurable list of summarized gateway prefixes advertised for the virtual network. Support shorthand-syntax, json- file and yaml-file. Try \"??\" to show more. WARNING: Argument '--summarized-gateway-prefixes' is in preview and under development. Reference and support levels: https://aka.ms/CLI_refstatus", + "help": "A configurable list of summarized gateway prefixes advertised for the virtual network. Support shorthand-syntax, json-file and yaml-file. Try \"??\" to show more. WARNING: Argument '--summarized-gateway-prefixes' is in preview and under development. Reference and support levels: https://aka.ms/CLI_refstatus", "choices": null, "default": null, "values_from": null @@ -372,7 +372,7 @@ "global": false, "takes_value": true, "value_kind": "list", - "help": "Space-separated list of address prefixes in CIDR format for the new subnet. If omitted, automatically reserves a /24 (or as large as available) block within the VNet address space. Support shorthand- syntax, json-file and yaml-file. Try \"??\" to show more.", + "help": "Space-separated list of address prefixes in CIDR format for the new subnet. If omitted, automatically reserves a /24 (or as large as available) block within the VNet address space. Support shorthand-syntax, json-file and yaml-file. Try \"??\" to show more.", "choices": null, "default": null, "values_from": null @@ -385,7 +385,7 @@ "global": false, "takes_value": true, "value_kind": "list", - "help": "Manage a list of subnets in a Virtual Network (similar to `az network vnet subnet`). Support shorthand-syntax, json- file and yaml-file. Try \"??\" to show more.", + "help": "Manage a list of subnets in a Virtual Network (similar to `az network vnet subnet`). Support shorthand-syntax, json-file and yaml-file. Try \"??\" to show more.", "choices": null, "default": null, "values_from": null diff --git a/internal/metadata/baseline/baseline/network_vnet_list.json b/internal/metadata/baseline/baseline/network_vnet_list.json index 0e43aad..ef33ba5 100644 --- a/internal/metadata/baseline/baseline/network_vnet_list.json +++ b/internal/metadata/baseline/baseline/network_vnet_list.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 10, "unparsed_lines": 0, diff --git a/internal/metadata/baseline/baseline/network_vnet_subnet_create.json b/internal/metadata/baseline/baseline/network_vnet_subnet_create.json index d5db84e..df5909b 100644 --- a/internal/metadata/baseline/baseline/network_vnet_subnet_create.json +++ b/internal/metadata/baseline/baseline/network_vnet_subnet_create.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 28, "unparsed_lines": 0, @@ -63,7 +63,7 @@ "global": false, "takes_value": true, "value_kind": "list", - "help": "Space-separated list of address prefixes in CIDR format. If provided, --ipam-allocations should not be specified. Support shorthand-syntax, json- file and yaml-file. Try \"??\" to show more.", + "help": "Space-separated list of address prefixes in CIDR format. If provided, --ipam-allocations should not be specified. Support shorthand-syntax, json-file and yaml-file. Try \"??\" to show more.", "choices": null, "default": null, "values_from": null @@ -113,9 +113,9 @@ "required": false, "group": "Optional Parameters", "global": false, - "takes_value": true, + "takes_value": false, "value_kind": "bool", - "help": "Disable private endpoint network policies on the subnet. Please note that it will be replaced by `--private- endpoint-network-policies` soon.", + "help": "Disable private endpoint network policies on the subnet. Please note that it will be replaced by `--private-endpoint-network-policies` soon.", "choices": [ "0", "1", @@ -137,9 +137,9 @@ "required": false, "group": "Optional Parameters", "global": false, - "takes_value": true, + "takes_value": false, "value_kind": "bool", - "help": "Disable private link service network policies on the subnet. Please note that it will be replaced by `--private-link- service-network-policies` soon.", + "help": "Disable private link service network policies on the subnet. Please note that it will be replaced by `--private-link-service-network-policies` soon.", "choices": [ "0", "1", @@ -163,7 +163,7 @@ "global": false, "takes_value": true, "value_kind": "string", - "help": "An array of service endpoints. Support shorthand-syntax, json- file and yaml-file. Try \"??\" to show more.", + "help": "An array of service endpoints. Support shorthand-syntax, json-file and yaml-file. Try \"??\" to show more.", "choices": null, "default": null, "values_from": null @@ -178,7 +178,7 @@ "global": false, "takes_value": true, "value_kind": "list", - "help": "A list of IPAM Pools for allocating IP address prefixes. If provided, --address-prefixes would be ignored by CLI and should not be specified. Support shorthand-syntax, json- file and yaml-file. Try \"??\" to show more.", + "help": "A list of IPAM Pools for allocating IP address prefixes. If provided, --address-prefixes would be ignored by CLI and should not be specified. Support shorthand-syntax, json-file and yaml-file. Try \"??\" to show more.", "choices": null, "default": null, "values_from": null @@ -217,9 +217,9 @@ "required": false, "group": "Optional Parameters", "global": false, - "takes_value": true, + "takes_value": false, "value_kind": "bool", - "help": "Do not wait for the long- running operation to finish.", + "help": "Do not wait for the long-running operation to finish.", "choices": [ "0", "1", @@ -307,10 +307,10 @@ "global": false, "takes_value": true, "value_kind": "list", - "help": "Space-separated list of services allowed private access to this subnet. Support shorthand- syntax, json-file and yaml- file. Try \"??\" to show more.", + "help": "Space-separated list of services allowed private access to this subnet. Support shorthand-syntax, json-file and yaml-file. Try \"??\" to show more.", "choices": null, "default": null, - "values_from": "az network vnet list-endpoint- services" + "values_from": "az network vnet list-endpoint-services" }, { "name": "--sharing-scope", diff --git a/internal/metadata/baseline/baseline/postgres_flexible_server_create.json b/internal/metadata/baseline/baseline/postgres_flexible_server_create.json index 7e0f4ba..2fc3af4 100644 --- a/internal/metadata/baseline/baseline/postgres_flexible_server_create.json +++ b/internal/metadata/baseline/baseline/postgres_flexible_server_create.json @@ -1,11 +1,11 @@ { "schema_version": 1, "command": "postgres flexible-server create", - "summary": "Create a PostgreSQL flexible server. Create a PostgreSQL flexible server with custom or default configuration. For more information for network configuration, see - Configure public access https://learn.microsoft.com/azure/postgresql/flexible-server/how-to-manage-firewall-cli - Configure private access https://learn.microsoft.com/azure/postgresql/flexible-server/how-to-manage-virtual-network- cli.", + "summary": "Create a PostgreSQL flexible server. Create a PostgreSQL flexible server with custom or default configuration. For more information for network configuration, see - Configure public access https://learn.microsoft.com/azure/postgresql/flexible-server/how-to-manage-firewall-cli - Configure private access https://learn.microsoft.com/azure/postgresql/flexible-server/how-to-manage-virtual-network-cli.", "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 48, "unparsed_lines": 0, @@ -179,8 +179,8 @@ "required": false, "group": "Optional Parameters", "global": false, - "takes_value": false, - "value_kind": "bool", + "takes_value": true, + "value_kind": "string", "help": "The name or resource identifier of the user assigned identity for data encryption.", "choices": null, "default": null, diff --git a/internal/metadata/baseline/baseline/redis_create.json b/internal/metadata/baseline/baseline/redis_create.json index 81f0f51..a3dff63 100644 --- a/internal/metadata/baseline/baseline/redis_create.json +++ b/internal/metadata/baseline/baseline/redis_create.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 30, "unparsed_lines": 0, @@ -25,7 +25,7 @@ "help": "Location. You can configure the default location using `az configure --defaults location=\u003clocation\u003e`.", "choices": null, "default": null, - "values_from": "az account list- locations" + "values_from": "az account list-locations" }, { "name": "--name", diff --git a/internal/metadata/baseline/baseline/rest.json b/internal/metadata/baseline/baseline/rest.json index 694ab88..6c18b9b 100644 --- a/internal/metadata/baseline/baseline/rest.json +++ b/internal/metadata/baseline/baseline/rest.json @@ -1,11 +1,11 @@ { "schema_version": 1, "command": "rest", - "summary": "Invoke a custom request. This command automatically authenticates using the logged-in credential: If Authorization header is not set, it attaches header `Authorization: Bearer \u003ctoken\u003e`, where `\u003ctoken\u003e` is retrieved from AAD. The target resource of the token is derived from --url if --url starts with an endpoint from `az cloud show --query endpoints`. You may also use --resource for a custom resource. If Content-Type header is not set and --body is a valid JSON string, Content-Type header will default to application/json. For passing JSON in PowerShell, see https://github.com/Azure/azure-cli/blob/dev/doc/quoting- issues-with-powershell.md.", + "summary": "Invoke a custom request. This command automatically authenticates using the logged-in credential: If Authorization header is not set, it attaches header `Authorization: Bearer \u003ctoken\u003e`, where `\u003ctoken\u003e` is retrieved from AAD. The target resource of the token is derived from --url if --url starts with an endpoint from `az cloud show --query endpoints`. You may also use --resource for a custom resource. If Content-Type header is not set and --body is a valid JSON string, Content-Type header will default to application/json. For passing JSON in PowerShell, see https://github.com/Azure/azure-cli/blob/dev/doc/quoting-issues-with-powershell.md.", "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 17, "unparsed_lines": 0, @@ -38,7 +38,7 @@ "global": false, "takes_value": true, "value_kind": "path", - "help": "Request body. Use @{file} to load from a file. For quoting issues in different terminals, see https://github.com/Azure/azure- cli/blob/dev/doc/use_cli_effectively.md#quoting-issues.", + "help": "Request body. Use @{file} to load from a file. For quoting issues in different terminals, see https://github.com/Azure/azure-cli/blob/dev/doc/use_cli_effectively.md#quoting-issues.", "choices": null, "default": null, "values_from": null diff --git a/internal/metadata/baseline/baseline/role_assignment_create.json b/internal/metadata/baseline/baseline/role_assignment_create.json index 1713def..e55aad7 100644 --- a/internal/metadata/baseline/baseline/role_assignment_create.json +++ b/internal/metadata/baseline/baseline/role_assignment_create.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 18, "unparsed_lines": 0, @@ -33,7 +33,7 @@ "global": false, "takes_value": true, "value_kind": "string", - "help": "Scope at which the role assignment or definition applies to, e.g., /subscriptions/0b1f6471-1bf0-4dda-aec3-111122223333, /subscriptions/0b1f6471-1bf0-4dda- aec3-111122223333/resourceGroups/myGroup, or /subscriptions/0b1f6471-1bf0-4dda-aec3- 111122223333/resourceGroups/myGroup/providers/Microsoft.Compute/ virtualMachines/myVM.", + "help": "Scope at which the role assignment or definition applies to, e.g., /subscriptions/0b1f6471-1bf0-4dda-aec3-111122223333, /subscriptions/0b1f6471-1bf0-4dda-aec3-111122223333/resourceGroups/myGroup, or /subscriptions/0b1f6471-1bf0-4dda-aec3- 111122223333/resourceGroups/myGroup/providers/Microsoft.Compute/ virtualMachines/myVM.", "choices": null, "default": null, "values_from": null diff --git a/internal/metadata/baseline/baseline/servicebus_queue_create.json b/internal/metadata/baseline/baseline/servicebus_queue_create.json index 8f320f4..74b2d19 100644 --- a/internal/metadata/baseline/baseline/servicebus_queue_create.json +++ b/internal/metadata/baseline/baseline/servicebus_queue_create.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 28, "unparsed_lines": 0, diff --git a/internal/metadata/baseline/baseline/sql_db_create.json b/internal/metadata/baseline/baseline/sql_db_create.json index 8371a4f..b066686 100644 --- a/internal/metadata/baseline/baseline/sql_db_create.json +++ b/internal/metadata/baseline/baseline/sql_db_create.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 44, "unparsed_lines": 0, diff --git a/internal/metadata/baseline/baseline/sql_server_create.json b/internal/metadata/baseline/baseline/sql_server_create.json index f567ca1..2b72226 100644 --- a/internal/metadata/baseline/baseline/sql_server_create.json +++ b/internal/metadata/baseline/baseline/sql_server_create.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 30, "unparsed_lines": 0, @@ -219,7 +219,7 @@ "help": "Location. You can configure the default location using `az configure --defaults location=\u003clocation\u003e`.", "choices": null, "default": null, - "values_from": "az account list- locations" + "values_from": "az account list-locations" }, { "name": "--minimal-tls-version", diff --git a/internal/metadata/baseline/baseline/storage_account.json b/internal/metadata/baseline/baseline/storage_account.json index 969d86c..eb4bc5f 100644 --- a/internal/metadata/baseline/baseline/storage_account.json +++ b/internal/metadata/baseline/baseline/storage_account.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 26, "unparsed_lines": 0, diff --git a/internal/metadata/baseline/baseline/storage_account_check_name.json b/internal/metadata/baseline/baseline/storage_account_check_name.json index 198fdea..ce25d79 100644 --- a/internal/metadata/baseline/baseline/storage_account_check_name.json +++ b/internal/metadata/baseline/baseline/storage_account_check_name.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 10, "unparsed_lines": 0, diff --git a/internal/metadata/baseline/baseline/storage_account_create.json b/internal/metadata/baseline/baseline/storage_account_create.json index cbd80fc..5c66ab2 100644 --- a/internal/metadata/baseline/baseline/storage_account_create.json +++ b/internal/metadata/baseline/baseline/storage_account_create.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 76, "unparsed_lines": 0, @@ -242,7 +242,7 @@ "required": false, "group": "Optional Parameters", "global": false, - "takes_value": true, + "takes_value": false, "value_kind": "bool", "help": "Enable local user features.", "choices": [ @@ -274,7 +274,7 @@ "required": false, "group": "Optional Parameters", "global": false, - "takes_value": true, + "takes_value": false, "value_kind": "bool", "help": "Enable Secure File Transfer Protocol.", "choices": [ @@ -294,7 +294,7 @@ "global": false, "takes_value": true, "value_kind": "enum", - "help": "Set the encryption key type for Queue service. \"Account\": Queue will be encrypted with account-scoped encryption key. \"Service\": Queue will always be encrypted with service- scoped keys. Currently the default encryption key type is \"Service\".", + "help": "Set the encryption key type for Queue service. \"Account\": Queue will be encrypted with account-scoped encryption key. \"Service\": Queue will always be encrypted with service-scoped keys. Currently the default encryption key type is \"Service\".", "choices": [ "Account", "Service" @@ -312,7 +312,7 @@ "global": false, "takes_value": true, "value_kind": "enum", - "help": "Set the encryption key type for Table service. \"Account\": Table will be encrypted with account-scoped encryption key. \"Service\": Table will always be encrypted with service- scoped keys. Currently the default encryption key type is \"Service\".", + "help": "Set the encryption key type for Table service. \"Account\": Table will be encrypted with account-scoped encryption key. \"Service\": Table will always be encrypted with service-scoped keys. Currently the default encryption key type is \"Service\".", "choices": [ "Account", "Service" @@ -617,7 +617,7 @@ "global": false, "takes_value": true, "value_kind": "enum", - "help": "Defines the mode of the policy. Disabled state disables the policy, Unlocked state allows increase and decrease of immutability retention time and also allows toggling allow- protected-append-write property, Locked state only allows the increase of the immutability retention time. A policy can only be created in a Disabled or Unlocked state and can be toggled between the two states. Only a policy in an Unlocked state can transition to a Locked state which cannot be reverted.", + "help": "Defines the mode of the policy. Disabled state disables the policy, Unlocked state allows increase and decrease of immutability retention time and also allows toggling allow-protected-append-write property, Locked state only allows the increase of the immutability retention time. A policy can only be created in a Disabled or Unlocked state and can be toggled between the two states. Only a policy in an Unlocked state can transition to a Locked state which cannot be reverted.", "choices": [ "Disabled", "Locked", @@ -689,7 +689,7 @@ "global": false, "takes_value": true, "value_kind": "string", - "help": "Specify the security identifier (SID). Required when --enable-files- adds is set to True.", + "help": "Specify the security identifier (SID). Required when --enable-files-adds is set to True.", "choices": null, "default": null, "values_from": null @@ -702,7 +702,7 @@ "global": false, "takes_value": true, "value_kind": "string", - "help": "Specify the Active Directory forest to get. Required when --enable-files- adds is set to True.", + "help": "Specify the Active Directory forest to get. Required when --enable-files-adds is set to True.", "choices": null, "default": null, "values_from": null @@ -759,7 +759,7 @@ "required": false, "group": "Azure Files Identity Based Authentication Arguments", "global": false, - "takes_value": true, + "takes_value": false, "value_kind": "bool", "help": "Enable Azure Active Directory Domain Services authentication for Azure Files.", "choices": [ @@ -775,7 +775,7 @@ "required": false, "group": "Azure Files Identity Based Authentication Arguments", "global": false, - "takes_value": true, + "takes_value": false, "value_kind": "bool", "help": "Enable Azure Files Active Directory Domain Service Kerberos Authentication for the storage account.", "choices": [ @@ -791,9 +791,9 @@ "required": false, "group": "Azure Files Identity Based Authentication Arguments", "global": false, - "takes_value": true, + "takes_value": false, "value_kind": "bool", - "help": "Enable Azure Files Active Directory Domain Service Authentication for storage account. When --enable-files- adds is set to true, Azure Active Directory Properties arguments must be provided.", + "help": "Enable Azure Files Active Directory Domain Service Authentication for storage account. When --enable-files-adds is set to true, Azure Active Directory Properties arguments must be provided.", "choices": [ "false", "true" diff --git a/internal/metadata/baseline/baseline/storage_account_delete.json b/internal/metadata/baseline/baseline/storage_account_delete.json index 2c85946..b1f2f9c 100644 --- a/internal/metadata/baseline/baseline/storage_account_delete.json +++ b/internal/metadata/baseline/baseline/storage_account_delete.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 13, "unparsed_lines": 0, diff --git a/internal/metadata/baseline/baseline/storage_account_keys_list.json b/internal/metadata/baseline/baseline/storage_account_keys_list.json index f257a2b..19d028f 100644 --- a/internal/metadata/baseline/baseline/storage_account_keys_list.json +++ b/internal/metadata/baseline/baseline/storage_account_keys_list.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 10, "unparsed_lines": 0, diff --git a/internal/metadata/baseline/baseline/storage_account_list.json b/internal/metadata/baseline/baseline/storage_account_list.json index c45270c..c29fac9 100644 --- a/internal/metadata/baseline/baseline/storage_account_list.json +++ b/internal/metadata/baseline/baseline/storage_account_list.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 8, "unparsed_lines": 0, diff --git a/internal/metadata/baseline/baseline/storage_account_show.json b/internal/metadata/baseline/baseline/storage_account_show.json index c53dc73..f521bbd 100644 --- a/internal/metadata/baseline/baseline/storage_account_show.json +++ b/internal/metadata/baseline/baseline/storage_account_show.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 11, "unparsed_lines": 0, diff --git a/internal/metadata/baseline/baseline/storage_account_update.json b/internal/metadata/baseline/baseline/storage_account_update.json index 80bdf65..c880a90 100644 --- a/internal/metadata/baseline/baseline/storage_account_update.json +++ b/internal/metadata/baseline/baseline/storage_account_update.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 71, "unparsed_lines": 0, @@ -163,7 +163,7 @@ "required": false, "group": "Optional Parameters", "global": false, - "takes_value": true, + "takes_value": false, "value_kind": "bool", "help": "Enable local user features.", "choices": [ @@ -179,7 +179,7 @@ "required": false, "group": "Optional Parameters", "global": false, - "takes_value": true, + "takes_value": false, "value_kind": "bool", "help": "Enable Secure File Transfer Protocol.", "choices": [ @@ -465,7 +465,7 @@ "global": false, "takes_value": true, "value_kind": "enum", - "help": "Defines the mode of the policy. Disabled state disables the policy, Unlocked state allows increase and decrease of immutability retention time and also allows toggling allow- protected-append-write property, Locked state only allows the increase of the immutability retention time. A policy can only be created in a Disabled or Unlocked state and can be toggled between the two states. Only a policy in an Unlocked state can transition to a Locked state which cannot be reverted.", + "help": "Defines the mode of the policy. Disabled state disables the policy, Unlocked state allows increase and decrease of immutability retention time and also allows toggling allow-protected-append-write property, Locked state only allows the increase of the immutability retention time. A policy can only be created in a Disabled or Unlocked state and can be toggled between the two states. Only a policy in an Unlocked state can transition to a Locked state which cannot be reverted.", "choices": [ "Disabled", "Locked", @@ -537,7 +537,7 @@ "global": false, "takes_value": true, "value_kind": "string", - "help": "Specify the security identifier (SID). Required when --enable-files- adds is set to True.", + "help": "Specify the security identifier (SID). Required when --enable-files-adds is set to True.", "choices": null, "default": null, "values_from": null @@ -550,7 +550,7 @@ "global": false, "takes_value": true, "value_kind": "string", - "help": "Specify the Active Directory forest to get. Required when --enable-files- adds is set to True.", + "help": "Specify the Active Directory forest to get. Required when --enable-files-adds is set to True.", "choices": null, "default": null, "values_from": null @@ -607,7 +607,7 @@ "required": false, "group": "Azure Files Identity Based Authentication Arguments", "global": false, - "takes_value": true, + "takes_value": false, "value_kind": "bool", "help": "Enable Azure Active Directory Domain Services authentication for Azure Files.", "choices": [ @@ -623,7 +623,7 @@ "required": false, "group": "Azure Files Identity Based Authentication Arguments", "global": false, - "takes_value": true, + "takes_value": false, "value_kind": "bool", "help": "Enable Azure Files Active Directory Domain Service Kerberos Authentication for the storage account.", "choices": [ @@ -639,9 +639,9 @@ "required": false, "group": "Azure Files Identity Based Authentication Arguments", "global": false, - "takes_value": true, + "takes_value": false, "value_kind": "bool", - "help": "Enable Azure Files Active Directory Domain Service Authentication for storage account. When --enable-files- adds is set to true, Azure Active Directory Properties arguments must be provided.", + "help": "Enable Azure Files Active Directory Domain Service Authentication for storage account. When --enable-files-adds is set to true, Azure Active Directory Properties arguments must be provided.", "choices": [ "false", "true" @@ -916,7 +916,7 @@ "global": false, "takes_value": true, "value_kind": "list", - "help": "One or more resource IDs (space- delimited). It should be a complete resource ID containing all information of 'Resource Id' arguments. You should provide either --ids or other 'Resource Id' arguments.", + "help": "One or more resource IDs (space-delimited). It should be a complete resource ID containing all information of 'Resource Id' arguments. You should provide either --ids or other 'Resource Id' arguments.", "choices": null, "default": null, "values_from": null diff --git a/internal/metadata/baseline/baseline/storage_blob_upload.json b/internal/metadata/baseline/baseline/storage_blob_upload.json index 108188b..38a9426 100644 --- a/internal/metadata/baseline/baseline/storage_blob_upload.json +++ b/internal/metadata/baseline/baseline/storage_blob_upload.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 45, "unparsed_lines": 0, diff --git a/internal/metadata/baseline/baseline/storage_container_create.json b/internal/metadata/baseline/baseline/storage_container_create.json index 660627a..0402137 100644 --- a/internal/metadata/baseline/baseline/storage_container_create.json +++ b/internal/metadata/baseline/baseline/storage_container_create.json @@ -1,11 +1,11 @@ { "schema_version": 1, "command": "storage container create", - "summary": "Create a container in a storage account. By default, container data is private (\"off\") to the account owner. Use \"blob\" to allow public read access for blobs. Use \"container\" to allow public read and list access to the entire container. You can configure the --public-access using `az storage container set- permission -n CONTAINER_NAME --public-access blob/container/off`.", + "summary": "Create a container in a storage account. By default, container data is private (\"off\") to the account owner. Use \"blob\" to allow public read access for blobs. Use \"container\" to allow public read and list access to the entire container. You can configure the --public-access using `az storage container set-permission -n CONTAINER_NAME --public-access blob/container/off`.", "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 23, "unparsed_lines": 0, @@ -137,7 +137,7 @@ "required": false, "group": "Encryption Policy Arguments", "global": false, - "takes_value": true, + "takes_value": false, "value_kind": "bool", "help": "Block override of encryption scope from the container default. WARNING: Argument '--prevent-encryption-scope-override' is in preview and under development. Reference and support levels: https://aka.ms/CLI_refstatus", "choices": [ diff --git a/internal/metadata/baseline/baseline/vm.json b/internal/metadata/baseline/baseline/vm.json index e34f2a0..02d1721 100644 --- a/internal/metadata/baseline/baseline/vm.json +++ b/internal/metadata/baseline/baseline/vm.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 46, "unparsed_lines": 0, diff --git a/internal/metadata/baseline/baseline/vm_create.json b/internal/metadata/baseline/baseline/vm_create.json index f0b5a49..92667da 100644 --- a/internal/metadata/baseline/baseline/vm_create.json +++ b/internal/metadata/baseline/baseline/vm_create.json @@ -1,11 +1,11 @@ { "schema_version": 1, "command": "vm create", - "summary": "Create an Azure Virtual Machine. For an end-to-end tutorial, see https://learn.microsoft.com/azure/virtual- machines/linux/quick-create-cli.", + "summary": "Create an Azure Virtual Machine. For an end-to-end tutorial, see https://learn.microsoft.com/azure/virtual-machines/linux/quick-create-cli.", "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 130, "unparsed_lines": 0, @@ -65,7 +65,7 @@ "global": false, "takes_value": true, "value_kind": "bool", - "help": "Specify whether to implicitly install the ProxyAgent Extension. This option is currently applicable only for Linux OS. Use with --enable-proxy- agent.", + "help": "Specify whether to implicitly install the ProxyAgent Extension. This option is currently applicable only for Linux OS. Use with --enable-proxy-agent.", "choices": [ "false", "true" @@ -119,7 +119,7 @@ "global": false, "takes_value": true, "value_kind": "bool", - "help": "Specifies if Scheduled Events should be auto- approved when all instances are down. Its default value is true.", + "help": "Specifies if Scheduled Events should be auto-approved when all instances are down. Its default value is true.", "choices": [ "false", "true" @@ -189,7 +189,7 @@ "global": false, "takes_value": true, "value_kind": "int", - "help": "Number of virtual machines to create. Value range is [2, 250], inclusive. Don't specify this parameter if you want to create a normal single VM. The VMs are created in parallel. The output of this command is an array of VMs instead of one single VM. Each VM has its own public IP, NIC. VNET and NSG are shared. It is recommended that no existing public IP, NIC, VNET and NSG are in resource group. When --count is specified, --attach-data- disks, --attach-os- disk, --boot- diagnostics- storage, --computer-name, --host, --host- group, --nics, --os-disk-name, --private-ip- address, --public- ip-address, --public-ip- address-dns-name, --storage-account, --storage- container-name, --subnet, --use- unmanaged-disk, --vnet-name are not allowed. WARNING: Argument '--count' is in preview and under development. Reference and support levels: https://aka.ms/CLI_refstatus", + "help": "Number of virtual machines to create. Value range is [2, 250], inclusive. Don't specify this parameter if you want to create a normal single VM. The VMs are created in parallel. The output of this command is an array of VMs instead of one single VM. Each VM has its own public IP, NIC. VNET and NSG are shared. It is recommended that no existing public IP, NIC, VNET and NSG are in resource group. When --count is specified, --attach-data-disks, --attach-os-disk, --boot-diagnostics-storage, --computer-name, --host, --host-group, --nics, --os-disk-name, --private-ip-address, --public-ip-address, --public-ip-address-dns-name, --storage-account, --storage-container-name, --subnet, --use-unmanaged-disk, --vnet-name are not allowed. WARNING: Argument '--count' is in preview and under development. Reference and support levels: https://aka.ms/CLI_refstatus", "choices": null, "default": null, "values_from": null @@ -202,7 +202,7 @@ "global": false, "takes_value": true, "value_kind": "path", - "help": "Custom init script file or text (cloud-init, cloud- config, etc..).", + "help": "Custom init script file or text (cloud-init, cloud-config, etc..).", "choices": null, "default": null, "values_from": null @@ -315,7 +315,7 @@ "global": false, "takes_value": true, "value_kind": "bool", - "help": "Patch VMs without requiring a reboot. --enable-agent must be set and --patch- mode must be set to AutomaticByPlatform.", + "help": "Patch VMs without requiring a reboot. --enable-agent must be set and --patch-mode must be set to AutomaticByPlatform.", "choices": [ "false", "true" @@ -394,7 +394,7 @@ "required": false, "group": "Optional Parameters", "global": false, - "takes_value": true, + "takes_value": false, "value_kind": "bool", "help": "Enable secure boot.", "choices": [ @@ -410,7 +410,7 @@ "required": false, "group": "Optional Parameters", "global": false, - "takes_value": true, + "takes_value": false, "value_kind": "bool", "help": "Enable vTPM.", "choices": [ @@ -457,7 +457,7 @@ "global": false, "takes_value": true, "value_kind": "list", - "help": "If \"--zone- placement-policy\" is set to \"Any\", availability zone selected by the system must not be present in the list of availability zones passed with \"excludeZones\". If \"--exclude-zones\" is not provided, all availability zones in region will be considered for selection.", + "help": "If \"--zone-placement-policy\" is set to \"Any\", availability zone selected by the system must not be present in the list of availability zones passed with \"excludeZones\". If \"--exclude-zones\" is not provided, all availability zones in region will be considered for selection.", "choices": null, "default": null, "values_from": null @@ -470,10 +470,10 @@ "global": false, "takes_value": true, "value_kind": "string", - "help": "The name of the operating system image as a URN alias, URN, custom image name or ID, custom image version ID, or VHD blob URI. In addition, it also supports shared gallery image. Please use the image alias including the version of the distribution you want to use. For example: please use Debian11 instead of Debian.' This parameter is required unless using `--attach-os- disk.` Valid URN format: \"Publisher: Offer:Sku:Version\". For more information, see ht tps://learn.microso ft.com/azure/virtua l- machines/linux/cli- ps-findimage.", + "help": "The name of the operating system image as a URN alias, URN, custom image name or ID, custom image version ID, or VHD blob URI. In addition, it also supports shared gallery image. Please use the image alias including the version of the distribution you want to use. For example: please use Debian11 instead of Debian.' This parameter is required unless using `--attach-os-disk.` Valid URN format: \"Publisher: Offer:Sku:Version\". For more information, see ht tps://learn.microso ft.com/azure/virtua l-machines/linux/cli-ps-findimage.", "choices": null, "default": null, - "values_from": "az vm image list, az vm image show, az sig image-version show- shared" + "values_from": "az vm image list, az vm image show, az sig image-version show-shared" }, { "name": "--imds-access-control-profile-reference-id", @@ -514,7 +514,7 @@ "global": false, "takes_value": true, "value_kind": "list", - "help": "If \"--zone- placement-policy\" is set to \"Any\", availability zone selected by the system must be present in the list of availability zones passed with \" --include-zones\". If \"--include- zones\" is not provided, all availability zones in region will be considered for selection.", + "help": "If \"--zone-placement-policy\" is set to \"Any\", availability zone selected by the system must be present in the list of availability zones passed with \" --include-zones\". If \"--include-zones\" is not provided, all availability zones in region will be considered for selection.", "choices": null, "default": null, "values_from": null @@ -540,7 +540,7 @@ "global": false, "takes_value": true, "value_kind": "enum", - "help": "Specifies that the Windows image or disk was licensed on-premises. To enable Azure Hybrid Benefit for Windows Server, use 'Windows_Server'. To enable Multi- tenant Hosting Rights for Windows 10, use 'Windows_Client'. For more information see the Azure Windows VM online docs.", + "help": "Specifies that the Windows image or disk was licensed on-premises. To enable Azure Hybrid Benefit for Windows Server, use 'Windows_Server'. To enable Multi-tenant Hosting Rights for Windows 10, use 'Windows_Client'. For more information see the Azure Windows VM online docs.", "choices": [ "None", "RHEL_BASE", @@ -643,7 +643,7 @@ "global": false, "takes_value": true, "value_kind": "enum", - "help": "Mode of in-guest patching to IaaS virtual machine. Allowed values for Windows VM: AutomaticByOS, Auto maticByPlatform, Manual. Allowed values for Linux VM: AutomaticByPlat form, ImageDefault. Manual - You control the application of patches to a virtual machine. You do this by applying patches manually inside the VM. In this mode, automatic updates are disabled; the paramater --enable- auto-update must be false. AutomaticByOS - The virtual machine will automatically be updated by the OS. The parameter --enable-auto- update must be true. AutomaticByPlatform - the virtual machine will automatically updated by the OS. ImageDefault - The virtual machine's default patching configuration is used. The parameter --enable-agent and --enable-auto- update must be true.", + "help": "Mode of in-guest patching to IaaS virtual machine. Allowed values for Windows VM: AutomaticByOS, Auto maticByPlatform, Manual. Allowed values for Linux VM: AutomaticByPlat form, ImageDefault. Manual - You control the application of patches to a virtual machine. You do this by applying patches manually inside the VM. In this mode, automatic updates are disabled; the paramater --enable-auto-update must be false. AutomaticByOS - The virtual machine will automatically be updated by the OS. The parameter --enable-auto-update must be true. AutomaticByPlatform - the virtual machine will automatically updated by the OS. ImageDefault - The virtual machine's default patching configuration is used. The parameter --enable-agent and --enable-auto-update must be true.", "choices": [ "AutomaticByOS", "Auto maticByPlatform", @@ -722,7 +722,7 @@ "global": false, "takes_value": true, "value_kind": "string", - "help": "Specifies the api- version to determine which Scheduled Events configuration schema version will be delivered.", + "help": "Specifies the api-version to determine which Scheduled Events configuration schema version will be delivered.", "choices": null, "default": null, "values_from": null @@ -830,7 +830,7 @@ "global": false, "takes_value": true, "value_kind": "string", - "help": "Specify the ratio of vCPU to physical core. Setting this property to 1 also means that hyper- threading is disabled.", + "help": "Specify the ratio of vCPU to physical core. Setting this property to 1 also means that hyper-threading is disabled.", "choices": null, "default": null, "values_from": null @@ -1057,7 +1057,7 @@ "global": false, "takes_value": true, "value_kind": "string", - "help": "Name or resource ID of the dedicated host group that the VM will reside in. --host and --host- group can't be used together. WARNING: Argument '--host-group' is in preview and under development. Reference and support levels: https://aka.ms/CLI_refstatus", + "help": "Name or resource ID of the dedicated host group that the VM will reside in. --host and --host-group can't be used together. WARNING: Argument '--host-group' is in preview and under development. Reference and support levels: https://aka.ms/CLI_refstatus", "choices": null, "default": null, "values_from": null @@ -1094,8 +1094,8 @@ "required": false, "group": "Managed Service Identity Arguments", "global": false, - "takes_value": false, - "value_kind": "bool", + "takes_value": true, + "value_kind": "list", "help": "Accept system or user assigned identities separated by spaces. Use '[system]' to refer system assigned identity, or a resource id to refer user assigned identity. Check out help for more examples.", "choices": null, "default": null, @@ -1198,7 +1198,7 @@ "required": false, "group": "Network Arguments", "global": false, - "takes_value": true, + "takes_value": false, "value_kind": "bool", "help": "Enable accelerated networking. Unless specified, CLI will enable it based on machine image and size.", "choices": [ @@ -1356,7 +1356,7 @@ "global": false, "takes_value": true, "value_kind": "string", - "help": "The name of the subnet when creating a new VNet or referencing an existing one. Can also reference an existing subnet by ID. If both vnet- name and subnet are omitted, an appropriate VNet and subnet will be selected automatically, or a new one will be created.", + "help": "The name of the subnet when creating a new VNet or referencing an existing one. Can also reference an existing subnet by ID. If both vnet-name and subnet are omitted, an appropriate VNet and subnet will be selected automatically, or a new one will be created.", "choices": null, "default": null, "values_from": null @@ -1500,7 +1500,7 @@ "required": false, "group": "Storage Arguments", "global": false, - "takes_value": true, + "takes_value": false, "value_kind": "bool", "help": "Enable Host Encryption for the VM or VMSS. This will enable the encryption for all the disks including Resource/Temp disk at host itself.", "choices": [ @@ -1554,7 +1554,7 @@ "global": false, "takes_value": true, "value_kind": "enum", - "help": "Only applicable when used with `--ephemeral-os- disk`. Allows you to choose the Ephemeral OS disk provisioning location.", + "help": "Only applicable when used with `--ephemeral-os-disk`. Allows you to choose the Ephemeral OS disk provisioning location.", "choices": [ "CacheDisk", "NvmeDisk", @@ -1735,7 +1735,7 @@ "global": false, "takes_value": true, "value_kind": "string", - "help": "Only applicable when used with `--use-unmanaged- disk`. The name to use when creating a new storage account or referencing an existing one. If omitted, an appropriate storage account in the same resource group and location will be used, or a new one will be created.", + "help": "Only applicable when used with `--use-unmanaged-disk`. The name to use when creating a new storage account or referencing an existing one. If omitted, an appropriate storage account in the same resource group and location will be used, or a new one will be created.", "choices": null, "default": null, "values_from": null @@ -1748,7 +1748,7 @@ "global": false, "takes_value": true, "value_kind": "string", - "help": "Only applicable when used with `--use-unmanaged- disk`. Name of the storage container for the VM OS disk.", + "help": "Only applicable when used with `--use-unmanaged-disk`. Name of the storage container for the VM OS disk.", "choices": null, "default": "vhds", "values_from": null @@ -1761,7 +1761,7 @@ "global": false, "takes_value": true, "value_kind": "enum", - "help": "The SKU of the storage account with which to persist VM. Use a singular sku that would be applied across all disks, or specify individual disks. Usage: [--storage- sku SKU | --storage-sku ID=SKU ID=SKU ID=SKU...], where each ID is \"os\" or a 0-indexed lun.", + "help": "The SKU of the storage account with which to persist VM. Use a singular sku that would be applied across all disks, or specify individual disks. Usage: [--storage-sku SKU | --storage-sku ID=SKU ID=SKU ID=SKU...], where each ID is \"os\" or a 0-indexed lun.", "choices": [ "Standard_LRS", "Premium_LRS", diff --git a/internal/metadata/baseline/baseline/vm_delete.json b/internal/metadata/baseline/baseline/vm_delete.json index 46754a3..fd3f61c 100644 --- a/internal/metadata/baseline/baseline/vm_delete.json +++ b/internal/metadata/baseline/baseline/vm_delete.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 15, "unparsed_lines": 0, @@ -42,7 +42,7 @@ "required": false, "group": "Optional Parameters", "global": false, - "takes_value": true, + "takes_value": false, "value_kind": "bool", "help": "Do not wait for the long-running operation to finish.", "choices": [ diff --git a/internal/metadata/baseline/baseline/vm_list.json b/internal/metadata/baseline/baseline/vm_list.json index 172e321..5560f2f 100644 --- a/internal/metadata/baseline/baseline/vm_list.json +++ b/internal/metadata/baseline/baseline/vm_list.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 10, "unparsed_lines": 0, diff --git a/internal/metadata/baseline/baseline/vm_show.json b/internal/metadata/baseline/baseline/vm_show.json index 53b7819..274f1ba 100644 --- a/internal/metadata/baseline/baseline/vm_show.json +++ b/internal/metadata/baseline/baseline/vm_show.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 12, "unparsed_lines": 0, diff --git a/internal/metadata/baseline/baseline/vm_start.json b/internal/metadata/baseline/baseline/vm_start.json index 6501f08..86e6cd7 100644 --- a/internal/metadata/baseline/baseline/vm_start.json +++ b/internal/metadata/baseline/baseline/vm_start.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 13, "unparsed_lines": 0, @@ -18,7 +18,7 @@ "required": false, "group": "Optional Parameters", "global": false, - "takes_value": true, + "takes_value": false, "value_kind": "bool", "help": "Do not wait for the long-running operation to finish.", "choices": [ diff --git a/internal/metadata/baseline/baseline/vm_stop.json b/internal/metadata/baseline/baseline/vm_stop.json index 5c0b7b3..4d41ff1 100644 --- a/internal/metadata/baseline/baseline/vm_stop.json +++ b/internal/metadata/baseline/baseline/vm_stop.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 14, "unparsed_lines": 0, diff --git a/internal/metadata/baseline/baseline/webapp_create.json b/internal/metadata/baseline/baseline/webapp_create.json index 35ee0b4..eb6a90a 100644 --- a/internal/metadata/baseline/baseline/webapp_create.json +++ b/internal/metadata/baseline/baseline/webapp_create.json @@ -5,7 +5,7 @@ "az_version": "embedded", "azform_version": "", "source": "help-parser", - "generated_at": "2026-09-02T17:41:57.364225Z", + "generated_at": "2026-09-26T15:09:13.4103499Z", "parse_health": { "params": 43, "unparsed_lines": 0, @@ -89,8 +89,8 @@ "required": false, "group": "Optional Parameters", "global": false, - "takes_value": false, - "value_kind": "bool", + "takes_value": true, + "value_kind": "list", "help": "Accept system or user assigned identities separated by spaces. Use '[system]' to refer system assigned identity, or a resource id to refer user assigned identity. Check out help for more examples.", "choices": null, "default": null, @@ -122,7 +122,7 @@ "global": false, "takes_value": true, "value_kind": "string", - "help": "The container custom image name and optionally the tag name (e.g., `\u003cregistry- name\u003e/\u003cimage-name\u003e:\u003ctag\u003e`). Note: if --container-registry-url is also provided, use `\u003cimage-name\u003e:\u003ctag\u003e` without the registry name.", + "help": "The container custom image name and optionally the tag name (e.g., `\u003cregistry-name\u003e/\u003cimage-name\u003e:\u003ctag\u003e`). Note: if --container-registry-url is also provided, use `\u003cimage-name\u003e:\u003ctag\u003e` without the registry name.", "choices": null, "default": null, "values_from": null @@ -282,7 +282,7 @@ "required": false, "group": "Optional Parameters", "global": false, - "takes_value": true, + "takes_value": false, "value_kind": "bool", "help": "Enable or disable end-to-end encryption between the Front End and the Workers.", "choices": [ @@ -426,7 +426,7 @@ "required": false, "group": "Optional Parameters", "global": false, - "takes_value": true, + "takes_value": false, "value_kind": "bool", "help": "Enable or disable site-scoped certificates.", "choices": [ diff --git a/internal/metadata/parse.go b/internal/metadata/parse.go index fa87c85..2eb05b6 100644 --- a/internal/metadata/parse.go +++ b/internal/metadata/parse.go @@ -402,6 +402,7 @@ func inferValueKind(p *Parameter, choices []string, defaultValue *string) ValueK return ValueKindKeyValue } if strings.Contains(lowerHelp, "space-separated") || strings.Contains(lowerHelp, "comma-separated") || + strings.Contains(lowerHelp, "separated by spaces") || strings.Contains(lowerHelp, "one or more") || strings.Contains(lowerHelp, "list of") { return ValueKindList } @@ -448,10 +449,14 @@ func isBoolChoiceSet(choices []string) bool { func looksLikeSwitch(name, help string) bool { switch name { + // Only names that are a bare switch on every command belong here. + // --identity, --assign-identity and --service-principal are switches on + // `az login` / `az vm create` but take a value elsewhere (`acr create + // --identity `, `aks create --service-principal `, …); their + // switch forms are recognised from the help text below instead. case "--debug", "--help", "--only-show-errors", "--verbose", "--yes", - "--no-wait", "--service-principal", "--use-device-code", "--identity", - "--assign-identity", "--generate-ssh-keys", "--validate", "--force", - "--skip-subscription-discovery", "--skip-authorization-header": + "--no-wait", "--use-device-code", "--generate-ssh-keys", "--validate", + "--force", "--skip-subscription-discovery", "--skip-authorization-header": return true } if strings.HasPrefix(name, "--no-") { @@ -625,13 +630,33 @@ func cleanupHelp(s string) string { return strings.TrimSpace(s) } +// joinText appends a wrapped continuation line. az's help formatter breaks +// hyphenated words at the hyphen ("comma-" / "separated", "list-" / +// "locations"); rejoining those with a space would corrupt both the help +// text and anything parsed out of it — "comma- separated" misses the list +// heuristic and "Values from: az account list- locations" runs a command +// that does not exist. A trailing hyphen glued to a word, followed by a +// line starting with a lowercase letter, is therefore joined without the +// space. func joinText(a, b string) string { if a == "" { return b } + if isWrappedHyphen(a, b) { + return a + b + } return a + " " + b } +func isWrappedHyphen(a, b string) bool { + if len(a) < 2 || a[len(a)-1] != '-' { + return false + } + prev, _ := utf8.DecodeLastRuneInString(a[:len(a)-1]) + next, _ := utf8.DecodeRuneInString(b) + return (unicode.IsLetter(prev) || unicode.IsDigit(prev)) && unicode.IsLower(next) +} + func leadingSpaces(s string) int { count := 0 for count < len(s) && s[count] == ' ' { diff --git a/testdata/golden/acr-create.json b/testdata/golden/acr-create.json index ca20d3d..020a3c7 100644 --- a/testdata/golden/acr-create.json +++ b/testdata/golden/acr-create.json @@ -202,8 +202,8 @@ "required": false, "group": "Customer managed key Arguments", "global": false, - "takes_value": false, - "value_kind": "bool", + "takes_value": true, + "value_kind": "string", "help": "Use assigned managed identity resource id or name if in the same resource group.", "choices": null, "default": null, diff --git a/testdata/golden/acr-login.json b/testdata/golden/acr-login.json index 4dcb823..9039d50 100644 --- a/testdata/golden/acr-login.json +++ b/testdata/golden/acr-login.json @@ -90,7 +90,7 @@ "global": false, "takes_value": true, "value_kind": "string", - "help": "The tenant suffix in registry login server. You may specify '--suffix tenant' if your registry login server is in the format 'registry- tenant.azurecr.io'. Applicable if you're accessing the registry from a different subscription or you have permission to access images but not the permission to manage the registry resource.", + "help": "The tenant suffix in registry login server. You may specify '--suffix tenant' if your registry login server is in the format 'registry-tenant.azurecr.io'. Applicable if you're accessing the registry from a different subscription or you have permission to access images but not the permission to manage the registry resource.", "choices": null, "default": null, "values_from": null diff --git a/testdata/golden/ad-app-create.json b/testdata/golden/ad-app-create.json index 7ea7917..75728a2 100644 --- a/testdata/golden/ad-app-create.json +++ b/testdata/golden/ad-app-create.json @@ -129,7 +129,7 @@ "global": false, "takes_value": true, "value_kind": "path", - "help": "Application developers can configure optional claims in their Microsoft Entra applications to specify the claims that are sent to their application by the Microsoft security token service. For more information, see https://learn.microsoft.com/azure/active- directory/develop/active-directory-optional-claims. Should be JSON file path or in-line JSON string. See examples for details.", + "help": "Application developers can configure optional claims in their Microsoft Entra applications to specify the claims that are sent to their application by the Microsoft security token service. For more information, see https://learn.microsoft.com/azure/active-directory/develop/active-directory-optional-claims. Should be JSON file path or in-line JSON string. See examples for details.", "choices": null, "default": null, "values_from": null diff --git a/testdata/golden/ad-sp-create-for-rbac.json b/testdata/golden/ad-sp-create-for-rbac.json index 3dcd4b6..67a2d2f 100644 --- a/testdata/golden/ad-sp-create-for-rbac.json +++ b/testdata/golden/ad-sp-create-for-rbac.json @@ -64,7 +64,7 @@ "global": false, "takes_value": true, "value_kind": "list", - "help": "Space-separated list of scopes the service principal's role assignment applies to. e.g., subscriptions/0b1f6471-1bf0-4dda- aec3-111122223333/resourceGroups/myGroup, /subscriptions/0b1f6471-1bf0-4dda-aec3- 111122223333/resourceGroups/myGroup/providers/Microsoft.Co mpute/virtualMachines/myVM.", + "help": "Space-separated list of scopes the service principal's role assignment applies to. e.g., subscriptions/0b1f6471-1bf0-4dda-aec3-111122223333/resourceGroups/myGroup, /subscriptions/0b1f6471-1bf0-4dda-aec3- 111122223333/resourceGroups/myGroup/providers/Microsoft.Co mpute/virtualMachines/myVM.", "choices": null, "default": null, "values_from": null diff --git a/testdata/golden/aks-create.json b/testdata/golden/aks-create.json index 8b0cebe..1a21aee 100644 --- a/testdata/golden/aks-create.json +++ b/testdata/golden/aks-create.json @@ -119,7 +119,7 @@ "global": false, "takes_value": true, "value_kind": "enum", - "help": "Set transit encryption type for ACNS security. Configures pod-to-pod encryption for Cilium-based clusters. Once enabled, all traffic between Cilium managed pods will be encrypted when it leaves the node boundary. Valid values are \"WireGuard\" and \"None\". On cluster creation, this must be used together with \"--enable- acns\".", + "help": "Set transit encryption type for ACNS security. Configures pod-to-pod encryption for Cilium-based clusters. Once enabled, all traffic between Cilium managed pods will be encrypted when it leaves the node boundary. Valid values are \"WireGuard\" and \"None\". On cluster creation, this must be used together with \"--enable-acns\".", "choices": [ "None", "WireGuard" @@ -189,7 +189,7 @@ "global": false, "takes_value": true, "value_kind": "string", - "help": "The ID of a subnet in an existing VNet into which to assign control plane apiserver pods(requires --enable-apiserver-vnet- integration).", + "help": "The ID of a subnet in an existing VNet into which to assign control plane apiserver pods(requires --enable-apiserver-vnet-integration).", "choices": null, "default": null, "values_from": null @@ -220,8 +220,8 @@ "required": false, "group": "Optional Parameters", "global": false, - "takes_value": false, - "value_kind": "bool", + "takes_value": true, + "value_kind": "string", "help": "Specify an existing user assigned identity for control plane's usage in order to manage cluster resource group.", "choices": null, "default": null, @@ -594,7 +594,7 @@ "global": false, "takes_value": true, "value_kind": "string", - "help": "An IP address assigned to the Kubernetes DNS service. This address must be within the Kubernetes service address range specified by \"--service- cidr\". For example, 10.0.0.10.", + "help": "An IP address assigned to the Kubernetes DNS service. This address must be within the Kubernetes service address range specified by \"--service-cidr\". For example, 10.0.0.10.", "choices": null, "default": null, "values_from": null @@ -646,9 +646,9 @@ "required": false, "group": "Optional Parameters", "global": false, - "takes_value": false, - "value_kind": "bool", - "help": "Enable the Kubernetes addons in a comma- separated list. These addons are available: - http_application_routing: configure ingress with automatic public DNS name creation. - monitoring: turn on Log Analytics monitoring. Uses the Log Analytics Default Workspace if it exists, else creates one. Specify \"--workspace-resource-id\" to use an existing workspace. Specify \"--enable-msi-auth-for-monitoring\" to use Managed Identity Auth. Specify \"--enable-syslog\" to enable syslog data collection from nodes. Note MSI must be enabled Specify \"--data-collection-settings\" to configure data collection settings Specify \"--ampls-resource-id\" for private link. Note MSI must be enabled. Specify \"--enable-high-log-scale-mode\" to enable high log scale mode for container logs. Note MSI must be enabled. If monitoring addon is enabled --no-wait argument will have no effect - azure-policy: enable Azure policy. The Azure Policy add-on for AKS enables at-scale enforcements and safeguards on your clusters in a centralized, consistent manner. Learn more at aka.ms/aks/policy. - virtual-node: enable AKS Virtual Node. Requires --aci-subnet-name to provide the name of an existing subnet for the Virtual Node to use. aci-subnet-name must be in the same vnet which is specified by --vnet-subnet-id (required as well). - confcom: enable confcom addon, this will enable SGX device plugin by default. - open-service-mesh: enable Open Service Mesh addon. - azure-keyvault-secrets-provider: enable Azure Keyvault Secrets Provider addon.", + "takes_value": true, + "value_kind": "list", + "help": "Enable the Kubernetes addons in a comma-separated list. These addons are available: - http_application_routing: configure ingress with automatic public DNS name creation. - monitoring: turn on Log Analytics monitoring. Uses the Log Analytics Default Workspace if it exists, else creates one. Specify \"--workspace-resource-id\" to use an existing workspace. Specify \"--enable-msi-auth-for-monitoring\" to use Managed Identity Auth. Specify \"--enable-syslog\" to enable syslog data collection from nodes. Note MSI must be enabled Specify \"--data-collection-settings\" to configure data collection settings Specify \"--ampls-resource-id\" for private link. Note MSI must be enabled. Specify \"--enable-high-log-scale-mode\" to enable high log scale mode for container logs. Note MSI must be enabled. If monitoring addon is enabled --no-wait argument will have no effect - azure-policy: enable Azure policy. The Azure Policy add-on for AKS enables at-scale enforcements and safeguards on your clusters in a centralized, consistent manner. Learn more at aka.ms/aks/policy. - virtual-node: enable AKS Virtual Node. Requires --aci-subnet-name to provide the name of an existing subnet for the Virtual Node to use. aci-subnet-name must be in the same vnet which is specified by --vnet-subnet-id (required as well). - confcom: enable confcom addon, this will enable SGX device plugin by default. - open-service-mesh: enable Open Service Mesh addon. - azure-keyvault-secrets-provider: enable Azure Keyvault Secrets Provider addon.", "choices": null, "default": null, "values_from": null @@ -715,7 +715,7 @@ "global": false, "takes_value": false, "value_kind": "bool", - "help": "Enable Gateway API based ingress on App Routing via Istio without service mesh functionality. This enables an ingress-only version of Istio that reconciles Gateway API resources for App Routing. It does not provide service mesh functionality (e.g. mTLS, traffic management between services). Cannot be used simultaneously with the Istio service mesh add-on (--enable-azure-service- mesh).", + "help": "Enable Gateway API based ingress on App Routing via Istio without service mesh functionality. This enables an ingress-only version of Istio that reconciles Gateway API resources for App Routing. It does not provide service mesh functionality (e.g. mTLS, traffic management between services). Cannot be used simultaneously with the Istio service mesh add-on (--enable-azure-service-mesh).", "choices": null, "default": null, "values_from": null @@ -849,7 +849,7 @@ "global": false, "takes_value": false, "value_kind": "bool", - "help": "Enable collection of Azure Monitor managed Prometheus control plane metrics for managed cluster components (controlplane- apiserver and controlplane-etcd targets by default). Requires Azure Monitor metrics to be enabled (already enabled or via --enable-azure-monitor-metrics).", + "help": "Enable collection of Azure Monitor managed Prometheus control plane metrics for managed cluster components (controlplane-apiserver and controlplane-etcd targets by default). Requires Azure Monitor metrics to be enabled (already enabled or via --enable-azure-monitor-metrics).", "choices": null, "default": null, "values_from": null @@ -862,7 +862,7 @@ "global": false, "takes_value": false, "value_kind": "bool", - "help": "Enable exporting Kubernetes Namespace and Deployment details to the Cost Analysis views in the Azure portal. For more information see aka.ms/aks/docs/cost- analysis.", + "help": "Enable exporting Kubernetes Namespace and Deployment details to the Cost Analysis views in the Azure portal. For more information see aka.ms/aks/docs/cost-analysis.", "choices": null, "default": null, "values_from": null @@ -927,7 +927,7 @@ "global": false, "takes_value": false, "value_kind": "bool", - "help": "Enable High Log Scale Mode for Container Logs. Auto-enabled when --enable-container- network-logs is specified.", + "help": "Enable High Log Scale Mode for Container Logs. Auto-enabled when --enable-container-network-logs is specified.", "choices": [ "false", "true" @@ -1050,7 +1050,7 @@ "global": false, "takes_value": false, "value_kind": "bool", - "help": "Enable secret rotation. Use with azure- keyvault-secrets-provider addon.", + "help": "Enable secret rotation. Use with azure-keyvault-secrets-provider addon.", "choices": null, "default": null, "values_from": null @@ -1407,7 +1407,7 @@ "global": false, "takes_value": true, "value_kind": "list", - "help": "Comma-separated list of additional Kubernetes label keys that will be used in the resource' labels metric. By default the metric contains only name and namespace labels. To include additional labels provide a list of resource names in their plural form and Kubernetes label keys you would like to allow for them (e.g.'=namespa ces=[k8s-label-1,k8s-label- n,...],pods=[app],...)'. A single '*' can be provided per resource instead to allow any labels, but that has severe performance implications (e.g. '=pods=[*]').", + "help": "Comma-separated list of additional Kubernetes label keys that will be used in the resource' labels metric. By default the metric contains only name and namespace labels. To include additional labels provide a list of resource names in their plural form and Kubernetes label keys you would like to allow for them (e.g.'=namespa ces=[k8s-label-1,k8s-label-n,...],pods=[app],...)'. A single '*' can be provided per resource instead to allow any labels, but that has severe performance implications (e.g. '=pods=[*]').", "choices": null, "default": null, "values_from": null @@ -1420,7 +1420,7 @@ "global": false, "takes_value": true, "value_kind": "list", - "help": "Comma-separated list of additional Kubernetes label keys that will be used in the resource' labels metric. By default the metric contains only name and namespace labels. To include additional labels provide a list of resource names in their plural form and Kubernetes label keys you would like to allow for them (e.g. '=namesp aces=[k8s-label-1,k8s-label- n,...],pods=[app],...)'. A single '*' can be provided per resource instead to allow any labels, but that has severe performance implications (e.g. '=pods=[*]').", + "help": "Comma-separated list of additional Kubernetes label keys that will be used in the resource' labels metric. By default the metric contains only name and namespace labels. To include additional labels provide a list of resource names in their plural form and Kubernetes label keys you would like to allow for them (e.g. '=namesp aces=[k8s-label-1,k8s-label-n,...],pods=[app],...)'. A single '*' can be provided per resource instead to allow any labels, but that has severe performance implications (e.g. '=pods=[*]').", "choices": null, "default": null, "values_from": null @@ -1589,7 +1589,7 @@ "help": "Location. You can configure the default location using `az configure --defaults location=`.", "choices": null, "default": null, - "values_from": "az account list- locations" + "values_from": "az account list-locations" }, { "name": "--max-count", @@ -1695,7 +1695,7 @@ "global": false, "takes_value": true, "value_kind": "enum", - "help": "The Kubernetes network plugin to use. Specify \"azure\" for highly scalable networking, \"kubenet\" for IP assignment from subnet NAT- based routing, or \"none\" for no networking configured. Defaults to \"azure\".", + "help": "The Kubernetes network plugin to use. Specify \"azure\" for highly scalable networking, \"kubenet\" for IP assignment from subnet NAT-based routing, or \"none\" for no networking configured. Defaults to \"azure\".", "choices": [ "azure", "kubenet", @@ -1944,7 +1944,7 @@ "global": false, "takes_value": true, "value_kind": "list", - "help": "Expose host ports on the node pool. When specified, format should be a space- separated list of ranges with protocol, eg. `80/TCP 443/TCP 4000-5000/TCP`.", + "help": "Expose host ports on the node pool. When specified, format should be a space-separated list of ranges with protocol, eg. `80/TCP 443/TCP 4000-5000/TCP`.", "choices": null, "default": null, "values_from": null @@ -2210,8 +2210,8 @@ "required": false, "group": "Optional Parameters", "global": false, - "takes_value": false, - "value_kind": "bool", + "takes_value": true, + "value_kind": "string", "help": "Service principal used for authentication to Azure APIs.", "choices": null, "default": null, @@ -2399,7 +2399,7 @@ "global": false, "takes_value": true, "value_kind": "list", - "help": "Comma-separated list of VM sizes. Valid for VirtualMachines node pool only. If `--vm- sizes` not specified but `--node-vm-size` specified, value of `--node-vm-size` will be used. If neither of them specified, defaults to Standard_DS2_v2 for Linux or Standard_D2s_v3 for Windows.", + "help": "Comma-separated list of VM sizes. Valid for VirtualMachines node pool only. If `--vm-sizes` not specified but `--node-vm-size` specified, value of `--node-vm-size` will be used. If neither of them specified, defaults to Standard_DS2_v2 for Linux or Standard_D2s_v3 for Windows.", "choices": null, "default": null, "values_from": null @@ -2509,7 +2509,7 @@ "global": false, "takes_value": true, "value_kind": "string", - "help": "Resource Id of an existing Application Gateway to use with AGIC. Use with ingress- azure addon.", + "help": "Resource Id of an existing Application Gateway to use with AGIC. Use with ingress-azure addon.", "choices": null, "default": null, "values_from": null diff --git a/testdata/golden/aks-upgrade.json b/testdata/golden/aks-upgrade.json index 138c5cb..777247d 100644 --- a/testdata/golden/aks-upgrade.json +++ b/testdata/golden/aks-upgrade.json @@ -186,7 +186,7 @@ "global": false, "takes_value": true, "value_kind": "string", - "help": "Until when the cluster upgradeSettings overrides are effective. It needs to be in a valid date-time format that's within the next 30 days. For example, 2023-04-01T13:00:00Z. Note that if --force-upgrade is set to true and --upgrade-override- until is not set, by default it will be set to 3 days from now.", + "help": "Until when the cluster upgradeSettings overrides are effective. It needs to be in a valid date-time format that's within the next 30 days. For example, 2023-04-01T13:00:00Z. Note that if --force-upgrade is set to true and --upgrade-override-until is not set, by default it will be set to 3 days from now.", "choices": null, "default": null, "values_from": null diff --git a/testdata/golden/appservice-plan-create.json b/testdata/golden/appservice-plan-create.json index 6ca5ae4..ecf6dee 100644 --- a/testdata/golden/appservice-plan-create.json +++ b/testdata/golden/appservice-plan-create.json @@ -49,7 +49,7 @@ "global": false, "takes_value": true, "value_kind": "string", - "help": "Name or ID of the app service environment. If you want to create the app service plan in different subscription than the app service environment, please use the resource ID for --app- service-environment parameter.", + "help": "Name or ID of the app service environment. If you want to create the app service plan in different subscription than the app service environment, please use the resource ID for --app-service-environment parameter.", "choices": null, "default": null, "values_from": null @@ -333,7 +333,7 @@ "global": false, "takes_value": true, "value_kind": "string", - "help": "Storage mount configurations. Provide key-value pairs for `name= source= type= destination- path= credentials-secret-uri=`.", + "help": "Storage mount configurations. Provide key-value pairs for `name= source= type= destination-path= credentials-secret-uri=`.", "choices": null, "default": null, "values_from": null diff --git a/testdata/golden/container-create.json b/testdata/golden/container-create.json index dfcc350..ae0fa00 100644 --- a/testdata/golden/container-create.json +++ b/testdata/golden/container-create.json @@ -148,7 +148,7 @@ "help": "Location. You can configure the default location using `az configure --defaults location=`.", "choices": null, "default": null, - "values_from": "az account list- locations" + "values_from": "az account list-locations" }, { "name": "--memory", diff --git a/testdata/golden/cosmosdb-create.json b/testdata/golden/cosmosdb-create.json index 3b56365..668a9cc 100644 --- a/testdata/golden/cosmosdb-create.json +++ b/testdata/golden/cosmosdb-create.json @@ -45,8 +45,8 @@ "required": false, "group": "Optional Parameters", "global": false, - "takes_value": false, - "value_kind": "bool", + "takes_value": true, + "value_kind": "list", "help": "Assign system or user assigned identities separated by spaces. Use '[system]' to refer system assigned identity.", "choices": null, "default": null, @@ -92,7 +92,7 @@ "global": false, "takes_value": true, "value_kind": "string", - "help": "The primary identity to access key vault in CMK related features. e.g. 'FirstPartyIdentity', 'SystemAssignedIdentity' and more. User-assigned identities are specified in format `UserAssignedIdentity=`.", + "help": "The primary identity to access key vault in CMK related features. e.g. 'FirstPartyIdentity', 'SystemAssignedIdentity' and more. User-assigned identities are specified in format `UserAssignedIdentity=`.", "choices": null, "default": null, "values_from": null diff --git a/testdata/golden/eventhubs-namespace-create.json b/testdata/golden/eventhubs-namespace-create.json index 12e4891..a5a8791 100644 --- a/testdata/golden/eventhubs-namespace-create.json +++ b/testdata/golden/eventhubs-namespace-create.json @@ -183,7 +183,7 @@ "help": "Location. You can configure the default location using `az configure --defaults location=`.", "choices": null, "default": null, - "values_from": "az account list- locations" + "values_from": "az account list-locations" }, { "name": "--max-lag", diff --git a/testdata/golden/functionapp-create.json b/testdata/golden/functionapp-create.json index aa1c18d..f5dc137 100644 --- a/testdata/golden/functionapp-create.json +++ b/testdata/golden/functionapp-create.json @@ -99,8 +99,8 @@ "required": false, "group": "Optional Parameters", "global": false, - "takes_value": false, - "value_kind": "bool", + "takes_value": true, + "value_kind": "list", "help": "Accept system or user assigned identities separated by spaces. Use '[system]' to refer system assigned identity, or a resource id to refer user assigned identity. Check out help for more examples.", "choices": null, "default": null, @@ -116,7 +116,7 @@ "global": false, "takes_value": true, "value_kind": "bool", - "help": "Use this option if you want to configure networking later for an app using network- restricted storage.", + "help": "Use this option if you want to configure networking later for an app using network-restricted storage.", "choices": [ "false", "true" @@ -134,7 +134,7 @@ "global": false, "takes_value": true, "value_kind": "string", - "help": "Geographic location where function app will be hosted. Use `az functionapp list-consumption- locations` to view available locations.", + "help": "Geographic location where function app will be hosted. Use `az functionapp list-consumption-locations` to view available locations.", "choices": null, "default": null, "values_from": null @@ -331,7 +331,7 @@ "global": false, "takes_value": true, "value_kind": "string", - "help": "The deployment storage account authentication value. For the user-assigned managed identity authentication type, this should be the user assigned identity resource id. For the storage account connection string authentication type, this should be the name of the app setting that will contain the storage account connection string. For the system assigned managed- identity authentication type, this parameter is not applicable and should be left empty.", + "help": "The deployment storage account authentication value. For the user-assigned managed identity authentication type, this should be the user assigned identity resource id. For the storage account connection string authentication type, this should be the name of the app setting that will contain the storage account connection string. For the system assigned managed-identity authentication type, this parameter is not applicable and should be left empty.", "choices": null, "default": null, "values_from": null @@ -465,7 +465,7 @@ "global": false, "takes_value": true, "value_kind": "string", - "help": "Geographic location where function app will be hosted. Use `az functionapp list- flexconsumption-locations` to view available locations.", + "help": "Geographic location where function app will be hosted. Use `az functionapp list-flexconsumption-locations` to view available locations.", "choices": null, "default": null, "values_from": null diff --git a/testdata/golden/keyvault-secret-set.json b/testdata/golden/keyvault-secret-set.json index 80bb05f..06b6f98 100644 --- a/testdata/golden/keyvault-secret-set.json +++ b/testdata/golden/keyvault-secret-set.json @@ -89,7 +89,7 @@ "global": false, "takes_value": true, "value_kind": "enum", - "help": "Source file encoding. The value is saved as a tag (`file- encoding=`) and used during download to automatically encode the resulting file.", + "help": "Source file encoding. The value is saved as a tag (`file-encoding=`) and used during download to automatically encode the resulting file.", "choices": [ "ascii", "base64", diff --git a/testdata/golden/network-application-gateway-create.json b/testdata/golden/network-application-gateway-create.json index afc6812..a91eb34 100644 --- a/testdata/golden/network-application-gateway-create.json +++ b/testdata/golden/network-application-gateway-create.json @@ -387,7 +387,7 @@ "global": false, "takes_value": true, "value_kind": "string", - "help": "The certificate name. Default will be `SslCert`.", + "help": "The certificate name. Default will be `SslCert`.", "choices": null, "default": null, "values_from": null @@ -437,8 +437,8 @@ "required": false, "group": "Identity Arguments", "global": false, - "takes_value": false, - "value_kind": "bool", + "takes_value": true, + "value_kind": "string", "help": "Name or ID of the ManagedIdentity Resource.", "choices": null, "default": null, @@ -452,7 +452,7 @@ "global": false, "takes_value": true, "value_kind": "string", - "help": "The application gateway trusted client certificate. Usage: --trusted-client-certificates name=client1 data=client.cer name: Required. Name of the trusted client certificate that is unique within an Application Gateway data: Required. Certificate public data. Multiple trusted client certificates can be specified by using more than one `--trusted- client-certificates` argument. WARNING: Argument '--trusted-client-cert' is in preview and under development. Reference and support levels: https://aka.ms/CLI_refstatus", + "help": "The application gateway trusted client certificate. Usage: --trusted-client-certificates name=client1 data=client.cer name: Required. Name of the trusted client certificate that is unique within an Application Gateway data: Required. Certificate public data. Multiple trusted client certificates can be specified by using more than one `--trusted-client-certificates` argument. WARNING: Argument '--trusted-client-cert' is in preview and under development. Reference and support levels: https://aka.ms/CLI_refstatus", "choices": null, "default": null, "values_from": null @@ -504,7 +504,7 @@ "global": false, "takes_value": true, "value_kind": "string", - "help": "Name or ID of the subnet. Will create resource if it does not exist. If name specified, also specify --vnet- name. If you want to use an existing subnet in other resource group or subscription, please provide the ID instead of the name of the subnet.", + "help": "Name or ID of the subnet. Will create resource if it does not exist. If name specified, also specify --vnet-name. If you want to use an existing subnet in other resource group or subscription, please provide the ID instead of the name of the subnet.", "choices": null, "default": "default", "values_from": null @@ -624,7 +624,7 @@ "global": false, "takes_value": true, "value_kind": "list", - "help": "The application gateway ssl profiles. Usage: --ssl-profile name=MySslProfile client-auth-configuration=True cipher- suites=TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 policy-type=Custom min-protocol-version=TLSv1_0 name: Required. Name of the SSL profile that is unique within an Application Gateway. polic-name: Name of Ssl Policy. policy-type: Type of Ssl Policy. min-protocol-version: Minimum version of Ssl protocol to be supported on application gateway. cipher-suites: Ssl cipher suites to be enabled in the specified order to application gateway. disabled-ssl-protocols: Space-separated list of protocols to disable. trusted-client-certificates: Array of references to application gateway trusted client certificates. client-auth-configuration: Client authentication configuration of the application gateway resource. Multiple ssl profiles can be specified by using more than one `--ssl-profile` argument. WARNING: Argument '--ssl-profile' is in preview and under development. Reference and support levels: https://aka.ms/CLI_refstatus", + "help": "The application gateway ssl profiles. Usage: --ssl-profile name=MySslProfile client-auth-configuration=True cipher-suites=TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 policy-type=Custom min-protocol-version=TLSv1_0 name: Required. Name of the SSL profile that is unique within an Application Gateway. polic-name: Name of Ssl Policy. policy-type: Type of Ssl Policy. min-protocol-version: Minimum version of Ssl protocol to be supported on application gateway. cipher-suites: Ssl cipher suites to be enabled in the specified order to application gateway. disabled-ssl-protocols: Space-separated list of protocols to disable. trusted-client-certificates: Array of references to application gateway trusted client certificates. client-auth-configuration: Client authentication configuration of the application gateway resource. Multiple ssl profiles can be specified by using more than one `--ssl-profile` argument. WARNING: Argument '--ssl-profile' is in preview and under development. Reference and support levels: https://aka.ms/CLI_refstatus", "choices": null, "default": null, "values_from": null diff --git a/testdata/golden/network-bastion-create.json b/testdata/golden/network-bastion-create.json index 14b9860..23a48de 100644 --- a/testdata/golden/network-bastion-create.json +++ b/testdata/golden/network-bastion-create.json @@ -156,7 +156,7 @@ "global": false, "takes_value": true, "value_kind": "string", - "help": "Resource tags. Support shorthand-syntax, json-file and yaml- file. Try \"??\" to show more.", + "help": "Resource tags. Support shorthand-syntax, json-file and yaml-file. Try \"??\" to show more.", "choices": null, "default": null, "values_from": null @@ -169,7 +169,7 @@ "global": false, "takes_value": true, "value_kind": "list", - "help": "A list of availability zones denoting where the resource needs to come from. Support shorthand-syntax, json-file and yaml- file. Try \"??\" to show more.", + "help": "A list of availability zones denoting where the resource needs to come from. Support shorthand-syntax, json-file and yaml-file. Try \"??\" to show more.", "choices": null, "default": null, "values_from": null @@ -302,7 +302,7 @@ "global": false, "takes_value": true, "value_kind": "list", - "help": "[Supported in Developer SKU only] Network ACLs IP rules. Space- separated list of IP addresses.", + "help": "[Supported in Developer SKU only] Network ACLs IP rules. Space-separated list of IP addresses.", "choices": null, "default": null, "values_from": null diff --git a/testdata/golden/network-vnet-create.json b/testdata/golden/network-vnet-create.json index 1102f51..3e21bf0 100644 --- a/testdata/golden/network-vnet-create.json +++ b/testdata/golden/network-vnet-create.json @@ -7,7 +7,7 @@ }, "command": { "command": "network vnet create", - "summary": "Create a virtual network. You may also create a subnet at the same time by specifying a subnet name and (optionally) an address prefix. To learn about how to create a virtual network visit https://learn.microsoft.com/azure/virtual-network/manage-virtual-network#create-a-virtual- network.", + "summary": "Create a virtual network. You may also create a subnet at the same time by specifying a subnet name and (optionally) an address prefix. To learn about how to create a virtual network visit https://learn.microsoft.com/azure/virtual-network/manage-virtual-network#create-a-virtual-network.", "parameters": [ { "name": "--name", @@ -47,7 +47,7 @@ "global": false, "takes_value": true, "value_kind": "list", - "help": "Space-separated list of IP address prefixes for the VNet. If provided, --ipam- allocations should not be specified. Support shorthand-syntax, json-file and yaml-file. Try \"??\" to show more.", + "help": "Space-separated list of IP address prefixes for the VNet. If provided, --ipam-allocations should not be specified. Support shorthand-syntax, json-file and yaml-file. Try \"??\" to show more.", "choices": null, "default": "10.0.0.0/16", "values_from": null @@ -248,7 +248,7 @@ "global": false, "takes_value": true, "value_kind": "keyvalue", - "help": "Space-separated tags: key[=value] [key[=value]...]. Support shorthand- syntax, json-file and yaml-file. Try \"??\" to show more.", + "help": "Space-separated tags: key[=value] [key[=value]...]. Support shorthand-syntax, json-file and yaml-file. Try \"??\" to show more.", "choices": null, "default": null, "values_from": null @@ -328,7 +328,7 @@ "global": false, "takes_value": true, "value_kind": "list", - "help": "A configurable list of summarized gateway prefixes advertised for the virtual network. Support shorthand-syntax, json- file and yaml-file. Try \"??\" to show more. WARNING: Argument '--summarized-gateway-prefixes' is in preview and under development. Reference and support levels: https://aka.ms/CLI_refstatus", + "help": "A configurable list of summarized gateway prefixes advertised for the virtual network. Support shorthand-syntax, json-file and yaml-file. Try \"??\" to show more. WARNING: Argument '--summarized-gateway-prefixes' is in preview and under development. Reference and support levels: https://aka.ms/CLI_refstatus", "choices": null, "default": null, "values_from": null @@ -369,7 +369,7 @@ "global": false, "takes_value": true, "value_kind": "list", - "help": "Space-separated list of address prefixes in CIDR format for the new subnet. If omitted, automatically reserves a /24 (or as large as available) block within the VNet address space. Support shorthand- syntax, json-file and yaml-file. Try \"??\" to show more.", + "help": "Space-separated list of address prefixes in CIDR format for the new subnet. If omitted, automatically reserves a /24 (or as large as available) block within the VNet address space. Support shorthand-syntax, json-file and yaml-file. Try \"??\" to show more.", "choices": null, "default": null, "values_from": null @@ -382,7 +382,7 @@ "global": false, "takes_value": true, "value_kind": "list", - "help": "Manage a list of subnets in a Virtual Network (similar to `az network vnet subnet`). Support shorthand-syntax, json- file and yaml-file. Try \"??\" to show more.", + "help": "Manage a list of subnets in a Virtual Network (similar to `az network vnet subnet`). Support shorthand-syntax, json-file and yaml-file. Try \"??\" to show more.", "choices": null, "default": null, "values_from": null diff --git a/testdata/golden/network-vnet-subnet-create.json b/testdata/golden/network-vnet-subnet-create.json index b7b3467..220a5e5 100644 --- a/testdata/golden/network-vnet-subnet-create.json +++ b/testdata/golden/network-vnet-subnet-create.json @@ -60,7 +60,7 @@ "global": false, "takes_value": true, "value_kind": "list", - "help": "Space-separated list of address prefixes in CIDR format. If provided, --ipam-allocations should not be specified. Support shorthand-syntax, json- file and yaml-file. Try \"??\" to show more.", + "help": "Space-separated list of address prefixes in CIDR format. If provided, --ipam-allocations should not be specified. Support shorthand-syntax, json-file and yaml-file. Try \"??\" to show more.", "choices": null, "default": null, "values_from": null @@ -112,7 +112,7 @@ "global": false, "takes_value": false, "value_kind": "bool", - "help": "Disable private endpoint network policies on the subnet. Please note that it will be replaced by `--private- endpoint-network-policies` soon.", + "help": "Disable private endpoint network policies on the subnet. Please note that it will be replaced by `--private-endpoint-network-policies` soon.", "choices": [ "0", "1", @@ -136,7 +136,7 @@ "global": false, "takes_value": false, "value_kind": "bool", - "help": "Disable private link service network policies on the subnet. Please note that it will be replaced by `--private-link- service-network-policies` soon.", + "help": "Disable private link service network policies on the subnet. Please note that it will be replaced by `--private-link-service-network-policies` soon.", "choices": [ "0", "1", @@ -160,7 +160,7 @@ "global": false, "takes_value": true, "value_kind": "string", - "help": "An array of service endpoints. Support shorthand-syntax, json- file and yaml-file. Try \"??\" to show more.", + "help": "An array of service endpoints. Support shorthand-syntax, json-file and yaml-file. Try \"??\" to show more.", "choices": null, "default": null, "values_from": null @@ -175,7 +175,7 @@ "global": false, "takes_value": true, "value_kind": "list", - "help": "A list of IPAM Pools for allocating IP address prefixes. If provided, --address-prefixes would be ignored by CLI and should not be specified. Support shorthand-syntax, json- file and yaml-file. Try \"??\" to show more.", + "help": "A list of IPAM Pools for allocating IP address prefixes. If provided, --address-prefixes would be ignored by CLI and should not be specified. Support shorthand-syntax, json-file and yaml-file. Try \"??\" to show more.", "choices": null, "default": null, "values_from": null @@ -216,7 +216,7 @@ "global": false, "takes_value": false, "value_kind": "bool", - "help": "Do not wait for the long- running operation to finish.", + "help": "Do not wait for the long-running operation to finish.", "choices": [ "0", "1", @@ -304,10 +304,10 @@ "global": false, "takes_value": true, "value_kind": "list", - "help": "Space-separated list of services allowed private access to this subnet. Support shorthand- syntax, json-file and yaml- file. Try \"??\" to show more.", + "help": "Space-separated list of services allowed private access to this subnet. Support shorthand-syntax, json-file and yaml-file. Try \"??\" to show more.", "choices": null, "default": null, - "values_from": "az network vnet list-endpoint- services" + "values_from": "az network vnet list-endpoint-services" }, { "name": "--sharing-scope", diff --git a/testdata/golden/postgres-flexible-server-create.json b/testdata/golden/postgres-flexible-server-create.json index 0104670..423d313 100644 --- a/testdata/golden/postgres-flexible-server-create.json +++ b/testdata/golden/postgres-flexible-server-create.json @@ -7,7 +7,7 @@ }, "command": { "command": "postgres flexible-server create", - "summary": "Create a PostgreSQL flexible server. Create a PostgreSQL flexible server with custom or default configuration. For more information for network configuration, see - Configure public access https://learn.microsoft.com/azure/postgresql/flexible-server/how-to-manage-firewall-cli - Configure private access https://learn.microsoft.com/azure/postgresql/flexible-server/how-to-manage-virtual-network- cli.", + "summary": "Create a PostgreSQL flexible server. Create a PostgreSQL flexible server with custom or default configuration. For more information for network configuration, see - Configure public access https://learn.microsoft.com/azure/postgresql/flexible-server/how-to-manage-firewall-cli - Configure private access https://learn.microsoft.com/azure/postgresql/flexible-server/how-to-manage-virtual-network-cli.", "parameters": [ { "name": "--admin-display-name", @@ -176,8 +176,8 @@ "required": false, "group": "Optional Parameters", "global": false, - "takes_value": false, - "value_kind": "bool", + "takes_value": true, + "value_kind": "string", "help": "The name or resource identifier of the user assigned identity for data encryption.", "choices": null, "default": null, diff --git a/testdata/golden/redis-create.json b/testdata/golden/redis-create.json index c30e905..72c2bda 100644 --- a/testdata/golden/redis-create.json +++ b/testdata/golden/redis-create.json @@ -22,7 +22,7 @@ "help": "Location. You can configure the default location using `az configure --defaults location=`.", "choices": null, "default": null, - "values_from": "az account list- locations" + "values_from": "az account list-locations" }, { "name": "--name", diff --git a/testdata/golden/rest.json b/testdata/golden/rest.json index 7b4163f..afa58dc 100644 --- a/testdata/golden/rest.json +++ b/testdata/golden/rest.json @@ -7,7 +7,7 @@ }, "command": { "command": "rest", - "summary": "Invoke a custom request. This command automatically authenticates using the logged-in credential: If Authorization header is not set, it attaches header `Authorization: Bearer `, where `` is retrieved from AAD. The target resource of the token is derived from --url if --url starts with an endpoint from `az cloud show --query endpoints`. You may also use --resource for a custom resource. If Content-Type header is not set and --body is a valid JSON string, Content-Type header will default to application/json. For passing JSON in PowerShell, see https://github.com/Azure/azure-cli/blob/dev/doc/quoting- issues-with-powershell.md.", + "summary": "Invoke a custom request. This command automatically authenticates using the logged-in credential: If Authorization header is not set, it attaches header `Authorization: Bearer `, where `` is retrieved from AAD. The target resource of the token is derived from --url if --url starts with an endpoint from `az cloud show --query endpoints`. You may also use --resource for a custom resource. If Content-Type header is not set and --body is a valid JSON string, Content-Type header will default to application/json. For passing JSON in PowerShell, see https://github.com/Azure/azure-cli/blob/dev/doc/quoting-issues-with-powershell.md.", "parameters": [ { "name": "--uri", @@ -35,7 +35,7 @@ "global": false, "takes_value": true, "value_kind": "path", - "help": "Request body. Use @{file} to load from a file. For quoting issues in different terminals, see https://github.com/Azure/azure- cli/blob/dev/doc/use_cli_effectively.md#quoting-issues.", + "help": "Request body. Use @{file} to load from a file. For quoting issues in different terminals, see https://github.com/Azure/azure-cli/blob/dev/doc/use_cli_effectively.md#quoting-issues.", "choices": null, "default": null, "values_from": null diff --git a/testdata/golden/role-assignment-create.json b/testdata/golden/role-assignment-create.json index 449991b..c8e49d1 100644 --- a/testdata/golden/role-assignment-create.json +++ b/testdata/golden/role-assignment-create.json @@ -30,7 +30,7 @@ "global": false, "takes_value": true, "value_kind": "string", - "help": "Scope at which the role assignment or definition applies to, e.g., /subscriptions/0b1f6471-1bf0-4dda-aec3-111122223333, /subscriptions/0b1f6471-1bf0-4dda- aec3-111122223333/resourceGroups/myGroup, or /subscriptions/0b1f6471-1bf0-4dda-aec3- 111122223333/resourceGroups/myGroup/providers/Microsoft.Compute/ virtualMachines/myVM.", + "help": "Scope at which the role assignment or definition applies to, e.g., /subscriptions/0b1f6471-1bf0-4dda-aec3-111122223333, /subscriptions/0b1f6471-1bf0-4dda-aec3-111122223333/resourceGroups/myGroup, or /subscriptions/0b1f6471-1bf0-4dda-aec3- 111122223333/resourceGroups/myGroup/providers/Microsoft.Compute/ virtualMachines/myVM.", "choices": null, "default": null, "values_from": null diff --git a/testdata/golden/sql-db-create.json b/testdata/golden/sql-db-create.json index d143444..b0583fe 100644 --- a/testdata/golden/sql-db-create.json +++ b/testdata/golden/sql-db-create.json @@ -62,7 +62,7 @@ "required": false, "group": "Optional Parameters", "global": false, - "takes_value": false, + "takes_value": true, "value_kind": "bool", "help": "Assign identity for database.", "choices": [ diff --git a/testdata/golden/sql-server-create.json b/testdata/golden/sql-server-create.json index b77f8be..3a932e2 100644 --- a/testdata/golden/sql-server-create.json +++ b/testdata/golden/sql-server-create.json @@ -216,7 +216,7 @@ "help": "Location. You can configure the default location using `az configure --defaults location=`.", "choices": null, "default": null, - "values_from": "az account list- locations" + "values_from": "az account list-locations" }, { "name": "--minimal-tls-version", diff --git a/testdata/golden/storage-account-create.json b/testdata/golden/storage-account-create.json index 72936c4..f9a7cab 100644 --- a/testdata/golden/storage-account-create.json +++ b/testdata/golden/storage-account-create.json @@ -291,7 +291,7 @@ "global": false, "takes_value": true, "value_kind": "enum", - "help": "Set the encryption key type for Queue service. \"Account\": Queue will be encrypted with account-scoped encryption key. \"Service\": Queue will always be encrypted with service- scoped keys. Currently the default encryption key type is \"Service\".", + "help": "Set the encryption key type for Queue service. \"Account\": Queue will be encrypted with account-scoped encryption key. \"Service\": Queue will always be encrypted with service-scoped keys. Currently the default encryption key type is \"Service\".", "choices": [ "Account", "Service" @@ -309,7 +309,7 @@ "global": false, "takes_value": true, "value_kind": "enum", - "help": "Set the encryption key type for Table service. \"Account\": Table will be encrypted with account-scoped encryption key. \"Service\": Table will always be encrypted with service- scoped keys. Currently the default encryption key type is \"Service\".", + "help": "Set the encryption key type for Table service. \"Account\": Table will be encrypted with account-scoped encryption key. \"Service\": Table will always be encrypted with service-scoped keys. Currently the default encryption key type is \"Service\".", "choices": [ "Account", "Service" @@ -614,7 +614,7 @@ "global": false, "takes_value": true, "value_kind": "enum", - "help": "Defines the mode of the policy. Disabled state disables the policy, Unlocked state allows increase and decrease of immutability retention time and also allows toggling allow- protected-append-write property, Locked state only allows the increase of the immutability retention time. A policy can only be created in a Disabled or Unlocked state and can be toggled between the two states. Only a policy in an Unlocked state can transition to a Locked state which cannot be reverted.", + "help": "Defines the mode of the policy. Disabled state disables the policy, Unlocked state allows increase and decrease of immutability retention time and also allows toggling allow-protected-append-write property, Locked state only allows the increase of the immutability retention time. A policy can only be created in a Disabled or Unlocked state and can be toggled between the two states. Only a policy in an Unlocked state can transition to a Locked state which cannot be reverted.", "choices": [ "Disabled", "Locked", @@ -686,7 +686,7 @@ "global": false, "takes_value": true, "value_kind": "string", - "help": "Specify the security identifier (SID). Required when --enable-files- adds is set to True.", + "help": "Specify the security identifier (SID). Required when --enable-files-adds is set to True.", "choices": null, "default": null, "values_from": null @@ -699,7 +699,7 @@ "global": false, "takes_value": true, "value_kind": "string", - "help": "Specify the Active Directory forest to get. Required when --enable-files- adds is set to True.", + "help": "Specify the Active Directory forest to get. Required when --enable-files-adds is set to True.", "choices": null, "default": null, "values_from": null @@ -790,7 +790,7 @@ "global": false, "takes_value": false, "value_kind": "bool", - "help": "Enable Azure Files Active Directory Domain Service Authentication for storage account. When --enable-files- adds is set to true, Azure Active Directory Properties arguments must be provided.", + "help": "Enable Azure Files Active Directory Domain Service Authentication for storage account. When --enable-files-adds is set to true, Azure Active Directory Properties arguments must be provided.", "choices": [ "false", "true" diff --git a/testdata/golden/storage-account-update.json b/testdata/golden/storage-account-update.json index 9a7e212..f81ffb3 100644 --- a/testdata/golden/storage-account-update.json +++ b/testdata/golden/storage-account-update.json @@ -462,7 +462,7 @@ "global": false, "takes_value": true, "value_kind": "enum", - "help": "Defines the mode of the policy. Disabled state disables the policy, Unlocked state allows increase and decrease of immutability retention time and also allows toggling allow- protected-append-write property, Locked state only allows the increase of the immutability retention time. A policy can only be created in a Disabled or Unlocked state and can be toggled between the two states. Only a policy in an Unlocked state can transition to a Locked state which cannot be reverted.", + "help": "Defines the mode of the policy. Disabled state disables the policy, Unlocked state allows increase and decrease of immutability retention time and also allows toggling allow-protected-append-write property, Locked state only allows the increase of the immutability retention time. A policy can only be created in a Disabled or Unlocked state and can be toggled between the two states. Only a policy in an Unlocked state can transition to a Locked state which cannot be reverted.", "choices": [ "Disabled", "Locked", @@ -534,7 +534,7 @@ "global": false, "takes_value": true, "value_kind": "string", - "help": "Specify the security identifier (SID). Required when --enable-files- adds is set to True.", + "help": "Specify the security identifier (SID). Required when --enable-files-adds is set to True.", "choices": null, "default": null, "values_from": null @@ -547,7 +547,7 @@ "global": false, "takes_value": true, "value_kind": "string", - "help": "Specify the Active Directory forest to get. Required when --enable-files- adds is set to True.", + "help": "Specify the Active Directory forest to get. Required when --enable-files-adds is set to True.", "choices": null, "default": null, "values_from": null @@ -638,7 +638,7 @@ "global": false, "takes_value": false, "value_kind": "bool", - "help": "Enable Azure Files Active Directory Domain Service Authentication for storage account. When --enable-files- adds is set to true, Azure Active Directory Properties arguments must be provided.", + "help": "Enable Azure Files Active Directory Domain Service Authentication for storage account. When --enable-files-adds is set to true, Azure Active Directory Properties arguments must be provided.", "choices": [ "false", "true" @@ -913,7 +913,7 @@ "global": false, "takes_value": true, "value_kind": "list", - "help": "One or more resource IDs (space- delimited). It should be a complete resource ID containing all information of 'Resource Id' arguments. You should provide either --ids or other 'Resource Id' arguments.", + "help": "One or more resource IDs (space-delimited). It should be a complete resource ID containing all information of 'Resource Id' arguments. You should provide either --ids or other 'Resource Id' arguments.", "choices": null, "default": null, "values_from": null diff --git a/testdata/golden/storage-container-create.json b/testdata/golden/storage-container-create.json index 2b2da54..4dd2b8b 100644 --- a/testdata/golden/storage-container-create.json +++ b/testdata/golden/storage-container-create.json @@ -7,7 +7,7 @@ }, "command": { "command": "storage container create", - "summary": "Create a container in a storage account. By default, container data is private (\"off\") to the account owner. Use \"blob\" to allow public read access for blobs. Use \"container\" to allow public read and list access to the entire container. You can configure the --public-access using `az storage container set- permission -n CONTAINER_NAME --public-access blob/container/off`.", + "summary": "Create a container in a storage account. By default, container data is private (\"off\") to the account owner. Use \"blob\" to allow public read access for blobs. Use \"container\" to allow public read and list access to the entire container. You can configure the --public-access using `az storage container set-permission -n CONTAINER_NAME --public-access blob/container/off`.", "parameters": [ { "name": "--name", diff --git a/testdata/golden/vm-create.json b/testdata/golden/vm-create.json index 6310841..43bf32e 100644 --- a/testdata/golden/vm-create.json +++ b/testdata/golden/vm-create.json @@ -7,7 +7,7 @@ }, "command": { "command": "vm create", - "summary": "Create an Azure Virtual Machine. For an end-to-end tutorial, see https://learn.microsoft.com/azure/virtual- machines/linux/quick-create-cli.", + "summary": "Create an Azure Virtual Machine. For an end-to-end tutorial, see https://learn.microsoft.com/azure/virtual-machines/linux/quick-create-cli.", "parameters": [ { "name": "--name", @@ -62,7 +62,7 @@ "global": false, "takes_value": true, "value_kind": "bool", - "help": "Specify whether to implicitly install the ProxyAgent Extension. This option is currently applicable only for Linux OS. Use with --enable-proxy- agent.", + "help": "Specify whether to implicitly install the ProxyAgent Extension. This option is currently applicable only for Linux OS. Use with --enable-proxy-agent.", "choices": [ "false", "true" @@ -116,7 +116,7 @@ "global": false, "takes_value": true, "value_kind": "bool", - "help": "Specifies if Scheduled Events should be auto- approved when all instances are down. Its default value is true.", + "help": "Specifies if Scheduled Events should be auto-approved when all instances are down. Its default value is true.", "choices": [ "false", "true" @@ -186,7 +186,7 @@ "global": false, "takes_value": true, "value_kind": "int", - "help": "Number of virtual machines to create. Value range is [2, 250], inclusive. Don't specify this parameter if you want to create a normal single VM. The VMs are created in parallel. The output of this command is an array of VMs instead of one single VM. Each VM has its own public IP, NIC. VNET and NSG are shared. It is recommended that no existing public IP, NIC, VNET and NSG are in resource group. When --count is specified, --attach-data- disks, --attach-os- disk, --boot- diagnostics- storage, --computer-name, --host, --host- group, --nics, --os-disk-name, --private-ip- address, --public- ip-address, --public-ip- address-dns-name, --storage-account, --storage- container-name, --subnet, --use- unmanaged-disk, --vnet-name are not allowed. WARNING: Argument '--count' is in preview and under development. Reference and support levels: https://aka.ms/CLI_refstatus", + "help": "Number of virtual machines to create. Value range is [2, 250], inclusive. Don't specify this parameter if you want to create a normal single VM. The VMs are created in parallel. The output of this command is an array of VMs instead of one single VM. Each VM has its own public IP, NIC. VNET and NSG are shared. It is recommended that no existing public IP, NIC, VNET and NSG are in resource group. When --count is specified, --attach-data-disks, --attach-os-disk, --boot-diagnostics-storage, --computer-name, --host, --host-group, --nics, --os-disk-name, --private-ip-address, --public-ip-address, --public-ip-address-dns-name, --storage-account, --storage-container-name, --subnet, --use-unmanaged-disk, --vnet-name are not allowed. WARNING: Argument '--count' is in preview and under development. Reference and support levels: https://aka.ms/CLI_refstatus", "choices": null, "default": null, "values_from": null @@ -199,7 +199,7 @@ "global": false, "takes_value": true, "value_kind": "path", - "help": "Custom init script file or text (cloud-init, cloud- config, etc..).", + "help": "Custom init script file or text (cloud-init, cloud-config, etc..).", "choices": null, "default": null, "values_from": null @@ -312,7 +312,7 @@ "global": false, "takes_value": true, "value_kind": "bool", - "help": "Patch VMs without requiring a reboot. --enable-agent must be set and --patch- mode must be set to AutomaticByPlatform.", + "help": "Patch VMs without requiring a reboot. --enable-agent must be set and --patch-mode must be set to AutomaticByPlatform.", "choices": [ "false", "true" @@ -454,7 +454,7 @@ "global": false, "takes_value": true, "value_kind": "list", - "help": "If \"--zone- placement-policy\" is set to \"Any\", availability zone selected by the system must not be present in the list of availability zones passed with \"excludeZones\". If \"--exclude-zones\" is not provided, all availability zones in region will be considered for selection.", + "help": "If \"--zone-placement-policy\" is set to \"Any\", availability zone selected by the system must not be present in the list of availability zones passed with \"excludeZones\". If \"--exclude-zones\" is not provided, all availability zones in region will be considered for selection.", "choices": null, "default": null, "values_from": null @@ -467,10 +467,10 @@ "global": false, "takes_value": true, "value_kind": "string", - "help": "The name of the operating system image as a URN alias, URN, custom image name or ID, custom image version ID, or VHD blob URI. In addition, it also supports shared gallery image. Please use the image alias including the version of the distribution you want to use. For example: please use Debian11 instead of Debian.' This parameter is required unless using `--attach-os- disk.` Valid URN format: \"Publisher: Offer:Sku:Version\". For more information, see ht tps://learn.microso ft.com/azure/virtua l- machines/linux/cli- ps-findimage.", + "help": "The name of the operating system image as a URN alias, URN, custom image name or ID, custom image version ID, or VHD blob URI. In addition, it also supports shared gallery image. Please use the image alias including the version of the distribution you want to use. For example: please use Debian11 instead of Debian.' This parameter is required unless using `--attach-os-disk.` Valid URN format: \"Publisher: Offer:Sku:Version\". For more information, see ht tps://learn.microso ft.com/azure/virtua l-machines/linux/cli-ps-findimage.", "choices": null, "default": null, - "values_from": "az vm image list, az vm image show, az sig image-version show- shared" + "values_from": "az vm image list, az vm image show, az sig image-version show-shared" }, { "name": "--imds-access-control-profile-reference-id", @@ -511,7 +511,7 @@ "global": false, "takes_value": true, "value_kind": "list", - "help": "If \"--zone- placement-policy\" is set to \"Any\", availability zone selected by the system must be present in the list of availability zones passed with \" --include-zones\". If \"--include- zones\" is not provided, all availability zones in region will be considered for selection.", + "help": "If \"--zone-placement-policy\" is set to \"Any\", availability zone selected by the system must be present in the list of availability zones passed with \" --include-zones\". If \"--include-zones\" is not provided, all availability zones in region will be considered for selection.", "choices": null, "default": null, "values_from": null @@ -537,7 +537,7 @@ "global": false, "takes_value": true, "value_kind": "enum", - "help": "Specifies that the Windows image or disk was licensed on-premises. To enable Azure Hybrid Benefit for Windows Server, use 'Windows_Server'. To enable Multi- tenant Hosting Rights for Windows 10, use 'Windows_Client'. For more information see the Azure Windows VM online docs.", + "help": "Specifies that the Windows image or disk was licensed on-premises. To enable Azure Hybrid Benefit for Windows Server, use 'Windows_Server'. To enable Multi-tenant Hosting Rights for Windows 10, use 'Windows_Client'. For more information see the Azure Windows VM online docs.", "choices": [ "None", "RHEL_BASE", @@ -640,7 +640,7 @@ "global": false, "takes_value": true, "value_kind": "enum", - "help": "Mode of in-guest patching to IaaS virtual machine. Allowed values for Windows VM: AutomaticByOS, Auto maticByPlatform, Manual. Allowed values for Linux VM: AutomaticByPlat form, ImageDefault. Manual - You control the application of patches to a virtual machine. You do this by applying patches manually inside the VM. In this mode, automatic updates are disabled; the paramater --enable- auto-update must be false. AutomaticByOS - The virtual machine will automatically be updated by the OS. The parameter --enable-auto- update must be true. AutomaticByPlatform - the virtual machine will automatically updated by the OS. ImageDefault - The virtual machine's default patching configuration is used. The parameter --enable-agent and --enable-auto- update must be true.", + "help": "Mode of in-guest patching to IaaS virtual machine. Allowed values for Windows VM: AutomaticByOS, Auto maticByPlatform, Manual. Allowed values for Linux VM: AutomaticByPlat form, ImageDefault. Manual - You control the application of patches to a virtual machine. You do this by applying patches manually inside the VM. In this mode, automatic updates are disabled; the paramater --enable-auto-update must be false. AutomaticByOS - The virtual machine will automatically be updated by the OS. The parameter --enable-auto-update must be true. AutomaticByPlatform - the virtual machine will automatically updated by the OS. ImageDefault - The virtual machine's default patching configuration is used. The parameter --enable-agent and --enable-auto-update must be true.", "choices": [ "AutomaticByOS", "Auto maticByPlatform", @@ -719,7 +719,7 @@ "global": false, "takes_value": true, "value_kind": "string", - "help": "Specifies the api- version to determine which Scheduled Events configuration schema version will be delivered.", + "help": "Specifies the api-version to determine which Scheduled Events configuration schema version will be delivered.", "choices": null, "default": null, "values_from": null @@ -827,7 +827,7 @@ "global": false, "takes_value": true, "value_kind": "string", - "help": "Specify the ratio of vCPU to physical core. Setting this property to 1 also means that hyper- threading is disabled.", + "help": "Specify the ratio of vCPU to physical core. Setting this property to 1 also means that hyper-threading is disabled.", "choices": null, "default": null, "values_from": null @@ -1054,7 +1054,7 @@ "global": false, "takes_value": true, "value_kind": "string", - "help": "Name or resource ID of the dedicated host group that the VM will reside in. --host and --host- group can't be used together. WARNING: Argument '--host-group' is in preview and under development. Reference and support levels: https://aka.ms/CLI_refstatus", + "help": "Name or resource ID of the dedicated host group that the VM will reside in. --host and --host-group can't be used together. WARNING: Argument '--host-group' is in preview and under development. Reference and support levels: https://aka.ms/CLI_refstatus", "choices": null, "default": null, "values_from": null @@ -1091,8 +1091,8 @@ "required": false, "group": "Managed Service Identity Arguments", "global": false, - "takes_value": false, - "value_kind": "bool", + "takes_value": true, + "value_kind": "list", "help": "Accept system or user assigned identities separated by spaces. Use '[system]' to refer system assigned identity, or a resource id to refer user assigned identity. Check out help for more examples.", "choices": null, "default": null, @@ -1353,7 +1353,7 @@ "global": false, "takes_value": true, "value_kind": "string", - "help": "The name of the subnet when creating a new VNet or referencing an existing one. Can also reference an existing subnet by ID. If both vnet- name and subnet are omitted, an appropriate VNet and subnet will be selected automatically, or a new one will be created.", + "help": "The name of the subnet when creating a new VNet or referencing an existing one. Can also reference an existing subnet by ID. If both vnet-name and subnet are omitted, an appropriate VNet and subnet will be selected automatically, or a new one will be created.", "choices": null, "default": null, "values_from": null @@ -1551,7 +1551,7 @@ "global": false, "takes_value": true, "value_kind": "enum", - "help": "Only applicable when used with `--ephemeral-os- disk`. Allows you to choose the Ephemeral OS disk provisioning location.", + "help": "Only applicable when used with `--ephemeral-os-disk`. Allows you to choose the Ephemeral OS disk provisioning location.", "choices": [ "CacheDisk", "NvmeDisk", @@ -1732,7 +1732,7 @@ "global": false, "takes_value": true, "value_kind": "string", - "help": "Only applicable when used with `--use-unmanaged- disk`. The name to use when creating a new storage account or referencing an existing one. If omitted, an appropriate storage account in the same resource group and location will be used, or a new one will be created.", + "help": "Only applicable when used with `--use-unmanaged-disk`. The name to use when creating a new storage account or referencing an existing one. If omitted, an appropriate storage account in the same resource group and location will be used, or a new one will be created.", "choices": null, "default": null, "values_from": null @@ -1745,7 +1745,7 @@ "global": false, "takes_value": true, "value_kind": "string", - "help": "Only applicable when used with `--use-unmanaged- disk`. Name of the storage container for the VM OS disk.", + "help": "Only applicable when used with `--use-unmanaged-disk`. Name of the storage container for the VM OS disk.", "choices": null, "default": "vhds", "values_from": null @@ -1758,7 +1758,7 @@ "global": false, "takes_value": true, "value_kind": "enum", - "help": "The SKU of the storage account with which to persist VM. Use a singular sku that would be applied across all disks, or specify individual disks. Usage: [--storage- sku SKU | --storage-sku ID=SKU ID=SKU ID=SKU...], where each ID is \"os\" or a 0-indexed lun.", + "help": "The SKU of the storage account with which to persist VM. Use a singular sku that would be applied across all disks, or specify individual disks. Usage: [--storage-sku SKU | --storage-sku ID=SKU ID=SKU ID=SKU...], where each ID is \"os\" or a 0-indexed lun.", "choices": [ "Standard_LRS", "Premium_LRS", diff --git a/testdata/golden/webapp-create.json b/testdata/golden/webapp-create.json index 9d6840d..691a5e8 100644 --- a/testdata/golden/webapp-create.json +++ b/testdata/golden/webapp-create.json @@ -86,8 +86,8 @@ "required": false, "group": "Optional Parameters", "global": false, - "takes_value": false, - "value_kind": "bool", + "takes_value": true, + "value_kind": "list", "help": "Accept system or user assigned identities separated by spaces. Use '[system]' to refer system assigned identity, or a resource id to refer user assigned identity. Check out help for more examples.", "choices": null, "default": null, @@ -119,7 +119,7 @@ "global": false, "takes_value": true, "value_kind": "string", - "help": "The container custom image name and optionally the tag name (e.g., `/:`). Note: if --container-registry-url is also provided, use `:` without the registry name.", + "help": "The container custom image name and optionally the tag name (e.g., `/:`). Note: if --container-registry-url is also provided, use `:` without the registry name.", "choices": null, "default": null, "values_from": null From 6e4edfc07f8fe7437f570049288088e7463c16a6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 26 Sep 2026 15:12:51 +0000 Subject: [PATCH 03/12] render: quote #, ~, braces, ^, leading = and control chars Unquoted they trigger comments, tilde/brace expansion or zsh globbing and '=cmd' expansion, so az would not receive the value as typed. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Dza8X5c3XGHUjejpqGLR8q --- internal/render/render.go | 16 +++++++++++++++- internal/render/render_test.go | 8 ++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/internal/render/render.go b/internal/render/render.go index b3ea280..b5a79ed 100644 --- a/internal/render/render.go +++ b/internal/render/render.go @@ -25,14 +25,28 @@ func EscapePOSIX(s string) string { return "'" + strings.ReplaceAll(s, "'", `'\''`) + "'" } +// needsQuoting reports whether s must be quoted to reach az as one +// unchanged argument in bash and zsh. Besides the obvious metacharacters it +// covers: '#' (starts a comment at the beginning of a word), '~' (tilde +// expansion), '{' / '}' (brace expansion: `{a,b}` becomes two arguments), +// '^' (zsh EXTENDED_GLOB negation), a leading '=' (zsh `=cmd` expands to +// the command's path) and control characters, which are invisible in the +// rendered command. func needsQuoting(s string) bool { + if s[0] == '=' { + return true + } for _, r := range s { switch r { case ' ', '\t', '\n', '$', '`', '\\', '"', '\'', '*', '?', '[', ']', '!', '&', '|', ';', '<', '>', - '(', ')': + '(', ')', + '#', '~', '{', '}', '^': + return true + } + if r < 0x20 || r == 0x7f { return true } } diff --git a/internal/render/render_test.go b/internal/render/render_test.go index f9e03cd..ce48476 100644 --- a/internal/render/render_test.go +++ b/internal/render/render_test.go @@ -26,6 +26,14 @@ func TestEscapePOSIX(t *testing.T) { {"a?b", "'a?b'"}, {"a[b]", "'a[b]'"}, {"a\\b", `'a\b'`}, + {"#tag", "'#tag'"}, + {"~/file", "'~/file'"}, + {"{a,b}", "'{a,b}'"}, + {"^neg", "'^neg'"}, + {"=value", "'=value'"}, + {"key=value", "key=value"}, + {"a\x01b", "'a\x01b'"}, + {"@file.json", "@file.json"}, } for _, tc := range cases { t.Run(tc.in, func(t *testing.T) { From f5b00b160875ac250cafb2fe0bfd1cc97c786341 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 26 Sep 2026 15:12:51 +0000 Subject: [PATCH 04/12] vars: require a separator for the _NAME/_LOCATION/... suffix heuristic The check ran on the separator-folded name, so USERNAME, HOSTNAME and LOGNAME all bound to --name. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Dza8X5c3XGHUjejpqGLR8q --- internal/vars/match.go | 9 +++++++-- internal/vars/match_test.go | 13 +++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/internal/vars/match.go b/internal/vars/match.go index 6092a65..5c19aaa 100644 --- a/internal/vars/match.go +++ b/internal/vars/match.go @@ -68,17 +68,22 @@ func matchByName(v Variable, params []metadata.Parameter) string { } // Suffix shortcut: variable ends in _NAME / _LOCATION / _GROUP / _SKU // maps to --name / --location / --resource-group / --sku. The longest - // matching suffix wins. + // matching suffix wins. The separator is required and checked on the + // raw name: on the normalised form (separators folded away) every + // USERNAME, HOSTNAME and LOGNAME in the environment would bind to + // --name. suffixMap := []struct{ suffix, param string }{ {"name", "--name"}, {"location", "--location"}, {"group", "--resource-group"}, {"sku", "--sku"}, } + lower := strings.ToLower(v.Name) bestLen := -1 var bestParam string for _, s := range suffixMap { - if strings.HasSuffix(vn, s.suffix) && len(s.suffix) > bestLen { + hasSuffix := strings.HasSuffix(lower, "_"+s.suffix) || strings.HasSuffix(lower, "-"+s.suffix) + if hasSuffix && len(s.suffix) > bestLen { bestLen = len(s.suffix) bestParam = s.param } diff --git a/internal/vars/match_test.go b/internal/vars/match_test.go index b7ca48f..b017054 100644 --- a/internal/vars/match_test.go +++ b/internal/vars/match_test.go @@ -51,6 +51,19 @@ func TestMatchNameSuffix(t *testing.T) { } } +// Environment names that merely end in the letters "name" (no separator) +// are not a _NAME suffix and must not bind to --name. +func TestMatchNameSuffixNeedsSeparator(t *testing.T) { + in := []vars.Variable{ + {Name: "USERNAME", Value: "alice"}, + {Name: "HOSTNAME", Value: "laptop"}, + {Name: "LOGNAME", Value: "alice"}, + } + if got := vars.MatchVariables(in, matchParams); len(got) != 0 { + t.Errorf("got %+v, want no matches", got) + } +} + func TestMatchValueSignal(t *testing.T) { // Unusual var name, but value matches --sku's choices. in := []vars.Variable{{Name: "WEIRD_VAR", Value: "Standard_LRS"}} From da4adc6b9ce4e5eb46605c84fdfece7f45bffde5 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 26 Sep 2026 15:12:51 +0000 Subject: [PATCH 05/12] shell: treat a cursor at a segment's end as inside it With the widget's usual end-of-line cursor, 'az a && az b' opened the form for the first command instead of the one being typed. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Dza8X5c3XGHUjejpqGLR8q --- internal/shell/match.go | 6 ++++-- internal/shell/segment.go | 7 +++++-- internal/shell/segment_test.go | 16 ++++++++++++++++ 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/internal/shell/match.go b/internal/shell/match.go index af3c5b6..2eb5d4f 100644 --- a/internal/shell/match.go +++ b/internal/shell/match.go @@ -194,12 +194,14 @@ func MatchParams(raw RawBuffer, params []metadata.Parameter) ParsedBuffer { // Cursor-to-param mapping (top-level only; inline segments have CursorByte==-1). // tok is the flag token. If a value token was consumed (inlineValue=="" and // RawValue!=""), i has been incremented and tokens[i] is the value token. + // End bounds are inclusive so a cursor just past the value (end of + // line, the widget's usual position) still focuses that param. if raw.CursorByte >= 0 { - if raw.CursorByte >= tok.Start && raw.CursorByte < tok.End { + if raw.CursorByte >= tok.Start && raw.CursorByte <= tok.End { pb.CursorParam = len(pb.Params) } else if inlineValue == "" && pp.RawValue != "" { valTok := tokens[i] - if raw.CursorByte >= valTok.Start && raw.CursorByte < valTok.End { + if raw.CursorByte >= valTok.Start && raw.CursorByte <= valTok.End { pb.CursorParam = len(pb.Params) } } diff --git a/internal/shell/segment.go b/internal/shell/segment.go index d9f2e42..13fcdf2 100644 --- a/internal/shell/segment.go +++ b/internal/shell/segment.go @@ -132,10 +132,13 @@ func extractSegment(tokens []Token, azIdx int, line string) (azSegment, int) { } // selectTarget picks the az segment that contains cursor, or the first segment -// when cursor is outside all segments. +// when cursor is outside all segments. The end bound is inclusive: a cursor +// sitting right after a segment's last character — the usual spot once the +// user has finished typing it, e.g. end of line in `az a && az b` — belongs +// to that segment. func selectTarget(segs []azSegment, cursor int) *azSegment { for i := range segs { - if cursor >= segs[i].outerStart && cursor < segs[i].outerEnd { + if cursor >= segs[i].outerStart && cursor <= segs[i].outerEnd { return &segs[i] } } diff --git a/internal/shell/segment_test.go b/internal/shell/segment_test.go index 6e10aec..37f8816 100644 --- a/internal/shell/segment_test.go +++ b/internal/shell/segment_test.go @@ -118,6 +118,22 @@ func TestParseRawCursorSelectsTarget(t *testing.T) { } } +// The widget's usual cursor is end-of-line, one past the last byte of the +// final segment; it must select that segment, not fall back to the first. +func TestParseRawCursorAtEndSelectsLastSegment(t *testing.T) { + line := "az group list && az vm list" + raw, ok := shell.ParseRaw(line, len(line)) + if !ok { + t.Fatal("ParseRaw returned false") + } + if raw.CommandPath != "vm list" { + t.Errorf("CommandPath = %q, want \"vm list\"", raw.CommandPath) + } + if raw.Prefix != "az group list && " { + t.Errorf("Prefix = %q", raw.Prefix) + } +} + func TestParseRawLineContinuation(t *testing.T) { line := "az group create \\\n --name my-group" raw, ok := shell.ParseRaw(line, 0) From 9d928a7ca2499294081ca7f660b3ca90a07874d1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 26 Sep 2026 15:12:51 +0000 Subject: [PATCH 06/12] install: implement documented --purge and purge the real state/cache dirs --purge was documented but only PURGE_STATE=1 worked, and the purge path ignored XDG_STATE_HOME and macOS locations used by the binary. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Dza8X5c3XGHUjejpqGLR8q --- install.sh | 55 +++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 46 insertions(+), 9 deletions(-) diff --git a/install.sh b/install.sh index b204070..5a7340c 100755 --- a/install.sh +++ b/install.sh @@ -13,7 +13,8 @@ # - Print a one-screen summary with the next user action. # # --uninstall reverses the above (binary, share dir, profile block). State -# (drafts/bindings) is preserved unless --purge is given. +# (drafts/bindings) and the metadata cache are preserved unless --purge is +# also given (or PURGE_STATE=1 is set). # # POSIX sh compatible (dash on Debian). set -eu @@ -21,7 +22,6 @@ set -eu REPO="${AZFORM_REPO:-someson/azform}" BIN_DIR="${AZFORM_BIN_DIR:-$HOME/.local/bin}" SHARE_DIR="${AZFORM_SHARE_DIR:-$HOME/.local/share/azform}" -STATE_DIR="${AZFORM_STATE_DIR:-$HOME/.local/state/azform}" VERSION="${AZFORM_VERSION:-}" MARKER_BEGIN="# >>> azform >>>" @@ -235,11 +235,41 @@ add_to_profile() { log "added azform block to $prof (backup: $backup)" } +# state_dir and cache_dir mirror state.DefaultStateDir and +# metadata.DefaultCacheDir, so --purge removes the directories the binary +# really uses (on macOS that is ~/Library/..., not ~/.local/...). Note the +# asymmetry, copied from the Go side: XDG_STATE_HOME wins over the macOS +# default, while the macOS cache default wins over XDG_CACHE_HOME. +state_dir() { + if [ -n "${AZFORM_STATE_DIR:-}" ]; then + echo "$AZFORM_STATE_DIR" + elif [ -n "${XDG_STATE_HOME:-}" ]; then + echo "$XDG_STATE_HOME/azform" + elif [ "$(uname -s)" = Darwin ]; then + echo "$HOME/Library/Application Support/azform" + else + echo "$HOME/.local/state/azform" + fi +} + +cache_dir() { + if [ -n "${AZFORM_CACHE_DIR:-}" ]; then + echo "$AZFORM_CACHE_DIR" + elif [ "$(uname -s)" = Darwin ]; then + echo "$HOME/Library/Caches/azform" + elif [ -n "${XDG_CACHE_HOME:-}" ]; then + echo "$XDG_CACHE_HOME/azform" + else + echo "$HOME/.cache/azform" + fi +} + uninstall() { rm -f "$BIN_DIR/azform" rm -rf "$SHARE_DIR" if [ "${PURGE_STATE:-0}" = "1" ]; then - rm -rf "$STATE_DIR" + rm -rf "$(state_dir)" "$(cache_dir)" + log "removed state and metadata cache" fi for prof in "$HOME/.zshrc" "$HOME/.bashrc" "$HOME/.bash_profile" "$HOME/.profile"; do if [ -f "$prof" ] && grep -qF "$MARKER_BEGIN" "$prof"; then @@ -263,12 +293,19 @@ if [ "${AZFORM_INSTALL_LIB:-0}" = "1" ]; then return 0 2>/dev/null || exit 0 fi -case "${1:-}" in - --uninstall) - uninstall - exit 0 - ;; -esac +UNINSTALL=0 +for arg in "$@"; do + case "$arg" in + --uninstall) UNINSTALL=1 ;; + --purge) PURGE_STATE=1 ;; + *) err "unknown argument: $arg" ;; + esac +done +if [ "$UNINSTALL" = 1 ]; then + uninstall + exit 0 +fi +[ "${PURGE_STATE:-0}" = "1" ] && err "--purge is only valid with --uninstall" platform=$(detect_platform) version=$(resolve_latest_version) From b726fbdfc50eb539f227e6f545f4c2b8e6e1354b Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 26 Sep 2026 15:24:51 +0000 Subject: [PATCH 07/12] ui: keep unknown buffer flags and bare optional-value flags in the command Flags the metadata does not know (outdated cache, extensions) were silently dropped on Done; they are now re-emitted exactly as typed. A value-taking flag typed with no value (vm create --assign-identity) is emitted bare while its field stays empty instead of disappearing. escape-error is now a warning: literal values are always single-quoted, so an unbalanced quote reaches az intact. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Dza8X5c3XGHUjejpqGLR8q --- internal/shell/match.go | 2 ++ internal/ui/field.go | 6 ++++ internal/ui/model.go | 55 ++++++++++++++++++++++++++++++++- internal/ui/prefill.go | 13 ++++++++ internal/ui/typed_value_test.go | 33 ++++++++++++++++++++ internal/ui/view.go | 7 ++++- internal/validate/builtin.go | 11 +++++-- 7 files changed, 122 insertions(+), 5 deletions(-) diff --git a/internal/shell/match.go b/internal/shell/match.go index 2eb5d4f..7796970 100644 --- a/internal/shell/match.go +++ b/internal/shell/match.go @@ -18,6 +18,7 @@ type ParsedParam struct { VarNames []string // for list-kind params: variable names referenced by each consumed token (e.g. ["a1","a2"] from `"$a1" "$a2"`); empty when not a multi-var list Unknown bool // true when the flag is not found in params Explicit bool // true when a value was provided (inline `--flag=…` or a value token), even if empty + Inline bool // true for the `--flag=value` form: RawFlag then holds the whole token, value included } // ParsedBuffer is the result of matching RawBuffer flag tokens against @@ -121,6 +122,7 @@ func MatchParams(raw RawBuffer, params []metadata.Parameter) ParsedBuffer { pp.Value = inlineValue pp.RawValue = inlineRawValue pp.Explicit = true + pp.Inline = true } else if param != nil && !param.TakesValue { // Bool flag: bare, no value token pp.Value = "true" diff --git a/internal/ui/field.go b/internal/ui/field.go index 80b0e06..9101e72 100644 --- a/internal/ui/field.go +++ b/internal/ui/field.go @@ -64,6 +64,12 @@ type Field struct { Enabled bool // whether this param is included in the output command Source FieldSource + // EmitBare is set when the buffer carried this value-taking flag with + // no value (`--assign-identity` before another flag). Several az flags + // accept an optional value (nargs='?'/'*'), so while the field stays + // empty the flag is emitted bare instead of being dropped. + EmitBare bool + // Lazy fetch state (spec §6.1). Idle for fields with no ValuesFrom; the // cursor-move logic in model.go promotes Idle → Loading on focus. FetchState FetchState diff --git a/internal/ui/model.go b/internal/ui/model.go index 30e7ff4..3d06793 100644 --- a/internal/ui/model.go +++ b/internal/ui/model.go @@ -179,6 +179,12 @@ type Form struct { result string src Sources + // passthrough holds buffer flags the metadata does not know, as typed + // (quoting preserved). They are re-emitted after the form's fields so + // an outdated cache or an extension flag is never silently dropped from + // the command; the unknown-flag warning still tells the user about them. + passthrough []string + findings []validate.Finding warningIdx int // index of the currently-shown warning in the footer; cycle with 'w' sessionVars map[string]bool @@ -469,11 +475,17 @@ func (m Form) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil case EnumSelectedMsg: + if m.mode == FormModeEnum && msg.Value == manualEntryChoice { + // Fetched choices are suggestions, not a closed set: the + // first popup row switches to free-text input instead. + return m, m.openEditor(m.enumIdx) + } if m.mode == FormModeEnum { m.fields[m.enumIdx].Value = msg.Value m.fields[m.enumIdx].VarValue = "" m.fields[m.enumIdx].Mode = FieldModeLiteral m.fields[m.enumIdx].Enabled = true + m.invalidateDependentFetches(m.fields[m.enumIdx].Param.Name) m.recomputeFindings(nil) m.mode = FormModeList } @@ -560,6 +572,12 @@ func (m *Form) maybeFetchField(idx int) tea.Cmd { if len(f.Param.Choices) > 0 { return nil } + command, ok := fetchCommand(*vf, m.contextValue) + if !ok { + // A required context param (e.g. --location for vm list-sizes) is + // still empty. Stay idle; focusing the field again retries. + return nil + } f.FetchState = FetchLoading f.FetchSpinnerShow = false f.FetchStartedAt = time.Now() @@ -575,10 +593,45 @@ func (m *Form) maybeFetchField(idx int) tea.Cmd { tea.Tick(fetchCancelOffer, func(time.Time) tea.Msg { return FieldFetchOfferCancelMsg{FieldIdx: idx} }), - fetchField(idx, *vf), + fetchField(idx, command), ) } +// contextValue returns the value an enabled param will pass to az — the +// resolved value for a var reference — or "" when it has none. +func (m *Form) contextValue(param string) string { + for i := range m.fields { + f := &m.fields[i] + if f.Param.Name != param || !f.Enabled { + continue + } + if f.Mode == FieldModeVar { + return f.VarValue + } + return f.Value + } + return "" +} + +// invalidateDependentFetches drops fetched choices that were computed from +// param's previous value (vm sizes for another --location, …) so the next +// focus fetches them again. +func (m *Form) invalidateDependentFetches(param string) { + for i := range m.fields { + f := &m.fields[i] + if f.Param.ValuesFrom == nil || f.FetchState == FetchLoading { + continue + } + for _, p := range fetchContextParams(*f.Param.ValuesFrom) { + if p == param { + f.FetchState = FetchIdle + f.FetchedChoices = nil + f.FetchError = "" + } + } + } +} + // anyFieldLoading reports whether any field is currently mid-fetch; used to // decide whether spinner ticks should keep animating the inline spinner. func anyFieldLoading(fs []Field) bool { diff --git a/internal/ui/prefill.go b/internal/ui/prefill.go index a1c137a..b24064b 100644 --- a/internal/ui/prefill.go +++ b/internal/ui/prefill.go @@ -110,6 +110,10 @@ func (m *Form) applyBufferPreFill(params []metadata.Parameter) bool { } parsed := shell.MatchParams(m.src.Buffer, params) for _, pp := range parsed.Params { + if pp.Unknown { + m.passthrough = append(m.passthrough, rawArg(pp)) + continue + } for i := range m.fields { if m.fields[i].Param.Name == pp.Flag { f := &m.fields[i] @@ -147,6 +151,7 @@ func (m *Form) applyBufferPreFill(params []metadata.Parameter) bool { f.Mode = mode f.Enabled = true f.Source = FieldSourceBuffer + f.EmitBare = !pp.Explicit && value == "" && f.Param.TakesValue break } } @@ -165,6 +170,14 @@ func (m *Form) applyBufferPreFill(params []metadata.Parameter) bool { return true } +// rawArg reassembles a parsed flag exactly as the user typed it. +func rawArg(pp shell.ParsedParam) string { + if pp.Inline || pp.RawValue == "" { + return pp.RawFlag + } + return pp.RawFlag + " " + pp.RawValue +} + // applyEnvPreFill consumes vars.MatchVariables results (priority 5). // Returns true if any field was filled. Skips fields already filled by the // buffer (priority 1). diff --git a/internal/ui/typed_value_test.go b/internal/ui/typed_value_test.go index e74081f..7c389a8 100644 --- a/internal/ui/typed_value_test.go +++ b/internal/ui/typed_value_test.go @@ -6,6 +6,7 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/someson/azform/internal/metadata" + "github.com/someson/azform/internal/shell" "github.com/someson/azform/internal/state" "github.com/someson/azform/internal/ui" "github.com/someson/azform/internal/validate" @@ -167,3 +168,35 @@ func TestVarPickerInsertAfterNonASCII(t *testing.T) { t.Errorf("input = %q, want %q", got, "é-$RG") } } + +// Flags the metadata does not know (an outdated cache, an extension) used +// to vanish from the command on Done; they are now kept exactly as typed. +func TestUnknownBufferFlagsArePassedThrough(t *testing.T) { + line := `az group show --name n --resource-group rg --new-flag "a b" --other=x --bare` + raw, _ := shell.ParseRaw(line, 0) + src := ui.Sources{Engine: validate.NewEngine(validate.BuiltinProvider{}), Buffer: raw} + f := ui.NewFormWithSources("group show", "/tmp/out.txt", t.TempDir(), "test", nil, src) + m, _ := f.Update(ui.MetadataLoadedMsg{Params: typedValueParams(), Summary: "."}) + f = submit(t, m.(ui.Form)) + want := `az group show --name n --resource-group rg --new-flag "a b" --other=x --bare` + if f.Result() != want { + t.Errorf("Result = %q, want %q (errorMsg=%q)", f.Result(), want, f.ErrorMsg()) + } +} + +// A value-taking flag typed without a value (optional-value flags such as +// `vm create --assign-identity`) stays bare instead of being dropped. +func TestBareOptionalValueFlagIsKept(t *testing.T) { + params := append(typedValueParams(), metadata.Parameter{ + Name: "--assign-identity", TakesValue: true, ValueKind: metadata.ValueKindList, Group: "Optional Parameters", + }) + raw, _ := shell.ParseRaw("az vm create --assign-identity --name n --resource-group rg", 0) + src := ui.Sources{Engine: validate.NewEngine(validate.BuiltinProvider{}), Buffer: raw} + f := ui.NewFormWithSources("vm create", "/tmp/out.txt", t.TempDir(), "test", nil, src) + m, _ := f.Update(ui.MetadataLoadedMsg{Params: params, Summary: "."}) + f = submit(t, m.(ui.Form)) + want := "az vm create --name n --resource-group rg --assign-identity" + if f.Result() != want { + t.Errorf("Result = %q, want %q (errorMsg=%q)", f.Result(), want, f.ErrorMsg()) + } +} diff --git a/internal/ui/view.go b/internal/ui/view.go index c8e8d68..77636ae 100644 --- a/internal/ui/view.go +++ b/internal/ui/view.go @@ -738,10 +738,15 @@ func (m *Form) buildCommand() string { Name: f.Param.Name, Value: val, IsVar: isVar, - IsSwitch: f.Param.IsSwitch(), + IsSwitch: f.Param.IsSwitch() || (f.EmitBare && val == ""), Enabled: f.Enabled, }) } + // Unknown buffer flags go out verbatim: a switch-shaped entry emits + // its text as-is, with no escaping and no value. + for _, raw := range m.passthrough { + fvs = append(fvs, render.FieldValue{Name: raw, IsSwitch: true, Enabled: true}) + } return render.Build(render.Command{ Path: m.command, Fields: fvs, diff --git a/internal/validate/builtin.go b/internal/validate/builtin.go index cdbaf02..774bf9a 100644 --- a/internal/validate/builtin.go +++ b/internal/validate/builtin.go @@ -113,6 +113,11 @@ func usesVarName(value, name string) bool { // escapeError: enabled literal-mode value with unclosed quote or backtick. // Disabled fields are never rendered, so they cannot break the command. +// +// It is a warning, not a blocker: literal values are always emitted inside +// single quotes, so a lone `"` reaches az intact (`--description 5"`) and +// the command stays valid. The finding only flags a probable typo, such as +// a value the user meant to quote themselves. type escapeError struct{} func (escapeError) ID() string { return "builtin/escape-error" } @@ -126,8 +131,8 @@ func (escapeError) Check(cmd *metadata.Command, st *FormState) []Finding { if strings.Count(val, `"`)%2 == 1 || strings.Count(val, "`")%2 == 1 { out = append(out, Finding{ Param: name, - Severity: SeverityBlocking, - Message: name + ": unclosed quote in value", + Severity: SeverityWarning, + Message: name + ": unbalanced quote in value (sent to az as typed)", RuleID: "builtin/escape-error", }) } @@ -193,7 +198,7 @@ func (unknownFlag) Check(cmd *metadata.Command, st *FormState) []Finding { out = append(out, Finding{ Param: "", Severity: SeverityWarning, - Message: "unknown flag " + f, + Message: "unknown flag " + f + " (kept as typed)", Suggest: suggestSimilar(f, cmd.Parameters), RuleID: "builtin/unknown-flag", }) From 60400bc9e7818e7dda32ccc06748df8ce9d225c1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 26 Sep 2026 15:24:51 +0000 Subject: [PATCH 08/12] ui: make fetched values pickable and run runnable Values-from commands - Enter on a field with fetched values opens a picker (first row: type a value manually); before, they were only counted as "(N options)". - Only the first command of a multi-command hint is run, punctuation and placeholders are handled, and commands that need context (vm list-sizes, aks get-versions/get-upgrades) get the form's --location / --resource-group / --name; edits to those drop stale choices. - Output parsing is deterministic and understands version keys and plain string arrays; error text is truncated on rune boundaries. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Dza8X5c3XGHUjejpqGLR8q --- cmd/azform/doctor.go | 4 +- internal/ui/fetch.go | 139 ++++++++++++++++++++++++++++++++------ internal/ui/fetch_test.go | 118 ++++++++++++++++++++++++++++++++ internal/ui/handlers.go | 53 ++++++++++----- 4 files changed, 273 insertions(+), 41 deletions(-) diff --git a/cmd/azform/doctor.go b/cmd/azform/doctor.go index 8e81d2e..87da99e 100644 --- a/cmd/azform/doctor.go +++ b/cmd/azform/doctor.go @@ -229,8 +229,8 @@ func oneLine(s string) string { s = strings.ReplaceAll(s, "\n", " ") s = strings.ReplaceAll(s, "\r", " ") s = strings.TrimSpace(s) - if len(s) > 200 { - s = s[:197] + "..." + if r := []rune(s); len(r) > 200 { + s = string(r[:197]) + "..." } return s } diff --git a/internal/ui/fetch.go b/internal/ui/fetch.go index 3fe9c73..56a39ab 100644 --- a/internal/ui/fetch.go +++ b/internal/ui/fetch.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "os/exec" + "sort" "strings" "time" @@ -84,9 +85,74 @@ func buildFetchArgs(valuesFrom string) []string { return args } +// fetchContext names the form params whose values some `Values from:` +// commands cannot run without. az prints the bare command in the help text +// (`az vm list-sizes`), which on its own always fails with a missing +// --location; the form passes the user's current values along instead. +var fetchContext = map[string][]string{ + "vm list-sizes": {"--location"}, + "vm list-skus": {"--location"}, + "aks get-versions": {"--location"}, + "aks get-upgrades": {"--resource-group", "--name"}, +} + +// fetchCommand turns a `Values from:` hint into the az command line to run +// (without the leading az). Hints may list alternatives +// ("az vm image list, az vm image show, …") — only the first is used — and +// carry sentence punctuation. lookup returns a param's current resolved +// value; ok is false when a required context param is still empty, in +// which case nothing should be run yet. +func fetchCommand(valuesFrom string, lookup func(param string) string) (cmd string, ok bool) { + first, _, _ := strings.Cut(valuesFrom, ",") + first = strings.TrimSpace(strings.Trim(strings.TrimSpace(first), "`.")) + args := strings.Fields(first) + if len(args) > 0 && args[0] == "az" { + args = args[1:] + } + if len(args) == 0 { + return "", false + } + for _, a := range args { + // Placeholders such as cannot be run. + if strings.ContainsAny(a, "<>") { + return "", false + } + } + for path, needs := range fetchContext { + if strings.Join(args, " ") != path { + continue + } + for _, param := range needs { + v := lookup(param) + if v == "" { + return "", false + } + args = append(args, param, v) + } + } + return strings.Join(args, " "), true +} + +// fetchContextParams returns the params that the fetch for valuesFrom +// depends on, so their edits can invalidate already-fetched choices. +func fetchContextParams(valuesFrom string) []string { + first, _, _ := strings.Cut(valuesFrom, ",") + args := strings.Fields(strings.Trim(strings.TrimSpace(first), "`.")) + if len(args) > 0 && args[0] == "az" { + args = args[1:] + } + return fetchContext[strings.Join(args, " ")] +} + +// choiceKeys are the object fields tried, in order, as an item's value. +// version / kubernetesVersion cover `aks get-versions` / `get-upgrades`. +var choiceKeys = []string{"name", "displayName", "version", "kubernetesVersion"} + // parseFetchedValues extracts the choice list from a `az ... --output json` -// response. Heuristic (spec §4.5): the array element's `name` field -// (fallback: `displayName`, then first non-empty string field) is the value. +// response. Heuristic (spec §4.5): in an array of plain strings each string +// is a value; +// for an object, the first of choiceKeys that is a non-empty string, else +// the first non-empty string field in key order. Duplicates are dropped. func parseFetchedValues(raw []byte) ([]string, error) { var v any if err := json.Unmarshal(raw, &v); err != nil { @@ -97,24 +163,30 @@ func parseFetchedValues(raw []byte) ([]string, error) { return nil, fmt.Errorf("az output: no array found") } var out []string - for _, item := range arr { - obj, ok := item.(map[string]any) - if !ok { - continue - } - if s := stringFromMap(obj, "name"); s != "" { + seen := map[string]bool{} + add := func(s string) { + if s != "" && !seen[s] { + seen[s] = true out = append(out, s) - continue } - if s := stringFromMap(obj, "displayName"); s != "" { - out = append(out, s) - continue + } + // Strings count only in an array of plain strings; in a mixed array + // they are noise next to the objects that describe resources. + hasObjects := false + for _, item := range arr { + if _, ok := item.(map[string]any); ok { + hasObjects = true + break } - for _, val := range obj { - if s, ok := val.(string); ok && s != "" { - out = append(out, s) - break + } + for _, item := range arr { + switch x := item.(type) { + case string: + if !hasObjects { + add(x) } + case map[string]any: + add(objectChoice(x)) } } if len(out) == 0 { @@ -123,16 +195,32 @@ func parseFetchedValues(raw []byte) ([]string, error) { return out, nil } +func objectChoice(obj map[string]any) string { + for _, k := range choiceKeys { + if s := stringFromMap(obj, k); s != "" { + return s + } + } + for _, k := range sortedKeys(obj) { + if s, ok := obj[k].(string); ok && s != "" { + return s + } + } + return "" +} + // findArray descends into a decoded JSON value, returning the first array it // finds. `az ... --output json` typically returns either an array or an -// object with one array-valued field (e.g. `{"value": [...]}`). +// object with one array-valued field (e.g. `{"value": [...]}`). Object keys +// are visited in sorted order so the result does not depend on Go's +// randomised map iteration. func findArray(v any) []any { switch x := v.(type) { case []any: return x case map[string]any: - for _, val := range x { - if arr := findArray(val); arr != nil { + for _, k := range sortedKeys(x) { + if arr := findArray(x[k]); arr != nil { return arr } } @@ -140,6 +228,15 @@ func findArray(v any) []any { return nil } +func sortedKeys(m map[string]any) []string { + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + return keys +} + func stringFromMap(m map[string]any, key string) string { if v, ok := m[key]; ok { if s, ok := v.(string); ok { @@ -153,8 +250,8 @@ func oneLine(s string) string { s = strings.TrimSpace(s) s = strings.ReplaceAll(s, "\n", " ") s = strings.ReplaceAll(s, "\r", " ") - if len(s) > 200 { - s = s[:197] + "..." + if r := []rune(s); len(r) > 200 { + s = string(r[:197]) + "..." } return s } diff --git a/internal/ui/fetch_test.go b/internal/ui/fetch_test.go index 5895700..308cd84 100644 --- a/internal/ui/fetch_test.go +++ b/internal/ui/fetch_test.go @@ -2,6 +2,10 @@ package ui import ( "testing" + + tea "github.com/charmbracelet/bubbletea" + + "github.com/someson/azform/internal/metadata" ) func TestBuildFetchArgs(t *testing.T) { @@ -133,3 +137,117 @@ func equalStrings(a, b []string) bool { } return true } + +func TestFetchCommand(t *testing.T) { + values := map[string]string{"--location": "westeurope", "--resource-group": "rg", "--name": "aks1"} + lookup := func(p string) string { return values[p] } + empty := func(string) string { return "" } + cases := []struct { + vf string + lookup func(string) string + want string + ok bool + }{ + {"az account list-locations", lookup, "account list-locations", true}, + {"`az account list-locations`.", lookup, "account list-locations", true}, + {"az vm image list, az vm image show, az sig image-version show-shared", lookup, "vm image list", true}, + {"az vm list-sizes", lookup, "vm list-sizes --location westeurope", true}, + {"az vm list-sizes", empty, "", false}, + {"az aks get-upgrades", lookup, "aks get-upgrades --resource-group rg --name aks1", true}, + {"az foo show --name ", lookup, "", false}, + {"", lookup, "", false}, + } + for _, tc := range cases { + got, ok := fetchCommand(tc.vf, tc.lookup) + if got != tc.want || ok != tc.ok { + t.Errorf("fetchCommand(%q) = %q, %v; want %q, %v", tc.vf, got, ok, tc.want, tc.ok) + } + } +} + +func TestParseFetchedValuesVersionsAndStrings(t *testing.T) { + // aks get-versions: object wrapper, items keyed by "version". + got, err := parseFetchedValues([]byte(`{"id":"x","name":"default","values":[{"version":"1.30","isPreview":false},{"version":"1.29"}]}`)) + if err != nil || !equalStrings(got, []string{"1.30", "1.29"}) { + t.Errorf("versions: got %v, %v", got, err) + } + // aks get-upgrades: nested object, items keyed by kubernetesVersion. + got, err = parseFetchedValues([]byte(`{"agentPoolProfiles":null,"controlPlaneProfile":{"upgrades":[{"kubernetesVersion":"1.31.1"}]}}`)) + if err != nil || !equalStrings(got, []string{"1.31.1"}) { + t.Errorf("upgrades: got %v, %v", got, err) + } + // Plain string array, duplicates dropped. + got, err = parseFetchedValues([]byte(`["a","b","a"]`)) + if err != nil || !equalStrings(got, []string{"a", "b"}) { + t.Errorf("strings: got %v, %v", got, err) + } +} + +// Fetched values used to be shown only as "(N options)"; Enter now offers +// them in the picker, whose first row falls back to free text. +func TestFetchedChoicesArePickable(t *testing.T) { + vf := "az account list-locations" + f := NewForm("group create", "/tmp/out.txt", t.TempDir(), "test", nil) + m, _ := f.Update(MetadataLoadedMsg{Params: []metadata.Parameter{ + {Name: "--location", TakesValue: true, ValueKind: metadata.ValueKindString, ValuesFrom: &vf}, + }}) + f = m.(Form) + idx := f.FieldIndex("--location") + f.fields[idx].FetchState = FetchLoaded + f.fields[idx].FetchedChoices = []string{"westeurope", "northeurope"} + + m, _ = f.Update(tea.KeyMsg{Type: tea.KeyEnter}) + f = m.(Form) + if f.mode != FormModeEnum { + t.Fatalf("Enter should open the picker, mode=%v", f.mode) + } + m, _ = f.Update(tea.KeyMsg{Type: tea.KeyDown}) + f = m.(Form) + m, cmd := f.Update(tea.KeyMsg{Type: tea.KeyEnter}) + f = m.(Form) + m, _ = f.Update(cmd()) + f = m.(Form) + if got := f.fields[idx].Value; got != "westeurope" { + t.Errorf("picked value = %q, want westeurope", got) + } + + // First row: free-text entry. + m, _ = f.Update(tea.KeyMsg{Type: tea.KeyEnter}) + f = m.(Form) + for f.enumPop.cursor > 0 { + m, _ = f.Update(tea.KeyMsg{Type: tea.KeyUp}) + f = m.(Form) + } + m, cmd = f.Update(tea.KeyMsg{Type: tea.KeyEnter}) + f = m.(Form) + m, _ = f.Update(cmd()) + f = m.(Form) + if f.mode != FormModeEdit { + t.Errorf("manual-entry row should open the editor, mode=%v", f.mode) + } +} + +// Editing a context param throws away choices fetched for its old value. +func TestContextEditInvalidatesFetchedChoices(t *testing.T) { + vf := "az vm list-sizes" + f := NewForm("vm create", "/tmp/out.txt", t.TempDir(), "test", nil) + m, _ := f.Update(MetadataLoadedMsg{Params: []metadata.Parameter{ + {Name: "--location", TakesValue: true, ValueKind: metadata.ValueKindString}, + {Name: "--size", TakesValue: true, ValueKind: metadata.ValueKindString, ValuesFrom: &vf}, + }}) + f = m.(Form) + size := f.FieldIndex("--size") + f.fields[size].FetchState = FetchLoaded + f.fields[size].FetchedChoices = []string{"Standard_B1s"} + if cmd := f.maybeFetchField(size); cmd != nil { + t.Fatalf("loaded field must not refetch") + } + f.invalidateDependentFetches("--location") + if f.fields[size].FetchState != FetchIdle || f.fields[size].FetchedChoices != nil { + t.Errorf("choices not invalidated: state=%v", f.fields[size].FetchState) + } + // With --location still empty, nothing is run. + if cmd := f.maybeFetchField(size); cmd != nil || f.fields[size].FetchState != FetchIdle { + t.Errorf("fetch without --location should not start") + } +} diff --git a/internal/ui/handlers.go b/internal/ui/handlers.go index 3fbc1e1..f17c5b3 100644 --- a/internal/ui/handlers.go +++ b/internal/ui/handlers.go @@ -49,6 +49,7 @@ func (m Form) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { if m.textInput.Value() != "" { m.fields[m.editIdx].Enabled = true } + m.invalidateDependentFetches(m.fields[m.editIdx].Param.Name) m.recomputeFindings(nil) m.mode = FormModeList return m, nil @@ -235,25 +236,16 @@ func (m Form) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { m.enumIdx = idx m.mode = FormModeEnum default: - m.textInput.SetValue(f.Value) - // Constrain the input to the grid value column so the - // in-place edit (renderGridCell replaces the value cell - // with the textinput) fits without overflowing into the - // next column. Single-column mode leaves the input at its - // natural width so long values can be edited without - // horizontal scrolling. - if _, _, cols := m.gridLayout(); cols >= 2 { - // Reserve 1 cell for the cursor so bubbles/textinput's - // View() output width stays within gridValueBudget and - // doesn't overflow into the next grid column. - m.textInput.Width = gridValueBudget - 1 - } else { - m.textInput.Width = 0 + if f.FetchState == FetchLoaded && len(f.FetchedChoices) > 0 { + // Lazily fetched values (spec §6.1) are offered as a + // picker; the first row falls back to free text. + choices := append([]string{manualEntryChoice}, f.FetchedChoices...) + m.enumPop = NewEnum(choices, f.Value, m.width-4) + m.enumIdx = idx + m.mode = FormModeEnum + return m, nil } - focusCmd := m.textInput.Focus() - m.editIdx = idx - m.mode = FormModeEdit - return m, focusCmd + return m, m.openEditor(idx) } return m, nil case "g": @@ -342,6 +334,31 @@ func (m Form) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { return m, nil } +// manualEntryChoice is the first row of a fetched-choices popup; picking it +// opens the free-text editor instead of selecting a value. +const manualEntryChoice = "✎ type a value…" + +// openEditor switches field idx into free-text edit mode. +func (m *Form) openEditor(idx int) tea.Cmd { + m.textInput.SetValue(m.fields[idx].Value) + // Constrain the input to the grid value column so the in-place edit + // (renderGridCell replaces the value cell with the textinput) fits + // without overflowing into the next column. Single-column mode leaves + // the input at its natural width so long values can be edited without + // horizontal scrolling. + if _, _, cols := m.gridLayout(); cols >= 2 { + // Reserve 1 cell for the cursor so bubbles/textinput's View() + // output width stays within gridValueBudget and doesn't overflow + // into the next grid column. + m.textInput.Width = gridValueBudget - 1 + } else { + m.textInput.Width = 0 + } + m.editIdx = idx + m.mode = FormModeEdit + return m.textInput.Focus() +} + func (m Form) confirmDone() (tea.Model, tea.Cmd) { m.recomputeFindings(nil) for _, f := range m.findings { From 03a0eb74012dff9347cd72bfc39535e517f116a9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 26 Sep 2026 15:24:51 +0000 Subject: [PATCH 09/12] metadata: detect az upgrades for apt, rpm and pip installs deb/rpm ship az as a launcher in /usr/bin, so the install root resolved to /usr, whose mtime never changes on upgrade; pip venv roots behave the same. Follow the launcher to the Python it runs and use the newest site-packages mtime, which changes when azure_cli-*.dist-info is renamed. Also recognise /usr/lib64/az. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Dza8X5c3XGHUjejpqGLR8q --- internal/metadata/cache.go | 69 ++++++++++++++++++++++++++++++++- internal/metadata/cache_test.go | 69 +++++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+), 2 deletions(-) diff --git a/internal/metadata/cache.go b/internal/metadata/cache.go index 5be65c6..afe7927 100644 --- a/internal/metadata/cache.go +++ b/internal/metadata/cache.go @@ -1,13 +1,16 @@ package metadata import ( + "bytes" "context" "encoding/json" "errors" "fmt" + "io" "os" "os/exec" "path/filepath" + "regexp" "runtime" "strings" "time" @@ -456,15 +459,22 @@ func DetectEnvironment() (Environment, error) { azPath = resolved } installPath := azureCLIInstallRoot(azPath) + if root := interpreterRoot(azPath); root != "" { + installPath = root + } installInfo, err := os.Stat(installPath) if err != nil { return Environment{}, fmt.Errorf("metadata: stat Azure CLI install root %s: %w", installPath, err) } + modTime := installInfo.ModTime() + if sp := sitePackagesModTime(installPath); sp.After(modTime) { + modTime = sp + } env := Environment{ AZPath: azPath, InstallPath: installPath, - InstallModTime: installInfo.ModTime().UTC(), + InstallModTime: modTime.UTC(), } env.ExtensionsPath = azureExtensionsDir() if info, err := os.Stat(env.ExtensionsPath); err == nil { @@ -473,10 +483,65 @@ func DetectEnvironment() (Environment, error) { return env, nil } +// launcherPythonRE finds the interpreter an az launcher script runs: +// `/opt/az/bin/python3 -Im azure.cli` (deb), `/usr/lib64/az/bin/python3` +// (rpm), `…/Cellar/azure-cli//libexec/bin/python` (Homebrew) or a +// `#!/path/to/venv/bin/python3` shebang (pip). +var launcherPythonRE = regexp.MustCompile(`(/[^\s"'=:;]+)/bin/python[0-9.]*`) + +// interpreterRoot returns the prefix of the Python environment the az +// launcher at azPath runs, or "" when it cannot tell (a binary, an +// unreadable file, no interpreter path). Every packaged install ships az as +// such a launcher, and on deb/rpm it is a plain file in /usr/bin, so the +// path alone points at /usr rather than at the real install. +func interpreterRoot(azPath string) string { + f, err := os.Open(azPath) + if err != nil { + return "" + } + defer func() { _ = f.Close() }() + buf := make([]byte, 4096) + n, _ := io.ReadFull(f, buf) + head := buf[:n] + if bytes.IndexByte(head, 0) >= 0 { + return "" + } + m := launcherPythonRE.FindSubmatch(head) + if m == nil { + return "" + } + return string(m[1]) +} + +// sitePackagesModTime returns the newest mtime among root's Python package +// directories. Upgrading azure-cli with apt, dnf or pip renames its +// *.dist-info directory there, which bumps the directory's mtime even when +// nothing at the install root itself changes. +func sitePackagesModTime(root string) time.Time { + var newest time.Time + for _, pattern := range []string{ + filepath.Join(root, "lib*", "python3*", "site-packages"), + filepath.Join(root, "lib*", "python3*", "dist-packages"), + filepath.Join(root, "lib", "python3", "dist-packages"), + } { + matches, _ := filepath.Glob(pattern) + for _, dir := range matches { + if info, err := os.Stat(dir); err == nil && info.ModTime().After(newest) { + newest = info.ModTime() + } + } + } + return newest +} + func azureCLIInstallRoot(azPath string) string { clean := filepath.Clean(azPath) sep := string(filepath.Separator) - for _, root := range []string{sep + filepath.Join("opt", "az"), sep + filepath.Join("lib64", "az")} { + for _, root := range []string{ + sep + filepath.Join("opt", "az"), + sep + filepath.Join("usr", "lib64", "az"), + sep + filepath.Join("lib64", "az"), + } { if clean == root || strings.HasPrefix(clean, root+sep) { return root } diff --git a/internal/metadata/cache_test.go b/internal/metadata/cache_test.go index b770b56..a69af50 100644 --- a/internal/metadata/cache_test.go +++ b/internal/metadata/cache_test.go @@ -383,6 +383,7 @@ func TestAzureCLIInstallRoot(t *testing.T) { }{ {name: "homebrew cellar", path: filepath.Join(sep, "opt", "homebrew", "Cellar", "azure-cli", "2.89.1", "bin", "az"), want: filepath.Join(sep, "opt", "homebrew", "Cellar", "azure-cli", "2.89.1")}, {name: "deb layout", path: filepath.Join(sep, "opt", "az", "bin", "az"), want: filepath.Join(sep, "opt", "az")}, + {name: "rhel usr layout", path: filepath.Join(sep, "usr", "lib64", "az", "bin", "az"), want: filepath.Join(sep, "usr", "lib64", "az")}, {name: "rhel layout", path: filepath.Join(sep, "lib64", "az", "bin", "az"), want: filepath.Join(sep, "lib64", "az")}, {name: "generic bin", path: filepath.Join(sep, "tmp", "cli", "bin", "az"), want: filepath.Join(sep, "tmp", "cli")}, } @@ -539,3 +540,71 @@ func nonEmptyLines(s string) []string { } return out } + +// deb/rpm install az as a launcher script in /usr/bin, so the path alone +// resolves to /usr, whose mtime never changes on an az upgrade. The +// launcher names the real install, and an upgrade renames the +// azure_cli-*.dist-info directory inside its site-packages. +func TestDetectEnvironmentFollowsLauncherInterpreter(t *testing.T) { + tmp := t.TempDir() + install := filepath.Join(tmp, "opt", "az") + site := filepath.Join(install, "lib", "python3.12", "site-packages") + if err := os.MkdirAll(site, 0o755); err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(filepath.Join(install, "bin"), 0o755); err != nil { + t.Fatal(err) + } + binDir := filepath.Join(tmp, "usr", "bin") + if err := os.MkdirAll(binDir, 0o755); err != nil { + t.Fatal(err) + } + launcher := "#!/usr/bin/env bash\n" + install + "/bin/python3 -Im azure.cli \"$@\"\n" + if err := os.WriteFile(filepath.Join(binDir, "az"), []byte(launcher), 0o755); err != nil { + t.Fatal(err) + } + t.Setenv("PATH", binDir) + + old := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC) + upgraded := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) + for _, p := range []string{install, filepath.Join(install, "lib"), filepath.Join(install, "lib", "python3.12")} { + _ = os.Chtimes(p, old, old) + } + if err := os.Chtimes(site, upgraded, upgraded); err != nil { + t.Fatal(err) + } + + env, err := DetectEnvironment() + if err != nil { + t.Fatalf("DetectEnvironment: %v", err) + } + if env.InstallPath != install { + t.Errorf("InstallPath = %q, want %q", env.InstallPath, install) + } + if !env.InstallModTime.Equal(upgraded) { + t.Errorf("InstallModTime = %v, want site-packages mtime %v", env.InstallModTime, upgraded) + } +} + +func TestInterpreterRoot(t *testing.T) { + dir := t.TempDir() + write := func(name, content string) string { + p := filepath.Join(dir, name) + if err := os.WriteFile(p, []byte(content), 0o755); err != nil { + t.Fatal(err) + } + return p + } + cases := []struct{ name, content, want string }{ + {"deb", "#!/usr/bin/env bash\n/opt/az/bin/python3 -Im azure.cli \"$@\"\n", "/opt/az"}, + {"brew", "#!/bin/bash\nexec \"/opt/homebrew/Cellar/azure-cli/2.70.0/libexec/bin/python\" -Im azure.cli \"$@\"\n", "/opt/homebrew/Cellar/azure-cli/2.70.0/libexec"}, + {"pip", "#!/home/u/.venv/bin/python3.12\nimport sys\n", "/home/u/.venv"}, + {"binary", "\x7fELF\x00\x00/opt/az/bin/python3", ""}, + {"none", "#!/bin/sh\necho hi\n", ""}, + } + for _, tc := range cases { + if got := interpreterRoot(write(tc.name, tc.content)); got != tc.want { + t.Errorf("%s: interpreterRoot = %q, want %q", tc.name, got, tc.want) + } + } +} From 02992529fb86d3b31aae087305994b695726c24f Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 26 Sep 2026 15:24:51 +0000 Subject: [PATCH 10/12] shell: byte-exact cursor from the widgets; keep unclosed substitutions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit zsh CURSOR and bash 5 READLINE_POINT count characters while the tokenizer uses bytes, so non-ASCII text before the cursor picked the wrong command. The widgets now pass the text left of the cursor (--cursor-prefix). An unclosed $(az … or `az … no longer gains a closing delimiter when the line is rebuilt. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Dza8X5c3XGHUjejpqGLR8q --- cmd/azform/cursor_test.go | 25 +++++++++++++++++++++++++ cmd/azform/main.go | 17 +++++++++++++++-- internal/shell/segment.go | 15 +++++++++------ internal/shell/segment_test.go | 17 +++++++++++++++++ widget/widget.bash | 4 ++++ widget/widget.zsh | 4 +++- 6 files changed, 73 insertions(+), 9 deletions(-) create mode 100644 cmd/azform/cursor_test.go diff --git a/cmd/azform/cursor_test.go b/cmd/azform/cursor_test.go new file mode 100644 index 0000000..f8e18f3 --- /dev/null +++ b/cmd/azform/cursor_test.go @@ -0,0 +1,25 @@ +package main + +import ( + "testing" + + "github.com/someson/azform/internal/shell" +) + +func TestCursorByte(t *testing.T) { + line := "echo ééé && az vm list" + // The shell reports 21 characters; the same position is 24 bytes. + if got := cursorByte(line, 21, line); got != len(line) { + t.Errorf("cursorByte = %d, want %d", got, len(line)) + } + raw, ok := shell.ParseRaw(line, cursorByte(line, 21, line)) + if !ok || raw.CommandPath != "vm list" { + t.Errorf("ParseRaw with prefix cursor: %q, %v", raw.CommandPath, ok) + } + if got := cursorByte(line, 5, ""); got != 5 { + t.Errorf("no prefix: got %d, want 5", got) + } + if got := cursorByte(line, 5, "unrelated"); got != 5 { + t.Errorf("mismatched prefix: got %d, want 5", got) + } +} diff --git a/cmd/azform/main.go b/cmd/azform/main.go index 69da95a..8b95987 100644 --- a/cmd/azform/main.go +++ b/cmd/azform/main.go @@ -52,6 +52,7 @@ func run(args []string) int { outPath string envOutPath string cursor int + cursorPrefix string varsPath string cwd string cacheDir string @@ -66,7 +67,8 @@ func run(args []string) int { fs.StringVar(&line, "line", "", "current shell buffer contents") fs.StringVar(&outPath, "out", "", "file path to write the assembled command") fs.StringVar(&envOutPath, "env-out", "", "file path to write pending shell-variable exports (g-popup); empty = disabled") - fs.IntVar(&cursor, "cursor", 0, "cursor position in --line") + fs.IntVar(&cursor, "cursor", 0, "cursor position in --line, in bytes") + fs.StringVar(&cursorPrefix, "cursor-prefix", "", "text of --line left of the cursor; overrides --cursor (zsh's CURSOR counts characters, not bytes)") fs.StringVar(&varsPath, "vars", "", "NUL-separated NAME=VALUE file from the shell widget") fs.StringVar(&cwd, "cwd", "", "shell working directory (for @ path completion)") fs.StringVar(&cacheDir, "cache-dir", "", "override metadata cache directory") @@ -135,7 +137,7 @@ func run(args []string) int { } // Parse the shell buffer to locate the az command. - raw, ok := shell.ParseRaw(line, cursor) + raw, ok := shell.ParseRaw(line, cursorByte(line, cursor, cursorPrefix)) if !ok { if len(fs.Args()) == 0 { fs.Usage() @@ -258,6 +260,17 @@ func runTUI(raw shell.RawBuffer, shellVars, azureDefaults []vars.Variable, outPa return 0 } +// cursorByte returns the cursor as a byte offset into line, which is what +// the tokenizer works in. zsh's CURSOR and bash 5's READLINE_POINT count +// characters, so the widgets also pass the text left of the cursor; its +// byte length is exact. A prefix that does not match line is ignored. +func cursorByte(line string, cursor int, prefix string) int { + if prefix != "" && strings.HasPrefix(line, prefix) { + return len(prefix) + } + return cursor +} + func printVersion() { fmt.Printf("azform %s", version) if commit != "" { diff --git a/internal/shell/segment.go b/internal/shell/segment.go index 13fcdf2..78da89d 100644 --- a/internal/shell/segment.go +++ b/internal/shell/segment.go @@ -70,14 +70,17 @@ func findSegments(line string) []azSegment { isBacktick := tok.Raw != "" && tok.Raw[0] == '`' innerSegs := findSegments(inner) for _, s := range innerSegs { - var prefix, suffix string + open, closing := "$(", ")" if isBacktick { - prefix = line[:tok.Start] + "`" + s.prefix - suffix = s.suffix + "`" + line[tok.End:] - } else { - prefix = line[:tok.Start] + "$(" + s.prefix - suffix = s.suffix + ")" + line[tok.End:] + open, closing = "`", "`" } + if tok.Unclosed { + // The user has not typed the closing delimiter yet; + // the rebuilt line must not invent one. + closing = "" + } + prefix := line[:tok.Start] + open + s.prefix + suffix := s.suffix + closing + line[tok.End:] segs = append(segs, azSegment{ commandPath: s.commandPath, flagTokens: s.flagTokens, diff --git a/internal/shell/segment_test.go b/internal/shell/segment_test.go index 37f8816..fd962cd 100644 --- a/internal/shell/segment_test.go +++ b/internal/shell/segment_test.go @@ -134,6 +134,23 @@ func TestParseRawCursorAtEndSelectsLastSegment(t *testing.T) { } } +// An unclosed substitution (the user is still typing it) must round-trip +// without a closing delimiter being added. +func TestParseRawUnclosedSubstitution(t *testing.T) { + for _, line := range []string{"RG=$(az group show --name x", "RG=`az group show --name x"} { + raw, ok := shell.ParseRaw(line, len(line)) + if !ok { + t.Fatalf("%q: ParseRaw returned false", line) + } + if raw.CommandPath != "group show" { + t.Errorf("%q: CommandPath = %q", line, raw.CommandPath) + } + if got := raw.Prefix + "az group show --name x" + raw.Suffix; got != line { + t.Errorf("rebuilt %q, want %q", got, line) + } + } +} + func TestParseRawLineContinuation(t *testing.T) { line := "az group create \\\n --name my-group" raw, ok := shell.ParseRaw(line, 0) diff --git a/widget/widget.bash b/widget/widget.bash index 3a00a34..ed7001a 100644 --- a/widget/widget.bash +++ b/widget/widget.bash @@ -73,7 +73,11 @@ azform-widget() { azform_bash_dump_vars "$vars" + # --cursor-prefix: azform needs a byte offset, but bash 5 counts + # READLINE_POINT in characters (checked on 5.2), as does the substring + # expansion, so the text left of the cursor is passed instead. azform --line "$READLINE_LINE" --cursor "$READLINE_POINT" \ + --cursor-prefix "${READLINE_LINE:0:READLINE_POINT}" \ --out "$out" --vars "$vars" --env-out "$env" --cwd "$PWD" \ /dev/tty 2>&1 diff --git a/widget/widget.zsh b/widget/widget.zsh index 0020eb7..4de91df 100644 --- a/widget/widget.zsh +++ b/widget/widget.zsh @@ -62,7 +62,9 @@ azform-widget() { print -r -- " BUFFER=${(qq)BUFFER}" } >> /tmp/azform-widget.log fi - azform --line "$BUFFER" --cursor "$CURSOR" --out "$out" --vars "$vars" --env-out "$env" --cwd "$PWD" /dev/tty 2>&1 + # --cursor-prefix: CURSOR counts characters but azform needs a byte + # offset; LBUFFER (the text left of the cursor) gives it exactly. + azform --line "$BUFFER" --cursor "$CURSOR" --cursor-prefix "$LBUFFER" --out "$out" --vars "$vars" --env-out "$env" --cwd "$PWD" /dev/tty 2>&1 local buf_after="" if [[ -s "$out" ]]; then BUFFER=$(cat "$out") From 407fd912957eee867ced33b1381b5147baca472d Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 26 Sep 2026 15:24:51 +0000 Subject: [PATCH 11/12] update, debug, lock, diagnostics: correctness fixes - update: SemVer precedence, so 1.0.0-rc1 users are told about 1.0.0. - debug: Close no longer races with background Event calls. - lock: fall back to a private per-user dir instead of shared /tmp, refuse symlinks, and re-check the locked inode to close the flock/unlink race; fix the outdated package comment. - diagnostics: stop rewriting parse-health.log on every append once it holds 200 entries. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Dza8X5c3XGHUjejpqGLR8q --- internal/debug/debug.go | 18 ++++-- internal/debug/debug_test.go | 19 ++++++ internal/diagnostics/health.go | 14 ++--- internal/diagnostics/health_test.go | 35 ++++++++++++ internal/lock/lock.go | 14 +---- internal/lock/lock_unix.go | 80 ++++++++++++++++++++++---- internal/lock/lock_unix_test.go | 63 ++++++++++++++++++++ internal/update/check.go | 89 +++++++++++++++++++++-------- internal/update/semver_test.go | 35 ++++++++++++ 9 files changed, 310 insertions(+), 57 deletions(-) create mode 100644 internal/update/semver_test.go diff --git a/internal/debug/debug.go b/internal/debug/debug.go index 90b8880..f9543b3 100644 --- a/internal/debug/debug.go +++ b/internal/debug/debug.go @@ -48,7 +48,7 @@ func (l *Logger) SetNow(now func() time.Time) { // Keys are emitted in alphabetical order for stable diffs. No-op on nil // receiver. func (l *Logger) Event(name string, fields map[string]any) { - if l == nil || l.w == nil { + if l == nil { return } e := make(map[string]any, len(fields)+2) @@ -84,14 +84,24 @@ func (l *Logger) Event(name string, fields map[string]any) { } buf = append(buf, '}', '\n') + // The writer is checked under the lock: background commands (metadata + // resolve and refresh) can still log after main has closed the logger. l.mu.Lock() - _, _ = l.w.Write(buf) + if l.w != nil { + _, _ = l.w.Write(buf) + } l.mu.Unlock() } -// Close flushes and closes the underlying file. Safe on nil; idempotent. +// Close flushes and closes the underlying file. Safe on nil; idempotent; +// safe to call while other goroutines are still logging. func (l *Logger) Close() error { - if l == nil || l.w == nil { + if l == nil { + return nil + } + l.mu.Lock() + defer l.mu.Unlock() + if l.w == nil { return nil } err := l.w.Close() diff --git a/internal/debug/debug_test.go b/internal/debug/debug_test.go index 86ba49e..b81da20 100644 --- a/internal/debug/debug_test.go +++ b/internal/debug/debug_test.go @@ -192,3 +192,22 @@ var _ io.WriteCloser = nopCloser{bytes.NewBuffer(nil)} // future debug sinks like a write-through in-memory buffer for tests.) var _ = nopCloser{} var _ = bytes.NewBuffer + +// Background commands may still log while main closes the logger; run +// with -race to catch unsynchronised access to the writer. +func TestEventConcurrentWithClose(t *testing.T) { + l, err := debug.Open(t.TempDir()) + if err != nil { + t.Fatalf("Open: %v", err) + } + done := make(chan struct{}) + go func() { + defer close(done) + for i := 0; i < 200; i++ { + l.Event("tick", map[string]any{"i": i}) + } + }() + _ = l.Close() + <-done + l.Event("after-close", nil) // must be a no-op, not a panic +} diff --git a/internal/diagnostics/health.go b/internal/diagnostics/health.go index 8a3c611..c1bec11 100644 --- a/internal/diagnostics/health.go +++ b/internal/diagnostics/health.go @@ -69,16 +69,14 @@ func rotateIfNeeded(path string) error { if err != nil { return fmt.Errorf("diagnostics: read log for rotation: %w", err) } - n := 1 - for _, b := range data { - if b == '\n' { - n++ - } - } - if n <= healthMaxRows { + // Count records, not separators: every entry ends in '\n', so a + // full log of healthMaxRows entries holds exactly healthMaxRows + // newlines. The previous count started at 1 and rewrote the whole + // file on every append once the log was full. + lines := splitLines(data) + if len(lines) <= healthMaxRows { return nil } - lines := splitLines(data) keep := lines[len(lines)-healthMaxRows:] tmp, err := os.CreateTemp(filepath.Dir(path), ".parse-health-*.tmp") if err != nil { diff --git a/internal/diagnostics/health_test.go b/internal/diagnostics/health_test.go index a280bf9..fbc6fc9 100644 --- a/internal/diagnostics/health_test.go +++ b/internal/diagnostics/health_test.go @@ -79,3 +79,38 @@ func splitLines(data []byte) [][]byte { } return out } + +// A full log (exactly 200 entries) is left alone: rotation rewrites the +// file only when an append takes it past the limit. +func TestAppendHealthNoRewriteAtLimit(t *testing.T) { + dir := t.TempDir() + now := time.Now() + for i := 0; i < 199; i++ { + _ = diagnostics.AppendHealth(dir, diagnostics.Entry{Command: "cmd", Params: i, SectionsOK: true}, now) + } + path := filepath.Join(dir, "parse-health.log") + before, err := os.Stat(path) + if err != nil { + t.Fatal(err) + } + // The 200th entry fits: rotation (temp file + rename) must not run. + _ = diagnostics.AppendHealth(dir, diagnostics.Entry{Command: "cmd", Params: 199, SectionsOK: true}, now) + at, _ := os.Stat(path) + if !os.SameFile(before, at) { + t.Errorf("log rewritten at exactly 200 entries") + } + before = at + data, _ := os.ReadFile(path) + if n := len(splitLines(data)); n != 200 { + t.Fatalf("log has %d entries, want 200", n) + } + _ = diagnostics.AppendHealth(dir, diagnostics.Entry{Command: "cmd", Params: 1, SectionsOK: true}, now) + after, _ := os.Stat(path) + if os.SameFile(before, after) { + t.Errorf("201st entry should rotate the log") + } + data, _ = os.ReadFile(path) + if n := len(splitLines(data)); n != 200 { + t.Errorf("after rotation: %d entries, want 200", n) + } +} diff --git a/internal/lock/lock.go b/internal/lock/lock.go index 920ccc1..bf2888b 100644 --- a/internal/lock/lock.go +++ b/internal/lock/lock.go @@ -1,6 +1,6 @@ // Package lock enforces "one azform per terminal" (spec §15.2). The lock is -// keyed by the controlling tty's (dev, inode) so multiple terminal windows -// stay independent. +// keyed by the terminal's session id (see terminalKey) so multiple terminal +// windows stay independent. package lock import ( @@ -40,13 +40,3 @@ func (l *Lock) Path() string { } return l.path } - -// runtimeDir returns the directory where lock files live. Honours -// $XDG_RUNTIME_DIR per the XDG Base Directory Specification; falls back to -// the system temp dir when the env var is unset (per spec §15.2). -func runtimeDir() string { - if d := os.Getenv("XDG_RUNTIME_DIR"); d != "" { - return d - } - return os.TempDir() -} diff --git a/internal/lock/lock_unix.go b/internal/lock/lock_unix.go index 8813be1..acc7289 100644 --- a/internal/lock/lock_unix.go +++ b/internal/lock/lock_unix.go @@ -3,8 +3,13 @@ package lock import ( + "errors" "fmt" + "io/fs" "os" + "path/filepath" + "strconv" + "syscall" "golang.org/x/sys/unix" ) @@ -20,20 +25,75 @@ func Acquire(tty *os.File) (*Lock, error) { if err != nil { return nil, err } - path := runtimeDir() + "/azform-" + key + ".lock" - - f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0o600) + dir, err := runtimeDir() if err != nil { - return nil, fmt.Errorf("azform: lock: open %s: %w", path, err) + return nil, err } - if err := unix.Flock(int(f.Fd()), unix.LOCK_EX|unix.LOCK_NB); err != nil { - _ = f.Close() - if err == unix.EWOULDBLOCK { - return nil, ErrLocked + path := filepath.Join(dir, "azform-"+key+".lock") + + // Close unlinks the file, so a competitor can open the old inode just + // before the unlink and flock it just after — while a third process + // creates and locks a fresh file at the same path. Re-checking that + // the locked fd is still the file at path closes that window; a few + // retries cover repeated losses of the race. + for attempt := 0; attempt < 5; attempt++ { + // O_NOFOLLOW: never let a planted symlink redirect the open. + f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|unix.O_NOFOLLOW, 0o600) + if err != nil { + return nil, fmt.Errorf("azform: lock: open %s: %w", path, err) + } + if err := unix.Flock(int(f.Fd()), unix.LOCK_EX|unix.LOCK_NB); err != nil { + _ = f.Close() + if err == unix.EWOULDBLOCK { + return nil, ErrLocked + } + return nil, fmt.Errorf("azform: lock: flock %s: %w", path, err) } - return nil, fmt.Errorf("azform: lock: flock %s: %w", path, err) + if sameFile(f, path) { + return &Lock{f: f, path: path}, nil + } + _ = f.Close() + } + return nil, ErrLocked +} + +// sameFile reports whether the open file f is still the file at path. +func sameFile(f *os.File, path string) bool { + held, err := f.Stat() + if err != nil { + return false + } + cur, err := os.Lstat(path) + if err != nil { + return false + } + return os.SameFile(held, cur) +} + +// runtimeDir returns the directory where lock files live: $XDG_RUNTIME_DIR +// per the XDG Base Directory Specification (per-user and private by +// definition), else a per-user directory under the system temp dir (spec +// §15.2). The fallback is not the temp dir itself: /tmp is shared, so any +// other user could pre-create azform-sid-.lock (session ids are easy to +// guess) and make every Acquire fail, or plant a symlink there. +func runtimeDir() (string, error) { + if d := os.Getenv("XDG_RUNTIME_DIR"); d != "" { + return d, nil + } + uid := os.Getuid() + dir := filepath.Join(os.TempDir(), "azform-"+strconv.Itoa(uid)) + if err := os.Mkdir(dir, 0o700); err != nil && !errors.Is(err, fs.ErrExist) { + return "", fmt.Errorf("azform: lock: create %s: %w", dir, err) + } + info, err := os.Lstat(dir) + if err != nil { + return "", fmt.Errorf("azform: lock: stat %s: %w", dir, err) + } + st, ok := info.Sys().(*syscall.Stat_t) + if !info.IsDir() || !ok || int(st.Uid) != uid || info.Mode().Perm()&0o077 != 0 { + return "", fmt.Errorf("azform: lock: %s is not a private directory owned by uid %d", dir, uid) } - return &Lock{f: f, path: path}, nil + return dir, nil } // terminalKey identifies the terminal session that owns tty. diff --git a/internal/lock/lock_unix_test.go b/internal/lock/lock_unix_test.go index 1758b56..3279007 100644 --- a/internal/lock/lock_unix_test.go +++ b/internal/lock/lock_unix_test.go @@ -6,6 +6,7 @@ import ( "errors" "os" "path/filepath" + "strconv" "strings" "testing" @@ -183,3 +184,65 @@ func TestAcquireNilTTY(t *testing.T) { t.Error("Acquire(nil tty) returned nil error, want non-nil") } } + +// Without XDG_RUNTIME_DIR the lock lives in a private per-user directory, +// not in the shared temp dir where other users could pre-create it. +func TestRuntimeDirFallbackIsPrivate(t *testing.T) { + t.Setenv("XDG_RUNTIME_DIR", "") + t.Setenv("TMPDIR", t.TempDir()) + lk, err := lock.Acquire(fakeTTY(t)) + if err != nil { + t.Fatalf("Acquire: %v", err) + } + t.Cleanup(func() { _ = lk.Close() }) + dir := filepath.Dir(lk.Path()) + if dir == os.TempDir() { + t.Fatalf("lock placed directly in the shared temp dir: %s", lk.Path()) + } + info, err := os.Stat(dir) + if err != nil { + t.Fatalf("stat: %v", err) + } + if perm := info.Mode().Perm(); perm != 0o700 { + t.Errorf("lock dir perm = %o, want 0700", perm) + } +} + +func TestRuntimeDirFallbackRejectsSharedDir(t *testing.T) { + t.Setenv("XDG_RUNTIME_DIR", "") + tmp := t.TempDir() + t.Setenv("TMPDIR", tmp) + dir := filepath.Join(tmp, "azform-"+strconv.Itoa(os.Getuid())) + if err := os.Mkdir(dir, 0o777); err != nil { + t.Fatal(err) + } + if err := os.Chmod(dir, 0o777); err != nil { + t.Fatal(err) + } + if _, err := lock.Acquire(fakeTTY(t)); err == nil { + t.Error("Acquire accepted a world-writable lock directory") + } +} + +func TestAcquireRefusesSymlink(t *testing.T) { + tty := fakeTTY(t) + dir := t.TempDir() + withRuntimeDir(t, dir) + lk, err := lock.Acquire(tty) + if err != nil { + t.Fatalf("Acquire: %v", err) + } + path := lk.Path() + _ = lk.Close() + target := filepath.Join(t.TempDir(), "victim") + if err := os.WriteFile(target, []byte("keep"), 0o600); err != nil { + t.Fatal(err) + } + if err := os.Symlink(target, path); err != nil { + t.Fatal(err) + } + if lk, err := lock.Acquire(tty); err == nil { + _ = lk.Close() + t.Error("Acquire followed a symlink at the lock path") + } +} diff --git a/internal/update/check.go b/internal/update/check.go index c55bc23..9927f1c 100644 --- a/internal/update/check.go +++ b/internal/update/check.go @@ -101,43 +101,86 @@ func newerThan(current, candidate string) string { return "" } -// compareSemver returns -1/0/1 for a vs b. Numeric components compare -// numerically; non-numeric components fall back to lexical comparison. +// compareSemver returns -1/0/1 for a vs b following SemVer precedence: +// the numeric core (major.minor.patch, missing parts = 0) decides first; +// on a tie a release outranks any of its pre-releases (1.0.0 > 1.0.0-rc1), +// and pre-releases compare identifier by identifier (numeric ones +// numerically and below alphanumeric ones). Build metadata (+…) is +// ignored. A non-numeric core such as the "dev" of a local build compares +// lexically, which keeps "dev" above every release so it is never nagged. func compareSemver(a, b string) int { - pa := splitSemver(a) - pb := splitSemver(b) - n := len(pa) - if len(pb) > n { - n = len(pb) - } + coreA, preA := splitSemver(a) + coreB, preB := splitSemver(b) + if c := compareIdentifiers(coreA, coreB, true); c != 0 { + return c + } + switch { + case len(preA) == 0 && len(preB) == 0: + return 0 + case len(preA) == 0: + return 1 + case len(preB) == 0: + return -1 + } + return compareIdentifiers(preA, preB, false) +} + +// compareIdentifiers compares dot-separated identifier lists. With +// padZero, a missing trailing identifier counts as "0" (1.2 == 1.2.0); +// otherwise the shorter list ranks lower (rc.1 < rc.1.1). +func compareIdentifiers(a, b []string, padZero bool) int { + n := max(len(a), len(b)) for i := 0; i < n; i++ { - if i >= len(pa) { + var x, y string + switch { + case i < len(a) && i < len(b): + x, y = a[i], b[i] + case padZero: + x, y = "0", "0" + if i < len(a) { + x = a[i] + } else { + y = b[i] + } + case i >= len(a): return -1 - } - if i >= len(pb) { + default: return 1 } - if pa[i] != pb[i] { - na, errA := strconv.Atoi(pa[i]) - nb, errB := strconv.Atoi(pb[i]) - if errA == nil && errB == nil { - if na < nb { - return -1 - } - return 1 - } - if pa[i] < pb[i] { + if x == y { + continue + } + nx, errX := strconv.Atoi(x) + ny, errY := strconv.Atoi(y) + switch { + case errX == nil && errY == nil: + if nx < ny { return -1 } return 1 + case errX == nil: + return -1 // numeric identifiers rank below alphanumeric ones + case errY == nil: + return 1 + case x < y: + return -1 + default: + return 1 } } return 0 } -func splitSemver(s string) []string { +// splitSemver splits "v1.2.3-rc.1+build" into (["1","2","3"], ["rc","1"]). +func splitSemver(s string) (core, pre []string) { s = strings.TrimPrefix(s, "v") - return strings.Split(s, ".") + s, _, _ = strings.Cut(s, "+") + s, preStr, hasPre := strings.Cut(s, "-") + core = strings.Split(s, ".") + if hasPre && preStr != "" { + pre = strings.Split(preStr, ".") + } + return core, pre } type cachedEntry struct { diff --git a/internal/update/semver_test.go b/internal/update/semver_test.go new file mode 100644 index 0000000..4515671 --- /dev/null +++ b/internal/update/semver_test.go @@ -0,0 +1,35 @@ +package update + +import "testing" + +func TestCompareSemver(t *testing.T) { + cases := []struct { + a, b string + want int + }{ + {"0.1.0", "0.2.0", -1}, + {"0.10.0", "0.9.0", 1}, + {"v1.0.0", "1.0.0", 0}, + {"1.2", "1.2.0", 0}, + {"1.0.0-rc1", "1.0.0", -1}, + {"1.0.0", "1.0.0-rc1", 1}, + {"1.0.0-rc.2", "1.0.0-rc.10", -1}, + {"1.0.0-alpha", "1.0.0-alpha.1", -1}, + {"1.0.0-1", "1.0.0-alpha", -1}, + {"1.0.0+build.5", "1.0.0", 0}, + {"0.3.0-5-gabcdef", "0.3.0", -1}, + {"0.3.0-5-gabcdef", "0.3.1", -1}, + } + for _, tc := range cases { + if got := compareSemver(tc.a, tc.b); got != tc.want { + t.Errorf("compareSemver(%q, %q) = %d, want %d", tc.a, tc.b, got, tc.want) + } + } + // A local "dev" build never reports an update. + if got := newerThan("dev", "9.9.9"); got != "" { + t.Errorf("newerThan(dev) = %q, want empty", got) + } + if got := newerThan("1.0.0-rc1", "1.0.0"); got != "1.0.0" { + t.Errorf("newerThan(rc1, 1.0.0) = %q, want 1.0.0", got) + } +} From 11725ef0feba50361a9a18e91109988effe662f7 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 26 Sep 2026 15:31:53 +0000 Subject: [PATCH 12/12] test: isolate e2e state dirs; own cmd.Wait in one goroutine The e2e tests ran azform against the shared default state dir. The bash test cancels 'az group create', saving a draft that carries env matches from the runner (--name $RUNNER_NAME); the zsh round-trip test then restored it and appended its input to it. With drafts now restored in var mode, '$RUNNER_NAMEwesteurope' is an undefined var and Done is blocked, so azform never exited. Each test now uses a private state dir. The zsh tests also called cmd.Wait twice, which the race detector flagged on the failure path. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Dza8X5c3XGHUjejpqGLR8q --- internal/ui/widget_bash_e2e_test.go | 3 +++ internal/ui/widget_e2e_test.go | 38 ++++++++++++++++++++--------- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/internal/ui/widget_bash_e2e_test.go b/internal/ui/widget_bash_e2e_test.go index 84bd9e9..3800d1b 100644 --- a/internal/ui/widget_bash_e2e_test.go +++ b/internal/ui/widget_bash_e2e_test.go @@ -64,6 +64,9 @@ func TestE2EBashWidgetEnvOut(t *testing.T) { "TERM=xterm-256color", "AZFORM_NO_UPDATE_CHECK=1", "AZFORM_ENV_OUT_KEEP="+keep, + // Keep this run's draft (Esc saves one) out of the shared default + // state dir, where later e2e tests would restore it. + "XDG_STATE_HOME="+tmp+"/state", ) f, err := pty.StartWithSize(cmd, &pty.Winsize{Rows: 30, Cols: 100}) if err != nil { diff --git a/internal/ui/widget_e2e_test.go b/internal/ui/widget_e2e_test.go index d77932d..64d1932 100644 --- a/internal/ui/widget_e2e_test.go +++ b/internal/ui/widget_e2e_test.go @@ -50,15 +50,24 @@ func TestE2EWidgetEnvOutRoundTrip(t *testing.T) { "--vars", varsPath, "--env-out", envPath, "--cwd", tmpDir, + // A private state dir: the default one is shared with every other + // run on the machine, and a draft left there (the bash e2e test + // cancels "group create") would be restored into this form. + "--state-dir", tmpDir+"/state", "--no-update-check", ) ptmx, err := pty.Start(cmd) if err != nil { t.Fatalf("pty.Start: %v", err) } + // One goroutine owns cmd.Wait (calling it twice is a data race); + // exited is closed, not sent on, so every reader sees it. + exited := make(chan struct{}) + var waitErr error + go func() { waitErr = cmd.Wait(); close(exited) }() defer func() { _ = cmd.Process.Kill() - _ = cmd.Wait() + <-exited _ = ptmx.Close() }() @@ -111,12 +120,10 @@ func TestE2EWidgetEnvOutRoundTrip(t *testing.T) { writeRunes("\t", 80*time.Millisecond, 400*time.Millisecond) writeRunes("\r", 80*time.Millisecond, 1500*time.Millisecond) // confirm Done - done := make(chan error, 1) - go func() { done <- cmd.Wait() }() select { - case err := <-done: - if err != nil { - t.Logf("azform exited with (possibly expected) error: %v", err) + case <-exited: + if waitErr != nil { + t.Logf("azform exited with (possibly expected) error: %v", waitErr) } case <-time.After(20 * time.Second): if data, err := os.ReadFile(envPath); err == nil { @@ -196,15 +203,24 @@ func TestE2ECancelFlushesEnvOut(t *testing.T) { "--vars", varsPath, "--env-out", envPath, "--cwd", tmpDir, + // A private state dir: the default one is shared with every other + // run on the machine, and a draft left there (the bash e2e test + // cancels "group create") would be restored into this form. + "--state-dir", tmpDir+"/state", "--no-update-check", ) ptmx, err := pty.Start(cmd) if err != nil { t.Fatalf("pty.Start: %v", err) } + // One goroutine owns cmd.Wait (calling it twice is a data race); + // exited is closed, not sent on, so every reader sees it. + exited := make(chan struct{}) + var waitErr error + go func() { waitErr = cmd.Wait(); close(exited) }() defer func() { _ = cmd.Process.Kill() - _ = cmd.Wait() + <-exited _ = ptmx.Close() }() if err := pty.Setsize(ptmx, &pty.Winsize{Rows: 40, Cols: 200}); err != nil { @@ -240,12 +256,10 @@ func TestE2ECancelFlushesEnvOut(t *testing.T) { // Esc from list mode closes the form (confirmCancel path). writeRunes("\033", 150*time.Millisecond, 3*time.Second) - done := make(chan error, 1) - go func() { done <- cmd.Wait() }() select { - case err := <-done: - if err != nil { - t.Logf("azform exited with (possibly expected) error: %v", err) + case <-exited: + if waitErr != nil { + t.Logf("azform exited with (possibly expected) error: %v", waitErr) } case <-time.After(20 * time.Second): if data, err := os.ReadFile(envPath); err == nil {