How to use Shell Parameter Expansion in Perl?

I am trying to implement a Bash parameter expansion technique to replace the existing extension of a file in a Perl script.

This is my bash code for replacing the file extension which is working fine:

mv "$f" "${f%.*}.png"

On the other hand, I am trying to use it inside a Perl script using system():

system('mv', "$a/$fileName", "$a/${fileName%.*}".'.png');

$a holds the directory name which is being passed via command-line argument.

It throws "syntax error near %" followed by "missing right curly or square bracket within string". There's no EOF in the error message.

When I do not use %.* then it simply appends .png after the existing file ending but it does not replace. Some help will be highly appreciated.


So you wish to replace the extension -- to change a filename. There is no reason to reach for the shell for that once you are in a Perl script.

Changing an extension is easily done "by hand" (see below for libraries). With regex

my $new_filename = $filename =~ s/.*\.\K.*/$new_extension/r;

The .* greedily matches up to the very last . (that follows in the pattern), and \K then drops all matches so that we replace only what follows after it. Or, use the end-of-string anchor $

my $new_filename = $filename =~ s/\.\K[^.]+$/$new_extension/r;

what matches, and replaces, all non-. characters at the end of the string after a .. In both cases the modifier /r makes it return the new string and leave the original unchanged; without it the string ($filename) would be changed in-place.

Now change the name of the file on disk. A standard tool is File::Copy

use File::Copy qw(move);

move $filename, $new_filename  or die "Can't move: $!";

(If you notice the builtin rename stay calm and walk past it. See its linked page for reasons.)


The most solid way in general is by using suitable libraries of course, and there are good ones that can be leveraged for this. Here it is with the useful Path::Tiny (which you need to install)

use Path::Tiny;

my $path = path($filename);

my $new_fqn = $path->parent->child( $path->basename(qr/[^.]+/) . $new_ext );

See linked documentation for this module but here is a brief explanation of the above.

The method parent returns the path up to the last component (file/directory), as a Path::Tiny object. Then the method child called on it adds another component to it.

For that we get the basename (the last component, with the extension stripped in this use), and then append the new extension to it. And voila, we get back the whole path with the extension replaced. This does parse the path twice, once for parent and once for basename.

An additional benefit of this is that the module has a move method as well, so

$path->move($new_fqn);

finishes the job.

The module checks for errors and either croaks or throws an exception of its own class.


The old and tried UNIX way is by using the core File::Basename

use File::Basename;

my ($name, $path, $ext) = fileparse($filename, qr/\.[^.]*/); 

my $new_fqn = "$path/$name.$new_ext";

Then use File::Copy::move to rename the file.


Do not use Bash and Perl syntax at the same time, both langauge has obscure syntax, using them at the same time make things worse.

Bash:

"${f%.*}.png"

Perl: Use File::Basename module https://perldoc.perl.org/File/Basename.html

$f_new=fileparse($f_old,qr/\.[^.]*/);
$f_new.=".png";

Bash:

mv

Perl:

rename / File::Copy (see comment)

Put together(demo in one-line-Perl): To Rename foo.jpg to foo.png

Using rename:

$ perl -MFile::Basename -e '$f_old=$ARGV[0];$f_new=fileparse($f_old,qr/\.[^.]*/);$f_new.=".png";rename $f_old, $f_new' foo.jpg

Using File::Copy

$ perl -MFile::Copy -MFile::Basename -e '$f_old=$ARGV[0];$f_new=fileparse($f_old,qr/\.[^.]*/);$f_new.=".png";File::Copy::move $f_old, $f_new' foo.jpg