· 5 years ago · Sep 30, 2020, 01:26 AM
1--- The shell API provides access to CraftOS's command line interface.
2--
3-- It allows you to @{run|start programs}, @{setCompletionFunction|add
4-- completion for a program}, and much more.
5--
6-- @{shell} is not a "true" API. Instead, it is a standard program, which its
7-- API into the programs that it launches. This allows for multiple shells to
8-- run at the same time, but means that the API is not available in the global
9-- environment, and so is unavailable to other @{os.loadAPI|APIs}.
10--
11-- @module[module] shell
12
13term.clear()
14term.setCursorPos(1,1)
15file = require("libraries.file")
16file.loadGrpLines("graphics/bootSplash.skgrp")
17gpswrapper = require("libraries.gpswrapper")
18--Do server side things BEFORE term.clear()
19local function gpsGet()
20 local x, y, z = gpswrapper.gpslocate(5)
21 gpsCoords = vector.new(math.floor(x+0.5), math.floor(y+0.5), math.floor(z+0.5))
22 gpsTable = file.split(gpsCoords,",")
23 return gpsTable
24end
25term.setBackgroundColour(colours.black)
26term.clear()
27--Load DE
28--parallel.waitForAll(function() shell.run("customPrograms/timeManager.lua") end, function() shell.run("customPrograms/applications.lua") end)
29local function drawTime(x,y,backColour,textColour)
30 local time = textutils.formatTime(os.time(), true)
31 term.setCursorPos(x,y)
32 term.setBackgroundColour(backColour)
33 term.setTextColour(textColour)
34 term.write(time)
35end
36local function drawDesktop()
37 desktopImg = "graphics/background/default.skgrp"
38 taskbarImg = "graphics/taskbar.skgrp"
39 file.loadGrpLines(desktopImg)
40 file.loadGrpLines(taskbarImg)
41end
42drawDesktop()
43while true do
44 term.setCursorPos(22,20)
45 term.write(" ")
46 drawTime(22,20,128,256)
47 os.queueEvent("")
48 local event,pressedKey = os.pullEvent()
49 if event == "key" then
50 if pressedKey == 18 then
51 break
52 end
53 end
54 sleep()
55end
56
57local expect = dofile("rom/modules/main/cc/expect.lua").expect
58local make_package = dofile("rom/modules/main/cc/require.lua").make
59
60local multishell = multishell
61local parentShell = shell
62local parentTerm = term.current()
63
64if multishell then
65 multishell.setTitle(multishell.getCurrent(), "shell")
66end
67
68local bExit = false
69local sDir = parentShell and parentShell.dir() or ""
70local sPath = parentShell and parentShell.path() or ".:/rom/programs"
71local tAliases = parentShell and parentShell.aliases() or {}
72local tCompletionInfo = parentShell and parentShell.getCompletionInfo() or {}
73local tProgramStack = {}
74
75local shell = {} --- @export
76local function createShellEnv(dir)
77 local env = { shell = shell, multishell = multishell }
78 env.require, env.package = make_package(env, dir)
79 return env
80end
81
82-- Colours
83local promptColour, textColour, bgColour
84if term.isColour() then
85 promptColour = colours.yellow
86 textColour = colours.white
87 bgColour = colours.black
88else
89 promptColour = colours.white
90 textColour = colours.white
91 bgColour = colours.black
92end
93
94--- Run a program with the supplied arguments.
95--
96-- Unlike @{shell.run}, each argument is passed to the program verbatim. While
97-- `shell.run("echo", "b c")` runs `echo` with `b` and `c`,
98-- `shell.execute("echo", "b c")` runs `echo` with a single argument `b c`.
99--
100-- @tparam string command The program to execute.
101-- @tparam string ... Arguments to this program.
102-- @treturn boolean Whether the program exited successfully.
103-- @usage Run `paint my-image` from within your program:
104--
105-- shell.execute("paint", "my-image")
106function shell.execute(command, ...)
107 expect(1, command, "string")
108 for i = 1, select('#', ...) do
109 expect(i + 1, select(i, ...), "string")
110 end
111
112 local sPath = shell.resolveProgram(command)
113 if sPath ~= nil then
114 tProgramStack[#tProgramStack + 1] = sPath
115 if multishell then
116 local sTitle = fs.getName(sPath)
117 if sTitle:sub(-4) == ".lua" then
118 sTitle = sTitle:sub(1, -5)
119 end
120 multishell.setTitle(multishell.getCurrent(), sTitle)
121 end
122
123 local sDir = fs.getDir(sPath)
124 local env = createShellEnv(sDir)
125 env.arg = { [0] = command, ... }
126 local result = os.run(env, sPath, ...)
127
128 tProgramStack[#tProgramStack] = nil
129 if multishell then
130 if #tProgramStack > 0 then
131 local sTitle = fs.getName(tProgramStack[#tProgramStack])
132 if sTitle:sub(-4) == ".lua" then
133 sTitle = sTitle:sub(1, -5)
134 end
135 multishell.setTitle(multishell.getCurrent(), sTitle)
136 else
137 multishell.setTitle(multishell.getCurrent(), "shell")
138 end
139 end
140 return result
141 else
142 printError("No such program")
143 return false
144 end
145end
146
147local function tokenise(...)
148 local sLine = table.concat({ ... }, " ")
149 local tWords = {}
150 local bQuoted = false
151 for match in string.gmatch(sLine .. "\"", "(.-)\"") do
152 if bQuoted then
153 table.insert(tWords, match)
154 else
155 for m in string.gmatch(match, "[^ \t]+") do
156 table.insert(tWords, m)
157 end
158 end
159 bQuoted = not bQuoted
160 end
161 return tWords
162end
163
164-- Install shell API
165
166--- Run a program with the supplied arguments.
167--
168-- All arguments are concatenated together and then parsed as a command line. As
169-- a result, `shell.run("program a b")` is the same as `shell.run("program",
170-- "a", "b")`.
171--
172-- @tparam string ... The program to run and its arguments.
173-- @treturn boolean Whether the program exited successfully.
174-- @usage Run `paint my-image` from within your program:
175--
176-- shell.run("paint", "my-image")
177-- @see shell.execute Run a program directly without parsing the arguments.
178function shell.run(...)
179 local tWords = tokenise(...)
180 local sCommand = tWords[1]
181 if sCommand then
182 return shell.execute(sCommand, table.unpack(tWords, 2))
183 end
184 return false
185end
186
187--- Exit the current shell.
188--
189-- This does _not_ terminate your program, it simply makes the shell terminate
190-- after your program has finished. If this is the toplevel shell, then the
191-- computer will be shutdown.
192function shell.exit()
193 bExit = true
194end
195
196--- Return the current working directory. This is what is displayed before the
197-- `> ` of the shell prompt, and is used by @{shell.resolve} to handle relative
198-- paths.
199--
200-- @treturn string The current working directory.
201-- @see setDir To change the working directory.
202function shell.dir()
203 return sDir
204end
205
206--- Set the current working directory.
207--
208-- @tparam string dir The new working directory.
209-- @throws If the path does not exist or is not a directory.
210-- @usage Set the working directory to "rom"
211--
212-- shell.setDir("rom")
213function shell.setDir(dir)
214 expect(1, dir, "string")
215 if not fs.isDir(dir) then
216 error("Not a directory", 2)
217 end
218 sDir = fs.combine(dir, "")
219end
220
221--- Set the path where programs are located.
222--
223-- The path is composed of a list of directory names in a string, each separated
224-- by a colon (`:`). On normal turtles will look in the current directory (`.`),
225-- `/rom/programs` and `/rom/programs/turtle` folder, making the path
226-- `.:/rom/programs:/rom/programs/turtle`.
227--
228-- @treturn string The current shell's path.
229-- @see setPath To change the current path.
230function shell.path()
231 return sPath
232end
233
234--- Set the @{path|current program path}.
235--
236-- Be careful to prefix directories with a `/`. Otherwise they will be searched
237-- for from the @{shell.dir|current directory}, rather than the computer's root.
238--
239-- @tparam string path The new program path.
240function shell.setPath(path)
241 expect(1, path, "string")
242 sPath = path
243end
244
245--- Resolve a relative path to an absolute path.
246--
247-- The @{fs} and @{io} APIs work using absolute paths, and so we must convert
248-- any paths relative to the @{dir|current directory} to absolute ones. This
249-- does nothing when the path starts with `/`.
250--
251-- @tparam string path The path to resolve.
252-- @usage Resolve `startup.lua` when in the `rom` folder.
253--
254-- shell.setDir("rom")
255-- print(shell.resolve("startup.lua"))
256-- -- => rom/startup.lua
257function shell.resolve(path)
258 expect(1, path, "string")
259 local sStartChar = string.sub(path, 1, 1)
260 if sStartChar == "/" or sStartChar == "\\" then
261 return fs.combine("", path)
262 else
263 return fs.combine(sDir, path)
264 end
265end
266
267local function pathWithExtension(_sPath, _sExt)
268 local nLen = #sPath
269 local sEndChar = string.sub(_sPath, nLen, nLen)
270 -- Remove any trailing slashes so we can add an extension to the path safely
271 if sEndChar == "/" or sEndChar == "\\" then
272 _sPath = string.sub(_sPath, 1, nLen - 1)
273 end
274 return _sPath .. "." .. _sExt
275end
276
277--- Resolve a program, using the @{path|program path} and list of @{aliases|aliases}.
278--
279-- @tparam string command The name of the program
280-- @treturn string|nil The absolute path to the program, or @{nil} if it could
281-- not be found.
282-- @usage Locate the `hello` program.
283--
284-- shell.resolveProgram("hello")
285-- -- => rom/programs/fun/hello.lua
286function shell.resolveProgram(command)
287 expect(1, command, "string")
288 -- Substitute aliases firsts
289 if tAliases[command] ~= nil then
290 command = tAliases[command]
291 end
292
293 -- If the path is a global path, use it directly
294 if command:find("/") or command:find("\\") then
295 local sPath = shell.resolve(command)
296 if fs.exists(sPath) and not fs.isDir(sPath) then
297 return sPath
298 else
299 local sPathLua = pathWithExtension(sPath, "lua")
300 if fs.exists(sPathLua) and not fs.isDir(sPathLua) then
301 return sPathLua
302 end
303 end
304 return nil
305 end
306
307 -- Otherwise, look on the path variable
308 for sPath in string.gmatch(sPath, "[^:]+") do
309 sPath = fs.combine(shell.resolve(sPath), command)
310 if fs.exists(sPath) and not fs.isDir(sPath) then
311 return sPath
312 else
313 local sPathLua = pathWithExtension(sPath, "lua")
314 if fs.exists(sPathLua) and not fs.isDir(sPathLua) then
315 return sPathLua
316 end
317 end
318 end
319
320 -- Not found
321 return nil
322end
323
324--- Return a list of all programs on the @{shell.path|path}.
325--
326-- @tparam[opt] boolean include_hidden Include hidden files. Namely, any which
327-- start with `.`.
328-- @treturn { string } A list of available programs.
329-- @usage textutils.tabulate(shell.programs())
330function shell.programs(include_hidden)
331 expect(1, include_hidden, "boolean", "nil")
332
333 local tItems = {}
334
335 -- Add programs from the path
336 for sPath in string.gmatch(sPath, "[^:]+") do
337 sPath = shell.resolve(sPath)
338 if fs.isDir(sPath) then
339 local tList = fs.list(sPath)
340 for n = 1, #tList do
341 local sFile = tList[n]
342 if not fs.isDir(fs.combine(sPath, sFile)) and
343 (include_hidden or string.sub(sFile, 1, 1) ~= ".") then
344 if #sFile > 4 and sFile:sub(-4) == ".lua" then
345 sFile = sFile:sub(1, -5)
346 end
347 tItems[sFile] = true
348 end
349 end
350 end
351 end
352
353 -- Sort and return
354 local tItemList = {}
355 for sItem in pairs(tItems) do
356 table.insert(tItemList, sItem)
357 end
358 table.sort(tItemList)
359 return tItemList
360end
361
362local function completeProgram(sLine)
363 if #sLine > 0 and (sLine:find("/") or sLine:find("\\")) then
364 -- Add programs from the root
365 return fs.complete(sLine, sDir, true, false)
366
367 else
368 local tResults = {}
369 local tSeen = {}
370
371 -- Add aliases
372 for sAlias in pairs(tAliases) do
373 if #sAlias > #sLine and string.sub(sAlias, 1, #sLine) == sLine then
374 local sResult = string.sub(sAlias, #sLine + 1)
375 if not tSeen[sResult] then
376 table.insert(tResults, sResult)
377 tSeen[sResult] = true
378 end
379 end
380 end
381
382 -- Add all subdirectories. We don't include files as they will be added in the block below
383 local tDirs = fs.complete(sLine, sDir, false, false)
384 for i = 1, #tDirs do
385 local sResult = tDirs[i]
386 if not tSeen[sResult] then
387 table.insert (tResults, sResult)
388 tSeen [sResult] = true
389 end
390 end
391
392 -- Add programs from the path
393 local tPrograms = shell.programs()
394 for n = 1, #tPrograms do
395 local sProgram = tPrograms[n]
396 if #sProgram > #sLine and string.sub(sProgram, 1, #sLine) == sLine then
397 local sResult = string.sub(sProgram, #sLine + 1)
398 if not tSeen[sResult] then
399 table.insert(tResults, sResult)
400 tSeen[sResult] = true
401 end
402 end
403 end
404
405 -- Sort and return
406 table.sort(tResults)
407 return tResults
408 end
409end
410
411local function completeProgramArgument(sProgram, nArgument, sPart, tPreviousParts)
412 local tInfo = tCompletionInfo[sProgram]
413 if tInfo then
414 return tInfo.fnComplete(shell, nArgument, sPart, tPreviousParts)
415 end
416 return nil
417end
418
419--- Complete a shell command line.
420--
421-- This accepts an incomplete command, and completes the program name or
422-- arguments. For instance, `l` will be completed to `ls`, and `ls ro` will be
423-- completed to `ls rom/`.
424--
425-- Completion handlers for your program may be registered with
426-- @{shell.setCompletionFunction}.
427--
428-- @tparam string sLine The input to complete.
429-- @treturn { string }|nil The list of possible completions.
430-- @see read For more information about completion.
431-- @see shell.completeProgram
432-- @see shell.setCompletionFunction
433-- @see shell.getCompletionInfo
434function shell.complete(sLine)
435 expect(1, sLine, "string")
436 if #sLine > 0 then
437 local tWords = tokenise(sLine)
438 local nIndex = #tWords
439 if string.sub(sLine, #sLine, #sLine) == " " then
440 nIndex = nIndex + 1
441 end
442 if nIndex == 1 then
443 local sBit = tWords[1] or ""
444 local sPath = shell.resolveProgram(sBit)
445 if tCompletionInfo[sPath] then
446 return { " " }
447 else
448 local tResults = completeProgram(sBit)
449 for n = 1, #tResults do
450 local sResult = tResults[n]
451 local sPath = shell.resolveProgram(sBit .. sResult)
452 if tCompletionInfo[sPath] then
453 tResults[n] = sResult .. " "
454 end
455 end
456 return tResults
457 end
458
459 elseif nIndex > 1 then
460 local sPath = shell.resolveProgram(tWords[1])
461 local sPart = tWords[nIndex] or ""
462 local tPreviousParts = tWords
463 tPreviousParts[nIndex] = nil
464 return completeProgramArgument(sPath , nIndex - 1, sPart, tPreviousParts)
465
466 end
467 end
468 return nil
469end
470
471--- Complete the name of a program.
472--
473-- @tparam string program The name of a program to complete.
474-- @treturn { string } A list of possible completions.
475-- @see cc.shell.completion.program
476function shell.completeProgram(program)
477 expect(1, program, "string")
478 return completeProgram(program)
479end
480
481--- Set the completion function for a program. When the program is entered on
482-- the command line, this program will be called to provide auto-complete
483-- information.
484--
485-- The completion function accepts four arguments:
486--
487-- 1. The current shell. As completion functions are inherited, this is not
488-- guaranteed to be the shell you registered this function in.
489-- 2. The index of the argument currently being completed.
490-- 3. The current argument. This may be the empty string.
491-- 4. A list of the previous arguments.
492--
493-- For instance, when completing `pastebin put rom/st` our pastebin completion
494-- function will receive the shell API, an index of 2, `rom/st` as the current
495-- argument, and a "previous" table of `{ "put" }`. This function may then wish
496-- to return a table containing `artup.lua`, indicating the entire command
497-- should be completed to `pastebin put rom/startup.lua`.
498--
499-- You completion entries may also be followed by a space, if you wish to
500-- indicate another argument is expected.
501--
502-- @tparam string program The path to the program. This should be an absolute path
503-- _without_ the leading `/`.
504-- @tparam function(shell: table, index: number, argument: string, previous: { string }):({ string }|nil) complete
505-- The completion function.
506-- @see cc.shell.completion Various utilities to help with writing completion functions.
507-- @see shell.complete
508-- @see read For more information about completion.
509function shell.setCompletionFunction(program, complete)
510 expect(1, program, "string")
511 expect(2, complete, "function")
512 tCompletionInfo[program] = {
513 fnComplete = complete,
514 }
515end
516
517--- Get a table containing all completion functions.
518--
519-- This should only be needed when building custom shells. Use
520-- @{setCompletionFunction} to add a completion function.
521--
522-- @treturn { [string] = { fnComplete = function } } A table mapping the
523-- absolute path of programs, to their completion functions.
524function shell.getCompletionInfo()
525 return tCompletionInfo
526end
527
528--- Returns the path to the currently running program.
529--
530-- @treturn string The absolute path to the running program.
531function shell.getRunningProgram()
532 if #tProgramStack > 0 then
533 return tProgramStack[#tProgramStack]
534 end
535 return nil
536end
537
538--- Add an alias for a program.
539--
540-- @tparam string command The name of the alias to add.
541-- @tparam string program The name or path to the program.
542-- @usage Alias `vim` to the `edit` program
543--
544-- shell.setAlias("vim", "edit")
545function shell.setAlias(command, program)
546 expect(1, command, "string")
547 expect(2, program, "string")
548 tAliases[command] = program
549end
550
551--- Remove an alias.
552--
553-- @tparam string command The alias name to remove.
554function shell.clearAlias(command)
555 expect(1, command, "string")
556 tAliases[command] = nil
557end
558
559--- Get the current aliases for this shell.
560--
561-- Aliases are used to allow multiple commands to refer to a single program. For
562-- instance, the `list` program is aliased `dir` or `ls`. Running `ls`, `dir` or
563-- `list` in the shell will all run the `list` program.
564--
565-- @treturn { [string] = string } A table, where the keys are the names of
566-- aliases, and the values are the path to the program.
567-- @see shell.setAlias
568-- @see shell.resolveProgram This uses aliases when resolving a program name to
569-- an absolute path.
570function shell.aliases()
571 -- Copy aliases
572 local tCopy = {}
573 for sAlias, sCommand in pairs(tAliases) do
574 tCopy[sAlias] = sCommand
575 end
576 return tCopy
577end
578
579if multishell then
580 --- Open a new @{multishell} tab running a command.
581 --
582 -- This behaves similarly to @{shell.run}, but instead returns the process
583 -- index.
584 --
585 -- This function is only available if the @{multishell} API is.
586 --
587 -- @tparam string ... The command line to run.
588 -- @see shell.run
589 -- @see multishell.launch
590 -- @usage Launch the Lua interpreter and switch to it.
591 --
592 -- local id = shell.openTab("lua")
593 -- shell.switchTab(id)
594 function shell.openTab(...)
595 local tWords = tokenise(...)
596 local sCommand = tWords[1]
597 if sCommand then
598 local sPath = shell.resolveProgram(sCommand)
599 if sPath == "rom/programs/shell.lua" then
600 return multishell.launch(createShellEnv("rom/programs"), sPath, table.unpack(tWords, 2))
601 elseif sPath ~= nil then
602 return multishell.launch(createShellEnv("rom/programs"), "rom/programs/shell.lua", sCommand, table.unpack(tWords, 2))
603 else
604 printError("No such program")
605 end
606 end
607 end
608
609 --- Switch to the @{multishell} tab with the given index.
610 --
611 -- @tparam number id The tab to switch to.
612 -- @see multishell.setFocus
613 function shell.switchTab(id)
614 expect(1, id, "number")
615 multishell.setFocus(id)
616 end
617end
618
619local tArgs = { ... }
620if #tArgs > 0 then
621 -- "shell x y z"
622 -- Run the program specified on the commandline
623 shell.run(...)
624
625else
626 -- "shell"
627 -- Print the header
628 term.setBackgroundColor(bgColour)
629 term.setTextColour(promptColour)
630 print(os.version())
631 term.setTextColour(textColour)
632
633 -- Run the startup program
634 if parentShell == nil then
635 shell.run("/rom/startup.lua")
636 end
637
638 -- Read commands and execute them
639 local tCommandHistory = {}
640 while not bExit do
641 term.redirect(parentTerm)
642 term.setBackgroundColor(bgColour)
643 term.setTextColour(promptColour)
644 write(shell.dir() .. ":S> ")
645 term.setTextColour(textColour)
646
647
648 local sLine
649 if settings.get("shell.autocomplete") then
650 sLine = read(nil, tCommandHistory, shell.complete)
651 else
652 sLine = read(nil, tCommandHistory)
653 end
654 if sLine:match("%S") and tCommandHistory[#tCommandHistory] ~= sLine then
655 table.insert(tCommandHistory, sLine)
656 end
657 shell.run(sLine)
658 end
659end