Home > Raku > Augmenting with Exitcode

Augmenting with Exitcode

In my last post I found a nice way to match against an Exitcode. I wanted to extend that to matching against STDERR if exitcode is non-zero. I already got a way to capture all error streams of a pipe.

px<find /tmp» |» px<your-script-here> |» @a :stderr(Capture);

I’m using Capture (the type object) in the same way as we use * or Whatever. It just indicated that magic stuff should happen. That magic boils down to sticking all STDERR streams into a 2-dimensional array. If I want to handle errors I might want to match against the exitcode, the name of the shell-command and of parts of it’s output to STDERR. A syntax like the following would be nice.

my $ex = Exitcode.new: :STDERR(<abc def ghi>), :exitint(42), :command<find>;
given $ex {
    when ‚find‘ & 42 & /def\s(\S+)/ {
        note „find terminated with 42 and $0“;
    }
}

As it turns out getting the match against Str, Numeric and Regex in a Junction is easily done. All we need to do is augmenting Regex.

augment class Regex {
    multi method ACCEPTS(Regex:D: Shell::Piping::Exitcode:D $ex) {
        ?$ex.STDERR.join(„\n“).match(self)
    }
}

augment class Int {
    multi method ACCEPTS(Int:D: Shell::Piping::Exitcode:D $ex) {
        self.ACCEPTS($ex.exitint)
    }
}

augment class Str {
    multi method ACCEPTS(Str:D: Shell::Piping::Exitcode:D $ex) {
        self.ACCEPTS($ex.command)
    }
}

This only works for matching. I don’t get (\S+) to capture into $0 that way. We know and love that Str.match does do that – seemingly with ease. Let’s steal codelearn from it!

proto method match(|) { $/ := nqp::getlexcaller('$/'); {*} }

So the first thing .match is doing is to bind it’s local $/ to the caller’s one. Thus any changes to the local version will actually change the caller’s. I tried to mimic that and it didn’t work. Neither the nqp-way nor the slightly cleaner Raku way.

augment class Regex {
    multi method ACCEPTS(Regex:D: Shell::Piping::Exitcode:D $ex) {
         CALLER::<$/> := $/;

        ?$ex.STDERR.join(„\n“).match(self)
    }
}

At least not in the given/when block. A simply say $ex ~~ def\s(\S+); in the global scope did work just fine. I even got an error message that \$ does not exist in OUTER. Given that it should exist in every block by definition, that was rather strange.

We can inspect the lexical scope of the caller with the following construct.

say CALLER::.keys;
say Backtrace.new.gist;

This will output the lexicals and how many stack frames there are. And indeed, given/when does introduce an additional stack frame that contains $_ but not $/ (and a few other bits and bobs). Since CALLER is a Stash, what is turn halve a Hash we can use :exists as usual and add another caller.

augment class Regex {
    multi method ACCEPTS(Regex:D: Shell::Piping::Exitcode:D $ex) {
        CALLER::<$/>:exists ?? (CALLER::<$/> := $/) !! (CALLER::CALLER::<$/> := $/);

        ?$ex.STDERR.join(„\n“).match(self)
    }
}

Now it works as envisioned.

However – actually HOWEVER – I am augmenting a buildin class. That is risky. I am not alone. We got an ircbot that can grep in the source codes of the ecosystem.

20:01 < gfldex> greppable6: augment class
20:01 < greppable6> gfldex, 49 lines, 26 modules: https://gist.github.com/4088b5b8e7b51d94276b15500c240a5f

When we write modules we introduce scopes where our custom names reside in. The user of a module can decide to import those names into a scope under the users control. When we augment we introduce a new name into a globalish scope. What happens if two modules have the same idea? If the injected method is actually a multi then it will most likely work. But it does not have to. When two or more multi candidates have the same precedence, the first one found will win. By using augment on a non-multi we got a chance to get an error message. If we add a method via MOP we wont. If I want to allow smart match in a when statement or block, I need to provide the ability to have my custom class on the LHS of ~~. So there is no way around augment. I would even go so far as to say that this is the reason augment was added to the design of the language. It gets worse when we consider Raku to get less young and the language version keeps increasing. A module with sloppy tests might collide with a new method added with a language release. Should we enforce a use statement with a language version in a compunit with an augment statement?

This is really bothering me. We can of cause use META6 and add the field augments:"Cool,Int,Regex". That way zef would have a chance to spot collisions and provide a warning. Sadly, there is no way to enforce this (becaue of EVAL). I will spend some more time thinking about this and might start a problem solving issue.

Categories: Raku
  1. No comments yet.
  1. August 3, 2020 at 15:17

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: