3
\$\begingroup\$

Raku script to open files from terminal with xdg-open and, with del option, asks for deletion. Happy to receive Raku syntax improvements. Thanks!

use v6;

#| Linux: Opens file 'pattern.ext' with xdg-open and, with del option, asks for deletion 
sub MAIN( $pattern is copy, Str $ext, Str $del? where ( $del ∈ < del nodel > ) = "nodel" ) {

    $pattern = "" if $pattern eq "all";
    my @files = '.'.IO.dir(test => /.*$pattern.*\.$ext/);

    for @files -> $file {
        my @args = 'xdg-open', $file.basename;
        my $command = run @args;
        $command.exitcode == 0 or die "system @args failed: $!";
        if $del eq 'del' {
          my $delete = prompt("\n \n Delete file $file (s/n) ");
          next if $delete ne "s";
          say "mv $file /tmp/$file";
          $file.IO.move("/tmp/$file");
        }
        my $exit = prompt("\n Press 'q' to end, return to continue ");
        last if $exit eq q{q};
    }

    @files = '.'.IO.dir(test => /.*$pattern.*$ext$/);
    say "-" x 60;
    for @files -> $file { $file.Str.say }
    say "-" x 60;
}
\$\endgroup\$
2
  • 1
    \$\begingroup\$ Well, it might be a bit cleaner if you use a multi with different values for $pattern, a multi sub MAIN( $pattern ) and multi sub MAIN("all"), with this one defined first. $pattern` is apparently only used to define a glob, so you might want to use that instead. . Also a multi for values of del but if that works for you, it's OK. What kind of improvement were you looking for, exactly? \$\endgroup\$
    – jjmerelo
    Sep 3 '20 at 16:45
  • 1
    \$\begingroup\$ Thank you very much! Even though it worked, he wanted to incorporate Raku's logic. Not used to the multi sub: a great improvement! Great! \$\endgroup\$
    – Mimosinnet
    Oct 6 '20 at 18:25
3
\$\begingroup\$

If I just run your program without any arguments, this is what I get:

Usage:
  foo.raku <pattern> <ext> [<del>] -- Linux: Opens file 'pattern.ext' with xdg-open and, with del option, asks for deletion

That's not super helfpul, so let's tackle that first.

#| Linux: Opens file 'pattern.ext' with xdg-open and, with del option, asks for deletion 
sub MAIN(
    $pattern is copy, #= The pattern to match against (use 'all' to match everything)
    Str $ext, #= The extension for the file
    Str $del? where ( $del ∈ < del nodel > ) #= Options for opening ('del' or 'nodel', defaults to 'nodel')
        = "nodel" 
) {
   ...
}

This produces much nicer output:

Usage:
  foo.raku <pattern> <ext> [<del>] -- Linux: Opens file 'pattern.ext' with xdg-open and, with del option, asks for deletion
  
    <pattern>    The pattern to match against (use 'all' to match everything)
    <ext>        The extension for the file
    [<del>]      Options for opening ('del' or 'nodel', defaults to 'nodel')

One thing you'll note is that the #= syntax doesn't play nicely with default values, so while I normally prefer using #= for parameters, the #| might be better when you have defaults. I personally like to let it breathe a bit more when doing this, but YMMV.

#| Linux: Opens file 'pattern.ext' with xdg-open and, with del option, asks for deletion 
sub MAIN(

    #| The pattern to match against (use 'all' to match everything)
    $pattern,

    #| The extension for the file
    Str $ext,

    #| Options for opening ('del' or 'nodel', defaults to 'nodel')
    Str $del? where {$del <del nodel>} = "nodel"
) {
    ...
}

You'll notice I adjust the $del where clause a bit. Where clauses expect a block, and using parentheses like you did along with the default value can cause problems IME where the equals sign gets gobbled into the block implied block, so explicit blocks are safest. Because you've set a default value, you don't need to mark it as explicitly optional.

Now let's look at your main code. First we have the line

my @files = '.'.IO.dir(test => /.*$pattern.*\.$ext/);

First, instead of '.'.IO, you can use $*CWD, which identifies the purpose a bit better. Your regex pattern is a bit off also. Let's comment it and you'll see what's going on:

    my @files = $*CWD.dir:
        test => /
            .*        # match any characters
            $pattern  # then the pattern
            .*        # then any characters
            \.        # then literal period 
            $ext      # then the extension
        /);

Regexen declared with / / aren't anchored to the start or end, so the initial .* isn't necessary. OTOH, you will want an explicit end so you can do

    my @files = '.'.IO.dir:
        test => /
            $pattern  # the pattern
            .*        # then any characters
            \.        # then literal period 
            $ext      # then the extension
            $         # and end the filename
        /);

It might add a few lines to your code, but commenting regex is a very good idea and can help you catch bugs sometimes.

Now in your main logic block

    for @files -> $file {
        my @args = 'xdg-open', $file.basename;
        my $command = run @args;
        $command.exitcode == 0 or die "system @args failed: $!";
        if $del eq 'del' {
          my $delete = prompt("\n \n Delete file $file (s/n) ");
          next if $delete ne "s";
          say "mv $file /tmp/$file";
          $file.IO.move("/tmp/$file");
        }
        my $exit = prompt("\n Press 'q' to end, return to continue ");
        last if $exit eq q{q};
    }

Separating out your arguments for run into a separate variable is nice, although obviously not strictly necessary. Because the result of run is truthy based on its success, and we don't use the exit code anywhere else, we can just put the run inside the unless clause. If that's compacting too much into too small of a space for you, or if you'll need to use the exit code for later expansions of the script, you could use my $command = run @args; fail '...' unless $command. (ht Brad in comments).

One thing to consider is if you really want to die. I don't know the xdg-open command personally, so not sure its mechanics. If you do need to die (terminates the entire script), then by all means do so. But since each file is being handled separately, perhaps you'd prefer to warn the user and continue with the others. In that case, you could use a fail, optionally with a CATCH handler to provide more detailed support:

    for @files -> $file {
        my @args = 'xdg-open', $file.basename;

        fail "system @args failed: $!" 
            unless run @args;

        if $del eq 'del' {
            my $delete = prompt("\n \n Delete file $file (s/n) ");
            next if $delete ne "s";
            say "mv $file /tmp/$file";
            $file.IO.move("/tmp/$file");
        }

        my $exit = prompt("\n Press 'q' to end, return to continue ");
        last if $exit eq 'q';

        CATCH {
           .message.say;
           next if prompt "Would you like to continue processing other files?" eq "y";
           exit 0;
        }
    }

You'll notice I also gave it a little bit of extra space. Think of it like separating paragraphs in your code. I would recommend against using q{q} to match q, just use 'q'. The special quote structures have their place, but this doesn't feel like one of them.

For the final bit of code, the same thing I mentioned about the regex will apply. For the output:

    say "-" x 60;
    for @files -> $file { $file.Str.say }
    say "-" x 60;

Works well, but the middle line can be greatly simplified to .Str.say for @files.

\$\endgroup\$
2
  • 1
    \$\begingroup\$ There is also no need to directly check the exit code. The result of calling run automatically does the right thing in Bool context. fail "…" unless $command; That is Proc.Bool() is exactly the same as Proc.exitcode == 0 \$\endgroup\$ Oct 9 '20 at 19:27
  • \$\begingroup\$ @BradGilbert Ah yes, I forgot about that. \$\endgroup\$ Oct 9 '20 at 20:34

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.