package main import ( "bytes" "os" "path/filepath" "reflect" "strings" "testing" ) // withAliases sets the aliases for a test and restores them afterwards. func withAliases(t *testing.T, defs map[string]string) { t.Helper() aliasMu.Lock() saved := aliases aliases = map[string]string{} for name, value := range defs { aliases[name] = value } aliasMu.Unlock() t.Cleanup(func() { aliasMu.Lock() aliases = saved aliasMu.Unlock() }) } func TestExpandAliases(t *testing.T) { withAliases(t, map[string]string{ "ll": "ls -l", "ls": "ls --color", "..": "cd ..", "e": "echo ", "w": "world", "a": "b", "b": "a", "multi": "cd /tmp && ls", }) tests := []struct { input, want string }{ {"ll", "ls --color -l"}, {" ll -a", " ls --color -l -a"}, {"ls", "ls --color"}, {"..", "cd .."}, // Only the command name is replaced {"echo ll", "echo ll"}, {"ll ll", "ls --color -l ll"}, // ... also after ;, &&, ||, | and & {"ll; ll && ll || ll | ll & ll", "ls --color -l; ls --color -l && ls --color -l || ls --color -l | ls --color -l & ls --color -l"}, // but not after redirections with & {"ll 2>&1 ll", "ls --color -l 2>&1 ll"}, {"ll &> ll", "ls --color -l &> ll"}, // Quoted or escaped words are not replaced {`\ll`, `\ll`}, {`'ll'`, `'ll'`}, {`"ll"`, `"ll"`}, {`echo "a; ll"`, `echo "a; ll"`}, {`echo 'a | ll'`, `echo 'a | ll'`}, // A trailing blank makes the next word a command name too {"e w", "echo world"}, {"e e w", "echo echo world"}, // Recursion stops at aliases that are already being expanded {"a", "a"}, {"b", "b"}, // The value can contain several commands {"multi", "cd /tmp && ls --color"}, // Comments are left alone {"echo x # ll", "echo x # ll"}, } for _, tc := range tests { if got := expandAliases(tc.input); got != tc.want { t.Errorf("expandAliases(%q) = %q, want %q", tc.input, got, tc.want) } } } func TestBuiltinAlias(t *testing.T) { withAliases(t, nil) run := func(args ...string) (string, error) { var out bytes.Buffer err := builtinAlias(append([]string{"alias"}, args...), &out) return out.String(), err } if _, err := run("ll=ls -l", "q=echo it's", "..=cd .."); err != nil { t.Fatal(err) } out, _ := run() want := "alias ..='cd ..'\nalias ll='ls -l'\nalias q='echo it'\\''s'\n" if out != want { t.Errorf("alias = %q, want %q", out, want) } if out, _ := run("ll"); out != "alias ll='ls -l'\n" { t.Errorf("alias ll = %q", out) } if _, err := run("nope"); err == nil || err.Error() != "alias: nope: not found" { t.Errorf("alias nope: %v", err) } for _, name := range []string{"a/b", "a=b", "$x", "a b", ""} { if isAliasName(name) { t.Errorf("isAliasName(%q) = true", name) } } if _, err := run("a/b=x"); err == nil || !strings.Contains(err.Error(), "invalid alias name") { t.Errorf("alias a/b=x: %v", err) } if err := builtinUnalias([]string{"unalias", "ll", "nope"}); err == nil || err.Error() != "unalias: nope: not found" { t.Errorf("unalias nope: %v", err) } if _, ok := lookupAlias("ll"); ok { t.Error("ll still defined after unalias") } builtinUnalias([]string{"unalias", "-a"}) if names := aliasNames(); len(names) != 0 { t.Errorf("aliases after unalias -a: %q", names) } } func TestExecInput_Aliases(t *testing.T) { withAliases(t, nil) origDir, _ := os.Getwd() defer os.Chdir(origDir) dir := t.TempDir() os.Chdir(dir) defer func(saved bool) { interactiveShell = saved }(interactiveShell) // Aliases are expanded in interactive shells only interactiveShell = true execInput(`alias say='echo hello' to=out`) execInput("say world > out") if got, _ := os.ReadFile("out"); string(got) != "hello world\n" { t.Errorf("alias in interactive shell: out = %q", got) } interactiveShell = false os.Remove("out") if err := execInput("say world > out"); err == nil { t.Error("alias was expanded in a non-interactive shell") } // alias works in pipelines (listing), unalias does not interactiveShell = true execInput("alias | grep say > out") if got, _ := os.ReadFile(filepath.Join(dir, "out")); string(got) != "alias say='echo hello'\n" { t.Errorf("alias | grep = %q", got) } if err := execInput("echo | unalias say"); err == nil { t.Error("unalias in a pipeline must fail") } } func TestShellCompleter_Aliases(t *testing.T) { withAliases(t, map[string]string{"gst": "git status"}) t.Setenv("PATH", t.TempDir()) got, length := completeLine("gs") if !reflect.DeepEqual(got, []string{"t "}) || length != 2 { t.Errorf("completion of gs = %q, %d", got, length) } }