2

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.

  • Does this answer your question? [How do I pass Shell parameters to Perl script?](https://stackoverflow.com/questions/13973285/how-do-i-pass-shell-parameters-to-perl-script) – Digvijay S Apr 29 '20 at 04:56
  • Also check --> https://stackoverflow.com/questions/6964536/accessing-shell-variable-in-a-perl-program – Digvijay S Apr 29 '20 at 04:59
  • 1
    The multi-argument form of `system` does not run a shell at all, so no shell syntax whatsoever is honored. Anyhow, why would you use shell PEs when you have Perl? Perl is far more expressive. – Charles Duffy Apr 29 '20 at 04:59
  • Hi, Thanks for the information. I am new to Perl and Bash scripting. I managed to perform the task in bash but I am not able to achieve the task in Perl. Is there any solution to this.? – Leon S. Kennedy Apr 29 '20 at 05:05
  • Use `move` from the core [`File:: Copy`](https://perldoc.perl.org/5.30.0/File/Copy.html) module, which, unlike [`rename`](https://perldoc.perl.org/5.30.0/functions/rename.html) will even work across filesystem boundaries. – Shawn Apr 29 '20 at 05:27
  • Hi @Shawn, thanks for the information. I used move: `move("$a/$fileName", "$a/$fileName".'.png');`. It simply appends the `.png` in addition to existing extension. Like `Paper.p.png`. I need to replace `.p` with `.png`. – Leon S. Kennedy Apr 29 '20 at 05:42
  • Well, the quick and dirty way (NOT recommended) if you're on an unix-like system would be to use `system('bash -c mv', "$a/$fileName", "$a/${fileName%.*}".'.png');`, but it's very dirty and no-one should do that. – ChatterOne Apr 29 '20 at 05:51
  • @CharlesDuffy According to https://perldoc.perl.org/functions/system.html it doesn't use a shell only `If there are no shell metacharacters in the argument`, otherwise it's passed to `/bin/sh -c ` on unix platforms – ChatterOne Apr 29 '20 at 05:53
  • Hi @ChatterOne. The NOT recommended way is giving the same error as mine. It has a problem with `%` symbol. – Leon S. Kennedy Apr 29 '20 at 06:04
  • 1
    First of all, there is no guarantee that `system` will use `bash`, so if you want to make sure that you have `bash` executing the code, you should do it explicitly. But since you do have parameter expansion in Perl (not in the full extent as available in _bash_, but at least to the extent as available in POSIX shellx) using the [glob](https://perldoc.perl.org/functions/glob.html) function, you could consider writing your code completely inside Perl, without creating a child process. – user1934428 Apr 29 '20 at 06:53
  • Hi @user1934428. Thanks for the guidance. – Leon S. Kennedy Apr 29 '20 at 06:55
  • @LeonS.Kennedy I didn't test it, so I'm not surprised that there's something to fix. Probably a bit of escaping would make it work, but you have a much better way of doing it in the answer. – ChatterOne Apr 29 '20 at 07:11

3 Answers3

4

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.

zdim
  • 64,580
  • 5
  • 52
  • 81
  • Thank you for your help. I will implement your solution. Please do not delete your answer. Many Thanks! – Leon S. Kennedy Apr 29 '20 at 06:07
  • @LeonS.Kennedy I'll only add to it, a couple more ways to do this :) – zdim Apr 29 '20 at 06:10
  • Thank you for your effort and explanation. It worked, Finally. Earlier, when I enclosed the move function in brackets then it was giving an error `"Useless use of private variable in void context"` then I removed the brackets and it ran. Any idea why it was saying "useless use"? Anyways, it worked. Many Thanks!!! – Leon S. Kennedy Apr 29 '20 at 06:39
  • @LeonS.Kennedy hm ... how precisely did you "enclose" ? it should work with parens of course (I just find it cleaner without them, that's why I leave them out). thank you for attribution -- stay tuned, I'm about to add... – zdim Apr 29 '20 at 06:47
  • At first I did like `move ("$a/$fileName", "$a/$new_filename" or die "Can't move: $!");` in addition to "useless use" error it was also saying `Usage: move(FROM, TO)` error. Then I removed the parentheses and it worked like a charm. :) – Leon S. Kennedy Apr 29 '20 at 06:51
  • @LeonS.Kennedy Ah -- need `move($old, $new) or die "...";` The `die` is `or`-ed with the `move(...)` (the way you had it it's `move $old, $new-or-die`). Added more to the post. – zdim Apr 29 '20 at 07:25
  • Thanks! I will observe your solution with more precision. (Y) – Leon S. Kennedy Apr 29 '20 at 07:56
1

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
Boying
  • 1,404
  • 13
  • 20
  • Thank you for your answer. I am new to Perl and I am understanding your answer and implementing it bit by bit. – Leon S. Kennedy Apr 29 '20 at 06:03
  • That use (`fileparse($f_old,qr/\.[^.]*/);`) returns only the bare name (without extension), without the path. That's not good for renaming files. Also, I'd recommend against `rename` -- way too many details to account for. See docs for it. – zdim Apr 29 '20 at 07:22
  • @zdim Thanks for the suggestion, I updated my answer a bit. I have not used Perl for a while, seems TIMTOWTDI days has gone :) – Boying Apr 29 '20 at 07:42
  • An interesting point! While TIMTOWTDI is alive and well of course, I'd say that indeed people now make a distinction that some ways are better than others. (note: way**s**. plural :) – zdim Apr 29 '20 at 07:53
  • @zdim Agree! and perlers are always kind :) – Boying Apr 29 '20 at 08:19
0

You have double-quoted strings in Perl, which means that $variables are interpolated inside Perl. It is like writing in Perl

my $xx="abc"
my $yy=${xx%.*}

You would get the same error.

user1934428
  • 19,864
  • 7
  • 42
  • 87