From 58012663f9e32c4b23774aff4091a78754b7dd8e Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 9 Jun 2026 03:22:15 +0000 Subject: [PATCH 1/3] test: add safety-net characterization tests for Readline behaviour Pin down the existing behaviour of the StreamReadline fallback and the stable LineInput.getStreamReadline factory before migrating the interactive input from the native GNU readline library to JLine. These tests pass against the current implementation and act as a regression safety net for the upcoming change. https://claude.ai/code/session_01Dve366yx4bjiFecJpbsZ7q --- .../org/tzi/use/util/input/LineInputTest.java | 59 +++++++++++ .../use/util/input/StreamReadlineTest.java | 100 ++++++++++++++++++ 2 files changed, 159 insertions(+) create mode 100644 use-core/src/test/java/org/tzi/use/util/input/LineInputTest.java create mode 100644 use-core/src/test/java/org/tzi/use/util/input/StreamReadlineTest.java diff --git a/use-core/src/test/java/org/tzi/use/util/input/LineInputTest.java b/use-core/src/test/java/org/tzi/use/util/input/LineInputTest.java new file mode 100644 index 000000000..d97b7b163 --- /dev/null +++ b/use-core/src/test/java/org/tzi/use/util/input/LineInputTest.java @@ -0,0 +1,59 @@ +/* + * USE - UML based specification environment + * Copyright (C) 1999-2004 Mark Richters, University of Bremen + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +package org.tzi.use.util.input; + +import org.junit.jupiter.api.Test; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.StringReader; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Characterization (safety-net) tests for the {@link LineInput} factory. + * + *

These cover the stable {@code getStreamReadline} factory method whose + * behaviour must be preserved across the migration to JLine.

