From 397c2b067aede19ea6ed0a3ad5ae9790430240a5 Mon Sep 17 00:00:00 2001 From: Lukas Mai Date: Wed, 30 Jul 2025 05:31:25 +0200 Subject: [PATCH 1/9] File::stat: always return a scalar, even in list context Previously, a failing stat would return an empty list in list context, not undef. This can cause surprises in code like: foo(stat $file); # would call foo() without arguments if stat fails my %hash = ( stat => stat($file), # Odd number of elements in hash assignment if stat fails ); --- lib/File/stat.pm | 7 +++---- lib/File/stat.t | 12 ++++++++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/lib/File/stat.pm b/lib/File/stat.pm index 192df7bbdb42..37a35a688f38 100644 --- a/lib/File/stat.pm +++ b/lib/File/stat.pm @@ -185,7 +185,7 @@ struct 'File::stat' => [ ]; sub populate { - return unless @_; + return undef unless @_; my $stob = new(); @$stob = ( $st_dev, $st_ino, $st_mode, $st_nlink, $st_uid, $st_gid, $st_rdev, @@ -199,8 +199,7 @@ sub lstat :prototype($) { populate(CORE::lstat(shift)) } sub stat :prototype($) { my $arg = shift; my $st = populate(CORE::stat $arg); - return $st if defined $st; - return if ref $arg; + return $st if defined $st || ref $arg; # ... maybe $arg is the name of a bareword handle? my $fh; { @@ -208,7 +207,7 @@ sub stat :prototype($) { no strict 'refs'; require Symbol; $fh = \*{ Symbol::qualify( $arg, caller() )}; - return unless defined fileno $fh; + return undef unless defined fileno $fh; } return populate(CORE::stat $fh); } diff --git a/lib/File/stat.t b/lib/File/stat.t index 0175a9304b3f..364b65b76e8f 100644 --- a/lib/File/stat.t +++ b/lib/File/stat.t @@ -225,6 +225,18 @@ SKIP: is stat({}), undef, 'stat({}) fails by returning undef'; } +{ + # list context + + my @ret = stat '/ no such file'; + is scalar(@ret), 1, 'stat returns one value in list context on failure'; + is $ret[0], undef, 'stat returns undef on failure'; + + @ret = stat $file; + is scalar(@ret), 1, 'stat returns one value in list context on success'; + isa_ok $ret[0], 'File::stat', 'successful stat in list context'; +} + # Testing pretty much anything else is unportable. done_testing; From d636ad6c8867ba1f9b99840e79253e3b7554f33f Mon Sep 17 00:00:00 2001 From: Lukas Mai Date: Wed, 30 Jul 2025 05:37:51 +0200 Subject: [PATCH 2/9] File::stat: default to $_ if called without arguments --- lib/File/stat.pm | 22 ++++++++++++---------- lib/File/stat.t | 6 ++++++ 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/lib/File/stat.pm b/lib/File/stat.pm index 37a35a688f38..d84ac3fd89be 100644 --- a/lib/File/stat.pm +++ b/lib/File/stat.pm @@ -194,9 +194,9 @@ sub populate { return $stob; } -sub lstat :prototype($) { populate(CORE::lstat(shift)) } +sub lstat :prototype(_) { populate(CORE::lstat(shift)) } -sub stat :prototype($) { +sub stat :prototype(_) { my $arg = shift; my $st = populate(CORE::stat $arg); return $st if defined $st || ref $arg; @@ -293,17 +293,19 @@ function functions with their full qualified names. On the other hand, the built-ins are still available via the C pseudo-package. -=head1 BUGS +As of version 1.15 (provided with perl 5.44) C and C can be +called without arguments, in which case C<$_> is used (just like the +built-in C/C functions): -As of Perl 5.8.0 after using this module you cannot use the implicit -C<$_> or the special filehandle C<_> with stat() or lstat(), trying -to do so leads into strange errors. The workaround is for C<$_> to -be explicit + my $st_1 = stat; # stat($_) + my $st_2 = lstat; # lstat($_) - my $stat_obj = stat $_; +=head1 BUGS -and for C<_> to explicitly populate the object using the unexported -and undocumented populate() function with CORE::stat(): +As of Perl 5.8.0 after using this module you cannot use the special +filehandle C<_> with stat() or lstat(); trying to do so leads to strange +errors. The workaround is to explicitly populate the object using the +unexported and undocumented populate() function with CORE::stat(): my $stat_obj = File::stat::populate(CORE::stat(_)); diff --git a/lib/File/stat.t b/lib/File/stat.t index 364b65b76e8f..cc2d8a42e6b3 100644 --- a/lib/File/stat.t +++ b/lib/File/stat.t @@ -237,6 +237,12 @@ SKIP: isa_ok $ret[0], 'File::stat', 'successful stat in list context'; } +{ + # implicit $_ + $_ = $file; + isa_ok stat, 'File::stat', 'stat()'; +} + # Testing pretty much anything else is unportable. done_testing; From a5ec4ca9d11e30528bb3af1505c3bd222c258a74 Mon Sep 17 00:00:00 2001 From: Lukas Mai Date: Wed, 30 Jul 2025 05:39:55 +0200 Subject: [PATCH 3/9] File::stat: we have signatures, so use them --- lib/File/stat.pm | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/File/stat.pm b/lib/File/stat.pm index d84ac3fd89be..d2d2252f6ff6 100644 --- a/lib/File/stat.pm +++ b/lib/File/stat.pm @@ -194,10 +194,11 @@ sub populate { return $stob; } -sub lstat :prototype(_) { populate(CORE::lstat(shift)) } +sub lstat :prototype(_) ($arg) { + populate(CORE::lstat $arg) +} -sub stat :prototype(_) { - my $arg = shift; +sub stat :prototype(_) ($arg) { my $st = populate(CORE::stat $arg); return $st if defined $st || ref $arg; # ... maybe $arg is the name of a bareword handle? From ceaf949258b72f1481fed47eadee49517e22e5f9 Mon Sep 17 00:00:00 2001 From: Lukas Mai Date: Wed, 30 Jul 2025 05:42:19 +0200 Subject: [PATCH 4/9] File::stat: show examples of (not) overriding stat That is, show how to access File::stat::stat without overriding anything and how to access CORE::stat despite overriding stat. --- lib/File/stat.pm | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/lib/File/stat.pm b/lib/File/stat.pm index d2d2252f6ff6..83d4688290cf 100644 --- a/lib/File/stat.pm +++ b/lib/File/stat.pm @@ -288,11 +288,18 @@ variables named with a preceding C in front their method names. Thus, C<$stat_obj-Edev()> corresponds to $st_dev if you import the fields. -To access this functionality without the core overrides, -pass the C an empty import list, and then access -function functions with their full qualified names. -On the other hand, the built-ins are still available -via the C pseudo-package. +To access this functionality without the core overrides, pass the C +an empty import list, and then access functions with their full qualified +names: + + use File::stat (); + my $st = File::stat::stat($file); + +On the other hand, the built-ins are still available via the C +pseudo-package even if you let File::stat override them: + + use File::stat; + my ($dev, $ino, $mode) = CORE::stat($file); As of version 1.15 (provided with perl 5.44) C and C can be called without arguments, in which case C<$_> is used (just like the From 5ed0fc7e25ab84d7fbef33c6b3176adbd845c89e Mon Sep 17 00:00:00 2001 From: Lukas Mai Date: Wed, 30 Jul 2025 05:45:10 +0200 Subject: [PATCH 5/9] File::stat: consistently indent verbatim blocks by 4 spaces Previously, the POD used a mix of 1-space and 4-space indents. Also, hyperlink stat(2) and Class::Struct. --- lib/File/stat.pm | 46 +++++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/lib/File/stat.pm b/lib/File/stat.pm index 83d4688290cf..438852d150ab 100644 --- a/lib/File/stat.pm +++ b/lib/File/stat.pm @@ -221,26 +221,26 @@ File::stat - by-name interface to Perl's built-in stat() functions =head1 SYNOPSIS - use File::stat; - my $st = stat($file) or die "No $file: $!"; - if ( ($st->mode & 0111) && ($st->nlink > 1) ) { - print "$file is executable with lotsa links\n"; - } - - if ( -x $st ) { - print "$file is executable\n"; - } - - use Fcntl "S_IRUSR"; - if ( $st->cando(S_IRUSR, 1) ) { - print "My effective uid can read $file\n"; - } - - use File::stat qw(:FIELDS); - stat($file) or die "No $file: $!"; - if ( ($st_mode & 0111) && ($st_nlink > 1) ) { - print "$file is executable with lotsa links\n"; - } + use File::stat; + my $st = stat($file) or die "No $file: $!"; + if ( ($st->mode & 0111) && ($st->nlink > 1) ) { + print "$file is executable with lotsa links\n"; + } + + if ( -x $st ) { + print "$file is executable\n"; + } + + use Fcntl "S_IRUSR"; + if ( $st->cando(S_IRUSR, 1) ) { + print "My effective uid can read $file\n"; + } + + use File::stat qw(:FIELDS); + stat($file) or die "No $file: $!"; + if ( ($st_mode & 0111) && ($st_nlink > 1) ) { + print "$file is executable with lotsa links\n"; + } =head1 DESCRIPTION @@ -248,7 +248,7 @@ This module's default exports override the core stat() and lstat() functions, replacing them with versions that return "File::stat" objects. This object has methods that return the similarly named structure field name from the -stat(2) function; namely, +L function; namely, dev, ino, mode, @@ -268,7 +268,7 @@ As of version 1.02 (provided with perl 5.12) the object provides C<"-X"> overloading, so you can call filetest operators (C<-f>, C<-x>, and so on) on it. It also provides a C<< ->cando >> method, called like - $st->cando( ACCESS, EFFECTIVE ) + $st->cando( ACCESS, EFFECTIVE ) where I is one of C, C or C from the L module, and I indicates whether to use @@ -353,7 +353,7 @@ do not, since the information required is not available. =head1 NOTE -While this class is currently implemented using the Class::Struct +While this class is currently implemented using the L module to build a struct-like class, you shouldn't rely upon this. =head1 AUTHOR From 6c02a484f0de1a76c167700c57f9728cffcb24c1 Mon Sep 17 00:00:00 2001 From: Lukas Mai Date: Wed, 30 Jul 2025 05:51:12 +0200 Subject: [PATCH 6/9] File::stat: add equivalent GH IDs to RT ticket references --- lib/File/stat-7896.t | 2 +- lib/File/stat.t | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/File/stat-7896.t b/lib/File/stat-7896.t index 57b268585203..7b049892c944 100644 --- a/lib/File/stat-7896.t +++ b/lib/File/stat-7896.t @@ -9,7 +9,7 @@ use File::stat; # should be revisited is($INC{'Symbol.pm'}, undef, "Symbol isn't loaded yet"); -# ID 20011110.104 (RT #7896) +# ID 20011110.104 (RT #7896 / GH #4572) $! = 0; is($!, '', '$! is empty'); is(File::stat::stat('/notafile'), undef, 'invalid file should fail'); diff --git a/lib/File/stat.t b/lib/File/stat.t index cc2d8a42e6b3..a51e08cb7fff 100644 --- a/lib/File/stat.t +++ b/lib/File/stat.t @@ -179,7 +179,7 @@ SKIP: { } SKIP: -{ # RT #111638 +{ # RT #111638 / GH #11992 skip "We can't check for FIFOs", 2 unless defined &Fcntl::S_ISFIFO; skip "No pipes", 2 unless defined $Config{d_pipe}; pipe my ($rh, $wh) From 4ba7ea672664adc104b461c2124e9c5cb2f57fbf Mon Sep 17 00:00:00 2001 From: Lukas Mai Date: Wed, 30 Jul 2025 05:53:14 +0200 Subject: [PATCH 7/9] File::stat: expand tabs, delete trailing spaces Most lines in these files used spaces for indentation, but a few of them still had tabs. Keep things consistent by using spaces everywhere. --- lib/File/stat.pm | 34 +++++++++++++++++----------------- lib/File/stat.t | 38 +++++++++++++++++++------------------- 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/lib/File/stat.pm b/lib/File/stat.pm index 438852d150ab..12cee0b96d5d 100644 --- a/lib/File/stat.pm +++ b/lib/File/stat.pm @@ -16,12 +16,12 @@ our ( $st_dev, $st_ino, $st_mode, use Exporter 'import'; our @EXPORT = qw(stat lstat); -our @fields = qw( $st_dev $st_ino $st_mode - $st_nlink $st_uid $st_gid - $st_rdev $st_size - $st_atime $st_mtime $st_ctime - $st_blksize $st_blocks - ); +our @fields = qw( $st_dev $st_ino $st_mode + $st_nlink $st_uid $st_gid + $st_rdev $st_size + $st_atime $st_mtime $st_ctime + $st_blksize $st_blocks + ); our @EXPORT_OK = ( @fields, "stat_cando" ); our %EXPORT_TAGS = ( FIELDS => [ @fields, @EXPORT ] ); @@ -73,7 +73,7 @@ sub _ingroup { # and interpreting it later would require this module to have an XS # component (at which point we might as well just call Perl_cando and # have done with it). - + if (grep $^O eq $_, qw/os2 MSWin32/) { # from doio.c @@ -152,7 +152,7 @@ my %op = ( use constant HINT_FILETEST_ACCESS => 0x00400000; # we need fallback=>1 or stringifying breaks -use overload +use overload fallback => 1, -X => sub { my ($s, $op) = @_; @@ -179,8 +179,8 @@ use overload use Class::Struct qw(struct); struct 'File::stat' => [ map { $_ => '$' } qw{ - dev ino mode nlink uid gid rdev size - atime mtime ctime blksize blocks + dev ino mode nlink uid gid rdev size + atime mtime ctime blksize blocks } ]; @@ -188,11 +188,11 @@ sub populate { return undef unless @_; my $stob = new(); @$stob = ( - $st_dev, $st_ino, $st_mode, $st_nlink, $st_uid, $st_gid, $st_rdev, - $st_size, $st_atime, $st_mtime, $st_ctime, $st_blksize, $st_blocks ) - = @_; + $st_dev, $st_ino, $st_mode, $st_nlink, $st_uid, $st_gid, $st_rdev, + $st_size, $st_atime, $st_mtime, $st_ctime, $st_blksize, $st_blocks ) + = @_; return $stob; -} +} sub lstat :prototype(_) ($arg) { populate(CORE::lstat $arg) @@ -244,8 +244,8 @@ File::stat - by-name interface to Perl's built-in stat() functions =head1 DESCRIPTION -This module's default exports override the core stat() -and lstat() functions, replacing them with versions that return +This module's default exports override the core stat() +and lstat() functions, replacing them with versions that return "File::stat" objects. This object has methods that return the similarly named structure field name from the L function; namely, @@ -262,7 +262,7 @@ mtime, ctime, blksize, and -blocks. +blocks. As of version 1.02 (provided with perl 5.12) the object provides C<"-X"> overloading, so you can call filetest operators (C<-f>, C<-x>, and so diff --git a/lib/File/stat.t b/lib/File/stat.t index a51e08cb7fff..081779aa017a 100644 --- a/lib/File/stat.t +++ b/lib/File/stat.t @@ -154,28 +154,28 @@ for (split //, "tTB") { } SKIP: { - skip("Could not open file: $!", 2) unless $canopen; - isa_ok(File::stat::stat('STAT'), 'File::stat', - '... should be able to find filehandle'); - - package foo; - local *STAT = *main::STAT; - my $stat2 = File::stat::stat('STAT'); - main::isa_ok($stat2, 'File::stat', - '... and filehandle in another package'); - close STAT; - -# VOS open() updates atime; ignore this error (posix-975). - my $stat3 = $stat2; - if ($^O eq 'vos') { - $$stat3[8] = $$stat[8]; - } + skip("Could not open file: $!", 2) unless $canopen; + isa_ok(File::stat::stat('STAT'), 'File::stat', + '... should be able to find filehandle'); + + package foo; + local *STAT = *main::STAT; + my $stat2 = File::stat::stat('STAT'); + main::isa_ok($stat2, 'File::stat', + '... and filehandle in another package'); + close STAT; + +# VOS open() updates atime; ignore this error (posix-975). + my $stat3 = $stat2; + if ($^O eq 'vos') { + $$stat3[8] = $$stat[8]; + } - main::skip("Win32: different stat-info on filehandle", 1) if $^O eq 'MSWin32'; + main::skip("Win32: different stat-info on filehandle", 1) if $^O eq 'MSWin32'; - main::skip("OS/2: inode number is not constant on os/2", 1) if $^O eq 'os2'; + main::skip("OS/2: inode number is not constant on os/2", 1) if $^O eq 'os2'; - main::is_deeply($stat, $stat3, '... and must match normal stat'); + main::is_deeply($stat, $stat3, '... and must match normal stat'); } SKIP: From d3e2b5710df8b82af0df1df038547ba5304d9cc7 Mon Sep 17 00:00:00 2001 From: Lukas Mai Date: Wed, 30 Jul 2025 22:33:56 +0200 Subject: [PATCH 8/9] File::stat: document/test stat \*_ syntax to reuse stat buffer I'm not sure why the "unexported and undocumented populate() function" had to be mentioned here. The more straightforward `stat(\*_)` syntax works on all perl versions back to (at least) 5.8.9 and is compatible with CORE::stat, too. --- lib/File/stat.pm | 17 +++++++++++++---- lib/File/stat.t | 12 +++++++++++- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/lib/File/stat.pm b/lib/File/stat.pm index 12cee0b96d5d..9b1eeb569f85 100644 --- a/lib/File/stat.pm +++ b/lib/File/stat.pm @@ -310,10 +310,19 @@ built-in C/C functions): =head1 BUGS -As of Perl 5.8.0 after using this module you cannot use the special -filehandle C<_> with stat() or lstat(); trying to do so leads to strange -errors. The workaround is to explicitly populate the object using the -unexported and undocumented populate() function with CORE::stat(): +The built-in C and C functions recognize the special +filehandle C<_> (underscore) to indicate that no actual C be done; +instead the results of the last C or C or filetest operation +should be returned. This syntax does not work with File::stat, but the +same result can be achieved by passing C a reference to the C<*_> +typeglob: + + use File::stat; + my $stat_obj = stat \*_; # reuse results of last stat operation + +Alternatively, another workaround is to explicitly populate the object +using the unexported and undocumented populate() function with +CORE::stat(): my $stat_obj = File::stat::populate(CORE::stat(_)); diff --git a/lib/File/stat.t b/lib/File/stat.t index 081779aa017a..7a385090ec03 100644 --- a/lib/File/stat.t +++ b/lib/File/stat.t @@ -240,7 +240,17 @@ SKIP: { # implicit $_ $_ = $file; - isa_ok stat, 'File::stat', 'stat()'; + my $st_1 = stat; + isa_ok $st_1, 'File::stat', 'stat()'; + + # reuse stat buffer + my $st_2 = stat \*_; + isa_ok $st_2, 'File::stat', 'stat(\\*_)'; + # we can't verify directly that no actual stat() was done, but we can check + # that the returned device/inode match those of $file even though *_{IO} + # (the actual _ handle) was never opened + is $st_1->dev, $st_2->dev, 'stat(\\*_)->dev matches that of last stat()'; + is $st_1->ino, $st_2->ino, 'stat(\\*_)->ino matches that of last stat()'; } # Testing pretty much anything else is unportable. From 1335adacab90a864da4c4d6775b80e4f4fac6d73 Mon Sep 17 00:00:00 2001 From: Lukas Mai Date: Wed, 30 Jul 2025 22:48:15 +0200 Subject: [PATCH 9/9] perldelta for File::stat 1.15 --- pod/perldelta.pod | 24 ++++++++++++++++++++++-- t/porting/known_pod_issues.dat | 1 + 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/pod/perldelta.pod b/pod/perldelta.pod index 25690a3852de..830944a88fcb 100644 --- a/pod/perldelta.pod +++ b/pod/perldelta.pod @@ -135,9 +135,29 @@ XXX Remove this section if F did not add any cont =item * -L has been upgraded from version A.xx to B.yy. +L has been upgraded from version 1.14 to 1.15. -XXX If there was something important to note about this change, include that here. +=over 4 + +=item * + +The overridden C and C functions now always return a scalar value, +even in list context. Previously a failed stat in list context would return an +empty list; now it returns C. + +=item * + +C and C can now be called without an argument, in which case they +will use C<$_>, just like the built-in C/C functions. + +=item * + +It is now safe to pass path objects (e.g. instances of L) to +C/C. Previously a failed stat operation on such an object would +die with a cryptic C error. +[GH #23507] + +=back =back diff --git a/t/porting/known_pod_issues.dat b/t/porting/known_pod_issues.dat index f0f31b52db8f..bb1db7311d70 100644 --- a/t/porting/known_pod_issues.dat +++ b/t/porting/known_pod_issues.dat @@ -255,6 +255,7 @@ Padre PadWalker Parse::Keyword passwd(1) +Path::Tiny pclose(3) perl(1) Perl4::CoreLibs