6
\$\begingroup\$

I have this tiny library for implementing simple command line languages. It is not flexible enough for handling actual programming languages, but hopefully it may help implementing simpler REPL's faster/cleaner.

CommandParser.java:

package net.coderodde.commandparser;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;

/**
 * This class implements a command parser.
 * 
 * @author Rodion "rodde" Efremov
 * @version 1.6 (Sep 30, 2015)
 */
public class CommandParser {

    private final List<CommandDescriptor> commandDescriptorList = 
            new ArrayList<>();

    private boolean isSorted = true;

    public void add(CommandDescriptor commandDescriptor) {
        Objects.requireNonNull(commandDescriptor,
                               "The input command descriptor is null.");

        if (commandDescriptor.size() == 0) {
            return;
        }

        commandDescriptorList.add(commandDescriptor);
        isSorted = false;
    }

    public void process(String command) {
        if (!isSorted) {
            Collections.sort(commandDescriptorList, comparator);
            isSorted = true;
        }

        for (CommandDescriptor descriptor : commandDescriptorList) {
            if (descriptor.parse(command)) {
                return;
            }
        }
    }

    private static final class CommandDescriptorComparator
    implements Comparator<CommandDescriptor> {

        @Override
        public int compare(CommandDescriptor o1, CommandDescriptor o2) {
            CommandToken token1 = o1.getToken(0);
            CommandToken token2 = o2.getToken(0);

            if (token1.getTokenType() == CommandToken.TokenType.IDENTIFIER) {
                return 1;
            } else if (token2.getTokenType() 
                    == CommandToken.TokenType.IDENTIFIER) {
                return -1;
            } else {
                return 0;
            }
        }
    }

    private static final CommandDescriptorComparator comparator = 
            new CommandDescriptorComparator();
}

CommandDescriptor.java:

package net.coderodde.commandparser;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/**
 * This class implements a command descriptor.
 * 
 * @author Rodion "rodde" Descriptor
 * @version 1.6 (Sep 30, 2015)
 */
public class CommandDescriptor {

    private final List<CommandToken> commandTokenList = new ArrayList<>();
    private final CommandAction commandActionOnMatch;

    public CommandDescriptor(CommandAction commandActionOnMatch) {
        this.commandActionOnMatch = commandActionOnMatch;
    }

    public void addCommandToken(CommandToken token) {
        Objects.requireNonNull(token, "The input token is null.");
        commandTokenList.add(token);
    }

    public int size() {
        return commandTokenList.size();
    }

    CommandToken getToken(int index) {
        return commandTokenList.get(index);
    }

    public boolean parse(String command) {
        String[] parts = command.trim().split("\\s+");

        if (parts.length < commandTokenList.size()) {
            return false;
        }

        for (int i = 0; i < commandTokenList.size(); ++i) {
            CommandToken token = commandTokenList.get(i); 

            if (!token.matches(parts[i])) {
                return false;
            }
        }

        // We have a match. 
        if (commandActionOnMatch != null) {
            commandActionOnMatch.act(parts);
        }

        return true;
    }
}

CommandToken.java:

package net.coderodde.commandparser;

import java.util.Objects;

/**
 * This class implements a command token which may be a keyword, identifier or
 * value.
 * 
 * @author Rodion "rodde" Efremov
 * @version 1.6 (Sep 30, 2015)
 */
public class CommandToken {

    public enum TokenType {
        KEYWORD,
        IDENTIFIER,
        VALUE_INT,
        VALUE_LONG,
        VALUE_FLOAT, 
        VALUE_DOUBLE
    }

    private final TokenType tokenType;
    private final String datum;
    private final IdentifierValidator identifierValidator;

    public CommandToken(TokenType tokenType, 
                        String datum, 
                        IdentifierValidator identifierValidator) {
        Objects.requireNonNull(tokenType, "Input token type is null.");

        if (tokenType == TokenType.KEYWORD && datum == null) {
            throw new IllegalArgumentException("A keyword string is null for " +
                                               "a keyword token.");
        }

        if (tokenType == TokenType.IDENTIFIER && identifierValidator == null) {
            throw new IllegalArgumentException(
                    "A identifier validator is null for an identifier token.");
        }

        this.tokenType = tokenType;
        this.datum = datum;
        this.identifierValidator = identifierValidator;
    }

