TL;DR

In the post about Text::GitIgnore we left with a possible issue on the provided matcher, here we discuss a possible way forward.

So I decided to work on a patch.

A lot of false things

Initially, I wanted to leverage the fact that Perl has at least four ways of returning a false value:

$ perl -E '(say $_ ? "true" : "false") for undef, "", 0, "0", 1'
false
false
false
false
true

So it would be possible to tweak the function to return a different false value depending on the specific condition, like the empty string when there is not an explicit match, and the number 0 when the path is excluded because of a negated pattern line.

This would be straighforward by the way:

$ git diff
diff --git a/lib/Text/Gitignore.pm b/lib/Text/Gitignore.pm
index fe61033..b4fcb51 100644
--- a/lib/Text/Gitignore.pm
+++ b/lib/Text/Gitignore.pm
@@ -80,7 +80,7 @@ sub build_gitignore_matcher {
     return sub {
         my $path = shift;
 
-        my $match = 0;
+        my $match = '';
 
         foreach my $pattern (@patterns_re) {
             my $re = $pattern->{re};
@@ -93,7 +93,7 @@ sub build_gitignore_matcher {
                 }
             }
             else {
-                $match = !!( $path =~ m/$re/ );
+                $match = 1 if $path =~ m/$re/;
 
                 if ( $match && !@negatives ) {
                     return $match;

But… I don’t know. I don’t even know if it’s going to break anything, although I strongly suspect it doesn’t because the current function already can return either a 0 or an empty string (by means of the line $match = !!( $path =~ m/$re/ );).

Fact is that this overloading seems hacky.

Pass the values?

The other idea I’m toying with now involves letting the user pass values to be returned in the three cases, inside an options anonymous hash passed as the second input parameter:

sub build_gitignore_matcher {
    my ($patterns, $args) = @_;

    if (defined $args) {
        if (ref($args) ne 'HASH') {
            my $msg = "pass multiple patterns as an array reference";
            if (eval "use Carp 'croak';") {
                croak $msg;
            }
            else {
                die "$msg\n";
            }
        }
    }
    else {
        $args = {};
    }

    # ...

This also allows addressing an annoyance on build_gitignore_matcher: if you pass the lines without enclosing them in an anonymous array, only the first will be considered but no error will come out…

By the way, I don’t know if the best thing here would be to recommend loading module Carp anyway.

Later, the idea is to leverage three variables holding the different truth values, defaulting to the current ones:

    # ...

    my $TRUE = exists($args->{true}) ? $args->{true} : 1;
    my $FALSE_UNMATCHED = exists($args->{false_from_no_match})
      ? $args->{false_from_no_match} : 0;
    my $FALSE_NEGATED = exists($args->{false_from_negated})
      ? $args->{false_from_negated} : 0;

    # ...

Last, the idea is to leave the basic logic around $match untouched and use another variable $retval to track the value to be returned:

    return sub {
        my $path = shift;

        my $match = 0;
        my $retval = $FALSE_UNMATCHED;

        foreach my $pattern (@patterns_re) {
            my $re = $pattern->{re};

            next if $match && !$pattern->{negative};

            if ( $pattern->{negative} ) {
                if ( $path =~ m/$re/ ) {
                    $match = 0;
                    $retval = $FALSE_NEGATED;
                }
            }
            else {
                if ( $path =~ m/$re/ ) {
                    $match = 1;
                    $retval = $TRUE;
                }

                if ( $match && !@negatives ) {
                    return $retval;
                }
            }
        }

        return $retval;
    };

In this way:

  • by default, the behaviour is exactly as before. Well, not entirely: the only false value returned would be 0 and never the empty string;

  • the consumer of this function can set its own return values, even to all-true strings like matched, unmatched, excluded.

This seems better, although a little too flexible than needed maybe?

In conclusion

These were only two alternatives, other exist of course:

  • set a global option to trigger the first behaviour

  • default to the value of $match unless a different truth value is passed explicitly

  • anything else…

Comments welcome!