2

I have a seemingly trivial problem: I want to start a terminal from a java process and give the terminal one or two commands. I have a simple example code, that works perfectly on windows with CMD. But I have not been able to achieve the same exact behavior on a Linux nor a Mac OS machine. I am aware, that the command needs to be changed, but unfortunately I have not been able to pass a string of arguments to a terminal on Mac.

Here the working code for windows:

import java.lang.ProcessBuilder.Redirect;

public class ExecTest {
    public static void main(String[] args){ 
        String cmd = "cmd /c start cmd.exe /K \"echo hello && echo bye\"";
        try {
            Process p = Runtime.getRuntime().exec(cmd);
            p.waitFor();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

On lubuntu I have been able to create a terminal with this command:

lxterminal -l -e 'echo hello && echo bye && read'

But this only works if called by a terminal but not with the java process.

.

TLDR: What is the Linux and Mac equivalent of this command:

cmd /c start cmd.exe /K \"echo hello && echo bye\"
Haeri
  • 621
  • 1
  • 12
  • 27
  • Do you really want a 'terminal'? Or do you just want to execute some shell commanfds? – bmargulies May 20 '17 at 01:43
  • I am able to open a terminal with command 'gnome-terminal' and same java code on my Ubuntu machine. Could you please try with this. – Vip May 20 '17 at 04:34
  • @bmargulies Yes. To give some context: I have made a Code Editor, that can compile some code. I have also created my own terminal that shows the result of the exec process. But there are some edge cases where I can't fully display the In and Outputstreams. So as a backup, I want to allow the user to execute the code in a native terminal. So yes I want to spawn a fresh native terminal and execute some code. – Haeri May 20 '17 at 18:05
  • @VipulGoyal I am not sure what you are suggesting.. I am aware that I can open a terminal via a java process, by just calling the name of the terminal, but the main problem is that I can't provide any commands to that terminal. – Haeri May 20 '17 at 18:09

2 Answers2

2

I suggest you use ProcessBuilder to benefit from easier output redirection and ability to consume it without using threads, and also pass the command as a String[] instead of flat String to be able to support the various wrapping approaches. If you prefer to stick with Runtime.exec(), it also supports String[], but the example below uses ProcessBuilder.

static int executeInTerminal(String command) throws IOException, InterruptedException {
    final String[] wrappedCommand;
    if (isWindows) {
        wrappedCommand = new String[]{ "cmd", "/c", "start", "/wait", "cmd.exe", "/K", command };
    }
    else if (isLinux) {
        wrappedCommand = new String[]{ "xterm", "-e", "bash", "-c", command};
    }
    else if (isMac) {
        wrappedCommand = new String[]{"osascript",
                "-e", "tell application \"Terminal\" to activate",
                "-e", "tell application \"Terminal\" to do script \"" + command + ";exit\""};
    }
    else {
        throw new RuntimeException("Unsupported OS ☹");
    }
    Process process = new ProcessBuilder(wrappedCommand)
            .redirectErrorStream(true)
            .start();
    try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
        String line;
        while ((line = reader.readLine()) != null) {
            System.out.println(line); // Your superior logging approach here
        }
    }
    return process.waitFor();
}

Tested on 3 operating systems. The 3 booleans is{Windows|Linux|Mac} are unexplained here as OS detection is another topic, so for the example I kept it simple and handled out of the method.

The ProcessBuilder is configured to redirect stderr to stdout, so that a single stream needs to be read. That stream is then read and logged, because you must consume stderr and stdout, in case the Terminal itself prints stuff (not the command you are running in the Terminal, this is about what the Terminal itself prints), if it prints too much you risk getting the process to block indefinitely, waiting for the buffer to be read. See this nice answer.

For macOS if the command you pass is always a single executable script, you could also use {"open", "-a", "Terminal", command}, but that will not work with echo hello && echo bye. Similarly you could use {"/Applications/Utilities/Terminal.app/Contents/MacOS/Terminal", command}, which would get you a second instance of the app running, but with same limitations.

Final note: you can offer a reasonable basic implementation, but you'll probably need to make it configurable to allow alternative terminal applications (especially on Linux where it varies a lot).

Community
  • 1
  • 1
Hugues M.
  • 19,846
  • 6
  • 37
  • 65
  • Just to verify, that we are on the same page, does your code call a terminal and open the terminal so the user can see what is going on and the prints go to that terminal? (I am asking, because you are gathering the Inputstream and printing it out) – Haeri May 20 '17 at 23:52
  • Holly Jesus! Your code does work! And I was about to go the script route where I had to create executable scripts and all that nonsense. A small addition that I would like to have: How can I disable the horribly spammy Mac OS process terminal? (Things like "last Login: .." and the "logout", "saving session", etc). Also what would I have to add to give a "press any button to exit" to the Linux command? Thank you so much for the help so far :D – Haeri May 21 '17 at 02:29
  • Sorry I don't know if you can control that output programmatically. Maybe there are options within the Terminal app. About your first question, the output that is logged is the one from the terminal itself, just in case (macOS Terminal does print 1 line when invoked this way). – Hugues M. May 21 '17 at 08:08
  • For xterm there is no button user can press to exit, other than the window close button. Did you mean key? Maybe append `; echo 'Press Enter to close...' && read` to the command – Hugues M. May 21 '17 at 08:22
  • Ok thanks about the mac thing. Maybe it is worth posting a seperate question about that? (I don' even know how I can ask that :S) – Haeri May 21 '17 at 11:41
  • And yes , sorry I meant "key" not "button". Unfortunately I have already tried with "read", but it won't accept that. Something about arg count :( I saw this in another thread "read -n 1 -s -p \"Press any key to continue\"" but it also won't work. – Haeri May 21 '17 at 11:44
  • Maybe just make it clear it's done and invite user to close window: `echo " ### DONE. You can safely close window, thanks, bye, XOXO ###"` – Hugues M. May 21 '17 at 11:48
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/144771/discussion-between-hugues-moreau-and-haeri). – Hugues M. May 21 '17 at 11:59
0

String[] cmd = {"echo", "hello", "&&", "echo", "hi"}; Runtime.getRuntime().exec(cmd);

There are multiple ways to do this, but this should work for you. Executables on Mac should run automatically in Terminal.

Possibly similar to: How To Run Mac OS Terminal Commands From Java (Using Runtime?) If anything they have a few methods for running scripts in the terminal.

Community
  • 1
  • 1
Alec O
  • 1,697
  • 1
  • 18
  • 31
  • 1
    Unfortunately this doesn't work. Also the linked thread is about executing custom programs, where as I am trying to just start the terminal and pass it some arguments. – Haeri May 20 '17 at 18:25