    TokenType getTokenType() {
        return tokenType;
    }

    boolean matches(String s) {
        Objects.requireNonNull(s, "The input word is null.");
        s = s.trim();

        switch (tokenType) {
            case KEYWORD: {
                return datum.equals(s);
            }

            case IDENTIFIER: {
                return identifierValidator.isValidIdentifier(s);
            }

            case VALUE_INT: {
                try {
                    Integer.parseInt(s);
                    return true;
                } catch (NumberFormatException ex) {
                    return false;
                }
            }

            case VALUE_LONG: {
                try {
                    Long.parseLong(s);
                    return true;
                } catch (NumberFormatException ex) {
                    return false;
                }
            }

            case VALUE_FLOAT: {
                try {
                    Float.parseFloat(s);
                    return true;
                } catch (NumberFormatException ex) {
                    return false;
                }
            }

            case VALUE_DOUBLE: {
                try {
                    Double.parseDouble(s);
                    return true;
                } catch (NumberFormatException ex) {
                    return false;
                }
            }

            default:
                throw new IllegalStateException("Should not get here ever.");
        }
    }
}

CommandAction.java:

package net.coderodde.commandparser;

/**
 * This class specifies a functional interface for a routine that handles a 
 * particular command.
 * 
 * @author Rodion "rodde" Efremov
 * @version 1.6 (Sep 30, 2015)
 */
@FunctionalInterface
public interface CommandAction {

    public void act(String[] tokens);
}

IdentifierValidator.java:

package net.coderodde.commandparser;

/**
 *
 * @author rodionefremov
 */
@FunctionalInterface
public interface IdentifierValidator {

    public boolean isValidIdentifier(String s);
}

Demo.java:

import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;
import net.coderodde.commandparser.CommandAction;
import net.coderodde.commandparser.CommandDescriptor;
import net.coderodde.commandparser.CommandParser;
import net.coderodde.commandparser.CommandToken;
import net.coderodde.commandparser.CommandToken.TokenType;
import net.coderodde.commandparser.IdentifierValidator;

public class Demo {

    private static final class MyNewAction implements CommandAction {

        private final Map<String, Double> variableMap;

        MyNewAction(Map<String, Double> variableMap) {
            this.variableMap = variableMap;
        }

        @Override
        public void act(String[] tokens) {
            String varName = tokens[1];
            double value = Double.parseDouble(tokens[2]);

            variableMap.put(varName, value);
        }        
    }

    private static final class MyDelAction implements CommandAction {

        private final Map<String, Double> variableMap;

        MyDelAction(Map<String, Double> variableMap) {
            this.variableMap = variableMap;
        }

        @Override
        public void act(String[] tokens) {
            String varName = tokens[1];
            variableMap.remove(varName);
        }        
    }

    private static final class MyPlusAction implements CommandAction {

        private final Map<String, Double> variableMap;

        MyPlusAction(Map<String, Double> variableMap) {
            this.variableMap = variableMap;
        }

        @Override
        public void act(String[] tokens) {
            String varName1 = tokens[0];
            String varName2 = tokens[2];

            if (!variableMap.containsKey(varName1)) {
                System.out.println(varName1 + ": no such variable.");
                return;
            }

            if (!variableMap.containsKey(varName2)) {
                System.out.println(varName2 + ": no such variable.");
                return;
            }

            System.out.println(variableMap.get(varName1) + 
                               variableMap.get(varName2));
        }        
    }

    private static final class MyShowAction implements CommandAction {

        private final Map<String, Double> variableMap;

        MyShowAction(Map<String, Double> variableMap) {
            this.variableMap = variableMap;
        }

        @Override
        public void act(String[] tokens) {
            String varName = tokens[0];

            if (!variableMap.containsKey(varName)) {
                System.out.println(varName + ": no such variable.");
                return;
            }

            System.out.println(variableMap.get(varName));
        }        
    }

