mirror of
https://github.com/wavetermdev/waveterm.git
synced 2024-12-29 17:58:54 +01:00
859 lines
24 KiB
Go
859 lines
24 KiB
Go
package shparse
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
|
|
"github.com/scripthaus-dev/sh2-server/pkg/utilfn"
|
|
)
|
|
|
|
//
|
|
// cmds := cmd (sep cmd)*
|
|
// sep := ';' | '&' | '&&' | '||' | '|' | '\n'
|
|
// cmd := simple-cmd | compound-command redirect-list?
|
|
// compound-command := brace-group | subshell | for-clause | case-clause | if-clause | while-clause | until-clause
|
|
// brace-group := '{' cmds '}'
|
|
// subshell := '(' cmds ')'
|
|
// simple-command := cmd-prefix cmd-word (io-redirect)*
|
|
// cmd-prefix := (io-redirect | assignment)*
|
|
// cmd-suffix := (io-redirect | word)*
|
|
// cmd-name := word
|
|
// cmd-word := word
|
|
// io-redirect := (io-number? io-file) | (io-number? io-here)
|
|
// io-file := ('<' | '<&' | '>' | '>&' | '>>' | '>|' ) filename
|
|
// io-here := ('<<' | '<<-') here_end
|
|
// here-end := word
|
|
// if-clause := 'if' compound-list 'then' compound-list else-part 'fi'
|
|
// else-part := 'elif' compound-list 'then' compound-list
|
|
// | 'elif' compount-list 'then' compound-list else-part
|
|
// | 'else' compound-list
|
|
// compound-list := linebreak term sep?
|
|
//
|
|
//
|
|
//
|
|
// A correctly-formed brace expansion must contain unquoted opening and closing braces, and at least one unquoted comma or a valid sequence expression
|
|
// Any incorrectly formed brace expansion is left unchanged.
|
|
//
|
|
// ambiguity between $((...)) and $((ls); ls)
|
|
// ambiguity between foo=([0]=hell) and foo=([abc)
|
|
// tokenization https://pubs.opengroup.org/onlinepubs/7908799/xcu/chap2.html#tag_001_003
|
|
|
|
// can-extend: WordTypeLit, WordTypeSimpleVar, WordTypeVarBrace, WordTypeDQ, WordTypeDDQ, WordTypeSQ, WordTypeDSQ
|
|
const (
|
|
WordTypeRaw = "raw"
|
|
WordTypeLit = "lit" // (can-extend)
|
|
WordTypeOp = "op" // single: & ; | ( ) < > \n multi(2): && || ;; << >> <& >& <> >| (( multi(3): <<- ('((' requires special processing)
|
|
WordTypeKey = "key" // if then else elif fi do done case esac while until for in { } ! (( [[
|
|
WordTypeGroup = "grp" // contains other words e.g. "hello"foo'bar'$x (has-subs)
|
|
WordTypeSimpleVar = "svar" // simplevar $ (can-extend)
|
|
|
|
WordTypeDQ = "dq" // " (quote-context) (can-extend) (has-subs)
|
|
WordTypeDDQ = "ddq" // $" (quote-context) (can-extend) (has-subs)
|
|
WordTypeVarBrace = "varb" // ${ (quote-context) (can-extend) (internals not parsed)
|
|
WordTypeDP = "dp" // $( (quote-context) (has-subs)
|
|
WordTypeBQ = "bq" // ` (quote-context) (has-subs)
|
|
|
|
WordTypeSQ = "sq" // ' (can-extend)
|
|
WordTypeDSQ = "dsq" // $' (can-extend)
|
|
WordTypeDPP = "dpp" // $(( (internals not parsed)
|
|
WordTypePP = "pp" // (( (internals not parsed)
|
|
WordTypeDB = "db" // $[ (internals not parsed)
|
|
)
|
|
|
|
const (
|
|
CmdTypeNone = "none" // holds control structures: '(' ')' 'for' 'while' etc.
|
|
CmdTypeSimple = "simple" // holds real commands
|
|
)
|
|
|
|
type WordType struct {
|
|
Type string
|
|
Offset int
|
|
QC QuoteContext
|
|
Raw []rune
|
|
Complete bool
|
|
Prefix []rune
|
|
Subs []*WordType
|
|
}
|
|
|
|
type CmdType struct {
|
|
Type string
|
|
AssignmentWords []*WordType
|
|
Words []*WordType
|
|
NoneComplete bool // set to true when last-word is a "separator"
|
|
}
|
|
|
|
type QuoteContext []string
|
|
|
|
var wordMetaMap map[string]wordMeta
|
|
|
|
// same order as https://www.gnu.org/software/bash/manual/html_node/Reserved-Words.html
|
|
var bashReservedWords = []string{
|
|
"if", "then", "elif", "else", "fi", "time",
|
|
"for", "in", "until", "while", "do", "done",
|
|
"case", "esac", "coproc", "select", "function",
|
|
"{", "}", "[[", "]]", "!",
|
|
}
|
|
|
|
// special reserved words: "for", "in", "case", "select", "function", "[[", and "]]"
|
|
|
|
var bashNoneRW = []string{
|
|
"if", "then",
|
|
"elif", "else", "fi", "time",
|
|
"until", "while", "do", "done",
|
|
"esac", "coproc",
|
|
"{", "}", "!",
|
|
}
|
|
|
|
type wordMeta struct {
|
|
Type string
|
|
EmptyWord []rune
|
|
PrefixLen int
|
|
SuffixLen int
|
|
CanExtend bool
|
|
QuoteContext bool
|
|
}
|
|
|
|
func makeWordMeta(wtype string, emptyWord string, prefixLen int, suffixLen int, canExtend bool, quoteContext bool) {
|
|
if len(emptyWord) != prefixLen+suffixLen {
|
|
panic(fmt.Sprintf("invalid empty word %s %d %d", emptyWord, prefixLen, suffixLen))
|
|
}
|
|
wordMetaMap[wtype] = wordMeta{wtype, []rune(emptyWord), prefixLen, suffixLen, canExtend, quoteContext}
|
|
}
|
|
|
|
func init() {
|
|
wordMetaMap = make(map[string]wordMeta)
|
|
makeWordMeta(WordTypeRaw, "", 0, 0, false, false)
|
|
makeWordMeta(WordTypeLit, "", 0, 0, true, false)
|
|
makeWordMeta(WordTypeOp, "", 0, 0, false, false)
|
|
makeWordMeta(WordTypeKey, "", 0, 0, false, false)
|
|
makeWordMeta(WordTypeGroup, "", 0, 0, false, false)
|
|
makeWordMeta(WordTypeSimpleVar, "$", 1, 0, true, false)
|
|
makeWordMeta(WordTypeVarBrace, "${}", 2, 1, true, true)
|
|
makeWordMeta(WordTypeDQ, `""`, 1, 1, true, true)
|
|
makeWordMeta(WordTypeDDQ, `$""`, 2, 1, true, true)
|
|
makeWordMeta(WordTypeDP, "$()", 2, 1, false, false)
|
|
makeWordMeta(WordTypeBQ, "``", 1, 1, false, false)
|
|
makeWordMeta(WordTypeSQ, "''", 1, 1, true, false)
|
|
makeWordMeta(WordTypeDSQ, "$''", 2, 1, true, false)
|
|
makeWordMeta(WordTypeDPP, "$(())", 3, 2, false, false)
|
|
makeWordMeta(WordTypePP, "(())", 2, 2, false, false)
|
|
makeWordMeta(WordTypeDB, "$[]", 2, 1, false, false)
|
|
}
|
|
|
|
func MakeEmptyWord(wtype string, qc QuoteContext, offset int) *WordType {
|
|
meta := wordMetaMap[wtype]
|
|
if meta.Type == "" {
|
|
meta = wordMetaMap[WordTypeRaw]
|
|
}
|
|
rtn := &WordType{Type: meta.Type, QC: qc, Offset: offset, Complete: true}
|
|
if len(meta.EmptyWord) > 0 {
|
|
rtn.Raw = append([]rune(nil), meta.EmptyWord...)
|
|
}
|
|
return rtn
|
|
}
|
|
|
|
func (qc QuoteContext) push(q string) QuoteContext {
|
|
rtn := make([]string, 0, len(qc)+1)
|
|
rtn = append(rtn, qc...)
|
|
rtn = append(rtn, q)
|
|
return rtn
|
|
}
|
|
|
|
func (qc QuoteContext) cur() string {
|
|
if len(qc) == 0 {
|
|
return ""
|
|
}
|
|
return qc[len(qc)-1]
|
|
}
|
|
|
|
func (qc QuoteContext) clone() QuoteContext {
|
|
if len(qc) == 0 {
|
|
return nil
|
|
}
|
|
return append([]string(nil), qc...)
|
|
}
|
|
|
|
func makeRepeatStr(ch byte, slen int) string {
|
|
if slen == 0 {
|
|
return ""
|
|
}
|
|
rtn := make([]byte, slen)
|
|
for i := 0; i < slen; i++ {
|
|
rtn[i] = ch
|
|
}
|
|
return string(rtn)
|
|
}
|
|
|
|
func (w *WordType) isBlank() bool {
|
|
return w.Type == WordTypeLit && len(w.Raw) == 0
|
|
}
|
|
|
|
func (w *WordType) contentEndPos() int {
|
|
if !w.Complete {
|
|
return len(w.Raw)
|
|
}
|
|
wmeta := wordMetaMap[w.Type]
|
|
return len(w.Raw) - wmeta.SuffixLen
|
|
}
|
|
|
|
func (w *WordType) contentStartPos() int {
|
|
wmeta := wordMetaMap[w.Type]
|
|
return wmeta.PrefixLen
|
|
}
|
|
|
|
func (w *WordType) uncompletable() bool {
|
|
switch w.Type {
|
|
case WordTypeRaw, WordTypeOp, WordTypeKey, WordTypeDPP, WordTypePP, WordTypeDB, WordTypeBQ, WordTypeDP:
|
|
return true
|
|
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func (w *WordType) stringWithPos(pos int) string {
|
|
notCompleteFlag := " "
|
|
if !w.Complete {
|
|
notCompleteFlag = "*"
|
|
}
|
|
str := string(w.Raw)
|
|
if pos != -1 {
|
|
str = utilfn.StrWithPos{Str: str, Pos: pos}.String()
|
|
}
|
|
return fmt.Sprintf("%-4s[%3d]%s %s%q", w.Type, w.Offset, notCompleteFlag, makeRepeatStr('_', len(w.Prefix)), str)
|
|
}
|
|
|
|
func (w *WordType) String() string {
|
|
notCompleteFlag := " "
|
|
if !w.Complete {
|
|
notCompleteFlag = "*"
|
|
}
|
|
return fmt.Sprintf("%-4s[%3d]%s %s%q", w.Type, w.Offset, notCompleteFlag, makeRepeatStr('_', len(w.Prefix)), string(w.Raw))
|
|
}
|
|
|
|
// offset = -1 for don't show
|
|
func dumpWords(words []*WordType, indentStr string, offset int) {
|
|
wrotePos := false
|
|
for _, word := range words {
|
|
posInWord := false
|
|
if !wrotePos && offset != -1 && offset <= word.Offset {
|
|
fmt.Printf("%s* [%3d] [*]\n", indentStr, offset)
|
|
wrotePos = true
|
|
}
|
|
if !wrotePos && offset != -1 && offset < word.Offset+len(word.Raw) {
|
|
fmt.Printf("%s%s\n", indentStr, word.stringWithPos(offset-word.Offset))
|
|
wrotePos = true
|
|
posInWord = true
|
|
} else {
|
|
fmt.Printf("%s%s\n", indentStr, word.String())
|
|
}
|
|
if len(word.Subs) > 0 {
|
|
if posInWord {
|
|
wmeta := wordMetaMap[word.Type]
|
|
dumpWords(word.Subs, indentStr+" ", offset-word.Offset-wmeta.PrefixLen)
|
|
} else {
|
|
dumpWords(word.Subs, indentStr+" ", -1)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func dumpCommands(cmds []*CmdType, indentStr string, pos int) {
|
|
for _, cmd := range cmds {
|
|
fmt.Printf("%sCMD: %s [%d] pos:%d\n", indentStr, cmd.Type, len(cmd.Words), pos)
|
|
dumpWords(cmd.AssignmentWords, indentStr+" *", pos)
|
|
dumpWords(cmd.Words, indentStr+" ", pos)
|
|
}
|
|
}
|
|
|
|
func wordsToStr(words []*WordType) string {
|
|
var buf bytes.Buffer
|
|
for _, word := range words {
|
|
if len(word.Prefix) > 0 {
|
|
buf.WriteString(string(word.Prefix))
|
|
}
|
|
buf.WriteString(string(word.Raw))
|
|
}
|
|
return buf.String()
|
|
}
|
|
|
|
// recognizes reserved words in first position
|
|
func convertToAnyReservedWord(w *WordType) bool {
|
|
if w == nil || w.Type != WordTypeLit {
|
|
return false
|
|
}
|
|
rawVal := string(w.Raw)
|
|
for _, rw := range bashReservedWords {
|
|
if rawVal == rw {
|
|
w.Type = WordTypeKey
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// recognizes the specific reserved-word given only ('in' and 'do' in 'for', 'case', and 'select' commands)
|
|
func convertToReservedWord(w *WordType, reservedWord string) {
|
|
if w == nil || w.Type != WordTypeLit {
|
|
return
|
|
}
|
|
if string(w.Raw) == reservedWord {
|
|
w.Type = WordTypeKey
|
|
}
|
|
}
|
|
|
|
func isNoneReservedWord(w *WordType) bool {
|
|
if w.Type != WordTypeKey {
|
|
return false
|
|
}
|
|
rawVal := string(w.Raw)
|
|
for _, rw := range bashNoneRW {
|
|
if rawVal == rw {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
type parseCmdState struct {
|
|
Input []*WordType
|
|
InputPos int
|
|
|
|
Rtn []*CmdType
|
|
Cur *CmdType
|
|
}
|
|
|
|
func (state *parseCmdState) isEof() bool {
|
|
return state.InputPos >= len(state.Input)
|
|
}
|
|
|
|
func (state *parseCmdState) curWord() *WordType {
|
|
if state.isEof() {
|
|
return nil
|
|
}
|
|
return state.Input[state.InputPos]
|
|
}
|
|
|
|
func (state *parseCmdState) lastCmd() *CmdType {
|
|
if len(state.Rtn) == 0 {
|
|
return nil
|
|
}
|
|
return state.Rtn[len(state.Rtn)-1]
|
|
}
|
|
|
|
func (state *parseCmdState) makeNoneCmd(sep bool) {
|
|
if state.Cur == nil || state.Cur.Type != CmdTypeNone {
|
|
state.Cur = &CmdType{Type: CmdTypeNone}
|
|
state.Rtn = append(state.Rtn, state.Cur)
|
|
}
|
|
state.Cur.Words = append(state.Cur.Words, state.curWord())
|
|
if sep {
|
|
state.Cur.NoneComplete = true
|
|
state.Cur = nil
|
|
}
|
|
state.InputPos++
|
|
}
|
|
|
|
func (state *parseCmdState) handleKeyword(word *WordType) bool {
|
|
if word.Type != WordTypeKey {
|
|
return false
|
|
}
|
|
if isNoneReservedWord(word) {
|
|
state.makeNoneCmd(true)
|
|
return true
|
|
}
|
|
rw := string(word.Raw)
|
|
if rw == "[[" {
|
|
// just ignore everything between [[ and ]]
|
|
for !state.isEof() {
|
|
curWord := state.curWord()
|
|
if curWord.Type == WordTypeLit && string(curWord.Raw) == "]]" {
|
|
convertToReservedWord(curWord, "]]")
|
|
state.makeNoneCmd(false)
|
|
break
|
|
}
|
|
state.makeNoneCmd(false)
|
|
}
|
|
return true
|
|
}
|
|
if rw == "case" {
|
|
// ignore everything between "case" and "esac"
|
|
for !state.isEof() {
|
|
curWord := state.curWord()
|
|
if curWord.Type == WordTypeKey && string(curWord.Raw) == "esac" {
|
|
state.makeNoneCmd(false)
|
|
break
|
|
}
|
|
state.makeNoneCmd(false)
|
|
}
|
|
return true
|
|
}
|
|
if rw == "for" || rw == "select" {
|
|
// ignore until a "do"
|
|
for !state.isEof() {
|
|
curWord := state.curWord()
|
|
if curWord.Type == WordTypeKey && string(curWord.Raw) == "do" {
|
|
state.makeNoneCmd(true)
|
|
break
|
|
}
|
|
state.makeNoneCmd(false)
|
|
}
|
|
return true
|
|
}
|
|
if rw == "in" {
|
|
// the "for" and "case" clauses should skip "in". so encountering an "in" here is a syntax error.
|
|
// just treat it as a none and allow a new command after.
|
|
state.makeNoneCmd(false)
|
|
return true
|
|
}
|
|
if rw == "function" {
|
|
// ignore until '{'
|
|
for !state.isEof() {
|
|
curWord := state.curWord()
|
|
if curWord.Type == WordTypeKey && string(curWord.Raw) == "{" {
|
|
state.makeNoneCmd(true)
|
|
break
|
|
}
|
|
state.makeNoneCmd(false)
|
|
}
|
|
return true
|
|
}
|
|
state.makeNoneCmd(true)
|
|
return true
|
|
}
|
|
|
|
func isCmdSeparatorOp(word *WordType) bool {
|
|
if word.Type != WordTypeOp {
|
|
return false
|
|
}
|
|
opVal := string(word.Raw)
|
|
return opVal == ";" || opVal == "\n" || opVal == "&" || opVal == "|" || opVal == "|&" || opVal == "&&" || opVal == "||" || opVal == "(" || opVal == ")"
|
|
}
|
|
|
|
func (state *parseCmdState) handleOp(word *WordType) bool {
|
|
opVal := string(word.Raw)
|
|
// sequential separators
|
|
if opVal == ";" || opVal == "\n" {
|
|
state.makeNoneCmd(true)
|
|
return true
|
|
}
|
|
// separator
|
|
if opVal == "&" {
|
|
state.makeNoneCmd(true)
|
|
return true
|
|
}
|
|
// pipelines
|
|
if opVal == "|" || opVal == "|&" {
|
|
state.makeNoneCmd(true)
|
|
return true
|
|
}
|
|
// lists
|
|
if opVal == "&&" || opVal == "||" {
|
|
state.makeNoneCmd(true)
|
|
return true
|
|
}
|
|
// subshell
|
|
if opVal == "(" || opVal == ")" {
|
|
state.makeNoneCmd(true)
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func wordSliceBoundedIdx(words []*WordType, idx int) *WordType {
|
|
if idx >= len(words) {
|
|
return nil
|
|
}
|
|
return words[idx]
|
|
}
|
|
|
|
// note that a newline "op" can appear in the third position of "for" or "case". the "in" keyword is still converted because of wordNum == 0
|
|
func identifyReservedWords(words []*WordType) {
|
|
wordNum := 0
|
|
lastReserved := false
|
|
for idx, word := range words {
|
|
if wordNum == 0 || lastReserved {
|
|
convertToAnyReservedWord(word)
|
|
}
|
|
if word.Type == WordTypeKey {
|
|
rwVal := string(word.Raw)
|
|
switch rwVal {
|
|
case "for":
|
|
lastReserved = false
|
|
third := wordSliceBoundedIdx(words, idx+2)
|
|
convertToReservedWord(third, "in")
|
|
convertToReservedWord(third, "do")
|
|
|
|
case "case":
|
|
lastReserved = false
|
|
third := wordSliceBoundedIdx(words, idx+2)
|
|
convertToReservedWord(third, "in")
|
|
|
|
case "in":
|
|
lastReserved = false
|
|
|
|
default:
|
|
lastReserved = true
|
|
}
|
|
continue
|
|
}
|
|
lastReserved = false
|
|
if isCmdSeparatorOp(word) {
|
|
wordNum = 0
|
|
continue
|
|
}
|
|
wordNum++
|
|
}
|
|
}
|
|
|
|
type CompletionPos struct {
|
|
RawPos int // the raw position of cursor
|
|
SuperOffset int // adjust all offsets in Cmd and CmdWord by SuperOffset
|
|
Cmd *CmdType // nil if between commands (otherwise will be a SimpleCommand)
|
|
|
|
// index into cmd.Words (only useful when Cmd is not nil, otherwise we look at CompCommand)
|
|
// 0 means command-word
|
|
// negative means assignment-words.
|
|
// can be past the end of Words (means start new word).
|
|
CmdWordPos int
|
|
|
|
CmdWord *WordType // set to the word we are completing (nil if we are starting a new word)
|
|
CmdWordOffset int // offset into cmdword (only if CmdWord is not nil)
|
|
CompInvalid bool // some words cannot be completed (e.g. in the middle of an operator, inside a control structure, etc.)
|
|
CompCommand bool // set when we think we are the first word of an existing or new command. otherwise we default to file completion
|
|
}
|
|
|
|
func (cmd *CmdType) findCompletionPos_simple(pos int, superOffset int) CompletionPos {
|
|
if cmd.Type != CmdTypeSimple {
|
|
panic("findCompletetionPos_simple only works for CmdTypeSimple")
|
|
}
|
|
for idx, word := range cmd.AssignmentWords {
|
|
startOffset := word.Offset
|
|
endOffset := word.Offset + len(word.Raw)
|
|
if pos <= startOffset {
|
|
// starting a new word at this position (before the current assignment word)
|
|
return CompletionPos{RawPos: pos, SuperOffset: superOffset, Cmd: cmd, CmdWordPos: idx - len(cmd.AssignmentWords) - 1}
|
|
}
|
|
if pos <= endOffset {
|
|
// completing an assignment word
|
|
return CompletionPos{RawPos: pos, SuperOffset: superOffset, Cmd: cmd, CmdWordPos: idx - len(cmd.AssignmentWords), CmdWord: word, CmdWordOffset: pos - word.Offset}
|
|
}
|
|
}
|
|
var foundWord *WordType
|
|
var foundWordIdx int
|
|
for idx, word := range cmd.Words {
|
|
startOffset := word.Offset
|
|
endOffset := word.Offset + len(word.Raw)
|
|
if pos <= startOffset {
|
|
// starting a new word at this position
|
|
return CompletionPos{RawPos: pos, SuperOffset: superOffset, Cmd: cmd, CmdWordPos: idx}
|
|
}
|
|
if pos == endOffset && word.Type == WordTypeOp {
|
|
// operators are special, they can allow a full-word completion at endpos
|
|
continue
|
|
}
|
|
if pos <= endOffset {
|
|
foundWord = word
|
|
foundWordIdx = idx
|
|
break
|
|
}
|
|
}
|
|
if foundWord != nil {
|
|
if foundWord.uncompletable() {
|
|
// invalid completion point
|
|
return CompletionPos{RawPos: pos, SuperOffset: superOffset, Cmd: cmd, CmdWordPos: foundWordIdx, CmdWord: foundWord, CmdWordOffset: pos - foundWord.Offset, CompInvalid: true}
|
|
}
|
|
return CompletionPos{RawPos: pos, SuperOffset: superOffset, Cmd: cmd, CmdWordPos: foundWordIdx, CmdWord: foundWord, CmdWordOffset: pos - foundWord.Offset}
|
|
}
|
|
// past the end, so we're starting a new word in Cmd
|
|
return CompletionPos{RawPos: pos, SuperOffset: superOffset, Cmd: cmd, CmdWordPos: len(cmd.Words)}
|
|
}
|
|
|
|
func (cmd *CmdType) findWordAtPos_none(pos int) *WordType {
|
|
if cmd.Type != CmdTypeNone {
|
|
panic("findWordAtPos_none only works for CmdTypeNone")
|
|
}
|
|
for _, word := range cmd.Words {
|
|
startOffset := word.Offset
|
|
endOffset := word.Offset + len(word.Raw)
|
|
if pos <= startOffset {
|
|
return nil
|
|
}
|
|
if pos <= endOffset {
|
|
if pos == endOffset && word.Type == WordTypeOp {
|
|
// operators are special, they can allow a full-word completion at endpos
|
|
continue
|
|
}
|
|
return word
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func findWordAtPos(words []*WordType, pos int) *WordType {
|
|
for _, word := range words {
|
|
if pos > word.Offset && pos < word.Offset+len(word.Raw) {
|
|
return word
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// recursively descend down the word, parse commands and find a sub completion point if any.
|
|
// return nil if there is no sub completion point in this word
|
|
func findCompletionPosInWord(word *WordType, pos int, superOffset int) *CompletionPos {
|
|
if word.Type == WordTypeGroup || word.Type == WordTypeDQ || word.Type == WordTypeDDQ {
|
|
// need to descend further
|
|
wmeta := wordMetaMap[word.Type]
|
|
if pos <= wmeta.PrefixLen {
|
|
return nil
|
|
}
|
|
endPos := len(word.Raw)
|
|
if word.Complete {
|
|
endPos = endPos - wmeta.SuffixLen
|
|
}
|
|
if pos >= endPos {
|
|
return nil
|
|
}
|
|
subWord := findWordAtPos(word.Subs, pos-wmeta.PrefixLen)
|
|
if subWord == nil {
|
|
return nil
|
|
}
|
|
fullOffset := subWord.Offset + wmeta.PrefixLen
|
|
return findCompletionPosInWord(subWord, pos-fullOffset, superOffset+fullOffset)
|
|
}
|
|
if word.Type == WordTypeDP || word.Type == WordTypeBQ {
|
|
wmeta := wordMetaMap[word.Type]
|
|
if pos < wmeta.PrefixLen {
|
|
return nil
|
|
}
|
|
if word.Complete && pos > len(word.Raw)-wmeta.SuffixLen {
|
|
return nil
|
|
}
|
|
subCmds := ParseCommands(word.Subs)
|
|
newPos := FindCompletionPos(subCmds, pos-wmeta.PrefixLen, superOffset+wmeta.PrefixLen)
|
|
return &newPos
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// returns the context for completion
|
|
// if we are completing in a simple-command, the returns the Cmd. the Cmd can be used for specialized completion (command name, arg position, etc.)
|
|
// if we are completing in a word, returns the Word. Word might be a group-word or DQ word, so it may need additional resolution (done in extend)
|
|
// otherwise we are going to create a new word to insert at offset (so the context does not matter)
|
|
func findCompletionPosCmds(cmds []*CmdType, pos int, superOffset int) CompletionPos {
|
|
if len(cmds) == 0 {
|
|
// set CompCommand because we're starting a new command
|
|
return CompletionPos{RawPos: pos, SuperOffset: superOffset, CompCommand: true}
|
|
}
|
|
for _, cmd := range cmds {
|
|
endOffset := cmd.endOffset()
|
|
if pos > endOffset || (cmd.Type == CmdTypeNone && pos == endOffset) {
|
|
continue
|
|
}
|
|
startOffset := cmd.offset()
|
|
if cmd.Type == CmdTypeSimple {
|
|
if pos <= startOffset {
|
|
return CompletionPos{RawPos: pos, SuperOffset: superOffset, CompCommand: true}
|
|
}
|
|
return cmd.findCompletionPos_simple(pos, superOffset)
|
|
} else {
|
|
// not in a simple-command
|
|
// if we're before the none-command, just start a new command
|
|
if pos <= startOffset {
|
|
return CompletionPos{RawPos: pos, SuperOffset: superOffset, CompCommand: true}
|
|
}
|
|
word := cmd.findWordAtPos_none(pos)
|
|
if word == nil {
|
|
// just revert to a file completion
|
|
return CompletionPos{RawPos: pos, SuperOffset: superOffset, CompCommand: false}
|
|
}
|
|
if word.uncompletable() {
|
|
// ok, we're inside of a word in CmdTypeNone. if we're in an uncompletable word, return CompInvalid
|
|
return CompletionPos{RawPos: pos, SuperOffset: superOffset, CmdWord: word, CmdWordOffset: pos - word.Offset, CompInvalid: true}
|
|
}
|
|
// revert to file completion
|
|
return CompletionPos{RawPos: pos, SuperOffset: superOffset, CmdWord: word, CmdWordOffset: pos - word.Offset}
|
|
}
|
|
}
|
|
// past the end
|
|
lastCmd := cmds[len(cmds)-1]
|
|
if lastCmd.Type == CmdTypeSimple {
|
|
// just extend last command
|
|
return CompletionPos{RawPos: pos, SuperOffset: superOffset, Cmd: lastCmd, CmdWordPos: len(lastCmd.Words)}
|
|
}
|
|
// use lastCmd.NoneComplete to see if last command ended on a "separator". use that to set CompCommand
|
|
return CompletionPos{RawPos: pos, SuperOffset: superOffset, CompCommand: lastCmd.NoneComplete}
|
|
}
|
|
|
|
func FindCompletionPos(cmds []*CmdType, pos int, superOffset int) CompletionPos {
|
|
cpos := findCompletionPosCmds(cmds, pos, superOffset)
|
|
if cpos.CmdWord == nil {
|
|
return cpos
|
|
}
|
|
subPos := findCompletionPosInWord(cpos.CmdWord, cpos.CmdWordOffset, superOffset+cpos.CmdWord.Offset)
|
|
if subPos == nil {
|
|
return cpos
|
|
} else {
|
|
return *subPos
|
|
}
|
|
}
|
|
|
|
func ResetWordOffsets(words []*WordType, startIdx int) {
|
|
pos := startIdx
|
|
for _, word := range words {
|
|
pos += len(word.Prefix)
|
|
word.Offset = pos
|
|
if len(word.Subs) > 0 {
|
|
ResetWordOffsets(word.Subs, 0)
|
|
}
|
|
pos += len(word.Raw)
|
|
}
|
|
}
|
|
|
|
func CommandsToWords(cmds []*CmdType) []*WordType {
|
|
var rtn []*WordType
|
|
for _, cmd := range cmds {
|
|
rtn = append(rtn, cmd.Words...)
|
|
}
|
|
return rtn
|
|
}
|
|
|
|
func (c *CmdType) stripPrefix() []rune {
|
|
if len(c.AssignmentWords) > 0 {
|
|
w := c.AssignmentWords[0]
|
|
prefix := w.Prefix
|
|
if len(prefix) == 0 {
|
|
return nil
|
|
}
|
|
newWord := *w
|
|
newWord.Prefix = nil
|
|
c.AssignmentWords[0] = &newWord
|
|
return prefix
|
|
}
|
|
if len(c.Words) > 0 {
|
|
w := c.Words[0]
|
|
prefix := w.Prefix
|
|
if len(prefix) == 0 {
|
|
return nil
|
|
}
|
|
newWord := *w
|
|
newWord.Prefix = nil
|
|
c.Words[0] = &newWord
|
|
return prefix
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *CmdType) isEmpty() bool {
|
|
return len(c.AssignmentWords) == 0 && len(c.Words) == 0
|
|
}
|
|
|
|
func (c *CmdType) lastWord() *WordType {
|
|
if len(c.Words) > 0 {
|
|
return c.Words[len(c.Words)-1]
|
|
}
|
|
if len(c.AssignmentWords) > 0 {
|
|
return c.AssignmentWords[len(c.AssignmentWords)-1]
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *CmdType) firstWord() *WordType {
|
|
if len(c.AssignmentWords) > 0 {
|
|
return c.AssignmentWords[0]
|
|
}
|
|
if len(c.Words) > 0 {
|
|
return c.Words[0]
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *CmdType) offset() int {
|
|
firstWord := c.firstWord()
|
|
if firstWord == nil {
|
|
return 0
|
|
}
|
|
return firstWord.Offset
|
|
}
|
|
|
|
func (c *CmdType) endOffset() int {
|
|
lastWord := c.lastWord()
|
|
if lastWord == nil {
|
|
return 0
|
|
}
|
|
return lastWord.Offset + len(lastWord.Raw)
|
|
}
|
|
|
|
func indexInRunes(arr []rune, ch rune) int {
|
|
for idx, r := range arr {
|
|
if r == ch {
|
|
return idx
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
func isAssignmentWord(w *WordType) bool {
|
|
if w.Type == WordTypeLit || w.Type == WordTypeGroup {
|
|
eqIdx := indexInRunes(w.Raw, '=')
|
|
if eqIdx == -1 {
|
|
return false
|
|
}
|
|
prefix := w.Raw[0:eqIdx]
|
|
return isSimpleVarName(prefix)
|
|
}
|
|
return false
|
|
}
|
|
|
|
// simple commands steal whitespace from subsequent commands
|
|
func cmdWhitespaceFixup(cmds []*CmdType) {
|
|
for idx := 0; idx < len(cmds)-1; idx++ {
|
|
cmd := cmds[idx]
|
|
if cmd.Type != CmdTypeSimple || cmd.isEmpty() {
|
|
continue
|
|
}
|
|
nextCmd := cmds[idx+1]
|
|
nextPrefix := nextCmd.stripPrefix()
|
|
if len(nextPrefix) > 0 {
|
|
blankWord := &WordType{Type: WordTypeLit, QC: cmd.lastWord().QC, Offset: cmd.endOffset() + len(nextPrefix), Prefix: nextPrefix, Complete: true}
|
|
cmd.Words = append(cmd.Words, blankWord)
|
|
}
|
|
}
|
|
}
|
|
|
|
func ParseCommands(words []*WordType) []*CmdType {
|
|
identifyReservedWords(words)
|
|
state := parseCmdState{Input: words}
|
|
for {
|
|
if state.isEof() {
|
|
break
|
|
}
|
|
word := state.curWord()
|
|
if word.Type == WordTypeKey {
|
|
done := state.handleKeyword(word)
|
|
if done {
|
|
continue
|
|
}
|
|
}
|
|
if word.Type == WordTypeOp {
|
|
done := state.handleOp(word)
|
|
if done {
|
|
continue
|
|
}
|
|
}
|
|
if state.Cur == nil || state.Cur.Type != CmdTypeSimple {
|
|
state.Cur = &CmdType{Type: CmdTypeSimple}
|
|
state.Rtn = append(state.Rtn, state.Cur)
|
|
}
|
|
if len(state.Cur.Words) == 0 && isAssignmentWord(word) {
|
|
state.Cur.AssignmentWords = append(state.Cur.AssignmentWords, word)
|
|
} else {
|
|
state.Cur.Words = append(state.Cur.Words, word)
|
|
}
|
|
state.InputPos++
|
|
}
|
|
cmdWhitespaceFixup(state.Rtn)
|
|
return state.Rtn
|
|
}
|