Exit inner loop only when EOF (Ctrl+D) is given via standard input

96 Views Asked by At

My program has a main while loop as the main logic, with an inner while loop for running the logic of the "command function". When inside the inner while loop, I want EOF (Ctrl + D) to exit from the inner while loop only and continue with the outer loop (to listen to more commands). However, its exiting from both the inner "command" loop and outer main while loop.

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Main {
    public static void main(String[] args) {

        String input, line;
        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));

        try {
            while (true) {
                input = reader.readLine();
                if (input == null)
                    break;

                if (input.compareTo("echo") == 0) {

                    while (true) {
                        line = reader.readLine();
                        // Ctrl+D (EOF) will exit loop
                        if (line == null)
                            break;
                        else System.out.println("echo: " + line);
                    }

                    System.out.println("Stop echo-ing");
                }
                else System.out.println("Cmd not recognized");

                // No EOF given, should not exit this loop
            }

            System.out.println("Exit main loop");

        } catch (IOException e) {
            System.err.println(e.getMessage());
        }
    }
}

To replicate problem:

  1. Copy and paste code, and run it
  2. Type echo to enter into inner while loop
  3. Press Ctrl + D to provide EOF to standard input

The following is printed:

^D
Stop echo-ing
Exit main loop

"Exit main loop" is printed unexpectedly.

How can I exit only the inner while loop when Ctrl + D is pressed?

1

There are 1 best solutions below

5
rzwitserloot On

What you want is not possible. CTRL+D doesn't 'send you a signal', it terminates standard in. It 'closes' System.in. The reason that your inner loop exits is because .readLine() returns null to signal 'the input source has closed'. Not because it returns null to signal: "User pressed CTRL+D". A thing that has closed is closed. Permanently. You can't un-close it.

Find another way to signal 'I wish to exit the inner loop'. For example, by having the user type DONE or EXIT or whatnot.

If that's not acceptable, read on for alternative solutions.

Signals

If you insist on using signal-esque features, java has not elevated the idea of 'OSes have signals they send to processes' to the "this is universal" level. Java is platform agnostic and has chosen the route of mostly not exposing any functionality that the underlying OS offers unless all OSes offer it in a way that java can unify.

But it is there - in a proprietary package that will get you warnings that it'll be removed soon. Of course. it's been emitting that warning for well over a decade; it still works in Java21:

// WARNING: You must hard-kill this process to end it!

import sun.misc.*;

class Test {
  public static void main(String[] args) throws Exception {
    Signal.handle(new Signal("INT"), new SignalHandler() {
      public void handle(Signal sig) {
        System.out.println("INT PRESSED!");
      }
    });

    while (true) {
      System.out.println("Main thread");
      Thread.sleep(1000L);
    }
  }
}

This will intercept CTRL+C (which sends the interrupt (INT) signal, and which is normally dealt with by a JDK by running all shutdown hooks and then shutting down the JVM).

It's very complicated: That code is run in a separate thread, so it needs to send a message to your main thread. It also needs to interrupt the main thread as it is 'waiting' for input which is OS-dependent (if it is not specced to throws InterruptedException, whether you can interrupt it is up to the implementation). It's a ton of really tricky work and all you get is that you can press CTRL+C (not CTRL+D!) to exit the inner loop, along with baggage that this will not work in future java versions.

I Strongly recommend you don't do this.

Cooked mode

A process's Standard in (in java, this is exposed as System.in) can be anything; if you run java myapp <somefile.txt, then standard in is the contents of that file. But, by default, it's the keyboard. Java starts its terminal interactions in this case in so-called 'cooked mode'. You get no input at all until the user presses enter, but the user gets to edit their input, for example.

You can move yourself to raw mode ('uncooked' mode). In raw mode, you get every keypress as they press it, and you can send terminal commands out that e.g. move the cursor, change the colour (or even background color) of the next thing that will be printed, and so forth.

In raw mode, CTRL+D and usually CTRL+C are simply sent to your app as if they were any other symbol - generally, with unicode value z where z is the letter in the alphabet (CTRL+A sends 1, CTRL+Z sends 26). As in, in.read() would return 1. Not '1' the character, 1 the number.

You probably don't want readLine() then, this wouldn't help you detect CTRL+D. You want just read(), and after inspecting what you read, either add it to a StringBuilder (if they typed a normal letter), or process the input (if they typed enter, that'd be in.read() == '\n'), or do whatever you want to do in response to CTRL+D if it's 4. Such as break the inner loop.

Cooked mode brings a lot of niceties and you'd have to handwrite them all. Also, moving to raw mode is OS dependent and java has no baked in ability to do it.

It's very complicated as well and I strongly recommend you don't do this, either. But, read all about it if you really want to burn yourself on this.

No, no! I want something simple

Don't shoot the messenger, but there is no simple answer. It's one of two very complicated solutions, or, forget about using CTRL+D as non-terminal signalling mechanism.