srsh 0.7.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/LICENSE +18 -14
- data/README.md +351 -134
- data/bin/srsh +53 -1473
- data/docs/assets/slut.txt +4 -0
- data/docs/assets/srsh-mark.svg +12 -0
- data/docs/css/style.css +696 -0
- data/docs/index.html +703 -0
- data/docs/js/app.js +203 -0
- data/examples/bridge.rsh +8 -0
- data/examples/calculator.rsh +253 -0
- data/examples/defer.rsh +14 -0
- data/examples/hot.rsh +14 -0
- data/examples/meta.rsh +20 -0
- data/examples/modules/text.rsh +6 -0
- data/examples/modules.rsh +6 -0
- data/examples/paste.rsh +15 -0
- data/examples/plugin.rb +8 -0
- data/examples/power.rsh +65 -0
- data/examples/tour.rsh +38 -0
- data/ext/srsh_native/extconf.rb +3 -0
- data/ext/srsh_native/srsh_native.c +48 -0
- data/language-docs/LANGUAGE.md +670 -0
- data/language-docs/MIGRATION.md +44 -0
- data/language-docs/SECURITY.md +44 -0
- data/lib/srsh/app.rb +261 -0
- data/lib/srsh/builtins.rb +492 -0
- data/lib/srsh/editor.rb +530 -0
- data/lib/srsh/errors.rb +23 -0
- data/lib/srsh/history.rb +74 -0
- data/lib/srsh/language/evaluator.rb +1175 -0
- data/lib/srsh/language/lexer.rb +316 -0
- data/lib/srsh/language/parser.rb +997 -0
- data/lib/srsh/language/token.rb +5 -0
- data/lib/srsh/language/values.rb +392 -0
- data/lib/srsh/paths.rb +29 -0
- data/lib/srsh/plugins.rb +59 -0
- data/lib/srsh/process_identity.rb +38 -0
- data/lib/srsh/security.rb +38 -0
- data/lib/srsh/shell/executor.rb +1182 -0
- data/lib/srsh/shell/job.rb +101 -0
- data/lib/srsh/shell/lexer.rb +114 -0
- data/lib/srsh/shell/terminal.rb +26 -0
- data/lib/srsh/state.rb +136 -0
- data/lib/srsh/theme.rb +108 -0
- data/lib/srsh/version.rb +1 -2
- data/lib/srsh.rb +15 -0
- metadata +59 -11
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
require_relative '../errors'
|
|
2
|
+
require_relative 'token'
|
|
3
|
+
|
|
4
|
+
module Srsh
|
|
5
|
+
module Language
|
|
6
|
+
class Lexer
|
|
7
|
+
MAX_TOKENS = 100_000
|
|
8
|
+
TWO = %w[== != <= >= ++ ** ?? =~ !~ .. && || |>].freeze
|
|
9
|
+
THREE = %w[..< === !==].freeze
|
|
10
|
+
|
|
11
|
+
def initialize(source, line: 1)
|
|
12
|
+
@s = source.to_s
|
|
13
|
+
@i = 0
|
|
14
|
+
@line = line
|
|
15
|
+
@column = 1
|
|
16
|
+
@tokens = 0
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def next_token
|
|
20
|
+
skip_space
|
|
21
|
+
return token(:eof, nil) if eof?
|
|
22
|
+
raise ParseError.new('expression has too many tokens', line: @line, column: @column) if (@tokens += 1) > MAX_TOKENS
|
|
23
|
+
|
|
24
|
+
c = peek
|
|
25
|
+
return read_number if digit?(c)
|
|
26
|
+
return read_string if c == '"' || c == "'"
|
|
27
|
+
return read_raw if c == '[' && peek(1) == '['
|
|
28
|
+
return read_dollar if c == '$'
|
|
29
|
+
return read_percent_map if c == '%' && peek(1) == '['
|
|
30
|
+
return read_ident if ident_start?(c)
|
|
31
|
+
|
|
32
|
+
start = @column
|
|
33
|
+
three = @s[@i, 3]
|
|
34
|
+
if THREE.include?(three)
|
|
35
|
+
advance(3)
|
|
36
|
+
return Token.new(:op, three, @line, start)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
two = @s[@i, 2]
|
|
40
|
+
case two
|
|
41
|
+
when '::'
|
|
42
|
+
advance(2)
|
|
43
|
+
return Token.new(:lambda, two, @line, start)
|
|
44
|
+
when '=>'
|
|
45
|
+
advance(2)
|
|
46
|
+
return Token.new(:fat_arrow, two, @line, start)
|
|
47
|
+
when '?.'
|
|
48
|
+
advance(2)
|
|
49
|
+
return Token.new(:safe_dot, two, @line, start)
|
|
50
|
+
when '?['
|
|
51
|
+
advance(2)
|
|
52
|
+
return Token.new(:safe_lbracket, two, @line, start)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
if TWO.include?(two)
|
|
56
|
+
advance(2)
|
|
57
|
+
return Token.new(:op, two, @line, start)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
type = case c
|
|
61
|
+
when '(' then :lparen
|
|
62
|
+
when ')' then :rparen
|
|
63
|
+
when '[' then :lbracket
|
|
64
|
+
when ']' then :rbracket
|
|
65
|
+
when ',' then :comma
|
|
66
|
+
when ':' then :colon
|
|
67
|
+
when '.' then :dot
|
|
68
|
+
else :op
|
|
69
|
+
end
|
|
70
|
+
advance
|
|
71
|
+
Token.new(type, c, @line, start)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def token(type, value, line = @line, col = @column) = Token.new(type, value, line, col)
|
|
77
|
+
def eof? = @i >= @s.length
|
|
78
|
+
def peek(offset = 0) = @s[@i + offset]
|
|
79
|
+
def digit?(c) = c && c >= '0' && c <= '9'
|
|
80
|
+
def ident_start?(c) = c && (c == '_' || c.match?(/[A-Za-z]/))
|
|
81
|
+
def ident_char?(c) = c && (c == '_' || c.match?(/[A-Za-z0-9]/))
|
|
82
|
+
|
|
83
|
+
def advance(n = 1)
|
|
84
|
+
n.times do
|
|
85
|
+
break if eof?
|
|
86
|
+
@i += 1
|
|
87
|
+
@column += 1
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def skip_space
|
|
92
|
+
advance while (c = peek) && (c == ' ' || c == "\t" || c == "\r")
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def read_number
|
|
96
|
+
line, col, start = @line, @column, @i
|
|
97
|
+
if peek == '0' && %w[x X b B o O].include?(peek(1))
|
|
98
|
+
base_mark = peek(1).downcase
|
|
99
|
+
advance(2)
|
|
100
|
+
digit_re = { 'x' => /[0-9A-Fa-f]/, 'b' => /[01]/, 'o' => /[0-7]/ }.fetch(base_mark)
|
|
101
|
+
saw_digit = false
|
|
102
|
+
while (c = peek) && (c == '_' || c.match?(digit_re))
|
|
103
|
+
saw_digit ||= c != '_'
|
|
104
|
+
advance
|
|
105
|
+
end
|
|
106
|
+
raise ParseError.new('bad base-prefixed number', line: line, column: col) unless saw_digit
|
|
107
|
+
if (c = peek) && c.match?(/[A-Za-z0-9_]/)
|
|
108
|
+
raise ParseError.new('invalid digit in number', line: line, column: col)
|
|
109
|
+
end
|
|
110
|
+
else
|
|
111
|
+
advance while (c = peek) && (digit?(c) || c == '_')
|
|
112
|
+
if peek == '.' && digit?(peek(1))
|
|
113
|
+
advance
|
|
114
|
+
advance while (c = peek) && (digit?(c) || c == '_')
|
|
115
|
+
end
|
|
116
|
+
if %w[e E].include?(peek)
|
|
117
|
+
advance
|
|
118
|
+
advance if %w[+ -].include?(peek)
|
|
119
|
+
raise ParseError.new('bad exponent', line: line, column: col) unless digit?(peek)
|
|
120
|
+
advance while (c = peek) && (digit?(c) || c == '_')
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
raw = @s[start...@i]
|
|
124
|
+
if raw.match?(/(?:\A_|_\z|__|_[.eE+\-]|[.eE+\-]_|[xXbBoO]_)/)
|
|
125
|
+
raise ParseError.new('bad underscore placement in number', line: line, column: col)
|
|
126
|
+
end
|
|
127
|
+
Token.new(:number, raw, line, col)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def read_string
|
|
131
|
+
quote = peek
|
|
132
|
+
line, col = @line, @column
|
|
133
|
+
advance
|
|
134
|
+
text = +''
|
|
135
|
+
segments = []
|
|
136
|
+
closed = false
|
|
137
|
+
|
|
138
|
+
until eof?
|
|
139
|
+
c = peek
|
|
140
|
+
|
|
141
|
+
if c == quote
|
|
142
|
+
advance
|
|
143
|
+
closed = true
|
|
144
|
+
break
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
if c == '\\'
|
|
148
|
+
advance
|
|
149
|
+
raise IncompleteInput.new('unterminated escape', line: line, column: col) if eof?
|
|
150
|
+
e = peek
|
|
151
|
+
advance
|
|
152
|
+
text << case e
|
|
153
|
+
when 'n' then "\n"
|
|
154
|
+
when 'r' then "\r"
|
|
155
|
+
when 't' then "\t"
|
|
156
|
+
when '0' then "\0"
|
|
157
|
+
when '\\' then '\\'
|
|
158
|
+
when '"' then '"'
|
|
159
|
+
when "'" then "'"
|
|
160
|
+
when '#' then '#'
|
|
161
|
+
else e
|
|
162
|
+
end
|
|
163
|
+
next
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
if quote == '"' && c == '#' && peek(1) == '{'
|
|
167
|
+
segments << [:text, text] unless text.empty?
|
|
168
|
+
text = +''
|
|
169
|
+
segments << [:expr, read_interpolation(line, col)]
|
|
170
|
+
next
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
text << c
|
|
174
|
+
advance
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
raise IncompleteInput.new('unterminated string', line: line, column: col) unless closed
|
|
178
|
+
if segments.empty?
|
|
179
|
+
Token.new(:string, text, line, col)
|
|
180
|
+
else
|
|
181
|
+
segments << [:text, text] unless text.empty?
|
|
182
|
+
Token.new(:template, segments, line, col)
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def read_interpolation(line, col)
|
|
187
|
+
# cursor is on '#{' here
|
|
188
|
+
advance(2)
|
|
189
|
+
start = @i
|
|
190
|
+
depth = 1
|
|
191
|
+
quote = nil
|
|
192
|
+
escaped = false
|
|
193
|
+
|
|
194
|
+
until eof?
|
|
195
|
+
c = peek
|
|
196
|
+
if escaped
|
|
197
|
+
escaped = false
|
|
198
|
+
elsif quote
|
|
199
|
+
if c == '\\'
|
|
200
|
+
escaped = true
|
|
201
|
+
elsif c == quote
|
|
202
|
+
quote = nil
|
|
203
|
+
end
|
|
204
|
+
elsif c == '"' || c == "'"
|
|
205
|
+
quote = c
|
|
206
|
+
elsif c == '{'
|
|
207
|
+
depth += 1
|
|
208
|
+
elsif c == '}'
|
|
209
|
+
depth -= 1
|
|
210
|
+
if depth.zero?
|
|
211
|
+
out = @s[start...@i]
|
|
212
|
+
advance
|
|
213
|
+
raise ParseError.new('empty string interpolation', line: line, column: col) if out.strip.empty?
|
|
214
|
+
return out
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
advance
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
raise IncompleteInput.new('unterminated string interpolation', line: line, column: col)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def read_raw
|
|
224
|
+
line, col = @line, @column
|
|
225
|
+
advance(2)
|
|
226
|
+
start = @i
|
|
227
|
+
idx = @s.index(']]', @i)
|
|
228
|
+
raise IncompleteInput.new('unterminated raw string', line: line, column: col) unless idx
|
|
229
|
+
out = @s[start...idx]
|
|
230
|
+
advance(idx - @i + 2)
|
|
231
|
+
Token.new(:string, out, line, col)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def read_dollar
|
|
235
|
+
line, col = @line, @column
|
|
236
|
+
advance
|
|
237
|
+
|
|
238
|
+
if peek == '('
|
|
239
|
+
advance
|
|
240
|
+
return Token.new(:capture, read_capture(line, col), line, col)
|
|
241
|
+
end
|
|
242
|
+
if peek == '?'
|
|
243
|
+
advance
|
|
244
|
+
return Token.new(:status, '?', line, col)
|
|
245
|
+
end
|
|
246
|
+
if digit?(peek)
|
|
247
|
+
start = @i
|
|
248
|
+
advance while digit?(peek)
|
|
249
|
+
return Token.new(:positional, @s[start...@i].to_i, line, col)
|
|
250
|
+
end
|
|
251
|
+
raise ParseError.new('expected environment variable after $', line: line, column: col) unless ident_start?(peek)
|
|
252
|
+
start = @i
|
|
253
|
+
advance while ident_char?(peek)
|
|
254
|
+
Token.new(:env, @s[start...@i], line, col)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def read_capture(line, col)
|
|
258
|
+
start = @i
|
|
259
|
+
depth = 1
|
|
260
|
+
single = false
|
|
261
|
+
double = false
|
|
262
|
+
escaped = false
|
|
263
|
+
|
|
264
|
+
until eof?
|
|
265
|
+
c = peek
|
|
266
|
+
if escaped
|
|
267
|
+
escaped = false
|
|
268
|
+
elsif c == '\\' && !single
|
|
269
|
+
escaped = true
|
|
270
|
+
elsif c == "'" && !double
|
|
271
|
+
single = !single
|
|
272
|
+
elsif c == '"' && !single
|
|
273
|
+
double = !double
|
|
274
|
+
elsif !single && !double && c == '('
|
|
275
|
+
depth += 1
|
|
276
|
+
elsif !single && !double && c == ')'
|
|
277
|
+
depth -= 1
|
|
278
|
+
if depth.zero?
|
|
279
|
+
out = @s[start...@i]
|
|
280
|
+
advance
|
|
281
|
+
return out
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
advance
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
raise IncompleteInput.new('unterminated command capture', line: line, column: col)
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def read_percent_map
|
|
291
|
+
line, col = @line, @column
|
|
292
|
+
advance(2)
|
|
293
|
+
Token.new(:map_open, '%[', line, col)
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def read_ident
|
|
297
|
+
line, col, start = @line, @column, @i
|
|
298
|
+
if defined?(::SrshNative) && @s.ascii_only? && ::SrshNative.respond_to?(:ident_end)
|
|
299
|
+
finish = ::SrshNative.ident_end(@s, @i)
|
|
300
|
+
advance(finish - @i)
|
|
301
|
+
else
|
|
302
|
+
advance while ident_char?(peek)
|
|
303
|
+
end
|
|
304
|
+
value = @s[start...@i]
|
|
305
|
+
type = case value
|
|
306
|
+
when 'yes', 'true' then :true
|
|
307
|
+
when 'no', 'false' then :false
|
|
308
|
+
when 'void', 'nil' then :void
|
|
309
|
+
when 'and', 'or', 'not', 'in' then :op
|
|
310
|
+
else :ident
|
|
311
|
+
end
|
|
312
|
+
Token.new(type, value, line, col)
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
end
|