add aliases
- alias name=value defines aliases, alias lists or shows them in a form that can be read back, unalias removes them (unalias -a: all) - the first word of every command (at the start and after ;, &, &&, || and |) is replaced, the value is expanded again except for aliases that are already being expanded (alias ls='ls --color' works) - a value ending with a blank makes the next word a command name too - quoted or escaped words (\ll, 'll') skip the alias - aliases are only expanded in interactive shells and their startup files, like in bash - tab completion includes aliases Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
dae4087a94
commit
187de30351
4 files changed
+422
-15
No files matched your search
@@ -0,0 +1,191 @@
|
||||
package main
|
||||
|
||||
// Aliases: like in bash, an alias replaces the first word of a command and
|
||||
// is only expanded in interactive shells (including the startup files), not
|
||||
// in scripts or with gosh -c.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
aliasMu sync.Mutex
|
||||
aliases = map[string]string{}
|
||||
)
|
||||
|
||||
// isAliasName reports whether s can be an alias name: letters, digits and a
|
||||
// few punctuation characters, but nothing that is special to the shell.
|
||||
func isAliasName(s string) bool {
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
for _, r := range s {
|
||||
switch {
|
||||
case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9':
|
||||
case strings.ContainsRune("_-.+:,@%^", r):
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// lookupAlias returns the value of an alias.
|
||||
func lookupAlias(name string) (string, bool) {
|
||||
aliasMu.Lock()
|
||||
defer aliasMu.Unlock()
|
||||
value, ok := aliases[name]
|
||||
return value, ok
|
||||
}
|
||||
|
||||
// aliasNames returns the names of all aliases, sorted.
|
||||
func aliasNames() []string {
|
||||
aliasMu.Lock()
|
||||
defer aliasMu.Unlock()
|
||||
names := make([]string, 0, len(aliases))
|
||||
for name := range aliases {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
return names
|
||||
}
|
||||
|
||||
// expandAliases replaces aliases at the start of every command in line (at
|
||||
// the start and after ;, &, &&, || and |). The value of an alias is expanded
|
||||
// again, except for aliases that are already being expanded, so that e.g.
|
||||
// alias ls='ls --color' works. If the value ends with a blank, the next word
|
||||
// is checked for an alias too. Quoted or escaped words (\ls, 'ls') are never
|
||||
// replaced.
|
||||
func expandAliases(line string) string {
|
||||
return expandAliasesIn(line, map[string]bool{})
|
||||
}
|
||||
|
||||
func expandAliasesIn(line string, active map[string]bool) string {
|
||||
var (
|
||||
out strings.Builder
|
||||
quote byte
|
||||
escaped bool
|
||||
commandPos = true // the next word is the name of a command
|
||||
)
|
||||
|
||||
for i := 0; i < len(line); i++ {
|
||||
ch := line[i]
|
||||
if quote == 0 && !escaped && commandPos {
|
||||
if ch == ' ' || ch == '\t' {
|
||||
out.WriteByte(ch)
|
||||
continue
|
||||
}
|
||||
commandPos = false
|
||||
end := i
|
||||
for end < len(line) && strings.IndexByte(" \t\n;&|<>", line[end]) < 0 {
|
||||
end++
|
||||
}
|
||||
name := line[i:end]
|
||||
if value, ok := lookupAlias(name); ok && isAliasName(name) && !active[name] {
|
||||
active[name] = true
|
||||
out.WriteString(expandAliasesIn(value, active))
|
||||
delete(active, name)
|
||||
// A trailing blank makes the next word a command name too
|
||||
commandPos = strings.HasSuffix(value, " ") || strings.HasSuffix(value, "\t")
|
||||
i = end - 1
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
out.WriteByte(ch)
|
||||
switch {
|
||||
case escaped:
|
||||
escaped = false
|
||||
case quote == '\'':
|
||||
if ch == '\'' {
|
||||
quote = 0
|
||||
}
|
||||
case ch == '\\':
|
||||
escaped = true
|
||||
case quote == '"':
|
||||
if ch == '"' {
|
||||
quote = 0
|
||||
}
|
||||
case ch == '"' || ch == '\'':
|
||||
quote = ch
|
||||
case ch == '#' && (i == 0 || strings.IndexByte(" \t", line[i-1]) >= 0):
|
||||
// Comment until the end of the line
|
||||
out.WriteString(line[i+1:])
|
||||
return out.String()
|
||||
case ch == ';' || ch == '|':
|
||||
commandPos = true
|
||||
case ch == '&':
|
||||
// & and && separate commands, &>, >& and <& do not
|
||||
if !(i+1 < len(line) && line[i+1] == '>') && !(i > 0 && strings.IndexByte("<>", line[i-1]) >= 0) {
|
||||
commandPos = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return out.String()
|
||||
}
|
||||
|
||||
// quoteAlias quotes a value for the output of alias, so that it can be read
|
||||
// back: 'value' with ' written as '\”.
|
||||
func quoteAlias(value string) string {
|
||||
return "'" + strings.ReplaceAll(value, "'", `'\''`) + "'"
|
||||
}
|
||||
|
||||
// builtinAlias defines aliases (alias name=value ...), prints them (alias
|
||||
// name ...) or lists all of them (alias).
|
||||
func builtinAlias(args []string, stdout io.Writer) error {
|
||||
if len(args) == 1 {
|
||||
for _, name := range aliasNames() {
|
||||
value, _ := lookupAlias(name)
|
||||
fmt.Fprintf(stdout, "alias %s=%s\n", name, sanitize(quoteAlias(value)))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var err error
|
||||
for _, arg := range args[1:] {
|
||||
name, value, define := strings.Cut(arg, "=")
|
||||
if !isAliasName(name) {
|
||||
err = fmt.Errorf("alias: `%s': invalid alias name", sanitize(name))
|
||||
continue
|
||||
}
|
||||
if define {
|
||||
aliasMu.Lock()
|
||||
aliases[name] = value
|
||||
aliasMu.Unlock()
|
||||
continue
|
||||
}
|
||||
if value, ok := lookupAlias(name); ok {
|
||||
fmt.Fprintf(stdout, "alias %s=%s\n", name, sanitize(quoteAlias(value)))
|
||||
} else {
|
||||
err = fmt.Errorf("alias: %s: not found", sanitize(name))
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// builtinUnalias removes aliases (unalias name ...) or all of them
|
||||
// (unalias -a).
|
||||
func builtinUnalias(args []string) error {
|
||||
if len(args) < 2 {
|
||||
return fmt.Errorf("usage: unalias [-a] name ...")
|
||||
}
|
||||
aliasMu.Lock()
|
||||
defer aliasMu.Unlock()
|
||||
if args[1] == "-a" {
|
||||
aliases = map[string]string{}
|
||||
return nil
|
||||
}
|
||||
var err error
|
||||
for _, name := range args[1:] {
|
||||
if _, ok := aliases[name]; !ok {
|
||||
err = fmt.Errorf("unalias: %s: not found", sanitize(name))
|
||||
continue
|
||||
}
|
||||
delete(aliases, name)
|
||||
}
|
||||
return err
|
||||
}
|
||||
Reference in new issue
Block a user