diff --git a/src/main/java/antlr/Bash.g4 b/src/main/java/antlr/Bash.g4 new file mode 100644 index 0000000..8dcc851 --- /dev/null +++ b/src/main/java/antlr/Bash.g4 @@ -0,0 +1,28 @@ +grammar Bash; + +start : pipeline EOF #pipe + | assignment EOF #assign + ; + +pipeline : command #singleCommand + | left=pipeline (' '*) '|' (' '*) right=command #multipleCommands + ; + +command : (part(' '*))+ + ; + +part : WORD #partWord + | SQSTR #sqstr + | DQSTR #dqstr + | '$' WORD #partVariable + ; + +assignment : name=WORD '=' value=WORD #wordAssignment + | name=WORD '=' '$' value=WORD #variableAssignment + ; + + +SQSTR: '\'' .*? '\''; +DQSTR: '"' .*? '"'; + +WORD : [a-zA-Z0-9-]+; diff --git a/src/main/java/bntler/BashAssignment.java b/src/main/java/bntler/BashAssignment.java new file mode 100644 index 0000000..cd9f413 --- /dev/null +++ b/src/main/java/bntler/BashAssignment.java @@ -0,0 +1,3 @@ +package bntler; + +public record BashAssignment(String result) implements BashNode {} diff --git a/src/main/java/bntler/BashCommand.java b/src/main/java/bntler/BashCommand.java new file mode 100644 index 0000000..dec9ac6 --- /dev/null +++ b/src/main/java/bntler/BashCommand.java @@ -0,0 +1,6 @@ +package bntler; + +import java.util.List; + +public record BashCommand (List parts) implements BashNode { +} diff --git a/src/main/java/bntler/BashNode.java b/src/main/java/bntler/BashNode.java new file mode 100644 index 0000000..de939c6 --- /dev/null +++ b/src/main/java/bntler/BashNode.java @@ -0,0 +1,4 @@ +package bntler; + +public interface BashNode { +} diff --git a/src/main/java/bntler/BashPipeline.java b/src/main/java/bntler/BashPipeline.java new file mode 100644 index 0000000..866633b --- /dev/null +++ b/src/main/java/bntler/BashPipeline.java @@ -0,0 +1,7 @@ +package bntler; + +import java.util.List; + +public record BashPipeline(BashNode left, BashNode right) implements BashNode{ + +} diff --git a/src/main/java/bntler/PipelineVisitor.java b/src/main/java/bntler/PipelineVisitor.java new file mode 100644 index 0000000..78fdf72 --- /dev/null +++ b/src/main/java/bntler/PipelineVisitor.java @@ -0,0 +1,45 @@ +package bntler; + +import antlr.BashBaseVisitor; +import antlr.BashLexer; +import antlr.BashParser; +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; + +import java.util.Objects; + +public class PipelineVisitor extends BashBaseVisitor { + + WordOrStringVisitor wordOrStringVisitor = new WordOrStringVisitor(); + + @Override + public BashNode visitPipe(BashParser.PipeContext ctx) { + return visit(ctx.pipeline()); + } + + @Override + public BashNode visitMultipleCommands(BashParser.MultipleCommandsContext ctx) { + var left = visit(ctx.left); + var right = visit(ctx.right); + return new BashPipeline(left, right); + } + + @Override + public BashNode visitSingleCommand(BashParser.SingleCommandContext ctx) { + return visit(ctx.command()); + } + + @Override + public BashNode visitCommand(BashParser.CommandContext ctx) { + var command = ctx.children.stream() + .map(it -> wordOrStringVisitor.visit(it)) + .filter(Objects::nonNull) + .toList(); + return new BashCommand(command); + } + + @Override + public BashNode visitAssign(BashParser.AssignContext ctx) { + return new BashAssignment(wordOrStringVisitor.visit(ctx.assignment())); + } +} diff --git a/src/main/java/bntler/WordOrStringVisitor.java b/src/main/java/bntler/WordOrStringVisitor.java new file mode 100644 index 0000000..0def8d7 --- /dev/null +++ b/src/main/java/bntler/WordOrStringVisitor.java @@ -0,0 +1,59 @@ +package bntler; + +import antlr.BashBaseVisitor; +import antlr.BashParser; +import parser.Environment; + +import java.util.Map; + +public class WordOrStringVisitor extends BashBaseVisitor { + + @Override + public String visitAssign(BashParser.AssignContext ctx) { + visit(ctx.assignment()); + return ""; + } + + @Override + public String visitPartWord(BashParser.PartWordContext ctx) { + return ctx.WORD().toString(); + } + + @Override + public String visitPartVariable(BashParser.PartVariableContext ctx) { + return Environment.values.get(ctx.WORD().toString()); + } + + @Override + public String visitVariableAssignment(BashParser.VariableAssignmentContext ctx) { + String newVar = Environment.values.get(ctx.WORD(1).toString()); + Environment.values.put(ctx.WORD(0).toString(), newVar); + return ""; + } + + @Override + public String visitWordAssignment(BashParser.WordAssignmentContext ctx) { + // TODO: 02.06.2022 figure out how to make this smoother + Environment.values.put(ctx.WORD(0).toString(), ctx.WORD(1).toString()); + return ""; + } + + @Override + public String visitDqstr(BashParser.DqstrContext ctx) { + String dqstr = ctx.DQSTR().toString(); + String noQuotes = dqstr.substring(1, dqstr.length() - 1); + + for (Map.Entry entry : Environment.values.entrySet()) { + if (noQuotes.contains("$"+entry.getKey())) { + noQuotes = noQuotes.replace("$"+entry.getKey(), entry.getValue()); + } + } + return noQuotes; + } + + @Override + public String visitSqstr(BashParser.SqstrContext ctx) { + String sqstr = ctx.SQSTR().toString(); + return sqstr.substring(1, sqstr.length() - 1); + } +} diff --git a/src/main/java/builtins/Cat.java b/src/main/java/builtins/Cat.java index b1ee4d2..817d917 100644 --- a/src/main/java/builtins/Cat.java +++ b/src/main/java/builtins/Cat.java @@ -1,8 +1,7 @@ package builtins; +import bntler.BashCommand; import parser.Bash; -import parser.Expr; -import parser.Token; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -12,11 +11,11 @@ import java.util.List; public class Cat { - public static String execute1(Expr.Command command, String stdin) { - List args = command.arguments; - int i = 0; + public static String execute(BashCommand command, String stdin) throws IOException { + List args = command.parts(); + int i = 1; while (i < args.size()) { - if (!args.get(i).literal.startsWith("-")) { + if (!args.get(i).startsWith("-")) { break; } i++; @@ -28,12 +27,8 @@ public static String execute1(Expr.Command command, String stdin) { StringBuilder concatenated = new StringBuilder(); while (i < args.size()) { - try { - Path path = Paths.get(args.get(i).literal); - concatenated.append(Files.readString(path, StandardCharsets.UTF_8)); - } catch (IOException e) { - Bash.error("No such file " +args.get(i)); - } + Path path = Paths.get(args.get(i)); + concatenated.append(Files.readString(path, StandardCharsets.UTF_8)); i++; } return concatenated.toString(); diff --git a/src/main/java/builtins/Echo.java b/src/main/java/builtins/Echo.java index 3a73c38..f416495 100644 --- a/src/main/java/builtins/Echo.java +++ b/src/main/java/builtins/Echo.java @@ -1,34 +1,24 @@ package builtins; +import bntler.BashCommand; import org.jetbrains.annotations.Nullable; -import parser.*; public class Echo { - public static String execute1(Expr.Command command, @Nullable String stdin) { + public static String execute(BashCommand command, @Nullable String stdin) { StringBuilder echoWhat = new StringBuilder(); - int i = 0; + int i = 1; boolean flag = true; - if (!command.arguments.isEmpty() && command.arguments.get(0).literal.equals("-n")) { - i = 1; + if (!command.parts().isEmpty() && command.parts().get(i).equals("-n")) { + i++; flag = false; } - for (; i < command.arguments.size(); i++) { - Token tok = command.arguments.get(i); - if (tok.type == TokenType.SINGLE_S) { - echoWhat.append(tok.literal); - } else if (tok.type == TokenType.DOUBLE_S) { - echoWhat.append(Bash.runExternal(tok.literal)); - } else { - if (tok.literal.startsWith("$")) { - String key = tok.literal.substring(1); - echoWhat.append(Interpreter.environment.get(key)); - } else { - echoWhat.append(tok.literal); - } + for (; i < command.parts().size(); i++) { + echoWhat.append(command.parts().get(i)); + if (i != command.parts().size() - 1) { + echoWhat.append(" "); } - echoWhat.append(" "); } if (flag) { diff --git a/src/main/java/builtins/Pwd.java b/src/main/java/builtins/Pwd.java index a29e672..9f946dd 100644 --- a/src/main/java/builtins/Pwd.java +++ b/src/main/java/builtins/Pwd.java @@ -1,11 +1,11 @@ package builtins; -import parser.Expr; +import bntler.BashCommand; import java.nio.file.Paths; public class Pwd { - public static String execute1(Expr.Command command, String stdin) { - return Paths.get(".").toAbsolutePath().normalize().toString(); + public static String execute(BashCommand command, String stdin) { + return Paths.get(".").toAbsolutePath().normalize().toString() + "\n"; } } diff --git a/src/main/java/builtins/Wc.java b/src/main/java/builtins/Wc.java index b71d299..655be9d 100644 --- a/src/main/java/builtins/Wc.java +++ b/src/main/java/builtins/Wc.java @@ -1,7 +1,7 @@ package builtins; +import bntler.BashCommand; import parser.Bash; -import parser.Expr; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -11,24 +11,19 @@ import java.util.regex.Pattern; public class Wc { - public static String execute1(Expr.Command command, String stdin) { - if (!command.arguments.isEmpty()) { - try { - Path path = Paths.get(command.arguments.get(0).literal); - String content = Files.readString(path, StandardCharsets.US_ASCII); - return wcInner(content); - } catch (IOException e) { - Bash.error("no such file"); - return ""; - } + public static String execute(BashCommand command, String stdin) throws IOException { + if (command.parts().size() > 1) { + Path path = Paths.get(command.parts().get(1)); + String content = Files.readString(path, StandardCharsets.US_ASCII); + return wcInner(content, path.getFileName().toString()); } else if (stdin != null) { - return wcInner(stdin); + return wcInner(stdin, ""); } else { throw new IllegalStateException("Not Implemented"); } } - private static String wcInner(String input) { + private static String wcInner(String input, String path) { final Pattern nonWordPattern = Pattern.compile("\\W"); long charCount = input.lines().flatMapToInt(String::chars).count(); @@ -36,6 +31,10 @@ private static String wcInner(String input) { long wordCount = input.lines() .flatMap(nonWordPattern::splitAsStream) .filter(str -> !str.isEmpty()).count(); - return lineCount + " " + wordCount + " " + charCount + "\n"; + if (path.isEmpty()) { + return lineCount + " " + wordCount + " " + charCount + "\n"; + } else { + return lineCount + " " + wordCount + " " + charCount + " " + path + "\n"; + } } } diff --git a/src/main/java/parser/Bash.java b/src/main/java/parser/Bash.java index 048a26e..f74254b 100644 --- a/src/main/java/parser/Bash.java +++ b/src/main/java/parser/Bash.java @@ -1,5 +1,11 @@ package parser; +import antlr.BashLexer; +import antlr.BashParser; +import bntler.PipelineVisitor; +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; + import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; @@ -7,8 +13,6 @@ public class Bash { private static final Interpreter interpreter = new Interpreter(); - static boolean hadError = false; - static boolean hadRuntimeError = false; public static void main(String[] args) throws IOException { if (args.length > 1) { @@ -25,7 +29,6 @@ private static void runPrompt() throws IOException { for (;;) { System.out.print("> "); run(reader.readLine()); - hadError = false; } } @@ -35,36 +38,24 @@ private static void run(String source) { } public static String runExternal(String source) { - Scanner scanner = new Scanner(source); - List tokens = scanner.scanTokens(); - Parser parser = new Parser(tokens); - Expr statements = parser.parse(); - - // Stop if there was a syntax error. - if (hadError) return ""; - - return interpreter.interpret(statements); - } - - public static void error(String message) { - report("", message); - } - private static void report(String where, String message) { - System.err.println("Error" + where + ": " + message); - hadError = true; - } - - public static void error(Token token, String message) { - if (token.type == TokenType.EOF) { - report(" at end", message); - } else { - report(" at '" + token.rawText + "'", message); + if (source.isEmpty()) { + return "\n"; } - } - public static void runtimeError(RuntimeError error) { - System.err.println(error.getMessage()); - hadRuntimeError = true; + var input = CharStreams.fromString(source); + var lexer = new BashLexer(input); + var tokens = new CommonTokenStream(lexer); + var parser = new BashParser(tokens); + var rootNode = parser.start(); + var rootNodeGood = new PipelineVisitor().visit(rootNode); + String result; + try { + result = interpreter.interpret(rootNodeGood); + } catch (Exception e) { +// e.printStackTrace(); for debug + result = e + "\n"; + } + return result; } } diff --git a/src/main/java/parser/Environment.java b/src/main/java/parser/Environment.java index c03fd8c..1a7587e 100644 --- a/src/main/java/parser/Environment.java +++ b/src/main/java/parser/Environment.java @@ -4,35 +4,13 @@ import java.util.Map; public class Environment { - final Environment enclosing; - private final Map values = new HashMap<>(); - Environment() { - enclosing = null; - } - - Environment(Environment enclosing) { - this.enclosing = enclosing; - } + public static final Map values = new HashMap<>(); public String get(String name) { return values.getOrDefault(name, ""); } -// void assign(Token1 name, Object value) { -// if (values.containsKey(name.lexeme)) { -// values.put(name.lexeme, value); -// return; -// } -// -// if (enclosing != null) { -// enclosing.assign(name, value); -// return; -// } -// -// throw new RuntimeError(name, "Undefined variable '" + name.lexeme + "'."); -// } - void define(String name, String value) { values.put(name, value); } diff --git a/src/main/java/parser/Interpreter.java b/src/main/java/parser/Interpreter.java index 7a57067..2312e4e 100644 --- a/src/main/java/parser/Interpreter.java +++ b/src/main/java/parser/Interpreter.java @@ -1,107 +1,72 @@ package parser; - +import bntler.BashAssignment; +import bntler.BashCommand; +import bntler.BashNode; +import bntler.BashPipeline; import builtins.Cat; import builtins.Echo; import builtins.Pwd; import builtins.Wc; -public class Interpreter implements Expr.Visitor { - - private String result = ""; +import java.io.BufferedReader; +import java.io.InputStreamReader; - public static Environment environment = new Environment(); +public class Interpreter { - String interpret(Expr statements) { - try { - Object value = evaluate(statements); - return stringify(value); + private String result = ""; - } catch (RuntimeError error) { - Bash.runtimeError(error); + String interpret(BashNode node) throws Exception { + if (node != null) { + if (node instanceof BashPipeline) { + result = interpret(((BashPipeline) node).left()); + result = interpret(((BashPipeline) node).right()); + } else if (node instanceof BashCommand) { + return visitCommand((BashCommand) node); + } else if (node instanceof BashAssignment) { + return ((BashAssignment) node).result(); + } else { + throw new IllegalStateException("Not implemented"); + } } - return ""; - } - - private String evaluate(Expr statements) { - return statements.accept(this); - } - - - @Override - public String visitPipeExpr(Expr.Pipe expr) { - result = evaluate(expr.left); - return evaluate(expr.right); - } - - @Override - public String visitApplicationExpr(Expr.Application expr) { - evaluate(expr.left); - evaluate(expr.right); - return null; + return result; } - @Override - public String visitCommandExpr(Expr.Command expr) { - switch (expr.operator.type) { - case WC -> { - return Wc.execute1(expr, result); + public String visitCommand(BashCommand command) throws Exception { + switch (command.parts().get(0)) { + case "wc" -> { + return Wc.execute(command, result); } - case ECHO -> { - return Echo.execute1(expr, result); + case "echo" -> { + return Echo.execute(command, result); } - case PWD -> { - return Pwd.execute1(expr, result); + case "pwd" -> { + return Pwd.execute(command, result); } - case CAT -> { - return Cat.execute1(expr, result); + case "cat" -> { + return Cat.execute(command, result); } -// case SINGLE_S -> { -// -// } -// case DOUBLE_S -> { -// -// } - default -> { + default -> { + String strCommand = String.join(" ", command.parts()); + return executeCommand(strCommand); } } - return null; } - private String stringify(Object object) { - if (object == null) return "nil"; + private String executeCommand(String command) throws Exception { + Process process = Runtime.getRuntime().exec(command); + BufferedReader in = new BufferedReader(new InputStreamReader(process.getInputStream())); + StringBuilder b = new StringBuilder(); - // Hack. Work around Java adding ".0" to integer-valued doubles. - if (object instanceof Double) { - String text = object.toString(); - if (text.endsWith(".0")) { - text = text.substring(0, text.length() - 2); + while (true) { + String line = in.readLine(); + if (line == null) { + break; } - return text; + b.append(line); + b.append("\n"); } - - return object.toString(); - } - - @Override - public String visitAssignmentExpr(Expr.Assignment expr) { - environment.define(expr.left.rawText, expr.right.rawText); - return ""; - } - - @Override - public String visitLiteralExpr(Expr.Literal expr) { - // TODO: 20.04.2022 add variable - if (expr.value.startsWith("$")) { - return environment.get(expr.value.substring(1)); - } else { - return expr.value; - } - } - - @Override - public String visitUnaryExpr(Expr.Unary expr) { - return null; + return b.toString(); } } diff --git a/src/test/java/parser/BashTest.java b/src/test/java/parser/BashTest.java new file mode 100644 index 0000000..6df69cd --- /dev/null +++ b/src/test/java/parser/BashTest.java @@ -0,0 +1,95 @@ +package parser; + +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class BashTest { + @Test + public void testSimpleEcho() { + String expected = "hello\n"; + String actual = Bash.runExternal("echo hello"); + assertEquals(expected, actual); + + String expected1 = "hello"; + String actual1 = Bash.runExternal("echo -n hello"); + assertEquals(expected1, actual1); + } + + @Test + public void testSimpleWc() { + String expected = "1 1 10 wctest\n"; + String actual = Bash.runExternal("wc wctest"); + assertEquals(expected, actual); + } + + @Test + public void testSimplePwd() { + String expected = Path.of(new File("").getAbsolutePath()) + "\n"; + String actual = Bash.runExternal("pwd"); + assertEquals(expected, actual); + } + + @Test + public void testSimpleCat() { + String expected = "hello world"; + String actual = Bash.runExternal("cat cattest"); + assertEquals(expected, actual); + } + + @Test + public void randomCommand() { + String expected = "java.io.IOException: Cannot run program \"rrandom\": error=2, No such file or directory\n"; + String actual = Bash.runExternal("rrandom"); + assertEquals(expected, actual); + } + + @Test + public void testSimpleVariable() { + String expected = "java.io.IOException: Cannot run program \"rrandom\": error=2, No such file or directory\n"; + Bash.runExternal("a=rrandom"); + String actual = Bash.runExternal("$a"); + assertEquals(expected, actual); + } + + @Test + public void testVariableWithCommand() { + String expected = Path.of(new File("").getAbsolutePath()) + "\n"; + Bash.runExternal("a=pwd"); + String actual = Bash.runExternal("$a"); + assertEquals(expected, actual); + } + + @Test + public void testSimpleDString() { + String expected = Path.of(new File("").getAbsolutePath()) + "\n"; + String actual = Bash.runExternal("\"pwd\""); + assertEquals(expected, actual); + } + + @Test + public void testSimpleVariableWithDString() { + String expected = "java.io.IOException: Cannot run program \"rrandom\": error=2, No such file or directory\n"; + Bash.runExternal("a=rrandom"); + String actual = Bash.runExternal("\"$a\""); + assertEquals(expected, actual); + } + + @Test + public void testSimpleVariableWithSString() { + String expected = "java.io.IOException: Cannot run program \"$a\": error=2, No such file or directory\n"; + Bash.runExternal("a=rrandom"); + String actual = Bash.runExternal("'$a'"); + assertEquals(expected, actual); + } + + @Test + public void testSomePipe() { + String expected = "1 1 5\n"; + String actual = Bash.runExternal("echo hello | wc"); + assertEquals(expected, actual); + } +} \ No newline at end of file