turbo-editors/turbo-corepublic Fork 0
v1.0.2
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-core.git
git clone ssh://git@rickub.com/turbo-editors/turbo-core.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

bash.go · 226 lines · 6.9 KBGo Blame HistoryRaw
🛟 Updated. 28d5985 k33g 21h ago1package syntax
2
3// bashKeywords are the words that structure a shell script, and the builtins
4// worth telling apart from a command on the PATH.
5//
6// The set is what sh, bash and zsh share: a script is coloured the same
7// whichever of them it names in its shebang, because the differences between
8// them are not what a highlighter can usefully show.
9var bashKeywords = map[string]Class{
10 "if": ClassKeyword, "then": ClassKeyword, "elif": ClassKeyword,
11 "else": ClassKeyword, "fi": ClassKeyword, "for": ClassKeyword,
12 "while": ClassKeyword, "until": ClassKeyword, "do": ClassKeyword,
13 "done": ClassKeyword, "case": ClassKeyword, "esac": ClassKeyword,
14 "in": ClassKeyword, "function": ClassKeyword, "select": ClassKeyword,
15 "time": ClassKeyword, "return": ClassKeyword, "break": ClassKeyword,
16 "continue": ClassKeyword, "exit": ClassKeyword,
17
18 "true": ClassConstant, "false": ClassConstant,
19
20 "echo": ClassBuiltin, "printf": ClassBuiltin, "read": ClassBuiltin,
21 "cd": ClassBuiltin, "export": ClassBuiltin, "local": ClassBuiltin,
22 "declare": ClassBuiltin, "readonly": ClassBuiltin, "unset": ClassBuiltin,
23 "shift": ClassBuiltin, "set": ClassBuiltin, "test": ClassBuiltin,
24 "source": ClassBuiltin, "eval": ClassBuiltin, "exec": ClassBuiltin,
25 "trap": ClassBuiltin, "wait": ClassBuiltin, "command": ClassBuiltin,
26}
27
28// highlightBash colours a shell script, one slice of spans per line.
29//
30// Heredocs are deliberately not recognised. Following `<<EOF` to its closing
31// delimiter means carrying an arbitrary word across lines and dealing with the
32// `<<-` and quoted-delimiter spellings, which is a good deal of machinery for
33// a construct that is usually a few lines of plain text.
34//
35// Nothing here spans lines, so the scanner carries no state — the empty struct
36// is what says so.
37func highlightBash(src string) [][]Span {
38 return ScanLines(src, scanBashLine)
39}
40
41// bashScanner walks one line of shell, remembering whether it has already
42// seen a word.
43//
44// That flag is the whole of the command rule: **the first bare word on a line
45// is the command being run; every later one is a plain argument.** Following
46// the real rule — a command also starts after ";", "|", "&&" and after "then"
47// or "do" — would colour a little more and cost a table of what resets it,
48// and getting it wrong colours an argument as a command, which reads as a
49// mistake in the script rather than in the highlighter.
50type bashScanner struct {
51 LineScanner
52 seenWord bool
53}
54
55// scanBashLine returns the spans of one line. Nothing is ever left open.
56func scanBashLine(line []rune, _ struct{}) ([]Span, struct{}) {
57 s := &bashScanner{LineScanner: LineScanner{line: line}}
58 for !s.AtEnd() {
59 s.step()
60 }
61 return s.spans, struct{}{}
62}
63
64// step recognises whatever starts at the current position.
65func (s *bashScanner) step() {
66 switch {
67 case s.line[s.pos] == ' ' || s.line[s.pos] == '\t':
68 s.pos++
69 case s.line[s.pos] == '#':
70 s.TakeRest(ClassComment)
71 case s.line[s.pos] == '\'':
72 // A single-quoted string has no escapes and no expansion: everything
73 // up to the next quote is literal, backslashes included.
74 takeLiteralQuoted(&s.LineScanner)
75 case s.line[s.pos] == '"':
76 takeDoubleQuoted(&s.LineScanner)
77 case s.line[s.pos] == '$':
78 takeExpansion(&s.LineScanner)
79 case IsDigit(s.line[s.pos]):
80 s.TakeWhile(ClassNumber, IsDigit)
81 case IsWordRune(s.line[s.pos]) || s.startsAnOption():
82 s.takeWord()
83 case IsPunctuationRune(s.line[s.pos]):
84 s.Take(1, ClassPunctuation)
85 case IsOperatorRune(s.line[s.pos]):
86 s.TakeWhile(ClassOperator, IsOperatorRune)
87 default:
88 s.pos++
89 }
90}
91
92// startsAnOption reports whether a hyphen here begins a flag rather than an
93// operator.
94//
95// Without this, "-eu" is a minus followed by a word, and every option in every
96// script is coloured as arithmetic. A hyphen followed by a letter, a digit or
97// another hyphen is an option; one followed by a space is not.
98func (s *bashScanner) startsAnOption() bool {
99 return s.line[s.pos] == '-' && (IsWordRune(s.Peek(1)) || s.Peek(1) == '-')
100}
101
102// takeLiteralQuoted colours '…', where nothing is escaped or expanded.
103func takeLiteralQuoted(s *LineScanner) {
104 start := s.pos
105 s.pos++
106
107 for !s.AtEnd() {
108 if s.line[s.pos] == '\'' {
109 s.pos++
110 s.Emit(start, s.pos, ClassString)
111 return
112 }
113 s.pos++
114 }
115 s.Emit(start, len(s.line), ClassString)
116}
117
118// takeDoubleQuoted colours "…", with the expansions inside it coloured as
119// expansions rather than as part of the string.
120//
121// That is the point of the double-quoted form: "$HOME/bin" is a path with a
122// variable in it, and seeing which part is the variable is most of what a
123// reader wants from it.
124func takeDoubleQuoted(s *LineScanner) {
125 start := s.pos
126 s.pos++ // the opening quote
127
128 for !s.AtEnd() {
129 switch {
130 case s.line[s.pos] == '\\' && s.pos+1 < len(s.line):
131 s.pos += 2
132 case s.line[s.pos] == '$':
133 s.Emit(start, s.pos, ClassString)
134 takeExpansion(s)
135 start = s.pos
136 case s.line[s.pos] == '"':
137 s.pos++
138 s.Emit(start, s.pos, ClassString)
139 return
140 default:
141 s.pos++
142 }
143 }
144 s.Emit(start, len(s.line), ClassString)
145}
146
147// takeExpansion colours $NAME, ${…}, $(…) and the one-character forms such as
148// $1, $?, $@ and $#.
149func takeExpansion(s *LineScanner) {
150 switch s.Peek(1) {
151 case '{':
152 takeBracketed(s, '{', '}')
153 case '(':
154 takeBracketed(s, '(', ')')
155 default:
156 takePlainExpansion(s)
157 }
158}
159
160// takePlainExpansion colours $NAME and the single-character variables.
161func takePlainExpansion(s *LineScanner) {
162 start := s.pos
163 s.pos++ // the dollar
164
165 if !s.AtEnd() && IsWordRune(s.line[s.pos]) {
166 for !s.AtEnd() && IsWordRune(s.line[s.pos]) {
167 s.pos++
168 }
169 s.Emit(start, s.pos, ClassBuiltin)
170 return
171 }
172 // $? $@ $# $* $$ $! and friends are one character each.
173 if !s.AtEnd() {
174 s.pos++
175 }
176 s.Emit(start, s.pos, ClassBuiltin)
177}
178
179// takeBracketed colours ${…} and $(…) up to the matching close, counting
180// nesting so that $(a $(b) c) is one span.
181//
182// An unclosed one is coloured to the end of the line, which is what it looks
183// like while it is being typed.
184func takeBracketed(s *LineScanner, open, close rune) {
185 start := s.pos
186 s.pos += 2 // the dollar and the bracket
187
188 depth := 1
189 for !s.AtEnd() && depth > 0 {
190 switch s.line[s.pos] {
191 case open:
192 depth++
193 case close:
194 depth--
195 }
196 s.pos++
197 }
198 s.Emit(start, s.pos, ClassBuiltin)
199}
200
201// takeWord colours a bare word: a keyword, a builtin, a variable being
202// assigned, the command being run, or one of its arguments.
203func (s *bashScanner) takeWord() {
204 start := s.pos
205 for !s.AtEnd() && (IsWordRune(s.line[s.pos]) || s.line[s.pos] == '-') {
206 s.pos++
207 }
208
209 class := s.classOf(string(s.line[start:s.pos]))
210 s.seenWord = true
211 s.Emit(start, s.pos, class)
212}
213
214// classOf decides what a bare word is.
215func (s *bashScanner) classOf(word string) Class {
216 if class, ok := bashKeywords[word]; ok {
217 return class
218 }
219 if !s.AtEnd() && s.line[s.pos] == '=' {
220 return ClassIdentifier // NAME=value
221 }
222 if s.seenWord {
223 return ClassIdentifier // an argument
224 }
225 return ClassFunction // the command being run
226}