diff --git a/src/main/java/org/perlonjava/frontend/parser/OperatorParser.java b/src/main/java/org/perlonjava/frontend/parser/OperatorParser.java index 6a8a059de..a5920c3c2 100644 --- a/src/main/java/org/perlonjava/frontend/parser/OperatorParser.java +++ b/src/main/java/org/perlonjava/frontend/parser/OperatorParser.java @@ -694,13 +694,22 @@ static OperatorNode parseStat(Parser parser, LexerToken token, int currentIndex) paren = true; } + if (nextToken.text.equals("_")) { + // Handle `stat _` + TokenUtils.consume(parser); + if (paren) { + TokenUtils.consume(parser, OPERATOR, ")"); + } + return new OperatorNode(token.text, + new IdentifierNode("_", parser.tokenIndex), parser.tokenIndex); + } + // stat/lstat: bareword filehandle (typically ALLCAPS) should be treated as a typeglob. // Consume it here, before generic expression parsing can turn it into a subroutine call. if (nextToken.type == IDENTIFIER) { String name = nextToken.text; if (name.matches("^[A-Z_][A-Z0-9_]*$")) { TokenUtils.consume(parser); - // autovivify filehandle and convert to globref GlobalVariable.getGlobalIO(FileHandle.normalizeBarewordHandle(parser, name)); Node fh = FileHandle.parseBarewordHandle(parser, name); Node operand = fh != null ? fh : new IdentifierNode(name, parser.tokenIndex); @@ -710,15 +719,6 @@ static OperatorNode parseStat(Parser parser, LexerToken token, int currentIndex) return new OperatorNode(token.text, operand, currentIndex); } } - if (nextToken.text.equals("_")) { - // Handle `stat _` - TokenUtils.consume(parser); - if (paren) { - TokenUtils.consume(parser, OPERATOR, ")"); - } - return new OperatorNode(token.text, - new IdentifierNode("_", parser.tokenIndex), parser.tokenIndex); - } // Parse optional single argument (or default to $_) // If we've already consumed '(', we must parse a full expression up to ')'. diff --git a/src/main/java/org/perlonjava/runtime/operators/FileTestOperator.java b/src/main/java/org/perlonjava/runtime/operators/FileTestOperator.java index 5037fc64a..635ab2770 100644 --- a/src/main/java/org/perlonjava/runtime/operators/FileTestOperator.java +++ b/src/main/java/org/perlonjava/runtime/operators/FileTestOperator.java @@ -259,7 +259,21 @@ public static RuntimeScalar fileTest(String operator, RuntimeScalar fileHandle) return scalarUndef; } - // For file test operators on file handles, return undef and set EBADF + // Try to use the stored file path for file test + if (fh.filePath != null) { + Path path = fh.filePath; + try { + boolean lstat = operator.equals("-l"); + statForFileTest(fileHandle, path, lstat); + return evaluateFileTest(operator, path, fileHandle); + } catch (Exception e) { + getGlobalVariable("main::!").set(5); + updateLastStat(fileHandle, false, 5); + return scalarUndef; + } + } + + // For non-file handles (stdin, in-memory, socket), return undef and set EBADF getGlobalVariable("main::!").set(9); updateLastStat(fileHandle, false, 9); return scalarUndef; @@ -306,232 +320,211 @@ public static RuntimeScalar fileTest(String operator, RuntimeScalar fileHandle) try { boolean lstat = operator.equals("-l"); statForFileTest(fileHandle, path, lstat); - return switch (operator) { - case "-r" -> { - // Check if file is readable - if (!Files.exists(path)) { - getGlobalVariable("main::!").set(2); // ENOENT - yield scalarUndef; - } - getGlobalVariable("main::!").set(0); // Clear error - yield getScalarBoolean(Files.isReadable(path)); - } - case "-w" -> { - // Check if file is writable - if (!Files.exists(path)) { - getGlobalVariable("main::!").set(2); // ENOENT - yield scalarUndef; - } - getGlobalVariable("main::!").set(0); // Clear error - yield getScalarBoolean(Files.isWritable(path)); + return evaluateFileTest(operator, path, fileHandle); + } catch (IOException e) { + getGlobalVariable("main::!").set(2); + updateLastStat(fileHandle, false, 2); + return scalarUndef; + } + } + + private static RuntimeScalar evaluateFileTest(String operator, Path path, RuntimeScalar fileHandle) throws IOException { + String filename = path.toString(); + return switch (operator) { + case "-r" -> { + if (!Files.exists(path)) { + getGlobalVariable("main::!").set(2); + yield scalarUndef; } - case "-x" -> { - // Check if file is executable - if (!Files.exists(path)) { - getGlobalVariable("main::!").set(2); // ENOENT - yield scalarUndef; - } - getGlobalVariable("main::!").set(0); // Clear error - yield getScalarBoolean(Files.isExecutable(path)); + getGlobalVariable("main::!").set(0); + yield getScalarBoolean(Files.isReadable(path)); + } + case "-w" -> { + if (!Files.exists(path)) { + getGlobalVariable("main::!").set(2); + yield scalarUndef; } - case "-e" -> { - // Check if file exists - boolean exists = Files.exists(path); - if (!exists) { - getGlobalVariable("main::!").set(2); // ENOENT - yield scalarUndef; - } - getGlobalVariable("main::!").set(0); // Clear error - yield scalarTrue; + getGlobalVariable("main::!").set(0); + yield getScalarBoolean(Files.isWritable(path)); + } + case "-x" -> { + if (!Files.exists(path)) { + getGlobalVariable("main::!").set(2); + yield scalarUndef; } - case "-z" -> { - // Check if file is empty (zero size) - if (!Files.exists(path)) { - getGlobalVariable("main::!").set(2); // ENOENT - yield scalarUndef; - } - getGlobalVariable("main::!").set(0); // Clear error - yield getScalarBoolean(Files.size(path) == 0); + getGlobalVariable("main::!").set(0); + yield getScalarBoolean(Files.isExecutable(path)); + } + case "-e" -> { + boolean exists = Files.exists(path); + if (!exists) { + getGlobalVariable("main::!").set(2); + yield scalarUndef; } - case "-s" -> { - // Return file size if non-zero, otherwise return false - if (!lastStatOk) { - yield scalarUndef; - } - long size = lastBasicAttr.size(); - yield size > 0 ? new RuntimeScalar(size) : RuntimeScalarCache.scalarZero; + getGlobalVariable("main::!").set(0); + yield scalarTrue; + } + case "-z" -> { + if (!Files.exists(path)) { + getGlobalVariable("main::!").set(2); + yield scalarUndef; } - case "-f" -> { - // Check if path is a regular file - if (!Files.exists(path)) { - getGlobalVariable("main::!").set(2); // ENOENT - yield scalarUndef; - } - getGlobalVariable("main::!").set(0); // Clear error - yield getScalarBoolean(Files.isRegularFile(path)); + getGlobalVariable("main::!").set(0); + yield getScalarBoolean(Files.size(path) == 0); + } + case "-s" -> { + if (!lastStatOk) { + yield scalarUndef; } - case "-d" -> { - // Check if path is a directory - if (!Files.exists(path)) { - getGlobalVariable("main::!").set(2); // ENOENT - yield scalarUndef; - } - getGlobalVariable("main::!").set(0); // Clear error - yield getScalarBoolean(Files.isDirectory(path)); + long size = lastBasicAttr.size(); + yield size > 0 ? new RuntimeScalar(size) : RuntimeScalarCache.scalarZero; + } + case "-f" -> { + if (!Files.exists(path)) { + getGlobalVariable("main::!").set(2); + yield scalarUndef; } - case "-l" -> { - // Check if path is a symbolic link - if (!lastStatOk) { - yield scalarUndef; - } - yield getScalarBoolean(lastBasicAttr.isSymbolicLink()); + getGlobalVariable("main::!").set(0); + yield getScalarBoolean(Files.isRegularFile(path)); + } + case "-d" -> { + if (!Files.exists(path)) { + getGlobalVariable("main::!").set(2); + yield scalarUndef; } - case "-o" -> { - // Check if file is owned by the effective user id (approximate with current user) - if (!Files.exists(path)) { - getGlobalVariable("main::!").set(2); // ENOENT - yield scalarUndef; - } - getGlobalVariable("main::!").set(0); // Clear error - UserPrincipal owner = Files.getOwner(path); - UserPrincipal currentUser = path.getFileSystem().getUserPrincipalLookupService() - .lookupPrincipalByName(System.getProperty("user.name")); - yield getScalarBoolean(owner.equals(currentUser)); + getGlobalVariable("main::!").set(0); + yield getScalarBoolean(Files.isDirectory(path)); + } + case "-l" -> { + if (!lastStatOk) { + yield scalarUndef; } - case "-p" -> { - // Approximate check for named pipe (FIFO) - if (!Files.exists(path)) { - getGlobalVariable("main::!").set(2); // ENOENT - yield scalarUndef; - } - getGlobalVariable("main::!").set(0); // Clear error - yield getScalarBoolean(Files.isRegularFile(path) && filename.endsWith(".fifo")); + yield getScalarBoolean(lastBasicAttr.isSymbolicLink()); + } + case "-o" -> { + if (!Files.exists(path)) { + getGlobalVariable("main::!").set(2); + yield scalarUndef; } - case "-S" -> { - // Approximate check for socket - if (!Files.exists(path)) { - getGlobalVariable("main::!").set(2); // ENOENT - yield scalarUndef; - } - getGlobalVariable("main::!").set(0); // Clear error - yield getScalarBoolean(Files.isRegularFile(path) && filename.endsWith(".sock")); + getGlobalVariable("main::!").set(0); + UserPrincipal owner = Files.getOwner(path); + UserPrincipal currentUser = path.getFileSystem().getUserPrincipalLookupService() + .lookupPrincipalByName(System.getProperty("user.name")); + yield getScalarBoolean(owner.equals(currentUser)); + } + case "-p" -> { + if (!Files.exists(path)) { + getGlobalVariable("main::!").set(2); + yield scalarUndef; } - case "-b" -> { - // Approximate check for block special file - if (!Files.exists(path)) { - getGlobalVariable("main::!").set(2); // ENOENT - yield scalarUndef; - } - getGlobalVariable("main::!").set(0); // Clear error - yield getScalarBoolean(Files.isRegularFile(path) && filename.startsWith("/dev/")); + getGlobalVariable("main::!").set(0); + yield getScalarBoolean(Files.isRegularFile(path) && filename.endsWith(".fifo")); + } + case "-S" -> { + if (!Files.exists(path)) { + getGlobalVariable("main::!").set(2); + yield scalarUndef; } - case "-c" -> { - // Approximate check for character special file - if (!Files.exists(path)) { - getGlobalVariable("main::!").set(2); // ENOENT - yield scalarUndef; - } - getGlobalVariable("main::!").set(0); // Clear error - yield getScalarBoolean(Files.isRegularFile(path) && filename.startsWith("/dev/")); + getGlobalVariable("main::!").set(0); + yield getScalarBoolean(Files.isRegularFile(path) && filename.endsWith(".sock")); + } + case "-b" -> { + if (!Files.exists(path)) { + getGlobalVariable("main::!").set(2); + yield scalarUndef; } - case "-u" -> { - // Check if setuid bit is set - if (!Files.exists(path)) { - getGlobalVariable("main::!").set(2); // ENOENT - yield scalarUndef; - } - getGlobalVariable("main::!").set(0); // Clear error - yield getScalarBoolean((Files.getPosixFilePermissions(path).contains(PosixFilePermission.OWNER_EXECUTE))); + getGlobalVariable("main::!").set(0); + yield getScalarBoolean(Files.isRegularFile(path) && filename.startsWith("/dev/")); + } + case "-c" -> { + if (!Files.exists(path)) { + getGlobalVariable("main::!").set(2); + yield scalarUndef; } - case "-g" -> { - // Check if setgid bit is set - if (!Files.exists(path)) { - getGlobalVariable("main::!").set(2); // ENOENT - yield scalarUndef; - } - getGlobalVariable("main::!").set(0); // Clear error - yield getScalarBoolean((Files.getPosixFilePermissions(path).contains(PosixFilePermission.GROUP_EXECUTE))); + getGlobalVariable("main::!").set(0); + yield getScalarBoolean(Files.isRegularFile(path) && filename.startsWith("/dev/")); + } + case "-u" -> { + if (!Files.exists(path)) { + getGlobalVariable("main::!").set(2); + yield scalarUndef; } - case "-k" -> { - // Approximate check for sticky bit (using others execute permission) - if (!Files.exists(path)) { - getGlobalVariable("main::!").set(2); // ENOENT - yield scalarUndef; - } - getGlobalVariable("main::!").set(0); // Clear error - yield getScalarBoolean((Files.getPosixFilePermissions(path).contains(PosixFilePermission.OTHERS_EXECUTE))); + getGlobalVariable("main::!").set(0); + yield getScalarBoolean((Files.getPosixFilePermissions(path).contains(PosixFilePermission.OWNER_EXECUTE))); + } + case "-g" -> { + if (!Files.exists(path)) { + getGlobalVariable("main::!").set(2); + yield scalarUndef; } - case "-T", "-B" -> { - // Check if file is text (-T) or binary (-B) - if (!Files.exists(path)) { - getGlobalVariable("main::!").set(2); // ENOENT - yield scalarUndef; - } - getGlobalVariable("main::!").set(0); // Clear error - yield isTextOrBinary(path, operator.equals("-T")); + getGlobalVariable("main::!").set(0); + yield getScalarBoolean((Files.getPosixFilePermissions(path).contains(PosixFilePermission.GROUP_EXECUTE))); + } + case "-k" -> { + if (!Files.exists(path)) { + getGlobalVariable("main::!").set(2); + yield scalarUndef; } - case "-M", "-A", "-C" -> { - // Get file time difference for modification (-M), access (-A), or creation (-C) time - if (!Files.exists(path)) { - getGlobalVariable("main::!").set(2); // ENOENT - yield scalarUndef; - } - getGlobalVariable("main::!").set(0); // Clear error - yield getFileTimeDifference(path, operator); + getGlobalVariable("main::!").set(0); + yield getScalarBoolean((Files.getPosixFilePermissions(path).contains(PosixFilePermission.OTHERS_EXECUTE))); + } + case "-T", "-B" -> { + if (!Files.exists(path)) { + getGlobalVariable("main::!").set(2); + yield scalarUndef; } - case "-R" -> { - // Check if file is readable by the real user ID - if (!Files.exists(path)) { - getGlobalVariable("main::!").set(2); // ENOENT - yield scalarUndef; - } - getGlobalVariable("main::!").set(0); // Clear error - yield getScalarBoolean(Files.isReadable(path)); + getGlobalVariable("main::!").set(0); + yield isTextOrBinary(path, operator.equals("-T")); + } + case "-M", "-A", "-C" -> { + if (!Files.exists(path)) { + getGlobalVariable("main::!").set(2); + yield scalarUndef; } - case "-W" -> { - // Check if file is writable by the real user ID - if (!Files.exists(path)) { - getGlobalVariable("main::!").set(2); // ENOENT - yield scalarUndef; - } - getGlobalVariable("main::!").set(0); // Clear error - yield getScalarBoolean(Files.isWritable(path)); + getGlobalVariable("main::!").set(0); + yield getFileTimeDifference(path, operator); + } + case "-R" -> { + if (!Files.exists(path)) { + getGlobalVariable("main::!").set(2); + yield scalarUndef; } - case "-X" -> { - // Check if file is executable by the real user ID - if (!Files.exists(path)) { - getGlobalVariable("main::!").set(2); // ENOENT - yield scalarUndef; - } - getGlobalVariable("main::!").set(0); // Clear error - yield getScalarBoolean(Files.isExecutable(path)); + getGlobalVariable("main::!").set(0); + yield getScalarBoolean(Files.isReadable(path)); + } + case "-W" -> { + if (!Files.exists(path)) { + getGlobalVariable("main::!").set(2); + yield scalarUndef; } - case "-O" -> { - // Check if file is owned by the current user - if (!Files.exists(path)) { - getGlobalVariable("main::!").set(2); // ENOENT - yield scalarUndef; - } - getGlobalVariable("main::!").set(0); // Clear error - UserPrincipal owner = Files.getOwner(path); - UserPrincipal currentUser = path.getFileSystem().getUserPrincipalLookupService() - .lookupPrincipalByName(System.getProperty("user.name")); - yield getScalarBoolean(owner.equals(currentUser)); + getGlobalVariable("main::!").set(0); + yield getScalarBoolean(Files.isWritable(path)); + } + case "-X" -> { + if (!Files.exists(path)) { + getGlobalVariable("main::!").set(2); + yield scalarUndef; } - case "-t" -> { - // -t on a string filename is an error in Perl (expects a filehandle) - // Set $! = EBADF and return undef - getGlobalVariable("main::!").set(9); // EBADF + getGlobalVariable("main::!").set(0); + yield getScalarBoolean(Files.isExecutable(path)); + } + case "-O" -> { + if (!Files.exists(path)) { + getGlobalVariable("main::!").set(2); yield scalarUndef; } - default -> throw new UnsupportedOperationException("Unsupported file test operator: " + operator); - }; - } catch (IOException e) { - // Set error message in global variable and return false/undef - getGlobalVariable("main::!").set(2); // ENOENT for most file operations - updateLastStat(fileHandle, false, 2); - return scalarUndef; - } + getGlobalVariable("main::!").set(0); + UserPrincipal owner = Files.getOwner(path); + UserPrincipal currentUser = path.getFileSystem().getUserPrincipalLookupService() + .lookupPrincipalByName(System.getProperty("user.name")); + yield getScalarBoolean(owner.equals(currentUser)); + } + case "-t" -> { + getGlobalVariable("main::!").set(9); + yield scalarUndef; + } + default -> throw new UnsupportedOperationException("Unsupported file test operator: " + operator); + }; } public static RuntimeScalar chainedFileTest(String[] operators, RuntimeScalar fileHandle) { diff --git a/src/main/java/org/perlonjava/runtime/operators/Stat.java b/src/main/java/org/perlonjava/runtime/operators/Stat.java index cfce73aa6..77b558879 100644 --- a/src/main/java/org/perlonjava/runtime/operators/Stat.java +++ b/src/main/java/org/perlonjava/runtime/operators/Stat.java @@ -126,8 +126,12 @@ public static RuntimeList stat(RuntimeScalar arg) { return res; // Return empty list } + // Try to stat via the stored file path + if (fh.filePath != null) { + return statPath(arg, fh.filePath, res, false); + } + // For in-memory file handles (like PerlIO::scalar), we can't stat them - // They should return EBADF getGlobalVariable("main::!").set(9); updateLastStat(arg, false, 9, false); return res; @@ -135,33 +139,34 @@ public static RuntimeList stat(RuntimeScalar arg) { // Handle string arguments String filename = arg.toString(); + Path path = resolvePath(filename); + return statPath(arg, path, res, false); + } - // Handle regular filenames + private static RuntimeList statPath(RuntimeScalar arg, Path path, RuntimeList res, boolean lstat) { try { - Path path = resolvePath(filename); - - // Basic file attributes (similar to some Perl stat fields) - BasicFileAttributes basicAttr = Files.readAttributes(path, BasicFileAttributes.class); - - // POSIX file attributes (for Unix-like systems) - PosixFileAttributes posixAttr = Files.readAttributes(path, PosixFileAttributes.class); + BasicFileAttributes basicAttr; + PosixFileAttributes posixAttr; + if (lstat) { + basicAttr = Files.readAttributes(path, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS); + posixAttr = Files.readAttributes(path, PosixFileAttributes.class, LinkOption.NOFOLLOW_LINKS); + } else { + basicAttr = Files.readAttributes(path, BasicFileAttributes.class); + posixAttr = Files.readAttributes(path, PosixFileAttributes.class); + } lastBasicAttr = basicAttr; lastPosixAttr = posixAttr; statInternal(res, basicAttr, posixAttr); - // Clear $! on success getGlobalVariable("main::!").set(0); - updateLastStat(arg, true, 0, false); + updateLastStat(arg, true, 0, lstat); } catch (NoSuchFileException e) { - // Set $! to ENOENT (No such file or directory) = 2 getGlobalVariable("main::!").set(2); - updateLastStat(arg, false, 2, false); + updateLastStat(arg, false, 2, lstat); } catch (IOException e) { - // Returns the empty list if "stat" fails. - // Set a generic error code for other IO errors - getGlobalVariable("main::!").set(5); // EIO (Input/output error) - updateLastStat(arg, false, 5, false); + getGlobalVariable("main::!").set(5); + updateLastStat(arg, false, 5, lstat); } return res; } @@ -191,8 +196,12 @@ public static RuntimeList lstat(RuntimeScalar arg) { return res; // Return empty list } + // Try to lstat via the stored file path + if (fh.filePath != null) { + return statPath(arg, fh.filePath, res, true); + } + // For in-memory file handles (like PerlIO::scalar), we can't lstat them - // They should return EBADF getGlobalVariable("main::!").set(9); updateLastStat(arg, false, 9, true); return res; @@ -200,37 +209,8 @@ public static RuntimeList lstat(RuntimeScalar arg) { // Handle string arguments String filename = arg.toString(); - - // Handle regular filenames - try { - Path path = resolvePath(filename); - - // Basic attributes without following symlink - BasicFileAttributes basicAttr = Files.readAttributes(path, - BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS); - - // POSIX attributes without following symlink - PosixFileAttributes posixAttr = Files.readAttributes(path, - PosixFileAttributes.class, LinkOption.NOFOLLOW_LINKS); - - lastBasicAttr = basicAttr; - lastPosixAttr = posixAttr; - - statInternal(res, basicAttr, posixAttr); - // Clear $! on success - getGlobalVariable("main::!").set(0); - updateLastStat(arg, true, 0, true); - } catch (NoSuchFileException e) { - // Set $! to ENOENT (No such file or directory) = 2 - getGlobalVariable("main::!").set(2); - updateLastStat(arg, false, 2, true); - } catch (IOException e) { - // Returns the empty list if "lstat" fails. - // Set a generic error code for other IO errors - getGlobalVariable("main::!").set(5); // EIO (Input/output error) - updateLastStat(arg, false, 5, true); - } - return res; + Path path = resolvePath(filename); + return statPath(arg, path, res, true); } public static void statInternal(RuntimeList res, BasicFileAttributes basicAttr, PosixFileAttributes posixAttr) { diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeIO.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeIO.java index 1d8d006ff..4021d2bbf 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeIO.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeIO.java @@ -154,6 +154,13 @@ protected boolean removeEldestEntry(Map.Entry eldest) { */ public DirectoryIO directoryIO; + /** + * The file system path associated with this I/O handle, if opened from a file. + * Used by stat() and file test operators (-f, -s, etc.) on filehandles. + * Null for non-file handles (STDIN/STDOUT/STDERR, pipes, sockets, in-memory). + */ + public Path filePath; + /** * The name of the glob that owns this IO handle (e.g., "main::STDOUT"). * Used for stringification when the filehandle is used in string context. @@ -373,6 +380,7 @@ public static RuntimeIO open(String fileName, String mode) { // Initialize ioHandle with CustomFileChannel fh.ioHandle = new CustomFileChannel(filePath, options); + fh.filePath = filePath; // Add the handle to the LRU cache addHandle(fh.ioHandle);