Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Add History to Custom Shell

Tags:

java

shell

I am creating a custom shell in Java. I have added history to it so that when up arrow is pressed it goes to the previous command, but the up arrow seems to not be working

Here is my code:

public class MyShell {

    public static class JavaStringHistory
    {
        private List<String> history = new ArrayList<String>();
    }

    public static void main(String[] args) throws java.io.IOException {
        JavaStringHistory javaStringHistory = new JavaStringHistory();
        javaStringHistory.history.add("");

        Integer indexOfHistory = 0;

        String commandLine;
        BufferedReader console = new BufferedReader
                (new InputStreamReader(System.in));


        //Break with Ctrl+C
        while (true) {
            //read the command
            System.out.print("Shell>");
            commandLine = console.readLine();
            javaStringHistory.history.add(commandLine);

            //if just a return, loop
            if (commandLine.equals(""))
                continue;
            //history

            if (commandLine.equals(KeyEvent.VK_UP))
            {
                System.out.println("up arrow");
            }
            //help command
            if (commandLine.equals("help"))
            {
                System.out.println();
                System.out.println();
                System.out.println("Welcome to the shell");
                System.out.println("Written by: Alex Frieden");
                System.out.println("--------------------");
                System.out.println();
                System.out.println("Commands to use:");
                System.out.println("1) cat");
                System.out.println("2) exit");
                System.out.println("3) clear");
                System.out.println();
                System.out.println();
                System.out.println("---------------------");
                System.out.println();
            }

            if (commandLine.equals("clear"))
            {

                for(int cls = 0; cls < 10; cls++ )
                {
                    System.out.println();
                }


            }

            if(commandLine.startsWith("cat"))
            {
                System.out.println("test");
                //ProcessBuilder pb = new ProcessBuilder();
                //pb = new ProcessBuilder(commandLine);
            }

            else
            {
                System.out.println("Incorrect Command");
            }


            if (commandLine.equals("exit"))
            {

                System.out.println("...Terminating the Virtual Machine");
                System.out.println("...Done");
                System.out.println("Please Close manually with Options > Close");
                System.exit(0);
            }

            indexOfHistory++;

        }
    }
}

All I am getting is

Shell>^[[A
Incorrect Command
Shell>

Any thoughts?

like image 673
Badmiral Avatar asked Mar 24 '23 08:03

Badmiral


2 Answers

There are several problems with your approach:

  • User blackSmith has mentioned before me that system console handling is platform-dependent when it comes to cursor key handling and similar topics.
  • BufferedReader.readLine is not a smart choice to use for history cycling in a shell because you want the shell to immediately react to cursor keys and not force the user to press Return or Enter. Reading whole lines is only required for user commands. Thus, you need to scan the keyboard input for each single character or key code and decide by yourself if it is e.g. a cursor key (up/down for history cycling, left/right for cursor movement within the command line) or delete/backspace for command line editing and so forth.
  • The text strings which are created by reading control characters via readLine can depend on the OS, maybe even on the shell and the character set (UTF-8, ISO-8859-1, US ASCII etc.) on the console.
  • Built-in shell editing functions like command history might get in the way with readLine, e.g. on Linux I see the "^[[A" stuff for cursor up, on Windows the cursor keys are passed through to the built-in command history feature of cmd.exe. I.e. you need to put the console in raw mode (line editing bypassed and no Enter key required) as opposed to cooked mode (line editing with Enter key required).

Anyway, so as to answer your initial question about how to find out which key codes are produced by BufferedReader.readLine, it is actually quite simple. Just dump the bytes to the console like so:

commandLine = console.readLine();
System.out.println("Entered command text:  " + commandLine);
System.out.print  ("Entered command bytes: ");
for (byte b : commandLine.getBytes())
    System.out.print(b + ", ");
System.out.println();

Under Linux cursor up might be something like "27, 91, 65" or just "91, 65", depending on the terminal. cursor down ends with "66" instead on my system. So you could do something like:

public class MyShell {
    private static final String UP_ARROW_1 = new String(new byte[] {91, 65});
    private static final String UP_ARROW_2 = new String(new byte[] {27, 91, 65});
    private static final String DN_ARROW_1 = new String(new byte[] {91, 66});
    private static final String DN_ARROW_2 = new String(new byte[] {27, 91, 66});

    // (...)

    public static void main(String[] args) throws IOException {
        // (...)
            // history
            else if (commandLine.startsWith(UP_ARROW_1) || commandLine.startsWith(UP_ARROW_2)) {
                System.out.println("up arrow");
            }
            else if (commandLine.startsWith(DN_ARROW_1) || commandLine.startsWith(DN_ARROW_2)) {
                System.out.println("down arrow");
            }
        // (...)
    }
}

But all this is just for explanation or demonstration and so as to answer your question - I do like to get the bounty. ;-)

Maybe a way to go is not to re-invent the wheel and use the work of others, e.g. a framework like JLine. It is not perfect either from what I have heard, but goes way further than anything you can develop by yourself in a short time. Someone has written a short introductory blog post about JLine. The library seems to do just what you need. Enjoy!


Update: I gave JLine 2.11 a little try with this code sample (basically the one from the blog post plus tab filename completion:

import java.io.IOException;

import jline.TerminalFactory;
import jline.console.ConsoleReader;
import jline.console.completer.FileNameCompleter;

public class MyJLineShell {
    public static void main(String[] args) {
        try {
            ConsoleReader console = new ConsoleReader();
            console.addCompleter(new FileNameCompleter());
            console.setPrompt("prompt> ");
            String line = null;
            while ((line = console.readLine()) != null) {
                console.println(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                TerminalFactory.get().restore();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

It works nicely on Windows and Linux, but for me tab completion only works on Linux, not on Windows. Anyway, command history works well on both platforms.

like image 50
kriegaex Avatar answered Apr 01 '23 04:04

kriegaex


VK_UP is an integer constant, while in.readLine() is a string. They won't equal each other. Why don't you try to test for the code that appears in console usually when you click up arrow? So like:

if (in.readLine().equals("^[[A"))

and then you could clear the line, and write the command in the arraylist with the highest index.

Also, I tested this and found a bug. Change your if statements besides the first to else if; after any command it will eventually get to the else and display "Incorrect Command"

like image 21
Dynomyte Avatar answered Apr 01 '23 02:04

Dynomyte