fsh/src/fsh.nim

425 lines
12 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
# shellVariables are an array of vars
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[7, string] = [
"set",
"setenv",
"echo",
"exit",
"if",
"cd",
"let",
]
# number of frames to be used for builtins like set
var numframes: int=1
# 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
InterruptCtrlC = object of CatchableError
# procs for making procedures
proc newShellException(msg: string, line=0, col=0, whileExec=""): ref ShellError =
var e = newException(ShellError, msg)
e.line=line
e.col=col
e.whileExec = whileExec
return e
proc newShellVariableException(msg: string, vari: string, line=0, col=0, whileExec=""): ref ShellVariableError =
var e= newException(ShellVariableError, vari & ": " & msg)
e.variable =vari
e.line=line
e.col=col
e.whileExec=whileExec
return e
proc newShellCommandException(msg: string, command: string, line=0, col=0, whileExec=""): ref ShellCommandError =
var e=newException(ShellCommandError, command & ": " & msg)
e.command=command
e.line=line
e.col=col
e.whileExec=whileExec
return e
# forward declarations
proc eval(cmd: string): string
proc execute(cmd: string): int
proc substitute(strm: Stream, delim = ";\p"): string
# ctrlc is the ctrl-c handler
proc ctrlc() {.noconv.} =
raise newException(InterruptCtrlC, "")
# 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
result.add('{')
of '}':
numbraces = numbraces - 1
if numbraces != 0: result.add('}')
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")
of '$': result = "$"
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 vari == "$": return "$"
for i in countdown((numframes-1), 0):
let frame = shellVariables[i]
if frame.hasKey(vari): return frame[vari]
if envVariables.hasKey(vari):
result = envVariables[vari]
else:
raise newShellVariableException("No such variable", vari)
# 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
# the mode is 0 for eval and 1 for execute
proc runBuiltin(builtin: string, args: openArray[string], mode = 0): (string, int) =
case builtin:
of "set":
if args.len < 2:
raise newShellCommandException("Not enough arguments provided", "set")
shellVariables[numframes-1][args[0]] = args[1..^1].join(" ")
return ("", 0)
of "setenv":
if args.len < 2:
raise newShellCommandException("Not enough arguments provided", "setenv")
try:
envVariables[args[0]] = args[1..^1].join(":")
putEnv(args[0], args[1..^1].join(":"))
except OSError as e:
raise newShellCommandException(e.msg, "setenv")
return ("", 0)
of "echo":
if args[0] == "-n":
return (args[1..^1].join(" "), 0)
return (args.join(" ") & "\p", 0)
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)
of "if":
var i=0
while true:
# if i is greater than args.high, we exit out. user probs doesn't want an else branch, we aren't going to stop him
if i > args.high:
break
# Check if this is the last item, if so just execute it
# It's an else branch basically
if i == args.high:
if mode == 0:
return (args[i].eval, 0)
elif mode == 1:
return ("", args[i].execute)
# Get the exitcode of the current arg (the condition)
let exitcode = args[i].execute
if exitcode == 0:
# if the condition is true
if mode == 0:
return (args[i+1].eval, 0)
elif mode == 1:
return ("", args[i+1].execute)
else: raise newException(Exception, "mode must be either 0 or 1")
else:
# if the condition is false
i.inc
i.inc
continue
of "cd":
try:
if args.len == 0:
shellVariables[0]["LASTPWD"] = getCurrentDir()
setCurrentDir(getHomeDir())
return ("", 0)
else:
if args[0] == "-":
let LASTPWD = getCurrentDir()
setCurrentDir(shellVariables[0]["LASTPWD"])
shellVariables[0]["LASTPWD"] = LASTPWD
return ("", 0)
shellVariables[0]["LASTPWD"] = getCurrentDir()
setCurrentDir(args[0])
return ("", 0)
except OsError as e:
raise newShellCommandException("No such directory", "cd")
of "let":
if args.len < 2:
raise newShellCommandException("Not enough arguments provided", "let")
shellVariables.add(newStringTable(modeCaseSensitive))
numframes.inc
let varvals = args[0].parseCommand
for varval in varvals:
let varval_parsed = varval.parseCommand
if varval_parsed.len < 2:
raise newShellCommandException("Missingvalue to go with key", "let")
let vari = varval_parsed[0]
let val = varval_parsed[1]
shellVariables[numframes-1][vari]=val
if mode == 0:
result[0] = args[1].eval
result[1] = 0
elif mode == 1:
result[0]=""
result[1]=args[1].execute
discard shellVariables.pop
numframes.dec
else:
raise newShellCommandException("No such builtin implemented", builtin)
# 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 == @[] or parsed == @[""]:
continue
let progname = parsed[0]
let args = parsed[1..parsed.high]
if progname in builtins:
let res=runBuiltin(progname, args, 1)
stdout.write(res[0])
result=res[1]
continue
try:
let ps = startProcess(progname, args=args, options={poUsePath, poParentStreams})
result = ps.waitForExit
ps.close
except OSError as e:
raise newShellCommandException("No such command", progname)
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 == @[] or parsed == @[""]:
continue
let progname=parsed[0]
let args = parsed[1..parsed.high]
if progname in builtins:
result = runBuiltin(progname, args, 0)[0]
continue
try:
let ps = startProcess(progname, args=args, options={poUsePath})
result=ps.outputStream.readAll
ps.close
except OSError as e:
raise newShellCommandException("No such command", progname)
strm.close
when isMainModule:
# ctrlc handler
setControlCHook(ctrlc)
let stdinstrm = stdin.newFileStream
# Set the default prompt
shellVariables[0]["PROMPT"] = "$PWD \\$"
stdout.write(shellVariables[0]["PROMPT"].newStringStream.substitute)
while not stdinstrm.atEnd():
try:
discard stdinstrm.readCommand.execute
stdout.write(shellVariables[0]["PROMPT"].newStringStream.substitute)
except ShellError, ShellCommandError, ShellVariableError:
let e = getCurrentException()
stderr.write(e.msg, "\n")
except InterruptCtrlC:
continue