diff --git a/README.md b/README.md new file mode 100644 index 0000000..d02f125 --- /dev/null +++ b/README.md @@ -0,0 +1,28 @@ +# au_software_design + +##1. Shell + +###Command line utility similar to Unix shell. + +####Supported features: + +* echo command +* cat command +* wc command +* exit command +* pwd command +* environment variables +* unknown commands are passed to system shell as separate process through Java.Process library. + +####Class diagram: +![shell class diagram](https://www.gliffy.com/go/share/image/smx5dub0j39jxied850w.png?utm_medium=live-embed&utm_source=custom) + +####Data flow: + * Main: run Shell object. + * Shell: Read line from System.in. +   + * Preprocessor: Substitute environment variables in input string. E. g. "Hello, $name" -> "Hello, Alex" + * Tokeniser: Split string into a list of tokens: words and operators. + * Parser: Parse list of tokens as sequence of commands divided by pipes. + * Command Executor: Perform chained computation passing output of one comand as input to the next one. + * Pass result to System.in and loop again. diff --git a/shell/.gradle/2.10/taskArtifacts/cache.properties b/shell/.gradle/2.10/taskArtifacts/cache.properties deleted file mode 100644 index 34e0db3..0000000 --- a/shell/.gradle/2.10/taskArtifacts/cache.properties +++ /dev/null @@ -1 +0,0 @@ -#Sat Mar 04 19:36:05 MSK 2017 diff --git a/shell/.gradle/2.10/taskArtifacts/cache.properties.lock b/shell/.gradle/2.10/taskArtifacts/cache.properties.lock deleted file mode 100644 index 7a2565a..0000000 Binary files a/shell/.gradle/2.10/taskArtifacts/cache.properties.lock and /dev/null differ diff --git a/shell/.gradle/2.10/taskArtifacts/fileHashes.bin b/shell/.gradle/2.10/taskArtifacts/fileHashes.bin deleted file mode 100644 index 4374ba0..0000000 Binary files a/shell/.gradle/2.10/taskArtifacts/fileHashes.bin and /dev/null differ diff --git a/shell/.gradle/2.10/taskArtifacts/fileSnapshots.bin b/shell/.gradle/2.10/taskArtifacts/fileSnapshots.bin deleted file mode 100644 index b6a62f2..0000000 Binary files a/shell/.gradle/2.10/taskArtifacts/fileSnapshots.bin and /dev/null differ diff --git a/shell/.gradle/2.10/taskArtifacts/outputFileStates.bin b/shell/.gradle/2.10/taskArtifacts/outputFileStates.bin deleted file mode 100644 index 9cea602..0000000 Binary files a/shell/.gradle/2.10/taskArtifacts/outputFileStates.bin and /dev/null differ diff --git a/shell/.gradle/2.10/taskArtifacts/taskArtifacts.bin b/shell/.gradle/2.10/taskArtifacts/taskArtifacts.bin deleted file mode 100644 index 04bc858..0000000 Binary files a/shell/.gradle/2.10/taskArtifacts/taskArtifacts.bin and /dev/null differ diff --git a/shell/.gradle/3.1/taskArtifacts/cache.properties b/shell/.gradle/3.1/taskArtifacts/cache.properties deleted file mode 100644 index 2f07c7a..0000000 --- a/shell/.gradle/3.1/taskArtifacts/cache.properties +++ /dev/null @@ -1 +0,0 @@ -#Tue Feb 28 22:43:07 MSK 2017 diff --git a/shell/.gradle/3.1/taskArtifacts/cache.properties.lock b/shell/.gradle/3.1/taskArtifacts/cache.properties.lock deleted file mode 100644 index fb71597..0000000 Binary files a/shell/.gradle/3.1/taskArtifacts/cache.properties.lock and /dev/null differ diff --git a/shell/.gradle/3.1/taskArtifacts/fileHashes.bin b/shell/.gradle/3.1/taskArtifacts/fileHashes.bin deleted file mode 100644 index 6d05f69..0000000 Binary files a/shell/.gradle/3.1/taskArtifacts/fileHashes.bin and /dev/null differ diff --git a/shell/.gradle/3.1/taskArtifacts/fileSnapshots.bin b/shell/.gradle/3.1/taskArtifacts/fileSnapshots.bin deleted file mode 100644 index 6963709..0000000 Binary files a/shell/.gradle/3.1/taskArtifacts/fileSnapshots.bin and /dev/null differ diff --git a/shell/.gradle/3.1/taskArtifacts/taskArtifacts.bin b/shell/.gradle/3.1/taskArtifacts/taskArtifacts.bin deleted file mode 100644 index 395bf7c..0000000 Binary files a/shell/.gradle/3.1/taskArtifacts/taskArtifacts.bin and /dev/null differ diff --git a/shell/build.gradle b/shell/build.gradle index afbf593..5247b51 100644 --- a/shell/build.gradle +++ b/shell/build.gradle @@ -2,6 +2,8 @@ group 'simiyutin' version '1.0-SNAPSHOT' apply plugin: 'java' +apply plugin: 'application' +mainClassName = "com.simiyutin.au.shell.core.Main" sourceCompatibility = 1.8 @@ -9,6 +11,10 @@ repositories { mavenCentral() } +run{ + standardInput = System.in +} + dependencies { testCompile group: 'junit', name: 'junit', version: '4.11' } diff --git a/shell/src/main/java/com/simiyutin/au/shell/commands/Cat.java b/shell/src/main/java/com/simiyutin/au/shell/commands/Cat.java new file mode 100644 index 0000000..6198516 --- /dev/null +++ b/shell/src/main/java/com/simiyutin/au/shell/commands/Cat.java @@ -0,0 +1,54 @@ +package com.simiyutin.au.shell.commands; + +import com.simiyutin.au.shell.core.exceptions.CommandExecutionException; +import com.simiyutin.au.shell.core.Command; +import com.simiyutin.au.shell.core.Environment; +import com.simiyutin.au.shell.core.Stream; + +import java.io.IOException; +import java.nio.charset.MalformedInputException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.Scanner; + +/** + * Reads file from parameter to stdout or reflects stdin to stdout + */ +public class Cat extends Command { + + public static final String NAME = "cat"; + + public Cat(List args, Environment env) { + super(args, env); + } + + @Override + public Stream run(Stream stream) throws CommandExecutionException { + + if (stream.hasNext()) { + return stream; + } + + Stream res = new Stream(); + if (args.isEmpty()) { + Scanner sc = new Scanner(System.in); + while (true) { + System.out.println(sc.nextLine()); + } + } else { + String filename = args.get(0); + Path file = Paths.get(filename); + try { + List lines = Files.readAllLines(file); + lines.forEach(res::write); + } catch (MalformedInputException e) { + throw new CommandExecutionException("Cannot determine file encoding"); + } catch (IOException e) { + throw new CommandExecutionException("Error while reading file: " + e.toString()); + } + } + return res; + } +} diff --git a/shell/src/main/java/com/simiyutin/au/shell/commands/Echo.java b/shell/src/main/java/com/simiyutin/au/shell/commands/Echo.java new file mode 100644 index 0000000..80487d5 --- /dev/null +++ b/shell/src/main/java/com/simiyutin/au/shell/commands/Echo.java @@ -0,0 +1,33 @@ +package com.simiyutin.au.shell.commands; + +import com.simiyutin.au.shell.core.exceptions.CommandExecutionException; +import com.simiyutin.au.shell.core.Command; +import com.simiyutin.au.shell.core.Environment; +import com.simiyutin.au.shell.core.Stream; + +import java.util.List; +import java.util.stream.Collectors; + + +/** + * Prints arguments to output stream + */ +public class Echo extends Command { + + public static final String NAME = "echo"; + + public Echo(List args, Environment env) { + super(args, env); + } + + @Override + public Stream run(Stream ignored) throws CommandExecutionException { + + String toPrint = args.stream().collect(Collectors.joining()); + + Stream stream = new Stream(); + stream.write(toPrint); + return stream; + } + +} diff --git a/shell/src/main/java/com/simiyutin/au/shell/commands/Eq.java b/shell/src/main/java/com/simiyutin/au/shell/commands/Eq.java new file mode 100644 index 0000000..e088b7d --- /dev/null +++ b/shell/src/main/java/com/simiyutin/au/shell/commands/Eq.java @@ -0,0 +1,26 @@ +package com.simiyutin.au.shell.commands; + +import com.simiyutin.au.shell.core.exceptions.CommandExecutionException; +import com.simiyutin.au.shell.core.Command; +import com.simiyutin.au.shell.core.Environment; +import com.simiyutin.au.shell.core.Stream; + +import java.util.List; + +/** + * Puts new variable in context or changes variable value + */ +public class Eq extends Command { + + public Eq(List args, Environment env) { + super(args, env); + } + + @Override + public Stream run(Stream ignored) throws CommandExecutionException { + String var = args.get(0); + String val = args.get(1); + env.put(var, val); + return new Stream(); + } +} diff --git a/shell/src/main/java/com/simiyutin/au/shell/commands/Exit.java b/shell/src/main/java/com/simiyutin/au/shell/commands/Exit.java new file mode 100644 index 0000000..8a9d4e9 --- /dev/null +++ b/shell/src/main/java/com/simiyutin/au/shell/commands/Exit.java @@ -0,0 +1,26 @@ +package com.simiyutin.au.shell.commands; + +import com.simiyutin.au.shell.core.exceptions.CommandExecutionException; +import com.simiyutin.au.shell.core.Command; +import com.simiyutin.au.shell.core.Environment; +import com.simiyutin.au.shell.core.Stream; + +import java.util.List; + +/** + * Exits shell + */ +public class Exit extends Command { + + public static final String NAME = "exit"; + + public Exit(List args, Environment env) { + super(args, env); + } + + @Override + public Stream run(Stream stream) throws CommandExecutionException { + System.exit(0); + return null; + } +} diff --git a/shell/src/main/java/com/simiyutin/au/shell/commands/OutSource.java b/shell/src/main/java/com/simiyutin/au/shell/commands/OutSource.java new file mode 100644 index 0000000..2e369ef --- /dev/null +++ b/shell/src/main/java/com/simiyutin/au/shell/commands/OutSource.java @@ -0,0 +1,59 @@ +package com.simiyutin.au.shell.commands; + +import com.simiyutin.au.shell.core.exceptions.CommandExecutionException; +import com.simiyutin.au.shell.core.Command; +import com.simiyutin.au.shell.core.Environment; +import com.simiyutin.au.shell.core.Stream; + +import java.io.*; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Passes input arguments and stream to external shell and reads result from its stdout + */ +public class OutSource extends Command { + + private String commandName; + + public OutSource(String commandName, List args, Environment env) { + super(args, env); + this.commandName = commandName; + } + + @Override + public Stream run(Stream stream) throws CommandExecutionException { + String command = commandName + " " + args.stream().collect(Collectors.joining(" ")); + Stream output = new Stream(); + try { + Process process = Runtime.getRuntime().exec(command); + write(process, stream); + output = read(process); + } catch (IOException | InterruptedException e) { + throw new CommandExecutionException("System error: " + e.toString()); + } + return output; + } + + private void write(Process process, Stream stream) throws IOException { + Writer handle = new PrintWriter(process.getOutputStream()); + final int lineFeed = 10; + while (stream.hasNext()) { + handle.write(stream.read()); + handle.write(lineFeed); + } + handle.close(); + } + + private Stream read(Process process) throws IOException, InterruptedException { + Stream output = new Stream(); + BufferedReader reader = + new BufferedReader(new InputStreamReader(process.getInputStream())); + String line; + while ((line = reader.readLine()) != null) { + output.write(line); + } + process.waitFor(); + return output; + } +} diff --git a/shell/src/main/java/com/simiyutin/au/shell/commands/Pwd.java b/shell/src/main/java/com/simiyutin/au/shell/commands/Pwd.java new file mode 100644 index 0000000..d802de4 --- /dev/null +++ b/shell/src/main/java/com/simiyutin/au/shell/commands/Pwd.java @@ -0,0 +1,31 @@ +package com.simiyutin.au.shell.commands; + +import com.simiyutin.au.shell.core.exceptions.CommandExecutionException; +import com.simiyutin.au.shell.core.Command; +import com.simiyutin.au.shell.core.Environment; +import com.simiyutin.au.shell.core.Stream; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; + +/** + * Prints current directory + */ +public class Pwd extends Command { + + public static final String NAME = "pwd"; + + public Pwd(List args, Environment env) { + super(args, env); + } + + @Override + public Stream run(Stream ignored) throws CommandExecutionException { + Path currentRelativePath = Paths.get(""); + String currentDir = currentRelativePath.toAbsolutePath().toString(); + Stream stream = new Stream(); + stream.write(currentDir); + return stream; + } +} diff --git a/shell/src/main/java/com/simiyutin/au/shell/commands/Wc.java b/shell/src/main/java/com/simiyutin/au/shell/commands/Wc.java new file mode 100644 index 0000000..f2e9222 --- /dev/null +++ b/shell/src/main/java/com/simiyutin/au/shell/commands/Wc.java @@ -0,0 +1,88 @@ +package com.simiyutin.au.shell.commands; + +import com.simiyutin.au.shell.core.exceptions.CommandExecutionException; +import com.simiyutin.au.shell.core.Command; +import com.simiyutin.au.shell.core.Environment; +import com.simiyutin.au.shell.core.Stream; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Scanner; +import java.util.stream.Collectors; + +/** + * Counts lines, words and bytes in given file or input stream + */ +public class Wc extends Command { + + public static final String NAME = "wc"; + + public Wc(List args, Environment env) { + super(args, env); + } + + @Override + public Stream run(Stream stream) throws CommandExecutionException { + if (!args.isEmpty()) { + return handleFile(); + } else if (stream.hasNext()) { + return handleInputStream(stream); + } else { + handleStdIn(); + return new Stream(); + } + } + + private Stream handleFile() { + String fileName = args.get(0); + Path file = Paths.get(fileName); + try { + List lines = Files.readAllLines(file); + return parseLines(lines); + } catch (IOException e) { + e.printStackTrace(); + return new Stream(); + } + } + + private Stream handleInputStream(Stream stream) { + List lines = new ArrayList<>(); + while (stream.hasNext()) { + lines.add(stream.read()); + } + return parseLines(lines); + } + + private void handleStdIn() { + Scanner sc = new Scanner(System.in); + while (true) { + List lines = Collections.singletonList(sc.nextLine()); + Stream result = parseLines(lines); + System.out.println(result.read()); + } + } + + private Stream parseLines(List lines) { + int linesCount = lines.size(); + char[] concatenated = lines.stream().collect(Collectors.joining(" ")).toCharArray(); + long wordCount = 0; + for (char c : concatenated) { + if (c == ' ') { + wordCount++; + } + } + wordCount++; + + long byteCount = concatenated.length; + + String result = String.format(" %d %d %d", linesCount, wordCount, byteCount); + Stream stream = new Stream(); + stream.write(result); + return stream; + } +} diff --git a/shell/src/main/java/com/simiyutin/au/shell/core/Command.java b/shell/src/main/java/com/simiyutin/au/shell/core/Command.java new file mode 100644 index 0000000..76190ac --- /dev/null +++ b/shell/src/main/java/com/simiyutin/au/shell/core/Command.java @@ -0,0 +1,33 @@ +package com.simiyutin.au.shell.core; + +import com.simiyutin.au.shell.core.exceptions.CommandExecutionException; + +import java.util.List; + +/** + * Abstract class which impersonates Command entity. + * Command can be run with Stream as an argument and can modify environment of a shell. + */ +public abstract class Command { + + protected List args; + protected Environment env; + + + /** + * Public constructor + * @param args arguments of a command as a List + * @param env environment of current running shell + */ + public Command(List args, Environment env) { + this.args = args; + this.env = env; + } + + /** + * Used as interface to command object. Takes input stream and returns result as output stream. + * @param stream input data as a Stream object + * @return output data as a Stream object + */ + public abstract Stream run(Stream stream) throws CommandExecutionException; +} diff --git a/shell/src/main/java/com/simiyutin/au/shell/core/CommandExecutor.java b/shell/src/main/java/com/simiyutin/au/shell/core/CommandExecutor.java new file mode 100644 index 0000000..d6d7d73 --- /dev/null +++ b/shell/src/main/java/com/simiyutin/au/shell/core/CommandExecutor.java @@ -0,0 +1,33 @@ +package com.simiyutin.au.shell.core; + +import com.simiyutin.au.shell.core.exceptions.CommandExecutionException; + +import java.util.List; + + +/** + * Entity to implement chained computations. + * Every computation is processed one after another + * and result of current one is passed to the next one. + */ +public class CommandExecutor { + private CommandExecutor() {} + + /** + * Sequentially executes chained list of commands. + * @param commands List of Command objects + * @return Stream object which contain result of a chain of computations + */ + public static Stream run(List commands) { + Stream stream = new Stream(); + try { + for (Command command : commands) { + stream = command.run(stream); + } + } catch (CommandExecutionException e) { + System.out.println(e.toString()); + return new Stream(); + } + return stream; + } +} diff --git a/shell/src/main/java/com/simiyutin/au/shell/core/CommandFactory.java b/shell/src/main/java/com/simiyutin/au/shell/core/CommandFactory.java new file mode 100644 index 0000000..0f70efa --- /dev/null +++ b/shell/src/main/java/com/simiyutin/au/shell/core/CommandFactory.java @@ -0,0 +1,61 @@ +package com.simiyutin.au.shell.core; + +import com.simiyutin.au.shell.commands.*; + +import java.util.List; + + +/** + * Factory which is used to construct Command objects relying on command token + */ +public class CommandFactory { + + + /** + * Factory method to generate commands from tokens. + * @param commandToken token, which contains command name as a String + * @param args arguments of a command + * @param env Environment object + * @return generated runnable Command object + */ + public static Command produce(Token commandToken, List args, Environment env) { + Command command; + switch (commandToken.getType()) { + case EQ: + command = new Eq(args, env); + return command; + case WORD: + command = getCommand(commandToken.toString(), args, env); + return command; + default: + return null; + } + } + + private static Command getCommand(String commandName, List args, Environment env) { + + Command command = null; + switch (commandName) { + case Echo.NAME: + command = new Echo(args, env); + break; + case Cat.NAME: + command = new Cat(args, env); + break; + case Wc.NAME: + command = new Wc(args, env); + break; + case Pwd.NAME: + command = new Pwd(args, env); + break; + case Exit.NAME: + command = new Exit(args, env); + break; + default: + command = new OutSource(commandName, args, env); + break; + + } + return command; + } +} diff --git a/shell/src/main/java/com/simiyutin/au/shell/core/Environment.java b/shell/src/main/java/com/simiyutin/au/shell/core/Environment.java new file mode 100644 index 0000000..37dcc9c --- /dev/null +++ b/shell/src/main/java/com/simiyutin/au/shell/core/Environment.java @@ -0,0 +1,42 @@ +package com.simiyutin.au.shell.core; + +import java.util.HashMap; +import java.util.Map; + +/** + * Entity which represents a concept of environment which handles state of a running shell. + * It can be asked to give variable by its name and to accept new variable by name and value + */ +public class Environment { + private Map map; + + public Environment() { + this.map = new HashMap<>(); + } + + /** + * Get value of variable + * @param var name of requested variable + * @return value of a requested variable or empty string if such does not exist in environment + */ + public String get(String var) { + + String res = map.get(var); + + if (res == null) { + return ""; + } else { + return res; + } + } + + + /** + * Put variable value + * @param var name of inserted variable + * @param val value of inserted variable + */ + public void put(String var, String val) { + map.put(var, val); + } +} diff --git a/shell/src/main/java/com/simiyutin/au/shell/core/Main.java b/shell/src/main/java/com/simiyutin/au/shell/core/Main.java new file mode 100644 index 0000000..b22e91e --- /dev/null +++ b/shell/src/main/java/com/simiyutin/au/shell/core/Main.java @@ -0,0 +1,8 @@ +package com.simiyutin.au.shell.core; + +public class Main { + public static void main(String[] args) { + Shell shell = new Shell(); + shell.run(); + } +} diff --git a/shell/src/main/java/com/simiyutin/au/shell/core/Parser.java b/shell/src/main/java/com/simiyutin/au/shell/core/Parser.java new file mode 100644 index 0000000..1adc5ef --- /dev/null +++ b/shell/src/main/java/com/simiyutin/au/shell/core/Parser.java @@ -0,0 +1,129 @@ +package com.simiyutin.au.shell.core; + +import com.simiyutin.au.shell.core.exceptions.ParserException; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.List; +import java.util.Queue; + +/** + * A DFA which accepts a list of tokens and evaluates + * corresponding shell command or a sequence of them + */ +public class Parser { + + private Queue queue; + private Environment env; + private List commands = new ArrayList<>(); + + private Parser(List tokens, Environment env) { + this.queue = new ArrayDeque<>(tokens); + this.env = env; + } + + + /** + * Parses list of tokens into list of commands + * @param tokens List of tokens to parse as a chain of shell commands + * @param env Environment object + * @return List of generated commands + */ + public static List run(List tokens, Environment env) { + try { + Parser parser = new Parser(tokens, env); + parser.start(); + return parser.commands; + } catch (ParserException e) { + System.out.println(e.toString()); + return new ArrayList<>(); + } + } + + private void start() throws ParserException { + Token token = queue.poll(); + if (token.getType() == Token.Type.WORD) { + parseCommand(token); + } else { + error("Expression must start with a command"); + } + + } + + private void parseCommand(Token firstWord) throws ParserException { + Token token = queue.poll(); + switch (token.getType()) { + case EQ: + parseEQ(firstWord); + break; + case WORD: + List args = new ArrayList<>(); + args.add(token.toString()); + parseArg(firstWord, args); + break; + case PIPE: + createCommand(firstWord, new ArrayList<>()); + start(); + break; + case EOF: + createCommand(firstWord, new ArrayList<>()); + break; + default: + error("Unexpected token"); + break; + } + } + + private void createCommand(Token commandToken, List args) throws ParserException { + Command command = CommandFactory.produce(commandToken, args, env); + commands.add(command); + } + + private void parseEQ(Token var) throws ParserException { + Token val = queue.poll(); + switch (val.getType()) { + case WORD: + checkEQSyntax(); + List args = new ArrayList<>(); + args.add(var.toString()); + args.add(val.toString()); + createCommand(Token.eq(), args); + break; + default: + error("Violated syntax of assignment operator"); + break; + } + } + + private void checkEQSyntax() throws ParserException { + Token token = queue.poll(); + if (token.getType() != Token.Type.EOF) { + error("Violated syntax of assignment operator"); + } + } + + private void parseArg(Token command, List args) throws ParserException { + Token token = queue.poll(); + switch (token.getType()) { + case WORD: + args.add(token.toString()); + parseArg(command, args); + break; + case PIPE: + createCommand(command, args); + start(); + break; + case EOF: + createCommand(command, args); + break; + default: + error("Violated syntax of argument list"); + break; + } + } + + private void error(String what) throws ParserException { + throw new ParserException(what); + } + +} diff --git a/shell/src/main/java/com/simiyutin/au/shell/core/Preprocessor.java b/shell/src/main/java/com/simiyutin/au/shell/core/Preprocessor.java new file mode 100644 index 0000000..154e4d1 --- /dev/null +++ b/shell/src/main/java/com/simiyutin/au/shell/core/Preprocessor.java @@ -0,0 +1,65 @@ +package com.simiyutin.au.shell.core; + +/** + * Takes care of substitution variable values in requested places + */ +public class Preprocessor { + + /** + * Substitutes variables in unprocessed string + * @param unprocessed unprocessed input String + * @param env Environment object + * @return String with substituted variables from environment + */ + public static String run(String unprocessed, Environment env) { + + char[] chars = unprocessed.toCharArray(); + + StringBuilder varName = new StringBuilder(); + StringBuilder processed = new StringBuilder(); + boolean varReading = false; + boolean isInsideDoubleQuotes = false; + boolean isInsideSingleQuotes = false; + + for (int i = 0; i < chars.length; i++) { + + if (!isInsideSingleQuotes && chars[i] == '$') { + varReading = true; + continue; + } + + if (varReading) { + varName.append(chars[i]); + if (isEndOfVarName(chars, i)) { + processed.append(env.get(varName.toString())); + varName.delete(0, chars.length); + varReading = false; + } + continue; + } + + if (chars[i] == '"' && !isInsideSingleQuotes) { + isInsideDoubleQuotes = !isInsideDoubleQuotes; + } + + if (chars[i] == '\'' && !isInsideDoubleQuotes) { + isInsideSingleQuotes = !isInsideSingleQuotes; + } + + processed.append(chars[i]); + } + + return processed.toString(); + } + + private static boolean isEndOfVarName(char[] chars, int index) { + + if (index == chars.length - 1) { + return true; + } + + char c = chars[index + 1]; + return c == ' ' || c == '"' || c == '\'' || c == '$'; + } + +} diff --git a/shell/src/main/java/com/simiyutin/au/shell/core/Shell.java b/shell/src/main/java/com/simiyutin/au/shell/core/Shell.java new file mode 100644 index 0000000..dab43d4 --- /dev/null +++ b/shell/src/main/java/com/simiyutin/au/shell/core/Shell.java @@ -0,0 +1,38 @@ +package com.simiyutin.au.shell.core; + +import java.util.List; +import java.util.Scanner; + + +/** + * Main entity. Reads from stdin in an infinite loop and executes commands + */ +public class Shell { + + private Environment env; + + /** + * constructs Shell with empty environment + */ + public Shell() { + env = new Environment(); + } + + /** + * runs Shell + */ + public void run() { + Scanner sc = new Scanner(System.in); + + while (true) { + String input = sc.nextLine(); + input = Preprocessor.run(input, env); + List tokens = Tokenizer.run(input); + List commands = Parser.run(tokens, env); + Stream stream = CommandExecutor.run(commands); + while (stream.hasNext()) { + System.out.println(stream.read()); + } + } + } +} diff --git a/shell/src/main/java/com/simiyutin/au/shell/core/Stream.java b/shell/src/main/java/com/simiyutin/au/shell/core/Stream.java new file mode 100644 index 0000000..2873004 --- /dev/null +++ b/shell/src/main/java/com/simiyutin/au/shell/core/Stream.java @@ -0,0 +1,59 @@ +package com.simiyutin.au.shell.core; + +import java.util.ArrayDeque; +import java.util.Queue; + +/** + * Entity to handle input and output of commands. + */ +public class Stream { + + private Queue vals; + + public Stream() { + this.vals = new ArrayDeque<>(); + } + + /** + * Read one line from stream + * @return next line in stream or null if stream is empty + */ + public String read() { + return vals.poll(); + } + + /** + * Write one line to stream + * @param val line to add to stream + */ + public void write(String val) { + vals.offer(val); + } + + /** + * Check if stream has more lines to read + * @return true if stream has at least one line to read + */ + public boolean hasNext() { + return vals.peek() != null; + } + + /** + * String representation of a stream object + * @return concatenated lines in stream + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + Queue replacement = new ArrayDeque<>(); + while (hasNext()) { + String line = read(); + sb.append('\n'); + sb.append(line); + replacement.add(line); + } + vals = replacement; + sb.delete(0, 1); + return sb.toString(); + } +} diff --git a/shell/src/main/java/com/simiyutin/au/shell/core/Token.java b/shell/src/main/java/com/simiyutin/au/shell/core/Token.java new file mode 100644 index 0000000..70defcd --- /dev/null +++ b/shell/src/main/java/com/simiyutin/au/shell/core/Token.java @@ -0,0 +1,162 @@ +package com.simiyutin.au.shell.core; + +import java.util.ArrayList; + +/** + * Describes possible lexemes + */ +public class Token { + + /** + * describes possible token types + */ + public enum Type { + SINGLE_QUOTE, + DOUBLE_QUOTE, + WHITESPACE, + PIPE, + WORD, + EQ, + EOF + } + + private String value; + private Type type; + + private static final String SINGLE_QUOTE = "'"; + private static final String DOUBLE_QUOTE = "\""; + private static final String EQUALITY_OPERATOR = "="; + private static final String WHITESPACE = " "; + private static final String PIPE = "|"; + private static final String EOF = "\0"; + + private Token(Token.Type type, String value) { + this.type = type; + this.value = value; + } + + /** + * Static factory method + * @return new single quote token + */ + public static Token singleQuote() { + return new Token(Type.SINGLE_QUOTE, SINGLE_QUOTE); + } + + /** + * Static factory method + * @return new double quote token + */ + public static Token doubleQuote() { + return new Token(Type.DOUBLE_QUOTE, DOUBLE_QUOTE); + } + + /** + * Static factory method + * @return new '=' token + */ + public static Token eq() { + return new Token(Type.EQ, EQUALITY_OPERATOR); + } + + /** + * Static factory method + * @return new whitespace token + */ + public static Token whitespace() { + return new Token(Type.WHITESPACE, WHITESPACE); + } + + /** + * Static factory method + * @return new pipe token + */ + public static Token pipe() { + return new Token(Type.PIPE, PIPE); + } + + /** + * Static factory method + * @param word string to encapsulate in newly created token + * @return new word token + */ + public static Token word(String word) { + return new Token(Type.WORD, word); + } + + /** + * Static factory method + * @return new end of line token + */ + public static Token eof() { + return new Token(Type.EOF, EOF); + } + + + /** + * Tests if given char corresponds to any delimiter token + * @param c character to test + */ + public static boolean isDelimiter(char c) { + ArrayList test = new ArrayList<>(); + test.add(SINGLE_QUOTE); + test.add(DOUBLE_QUOTE); + test.add(WHITESPACE); + test.add(EQUALITY_OPERATOR); + test.add(PIPE); + return test.contains(String.valueOf(c)); + } + + /** + * Static factory method + * @param c character to get corresponding token + * @return generate corresponding token to passed char + */ + public static Token valueOf(char c) { + switch (String.valueOf(c)) { + case SINGLE_QUOTE: + return singleQuote(); + case DOUBLE_QUOTE: + return doubleQuote(); + case WHITESPACE: + return whitespace(); + case EQUALITY_OPERATOR: + return eq(); + case PIPE: + return pipe(); + default: + return word(String.valueOf(c)); + } + } + + public Type getType() { + return type; + } + + public String getValue() { + return value; + } + + @Override + public String toString() { + return value; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Token token = (Token) o; + + if (value != null ? !value.equals(token.value) : token.value != null) return false; + return type == token.type; + } + + @Override + public int hashCode() { + int result = value != null ? value.hashCode() : 0; + result = 31 * result + (type != null ? type.hashCode() : 0); + return result; + } +} diff --git a/shell/src/main/java/com/simiyutin/au/shell/core/Tokenizer.java b/shell/src/main/java/com/simiyutin/au/shell/core/Tokenizer.java new file mode 100644 index 0000000..d902e0f --- /dev/null +++ b/shell/src/main/java/com/simiyutin/au/shell/core/Tokenizer.java @@ -0,0 +1,74 @@ +package com.simiyutin.au.shell.core; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; + +/** + * Takes care of translating given char string to list of lexemes. + */ +public class Tokenizer { + + /** + * Start tokenizer on preprocessed string + * @param input processed string corresponding to some shell command + * @return list of words and operators obtained from passed string + */ + public static List run(String input) { + + List tokens = new ArrayList<>(); + + char[] chars = input.toCharArray(); + for (int i = 0; i < chars.length;) { + char c = chars[i]; + if (!Token.isDelimiter(c)) { + String word = readWord(chars, i); + i += word.length(); + tokens.add(Token.word(word)); + continue; + } + Token.Type type = Token.valueOf(c).getType(); + switch (type) { + case SINGLE_QUOTE: + case DOUBLE_QUOTE: + i++; + String word = readWordInsideQuotes(chars, i, c); + i += word.length() + 1; + tokens.add(Token.word(word)); + break; + case EQ: + tokens.add(Token.eq()); + i++; + break; + case WHITESPACE: + i++; + break; + case PIPE: + tokens.add(Token.pipe()); + i++; + break; + } + } + + tokens.add(Token.eof()); + + return tokens; + } + + private static String readWordInsideQuotes(char[] chars, int i, char matcher) { + return readWordUntil(c -> c.equals(matcher), chars, i); + } + + private static String readWord(char[] chars, int i) { + return readWordUntil(Token::isDelimiter, chars, i); + } + + private static String readWordUntil(Predicate breaker, char[] chars, int i) { + StringBuilder sb = new StringBuilder(); + while (i < chars.length && !breaker.test(chars[i])) { + sb.append(chars[i++]); + } + return sb.toString(); + } + +} diff --git a/shell/src/main/java/com/simiyutin/au/shell/core/exceptions/CommandExecutionException.java b/shell/src/main/java/com/simiyutin/au/shell/core/exceptions/CommandExecutionException.java new file mode 100644 index 0000000..b34eda5 --- /dev/null +++ b/shell/src/main/java/com/simiyutin/au/shell/core/exceptions/CommandExecutionException.java @@ -0,0 +1,10 @@ +package com.simiyutin.au.shell.core.exceptions; + +/** + * This is used when errors while commands execution encountered + */ +public class CommandExecutionException extends ShellException { + public CommandExecutionException(String what) { + super(what); + } +} diff --git a/shell/src/main/java/com/simiyutin/au/shell/core/exceptions/ParserException.java b/shell/src/main/java/com/simiyutin/au/shell/core/exceptions/ParserException.java new file mode 100644 index 0000000..c7d68a1 --- /dev/null +++ b/shell/src/main/java/com/simiyutin/au/shell/core/exceptions/ParserException.java @@ -0,0 +1,11 @@ +package com.simiyutin.au.shell.core.exceptions; + + +/** + * This is thrown when command has incorrect syntax + */ +public class ParserException extends ShellException { + public ParserException(String what) { + super(what); + } +} diff --git a/shell/src/main/java/com/simiyutin/au/shell/core/exceptions/ShellException.java b/shell/src/main/java/com/simiyutin/au/shell/core/exceptions/ShellException.java new file mode 100644 index 0000000..77003e3 --- /dev/null +++ b/shell/src/main/java/com/simiyutin/au/shell/core/exceptions/ShellException.java @@ -0,0 +1,17 @@ +package com.simiyutin.au.shell.core.exceptions; + + +/** + * Base class for shell exceptions + */ +public class ShellException extends Exception { + private String what = ""; + public ShellException(String what) { + this.what = what; + } + + @Override + public String toString() { + return what; + } +} diff --git a/shell/src/test/java/com/simiyutin/au/shell/ParserTests.java b/shell/src/test/java/com/simiyutin/au/shell/ParserTests.java new file mode 100644 index 0000000..4429e8e --- /dev/null +++ b/shell/src/test/java/com/simiyutin/au/shell/ParserTests.java @@ -0,0 +1,92 @@ +package com.simiyutin.au.shell; + +import org.junit.Test; +import com.simiyutin.au.shell.core.*; + +import java.util.List; + +import static org.junit.Assert.assertEquals; + +public class ParserTests { + + @Test + public void smokeTest() throws Exception { + Environment env = getEnv(); + String input = "hello=hacked"; + List tokens = Tokenizer.run(input); + List commands = Parser.run(tokens, env); + commands.get(0).run(new Stream()); + assertEquals("hacked", Preprocessor.run("$hello", env)); + } + + @Test + public void reduceTest() throws Exception { + Environment env = getEnv(); + String input = "hello='hacked = | azazazazaa'"; + List tokens = Tokenizer.run(input); + List commands = Parser.run(tokens, env); + commands.get(0).run(new Stream()); + assertEquals("hacked = | azazazazaa", Preprocessor.run("$hello", env)); + } + + @Test + public void reduceWithSubstitutionTest() throws Exception { + Environment env = getEnv(); + String input = "hello=\"hacked $vladimir\""; + input = Preprocessor.run(input, env); + List tokens = Tokenizer.run(input); + List commands = Parser.run(tokens, env); + commands.get(0).run(new Stream()); + assertEquals("hacked putin", Preprocessor.run("$hello", env)); + } + + @Test + public void recursiveReduceWithSubstitutionTest() throws Exception { + Environment env = getEnv(); + String input = "hello=\"hacked $hello\""; + input = Preprocessor.run(input, env); + List tokens = Tokenizer.run(input); + List commands = Parser.run(tokens, env); + commands.get(0).run(new Stream()); + assertEquals("hacked world", Preprocessor.run("$hello", env)); + } + + @Test + public void echoTest() { + Environment env = getEnv(); + String input = "echo hello"; + List tokens = Tokenizer.run(input); + List commands = Parser.run(tokens, env); + Stream output = CommandExecutor.run(commands); + assertEquals("hello", output.toString()); + } + + @Test + public void echoToPipeToCatTest() { + Environment env = getEnv(); + String input = "echo hello | cat"; + List tokens = Tokenizer.run(input); + List commands = Parser.run(tokens, env); + Stream output = CommandExecutor.run(commands); + assertEquals("hello", output.toString()); + } + + @Test + public void echoToSedAsOutSourceTest() { + + Environment env = getEnv(); + String input = "echo \"hello_world\" | sed 's/hello/goodbye/'"; + List tokens = Tokenizer.run(input); + List commands = Parser.run(tokens, env); + Stream output = CommandExecutor.run(commands); + assertEquals("goodbye_world", output.toString()); + } + + private Environment getEnv() { + Environment env = new Environment(); + env.put("hello", "world"); + env.put("vladimir", "putin"); + + return env; + } +} diff --git a/shell/src/test/java/com/simiyutin/au/shell/PreprocessorTests.java b/shell/src/test/java/com/simiyutin/au/shell/PreprocessorTests.java new file mode 100644 index 0000000..38a3f52 --- /dev/null +++ b/shell/src/test/java/com/simiyutin/au/shell/PreprocessorTests.java @@ -0,0 +1,73 @@ +package com.simiyutin.au.shell; + +import org.junit.Test; +import com.simiyutin.au.shell.core.Environment; +import com.simiyutin.au.shell.core.Preprocessor; + +import static org.junit.Assert.assertEquals; + +public class PreprocessorTests { + + @Test + public void smokeTest() { + + Environment env = getEnv(); + + assertEquals("cat world | echo putin azazaza", + Preprocessor.run("cat $hello | echo $vladimir azazaza", env)); + } + + @Test + public void emptySubstitutionTest() { + + Environment env = getEnv(); + + assertEquals("", Preprocessor.run("$not_in_env", env)); + } + + @Test + public void lastSub() { + Environment env = getEnv(); + + assertEquals("world", Preprocessor.run("$hello", env)); + } + + @Test + public void varNameBreakers() { + Environment env = getEnv(); + + assertEquals("world'hidden' \"world\" worldputin putin", + Preprocessor.run("$hello'hidden' \"$hello\" $hello$vladimir $vladimir", env)); + } + + @Test + public void hidesInSingleQuotes() { + Environment env = getEnv(); + + assertEquals("'$hello' world", Preprocessor.run("'$hello' $hello", env)); + } + + @Test + public void handlesIncorrectInput() { + Environment env = getEnv(); + + assertEquals("'$hello", Preprocessor.run("'$hello", env)); + } + + @Test + public void nestedQuotes() { + Environment env = getEnv(); + + assertEquals("\"'hello'\" world", Preprocessor.run("\"'$test'\" world", env)); + } + + private Environment getEnv() { + Environment env = new Environment(); + env.put("hello", "world"); + env.put("vladimir", "putin"); + env.put("test", "hello"); + + return env; + } + +} diff --git a/shell/src/test/java/com/simiyutin/au/shell/TokenizerTests.java b/shell/src/test/java/com/simiyutin/au/shell/TokenizerTests.java new file mode 100644 index 0000000..f524f10 --- /dev/null +++ b/shell/src/test/java/com/simiyutin/au/shell/TokenizerTests.java @@ -0,0 +1,67 @@ +package com.simiyutin.au.shell; + +import org.junit.Test; +import com.simiyutin.au.shell.core.Token; +import com.simiyutin.au.shell.core.Tokenizer; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +public class TokenizerTests { + + @Test + public void smokeTest() { + List expected = new ArrayList<>(); + expected.add(Token.word("hello world")); + expected.add(Token.eof()); + + List actual = Tokenizer.run("'hello world'"); + + assertEquals(expected, actual); + } + + @Test + public void emptyTest() { + List expected = new ArrayList<>(); + expected.add(Token.eof()); + + List actual = Tokenizer.run(""); + + assertEquals(expected, actual); + } + + @Test + public void assignmentTest() { + List expected = new ArrayList<>(); + expected.add(Token.word("hello")); + expected.add(Token.eq()); + expected.add(Token.word("world")); + expected.add(Token.eof()); + + List actual = Tokenizer.run("hello=world"); + + assertEquals(expected, actual); + } + + @Test + public void mixedTest() { + List expected = new ArrayList<>(); + expected.add(Token.word("cat")); + expected.add(Token.word("input.txt")); + expected.add(Token.pipe()); + expected.add(Token.word("cat")); + expected.add(Token.pipe()); + expected.add(Token.word("grep")); + expected.add(Token.word("-v")); + expected.add(Token.word("azazaazz")); + expected.add(Token.word("long \"string\" constant")); + expected.add(Token.eof()); + + + List actual = Tokenizer.run("cat input.txt | cat | grep -v azazaazz 'long \"string\" constant' "); + + assertEquals(expected, actual); + } +}