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 } } if quote != '\'' && !escaped && isSubstStart(line, i) { // $(...) is copied as it is; the subshell does not expand // aliases if end := substEnd(line, i); end >= 0 { out.WriteString(line[i : end+1]) i = end 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 }