Skip to content

Commit 6d507d2

Browse files
authored
Merge pull request #18 from fglock/jline-command-line-editor
Jline command line editor
2 parents 9f3e812 + 11ab63c commit 6d507d2

File tree

12 files changed

+193
-54
lines changed

12 files changed

+193
-54
lines changed

MILESTONES.md

Lines changed: 37 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,42 @@
99

1010
## Completed Milestones
1111

12+
- **v3.1.0**: Tracks Perl 5.42.0
13+
- Update Perl version to `5.42.0`.
14+
- Added features: `keyword_all`, `keyword_any`
15+
16+
- Accept input program in several ways:
17+
1. **Piped input**: `echo 'print "Hello\n"' | ./jperl` - reads from pipe and executes immediately
18+
2. **Interactive input**: `./jperl` - shows a prompt and waits for you to type code, then press Ctrl+D (on Unix/Linux/Mac) or Ctrl+Z (on Windows) to signal end of input
19+
3. **File redirection**: `./jperl < script.pl` - reads from the file
20+
4. **With arguments**: `./jperl -e 'print "Hello\n"'` or `./jperl script.pl`
21+
22+
- Added overload operators: `!`, `+`, `-`, `*`, `/`, `%`, `int`, `neg`, `log`, `sqrt`, `cos`, `sin`, `exp`, `abs`, `atan2`, `**`, `@{}`, `%{}`. `${}`, `&{}`, `*{}`.
23+
- Subroutine prototypes are fully implemented. Added or fixed: `+`, `;`, `*`, `\@`, `\%`, `\$`, `\[@%]`.
24+
- Added double quoted string escapes: `\U`, `\L`, `\u`, `\l`.
25+
- Added star count (`C*`) in `pack`, `unpack`.
26+
- Added operators: `read`, `tell`, `seek`, `system`, `exec`, `sysopen`, `chmod`.
27+
- Added operator: `select(undef,undef,undef,$time)`.
28+
- Added operator: `^^=`.
29+
- Added operator: `delete`, `exists` for array indexes.
30+
- Added `open` option: in-memory files.
31+
- Syntax: identifiers starting with `::` are in `main` package.
32+
- Added I/O layers support to `open`, `binmode`: `:raw`, `:bytes`, `:crlf`, `:utf8`, `:unix`, `:encoding()`.
33+
- Add `open` support for pipe `-|`, `|-`, `ls|`, `|sort`.
34+
- Added `# line` preprocessor directive.
35+
- `Test::More` module: added `subtest`, `use_ok`, `require_ok`.
36+
- `CORE::` operators have the same prototypes as in Perl.
37+
- Added modules: `Fcntl`, `Test`, `Text::CSV`.
38+
- Operator `$#` returns an lvalue.
39+
- Improved autovivification handling: distinguish between contexts where undefined references should automatically create data structures versus where they should throw errors.
40+
- Bugfix: fix a problem with Windows newlines and qw(). Also fixed `mkdir` in Windows.
41+
- Bugfix: `-E` switch was setting strict mode.
42+
- BugFix: fix calling context in operators that return list.
43+
- BugFix: fix rules for overriding operators.
44+
- Added Makefile.
45+
- Debian package can be created with `make deb`.
46+
47+
1248
- **v3.0.0**: Performance Boost, New Modules, and Streamlined Configuration
1349
- Added `--upgrade` option to `Configure.pl` to upgrade dependencies.
1450
- Added `Dockerfile` configuration.
@@ -236,39 +272,7 @@ The following areas are currently under active development to enhance the functi
236272

237273
## Upcoming Milestones
238274

