Status: Design Proposal Created: 2026-02-16 Author: dtctl team
Command aliases let users define shorthand names for frequently used dtctl commands.
An alias expands to a full dtctl command string before execution, supporting
positional parameters and shell pipelines. Aliases are stored in the user's config
file and managed through a dedicated dtctl alias command group.
Aliases are a well-established CLI pattern (git, gh) that reduces repetitive typing and lets teams share standardized shortcuts.
- Reduce repetition -- One-word shortcuts for common multi-flag commands
- Parameterization -- Positional placeholders (
$1,$2) for reusable templates - Shell expansion -- Pipe dtctl output through jq, grep, etc. via
!prefix - Team sharing -- Import/export aliases as YAML for team-wide consistency
- Safety -- Aliases cannot shadow built-in commands
- Alias chaining (alias referencing another alias)
- Context-scoped aliases (all aliases are global)
- Interactive alias builder / wizard
- Auto-generated aliases from usage patterns
# Set a simple alias
dtctl alias set my-wf "get workflows --context=production"
# Use it
dtctl my-wf
# => dtctl get workflows --context=production
# Set an alias with positional parameters
dtctl alias set wf-logs 'query "fetch logs | filter workflow.id == \"$1\"" --context=production'
# Use it -- $1 is replaced with the argument
dtctl wf-logs abc-123
# => dtctl query "fetch logs | filter workflow.id == \"abc-123\"" --context=production
# Set a shell alias (! prefix)
dtctl alias set wf-count '!dtctl get workflows -o json | jq length'
# Use it -- executed through sh
dtctl wf-count
# => 42
# List all aliases
dtctl alias list
# Delete an alias
dtctl alias delete my-wf
# Delete multiple aliases
dtctl alias delete my-wf wf-logs wf-count# Export all aliases to a file
dtctl alias export -f team-aliases.yaml
# Import aliases from a file (merges with existing)
dtctl alias import -f team-aliases.yaml
# Import with overwrite confirmation
dtctl alias import -f team-aliases.yaml
# Alias "deploy" already exists. Overwrite? [y/N]
# Force overwrite without prompting
dtctl alias import -f team-aliases.yaml --overwritealiases:
prod-wf: "get workflows --context=production"
staging-dash: "get dashboards --context=staging"
deploy: 'apply -f deployments/ --context=$1'
wf-count: "!dtctl get workflows -o json | jq length"$ dtctl alias list
NAME EXPANSION
deploy apply -f deployments/ --context=$1
prod-wf get workflows --context=production
staging-dash get dashboards --context=staging
wf-count !dtctl get workflows -o json | jq length
# Cannot shadow a built-in command
$ dtctl alias set get "describe workflows"
Error: "get" is a built-in command and cannot be used as an alias name
# Cannot shadow a built-in subcommand path
$ dtctl alias set config "get workflows"
Error: "config" is a built-in command and cannot be used as an alias name
# Unknown alias
$ dtctl nonexistent
Error: unknown command "nonexistent"
Did you mean one of these?
get
describe
# Missing required positional argument
$ dtctl alias set greet 'query "hello $1"'
$ dtctl greet
Error: alias "greet" requires at least 1 argument ($1), got 0Aliases are stored in the existing config file under a top-level aliases key:
apiVersion: v1
kind: Config
current-context: production
contexts: [...]
tokens: [...]
preferences:
output: table
editor: vim
aliases:
prod-wf: "get workflows --context=production"
deploy: "apply -f deployments/ --context=$1"
wf-count: "!dtctl get workflows -o json | jq length"This keeps aliases co-located with the rest of the configuration and means they
benefit from the existing local-config (.dtctl.yaml) vs global-config precedence.
Project-local .dtctl.yaml files can define project-specific aliases.
// pkg/config/config.go
type Config struct {
APIVersion string `yaml:"apiVersion"`
Kind string `yaml:"kind"`
CurrentContext string `yaml:"current-context"`
Contexts []NamedContext `yaml:"contexts"`
Tokens []NamedToken `yaml:"tokens"`
Preferences Preferences `yaml:"preferences"`
Aliases map[string]string `yaml:"aliases,omitempty"`
}// pkg/config/alias.go
package config
import (
"fmt"
"os"
"regexp"
"sort"
"strings"
"gopkg.in/yaml.v3"
)
// aliasNameRegex validates alias names: letters, numbers, hyphens, underscores
var aliasNameRegex = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9_-]*$`)
// maxPositionalParam is the highest $N we scan for when validating argument count
const maxPositionalParam = 9
// ValidateAliasName checks that an alias name is syntactically valid.
func ValidateAliasName(name string) error {
if name == "" {
return fmt.Errorf("alias name cannot be empty")
}
if !aliasNameRegex.MatchString(name) {
return fmt.Errorf("alias name %q is invalid: use only letters, numbers, hyphens, and underscores", name)
}
return nil
}
// SetAlias adds or updates an alias. Returns an error if the name is invalid.
// builtinCheck is called to verify the name does not shadow a built-in command.
func (c *Config) SetAlias(name, expansion string, builtinCheck func(string) bool) error {
if err := ValidateAliasName(name); err != nil {
return err
}
if builtinCheck != nil && builtinCheck(name) {
return fmt.Errorf("%q is a built-in command and cannot be used as an alias name", name)
}
if expansion == "" {
return fmt.Errorf("alias expansion cannot be empty")
}
if c.Aliases == nil {
c.Aliases = make(map[string]string)
}
c.Aliases[name] = expansion
return nil
}
// DeleteAlias removes an alias by name. Returns an error if it does not exist.
func (c *Config) DeleteAlias(name string) error {
if c.Aliases == nil {
return fmt.Errorf("alias %q not found", name)
}
if _, ok := c.Aliases[name]; !ok {
return fmt.Errorf("alias %q not found", name)
}
delete(c.Aliases, name)
return nil
}
// GetAlias returns the expansion for an alias, or empty string if not found.
func (c *Config) GetAlias(name string) (string, bool) {
if c.Aliases == nil {
return "", false
}
exp, ok := c.Aliases[name]
return exp, ok
}
// ListAliases returns all aliases sorted alphabetically by name.
func (c *Config) ListAliases() []AliasEntry {
entries := make([]AliasEntry, 0, len(c.Aliases))
for name, expansion := range c.Aliases {
entries = append(entries, AliasEntry{Name: name, Expansion: expansion})
}
sort.Slice(entries, func(i, j int) bool {
return entries[i].Name < entries[j].Name
})
return entries
}
// AliasEntry is a single alias for display purposes.
type AliasEntry struct {
Name string `table:"NAME"`
Expansion string `table:"EXPANSION"`
}
// AliasFile represents the YAML structure for import/export.
type AliasFile struct {
Aliases map[string]string `yaml:"aliases"`
}
// ExportAliases writes aliases to a file in YAML format.
func (c *Config) ExportAliases(path string) error {
af := AliasFile{Aliases: c.Aliases}
data, err := yaml.Marshal(af)
if err != nil {
return fmt.Errorf("failed to marshal aliases: %w", err)
}
return os.WriteFile(path, data, 0600)
}
// ImportAliases reads aliases from a YAML file and merges them into the config.
// If overwrite is false, existing aliases are not replaced and conflicts are
// returned as a list of names.
func (c *Config) ImportAliases(path string, overwrite bool, builtinCheck func(string) bool) ([]string, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to read alias file: %w", err)
}
var af AliasFile
if err := yaml.Unmarshal(data, &af); err != nil {
return nil, fmt.Errorf("failed to parse alias file: %w", err)
}
if c.Aliases == nil {
c.Aliases = make(map[string]string)
}
var conflicts []string
for name, expansion := range af.Aliases {
if err := ValidateAliasName(name); err != nil {
return nil, fmt.Errorf("invalid alias in file: %w", err)
}
if builtinCheck != nil && builtinCheck(name) {
return nil, fmt.Errorf("alias %q in file shadows a built-in command", name)
}
if _, exists := c.Aliases[name]; exists && !overwrite {
conflicts = append(conflicts, name)
continue
}
c.Aliases[name] = expansion
}
return conflicts, nil
}Resolution happens in cmd/root.go before Cobra parses args. This is the same
approach used by gh -- intercept os.Args, check if the first non-flag argument
matches an alias, expand it, and either re-invoke Cobra or exec a shell.
// cmd/alias_resolve.go
package cmd
import (
"fmt"
"os"
"os/exec"
"regexp"
"strconv"
"strings"
"github.com/dynatrace-oss/dtctl/pkg/config"
)
// resolveAlias checks if the first argument is an alias and expands it.
// Returns (expanded args, isShellAlias, error).
// If no alias matches, returns (nil, false, nil).
func resolveAlias(args []string, cfg *config.Config) ([]string, bool, error) {
if len(args) == 0 || cfg == nil {
return nil, false, nil
}
// Skip if the first arg is a flag
if strings.HasPrefix(args[0], "-") {
return nil, false, nil
}
name := args[0]
expansion, ok := cfg.GetAlias(name)
if !ok {
return nil, false, nil
}
// Shell alias: starts with !
if strings.HasPrefix(expansion, "!") {
shellCmd := expansion[1:]
// Append extra args
if len(args) > 1 {
shellCmd += " " + strings.Join(args[1:], " ")
}
return []string{shellCmd}, true, nil
}
// Regular alias: split and substitute positional params
parts := splitCommand(expansion)
extraArgs := args[1:]
// Substitute $1..$9
maxUsed := 0
for i, part := range parts {
parts[i] = substituteParams(part, extraArgs, &maxUsed)
}
// Append unconsumed args (those beyond the highest $N used)
if maxUsed < len(extraArgs) {
parts = append(parts, extraArgs[maxUsed:]...)
}
// Validate: if $N was used, require that many args
if maxUsed > len(extraArgs) {
return nil, false, fmt.Errorf(
"alias %q requires at least %d argument(s) ($1-$%d), got %d",
name, maxUsed, maxUsed, len(extraArgs))
}
return parts, false, nil
}
// substituteParams replaces $1..$9 in s with values from args.
// Tracks the highest parameter index used.
func substituteParams(s string, args []string, maxUsed *int) string {
re := regexp.MustCompile(`\$(\d)`)
return re.ReplaceAllStringFunc(s, func(match string) string {
idx, _ := strconv.Atoi(match[1:])
if idx > *maxUsed {
*maxUsed = idx
}
if idx >= 1 && idx <= len(args) {
return args[idx-1]
}
return match // leave unreplaced if not enough args
})
}
// splitCommand splits a command string respecting quotes.
func splitCommand(s string) []string {
var parts []string
var current strings.Builder
inSingle := false
inDouble := false
for i := 0; i < len(s); i++ {
ch := s[i]
switch {
case ch == '\'' && !inDouble:
inSingle = !inSingle
case ch == '"' && !inSingle:
inDouble = !inDouble
case ch == ' ' && !inSingle && !inDouble:
if current.Len() > 0 {
parts = append(parts, current.String())
current.Reset()
}
default:
current.WriteByte(ch)
}
}
if current.Len() > 0 {
parts = append(parts, current.String())
}
return parts
}
// execShellAlias runs a shell alias via sh -c.
func execShellAlias(shellCmd string) error {
cmd := exec.Command("sh", "-c", shellCmd)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}// cmd/root.go (modified)
func Execute() {
setupErrorHandlers(rootCmd)
// --- Alias resolution (before Cobra parses args) ---
// Load config quietly; if it fails, skip alias resolution (the real
// command will produce the proper error later).
if cfg, err := config.Load(); err == nil {
// os.Args[0] is the binary name; work with os.Args[1:]
expanded, isShell, err := resolveAlias(os.Args[1:], cfg)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
os.Exit(1)
}
if isShell {
if err := execShellAlias(expanded[0]); err != nil {
os.Exit(1)
}
return
}
if expanded != nil {
rootCmd.SetArgs(expanded)
}
}
// --- End alias resolution ---
if err := rootCmd.Execute(); err != nil {
// ... existing error handling ...
}
}The builtin check is passed as a function so the config package doesn't depend
on Cobra:
// cmd/alias.go
// isBuiltinCommand returns true if name matches any registered Cobra command.
func isBuiltinCommand(name string) bool {
for _, cmd := range rootCmd.Commands() {
if cmd.Name() == name {
return true
}
for _, alias := range cmd.Aliases {
if alias == name {
return true
}
}
}
return false
}// cmd/alias.go
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
var aliasCmd = &cobra.Command{
Use: "alias",
Short: "Manage command aliases",
Long: `Create, list, and delete shorthand names for dtctl commands.
Aliases expand before command parsing, so they work exactly like typing
the full command. Use positional parameters ($1, $2, ...) for reusable
templates, or prefix with ! for shell expansion.
Examples:
# Simple alias
dtctl alias set prod-wf "get workflows --context=production"
dtctl prod-wf
# Parameterized alias
dtctl alias set wf 'get workflow $1 --context=production'
dtctl wf my-workflow-id
# Shell alias (pipes, jq, etc.)
dtctl alias set wf-count '!dtctl get workflows -o json | jq length'
dtctl wf-count`,
}
var aliasSetCmd = &cobra.Command{
Use: "set <name> <expansion>",
Short: "Create or update an alias",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
name, expansion := args[0], args[1]
cfg, err := loadConfigRaw()
if err != nil {
return err
}
if err := cfg.SetAlias(name, expansion, isBuiltinCommand); err != nil {
return err
}
if err := saveConfig(cfg); err != nil {
return err
}
fmt.Printf("Alias %q set to %q\n", name, expansion)
return nil
},
}
var aliasListCmd = &cobra.Command{
Use: "list",
Short: "List all aliases",
Aliases: []string{"ls"},
RunE: func(cmd *cobra.Command, args []string) error {
cfg, err := loadConfigRaw()
if err != nil {
return err
}
entries := cfg.ListAliases()
if len(entries) == 0 {
fmt.Println("No aliases configured.")
fmt.Println("Use 'dtctl alias set <name> <command>' to create one.")
return nil
}
printer := NewPrinter()
return printer.PrintList(entries)
},
}
var aliasDeleteCmd = &cobra.Command{
Use: "delete <name> [name...]",
Short: "Delete one or more aliases",
Aliases: []string{"rm"},
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
cfg, err := loadConfigRaw()
if err != nil {
return err
}
for _, name := range args {
if err := cfg.DeleteAlias(name); err != nil {
return err
}
fmt.Printf("Alias %q deleted\n", name)
}
return saveConfig(cfg)
},
}
var aliasExportCmd = &cobra.Command{
Use: "export",
Short: "Export aliases to a YAML file",
RunE: func(cmd *cobra.Command, args []string) error {
file, _ := cmd.Flags().GetString("file")
if file == "" {
return fmt.Errorf("--file is required")
}
cfg, err := loadConfigRaw()
if err != nil {
return err
}
if len(cfg.Aliases) == 0 {
return fmt.Errorf("no aliases to export")
}
if err := cfg.ExportAliases(file); err != nil {
return err
}
fmt.Printf("Exported %d alias(es) to %s\n", len(cfg.Aliases), file)
return nil
},
}
var aliasImportCmd = &cobra.Command{
Use: "import",
Short: "Import aliases from a YAML file",
RunE: func(cmd *cobra.Command, args []string) error {
file, _ := cmd.Flags().GetString("file")
overwrite, _ := cmd.Flags().GetBool("overwrite")
if file == "" {
return fmt.Errorf("--file is required")
}
cfg, err := loadConfigRaw()
if err != nil {
return err
}
conflicts, err := cfg.ImportAliases(file, overwrite, isBuiltinCommand)
if err != nil {
return err
}
if len(conflicts) > 0 && !overwrite {
fmt.Printf("Skipped %d existing alias(es): %s\n",
len(conflicts), strings.Join(conflicts, ", "))
fmt.Println("Use --overwrite to replace existing aliases.")
}
if err := saveConfig(cfg); err != nil {
return err
}
fmt.Println("Aliases imported successfully.")
return nil
},
}
func init() {
rootCmd.AddCommand(aliasCmd)
aliasCmd.AddCommand(aliasSetCmd)
aliasCmd.AddCommand(aliasListCmd)
aliasCmd.AddCommand(aliasDeleteCmd)
aliasCmd.AddCommand(aliasExportCmd)
aliasCmd.AddCommand(aliasImportCmd)
aliasExportCmd.Flags().StringP("file", "f", "", "output file path")
aliasImportCmd.Flags().StringP("file", "f", "", "input file path")
aliasImportCmd.Flags().Bool("overwrite", false, "overwrite existing aliases")
}When dtctl receives a command, resolution follows this order:
- Global flags -- If the first arg starts with
-, skip alias lookup entirely - Built-in commands -- Cobra matches registered commands first (get, describe, config, alias, ctx, doctor, etc.). Built-ins always win.
- Aliases -- If no built-in matches, check the alias map. Expand and re-parse.
- Unknown command -- If no alias matches either, fall through to Cobra's error
handling (which already provides "did you mean?" suggestions via
pkg/suggest).
This means aliases can never accidentally override built-in commands, even if a
future dtctl release adds a command with the same name as an existing alias. In
that case, the built-in wins silently. Users can run dtctl alias list to audit.
# Quick access to production resources
dtctl alias set prod-wf "get workflows --context=production"
dtctl alias set staging-dash "get dashboards --context=staging -o wide"
dtctl prod-wf # => dtctl get workflows --context=production
dtctl staging-dash # => dtctl get dashboards --context=staging -o wide# Look up a workflow by name in production
dtctl alias set pw 'get workflow $1 --context=production'
dtctl pw error-handler
# => dtctl get workflow error-handler --context=production
# Deploy a specific file to a specific context
dtctl alias set deploy 'apply -f $1 --context=$2'
dtctl deploy workflows/handler.yaml production
# => dtctl apply -f workflows/handler.yaml --context=production# Count workflows
dtctl alias set wf-count '!dtctl get workflows -o json | jq length'
# Get workflow names only
dtctl alias set wf-names '!dtctl get workflows -o json | jq -r ".[].title"'
# Find workflows modified today
dtctl alias set wf-today '!dtctl get workflows -o json | jq "[.[] | select(.lastModified > \"$(date -u +%Y-%m-%dT00:00:00Z)\")]"'# Team lead creates standard aliases
cat > team-aliases.yaml <<EOF
aliases:
prod-wf: "get workflows --context=production"
staging-wf: "get workflows --context=staging"
deploy: "apply -f $1 --context=$2"
check: "diff -f $1 --context=production"
EOF
# Distribute to team
dtctl alias import -f team-aliases.yaml# .dtctl.yaml in project root
apiVersion: v1
kind: Config
current-context: ci
contexts:
- name: ci
context:
environment: https://ci.dynatrace.com
token-ref: ci-token
aliases:
deploy: "apply -f ./dynatrace/ --plain"
drift-check: "diff -f ./dynatrace/ --plain --quiet"
validate: "apply -f ./dynatrace/ --dry-run --plain"func TestSetAlias(t *testing.T) {
tests := []struct {
name string
aliasName string
expansion string
builtin func(string) bool
wantErr string
}{
{
name: "simple alias",
aliasName: "wf",
expansion: "get workflows",
},
{
name: "alias with hyphens and underscores",
aliasName: "prod-wf_v2",
expansion: "get workflows --context=production",
},
{
name: "rejects empty name",
aliasName: "",
expansion: "get workflows",
wantErr: "alias name cannot be empty",
},
{
name: "rejects invalid characters",
aliasName: "my alias!",
expansion: "get workflows",
wantErr: "invalid",
},
{
name: "rejects builtin shadow",
aliasName: "get",
expansion: "describe workflows",
builtin: func(s string) bool { return s == "get" },
wantErr: "built-in command",
},
{
name: "rejects empty expansion",
aliasName: "wf",
expansion: "",
wantErr: "cannot be empty",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := NewConfig()
err := cfg.SetAlias(tt.aliasName, tt.expansion, tt.builtin)
if tt.wantErr != "" {
require.ErrorContains(t, err, tt.wantErr)
} else {
require.NoError(t, err)
got, ok := cfg.GetAlias(tt.aliasName)
require.True(t, ok)
require.Equal(t, tt.expansion, got)
}
})
}
}
func TestDeleteAlias(t *testing.T) { /* not found, success */ }
func TestListAliases_Sorted(t *testing.T) { /* alphabetical */ }
func TestImportAliases_MergeAndConflicts(t *testing.T) { /* merge, skip, overwrite */ }
func TestImportAliases_RejectsBuiltinShadow(t *testing.T) { /* shadow in file */ }
func TestExportAliases_RoundTrip(t *testing.T) { /* export then import */ }func TestResolveAlias(t *testing.T) {
tests := []struct {
name string
args []string
aliases map[string]string
wantArgs []string
wantShell bool
wantErr string
}{
{
name: "simple expansion",
args: []string{"wf"},
aliases: map[string]string{"wf": "get workflows"},
wantArgs: []string{"get", "workflows"},
},
{
name: "positional params",
args: []string{"pw", "my-id"},
aliases: map[string]string{"pw": "get workflow $1"},
wantArgs: []string{"get", "workflow", "my-id"},
},
{
name: "extra args appended",
args: []string{"wf", "--context=prod"},
aliases: map[string]string{"wf": "get workflows"},
wantArgs: []string{"get", "workflows", "--context=prod"},
},
{
name: "shell alias",
args: []string{"count"},
aliases: map[string]string{"count": "!dtctl get wf -o json | jq length"},
wantArgs: []string{"dtctl get wf -o json | jq length"},
wantShell: true,
},
{
name: "missing required arg",
args: []string{"pw"},
aliases: map[string]string{"pw": "get workflow $1"},
wantErr: "requires at least 1 argument",
},
{
name: "no match returns nil",
args: []string{"unknown"},
aliases: map[string]string{"wf": "get workflows"},
wantArgs: nil,
},
{
name: "flag as first arg skips lookup",
args: []string{"--help"},
aliases: map[string]string{"--help": "bad"},
wantArgs: nil,
},
}
// ...
}
func TestSplitCommand_Quotes(t *testing.T) {
tests := []struct {
input string
want []string
}{
{`get workflows`, []string{"get", "workflows"}},
{`query "fetch logs | limit 10"`, []string{"query", "fetch logs | limit 10"}},
{`get workflow 'my workflow'`, []string{"get", "workflow", "my workflow"}},
}
// ...
}func TestAliasSetAndList(t *testing.T) { /* set, then list, verify output */ }
func TestAliasDelete(t *testing.T) { /* set, delete, verify gone */ }
func TestAliasImportExport(t *testing.T) { /* export, modify, import, verify */ }
func TestAliasExecution(t *testing.T) { /* set alias, execute it, verify args */ }
func TestAliasCannotShadowBuiltin(t *testing.T) { /* try to set "get", verify error */ }| Scenario | Error message |
|---|---|
| Invalid alias name | alias name "..." is invalid: use only letters, numbers, hyphens, and underscores |
| Shadow built-in | "get" is a built-in command and cannot be used as an alias name |
| Empty expansion | alias expansion cannot be empty |
| Delete nonexistent | alias "foo" not found |
| Missing positional arg | alias "pw" requires at least 1 argument ($1), got 0 |
| Import invalid YAML | failed to parse alias file: ... |
| Import shadow | alias "get" in file shadows a built-in command |
| Export with no aliases | no aliases to export |
Pros: Clean separation, easy to share just aliases.
Cons: Another file to manage; config loading becomes more complex; can't use
project-local .dtctl.yaml for project aliases without extra logic.
Decision: Store in existing config. One file, one source of truth. Export
covers the sharing use case.
Pros: More composable. Cons: Risk of circular references, harder to debug, increases complexity significantly for marginal value. Neither git nor gh support this. Decision: Not supported. Keep it simple. Shell aliases cover complex composition.
Pros: Full Cobra integration (help text, completion).
Cons: Requires loading config before Cobra init, complicates startup, aliases
would appear in --help output cluttering it.
Decision: Resolve before Cobra parses. Aliases are user shortcuts, not
first-class commands.
Pros: Explicit "pass everything through" syntax.
Cons: The current design already appends unused args, so $@ is redundant.
Extra syntax to learn for no benefit.
Decision: Not needed. Unconsumed args are appended automatically.
These are explicitly out of scope for the initial implementation but could be added later if there is demand:
- Alias descriptions -- Optional
--descriptiononalias setfor self-documenting aliases - Alias history -- Track when aliases were last used for cleanup suggestions
- Completion for alias names -- Shell completion that includes alias names
- Alias validation on set -- Parse the expansion and warn if it references unknown commands
- Add
Aliases map[string]stringtoConfigstruct inpkg/config/config.go - Create
pkg/config/alias.gowithSetAlias,DeleteAlias,GetAlias,ListAliases,ImportAliases,ExportAliases,ValidateAliasName - Create
pkg/config/alias_test.gowith table-driven tests - Create
cmd/alias.gowithalias set/list/delete/import/exportcommands - Create
cmd/alias_resolve.gowithresolveAlias,splitCommand,substituteParams - Create
cmd/alias_resolve_test.gowith resolution and quote-parsing tests - Modify
cmd/root.goExecute()to callresolveAliasbeforerootCmd.Execute() - Update
examples/config-example.yamlwith alias examples - Update
docs/dev/IMPLEMENTATION_STATUS.md
- Alias CRUD works reliably (set, get, list, delete)
- Positional parameter substitution handles $1-$9 correctly
- Shell aliases execute through
sh -cand inherit stdin/stdout/stderr - Built-in commands can never be shadowed
- Import/export round-trips without data loss
- All tests pass with race detection enabled
- Config file remains valid YAML after alias operations
- git aliases: https://git-scm.com/docs/git-config#Documentation/git-config.txt-aliasltnamegt
- gh alias: https://cli.github.com/manual/gh_alias
- Cobra command framework: https://github.com/spf13/cobra