· 5 years ago · Dec 24, 2020, 02:40 PM
1--- The @{textutils} API provides helpful utilities for formatting and
2-- manipulating strings.
3--
4-- @module textutils
5
6local expect = dofile("rom/modules/main/cc/expect.lua")
7local expect, field = expect.expect, expect.field
8
9--- Slowly writes string text at current cursor position,
10-- character-by-character.
11--
12-- Like @{_G.write}, this does not insert a newline at the end.
13--
14-- @tparam string sText The the text to write to the screen
15-- @tparam[opt] number nRate The number of characters to write each second,
16-- Defaults to 20.
17-- @usage textutils.slowWrite("Hello, world!")
18-- @usage textutils.slowWrite("Hello, world!", 5)
19function slowWrite(sText, nRate)
20 expect(2, nRate, "number", "nil")
21 nRate = nRate or 20
22 if nRate < 0 then
23 error("Rate must be positive", 2)
24 end
25 local nSleep = 1 / nRate
26
27 sText = tostring(sText)
28 local x, y = term.getCursorPos()
29 local len = #sText
30
31 for n = 1, len do
32 term.setCursorPos(x, y)
33 sleep(nSleep)
34 local nLines = write(string.sub(sText, 1, n))
35 local _, newY = term.getCursorPos()
36 y = newY - nLines
37 end
38end
39
40--- Slowly prints string text at current cursor position,
41-- character-by-character.
42--
43-- Like @{print}, this inserts a newline after printing.
44--
45-- @tparam string sText The the text to write to the screen
46-- @tparam[opt] number nRate The number of characters to write each second,
47-- Defaults to 20.
48-- @usage textutils.slowPrint("Hello, world!")
49-- @usage textutils.slowPrint("Hello, world!", 5)
50function slowPrint(sText, nRate)
51 slowWrite(sText, nRate)
52 print()
53end
54
55--- Takes input time and formats it in a more readable format such as `6:30 PM`.
56--
57-- @tparam number nTime The time to format, as provided by @{os.time}.
58-- @tparam[opt] boolean bTwentyFourHour Whether to format this as a 24-hour
59-- clock (`18:30`) rather than a 12-hour one (`6:30 AM`)
60-- @treturn string The formatted time
61-- @usage textutils.formatTime(os.time())
62function formatTime(nTime, bTwentyFourHour)
63 expect(1, nTime, "number")
64 expect(2, bTwentyFourHour, "boolean", "nil")
65 local sTOD = nil
66 if not bTwentyFourHour then
67 if nTime >= 12 then
68 sTOD = "PM"
69 else
70 sTOD = "AM"
71 end
72 if nTime >= 13 then
73 nTime = nTime - 12
74 end
75 end
76
77 local nHour = math.floor(nTime)
78 local nMinute = math.floor((nTime - nHour) * 60)
79 if sTOD then
80 return string.format("%d:%02d %s", nHour == 0 and 12 or nHour, nMinute, sTOD)
81 else
82 return string.format("%d:%02d", nHour, nMinute)
83 end
84end
85
86local function makePagedScroll(_term, _nFreeLines)
87 local nativeScroll = _term.scroll
88 local nFreeLines = _nFreeLines or 0
89 return function(_n)
90 for _ = 1, _n do
91 nativeScroll(1)
92
93 if nFreeLines <= 0 then
94 local _, h = _term.getSize()
95 _term.setCursorPos(1, h)
96 _term.write("Press any key to continue")
97 os.pullEvent("key")
98 _term.clearLine()
99 _term.setCursorPos(1, h)
100 else
101 nFreeLines = nFreeLines - 1
102 end
103 end
104 end
105end
106
107--- Prints a given string to the display.
108--
109-- If the action can be completed without scrolling, it acts much the same as
110-- @{print}; otherwise, it will throw up a "Press any key to continue" prompt at
111-- the bottom of the display. Each press will cause it to scroll down and write
112-- a single line more before prompting again, if need be.
113--
114-- @tparam string _sText The text to print to the screen.
115-- @tparam[opt] number _nFreeLines The number of lines which will be
116-- automatically scrolled before the first prompt appears (meaning _nFreeLines +
117-- 1 lines will be printed). This can be set to the terminal's height - 2 to
118-- always try to fill the screen. Defaults to 0, meaning only one line is
119-- displayed before prompting.
120-- @treturn number The number of lines printed.
121-- @usage
122-- local width, height = term.getSize()
123-- textutils.pagedPrint(("This is a rather verbose dose of repetition.\n"):rep(30), height - 2)
124function pagedPrint(_sText, _nFreeLines)
125 expect(2, _nFreeLines, "number", "nil")
126 -- Setup a redirector
127 local oldTerm = term.current()
128 local newTerm = {}
129 for k, v in pairs(oldTerm) do
130 newTerm[k] = v
131 end
132 newTerm.scroll = makePagedScroll(oldTerm, _nFreeLines)
133 term.redirect(newTerm)
134
135 -- Print the text
136 local result
137 local ok, err = pcall(function()
138 if _sText ~= nil then
139 result = print(_sText)
140 else
141 result = print()
142 end
143 end)
144
145 -- Removed the redirector
146 term.redirect(oldTerm)
147
148 -- Propogate errors
149 if not ok then
150 error(err, 0)
151 end
152 return result
153end
154
155local function tabulateCommon(bPaged, ...)
156 local tAll = table.pack(...)
157 for i = 1, tAll.n do
158 expect(i, tAll[i], "number", "table")
159 end
160
161 local w, h = term.getSize()
162 local nMaxLen = w / 8
163 for n, t in ipairs(tAll) do
164 if type(t) == "table" then
165 for nu, sItem in pairs(t) do
166 local ty = type(sItem)
167 if ty ~= "string" and ty ~= "number" then
168 error("bad argument #" .. n .. "." .. nu .. " (expected string, got " .. ty .. ")", 3)
169 end
170 nMaxLen = math.max(#tostring(sItem) + 1, nMaxLen)
171 end
172 end
173 end
174 local nCols = math.floor(w / nMaxLen)
175 local nLines = 0
176 local function newLine()
177 if bPaged and nLines >= h - 3 then
178 pagedPrint()
179 else
180 print()
181 end
182 nLines = nLines + 1
183 end
184
185 local function drawCols(_t)
186 local nCol = 1
187 for _, s in ipairs(_t) do
188 if nCol > nCols then
189 nCol = 1
190 newLine()
191 end
192
193 local cx, cy = term.getCursorPos()
194 cx = 1 + (nCol - 1) * nMaxLen
195 term.setCursorPos(cx, cy)
196 term.write(s)
197
198 nCol = nCol + 1
199 end
200 print()
201 end
202 for _, t in ipairs(tAll) do
203 if type(t) == "table" then
204 if #t > 0 then
205 drawCols(t)
206 end
207 elseif type(t) == "number" then
208 term.setTextColor(t)
209 end
210 end
211end
212
213--- Prints tables in a structured form.
214--
215-- This accepts multiple arguments, either a table or a number. When
216-- encountering a table, this will be treated as a table row, with each column
217-- width being auto-adjusted.
218--
219-- When encountering a number, this sets the text color of the subsequent rows to it.
220--
221-- @tparam {string...}|number ... The rows and text colors to display.
222-- @usage textutils.tabulate(colors.orange, { "1", "2", "3" }, colors.lightBlue, { "A", "B", "C" })
223function tabulate(...)
224 return tabulateCommon(false, ...)
225end
226
227--- Prints tables in a structured form, stopping and prompting for input should
228-- the result not fit on the terminal.
229--
230-- This functions identically to @{textutils.tabulate}, but will prompt for user
231-- input should the whole output not fit on the display.
232--
233-- @tparam {string...}|number ... The rows and text colors to display.
234-- @usage textutils.tabulate(colors.orange, { "1", "2", "3" }, colors.lightBlue, { "A", "B", "C" })
235-- @see textutils.tabulate
236-- @see textutils.pagedPrint
237function pagedTabulate(...)
238 return tabulateCommon(true, ...)
239end
240
241local g_tLuaKeywords = {
242 ["and"] = true,
243 ["break"] = true,
244 ["do"] = true,
245 ["else"] = true,
246 ["elseif"] = true,
247 ["end"] = true,
248 ["false"] = true,
249 ["for"] = true,
250 ["function"] = true,
251 ["if"] = true,
252 ["in"] = true,
253 ["local"] = true,
254 ["nil"] = true,
255 ["not"] = true,
256 ["or"] = true,
257 ["repeat"] = true,
258 ["return"] = true,
259 ["then"] = true,
260 ["true"] = true,
261 ["until"] = true,
262 ["while"] = true,
263}
264
265local function serializeImpl(t, tTracking, sIndent)
266 local sType = type(t)
267 if sType == "table" then
268 if tTracking[t] ~= nil then
269 error("Cannot serialize table with recursive entries", 0)
270 end
271 tTracking[t] = true
272
273 if next(t) == nil then
274 -- Empty tables are simple
275 return "{}"
276 else
277 -- Other tables take more work
278 local sResult = "{\n"
279 local sSubIndent = sIndent .. " "
280 local tSeen = {}
281 for k, v in ipairs(t) do
282 tSeen[k] = true
283 sResult = sResult .. sSubIndent .. serializeImpl(v, tTracking, sSubIndent) .. ",\n"
284 end
285 for k, v in pairs(t) do
286 if not tSeen[k] then
287 local sEntry
288 if type(k) == "string" and not g_tLuaKeywords[k] and string.match(k, "^[%a_][%a%d_]*$") then
289 sEntry = k .. " = " .. serializeImpl(v, tTracking, sSubIndent) .. ",\n"
290 else
291 sEntry = "[ " .. serializeImpl(k, tTracking, sSubIndent) .. " ] = " .. serializeImpl(v, tTracking, sSubIndent) .. ",\n"
292 end
293 sResult = sResult .. sSubIndent .. sEntry
294 end
295 end
296 sResult = sResult .. sIndent .. "}"
297 return sResult
298 end
299
300 elseif sType == "string" then
301 return string.format("%q", t)
302
303 elseif sType == "number" or sType == "boolean" or sType == "nil" then
304 return tostring(t)
305
306 else
307 error("Cannot serialize type " .. sType, 0)
308
309 end
310end
311
312local function mk_tbl(str, name)
313 local msg = "attempt to mutate textutils." .. name
314 return setmetatable({}, {
315 __newindex = function() error(msg, 2) end,
316 __tostring = function() return str end,
317 })
318end
319
320--- A table representing an empty JSON array, in order to distinguish it from an
321-- empty JSON object.
322--
323-- The contents of this table should not be modified.
324--
325-- @usage textutils.serialiseJSON(textutils.empty_json_array)
326-- @see textutils.serialiseJSON
327-- @see textutils.unserialiseJSON
328empty_json_array = mk_tbl("[]", "empty_json_array")
329
330--- A table representing the JSON null value.
331--
332-- The contents of this table should not be modified.
333--
334-- @usage textutils.serialiseJSON(textutils.json_null)
335-- @see textutils.serialiseJSON
336-- @see textutils.unserialiseJSON
337json_null = mk_tbl("null", "json_null")
338
339local serializeJSONString
340do
341 local function hexify(c)
342 return ("\\u00%02X"):format(c:byte())
343 end
344
345 local map = {
346 ["\""] = "\\\"",
347 ["\\"] = "\\\\",
348 ["\b"] = "\\b",
349 ["\f"] = "\\f",
350 ["\n"] = "\\n",
351 ["\r"] = "\\r",
352 ["\t"] = "\\t",
353 }
354 for i = 0, 0x1f do
355 local c = string.char(i)
356 if map[c] == nil then map[c] = hexify(c) end
357 end
358
359 serializeJSONString = function(s)
360 return ('"%s"'):format(s:gsub("[\0-\x1f\"\\]", map):gsub("[\x7f-\xff]", hexify))
361 end
362end
363
364local function serializeJSONImpl(t, tTracking, bNBTStyle)
365 local sType = type(t)
366 if t == empty_json_array then return "[]"
367 elseif t == json_null then return "null"
368
369 elseif sType == "table" then
370 if tTracking[t] ~= nil then
371 error("Cannot serialize table with recursive entries", 0)
372 end
373 tTracking[t] = true
374
375 if next(t) == nil then
376 -- Empty tables are simple
377 return "{}"
378 else
379 -- Other tables take more work
380 local sObjectResult = "{"
381 local sArrayResult = "["
382 local nObjectSize = 0
383 local nArraySize = 0
384 for k, v in pairs(t) do
385 if type(k) == "string" then
386 local sEntry
387 if bNBTStyle then
388 sEntry = tostring(k) .. ":" .. serializeJSONImpl(v, tTracking, bNBTStyle)
389 else
390 sEntry = serializeJSONString(k) .. ":" .. serializeJSONImpl(v, tTracking, bNBTStyle)
391 end
392 if nObjectSize == 0 then
393 sObjectResult = sObjectResult .. sEntry
394 else
395 sObjectResult = sObjectResult .. "," .. sEntry
396 end
397 nObjectSize = nObjectSize + 1
398 end
399 end
400 for _, v in ipairs(t) do
401 local sEntry = serializeJSONImpl(v, tTracking, bNBTStyle)
402 if nArraySize == 0 then
403 sArrayResult = sArrayResult .. sEntry
404 else
405 sArrayResult = sArrayResult .. "," .. sEntry
406 end
407 nArraySize = nArraySize + 1
408 end
409 sObjectResult = sObjectResult .. "}"
410 sArrayResult = sArrayResult .. "]"
411 if nObjectSize > 0 or nArraySize == 0 then
412 return sObjectResult
413 else
414 return sArrayResult
415 end
416 end
417
418 elseif sType == "string" then
419 return serializeJSONString(t)
420
421 elseif sType == "number" or sType == "boolean" then
422 return tostring(t)
423
424 else
425 error("Cannot serialize type " .. sType, 0)
426
427 end
428end
429
430local unserialise_json
431do
432 local sub, find, match, concat, tonumber = string.sub, string.find, string.match, table.concat, tonumber
433
434 --- Skip any whitespace
435 local function skip(str, pos)
436 local _, last = find(str, "^[ \n\r\t]+", pos)
437 if last then return last + 1 else return pos end
438 end
439
440 local escapes = {
441 ["b"] = '\b', ["f"] = '\f', ["n"] = '\n', ["r"] = '\r', ["t"] = '\t',
442 ["\""] = "\"", ["/"] = "/", ["\\"] = "\\",
443 }
444
445 local mt = {}
446
447 local function error_at(pos, msg, ...)
448 if select('#', ...) > 0 then msg = msg:format(...) end
449 error(setmetatable({ pos = pos, msg = msg }, mt))
450 end
451
452 local function expected(pos, actual, exp)
453 if actual == "" then actual = "end of input" else actual = ("%q"):format(actual) end
454 error_at(pos, "Unexpected %s, expected %s.", actual, exp)
455 end
456
457 local function parse_string(str, pos, terminate)
458 local buf, n = {}, 1
459
460 while true do
461 local c = sub(str, pos, pos)
462 if c == "" then error_at(pos, "Unexpected end of input, expected '\"'.") end
463 if c == terminate then break end
464
465 if c == '\\' then
466 -- Handle the various escapes
467 c = sub(str, pos + 1, pos + 1)
468 if c == "" then error_at(pos, "Unexpected end of input, expected escape sequence.") end
469
470 if c == "u" then
471 local num_str = match(str, "^%x%x%x%x", pos + 2)
472 if not num_str then error_at(pos, "Malformed unicode escape %q.", sub(str, pos + 2, pos + 5)) end
473 buf[n], n, pos = utf8.char(tonumber(num_str, 16)), n + 1, pos + 6
474 else
475 local unesc = escapes[c]
476 if not unesc then error_at(pos + 1, "Unknown escape character %q.", c) end
477 buf[n], n, pos = unesc, n + 1, pos + 2
478 end
479 elseif c >= '\x20' then
480 buf[n], n, pos = c, n + 1, pos + 1
481 else
482 error_at(pos + 1, "Unescaped whitespace %q.", c)
483 end
484 end
485
486 return concat(buf, "", 1, n - 1), pos + 1
487 end
488
489 local num_types = { b = true, B = true, s = true, S = true, l = true, L = true, f = true, F = true, d = true, D = true }
490 local function parse_number(str, pos, opts)
491 local _, last, num_str = find(str, '^(-?%d+%.?%d*[eE]?[+-]?%d*)', pos)
492 local val = tonumber(num_str)
493 if not val then error_at(pos, "Malformed number %q.", num_str) end
494
495 if opts.nbt_style and num_types[sub(str, last + 1, last + 1)] then return val, last + 2 end
496
497 return val, last + 1
498 end
499
500 local function parse_ident(str, pos)
501 local _, last, val = find(str, '^([%a][%w_]*)', pos)
502 return val, last + 1
503 end
504
505 local arr_types = { I = true, L = true, B = true }
506 local function decode_impl(str, pos, opts)
507 local c = sub(str, pos, pos)
508 if c == '"' then return parse_string(str, pos + 1, '"')
509 elseif c == "'" and opts.nbt_style then return parse_string(str, pos + 1, "\'")
510 elseif c == "-" or c >= "0" and c <= "9" then return parse_number(str, pos, opts)
511 elseif c == "t" then
512 if sub(str, pos + 1, pos + 3) == "rue" then return true, pos + 4 end
513 elseif c == 'f' then
514 if sub(str, pos + 1, pos + 4) == "alse" then return false, pos + 5 end
515 elseif c == 'n' then
516 if sub(str, pos + 1, pos + 3) == "ull" then
517 if opts.parse_null then
518 return json_null, pos + 4
519 else
520 return nil, pos + 4
521 end
522 end
523 elseif c == "{" then
524 local obj = {}
525
526 pos = skip(str, pos + 1)
527 c = sub(str, pos, pos)
528
529 if c == "" then return error_at(pos, "Unexpected end of input, expected '}'.") end
530 if c == "}" then return obj, pos + 1 end
531
532 while true do
533 local key, value
534 if c == "\"" then key, pos = parse_string(str, pos + 1, "\"")
535 elseif opts.nbt_style then key, pos = parse_ident(str, pos)
536 else return expected(pos, c, "object key")
537 end
538
539 pos = skip(str, pos)
540
541 c = sub(str, pos, pos)
542 if c ~= ":" then return expected(pos, c, "':'") end
543
544 value, pos = decode_impl(str, skip(str, pos + 1), opts)
545 obj[key] = value
546
547 -- Consume the next delimiter
548 pos = skip(str, pos)
549 c = sub(str, pos, pos)
550 if c == "}" then break
551 elseif c == "," then pos = skip(str, pos + 1)
552 else return expected(pos, c, "',' or '}'")
553 end
554
555 c = sub(str, pos, pos)
556 end
557
558 return obj, pos + 1
559
560 elseif c == "[" then
561 local arr, n = {}, 1
562
563 pos = skip(str, pos + 1)
564 c = sub(str, pos, pos)
565
566 if arr_types[c] and sub(str, pos + 1, pos + 1) == ";" and opts.nbt_style then
567 pos = skip(str, pos + 2)
568 c = sub(str, pos, pos)
569 end
570
571 if c == "" then return expected(pos, c, "']'") end
572 if c == "]" then return empty_json_array, pos + 1 end
573
574 while true do
575 n, arr[n], pos = n + 1, decode_impl(str, pos, opts)
576
577 -- Consume the next delimiter
578 pos = skip(str, pos)
579 c = sub(str, pos, pos)
580 if c == "]" then break
581 elseif c == "," then pos = skip(str, pos + 1)
582 else return expected(pos, c, "',' or ']'")
583 end
584 end
585
586 return arr, pos + 1
587 elseif c == "" then error_at(pos, 'Unexpected end of input.')
588 end
589
590 error_at(pos, "Unexpected character %q.", c)
591 end
592
593 --- Converts a serialised JSON string back into a reassembled Lua object.
594 --
595 -- This may be used with @{textutils.serializeJSON}, or when communicating
596 -- with command blocks or web APIs.
597 --
598 -- @tparam string s The serialised string to deserialise.
599 -- @tparam[opt] { nbt_style? = boolean, parse_null? = boolean } options
600 -- Options which control how this JSON object is parsed.
601 --
602 -- - `nbt_style`: When true, this will accept [stringified NBT][nbt] strings,
603 -- as produced by many commands.
604 -- - `parse_null`: When true, `null` will be parsed as @{json_null}, rather
605 -- than `nil`.
606 --
607 -- [nbt]: https://minecraft.gamepedia.com/NBT_format
608 -- @return[1] The deserialised object
609 -- @treturn[2] nil If the object could not be deserialised.
610 -- @treturn string A message describing why the JSON string is invalid.
611 unserialise_json = function(s, options)
612 expect(1, s, "string")
613 expect(2, options, "table", "nil")
614
615 if options then
616 field(options, "nbt_style", "boolean", "nil")
617 field(options, "nbt_style", "boolean", "nil")
618 else
619 options = {}
620 end
621
622 local ok, res, pos = pcall(decode_impl, s, skip(s, 1), options)
623 if not ok then
624 if type(res) == "table" and getmetatable(res) == mt then
625 return nil, ("Malformed JSON at position %d: %s"):format(res.pos, res.msg)
626 end
627
628 error(res, 0)
629 end
630
631 pos = skip(s, pos)
632 if pos <= #s then
633 return nil, ("Malformed JSON at position %d: Unexpected trailing character %q."):format(pos, sub(s, pos, pos))
634 end
635 return res
636
637 end
638end
639
640--- Convert a Lua object into a textual representation, suitable for
641-- saving in a file or pretty-printing.
642--
643-- @param t The object to serialise
644-- @treturn string The serialised representation
645-- @throws If the object contains a value which cannot be
646-- serialised. This includes functions and tables which appear multiple
647-- times.
648function serialize(t)
649 local tTracking = {}
650 return serializeImpl(t, tTracking, "")
651end
652
653serialise = serialize -- GB version
654
655--- Converts a serialised string back into a reassembled Lua object.
656--
657-- This is mainly used together with @{textutils.serialize}.
658--
659-- @tparam string s The serialised string to deserialise.
660-- @return[1] The deserialised object
661-- @treturn[2] nil If the object could not be deserialised.
662function unserialize(s)
663 expect(1, s, "string")
664 local func = load("return " .. s, "unserialize", "t", {})
665 if func then
666 local ok, result = pcall(func)
667 if ok then
668 return result
669 end
670 end
671 return nil
672end
673
674unserialise = unserialize -- GB version
675
676--- Returns a JSON representation of the given data.
677--
678-- This function attempts to guess whether a table is a JSON array or
679-- object. However, empty tables are assumed to be empty objects - use
680-- @{textutils.empty_json_array} to mark an empty array.
681--
682-- This is largely intended for interacting with various functions from the
683-- @{commands} API, though may also be used in making @{http} requests.
684--
685-- @param t The value to serialise. Like @{textutils.serialise}, this should not
686-- contain recursive tables or functions.
687-- @tparam[opt] boolean bNBTStyle Whether to produce NBT-style JSON (non-quoted keys)
688-- instead of standard JSON.
689-- @treturn string The JSON representation of the input.
690-- @throws If the object contains a value which cannot be
691-- serialised. This includes functions and tables which appear multiple
692-- times.
693-- @usage textutils.serializeJSON({ values = { 1, "2", true } })
694function serializeJSON(t, bNBTStyle)
695 expect(1, t, "table", "string", "number", "boolean")
696 expect(2, bNBTStyle, "boolean", "nil")
697 local tTracking = {}
698 return serializeJSONImpl(t, tTracking, bNBTStyle or false)
699end
700
701serialiseJSON = serializeJSON -- GB version
702
703unserializeJSON = unserialise_json
704unserialiseJSON = unserialise_json
705
706--- Replaces certain characters in a string to make it safe for use in URLs or POST data.
707--
708-- @tparam string str The string to encode
709-- @treturn string The encoded string.
710-- @usage print("https://example.com/?view=" .. textutils.urlEncode("some text&things"))
711function urlEncode(str)
712 expect(1, str, "string")
713 if str then
714 str = string.gsub(str, "\n", "\r\n")
715 str = string.gsub(str, "([^A-Za-z0-9 %-%_%.])", function(c)
716 local n = string.byte(c)
717 if n < 128 then
718 -- ASCII
719 return string.format("%%%02X", n)
720 else
721 -- Non-ASCII (encode as UTF-8)
722 return
723 string.format("%%%02X", 192 + bit32.band(bit32.arshift(n, 6), 31)) ..
724 string.format("%%%02X", 128 + bit32.band(n, 63))
725 end
726 end)
727 str = string.gsub(str, " ", "+")
728 end
729 return str
730end
731
732local tEmpty = {}
733
734--- Provides a list of possible completions for a partial Lua expression.
735--
736-- If the completed element is a table, suggestions will have `.` appended to
737-- them. Similarly, functions have `(` appended to them.
738--
739-- @tparam string sSearchText The partial expression to complete, such as a
740-- variable name or table index.
741--
742-- @tparam[opt] table tSearchTable The table to find variables in, defaulting to
743-- the global environment (@{_G}). The function also searches the "parent"
744-- environment via the `__index` metatable field.
745--
746-- @treturn { string... } The (possibly empty) list of completions.
747-- @see shell.setCompletionFunction
748-- @see _G.read
749-- @usage textutils.complete( "pa", _ENV )
750function complete(sSearchText, tSearchTable)
751 expect(1, sSearchText, "string")
752 expect(2, tSearchTable, "table", "nil")
753
754 if g_tLuaKeywords[sSearchText] then return tEmpty end
755 local nStart = 1
756 local nDot = string.find(sSearchText, ".", nStart, true)
757 local tTable = tSearchTable or _ENV
758 while nDot do
759 local sPart = string.sub(sSearchText, nStart, nDot - 1)
760 local value = tTable[sPart]
761 if type(value) == "table" then
762 tTable = value
763 nStart = nDot + 1
764 nDot = string.find(sSearchText, ".", nStart, true)
765 else
766 return tEmpty
767 end
768 end
769 local nColon = string.find(sSearchText, ":", nStart, true)
770 if nColon then
771 local sPart = string.sub(sSearchText, nStart, nColon - 1)
772 local value = tTable[sPart]
773 if type(value) == "table" then
774 tTable = value
775 nStart = nColon + 1
776 else
777 return tEmpty
778 end
779 end
780
781 local sPart = string.sub(sSearchText, nStart)
782 local nPartLength = #sPart
783
784 local tResults = {}
785 local tSeen = {}
786 while tTable do
787 for k, v in pairs(tTable) do
788 if not tSeen[k] and type(k) == "string" then
789 if string.find(k, sPart, 1, true) == 1 then
790 if not g_tLuaKeywords[k] and string.match(k, "^[%a_][%a%d_]*$") then
791 local sResult = string.sub(k, nPartLength + 1)
792 if nColon then
793 if type(v) == "function" then
794 table.insert(tResults, sResult .. "(")
795 elseif type(v) == "table" then
796 local tMetatable = getmetatable(v)
797 if tMetatable and (type(tMetatable.__call) == "function" or type(tMetatable.__call) == "table") then
798 table.insert(tResults, sResult .. "(")
799 end
800 end
801 else
802 if type(v) == "function" then
803 sResult = sResult .. "("
804 elseif type(v) == "table" and next(v) ~= nil then
805 sResult = sResult .. "."
806 end
807 table.insert(tResults, sResult)
808 end
809 end
810 end
811 end
812 tSeen[k] = true
813 end
814 local tMetatable = getmetatable(tTable)
815 if tMetatable and type(tMetatable.__index) == "table" then
816 tTable = tMetatable.__index
817 else
818 tTable = nil
819 end
820 end
821
822 table.sort(tResults)
823 return tResults
824end