Complete aliases that expand to a shell escape
`alias ll '!ls -la'` makes everything after `ll` a shell argument just as surely as typing the `!` does, but Tab there still went to the builtin command tree and found nothing. The dispatch now asks what a line will turn into rather than how it starts: a '!' escape, or a name that is not a builtin and resolves to an alias whose body starts with '!'. Only the arguments complete — the command word is fixed by the alias body, so `ll vi` offers the file, never the editor. An alias to a builtin stays with the builtin tree. Only the alias itself is inspected, not what its expansion might expand to in turn: an alias chain can rewrite its own arguments, and guessing at that would offer candidates for a command line other than the one being built. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+45
-7
@@ -24,17 +24,55 @@ import (
|
||||
// installed on the machine running them.
|
||||
var shellCommandList = pathExecutables
|
||||
|
||||
// shellCandidates returns the completion candidates for the text to the left of
|
||||
// the cursor, together with the prefix they replace. text is expected to start
|
||||
// (after leading blanks) with the '!' that marks a shell escape.
|
||||
func shellCandidates(text string) (cands []string, prefix string) {
|
||||
body, ok := strings.CutPrefix(strings.TrimLeft(text, " \t"), "!")
|
||||
// completeShellLine returns the candidates and the prefix they replace for a
|
||||
// line that is headed for a shell. ok is false for any other line, which is
|
||||
// then left to the builtin command tree.
|
||||
func completeShellLine(typed string) (cands []string, prefix string, ok bool) {
|
||||
body, allowCommand, ok := shellLine(typed)
|
||||
if !ok {
|
||||
return nil, ""
|
||||
return nil, "", false
|
||||
}
|
||||
cands, prefix = shellCandidates(body, allowCommand)
|
||||
return cands, prefix, true
|
||||
}
|
||||
|
||||
// shellLine works out which part of a typed line will reach a shell, and
|
||||
// whether its command word is still open for completion. Two things get there:
|
||||
// a '!' escape, and an alias that expands to one — `alias ll '!ls -la'` makes
|
||||
// everything after `ll` a shell argument just as surely.
|
||||
//
|
||||
// Only the alias itself is inspected, not what its expansion might expand to
|
||||
// again: an alias chain can rewrite its arguments, and guessing at that would
|
||||
// offer candidates for a command line that is not the one being built.
|
||||
func shellLine(typed string) (body string, allowCommand, ok bool) {
|
||||
trimmed := strings.TrimLeft(typed, " \t")
|
||||
if rest, found := strings.CutPrefix(trimmed, "!"); found {
|
||||
return rest, true, true
|
||||
}
|
||||
|
||||
// the alias name has to be complete — while it is still being typed there
|
||||
// is no way to know what it will turn out to be
|
||||
sep := strings.IndexAny(trimmed, " \t")
|
||||
if sep < 0 {
|
||||
return "", false, false
|
||||
}
|
||||
name := trimmed[:sep]
|
||||
if isBuiltin(name) { // a builtin can never be shadowed by an alias
|
||||
return "", false, false
|
||||
}
|
||||
expansion, defined := aliases[name]
|
||||
if !defined || !strings.HasPrefix(strings.TrimSpace(expansion), "!") {
|
||||
return "", false, false
|
||||
}
|
||||
// the command comes from the alias body, so only arguments are left to complete
|
||||
return trimmed[sep:], false, true
|
||||
}
|
||||
|
||||
// shellCandidates completes the last word of a shell command line. allowCommand
|
||||
// says whether its first word may still be completed against PATH.
|
||||
func shellCandidates(body string, allowCommand bool) (cands []string, prefix string) {
|
||||
word := body[strings.LastIndexAny(body, " \t")+1:]
|
||||
inCommand := strings.TrimLeft(body[:len(body)-len(word)], " \t") == ""
|
||||
inCommand := allowCommand && strings.TrimLeft(body[:len(body)-len(word)], " \t") == ""
|
||||
|
||||
// a command word without a separator names something on PATH; with one it
|
||||
// is a path like ./script, exactly as a shell reads it
|
||||
|
||||
Reference in New Issue
Block a user