2021-05-10 20:22:20 +00:00
|
|
|
# fsh - Forka shell
|
|
|
|
# 2021 - Seculum Forka
|
|
|
|
# This program is licensed under the agpl v3.0 only license
|
|
|
|
|
|
|
|
# imports
|
|
|
|
import streams
|
|
|
|
import osproc
|
2021-05-11 16:31:02 +00:00
|
|
|
import strtabs
|
|
|
|
import os
|
2021-05-12 12:44:24 +00:00
|
|
|
import strutils
|
2021-05-11 16:31:02 +00:00
|
|
|
|
|
|
|
# globals
|
|
|
|
var shellVariables = newStringTable(modeCaseSensitive)
|
|
|
|
# Environment variables get defined in a separate var
|
|
|
|
var envVariables = newStringTable(modeCaseSensitive)
|
|
|
|
# populate the table
|
|
|
|
for k, v in envPairs():
|
|
|
|
envVariables[k]=v
|
2021-05-10 20:22:20 +00:00
|
|
|
|
2021-05-12 12:44:24 +00:00
|
|
|
# list of builtin commands
|
2021-05-12 13:43:40 +00:00
|
|
|
let builtins: array[4, string] = [
|
2021-05-12 12:44:24 +00:00
|
|
|
"set",
|
|
|
|
"setenv",
|
|
|
|
"echo",
|
2021-05-12 13:43:40 +00:00
|
|
|
"exit",
|
2021-05-12 12:44:24 +00:00
|
|
|
]
|
|
|
|
|
2021-05-13 09:19:39 +00:00
|
|
|
# Exception types
|
|
|
|
type
|
|
|
|
ShellError= object of CatchableError
|
|
|
|
line: int
|
|
|
|
col: int
|
|
|
|
whileExec: string
|
|
|
|
ShellVariableError=object of ShellError
|
|
|
|
variable: string
|
|
|
|
ShellCommandError = object of ShellError
|
|
|
|
command: string
|
|
|
|
|
2021-05-10 20:22:20 +00:00
|
|
|
# forward declarations
|
|
|
|
proc eval(cmd: string): string
|
2021-05-11 16:31:02 +00:00
|
|
|
proc substitute(strm: Stream, delim = ";\p"): string
|
2021-05-10 20:22:20 +00:00
|
|
|
|
|
|
|
# skipSpaces reads the stream until a non-space character is found
|
|
|
|
proc skipSpaces(strm: Stream): int =
|
|
|
|
while not strm.atEnd():
|
|
|
|
if strm.peekChar() == ' ':
|
|
|
|
discard strm.readChar()
|
|
|
|
result.inc
|
|
|
|
else: break
|
|
|
|
|
|
|
|
# readLiterals parses the string passed in as a literal, preserving all braces except the first and last ones
|
|
|
|
proc readLiteral(strm: Stream): string =
|
|
|
|
var numbraces=1
|
|
|
|
while not strm.atEnd() and numbraces > 0:
|
|
|
|
let c = strm.readChar()
|
|
|
|
case c:
|
|
|
|
of '{':
|
|
|
|
numbraces.inc
|
|
|
|
of '}':
|
|
|
|
numbraces = numbraces - 1
|
|
|
|
of '\\':
|
|
|
|
if strm.peekChar notin {'{', '}'}: result.add(strm.readChar)
|
|
|
|
result.add(strm.readChar)
|
|
|
|
else:
|
|
|
|
result.add(c)
|
|
|
|
|
|
|
|
# a forward declaration. Fuck this shit
|
|
|
|
proc readSubstitution(strm: Stream): string
|
|
|
|
|
2021-05-11 16:38:32 +00:00
|
|
|
# readInterpelation reads the interpelation in between [ and ]
|
2021-05-10 20:22:20 +00:00
|
|
|
proc readInterpelation(strm: Stream): string =
|
|
|
|
while not strm.atEnd():
|
|
|
|
let c = strm.readChar()
|
|
|
|
case c:
|
|
|
|
of '{':
|
|
|
|
result.add('{')
|
|
|
|
result.add(strm.readLiteral)
|
|
|
|
result.add('}')
|
|
|
|
of '[':
|
|
|
|
result.add('[')
|
|
|
|
result.add(strm.readInterpelation)
|
|
|
|
result.add(']')
|
|
|
|
of '"':
|
|
|
|
result.add("\"")
|
|
|
|
result.add(strm.readSubstitution)
|
|
|
|
result.add("\"")
|
|
|
|
of '\\':
|
|
|
|
result.add("\\")
|
|
|
|
result.add(strm.readChar())
|
|
|
|
of ']':
|
|
|
|
break
|
|
|
|
else: result.add(c)
|
|
|
|
|
|
|
|
# readSubstitution reads a substitutionbetween "quotes"
|
|
|
|
proc readSubstitution(strm: Stream): string =
|
|
|
|
while not strm.atEnd():
|
|
|
|
let c = strm.readChar()
|
|
|
|
case c:
|
|
|
|
of '{':
|
|
|
|
result.add('{')
|
|
|
|
result.add(strm.readLiteral)
|
|
|
|
result.add('}')
|
|
|
|
of '[':
|
|
|
|
result.add('[')
|
|
|
|
result.add(strm.readInterpelation)
|
|
|
|
result.add(']')
|
|
|
|
of '"':
|
|
|
|
break
|
|
|
|
of '\\':
|
|
|
|
result.add("\\")
|
|
|
|
result.add(strm.readChar())
|
|
|
|
else: result.add(c)
|
|
|
|
|
|
|
|
# readCommand reads a command from stream
|
|
|
|
proc readCommand(strm: Stream): string =
|
|
|
|
while not strm.atEnd():
|
|
|
|
let c = strm.readChar()
|
|
|
|
case c:
|
|
|
|
of '{':
|
|
|
|
result.add('{')
|
|
|
|
result.add(strm.readLiteral)
|
|
|
|
result.add('}')
|
|
|
|
of '[':
|
|
|
|
result.add('[')
|
|
|
|
result.add(strm.readInterpelation)
|
|
|
|
result.add(']')
|
|
|
|
of '"':
|
|
|
|
result.add("\"")
|
|
|
|
result.add(strm.readSubstitution)
|
|
|
|
result.add("\"")
|
|
|
|
of '\\':
|
|
|
|
result.add("\\")
|
|
|
|
result.add(strm.readChar())
|
2021-05-12 19:04:29 +00:00
|
|
|
of '\n', '\r', ';':
|
|
|
|
break
|
2021-05-10 20:22:20 +00:00
|
|
|
else: result.add(c)
|
|
|
|
|
2021-05-11 16:31:02 +00:00
|
|
|
# readVariable reads a variable
|
|
|
|
proc readVariable(strm: Stream): string =
|
|
|
|
let c = strm.readChar()
|
|
|
|
case c:
|
|
|
|
of '{':
|
|
|
|
result = strm.readLiteral()
|
|
|
|
of '[':
|
|
|
|
result.add(strm.readInterpelation)
|
|
|
|
of '"':
|
|
|
|
result.add(strm.readSubstitution())
|
|
|
|
of ']', '}':
|
|
|
|
stderr.write("Unexpected " & c & ".\n")
|
|
|
|
else:
|
|
|
|
result.add(c)
|
|
|
|
while not strm.atEnd():
|
|
|
|
let d = strm.readChar()
|
|
|
|
case d:
|
|
|
|
of 'a'..'z', 'A'..'Z', '0'..'9', '_', ':': result.add(d)
|
|
|
|
else: break
|
|
|
|
|
|
|
|
# evalVariable finds a value of a variable
|
|
|
|
proc evalVariable(vari: string): string =
|
|
|
|
if shellVariables.hasKey(vari):
|
|
|
|
result=shellVariables[vari]
|
|
|
|
elif envVariables.hasKey(vari):
|
|
|
|
result = envVariables[vari]
|
2021-05-13 09:25:28 +00:00
|
|
|
else:
|
|
|
|
var e = newException(ShellVariableError, "No such variable: " & vari)
|
|
|
|
e.variable = vari
|
|
|
|
raise e
|
2021-05-11 16:31:02 +00:00
|
|
|
|
2021-05-10 20:22:20 +00:00
|
|
|
# substitute does a substitution on the stream passed in
|
|
|
|
proc substitute(strm: Stream, delim=";\p"): string =
|
|
|
|
while not strm.atEnd():
|
|
|
|
let c = strm.readChar()
|
|
|
|
if c in delim: break
|
|
|
|
case c:
|
|
|
|
of '{':
|
|
|
|
result.add(strm.readLiteral)
|
|
|
|
of '}':
|
|
|
|
stderr.write("Extra closing brace")
|
|
|
|
of '[':
|
|
|
|
result.add(strm.readInterpelation.eval)
|
|
|
|
of ']':
|
|
|
|
stderr.write("Extra closing bracket")
|
|
|
|
of '\\':
|
|
|
|
result.add(strm.readChar())
|
2021-05-11 16:38:32 +00:00
|
|
|
of '$': result.add(strm.readVariable.evalVariable)
|
2021-05-12 19:04:29 +00:00
|
|
|
of ';', '\n', '\r': break
|
2021-05-10 20:22:20 +00:00
|
|
|
else:
|
|
|
|
result.add(c)
|
|
|
|
|
|
|
|
# parseCommand parses the string given into a list representing a command
|
|
|
|
proc parseCommand(cmd: string): seq[string] =
|
|
|
|
var newitem = ""
|
|
|
|
let strm = newStringStream(cmd)
|
|
|
|
while true:
|
|
|
|
if strm.atEnd():
|
|
|
|
result.add(newitem)
|
|
|
|
break
|
|
|
|
let c = strm.readChar()
|
|
|
|
case c:
|
|
|
|
of '{':
|
|
|
|
newitem.add(strm.readLiteral)
|
|
|
|
of '[':
|
|
|
|
newitem.add(strm.readInterpelation.eval)
|
|
|
|
of '"':
|
|
|
|
newitem.add(strm.readSubstitution.newStringStream.substitute)
|
2021-05-11 16:38:32 +00:00
|
|
|
of '$': result.add(strm.readVariable.evalVariable)
|
2021-05-10 20:22:20 +00:00
|
|
|
of ' ':
|
2021-05-12 20:12:58 +00:00
|
|
|
if (result == @[]) and (newitem == ""): continue
|
2021-05-10 20:22:20 +00:00
|
|
|
result.add(newitem)
|
|
|
|
newitem = ""
|
|
|
|
discard strm.skipSpaces()
|
2021-05-12 19:04:29 +00:00
|
|
|
of ';', '\n', '\r':
|
|
|
|
result.add(newitem)
|
|
|
|
break
|
2021-05-10 20:22:20 +00:00
|
|
|
else: newitem.add(c)
|
|
|
|
|
2021-05-12 12:44:24 +00:00
|
|
|
# runBuiltin runs a shell builtin
|
|
|
|
proc runBuiltin(builtin: string, args: openArray[string]): string =
|
|
|
|
case builtin:
|
|
|
|
of "set":
|
|
|
|
if args.len < 2:
|
2021-05-13 09:27:53 +00:00
|
|
|
var e = newException(ShellCommandError, "Set: not enough arguments provided")
|
|
|
|
e.command="set"
|
|
|
|
raise e
|
2021-05-12 12:44:24 +00:00
|
|
|
shellVariables[args[0]] = args[1..^1].join(" ")
|
|
|
|
return ""
|
|
|
|
of "setenv":
|
|
|
|
if args.len < 2:
|
|
|
|
raise newException(Exception, "setenv: Not enough arguments provided")
|
|
|
|
try:
|
|
|
|
envVariables[args[0]] = args[1..^1].join(":")
|
|
|
|
putEnv(args[0], args[1..^1].join(":"))
|
|
|
|
except OSError as e:
|
|
|
|
raise newException(Exception, "setenv: " & e.msg)
|
|
|
|
return ""
|
|
|
|
of "echo":
|
|
|
|
if args[0] == "-n":
|
|
|
|
return args[1..^1].join(" ")
|
|
|
|
return args.join(" ") & "\p"
|
2021-05-12 13:43:40 +00:00
|
|
|
of "exit":
|
|
|
|
if args.len == 0:
|
|
|
|
quit(0)
|
|
|
|
elif args.len == 1:
|
|
|
|
try:
|
|
|
|
quit(args[0].parseInt)
|
|
|
|
except ValueError:
|
|
|
|
quit(0)
|
|
|
|
else:
|
|
|
|
try:
|
|
|
|
let exitcode=args[0].parseInt
|
|
|
|
if exitcode > 0:
|
|
|
|
stderr.write(args[1..^1].join(" "), "\p")
|
|
|
|
else:
|
|
|
|
stdout.write(args[1..^1].join(" "), "\p")
|
|
|
|
quit(exitcode)
|
|
|
|
except ValueError:
|
|
|
|
stdout.write(args.join(" "), "\p")
|
|
|
|
quit(0)
|
2021-05-12 12:44:24 +00:00
|
|
|
else:
|
|
|
|
raise newException(Exception, "Fsh: No such builtin implemented")
|
|
|
|
|
2021-05-10 20:22:20 +00:00
|
|
|
# executes executes the command. For now it involves catting the command and printing it
|
2021-05-11 08:27:40 +00:00
|
|
|
proc execute(cmd: string): int =
|
2021-05-12 19:07:12 +00:00
|
|
|
let strm=cmd.newStringStream
|
|
|
|
while not strm.atEnd:
|
|
|
|
let parsed = strm.readCommand.parseCommand
|
2021-05-12 20:19:26 +00:00
|
|
|
if parsed == @[]:
|
|
|
|
continue
|
2021-05-12 19:07:12 +00:00
|
|
|
let progname = parsed[0]
|
|
|
|
let args = parsed[1..parsed.high]
|
|
|
|
if progname in builtins:
|
|
|
|
stdout.write(runBuiltin(progname, args))
|
|
|
|
result = 0
|
2021-05-12 20:16:15 +00:00
|
|
|
continue
|
2021-05-12 19:07:12 +00:00
|
|
|
let ps = startProcess(progname, args=args, options={poUsePath, poParentStreams})
|
|
|
|
result = ps.waitForExit
|
|
|
|
ps.close
|
|
|
|
strm.close
|
2021-05-10 20:22:20 +00:00
|
|
|
|
|
|
|
# eval evaluates the given string
|
|
|
|
proc eval(cmd: string): string =
|
2021-05-12 19:04:29 +00:00
|
|
|
let strm=cmd.newStringStream
|
|
|
|
while not strm.atEnd():
|
|
|
|
let parsed = strm.readCommand.parseCommand
|
2021-05-12 20:19:26 +00:00
|
|
|
if parsed == @[]:
|
|
|
|
continue
|
2021-05-12 19:04:29 +00:00
|
|
|
let progname=parsed[0]
|
|
|
|
let args = parsed[1..parsed.high]
|
|
|
|
if progname in builtins:
|
|
|
|
result = runBuiltin(progname, args)
|
2021-05-12 20:16:15 +00:00
|
|
|
continue
|
2021-05-12 19:04:29 +00:00
|
|
|
let ps = startProcess(progname, args=args, options={poUsePath})
|
|
|
|
result=ps.outputStream.readAll
|
|
|
|
ps.close
|
|
|
|
strm.close
|
2021-05-10 20:22:20 +00:00
|
|
|
|
|
|
|
when isMainModule:
|
|
|
|
let stdinstrm = stdin.newFileStream
|
|
|
|
while not stdinstrm.atEnd():
|
2021-05-11 09:05:35 +00:00
|
|
|
stdinstrm.readCommand.execute.echo
|