fsh/src/fsh.nim

301 lines
8.0 KiB
Nim

# fsh - Forka shell
# 2021 - Seculum Forka
# This program is licensed under the agpl v3.0 only license
# imports
import streams
import osproc
import strtabs
import os
import strutils
# 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
# list of builtin commands
let builtins: array[4, string] = [
"set",
"setenv",
"echo",
"exit",
]
# 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
# forward declarations
proc eval(cmd: string): string
proc substitute(strm: Stream, delim = ";\p"): string
# 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
# readInterpelation reads the interpelation in between [ and ]
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())
of '\n', '\r', ';':
break
else: result.add(c)
# 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]
else:
var e = newException(ShellVariableError, "No such variable: " & vari)
e.variable = vari
raise e
# 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())
of '$': result.add(strm.readVariable.evalVariable)
of ';', '\n', '\r': break
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)
of '$': result.add(strm.readVariable.evalVariable)
of ' ':
if (result == @[]) and (newitem == ""): continue
result.add(newitem)
newitem = ""
discard strm.skipSpaces()
of ';', '\n', '\r':
result.add(newitem)
break
else: newitem.add(c)
# runBuiltin runs a shell builtin
proc runBuiltin(builtin: string, args: openArray[string]): string =
case builtin:
of "set":
if args.len < 2:
var e = newException(ShellCommandError, "Set: not enough arguments provided")
e.command="set"
raise e
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"
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)
else:
raise newException(Exception, "Fsh: No such builtin implemented")
# executes executes the command. For now it involves catting the command and printing it
proc execute(cmd: string): int =
let strm=cmd.newStringStream
while not strm.atEnd:
let parsed = strm.readCommand.parseCommand
if parsed == @[]:
continue
let progname = parsed[0]
let args = parsed[1..parsed.high]
if progname in builtins:
stdout.write(runBuiltin(progname, args))
result = 0
continue
let ps = startProcess(progname, args=args, options={poUsePath, poParentStreams})
result = ps.waitForExit
ps.close
strm.close
# eval evaluates the given string
proc eval(cmd: string): string =
let strm=cmd.newStringStream
while not strm.atEnd():
let parsed = strm.readCommand.parseCommand
if parsed == @[]:
continue
let progname=parsed[0]
let args = parsed[1..parsed.high]
if progname in builtins:
result = runBuiltin(progname, args)
continue
let ps = startProcess(progname, args=args, options={poUsePath})
result=ps.outputStream.readAll
ps.close
strm.close
when isMainModule:
let stdinstrm = stdin.newFileStream
while not stdinstrm.atEnd():
stdinstrm.readCommand.execute.echo