# 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