# 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