    private static final IdentifierValidator myIdentifierValidator = 
    new IdentifierValidator() {

        @Override
        public boolean isValidIdentifier(String s) {
            if (s.isEmpty()) {
                return false;
            }

            char[] chars = s.toCharArray();

            if (!Character.isJavaIdentifierStart(chars[0])) {
                return false;
            }

            for (int i = 1; i < chars.length; ++i) {
                if (!Character.isJavaIdentifierPart(chars[i])) {
                    return false;
                }
            }

            return true;
        }

    };

    private static CommandParser buildCommandParser(Map<String, Double> map) {
        CommandParser parser = new CommandParser();

        MyNewAction newAction = new MyNewAction(map);
        MyDelAction delAction = new MyDelAction(map);
        MyPlusAction plusAction = new MyPlusAction(map);
        MyShowAction showAction = new MyShowAction(map);

        //// Start creating command descriptors.
        // 'new' command.
        CommandDescriptor descriptorNew = new CommandDescriptor(newAction);
        descriptorNew.addCommandToken(new CommandToken(TokenType.KEYWORD, 
                                                       "new", 
                                                       null));
        descriptorNew.addCommandToken(new CommandToken(TokenType.IDENTIFIER,
                                                       null,
                                                       myIdentifierValidator));
        descriptorNew.addCommandToken(new CommandToken(TokenType.VALUE_DOUBLE,
                                                       null,
                                                       null));

        // 'del' command.
        CommandDescriptor descriptorDel = new CommandDescriptor(delAction);
        descriptorDel.addCommandToken(new CommandToken(TokenType.KEYWORD,
                                                       "del",
                                                       null));
        descriptorDel.addCommandToken(new CommandToken(TokenType.IDENTIFIER,
                                                       null,
                                                       myIdentifierValidator));

        // '+' command. Adding two variable. If you want to add with constants
        // as well, just adde more descriptors with particular IDENTIFIER 
        // tokens.
        CommandDescriptor descriptorPlus = new CommandDescriptor(plusAction);
        descriptorPlus.addCommandToken(new CommandToken(TokenType.IDENTIFIER,
                                                        null,
                                                        myIdentifierValidator));

        descriptorPlus.addCommandToken(new CommandToken(TokenType.KEYWORD,
                                                        "+",
                                                        null));

        descriptorPlus.addCommandToken(new CommandToken(TokenType.IDENTIFIER,
                                                        null,
                                                        myIdentifierValidator));
        // 'show' command.
        CommandDescriptor descriptorShow = new CommandDescriptor(showAction);
        descriptorShow.addCommandToken(new CommandToken(TokenType.IDENTIFIER,
                                                        null,
                                                        myIdentifierValidator));
        parser.add(descriptorNew);
        parser.add(descriptorDel);
        parser.add(descriptorPlus);
        parser.add(descriptorShow);

        return parser;
    }

    public static void main(String[] args) {
        Map<String, Double> variableMap = new HashMap<>();
        CommandParser parser = buildCommandParser(variableMap);
        Scanner scanner = new Scanner(System.in);

        while (true) {
            System.out.print("> ");
            String command = scanner.nextLine().trim();

            if (command.equals("quit")) {
                break;
            }

            parser.process(command);
        }

        System.out.println("Bye!");
    }
}

A simple session might go this way:

> new A 29
> new B 26
> A
29.0
> B
26.0
> B + A
55.0
> del B
> B + A
B: no such variable.
> quit
Bye!
\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

I can't help but think you could easily use ANTLR to back your command line API. It could require a couple of days to grasp first, but then you have something fast, robust, and dev-friendly.

The Command Token/Descriptor/Parser approach could be a hell to maintain or evolve.

By the way, something is missing in Demo.java, one should read:

// 'show' command.
CommandDescriptor descriptorShow = new CommandDescriptor(showAction);
descriptorShow.addCommandToken(new CommandToken(TokenType.KEYWORD,
                                               "show",
                                               null));
descriptorShow.addCommandToken(new CommandToken(TokenType.IDENTIFIER,
                                                null,
                                                myIdentifierValidator));

The Demo doesn't work because of the missing TokenType.KEYWORD "show"

\$\endgroup\$
1

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.