+ */ +class LineInputTest { + + @Test + void getStreamReadline_returnsReadlineThatReadsTheStream() throws IOException { + BufferedReader reader = new BufferedReader(new StringReader("test line\n")); + try (Readline rl = LineInput.getStreamReadline(reader, false, "")) { + assertEquals("test line", rl.readline("> ")); + } + } + + @Test + void getStreamReadline_withoutEcho_doEchoIsFalse() { + BufferedReader reader = new BufferedReader(new StringReader("")); + Readline rl = LineInput.getStreamReadline(reader, false, ""); + assertFalse(rl.doEcho()); + } + + @Test + void getStreamReadline_withEcho_doEchoIsTrue() { + BufferedReader reader = new BufferedReader(new StringReader("")); + Readline rl = LineInput.getStreamReadline(reader, true, ""); + assertTrue(rl.doEcho()); + } +} diff --git a/use-core/src/test/java/org/tzi/use/util/input/StreamReadlineTest.java b/use-core/src/test/java/org/tzi/use/util/input/StreamReadlineTest.java new file mode 100644 index 000000000..04b1c2359 --- /dev/null +++ b/use-core/src/test/java/org/tzi/use/util/input/StreamReadlineTest.java @@ -0,0 +1,100 @@ +/* + * USE - UML based specification environment + * Copyright (C) 1999-2004 Mark Richters, University of Bremen + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +package org.tzi.use.util.input; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.StringReader; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Characterization (safety-net) tests for {@link StreamReadline} — the + * fallback {@link Readline} implementation used when no interactive terminal + * is available. + * + *

These tests pin down the behaviour of the existing implementation so we + * can be confident the migration from native GNU readline to JLine does not + * change the contract every {@link Readline} must satisfy.

+ */ +class StreamReadlineTest { + + private static StreamReadline from(String input) { + return new StreamReadline(new BufferedReader(new StringReader(input)), false); + } + + @Test + void readline_returnsInputLineWithoutLineTerminator() throws IOException { + try (var rl = from("hello\n")) { + assertEquals("hello", rl.readline("prompt> ")); + } + } + + @Test + void readline_returnsNullAtEndOfStream() throws IOException { + try (var rl = from("")) { + assertNull(rl.readline("prompt> ")); + } + } + + @Test + void readline_handlesMultipleLinesThenEof() throws IOException { + try (var rl = from("first\nsecond\n")) { + assertEquals("first", rl.readline("> ")); + assertEquals("second", rl.readline("> ")); + assertNull(rl.readline("> ")); + } + } + + @Test + void doEcho_isFalseByDefault() { + assertFalse(from("").doEcho()); + } + + @Test + void doEcho_isTrueWhenConstructedWithEcho() { + var rl = new StreamReadline(new BufferedReader(new StringReader("")), true); + assertTrue(rl.doEcho()); + } + + @Test + void usingHistory_isANoOp() { + assertDoesNotThrow(() -> from("").usingHistory()); + } + + @Test + void readHistory_isANoOp(@TempDir Path tmp) { + assertDoesNotThrow(() -> from("").readHistory(tmp.resolve("history").toString())); + } + + @Test + void writeHistory_isANoOp(@TempDir Path tmp) { + assertDoesNotThrow(() -> from("").writeHistory(tmp.resolve("history").toString())); + } + + @Test + void close_doesNotThrow() { + assertDoesNotThrow(() -> from("").close()); + } +} From 58752f9d452f7fe86e6185b7611a272badad0c1d Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 9 Jun 2026 03:28:43 +0000 Subject: [PATCH 2/3] feat: replace native GNU readline with JLine Resolves useocl/use#5. USE's interactive command line previously relied on the native Unix GNU readline library through a JNI binding (GNUReadline + natGNUReadline.c), which required a platform-dependent native build step and was effectively unavailable on Windows. Replace it with the pure-Java JLine library (already a declared dependency), giving line editing and a persistent command history on Linux, macOS and Windows with no native build steps. - Add JLineReadline, a Readline implementation backed by JLine's ConsoleReader with a FileHistory. History expansion is disabled so the SOIL '!' / '!!' commands are passed through verbatim. - LineInput.getUserInputReadline() now returns the JLine implementation for interactive terminals and falls back to the existing StreamReadline when no console is available (piped/headless), preserving current non-interactive behaviour. - Remove the JNI GNUReadline class and the natGNUReadline.c native source; declare the jline module in module-info. - Drop the obsolete "GNU readline not available" warning from the shell. The -nr command line switch is kept as an accepted no-op for backward compatibility (it is still passed by the Windows launcher and tests). - Cover the new behaviour with JLineReadlineTest and an additional LineInput test; the StreamReadline safety net continues to pass. https://claude.ai/code/session_01Dve366yx4bjiFecJpbsZ7q --- use-core/src/main/java/module-info.java | 1 + .../main/java/org/tzi/use/config/Options.java | 12 +- .../org/tzi/use/util/input/GNUReadline.java | 47 ------- .../org/tzi/use/util/input/JLineReadline.java | 95 +++++++++++++ .../org/tzi/use/util/input/LineInput.java | 54 ++++---- .../org/tzi/use/util/input/Readline_test.java | 2 +- .../org/tzi/use/util/input/natGNUReadline.c | 127 ------------------ .../tzi/use/util/input/JLineReadlineTest.java | 94 +++++++++++++ .../org/tzi/use/util/input/LineInputTest.java | 11 ++ .../java/org/tzi/use/main/shell/Shell.java | 13 +- 10 files changed, 234 insertions(+), 222 deletions(-) delete mode 100644 use-core/src/main/java/org/tzi/use/util/input/GNUReadline.java create mode 100644 use-core/src/main/java/org/tzi/use/util/input/JLineReadline.java delete mode 100644 use-core/src/main/java/org/tzi/use/util/input/natGNUReadline.c create mode 100644 use-core/src/test/java/org/tzi/use/util/input/JLineReadlineTest.java diff --git a/use-core/src/main/java/module-info.java b/use-core/src/main/java/module-info.java index 350e9e572..4413df9d9 100644 --- a/use-core/src/main/java/module-info.java +++ b/use-core/src/main/java/module-info.java @@ -8,6 +8,7 @@ requires java.scripting; requires org.jruby.dist; requires combinatoricslib; + requires jline; requires java.datatransfer; requires java.desktop; exports org.tzi.use.config; diff --git a/use-core/src/main/java/org/tzi/use/config/Options.java b/use-core/src/main/java/org/tzi/use/config/Options.java index 9064cdb5e..e14948684 100644 --- a/use-core/src/main/java/org/tzi/use/config/Options.java +++ b/use-core/src/main/java/org/tzi/use/config/Options.java @@ -117,9 +117,7 @@ public static String getIconPath(String iconName) { * Otherwise, only the shell is available. */ public static boolean doGUI = true; - - public static boolean suppressWarningsAboutMissingReadlineLibrary = false; - + public static boolean quiet = false; private static boolean debug = false; @@ -246,7 +244,7 @@ private static void printHelp() { System.out.println(" -noplugins do not use plugins"); System.out.println(" -h print help"); System.out.println(" -H=path home of use installation"); - System.out.println(" -nr suppress warnings about missing readline library"); + System.out.println(" -nr deprecated, ignored (kept for backward compatibility)"); System.out.println(" -q reads spec_file, executes cmd_file, and checks constraints"); System.out.println(" exit code is 1 if constraints fail, otherwise 0"); System.out.println(" -qv like -q but with verbose output of constraint check"); @@ -306,7 +304,6 @@ public static void resetOptions() { compileOnly = false; compileAndPrint = false; doGUI = true; - suppressWarningsAboutMissingReadlineLibrary = false; quiet = false; debug = false; quietAndVerboseConstraintCheck = false; @@ -356,8 +353,9 @@ public static void processArgs(String[] args) { System.err.println("Invalid path " + StringUtil.inQuotes(arg.substring(2)) + " for home directory specified."); System.exit(1); } - } else if (arg.equals("nr")) { - suppressWarningsAboutMissingReadlineLibrary = true; + } else if (arg.equals("nr")) { + // Deprecated: the GNU readline library is no longer used. + // Accepted as a no-op for backward compatibility. } else if (arg.equals("q")) { Options.quiet = true; Options.doGUI = false; diff --git a/use-core/src/main/java/org/tzi/use/util/input/GNUReadline.java b/use-core/src/main/java/org/tzi/use/util/input/GNUReadline.java deleted file mode 100644 index bc73cac61..000000000 --- a/use-core/src/main/java/org/tzi/use/util/input/GNUReadline.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * USE - UML based specification environment - * Copyright (C) 1999-2004 Mark Richters, University of Bremen - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation; either version 2 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. - */ - -package org.tzi.use.util.input; - -import java.io.IOException; - -/** - * Native wrapper for the GNU Readline Library. - * - * Don't use this directly since it may no be available on all - * platforms. Use LineInput.getReadline() to get an appropriate - * implementation instead. - * - * @author Mark Richters - */ -public class GNUReadline implements Readline { - - static { - System.loadLibrary("natGNUReadline"); - } - - public native String readline(String prompt); - public native void usingHistory(); - public native void readHistory(String filename) throws IOException; - public native void writeHistory(String filename) throws IOException; - public native void close(); - public boolean doEcho() { - return false; - } -} diff --git a/use-core/src/main/java/org/tzi/use/util/input/JLineReadline.java b/use-core/src/main/java/org/tzi/use/util/input/JLineReadline.java new file mode 100644 index 000000000..aa957a00f --- /dev/null +++ b/use-core/src/main/java/org/tzi/use/util/input/JLineReadline.java @@ -0,0 +1,95 @@ +/* + * USE - UML based specification environment + * Copyright (C) 1999-2004 Mark Richters, University of Bremen + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +package org.tzi.use.util.input; + +import jline.console.ConsoleReader; +import jline.console.history.FileHistory; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * A {@link Readline} implementation backed by the pure-Java + * JLine library. It provides + * line editing and a persistent command history on all platforms (Linux, + * macOS and Windows) without requiring a platform-dependent native library. + * + *

