From 191ba2990581407692942945664a59f60ee2c8d7 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 15 Jun 2024 12:09:12 +0200 Subject: [PATCH 1/6] Add change_extension utility method to rename the file extension of a file Motivated by C# Path.ChangeExtension method --- lib/Path/Tiny.pm | 44 +++++++++++++++++++++++++++++++++++++++++ t/change_extension.t | 47 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 t/change_extension.t diff --git a/lib/Path/Tiny.pm b/lib/Path/Tiny.pm index b721f78..1573d9d 100644 --- a/lib/Path/Tiny.pm +++ b/lib/Path/Tiny.pm @@ -2415,6 +2415,50 @@ sub volume { return $self->[VOL]; } +=method change_extension + + my $foo = path('C:/mydir/myfile.com.extension'); + my $renamed_foo = $foo->change_extension('.old'); + +Changes the extension of a path. + +The argument is the new extension (with or without a leading period). +Specify undef to remove an existing extension from the path. + +Returns a Path::Tiny object. + +If extension is C, the returned string contains the specified path with its extension removed. +If path has no extension, and extension is not C, the returned path string contains extension appended to the end of path. + +Current API available since 0.148. + +=cut + +sub change_extension { + my $self = shift; + my $new_extension = shift; # may be undef + + my $path_str = $self->stringify; + $path_str =~ s/\.[^.\/]+$//; # Remove existing extension + + # If extension is undef, the returned string contains the specified path with its extension removed. + return path($path_str) unless defined $new_extension; + + if( $new_extension !~ m/^\./ ) { + # add leading period if there is no period + $new_extension = '.' . $new_extension; + } + + $path_str .= $new_extension; # Add new extension + + # Construct the new path + my $new_path = path($path_str); + + return $new_path; +} + + + package Path::Tiny::Error; our @CARP_NOT = qw/Path::Tiny/; diff --git a/t/change_extension.t b/t/change_extension.t new file mode 100644 index 0000000..3c1ef84 --- /dev/null +++ b/t/change_extension.t @@ -0,0 +1,47 @@ +use 5.008001; +use strict; +use warnings; +use Test::More 0.96; + +use lib 't/lib'; + +use Path::Tiny; + + +{ + my $renamed1 = path("C:/mydir/myfile.com.extension")->change_extension(".old"); + ok($renamed1->stringify eq 'C:/mydir/myfile.com.old', 'rename to .old with leading perdiod'); + + my $renamed2 = path("C:/mydir/myfile.com.extension")->change_extension("old"); + ok($renamed2->stringify eq 'C:/mydir/myfile.com.old', 'rename to .old without leading perdiod'); +} + +{ + my $removed_extension1 = path("C:/mydir/myfile.com.extension")->change_extension(undef); + ok($removed_extension1->stringify eq 'C:/mydir/myfile.com', 'remove extension'); +} + +{ + # test for invalid renames of files starting with a period such as .htaccess + my $died = 0; + eval { + path('.htaccess')->change_extension(undef); + }; + if ($@) { + $died = 1; + } + ok($died, 'Remove extension from file starting with period (and no further etxension) dies as expected'); +} + +{ + my $dir1 = path("C:/mydir/lookslikedorectory")->change_extension(undef); + ok($dir1->stringify eq 'C:/mydir/lookslikedorectory', 'directory names without period are kept when removing suffix'); + + my $dir2 = path("C:/mydir/lookslikedirectory")->change_extension(".exten"); + ok($dir2->stringify eq 'C:/mydir/lookslikedirectory.exten', 'directory names are extended when adding suffix'); + + my $dir3 = path("C:/mydir/lookslikedirectory")->change_extension("exten"); + ok($dir3->stringify eq 'C:/mydir/lookslikedirectory.exten', 'directory names are extended when adding suffix'); +} + +done_testing; From 0c7dc420537f71e2fed329ad5fbf0a7717b76cf6 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 15 Jun 2024 21:24:44 +0200 Subject: [PATCH 2/6] replace directory wording by path Except in very specific cases, Path::Tiny does not distinguish between "files" and "directories" because it works on paths without reference to the filesystem. Source: https://github.com/dagolden/Path-Tiny/pull/292#discussion_r1641160259 --- t/change_extension.t | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/t/change_extension.t b/t/change_extension.t index 3c1ef84..368b041 100644 --- a/t/change_extension.t +++ b/t/change_extension.t @@ -34,14 +34,14 @@ use Path::Tiny; } { - my $dir1 = path("C:/mydir/lookslikedorectory")->change_extension(undef); - ok($dir1->stringify eq 'C:/mydir/lookslikedorectory', 'directory names without period are kept when removing suffix'); + my $path_wo_extension1 = path("C:/mydir/pathwithoutextension")->change_extension(undef); + ok($path_wo_extension1->stringify eq 'C:/mydir/pathwithoutextension', 'paths without period are kept as is when removing suffix'); - my $dir2 = path("C:/mydir/lookslikedirectory")->change_extension(".exten"); - ok($dir2->stringify eq 'C:/mydir/lookslikedirectory.exten', 'directory names are extended when adding suffix'); + my $path_wo_extension2 = path("C:/mydir/pathwithoutextension")->change_extension(".exten"); + ok($path_wo_extension2->stringify eq 'C:/mydir/pathwithoutextension.exten', 'paths are extended when adding suffix'); - my $dir3 = path("C:/mydir/lookslikedirectory")->change_extension("exten"); - ok($dir3->stringify eq 'C:/mydir/lookslikedirectory.exten', 'directory names are extended when adding suffix'); + my $path_wo_extension3 = path("C:/mydir/pathwithoutextension")->change_extension("exten"); + ok($path_wo_extension3->stringify eq 'C:/mydir/pathwithoutextension.exten', 'paths are extended when adding suffix'); } done_testing; From e5e936e93f6fb6a0ffff1ed5a3003feb7bb89f4e Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 15 Jun 2024 21:37:16 +0200 Subject: [PATCH 3/6] combine path extension and path construction Cf. https://github.com/dagolden/Path-Tiny/pull/292#discussion_r1641145534 --- lib/Path/Tiny.pm | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/Path/Tiny.pm b/lib/Path/Tiny.pm index 1573d9d..9ebc371 100644 --- a/lib/Path/Tiny.pm +++ b/lib/Path/Tiny.pm @@ -2449,10 +2449,8 @@ sub change_extension { $new_extension = '.' . $new_extension; } - $path_str .= $new_extension; # Add new extension - - # Construct the new path - my $new_path = path($path_str); + # Add extension and construct the new path + my $new_path = path($path_str . $new_extension); return $new_path; } From 646e65b205a5593e7a58c8d8c74ba6dc51f0148b Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 15 Jun 2024 21:38:35 +0200 Subject: [PATCH 4/6] For paths created by functions, use _path instead of path. Cf. https://github.com/dagolden/Path-Tiny/pull/292#discussion_r1641145567 --- lib/Path/Tiny.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Path/Tiny.pm b/lib/Path/Tiny.pm index 9ebc371..c255acf 100644 --- a/lib/Path/Tiny.pm +++ b/lib/Path/Tiny.pm @@ -2450,7 +2450,7 @@ sub change_extension { } # Add extension and construct the new path - my $new_path = path($path_str . $new_extension); + my $new_path = _path($path_str . $new_extension); return $new_path; } From 166617ad936c9be96b606166ace02ebdbf2681be Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 15 Jun 2024 21:46:42 +0200 Subject: [PATCH 5/6] Make documentation more consice First iteration, might need more work. Cf. https://github.com/dagolden/Path-Tiny/pull/292#discussion_r1641149976 --- lib/Path/Tiny.pm | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/Path/Tiny.pm b/lib/Path/Tiny.pm index c255acf..52ee15d 100644 --- a/lib/Path/Tiny.pm +++ b/lib/Path/Tiny.pm @@ -2422,13 +2422,11 @@ sub volume { Changes the extension of a path. -The argument is the new extension (with or without a leading period). -Specify undef to remove an existing extension from the path. +Returns a new Path::Tiny object with a different extension. The argument is a +string representing the new extension or undef to remove an extension. -Returns a Path::Tiny object. - -If extension is C, the returned string contains the specified path with its extension removed. If path has no extension, and extension is not C, the returned path string contains extension appended to the end of path. +If the last part of a path has a leading period (e.g. C<~/.bashrc>), it is not considered an extension. Current API available since 0.148. From 91c1452a68d5db4a803c7504a080b5d588bd37ac Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 23 Jun 2024 21:06:13 +0200 Subject: [PATCH 6/6] Convert to table-driven test Makes it easier to extend with new cases. Cf. https://github.com/dagolden/Path-Tiny/pull/292#discussion_r1641151497 --- t/change_extension.t | 80 +++++++++++++++++++++++++++----------------- 1 file changed, 50 insertions(+), 30 deletions(-) diff --git a/t/change_extension.t b/t/change_extension.t index 368b041..6b95a9c 100644 --- a/t/change_extension.t +++ b/t/change_extension.t @@ -7,41 +7,61 @@ use lib 't/lib'; use Path::Tiny; +my @cases = ( + # path1 => path2 => path1->subsumes(path2) -{ - my $renamed1 = path("C:/mydir/myfile.com.extension")->change_extension(".old"); - ok($renamed1->stringify eq 'C:/mydir/myfile.com.old', 'rename to .old with leading perdiod'); - - my $renamed2 = path("C:/mydir/myfile.com.extension")->change_extension("old"); - ok($renamed2->stringify eq 'C:/mydir/myfile.com.old', 'rename to .old without leading perdiod'); -} + "rename path with extension" => [ + [ '.', '.ext', '.ext' ], + [ '/', '.ext', '/.ext' ], + [ '..', '.ext', '..ext' ], + [ '../..', '.ext', '../..ext' ], + [ '/foo/', '.ext', '/foo.ext' ], # differs from C#: /foo/.ext + [ '/foo', '.ext', '/foo.ext' ], + [ 'foo/', '.ext', 'foo.ext' ], # differs from C#: foo/.ext + [ './foo', '.ext', 'foo.ext' ], # differs from C#: ./foo.ext + [ 'foo/.', '.ext', 'foo.ext' ], # differs from C#: foo/.ext + [ 'C:/temp/myfile.com.extension', '.old', 'C:/temp/myfile.com.old' ], + [ 'C:/temp/myfile.com.extension', 'old', 'C:/temp/myfile.com.old' ], + [ 'C:/pathwithoutextension', '.old', 'C:/pathwithoutextension.old' ], + [ 'C:/pathwithoutextension', 'old', 'C:/pathwithoutextension.old' ], + # ~ paths + ], -{ - my $removed_extension1 = path("C:/mydir/myfile.com.extension")->change_extension(undef); - ok($removed_extension1->stringify eq 'C:/mydir/myfile.com', 'remove extension'); -} + "remove extension" => [ + [ '.', undef, '' ], + [ '/', undef, '/' ], + [ '..', undef, '.' ], + [ '../..', undef, '../.' ], + [ '/foo/', undef, '/foo' ], # differs from C#: /foo/ + [ '/foo', undef, '/foo' ], + [ 'foo/', undef, 'foo' ], # differs from C#: foo/ + [ './foo', undef, 'foo' ], # differs from C#: ./foo + [ 'foo/.', undef, 'foo' ], # differs from C#: foo/ + [ 'C:/temp/myfile.com.extension', undef, 'C:/temp/myfile.com' ], + [ 'C:/temp/myfile.com.extension', undef, 'C:/temp/myfile.com' ], + [ 'C:/pathwithoutextension', undef, 'C:/pathwithoutextension' ], + [ 'C:/pathwithoutextension', undef, 'C:/pathwithoutextension' ], + ], -{ - # test for invalid renames of files starting with a period such as .htaccess - my $died = 0; - eval { - path('.htaccess')->change_extension(undef); - }; - if ($@) { - $died = 1; - } - ok($died, 'Remove extension from file starting with period (and no further etxension) dies as expected'); -} +); -{ - my $path_wo_extension1 = path("C:/mydir/pathwithoutextension")->change_extension(undef); - ok($path_wo_extension1->stringify eq 'C:/mydir/pathwithoutextension', 'paths without period are kept as is when removing suffix'); - - my $path_wo_extension2 = path("C:/mydir/pathwithoutextension")->change_extension(".exten"); - ok($path_wo_extension2->stringify eq 'C:/mydir/pathwithoutextension.exten', 'paths are extended when adding suffix'); + + +while (@cases) { + my ( $subtest, $tests ) = splice( @cases, 0, 2 ); - my $path_wo_extension3 = path("C:/mydir/pathwithoutextension")->change_extension("exten"); - ok($path_wo_extension3->stringify eq 'C:/mydir/pathwithoutextension.exten', 'paths are extended when adding suffix'); + subtest $subtest => sub { + for my $t (@$tests) { + my ( $path1, $ext, $path2 ) = @$t; + my $label = sprintf("%s + %s -> %s", $path1, (defined $ext ? $ext : 'undef'), $path2); + my $changed_path = path($path1)->change_extension($ext); + ok( $changed_path->stringify eq $path2, $label ) + or diag "PATH 1:\n", explain( path($path1) ), "\nCHANGED PATH:\n", explain( $changed_path ), "\nPATH2:\n", + explain( path($path2) ); + } + }; } +ok(1); + done_testing;