239-
- **v3.0.1**: Next minor version
240-
- Update Perl version to `5.42.0`.
241-
242-
- Accept input program in several ways:
243-
1. **Piped input**: `echo 'print "Hello\n"' | ./jperl` - reads from pipe and executes immediately
244-
2. **Interactive input**: `./jperl` - shows a prompt and waits for you to type code, then press Ctrl+D (on Unix/Linux/Mac) or Ctrl+Z (on Windows) to signal end of input
245-
3. **File redirection**: `./jperl < script.pl` - reads from the file
246-
4. **With arguments**: `./jperl -e 'print "Hello\n"'` or `./jperl script.pl`
247-
248-
- Added overload operators: `!`, `+`, `-`, `*`, `/`, `%`, `int`, `neg`, `log`, `sqrt`, `cos`, `sin`, `exp`, `abs`, `atan2`, `**`, `@{}`, `%{}`. `${}`, `&{}`, `*{}`.
249-
- Subroutine prototypes are fully implemented. Added or fixed: `+`, `;`, `*`, `\@`, `\%`, `\$`, `\[@%]`.
250-
- Added double quoted string escapes: `\U`, `\L`, `\u`, `\l`.
251-
- Added star count (`C*`) in `pack`, `unpack`.
252-
- Added operators: `read`, `tell`, `seek`, `system`, `exec`, `sysopen`, `chmod`.
253-
- Added operator: `select(undef,undef,undef,$time)`.
254-
- Added operator: `^^=`.
255-
- Added operator: `delete`, `exists` for array indexes.
256-
- Added `open` option: in-memory files.
257-
- Syntax: identifiers starting with `::` are in `main` package.
258-
- Added I/O layers support to `open`, `binmode`: `:raw`, `:bytes`, `:crlf`, `:utf8`, `:unix`, `:encoding()`.
259-
- Add `open` support for pipe `-|`, `|-`, `ls|`, `|sort`.
260-
- Added `# line` preprocessor directive.
261-
- `Test::More` module: added `subtest`, `use_ok`, `require_ok`.
262-
- `CORE::` operators have the same prototypes as in Perl.
263-
- Added modules: `Fcntl`, `Test`, `Text::CSV`.
264-
- Operator `$#` returns an lvalue.
265-
- Improved autovivification handling: distinguish between contexts where undefined references should automatically create data structures versus where they should throw errors.
266-
- Bugfix: fix a problem with Windows newlines and qw(). Also fixed `mkdir` in Windows.
267-
- Bugfix: `-E` switch was setting strict mode.
268-
- BugFix: fix calling context in operators that return list.
269-
- BugFix: fix rules for overriding operators.
270-
- Added Makefile.
271-
- Debian package can be created with `make deb`.
275+
- **v3.2.0**: Next minor version
272276
- Planned release date: 2025-12-10.
273277

274278
- Work in Progress