This replaces the former JNI binding to the native GNU readline library + * and removes the corresponding native build steps.

+ * + * @author Mark Richters + */ +public class JLineReadline implements Readline { + + private final ConsoleReader reader; + private FileHistory history; + + public JLineReadline() throws IOException { + this(null, null); + } + + /** + * Package-private constructor used by tests to supply explicit streams + * instead of attaching to the real terminal. + */ + JLineReadline(InputStream in, OutputStream out) throws IOException { + reader = (in != null && out != null) + ? new ConsoleReader(in, out) + : new ConsoleReader(); + // USE uses '!' and '!!' to start SOIL statements. Disable JLine's + // history expansion so these characters are passed through verbatim. + reader.setExpandEvents(false); + } + + @Override + public String readline(String prompt) throws IOException { + return reader.readLine(prompt); + } + + @Override + public void usingHistory() { + reader.setHistoryEnabled(true); + } + + @Override + public void readHistory(String filename) throws IOException { + history = new FileHistory(new File(filename)); + reader.setHistory(history); + } + + @Override + public void writeHistory(String filename) throws IOException { + if (history != null) { + history.flush(); + } + } + + @Override + public void close() throws IOException { + reader.shutdown(); + } + + @Override + public boolean doEcho() { + return false; + } +} diff --git a/use-core/src/main/java/org/tzi/use/util/input/LineInput.java b/use-core/src/main/java/org/tzi/use/util/input/LineInput.java index 729a483b1..16a20ef2d 100644 --- a/use-core/src/main/java/org/tzi/use/util/input/LineInput.java +++ b/use-core/src/main/java/org/tzi/use/util/input/LineInput.java @@ -20,47 +20,45 @@ package org.tzi.use.util.input; import java.io.BufferedReader; +import java.io.IOException; import java.io.InputStreamReader; /** - * Interface for getting a suitable platform-dependent readline - * implementation. The GNU readline library is preferably used if - * installed. - * - * @author Mark Richters + * Factory for obtaining a suitable {@link Readline} implementation. + * + *

For interactive use the pure-Java {@link JLineReadline} is returned, + * providing line editing and command history on all platforms. When no + * interactive terminal is available (e.g. the input is piped or USE runs in + * a headless environment) a plain {@link StreamReadline} reading from + * {@code System.in} is used instead.

+ * + * @author Mark Richters */ public class LineInput { // utility class private LineInput() {} - + /** - * Returns a readline implementation. If the native GNU readline - * library is available, return that. Otherwise, a stream readline - * implementation with System.in as source is returned. - * - * @param errorMessage if not null print a message when the native - * GNU readline library is not available, otherwise - * fail silently. + * Returns a {@link Readline} implementation for reading interactive user + * input. A JLine-backed implementation is used when a terminal is + * available; otherwise a simple stream-based implementation reading from + * {@code System.in} is returned. */ - public static Readline getUserInputReadline(String errorMessage) { - Readline rl = null; - try { - System.loadLibrary("natGNUReadline"); - rl = new GNUReadline(); - } catch (UnsatisfiedLinkError ex) { - if (errorMessage != null ) { - System.out.println(ex.toString()); - System.out.println(errorMessage); + public static Readline getUserInputReadline() { + if (System.console() != null) { + try { + return new JLineReadline(); + } catch (IOException ex) { + // JLine could not attach to the terminal; fall back below. } - BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); - // no echo, do protocol - rl = new StreamReadline(reader, false); } - return rl; + BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); + // no echo, do protocol + return new StreamReadline(reader, false); } - public static Readline getStreamReadline(BufferedReader reader, boolean doEcho, String string) { - return new StreamReadline(reader, doEcho, string); + public static Readline getStreamReadline(BufferedReader reader, boolean doEcho, String prompt) { + return new StreamReadline(reader, doEcho, prompt); } } diff --git a/use-core/src/main/java/org/tzi/use/util/input/Readline_test.java b/use-core/src/main/java/org/tzi/use/util/input/Readline_test.java index 38863288f..b4c002230 100644 --- a/use-core/src/main/java/org/tzi/use/util/input/Readline_test.java +++ b/use-core/src/main/java/org/tzi/use/util/input/Readline_test.java @@ -29,7 +29,7 @@ class Readline_test { public static void main(String[] args) { - Readline rl = LineInput.getUserInputReadline("readline library not found"); + Readline rl = LineInput.getUserInputReadline(); String line; try { do { diff --git a/use-core/src/main/java/org/tzi/use/util/input/natGNUReadline.c b/use-core/src/main/java/org/tzi/use/util/input/natGNUReadline.c deleted file mode 100644 index 7a4d73f42..000000000 --- a/use-core/src/main/java/org/tzi/use/util/input/natGNUReadline.c +++ /dev/null @@ -1,127 +0,0 @@ -/* - * USE - UML based specification environment - * Copyright (C) 1999-2004 Mark Richters, University of Bremen - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation; either version 2 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. - */ - -/* $ProjectHeader: use 0.393 Wed, 16 May 2007 14:10:28 +0200 opti $ */ - -#include -#include "org_tzi_use_util_input_GNUReadline.h" -#include -#include -#include -#include - -/* - * Class: org_tzi_use_util_input_GNUReadline - * Method: prepareClose - * Signature: ()V - */ -JNIEXPORT void JNICALL -Java_org_tzi_use_util_input_GNUReadline_close(JNIEnv *env, - jobject obj) -{ - - rl_done = 1; - rl_free_line_state(); - rl_deprep_terminal(); -} - -/* - * Class: org_tzi_use_util_input_GNUReadline - * Method: readline - * Signature: (Ljava/lang/String;)Ljava/lang/String; - */ -JNIEXPORT jstring JNICALL -Java_org_tzi_use_util_input_GNUReadline_readline(JNIEnv *env, - jobject obj, - jstring prompt) -{ - jstring result; - char *line; - const char *cprompt = (*env)->GetStringUTFChars(env, prompt, 0); - - /* Get a line from the user. */ - line = readline((char *) cprompt); - - /* If the line has any text in it, save it on the history. */ - if ( line && *line) - add_history(line); - - /* Create the Java result string object. */ - result = (*env)->NewStringUTF(env, line); - - /* Free memory. */ - (*env)->ReleaseStringUTFChars(env, prompt, cprompt); - free(line); - - return result; -} - -/* - * Class: org_tzi_use_util_input_GNUReadline - * Method: usingHistory - * Signature: ()V - */ -JNIEXPORT void JNICALL -Java_org_tzi_use_util_input_GNUReadline_usingHistory(JNIEnv *env, - jobject obj) -{ - using_history(); -} - -/* - * Class: org_tzi_use_util_input_GNUReadline - * Method: readHistory - * Signature: (Ljava/lang/String;)V - */ -JNIEXPORT void JNICALL -Java_org_tzi_use_util_input_GNUReadline_readHistory(JNIEnv *env, - jobject obj, - jstring filename) -{ - const char *cfilename = (*env)->GetStringUTFChars(env, filename, 0); - int res = read_history((char *) cfilename); - if ( res ) { - /* Could not read file, res == errno */ - char *msg = strerror(res); - jclass io_exception = (*env)->FindClass(env, "java/io/IOException"); - (*env)->ThrowNew(env, io_exception, msg); - } - return; -} - -/* - * Class: org_tzi_use_util_input_GNUReadline - * Method: writeHistory - * Signature: (Ljava/lang/String;)V - */ -JNIEXPORT void JNICALL -Java_org_tzi_use_util_input_GNUReadline_writeHistory(JNIEnv *env, - jobject obj, - jstring filename) -{ - const char *cfilename = (*env)->GetStringUTFChars(env, filename, 0); - int res = write_history((char *) cfilename); - if ( res ) { - /* Could not write file, res == errno */ - char *msg = strerror(res); - jclass io_exception = (*env)->FindClass(env, "java/io/IOException"); - (*env)->ThrowNew(env, io_exception, msg); - } - return; -} diff --git a/use-core/src/test/java/org/tzi/use/util/input/JLineReadlineTest.java b/use-core/src/test/java/org/tzi/use/util/input/JLineReadlineTest.java new file mode 100644 index 000000000..13734b3e8 --- /dev/null +++ b/use-core/src/test/java/org/tzi/use/util/input/JLineReadlineTest.java @@ -0,0 +1,94 @@ +/* + * USE - UML based specification environment + * Copyright (C) 1999-2004 Mark Richters, University of Bremen + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +package org.tzi.use.util.input; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests for the new JLine-backed {@link JLineReadline}. + * + *

A package-private constructor accepting explicit streams is used so the + * tests never need to attach to a real terminal.

+ */ +class JLineReadlineTest { + + private static JLineReadline readingFrom(String input) throws IOException { + return new JLineReadline(new ByteArrayInputStream(input.getBytes()), new ByteArrayOutputStream()); + } + + @Test + void readline_returnsInputLine() throws IOException { + try (var rl = readingFrom("hello\n")) { + assertEquals("hello", rl.readline("prompt> ")); + } + } + + @Test + void readline_returnsNullAtEndOfStream() throws IOException { + try (var rl = readingFrom("")) { + assertNull(rl.readline("prompt> ")); + } + } + + @Test + void doEcho_returnsFalse() throws IOException { + try (var rl = readingFrom("")) { + assertFalse(rl.doEcho()); + } + } + + @Test + void usingHistory_doesNotThrow() throws IOException { + try (var rl = readingFrom("")) { + assertDoesNotThrow(rl::usingHistory); + } + } + + @Test + void writeHistory_persistsEnteredCommandsToFile(@TempDir Path tmp) throws IOException { + Path histFile = tmp.resolve(".use_history_test"); + + try (var rl = new JLineReadline( + new ByteArrayInputStream("cmd1\ncmd2\n".getBytes()), new ByteArrayOutputStream())) { + rl.usingHistory(); + rl.readHistory(histFile.toString()); + rl.readline("> "); + rl.readline("> "); + rl.writeHistory(histFile.toString()); + } + + assertTrue(histFile.toFile().exists(), "history file should be created"); + assertTrue(histFile.toFile().length() > 0, "history file should not be empty"); + } + + @Test + void close_doesNotThrow() throws IOException { + var rl = readingFrom(""); + assertDoesNotThrow(rl::close); + } +} diff --git a/use-core/src/test/java/org/tzi/use/util/input/LineInputTest.java b/use-core/src/test/java/org/tzi/use/util/input/LineInputTest.java index d97b7b163..25991794e 100644 --- a/use-core/src/test/java/org/tzi/use/util/input/LineInputTest.java +++ b/use-core/src/test/java/org/tzi/use/util/input/LineInputTest.java @@ -56,4 +56,15 @@ void getStreamReadline_withEcho_doEchoIsTrue() { Readline rl = LineInput.getStreamReadline(reader, true, ""); assertTrue(rl.doEcho()); } + + @Test + void getUserInputReadline_returnsNonNullNonEchoingReadline() { + // The interactive factory must always yield a usable, non-echoing + // Readline. Whether that is a JLineReadline (interactive terminal) + // or the StreamReadline fallback (headless/piped) depends on the + // environment, but the behavioural contract is the same. + Readline rl = LineInput.getUserInputReadline(); + assertNotNull(rl); + assertFalse(rl.doEcho()); + } } diff --git a/use-gui/src/main/java/org/tzi/use/main/shell/Shell.java b/use-gui/src/main/java/org/tzi/use/main/shell/Shell.java index ce5c49d93..4afcdfb4c 100644 --- a/use-gui/src/main/java/org/tzi/use/main/shell/Shell.java +++ b/use-gui/src/main/java/org/tzi/use/main/shell/Shell.java @@ -268,18 +268,7 @@ private void setupReadline() { return; } - String GNUReadlineNotAvailable; - if (Options.suppressWarningsAboutMissingReadlineLibrary) { - GNUReadlineNotAvailable = null; - } else { - GNUReadlineNotAvailable = "Apparently, the GNU readline library is not available on your system." - + Options.LINE_SEPARATOR - + "The program will continue using a simple readline implementation." - + Options.LINE_SEPARATOR - + "You can turn off this warning message by using the switch -nr"; - } - - fReadline = LineInput.getUserInputReadline(GNUReadlineNotAvailable); + fReadline = LineInput.getUserInputReadline(); fReadline.usingHistory(); // Read command history from previous sessions From 2eea5b60ce2f3649852a5b195020070f3c3818a7 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 9 Jun 2026 03:31:21 +0000 Subject: [PATCH 3/3] docs: update install notes and launch scripts for JLine Reflect the move from the native GNU readline library to the pure-Java JLine library: - INSTALL: describe JLine-based line editing/history; no native build step. - README: list JLine among the bundled third-party libraries. - NEWS: add an entry for the readline -> JLine change. - bin/use: drop LD_LIBRARY_PATH, which only existed for the native readline library. - bin/start_use.bat: drop the now-obsolete -nr switch (the missing readline warning it suppressed no longer exists). - .gitignore: ignore the .claude directory. https://claude.ai/code/session_01Dve366yx4bjiFecJpbsZ7q --- .gitignore | 3 +++ INSTALL | 17 +++++++---------- NEWS | 4 ++++ README.md | 1 + use-gui/src/main/resources/bin/start_use.bat | 4 ++-- use-gui/src/main/resources/bin/use | 2 -- 6 files changed, 17 insertions(+), 14 deletions(-) diff --git a/.gitignore b/.gitignore index 0d379eafe..c3b46be68 100644 --- a/.gitignore +++ b/.gitignore @@ -117,3 +117,6 @@ fabric.properties /use-assembly/target **/.DS_Store + +# Claude Code +.claude/ diff --git a/INSTALL b/INSTALL index c060eda80..b2d2769db 100644 --- a/INSTALL +++ b/INSTALL @@ -29,16 +29,13 @@ Otherwise, a simple "mvn compile" or "mwn verify" should compile the whole USE package. "mvn verify" will run all tests whereas "mvn compile" just compiles. -Interaction with USE is done via a command line interface. This will -be much more comfortable if you have the GNU readline library -available on your system. USE provides an interface to the readline -library. For example, it allows bash or emacs-like editing of lines -and keeps a history file of recent input. Since the readline library -is platform dependent, the interface has to be compiled on your -system. Changes to the build file may be necessary for your -environment. If you don't have the readline library - don't worry. -USE will fall back to a simple input mechanism if it cannot find or -load the readline library at runtime. +Interaction with USE is done via a command line interface. It uses the +pure-Java JLine library to provide comfortable line editing (bash or +emacs-like) and to keep a history file of recent input. JLine works out +of the box on Linux, macOS and Windows, so no platform-dependent native +library has to be built or installed. When USE is run without an +interactive terminal (for example with piped input), it automatically +falls back to a simple input mechanism. diff --git a/NEWS b/NEWS index 95c0f3365..12f557a62 100644 --- a/NEWS +++ b/NEWS @@ -3,6 +3,10 @@ Please see the file `README' for a description of how to report bugs. ** Changes between version 7.1.0 and X.X.X * USE now supports data types +* The interactive command line now uses the pure-Java JLine library for line + editing and history instead of the native GNU readline library. This removes + the native build step and provides command-line editing on Windows, macOS + and Linux out of the box. ** Changes between version 5.2.0 and 6.0.0 * New OCL complexity plugin diff --git a/README.md b/README.md index 323637266..7974f96f0 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,7 @@ the web. - The [ANTLR parser generator tool](http://www.antlr.org) - The [JUnit library](http://www.junit.org) +- The [JLine library](https://github.com/jline/jline2) for command-line editing and history ## Reporting bugs diff --git a/use-gui/src/main/resources/bin/start_use.bat b/use-gui/src/main/resources/bin/start_use.bat index 3b07f5c16..30c7131ad 100644 --- a/use-gui/src/main/resources/bin/start_use.bat +++ b/use-gui/src/main/resources/bin/start_use.bat @@ -21,9 +21,9 @@ IF NOT EXIST %USE_JAR% ( REM Check if first argument is "jfx" (/I makes the comparison case-insensitive) IF /I "%1"=="-jfx" ( - java %VMARGS% --module-path %JAVAFX_LIB% --add-modules javafx.controls,javafx.fxml,javafx.web,javafx.graphics,javafx.swing --add-opens javafx.graphics/com.sun.javafx.scene=ALL-UNNAMED --add-exports javafx.base/com.sun.javafx.event=ALL-UNNAMED -jar %USE_JAR% -nr %* + java %VMARGS% --module-path %JAVAFX_LIB% --add-modules javafx.controls,javafx.fxml,javafx.web,javafx.graphics,javafx.swing --add-opens javafx.graphics/com.sun.javafx.scene=ALL-UNNAMED --add-exports javafx.base/com.sun.javafx.event=ALL-UNNAMED -jar %USE_JAR% %* ) ELSE ( - java %VMARGS% -jar %USE_JAR% -nr %* + java %VMARGS% -jar %USE_JAR% %* ) if "%OS%"=="Windows_NT" @endlocal diff --git a/use-gui/src/main/resources/bin/use b/use-gui/src/main/resources/bin/use index ce8c0b78b..cde6bb5e1 100755 --- a/use-gui/src/main/resources/bin/use +++ b/use-gui/src/main/resources/bin/use @@ -16,7 +16,5 @@ if [ ! -f "$USE_JAR" ]; then exit 1 fi -export LD_LIBRARY_PATH="$USE_HOME/lib:$LD_LIBRARY_PATH" - # set cmd for running USE java $VMARGS -jar "$USE_JAR" "$@"