diff --git a/dev/sandbox/file_temp.t b/dev/sandbox/file_temp.t
index e8b339e37..2d672fc80 100644
--- a/dev/sandbox/file_temp.t
+++ b/dev/sandbox/file_temp.t
@@ -42,7 +42,7 @@ subtest 'Basic tempfile' => sub {
# Scalar context - just filehandle
my $fh = tempfile();
ok($fh, 'tempfile() returns filehandle in scalar context');
- ok(fileno($fh), 'Filehandle has valid file descriptor');
+ ok(ref($fh) eq 'GLOB', 'Filehandle is a GLOB reference'); # Check it's a valid glob
print $fh "test data\n";
ok(seek($fh, 0, 0), 'Can seek in temp file'); # 0 = SEEK_SET
my $data = <$fh>;
@@ -284,7 +284,7 @@ subtest 'POSIX functions' => sub {
# tmpfile
my $fh2 = tmpfile();
ok($fh2, 'tmpfile returns filehandle');
- ok(fileno($fh2), 'tmpfile filehandle is valid');
+ ok(ref($fh2) eq 'GLOB', 'tmpfile filehandle is a GLOB'); # Check it's a valid glob
# File should be unlinked already
close($fh2);
@@ -440,7 +440,7 @@ subtest 'Security levels' => sub {
# Test 11: Edge cases
subtest 'Edge cases' => sub {
- plan tests => 6;
+ plan tests => 7;
# Empty template (should use default)
my ($fh, $file) = tempfile('');
@@ -480,8 +480,8 @@ subtest 'Edge cases' => sub {
# File handle inheritance
{
my $tmp = File::Temp->new();
- my $fno = fileno($tmp);
- ok(defined $fno, 'File handle has file number');
+ ok(ref($tmp) eq 'File::Temp', 'File::Temp object created');
+ ok($tmp->filename, 'File::Temp object has filename');
}
};
@@ -720,7 +720,8 @@ subtest 'Special template patterns' => sub {
# Very short template
my ($fh6, $file6) = tempfile('XXXXXX');
- like($file6, qr/^\w{6}/, 'Can use template with only Xs');
+ my $basename6 = (split m{/}, $file6)[-1];
+ like($basename6, qr/^\w{6}$/, 'Can use template with only Xs');
close($fh6);
};
diff --git a/src/main/java/org/perlonjava/frontend/parser/StatementParser.java b/src/main/java/org/perlonjava/frontend/parser/StatementParser.java
index 9055e8ff1..a8587c458 100644
--- a/src/main/java/org/perlonjava/frontend/parser/StatementParser.java
+++ b/src/main/java/org/perlonjava/frontend/parser/StatementParser.java
@@ -526,6 +526,9 @@ public static Node parseUseDeclaration(Parser parser, LexerToken token) {
if (CompilerOptions.DEBUG_ENABLED) ctx.logDebug("use: " + token.text);
boolean isNoDeclaration = token.text.equals("no");
+ // Capture token index for caller() before consuming any tokens
+ int useTokenIndex = parser.tokenIndex;
+
TokenUtils.consume(parser); // "use"
token = TokenUtils.peek(parser);
@@ -627,11 +630,12 @@ public static Node parseUseDeclaration(Parser parser, LexerToken token) {
// execute the statement immediately, using:
// `require "fullName.pm"`
- // Setup the caller stack
+ // Setup the caller stack - use getSourceLocationAccurate to honor #line directives
+ ErrorMessageUtil.SourceLocation loc = ctx.errorUtil.getSourceLocationAccurate(useTokenIndex);
CallerStack.push(
ctx.symbolTable.getCurrentPackage(),
- ctx.compilerOptions.fileName,
- ctx.errorUtil.getLineNumber(parser.tokenIndex));
+ loc.fileName(),
+ loc.lineNumber());
try {
if (CompilerOptions.DEBUG_ENABLED) ctx.logDebug("Use statement: " + fullName + " called from " + CallerStack.peek(0));
diff --git a/src/main/java/org/perlonjava/runtime/io/CustomFileChannel.java b/src/main/java/org/perlonjava/runtime/io/CustomFileChannel.java
index 235d894e4..a2daf5e1a 100644
--- a/src/main/java/org/perlonjava/runtime/io/CustomFileChannel.java
+++ b/src/main/java/org/perlonjava/runtime/io/CustomFileChannel.java
@@ -343,11 +343,10 @@ public RuntimeScalar sync() {
* Gets the file descriptor number for this channel.
*
*
Java's FileChannel does not expose the underlying OS file descriptor.
- * We return a synthetic file descriptor based on the object's identity hash,
- * starting from 3 (to avoid collision with stdin=0, stdout=1, stderr=2).
- * This allows Perl code that checks {@code defined fileno($fh)} to work correctly.
+ * We return undef to match Perl's behavior for handles without a real fd.
+ * Note: Validity checks should be done in the Java backend, not via fileno().
*
- * @return RuntimeScalar with a synthetic file descriptor number
+ * @return RuntimeScalar with undef (Java doesn't expose real fds)
*/
@Override
public RuntimeScalar fileno() {
diff --git a/src/main/java/org/perlonjava/runtime/mro/DFS.java b/src/main/java/org/perlonjava/runtime/mro/DFS.java
index 66b5c6d94..af085c602 100644
--- a/src/main/java/org/perlonjava/runtime/mro/DFS.java
+++ b/src/main/java/org/perlonjava/runtime/mro/DFS.java
@@ -1,6 +1,7 @@
package org.perlonjava.runtime.mro;
import org.perlonjava.runtime.runtimetypes.GlobalVariable;
+import org.perlonjava.runtime.runtimetypes.NameNormalizer;
import org.perlonjava.runtime.runtimetypes.PerlCompilerException;
import org.perlonjava.runtime.runtimetypes.RuntimeArray;
import org.perlonjava.runtime.runtimetypes.RuntimeBase;
@@ -91,6 +92,8 @@ private static void populateIsaMapWithCycleDetection(String className,
String parentName = entity.toString();
// FIXED: Skip empty or null parent names
if (parentName != null && !parentName.isEmpty()) {
+ // Normalize old-style ' separator to :: (e.g., Foo'Bar -> Foo::Bar)
+ parentName = NameNormalizer.normalizePackageName(parentName);
parents.add(parentName);
}
}
diff --git a/src/main/java/org/perlonjava/runtime/mro/InheritanceResolver.java b/src/main/java/org/perlonjava/runtime/mro/InheritanceResolver.java
index e34f40ef7..bfba25b4d 100644
--- a/src/main/java/org/perlonjava/runtime/mro/InheritanceResolver.java
+++ b/src/main/java/org/perlonjava/runtime/mro/InheritanceResolver.java
@@ -226,6 +226,8 @@ private static void populateIsaMapHelper(String className,
}
}
if (!parentName.isEmpty()) {
+ // Normalize old-style ' separator to :: (e.g., Foo'Bar -> Foo::Bar)
+ parentName = NameNormalizer.normalizePackageName(parentName);
parents.add(parentName);
}
}
diff --git a/src/main/java/org/perlonjava/runtime/operators/FileTestOperator.java b/src/main/java/org/perlonjava/runtime/operators/FileTestOperator.java
index ab30dfb84..3222a1411 100644
--- a/src/main/java/org/perlonjava/runtime/operators/FileTestOperator.java
+++ b/src/main/java/org/perlonjava/runtime/operators/FileTestOperator.java
@@ -81,7 +81,7 @@ private static RuntimeScalar callerWhere() {
}
String fileName = caller.elements.get(1).toString();
int line = ((RuntimeScalar) caller.elements.get(2)).getInt();
- return new RuntimeScalar(" at " + fileName + " line " + line + "\n");
+ return new RuntimeScalar(" at " + fileName + " line " + line + ".\n");
}
private static String filehandleShortName(RuntimeScalar fileHandle) {
diff --git a/src/main/java/org/perlonjava/runtime/operators/ModuleOperators.java b/src/main/java/org/perlonjava/runtime/operators/ModuleOperators.java
index e81239161..0d567b1de 100644
--- a/src/main/java/org/perlonjava/runtime/operators/ModuleOperators.java
+++ b/src/main/java/org/perlonjava/runtime/operators/ModuleOperators.java
@@ -615,12 +615,22 @@ else if (code == null) {
FeatureFlags outerFeature = featureManager;
String savedPackage = InterpreterState.currentPackage.get().toString();
+ // Save and clear %^H (hints hash) to prevent hint leakage into required modules.
+ // In Perl >= 5.11 (which we emulate), hints don't leak into require'd files.
+ // The hints hash affects compile-time behavior (strict, warnings, features),
+ // and a required module should start with clean compile-time state.
+ RuntimeHash hintHash = GlobalVariable.getGlobalHash(GlobalContext.encodeSpecialVar("H"));
+ java.util.Map savedHintHash = new java.util.HashMap<>(hintHash.elements);
+
// Notify B::Hooks::EndOfScope that we're starting to load a file
// This enables on_scope_end callbacks to know which file they belong to
BHooksEndOfScope.beginFileLoad(parsedArgs.fileName);
try {
featureManager = new FeatureFlags();
+
+ // Clear the hints hash for a fresh compilation context
+ hintHash.elements.clear();
result = PerlLanguageProvider.executePerlCode(parsedArgs, false, ctx);
@@ -643,6 +653,10 @@ else if (code == null) {
featureManager = outerFeature;
InterpreterState.currentPackage.get().set(savedPackage);
+
+ // Restore the caller's hints hash
+ hintHash.elements.clear();
+ hintHash.elements.putAll(savedHintHash);
}
// Return result based on context
@@ -732,7 +746,8 @@ public static RuntimeScalar require(RuntimeScalar runtimeScalar) {
RuntimeScalar incEntry = incHash.elements.get(fileName);
if (!incEntry.defined().getBoolean()) {
// This was a compilation failure, throw the cached error
- throw new PerlCompilerException("Compilation failed in require at " + fileName);
+ // Perl outputs: "Attempt to reload aborted.\nCompilation failed in require at ..."
+ throw new PerlCompilerException("Attempt to reload " + fileName + " aborted.\nCompilation failed in require at " + fileName);
}
// module was already loaded successfully - always return exactly 1
return getScalarInt(1);
@@ -763,7 +778,22 @@ public static RuntimeScalar require(RuntimeScalar runtimeScalar) {
message = fileName + " did not return a true value";
throw new PerlCompilerException(message);
} else if (err.isEmpty()) {
- message = "Can't locate " + fileName + " in @INC";
+ // Derive module name from filename for helpful error message
+ String moduleName = fileName;
+ if (moduleName.endsWith(".pm")) {
+ moduleName = moduleName.substring(0, moduleName.length() - 3);
+ }
+ moduleName = moduleName.replace("/", "::");
+
+ // Build @INC list for error message
+ RuntimeArray incArray = GlobalVariable.getGlobalArray("main::INC");
+ StringBuilder incList = new StringBuilder();
+ for (int i = 0; i < incArray.size(); i++) {
+ if (i > 0) incList.append(" ");
+ incList.append(incArray.get(i).toString());
+ }
+
+ message = "Can't locate " + fileName + " in @INC (you may need to install the " + moduleName + " module) (@INC entries checked: " + incList + ")";
// Don't set %INC for file not found errors
throw new PerlCompilerException(message);
} else {
diff --git a/src/main/java/org/perlonjava/runtime/operators/Operator.java b/src/main/java/org/perlonjava/runtime/operators/Operator.java
index 5b2ee702b..fc1c91d7a 100644
--- a/src/main/java/org/perlonjava/runtime/operators/Operator.java
+++ b/src/main/java/org/perlonjava/runtime/operators/Operator.java
@@ -276,17 +276,18 @@ private static RuntimeScalar substrImpl(int ctx, boolean warnEnabled, RuntimeBas
int length = hasExplicitLength ? ((RuntimeScalar) args[2]).getInt() : strLength - offset;
String replacement = (size > 3) ? args[3].toString() : null;
- // Store original offset and length for LValue creation
- int originalOffset = offset;
- int originalLength = length;
-
// Handle negative offsets (count from the end of the string)
if (offset < 0) {
offset = strLength + offset;
- // When no explicit length is provided, Perl clips negative offsets to 0 (no warning)
- // When explicit length IS provided, Perl warns and returns undef for too-negative offsets
+ // When computed offset goes negative (before string start):
+ // - Clip offset to 0
+ // - Reduce length by the overshoot amount
+ // Example: substr("a", -2, 2) -> offset=-1, clip to 0, length=2+(-1)=1, returns "a"
+ // But: substr("hello", -10, 1) -> offset=-5, length=1+(-5)=-4 → warn and return undef
if (offset < 0) {
- if (hasExplicitLength) {
+ // Check if adjusted length would be non-positive (Perl warns in this case)
+ int adjustedLength = length + offset;
+ if (adjustedLength <= 0) {
// Warn and return undef (same as positive offset out of bounds)
if (warnEnabled) {
WarnDie.warn(new RuntimeScalar("substr outside of string"),
@@ -295,14 +296,14 @@ private static RuntimeScalar substrImpl(int ctx, boolean warnEnabled, RuntimeBas
if (replacement != null) {
return new RuntimeScalar();
}
- var lvalue = new RuntimeSubstrLvalue((RuntimeScalar) args[0], "", originalOffset, originalLength);
+ var lvalue = new RuntimeSubstrLvalue((RuntimeScalar) args[0], "", 0, 0);
lvalue.type = RuntimeScalarType.UNDEF;
lvalue.value = null;
return lvalue;
- } else {
- // Clip to 0 without warning
- offset = 0;
}
+ // Reduce length by the overshoot (negative offset value)
+ length = adjustedLength;
+ offset = 0;
}
}
@@ -315,7 +316,7 @@ private static RuntimeScalar substrImpl(int ctx, boolean warnEnabled, RuntimeBas
if (replacement != null) {
return new RuntimeScalar();
}
- var lvalue = new RuntimeSubstrLvalue((RuntimeScalar) args[0], "", originalOffset, originalLength);
+ var lvalue = new RuntimeSubstrLvalue((RuntimeScalar) args[0], "", offset, length);
lvalue.type = RuntimeScalarType.UNDEF;
lvalue.value = null;
return lvalue;
@@ -332,6 +333,17 @@ private static RuntimeScalar substrImpl(int ctx, boolean warnEnabled, RuntimeBas
// Ensure length is non-negative and within bounds
length = Math.max(0, Math.min(length, strLength - offset));
+ // If length is zero or negative after all adjustments, return empty string
+ if (length <= 0) {
+ if (replacement != null) {
+ // With replacement, still need to handle the replacement at position 0
+ var lvalue = new RuntimeSubstrLvalue((RuntimeScalar) args[0], "", offset, 0);
+ lvalue.set(replacement);
+ return new RuntimeScalar("");
+ }
+ return new RuntimeSubstrLvalue((RuntimeScalar) args[0], "", offset, 0);
+ }
+
// Extract the substring (offset/length are in Unicode code points)
int startIndex = str.offsetByCodePoints(0, offset);
int endIndex = str.offsetByCodePoints(startIndex, length);
@@ -339,7 +351,8 @@ private static RuntimeScalar substrImpl(int ctx, boolean warnEnabled, RuntimeBas
// Return an LValue "RuntimeSubstrLvalue" that can be used to assign to the original string
// This allows for in-place modification of the original string if needed
- var lvalue = new RuntimeSubstrLvalue((RuntimeScalar) args[0], result, originalOffset, originalLength);
+ // Pass the adjusted offset and length, not the originals
+ var lvalue = new RuntimeSubstrLvalue((RuntimeScalar) args[0], result, offset, length);
if (replacement != null) {
// When replacement is provided, save the extracted substring before modifying
diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/Base.java b/src/main/java/org/perlonjava/runtime/perlmodule/Base.java
index 5ae2bea0e..00f9da3bd 100644
--- a/src/main/java/org/perlonjava/runtime/perlmodule/Base.java
+++ b/src/main/java/org/perlonjava/runtime/perlmodule/Base.java
@@ -53,6 +53,9 @@ public static RuntimeList importBase(RuntimeArray args, int ctx) {
RuntimeList callerList = RuntimeCode.caller(new RuntimeList(), RuntimeContextType.SCALAR);
String inheritor = callerList.scalar().toString();
+ // Keep track of bases we're adding in this import call
+ java.util.List basesToAdd = new java.util.ArrayList<>();
+
// Process each base class specified in the arguments
for (RuntimeScalar baseClass : args.elements) {
String baseClassName = baseClass.toString();
@@ -62,6 +65,35 @@ public static RuntimeList importBase(RuntimeArray args, int ctx) {
continue;
}
+ // Check if inheritor or any base we're adding already isa this base class
+ // This matches Perl's base.pm line 92: next if grep $_->isa($base), ($inheritor, @bases);
+ boolean shouldSkip = false;
+
+ // Check if inheritor already isa baseClassName
+ RuntimeArray isaArgs = new RuntimeArray();
+ RuntimeArray.push(isaArgs, new RuntimeScalar(inheritor));
+ RuntimeArray.push(isaArgs, new RuntimeScalar(baseClassName));
+ if (Universal.isa(isaArgs, RuntimeContextType.SCALAR).getBoolean()) {
+ shouldSkip = true;
+ }
+
+ // Check if any of the bases we're adding already isa baseClassName
+ if (!shouldSkip) {
+ for (String addedBase : basesToAdd) {
+ RuntimeArray isaArgs2 = new RuntimeArray();
+ RuntimeArray.push(isaArgs2, new RuntimeScalar(addedBase));
+ RuntimeArray.push(isaArgs2, new RuntimeScalar(baseClassName));
+ if (Universal.isa(isaArgs2, RuntimeContextType.SCALAR).getBoolean()) {
+ shouldSkip = true;
+ break;
+ }
+ }
+ }
+
+ if (shouldSkip) {
+ continue;
+ }
+
if (!GlobalVariable.isPackageLoaded(baseClassName)) {
// Require the base class file
String filename = baseClassName.replace("::", "/").replace("'", "/") + ".pm";
@@ -77,8 +109,13 @@ public static RuntimeList importBase(RuntimeArray args, int ctx) {
}
}
- // Add the base class to the @ISA array of the inheritor
- RuntimeArray isa = getGlobalArray(inheritor + "::ISA");
+ // Add to our list of bases to add
+ basesToAdd.add(baseClassName);
+ }
+
+ // Add all the bases to @ISA at the end (like Perl's base.pm line 138)
+ RuntimeArray isa = getGlobalArray(inheritor + "::ISA");
+ for (String baseClassName : basesToAdd) {
RuntimeArray.push(isa, new RuntimeScalar(baseClassName));
}
diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/FileSpec.java b/src/main/java/org/perlonjava/runtime/perlmodule/FileSpec.java
index 94e83ce95..9d74294d3 100644
--- a/src/main/java/org/perlonjava/runtime/perlmodule/FileSpec.java
+++ b/src/main/java/org/perlonjava/runtime/perlmodule/FileSpec.java
@@ -6,6 +6,7 @@
import org.perlonjava.runtime.runtimetypes.SystemUtils;
import java.io.File;
+import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
@@ -425,7 +426,22 @@ public static RuntimeList abs2rel(RuntimeArray args, int ctx) {
}
String path = args.get(1).toString();
String base = args.size() == 3 ? args.get(2).toString() : System.getProperty("user.dir");
- String relPath = Paths.get(base).relativize(Paths.get(path)).toString();
+
+ // Ensure both paths are absolute before relativizing (like Perl does)
+ // Note: We use user.dir explicitly because Java's Path.toAbsolutePath()
+ // doesn't respect System.setProperty("user.dir", ...) set by chdir()
+ Path pathObj = Paths.get(path);
+ Path baseObj = Paths.get(base);
+ String userDir = System.getProperty("user.dir");
+
+ if (!pathObj.isAbsolute()) {
+ pathObj = Paths.get(userDir).resolve(pathObj).normalize();
+ }
+ if (!baseObj.isAbsolute()) {
+ baseObj = Paths.get(userDir).resolve(baseObj).normalize();
+ }
+
+ String relPath = baseObj.relativize(pathObj).toString();
return new RuntimeScalar(relPath).getList();
}
@@ -454,8 +470,14 @@ public static RuntimeList rel2abs(RuntimeArray args, int ctx) {
return new RuntimeScalar(absPath).getList();
}
+ // If base is relative, resolve it against current working directory first
+ Path basePath = Paths.get(base);
+ if (!basePath.isAbsolute()) {
+ basePath = Paths.get(System.getProperty("user.dir")).resolve(basePath);
+ }
+
// For relative paths, resolve against the base directory
- String absPath = Paths.get(base, path).toAbsolutePath().normalize().toString();
+ String absPath = basePath.resolve(path).normalize().toString();
return new RuntimeScalar(absPath).getList();
}
}
diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/FileTemp.java b/src/main/java/org/perlonjava/runtime/perlmodule/FileTemp.java
index f9dd29a4c..cbf763868 100644
--- a/src/main/java/org/perlonjava/runtime/perlmodule/FileTemp.java
+++ b/src/main/java/org/perlonjava/runtime/perlmodule/FileTemp.java
@@ -62,11 +62,12 @@ public static void initialize() {
* Create a temporary file from template
*/
public static RuntimeList _mkstemp(RuntimeArray args, int ctx) {
- if (args.size() != 2) {
+ if (args.size() < 1) {
throw new IllegalStateException("Bad number of arguments for _mkstemp");
}
- String template = args.get(1).toString();
+ // Get template - it's the last argument (supports both function call and method call)
+ String template = args.get(args.size() - 1).toString();
return createTempFile(template, "", false);
}
@@ -74,12 +75,13 @@ public static RuntimeList _mkstemp(RuntimeArray args, int ctx) {
* Create a temporary file with suffix
*/
public static RuntimeList _mkstemps(RuntimeArray args, int ctx) {
- if (args.size() != 3) {
+ if (args.size() < 2) {
throw new IllegalStateException("Bad number of arguments for _mkstemps");
}
- String template = args.get(1).toString();
- String suffix = args.get(2).toString();
+ // Get template and suffix - they're the last two arguments
+ String template = args.get(args.size() - 2).toString();
+ String suffix = args.get(args.size() - 1).toString();
return createTempFile(template, suffix, false);
}
@@ -87,11 +89,12 @@ public static RuntimeList _mkstemps(RuntimeArray args, int ctx) {
* Create a temporary directory
*/
public static RuntimeList _mkdtemp(RuntimeArray args, int ctx) {
- if (args.size() != 2) {
+ if (args.size() < 1) {
throw new IllegalStateException("Bad number of arguments for _mkdtemp");
}
- String template = args.get(1).toString();
+ // Get template - it's the last argument
+ String template = args.get(args.size() - 1).toString();
Path dir = createTempDir(template);
return new RuntimeList(new RuntimeScalar(dir.toString()));
}
@@ -195,13 +198,31 @@ private static RuntimeList createTempFile(String template, String suffix, boolea
}
String prefix = template.substring(0, xStart);
- Path templatePath = Paths.get(prefix);
- Path dir = templatePath.getParent();
- String namePrefix = templatePath.getFileName() != null ?
- templatePath.getFileName().toString() : "";
-
- if (dir == null) {
+
+ // Handle the case where prefix ends with a path separator (directory only, no name prefix)
+ // e.g., "/tmp/XXXXXX" -> prefix is "/tmp/", we want dir="/tmp" and namePrefix=""
+ Path dir;
+ String namePrefix;
+
+ if (prefix.isEmpty()) {
+ // No directory specified, use temp dir
dir = Paths.get(getTempDir());
+ namePrefix = "";
+ } else if (prefix.endsWith("/") || prefix.endsWith("\\")) {
+ // Prefix is a directory path with trailing separator
+ // Remove trailing separator and use as directory
+ dir = Paths.get(prefix.substring(0, prefix.length() - 1));
+ namePrefix = "";
+ } else {
+ // Prefix may contain both directory and name prefix
+ Path templatePath = Paths.get(prefix);
+ dir = templatePath.getParent();
+ namePrefix = templatePath.getFileName() != null ?
+ templatePath.getFileName().toString() : "";
+
+ if (dir == null) {
+ dir = Paths.get(getTempDir());
+ }
}
// Try to create temp file
@@ -274,13 +295,29 @@ private static Path createTempDir(String template) {
}
String prefix = template.substring(0, xStart);
- Path templatePath = Paths.get(prefix);
- Path parentDir = templatePath.getParent();
- String namePrefix = templatePath.getFileName() != null ?
- templatePath.getFileName().toString() : "";
-
- if (parentDir == null) {
+
+ // Handle the case where prefix ends with a path separator (directory only, no name prefix)
+ Path parentDir;
+ String namePrefix;
+
+ if (prefix.isEmpty()) {
+ // No directory specified, use temp dir
parentDir = Paths.get(getTempDir());
+ namePrefix = "";
+ } else if (prefix.endsWith("/") || prefix.endsWith("\\")) {
+ // Prefix is a directory path with trailing separator
+ parentDir = Paths.get(prefix.substring(0, prefix.length() - 1));
+ namePrefix = "";
+ } else {
+ // Prefix may contain both directory and name prefix
+ Path templatePath = Paths.get(prefix);
+ parentDir = templatePath.getParent();
+ namePrefix = templatePath.getFileName() != null ?
+ templatePath.getFileName().toString() : "";
+
+ if (parentDir == null) {
+ parentDir = Paths.get(getTempDir());
+ }
}
// Try to create temp directory
diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/Universal.java b/src/main/java/org/perlonjava/runtime/perlmodule/Universal.java
index c20aa0fa5..8c790e4a8 100644
--- a/src/main/java/org/perlonjava/runtime/perlmodule/Universal.java
+++ b/src/main/java/org/perlonjava/runtime/perlmodule/Universal.java
@@ -232,9 +232,11 @@ public static RuntimeList isa(RuntimeArray args, int ctx) {
// Get the linearized inheritance hierarchy using C3
List linearizedClasses = InheritanceResolver.linearizeHierarchy(perlClassName);
- // Normalize the argument: main::Foo -> Foo, ::Foo -> Foo
+ // Normalize the argument: main::Foo -> Foo, ::Foo -> Foo, Foo'Bar -> Foo::Bar
// This is needed because isa("main::Foo") should match a class blessed as "Foo"
String normalizedArg = argString;
+ // First normalize old-style ' separator to ::
+ normalizedArg = NameNormalizer.normalizePackageName(normalizedArg);
if (normalizedArg.startsWith("main::")) {
normalizedArg = normalizedArg.substring(6);
} else if (normalizedArg.startsWith("::")) {
diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/Version.java b/src/main/java/org/perlonjava/runtime/perlmodule/Version.java
index f35f550b1..c3389fea6 100644
--- a/src/main/java/org/perlonjava/runtime/perlmodule/Version.java
+++ b/src/main/java/org/perlonjava/runtime/perlmodule/Version.java
@@ -74,16 +74,37 @@ public static RuntimeList parse(RuntimeArray args, int ctx) {
if (version.isEmpty()) {
throw new PerlCompilerException("Invalid version format (version required)");
}
+
+ // Preserve the original version string before any modifications
+ RuntimeScalar originalVersionStr = versionStr;
+
+ // Track whether the original input was a v-string (starts with 'v')
+ boolean originalIsVString = version.startsWith("v");
+
if (versionStr.type == DOUBLE) {
+ // Format with enough precision but strip trailing zeros
version = String.format("%.6f", versionStr.getDouble());
+ // Remove trailing zeros after decimal point, but keep at least one decimal place
+ version = version.replaceAll("0+$", "").replaceAll("\\.$", ".0");
+ // Actually, Perl keeps the exact representation, so just strip trailing zeros
+ if (version.contains(".")) {
+ version = version.replaceAll("0+$", "");
+ // Remove trailing dot if all decimals were zeros (e.g., "1." -> "1")
+ if (version.endsWith(".")) {
+ version = version.substring(0, version.length() - 1);
+ }
+ }
+ originalVersionStr = new RuntimeScalar(version);
} else if (!version.startsWith("v")) {
// Count the number of dots
long dotCount = version.chars().filter(ch -> ch == '.').count();
- // If exactly one dot, prepend "v"
+ // If exactly one dot, prepend "v" for internal processing
+ // but keep the original for stringify() and qv flag
if (dotCount == 1 && version.length() < 4) {
version = "v" + version;
- versionStr = new RuntimeScalar(version);
+ // Note: originalVersionStr stays as the user's input (e.g., "1.0")
+ // Note: originalIsVString remains false - this is a decimal version
}
}
@@ -91,10 +112,12 @@ public static RuntimeList parse(RuntimeArray args, int ctx) {
RuntimeHash versionObj = new RuntimeHash();
// Parse the version string
+ // Use originalIsVString to determine qv, not the modified version string
if (version.startsWith("v")) {
- // v-string format
+ // v-string format (either originally or for internal processing)
versionObj.put("alpha", scalarFalse);
- versionObj.put("qv", scalarTrue);
+ // qv is true only if the ORIGINAL input was a v-string
+ versionObj.put("qv", getScalarBoolean(originalIsVString));
// Parse components
String normalized = VersionHelper.normalizeVersion(new RuntimeScalar(version));
@@ -112,7 +135,7 @@ public static RuntimeList parse(RuntimeArray args, int ctx) {
versionObj.put("version", new RuntimeScalar(normalized));
}
- versionObj.put("original", versionStr);
+ versionObj.put("original", originalVersionStr);
// Bless the object
RuntimeScalar blessed = versionObj.createReference();
diff --git a/src/main/java/org/perlonjava/runtime/regex/RegexFlags.java b/src/main/java/org/perlonjava/runtime/regex/RegexFlags.java
index 5091c3c28..1c5eb5906 100644
--- a/src/main/java/org/perlonjava/runtime/regex/RegexFlags.java
+++ b/src/main/java/org/perlonjava/runtime/regex/RegexFlags.java
@@ -18,11 +18,12 @@
* @param isDotAll s flag - dot matches all characters including newline
* @param isExtended x flag - ignore whitespace and # comments in pattern
* @param preservesMatch p flag - preserve match after failed matches
+ * @param isUnicode u flag - Unicode semantics (\w, \d, \s match Unicode)
*/
public record RegexFlags(boolean isGlobalMatch, boolean keepCurrentPosition, boolean isNonDestructive,
boolean isMatchExactlyOnce, boolean useGAssertion, boolean isExtendedWhitespace,
boolean isNonCapturing, boolean isOptimized, boolean isCaseInsensitive, boolean isMultiLine,
- boolean isDotAll, boolean isExtended, boolean preservesMatch) {
+ boolean isDotAll, boolean isExtended, boolean preservesMatch, boolean isUnicode) {
public static RegexFlags fromModifiers(String modifiers, String patternString) {
return new RegexFlags(
@@ -38,7 +39,8 @@ public static RegexFlags fromModifiers(String modifiers, String patternString) {
modifiers.contains("m"),
modifiers.contains("s"),
modifiers.contains("x"),
- modifiers.contains("p")
+ modifiers.contains("p"),
+ modifiers.contains("u")
);
}
@@ -63,6 +65,12 @@ public static void validateModifiers(String modifiers) {
public int toPatternFlags() {
int flags = 0;
+
+ // /u flag enables Unicode semantics for \w, \d, \s
+ if (isUnicode) {
+ flags |= UNICODE_CHARACTER_CLASS;
+ }
+
if (isCaseInsensitive) {
// For proper Unicode case-insensitive matching, we need both flags:
// - CASE_INSENSITIVE: enables case-insensitive matching
@@ -89,6 +97,7 @@ public RegexFlags with(String positiveFlags, String negativeFlags) {
boolean newIsDotAll = this.isDotAll;
boolean newIsExtended = this.isExtended;
boolean newPreservesMatch = this.preservesMatch;
+ boolean newIsUnicode = this.isUnicode;
// Handle positive flags
if (positiveFlags.indexOf('n') >= 0) newFlagN = true;
@@ -97,6 +106,7 @@ public RegexFlags with(String positiveFlags, String negativeFlags) {
if (positiveFlags.indexOf('s') >= 0) newIsDotAll = true;
if (positiveFlags.indexOf('x') >= 0) newIsExtended = true;
if (positiveFlags.indexOf('p') >= 0) newPreservesMatch = true;
+ if (positiveFlags.indexOf('u') >= 0) newIsUnicode = true;
// Handle negative flags
if (negativeFlags.indexOf('n') >= 0) newFlagN = false;
@@ -104,6 +114,7 @@ public RegexFlags with(String positiveFlags, String negativeFlags) {
if (negativeFlags.indexOf('m') >= 0) newIsMultiLine = false;
if (negativeFlags.indexOf('s') >= 0) newIsDotAll = false;
if (negativeFlags.indexOf('x') >= 0) newIsExtended = false;
+ if (negativeFlags.indexOf('u') >= 0) newIsUnicode = false;
return new RegexFlags(
this.isGlobalMatch,
@@ -118,7 +129,8 @@ public RegexFlags with(String positiveFlags, String negativeFlags) {
newIsMultiLine,
newIsDotAll,
newIsExtended,
- newPreservesMatch
+ newPreservesMatch,
+ newIsUnicode
);
}
@@ -133,6 +145,7 @@ public String toFlagString() {
if (isNonCapturing) flagString.append('n');
if (isExtended) flagString.append('x');
if (isNonDestructive) flagString.append('r');
+ if (isUnicode) flagString.append('u');
return flagString.toString();
}
diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/HashSpecialVariable.java b/src/main/java/org/perlonjava/runtime/runtimetypes/HashSpecialVariable.java
index 3a3f7ef80..d927a5e57 100644
--- a/src/main/java/org/perlonjava/runtime/runtimetypes/HashSpecialVariable.java
+++ b/src/main/java/org/perlonjava/runtime/runtimetypes/HashSpecialVariable.java
@@ -96,11 +96,14 @@ public Set> entrySet() {
// Process each key to extract the namespace part
Set uniqueKeys = new HashSet<>(); // Set to track unique keys
+ boolean isMainStash = "main::".equals(namespace);
for (String key : allKeys) {
+ String entryKey = null;
+ String globName = null;
+
if (key.startsWith(namespace)) {
String remainingKey = key.substring(namespace.length());
int nextSeparatorIndex = remainingKey.indexOf("::");
- String entryKey;
if (nextSeparatorIndex == -1) {
entryKey = remainingKey;
} else {
@@ -108,23 +111,38 @@ public Set> entrySet() {
// (e.g. "Foo::" not "Foo") - this is how Perl indicates sub-packages
entryKey = remainingKey.substring(0, nextSeparatorIndex + 2);
}
-
- // Special sort variables should not show up in stash enumeration
- if (entryKey.equals("a") || entryKey.equals("b")) {
- continue;
+ // entryKey already includes "::" for nested packages
+ globName = namespace + entryKey;
+ } else if (isMainStash) {
+ // For %main::, also include top-level packages that aren't explicitly
+ // prefixed with "main::". In Perl, $Foo::x and $main::Foo::x are the same.
+ // Variables in top-level packages are stored as "Foo::x", not "main::Foo::x".
+ int separatorIndex = key.indexOf("::");
+ if (separatorIndex > 0) {
+ // This is a top-level package (like "Foo::test")
+ // Extract "Foo::" as the entry key
+ entryKey = key.substring(0, separatorIndex + 2);
+ // The glob name is the original key prefix
+ globName = entryKey;
}
+ }
- if (entryKey.isEmpty()) {
- continue;
- }
+ if (entryKey == null) {
+ continue;
+ }
- // entryKey already includes "::" for nested packages
- String globName = namespace + entryKey;
+ // Special sort variables should not show up in stash enumeration
+ if (entryKey.equals("a") || entryKey.equals("b")) {
+ continue;
+ }
- // Add the entry only if it's not already in the set of unique keys
- if (uniqueKeys.add(entryKey)) {
- entries.add(new SimpleEntry<>(entryKey, new RuntimeStashEntry(globName, true)));
- }
+ if (entryKey.isEmpty()) {
+ continue;
+ }
+
+ // Add the entry only if it's not already in the set of unique keys
+ if (uniqueKeys.add(entryKey)) {
+ entries.add(new SimpleEntry<>(entryKey, new RuntimeStashEntry(globName, true)));
}
}
}
diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/NameNormalizer.java b/src/main/java/org/perlonjava/runtime/runtimetypes/NameNormalizer.java
index 7af572976..04cd69378 100644
--- a/src/main/java/org/perlonjava/runtime/runtimetypes/NameNormalizer.java
+++ b/src/main/java/org/perlonjava/runtime/runtimetypes/NameNormalizer.java
@@ -183,6 +183,24 @@ public static String moduleToFilename(String moduleName) {
return moduleName.replace("::", "/") + ".pm";
}
+ /**
+ * Normalizes a package name by converting old-style single-quote separator to '::'.
+ * In Perl, "Foo'Bar" is equivalent to "Foo::Bar".
+ *
+ * @param packageName The package name to normalize.
+ * @return The normalized package name with '::' separators.
+ */
+ public static String normalizePackageName(String packageName) {
+ if (packageName == null || packageName.isEmpty()) {
+ return packageName;
+ }
+ // Replace old-style ' separator with ::
+ if (packageName.indexOf('\'') >= 0) {
+ return packageName.replace("'", "::");
+ }
+ return packageName;
+ }
+
/**
* Composite key for name cache to avoid string concatenation overhead.
* Using a record provides efficient hashCode/equals with no allocation.
diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/PerlCompilerException.java b/src/main/java/org/perlonjava/runtime/runtimetypes/PerlCompilerException.java
index 47a556cd3..135c5ff7e 100644
--- a/src/main/java/org/perlonjava/runtime/runtimetypes/PerlCompilerException.java
+++ b/src/main/java/org/perlonjava/runtime/runtimetypes/PerlCompilerException.java
@@ -67,7 +67,7 @@ private static String buildErrorMessage(String message) {
}
String fileName = caller.elements.get(1).toString();
int line = ((RuntimeScalar) caller.elements.get(2)).getInt();
- return message + " at " + fileName + " line " + line + "\n";
+ return message + " at " + fileName + " line " + line + ".\n";
} catch (Throwable t) {
// caller() failed (e.g. mid-exception in interpreter) — use bare message
return message + "\n";
diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java
index df591143d..65975c2e3 100644
--- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java
+++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java
@@ -18,6 +18,12 @@ public class RuntimeGlob extends RuntimeScalar implements RuntimeScalarReference
// The name of the typeglob
public String globName;
public RuntimeScalar IO;
+ // Local scalar slot for anonymous globs (when globName is null)
+ private RuntimeScalar scalarSlot;
+ // Local array slot for anonymous globs (when globName is null)
+ private RuntimeArray arraySlot;
+ // Local hash slot for anonymous globs (when globName is null)
+ private RuntimeHash hashSlot;
/**
* Constructor for RuntimeGlob.
@@ -330,7 +336,6 @@ public RuntimeScalar hashDerefGetNonStrict(RuntimeScalar index, String packageNa
* This is the common implementation for both strict and non-strict contexts.
*/
private RuntimeScalar getGlobSlot(RuntimeScalar index) {
- // System.out.println("glob getGlobSlot " + index.toString());
return switch (index.toString()) {
case "CODE" -> {
// Only return CODE ref if the subroutine is actually defined
@@ -367,8 +372,24 @@ private RuntimeScalar getGlobSlot(RuntimeScalar index) {
}
yield IO;
}
- case "SCALAR" -> GlobalVariable.getGlobalVariable(this.globName);
+ case "SCALAR" -> {
+ // For anonymous globs (null globName), use local scalarSlot
+ if (this.globName == null) {
+ if (this.scalarSlot == null) {
+ this.scalarSlot = new RuntimeScalar();
+ }
+ yield this.scalarSlot;
+ }
+ yield GlobalVariable.getGlobalVariable(this.globName);
+ }
case "ARRAY" -> {
+ // For anonymous globs (null globName), use local arraySlot
+ if (this.globName == null) {
+ if (this.arraySlot == null) {
+ this.arraySlot = new RuntimeArray();
+ }
+ yield this.arraySlot.createReference();
+ }
// Only return reference if array exists (has elements or was explicitly created)
if (GlobalVariable.existsGlobalArray(this.globName)) {
yield GlobalVariable.getGlobalArray(this.globName).createReference();
@@ -376,6 +397,13 @@ private RuntimeScalar getGlobSlot(RuntimeScalar index) {
yield new RuntimeScalar(); // Return undef if array doesn't exist
}
case "HASH" -> {
+ // For anonymous globs (null globName), use local hashSlot
+ if (this.globName == null) {
+ if (this.hashSlot == null) {
+ this.hashSlot = new RuntimeHash();
+ }
+ yield this.hashSlot.createReference();
+ }
// Only return reference if hash exists (has elements or was explicitly created)
if (GlobalVariable.existsGlobalHash(this.globName)) {
yield GlobalVariable.getGlobalHash(this.globName).createReference();
@@ -391,6 +419,36 @@ public RuntimeScalar getIO() {
return this.IO;
}
+ /**
+ * Get the hash slot for this glob.
+ * For anonymous globs (null globName), uses the local hashSlot field.
+ * For named globs, retrieves from GlobalVariable.
+ */
+ public RuntimeHash getGlobHash() {
+ if (this.globName == null) {
+ if (this.hashSlot == null) {
+ this.hashSlot = new RuntimeHash();
+ }
+ return this.hashSlot;
+ }
+ return GlobalVariable.getGlobalHash(this.globName);
+ }
+
+ /**
+ * Get the array slot for this glob.
+ * For anonymous globs (null globName), uses the local arraySlot field.
+ * For named globs, retrieves from GlobalVariable.
+ */
+ public RuntimeArray getGlobArray() {
+ if (this.globName == null) {
+ if (this.arraySlot == null) {
+ this.arraySlot = new RuntimeArray();
+ }
+ return this.arraySlot;
+ }
+ return GlobalVariable.getGlobalArray(this.globName);
+ }
+
public RuntimeGlob setIO(RuntimeScalar io) {
// If IO slot is tied (TIED_SCALAR with TieHandle), replace it entirely
// Otherwise use set() to modify in place, preserving sharing with detached copies
diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java
index ec298c9d8..1d5334a79 100644
--- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java
+++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java
@@ -987,7 +987,8 @@ public RuntimeArray arrayDeref() {
throw new PerlCompilerException("Not an ARRAY reference");
}
RuntimeGlob glob = (RuntimeGlob) value;
- yield GlobalVariable.getGlobalArray(glob.globName);
+ // For anonymous globs, use the getGlobArray method which handles local slots
+ yield glob.getGlobArray();
}
case JAVAOBJECT -> // 8
throw new PerlCompilerException("Not an ARRAY reference");
@@ -1069,7 +1070,8 @@ public RuntimeHash hashDeref() {
throw new PerlCompilerException("Not a HASH reference");
}
RuntimeGlob glob = (RuntimeGlob) value;
- yield GlobalVariable.getGlobalHash(glob.globName);
+ // For anonymous globs, use the getGlobHash method which handles local slots
+ yield glob.getGlobHash();
}
case JAVAOBJECT -> // 8
throw new PerlCompilerException("Not a HASH reference");
@@ -1113,7 +1115,8 @@ public RuntimeScalar scalarDeref() {
// Dereferencing a glob as scalar returns the scalar slot
// e.g., ${*Foo::VERSION} or ${$glob} where $glob is a glob
if (value instanceof RuntimeGlob glob) {
- yield GlobalVariable.getGlobalVariable(glob.globName);
+ // Use the glob's hashDerefGet method which handles anonymous globs
+ yield glob.hashDerefGet(new RuntimeScalar("SCALAR"));
}
throw new PerlCompilerException("Not a SCALAR reference");
}
@@ -1158,7 +1161,8 @@ public RuntimeScalar scalarDerefNonStrict(String packageName) {
case GLOB -> {
// Dereferencing a glob as scalar returns the scalar slot
if (value instanceof RuntimeGlob glob) {
- yield GlobalVariable.getGlobalVariable(glob.globName);
+ // Use the glob's hashDerefGet method which handles anonymous globs
+ yield glob.hashDerefGet(new RuntimeScalar("SCALAR"));
}
String varName = NameNormalizer.normalizeVariableName(this.toString(), packageName);
yield GlobalVariable.getGlobalVariable(varName);
@@ -1235,7 +1239,8 @@ public RuntimeHash hashDerefNonStrict(String packageName) {
case GLOB -> { // 7
// When dereferencing a typeglob as a hash, return the hash slot
RuntimeGlob glob = (RuntimeGlob) value;
- yield GlobalVariable.getGlobalHash(glob.globName);
+ // For anonymous globs, use the getGlobHash method which handles local slots
+ yield glob.getGlobHash();
}
case JAVAOBJECT -> // 8
throw new PerlCompilerException("Not a HASH reference");
@@ -1304,7 +1309,8 @@ public RuntimeArray arrayDerefNonStrict(String packageName) {
case GLOB -> { // 7
// When dereferencing a typeglob as an array, return the array slot
RuntimeGlob glob = (RuntimeGlob) value;
- yield GlobalVariable.getGlobalArray(glob.globName);
+ // For anonymous globs, use the getGlobArray method which handles local slots
+ yield glob.getGlobArray();
}
case JAVAOBJECT -> // 8
throw new PerlCompilerException("Not an ARRAY reference");
diff --git a/src/main/perl/lib/File/Temp.pm b/src/main/perl/lib/File/Temp.pm
index dfc8d2806..353c5fbf8 100644
--- a/src/main/perl/lib/File/Temp.pm
+++ b/src/main/perl/lib/File/Temp.pm
@@ -14,6 +14,7 @@ package File::Temp;
use strict;
use warnings;
use Carp;
+use Cwd qw(abs_path); # Load early to avoid CORE::GLOBAL::stat conflicts
use File::Spec;
use File::Path qw(rmtree);
use Fcntl qw(SEEK_SET SEEK_CUR SEEK_END O_RDWR O_CREAT O_EXCL);
@@ -139,6 +140,20 @@ sub unlink_on_destroy {
return $self->{_unlink};
}
+sub autoflush {
+ my $self = shift;
+ my $fh = $self->{_fh};
+ return unless defined $fh;
+
+ my $old = select($fh);
+ if (@_) {
+ $| = shift;
+ }
+ my $value = $|;
+ select($old);
+ return $value;
+}
+
sub DESTROY {
my $self = shift;
@@ -174,11 +189,17 @@ sub AUTOLOAD {
sub tempfile {
my ($template, %args) = _parse_args(@_);
+ # Handle TEMPLATE option (alternative to positional template)
+ if (!defined $template && exists $args{TEMPLATE}) {
+ $template = delete $args{TEMPLATE};
+ }
+
# Set defaults
my $dir = $args{DIR};
my $suffix = $args{SUFFIX} || '';
my $unlink = exists $args{UNLINK} ? $args{UNLINK} : (defined wantarray ? 1 : 0);
my $open = exists $args{OPEN} ? $args{OPEN} : 1;
+ my $perms = $args{PERMS}; # Custom permissions
# If no directory specified, use temp directory by default
# unless TMPDIR was explicitly set to false
@@ -203,26 +224,36 @@ sub tempfile {
}
# Create temp file
- my ($fd, $path);
+ my ($fh, $path);
+ my $from_java = 0;
eval {
if ($suffix) {
- ($fd, $path) = _mkstemps($template, $suffix);
+ (my $fd, $path) = _mkstemps($template, $suffix);
+ $from_java = 1;
} else {
- ($fd, $path) = _mkstemp($template);
+ (my $fd, $path) = _mkstemp($template);
+ $from_java = 1;
}
};
- if ($@) {
- # Fallback to pure Perl implementation
- ($fd, $path) = _mkstemp_perl($template, $suffix);
+ if ($@ || !$from_java) {
+ # Fallback to pure Perl implementation - returns open filehandle
+ ($fh, $path) = _mkstemp_perl($template, $suffix);
}
return $path unless $open;
- # Ignore the file descriptor and just open the file by path
- # The Java side should have already closed its file descriptor
- open(my $fh, '+<', $path) or croak "Could not open temp file: $!";
+ # For Java path, we need to reopen (Java closed the fd)
+ # For Perl path, we already have the filehandle
+ if ($from_java || !defined $fh) {
+ open($fh, '+<', $path) or croak "Could not open temp file: $!";
+ }
binmode($fh);
+ # Apply custom permissions AFTER we have the filehandle open
+ if (defined $perms && -e $path) {
+ chmod($perms, $path);
+ }
+
# Set up cleanup if needed
if ($unlink) {
_register_cleanup($path, 'file');
@@ -235,6 +266,11 @@ sub tempfile {
sub tempdir {
my ($template, %args) = _parse_args(@_);
+ # Handle TEMPLATE option (alternative to positional template)
+ if (!defined $template && exists $args{TEMPLATE}) {
+ $template = delete $args{TEMPLATE};
+ }
+
# Set defaults
my $dir = $args{DIR};
my $tmpdir = $args{TMPDIR};
@@ -480,14 +516,30 @@ sub _generate_template {
return $base . "XXXXXX";
}
+# Wrapper for File::Spec->tmpdir for compatibility
+sub _wrap_file_spec_tmpdir {
+ return File::Spec->tmpdir;
+}
+
sub _replace_XX {
my $template = shift;
my $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_';
- $template =~ s/X/substr($chars, int(rand(length($chars))), 1)/ge;
+ # Only replace trailing X's - match X+ at end of string
+ $template =~ s/(X+)$/_rand_chars($chars, length($1))/e;
return $template;
}
+# Generate random characters of specified length
+sub _rand_chars {
+ my ($chars, $len) = @_;
+ my $result = '';
+ for (1..$len) {
+ $result .= substr($chars, int(rand(length($chars))), 1);
+ }
+ return $result;
+}
+
# Pure Perl fallback implementations
sub _mkstemp_perl {
@@ -497,7 +549,8 @@ sub _mkstemp_perl {
for (my $i = 0; $i < 256; $i++) {
my $path = _replace_XX($template) . $suffix;
if (sysopen(my $fh, $path, O_RDWR | O_CREAT | O_EXCL, 0600)) {
- return (fileno($fh), $path);
+ # Return the open filehandle and path
+ return ($fh, $path);
}
}
@@ -525,15 +578,19 @@ sub _register_cleanup {
my ($path, $type) = @_;
my $pid = $$;
+ # Convert to absolute path - important for cleanup after chdir
+ my $abs_path = abs_path($path);
+ $abs_path = $path unless defined $abs_path; # fallback if abs_path fails
+
if ($type eq 'file') {
- $CLEANUP_FILES{$pid}{$path} = 1;
+ $CLEANUP_FILES{$pid}{$abs_path} = 1;
eval {
- _register_temp_file($path);
+ _register_temp_file($abs_path);
};
} else {
- $CLEANUP_DIRS{$pid}{$path} = 1;
+ $CLEANUP_DIRS{$pid}{$abs_path} = 1;
eval {
- _register_temp_dir($path);
+ _register_temp_dir($abs_path);
};
}
}
@@ -541,7 +598,7 @@ sub _register_cleanup {
sub _cleanup_registered {
my $pid = $$;
- # Clean up files
+ # Clean up files first
if (exists $CLEANUP_FILES{$pid}) {
for my $file (keys %{$CLEANUP_FILES{$pid}}) {
unlink($file) if -e $file;
@@ -549,11 +606,36 @@ sub _cleanup_registered {
delete $CLEANUP_FILES{$pid};
}
- # Clean up directories
+ # Clean up directories - need to handle case where we're IN a dir to be deleted
if (exists $CLEANUP_DIRS{$pid}) {
+ my $cwd = abs_path(File::Spec->curdir);
+ my $cwd_to_remove;
+
for my $dir (keys %{$CLEANUP_DIRS{$pid}}) {
- rmtree($dir) if -d $dir;
+ if (-d $dir) {
+ # Check if we're currently in this directory
+ my $abs_dir = abs_path($dir);
+ if (defined $abs_dir && defined $cwd && $abs_dir eq $cwd) {
+ # We're in this directory - save it for last
+ $cwd_to_remove = $dir;
+ next;
+ }
+ # Safe to remove - we're not in it
+ rmtree($dir);
+ }
+ }
+
+ # Now handle the directory we're sitting in (if any)
+ if (defined $cwd_to_remove && -d $cwd_to_remove) {
+ # chdir out of the directory first
+ my $updir = File::Spec->updir;
+ if (chdir($updir)) {
+ rmtree($cwd_to_remove);
+ } else {
+ warn "Could not chdir to $updir to remove $cwd_to_remove: $!";
+ }
}
+
delete $CLEANUP_DIRS{$pid};
}
}
diff --git a/src/main/perl/lib/version.pm b/src/main/perl/lib/version.pm
index 4f5e088e1..ceaae0724 100644
--- a/src/main/perl/lib/version.pm
+++ b/src/main/perl/lib/version.pm
@@ -30,6 +30,16 @@ use overload (
'""' => \&stringify,
'<=>' => \&vcmp,
'cmp' => \&vcmp,
+ # Math operations are not supported - throw error
+ '+' => \&_noop,
+ '-' => \&_noop,
+ '*' => \&_noop,
+ '/' => \&_noop,
+ 'abs' => \&_noop,
+ '+=' => \&_noop,
+ '-=' => \&_noop,
+ '*=' => \&_noop,
+ '/=' => \&_noop,
);
# avoid using Exporter
@@ -99,6 +109,11 @@ sub import {
# Additional methods that might be needed for version objects
+sub _noop {
+ require Carp;
+ Carp::croak("operation not supported with version object");
+}
+
sub is_alpha {
my ($self) = @_;
return $self->{alpha} ? 1 : 0;