build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ repositories {
6868
// Project dependencies
6969
dependencies {
7070
// Core dependencies
71+
implementation 'org.jline:jline-reader:3.30.4'
72+
implementation 'org.jline:jline-terminal:3.30.4'
7173
implementation 'org.ow2.asm:asm:9.8' // ByteCode manipulation
7274
implementation 'org.ow2.asm:asm-util:9.8' // ASM utilities
7375
implementation 'com.ibm.icu:icu4j:77.1' // Unicode support

docs/FEATURE_MATRIX.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -471,7 +471,7 @@ The `:encoding()` layer supports all encodings provided by Java's `Charset.forNa
471471
-**subs** pragma
472472
- 🚧 **utf8** pragma: utf8 is always on. Disabling utf8 might work in a future version.
473473
-**feature** pragma
474-
- ✅ Features implemented: `fc`, `say`, `current_sub`, `isa`, `state`, `try`, `bitwise`, `postderef`, `evalbytes`, `module_true`, `signatures`, `class`.
474+
- ✅ Features implemented: `fc`, `say`, `current_sub`, `isa`, `state`, `try`, `bitwise`, `postderef`, `evalbytes`, `module_true`, `signatures`, `class`, `keyword_all`, `keyword_any`.
475475
- ❌ Features missing: `postderef_qq`, `unicode_eval`, `unicode_strings`, `defer`.
476476
- 🚧 **warnings** pragma
477477
-**version** pragma: version objects are not yet supported.

pom.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,16 @@
1717
</properties>
1818

1919
<dependencies>
20+
<dependency>
21+
<groupId>org.jline</groupId>
22+
<artifactId>jline-reader</artifactId>
23+
<version>3.30.4</version>
24+
</dependency>
25+
<dependency>
26+
<groupId>org.jline</groupId>
27+
<artifactId>jline-terminal</artifactId>
28+
<version>3.30.4</version>
29+
</dependency>
2030
<dependency>
2131
<groupId>org.ow2.asm</groupId>
2232
<artifactId>asm</artifactId>

src/main/java/org/perlonjava/ArgumentParser.java

Lines changed: 57 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@
1212
import java.util.ArrayList;
1313
import java.util.Arrays;
1414
import java.util.List;
15+
import org.jline.reader.LineReader;
16+
import org.jline.reader.LineReaderBuilder;
17+
import org.jline.reader.EndOfFileException;
18+
import org.jline.reader.UserInterruptException;
19+
import org.jline.terminal.Terminal;
20+
import org.jline.terminal.TerminalBuilder;
1521

1622
import static org.perlonjava.Configuration.getPerlVersionBundle;
1723
import static org.perlonjava.Configuration.perlVersion;
@@ -40,27 +46,58 @@ public static CompilerOptions parseArguments(String[] args) {
4046
// If no code was provided and no filename, try reading from stdin
4147
if (parsedArgs.code == null) {
4248
try {
43-
// Try to read from stdin - this will work for pipes, redirections, and interactive input
4449
StringBuilder stdinContent = new StringBuilder();
45-
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
46-
47-
// Check if we're reading from a pipe/redirection vs interactive terminal
4850
boolean isInteractive = System.console() != null;
4951

5052
if (isInteractive) {
51-
// Interactive mode - prompt the user and read until EOF (Ctrl+D)
52-
System.err.println("Enter Perl code (press Ctrl+D when done):");
53-
}
53+
// Interactive mode with JLine for better editing experience
54+
try {
55+
Terminal terminal = TerminalBuilder.builder()
56+
.system(true)
57+
.build();
58+
59+
LineReader lineReader = LineReaderBuilder.builder()
60+
.terminal(terminal)
61+
.build();
62+
63+
System.err.println("Enter Perl code (press Ctrl+D when done, or type 'exit' to quit):");
64+
System.err.println("Use arrow keys to navigate, Ctrl+A/E for home/end");
65+
66+
String line;
67+
while (true) {
68+
try {
69+
line = lineReader.readLine("> ");
70+
if (line != null) {
71+
if ("exit".equals(line.trim())) {
72+
break;
73+
}
74+
stdinContent.append(line).append("\n");
75+
}
76+
} catch (EndOfFileException e) {
77+
// User pressed Ctrl+D
78+
break;
79+
} catch (UserInterruptException e) {
80+
// User pressed Ctrl+C
81+
System.err.println("\nInterrupted. Use 'exit' or Ctrl+D to quit.");
82+
break;
83+
}
84+
}
5485

55-
// Read from stdin regardless of whether it's interactive or not
56-
String line;
57-
while ((line = reader.readLine()) != null) {
58-
stdinContent.append(line).append("\n");
86+
terminal.close();
87+
} catch (Exception e) {
88+
// Fall back to basic readline if JLine fails
89+
System.err.println("Enhanced editing not available, falling back to basic mode.");
90+
System.err.println("Enter Perl code (press Ctrl+D when done):");
91+
fallbackReadlines(stdinContent);
92+
}
93+
} else {
94+
// Non-interactive mode (pipes, redirections)
95+
fallbackReadlines(stdinContent);
5996
}
6097

6198
if (stdinContent.length() > 0) {
6299
parsedArgs.code = stdinContent.toString();
63-
parsedArgs.fileName = "-"; // Indicate that code came from stdin
100+
parsedArgs.fileName = "-";
64101
}
65102
} catch (IOException e) {
66103
// If we can't read from stdin, continue with normal error handling
@@ -72,6 +109,14 @@ public static CompilerOptions parseArguments(String[] args) {
72109
return parsedArgs;
73110
}
74111

112+
private static void fallbackReadlines(StringBuilder stdinContent) throws IOException {
113+
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
114+
String line;
115+
while ((line = reader.readLine()) != null) {
116+
stdinContent.append(line).append("\n");
117+
}
118+
}
119+
75120
/**
76121
* Processes the command-line arguments, distinguishing between switch and non-switch arguments.
77122
*

src/main/java/org/perlonjava/codegen/EmitBinaryOperatorNode.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public static void emitBinaryOperatorNode(EmitterVisitor emitterVisitor, BinaryO
4848
case "push", "unshift" -> EmitOperator.handlePushOperator(emitterVisitor, node);
4949

5050
// Higher-order functions
51-
case "map", "sort", "grep" -> EmitOperator.handleMapOperator(emitterVisitor, node);
51+
case "map", "sort", "grep", "all", "any" -> EmitOperator.handleMapOperator(emitterVisitor, node);
5252

5353
// I/O operations
5454
case "eof", "open", "printf", "print", "say" ->
@@ -97,7 +97,7 @@ public static void emitBinaryOperatorNode(EmitterVisitor emitterVisitor, BinaryO
9797
OperatorHandler.get(node.operator));
9898

9999
default -> throw new PerlCompilerException(node.tokenIndex,
100-
"Unexpected infix operator: " + node.operator, emitterVisitor.ctx.errorUtil);
100+
"Not implemented operator: " + node.operator, emitterVisitor.ctx.errorUtil);
101101
}
102102
}
103103
}

src/main/java/org/perlonjava/operators/ListOperators.java

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import java.util.List;
88

99
import static org.perlonjava.runtime.GlobalVariable.getGlobalVariable;
10+
import static org.perlonjava.runtime.RuntimeScalarCache.scalarFalse;
11+
import static org.perlonjava.runtime.RuntimeScalarCache.scalarTrue;
1012

1113
public class ListOperators {
1214
/**
@@ -116,16 +118,14 @@ public static RuntimeList sort(RuntimeList runtimeList, RuntimeScalar perlCompar
116118
* @throws RuntimeException If the Perl filter subroutine throws an exception.
117119
*/
118120
public static RuntimeList grep(RuntimeList runtimeList, RuntimeScalar perlFilterClosure, int ctx) {
119-
RuntimeArray array = runtimeList.getArrayOfAlias();
120-
121121
// Create a new list to hold the filtered elements
122122
List<RuntimeBaseEntity> filteredElements = new ArrayList<>();
123123

124124
RuntimeScalar var_ = getGlobalVariable("main::_");
125125
RuntimeArray filterArgs = new RuntimeArray();
126126

127127
// Iterate over each element in the current RuntimeArray
128-
for (RuntimeScalar element : array.elements) {
128+
for (RuntimeScalar element : runtimeList) {
129129
try {
130130
// Create $_ argument for the filter subroutine
131131
var_.set(element);
@@ -158,4 +158,70 @@ public static RuntimeList grep(RuntimeList runtimeList, RuntimeScalar perlFilter
158158
return filteredList;
159159
}
160160
}
161+
162+
/**
163+
* Check the elements of this RuntimeArray using a Perl subroutine.
164+
*
165+
* @param runtimeList
166+
* @param perlFilterClosure A RuntimeScalar representing the Perl filter subroutine.
167+
* @return A new RuntimeScalar boolean true if all elements match the filter criteria.
168+
* @throws RuntimeException If the Perl filter subroutine throws an exception.
169+
*/
170+
public static RuntimeList all(RuntimeList runtimeList, RuntimeScalar perlFilterClosure, int ctx) {
171+
RuntimeScalar var_ = getGlobalVariable("main::_");
172+
RuntimeArray filterArgs = new RuntimeArray();
173+
174+
// Iterate over each element in the current RuntimeArray
175+
for (RuntimeScalar element : runtimeList) {
176+
try {
177+
// Create $_ argument for the filter subroutine
178+
var_.set(element);
179+
180+
// Apply the Perl filter subroutine with the argument
181+
RuntimeList result = RuntimeCode.apply(perlFilterClosure, filterArgs, RuntimeContextType.SCALAR);
182+
183+
// Check the result of the filter subroutine
184+
if (!result.getFirst().getBoolean()) {
185+
return scalarFalse.getList();
186+
}
187+
} catch (Exception e) {
188+
// Wrap any exceptions thrown by the filter subroutine in a RuntimeException
189+
throw new RuntimeException(e);
190+
}
191+
}
192+
return scalarTrue.getList();
193+
}
194+
195+
/**
196+
* Check the elements of this RuntimeArray using a Perl subroutine.
197+
*
198+
* @param runtimeList
199+
* @param perlFilterClosure A RuntimeScalar representing the Perl filter subroutine.
200+
* @return A new RuntimeScalar boolean true if any elements match the filter criteria.
201+
* @throws RuntimeException If the Perl filter subroutine throws an exception.
202+
*/
203+
public static RuntimeList any(RuntimeList runtimeList, RuntimeScalar perlFilterClosure, int ctx) {
204+
RuntimeScalar var_ = getGlobalVariable("main::_");
205+
RuntimeArray filterArgs = new RuntimeArray();
206+
207+
// Iterate over each element in the current RuntimeArray
208+
for (RuntimeScalar element : runtimeList) {
209+
try {
210+
// Create $_ argument for the filter subroutine
211+
var_.set(element);
212+
213+
// Apply the Perl filter subroutine with the argument
214+
RuntimeList result = RuntimeCode.apply(perlFilterClosure, filterArgs, RuntimeContextType.SCALAR);
215+
216+
// Check the result of the filter subroutine
217+
if (result.getFirst().getBoolean()) {
218+
return scalarTrue.getList();
219+
}
220+
} catch (Exception e) {
221+
// Wrap any exceptions thrown by the filter subroutine in a RuntimeException
222+
throw new RuntimeException(e);
223+
}
224+
}
225+
return scalarFalse.getList();
226+
}
161227
}

src/main/java/org/perlonjava/operators/OperatorHandler.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,12 @@ public class OperatorHandler {
181181
put("sort", "sort",
182182
"org/perlonjava/operators/ListOperators",
183183
"(Lorg/perlonjava/runtime/RuntimeList;Lorg/perlonjava/runtime/RuntimeScalar;Ljava/lang/String;)Lorg/perlonjava/runtime/RuntimeList;");
184+
put("all", "all",
185+
"org/perlonjava/operators/ListOperators",
186+
"(Lorg/perlonjava/runtime/RuntimeList;Lorg/perlonjava/runtime/RuntimeScalar;I)Lorg/perlonjava/runtime/RuntimeList;");
187+
put("any", "any",
188+
"org/perlonjava/operators/ListOperators",
189+
"(Lorg/perlonjava/runtime/RuntimeList;Lorg/perlonjava/runtime/RuntimeScalar;I)Lorg/perlonjava/runtime/RuntimeList;");
184190

185191
operatorHandlers.put("scalar",
186192
new OperatorHandler("org/perlonjava/runtime/RuntimeDataProvider",

src/main/java/org/perlonjava/parser/CoreOperatorResolver.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ public static Node parseCoreOperator(Parser parser, LexerToken token, int startI
170170
case "sort":
171171
// Handle 'sort' operator
172172
return OperatorParser.parseSort(parser, token);
173-
case "map", "grep":
173+
case "map", "grep", "all", "any":
174174
// Handle 'map' and 'grep' operators
175175
return OperatorParser.parseMapGrep(parser, token);
176176
case "pack":

src/main/java/org/perlonjava/parser/ParsePrimary.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,10 @@ private static Node parseIdentifier(Parser parser, int startIndex, LexerToken to
118118
// Check if the operator is enabled in the current scope
119119
// Some operators require specific features to be enabled
120120
operatorEnabled = switch (operator) {
121+
case "all" ->
122+
parser.ctx.symbolTable.isFeatureCategoryEnabled("keyword_all");
123+
case "any" ->
124+
parser.ctx.symbolTable.isFeatureCategoryEnabled("keyword_any");
121125
case "say", "fc", "state", "evalbytes" ->
122126
parser.ctx.symbolTable.isFeatureCategoryEnabled(operator);
123127
case "__SUB__" ->

0 commit comments

Comments
 (0)