irssi softignore script

A few weeks ago, my friend Steven wrote about a softignore script he wrote for X-Chat. The idea is that instead of completely blocking what a user says, the script captures their text and colors it in a way that the text “blends” with the background, so you can more easily ignore what these people say.

I’m an irssi fan myself, but I was a bit jealous of his script and quite surprised not to find anything like it in irssi’s script repository. So I decided to go ahead and write my own. Irssi is scripted in Perl, of which I have a little bit of knowledge, but definitely no expertise. The documentation on how to script Irssi is a bit thin; you can find a little on their web site, a little on the site of a guy who wrote a mini-tutorial and a little on the #irssi channel.

After a couple hours of hacking, I had a script that did what I wanted. There are three commands, /softignore_add, /softignore_remove and /softignore_list. The parameter to the add and remove commands is a regular expression to match the nickname. The list is kept in memory as well as in a plain text file with one regex string per line. I imagine that the more advanced Perl programmers will suggest better ways to accomplish this, but for the time being, this works well enough.

I have not made the color configurable yet, so if you need a color other than dark grey, just edit the script file and change the value of the $COLOR variable.

I implemented a few functions (slurp, spit and trim) that are most likely available in CPAN modules; however I did not want my simple script to have external dependencies, so I knowingly violated the DRY principle.

Here is the code for the script. Just copy it in ~/.irssi/scripts/ and use the /script load softignore command to load it.

use strict;
use warnings;

use Irssi qw(command_bind signal_add signal_continue command);

# Irssi globals
our $VERSION = "0.00";
our %IRSSI = (
    authors     => "Vincent Foley",
    contact     => "vfoleybourgon at yahoo dot ca",
    name        => "Soft Ignore",
    description => "Ignore users by putting their messages in a dim color.",
    license     => "MIT",
);

# Soft ignore globals
our $FILENAME = "$ENV{HOME}/.irssi/saved_softignore";
our $COLOR    = 14;
our @ignores  = slurp($FILENAME);

sub slurp {
    my ($filename) = @_;

    my $lines;
    {
        local $/;
        open(my $fd, '<', $filename) or return ();
        $lines = <$fd>;
    }
    return split(/\n/, $lines);
}

sub spit {
    my ($filename, @lines) = @_;

    open(my $fd, '>', $filename) or return;
    for my $line (@lines) {
        print {$fd} $line."\n";
    }
    close($fd);
}

sub trim {
    my ($str) = @_;

    $str =~ s/^\s*(\S+)\s*$/$1/;
    return $str;
}


sub softignore_add {
    my ($data, $server, $witem) = @_;

    push(@ignores, trim($data));
    spit($FILENAME, @ignores);
}

sub softignore_remove {
    my ($data, $server, $witem) = @_;

    @ignores = grep { $_ ne trim($data) } @ignores;
    spit($FILENAME, @ignores);
}

sub softignore_list {
    my ($data, $server, $witem) = @_;

    return unless $witem;

    if (@ignores == 0) {
        $witem->print("No soft ignore entries.");
    }
    else {
        $witem->print("Soft ignore list:");
        for my $ignore (@ignores) {
            $witem->print("* " . $ignore);
        }
    }
}

sub softignore_message {
    my ($server, $msg, $nick, $address, $target) = @_;

    for my $ignore (@ignores) {
        if ($nick =~ /$ignore/i) {
            signal_continue($server, "03$COLOR$msg",
                            $nick, $address, $target);
        }
    }
}

command_bind softignore_list   => \&softignore_list;
command_bind softignore_add    => \&softignore_add;
command_bind softignore_remove => \&softignore_remove;

signal_add "message public" => \&softignore_message;

Perl vs Lisp

Yesterday, a redditor judged important to resurrect a 7 year-old post by the colorful Usenet troll, Erik Naggum. The post itself is about Perl vs. Lisp and isn’t very interesting because Mr. Naggum is probably the most biased person to post on comp.lang.lisp.

This post generated a significant amount of comments on Reddit and I found one comment particularly interesting

Perl vs. Lisp? Seriously? They aren’t even remotely similar languages. Why not Basic vs. Assembly? Sheesh.

He has a point: syntactically, Perl and (Common) Lisp are probably the languages least similar. They do however share a significant amount of semantics, and those similarities are certainly more important than syntax. Both languages are garbage collected, are dynamically typed, can treat code as data, are lexically scoped, have closures, and many other less important features or features I am forgetting about. In fact, I think the only thing that Lisp has that Perl lacks are macros (even then, Perl 6 will have macros.)

Let’s look at some simple examples of Perl and Common Lisp code. Here is a counter function. It takes a starting value as a parameter and returns a function that increments the counter by one and returns the new value (actually, that’s not quite exact for the Perl version.)


# Perl
sub counter {
    my $start = shift;
    return sub {
        return $start++;
    }
}

my $counter = counter(0);
for (1..10) {
    print $counter->(), "\\n";
}

; Common Lisp
(defun make-counter (start)
  (decf start)
  #'(lambda ()
      (incf start)))

(let ((counter (make-counter 0)))
  (dotimes (_ 10)
    (format t "~a~%" (funcall counter))))

When you run these two programs, they return the exact same output. Now of course, you could write a program that returns the same output in any language. The real interest of these programs is that they use the same, exact method to accomplish their task. Because functions are data just like strings or numbers in both languages, a function can return another function. The solutions also depend on the language having lexical scoping, so that the values are kept around between each call. You can convert the Common Lisp code to Emacs Lisp and you’ll see that it doesn’t work, Emacs Lisp will complain that the start variable does not exist.

I could give plenty more examples of how Perl and Lisp are similar but Mark-Jason Dominus has done a better job than I could ever hope to with his Perl book, Higher-Order Perl.

Tip on using CPAN

For as long as I have used Perl (which is not *that* long, mind you) I have had some difficulties installing modules from CPAN. I never really looked into it, but yesterday I did and found an easy way to make sure things work:

Install as root

Before, I was trying to install my modules in my home directory without using sudo. Things often failed because some modules needed to modify files in /usr. Using sudo for the entire process, from the configuration of CPAN to the installation of modules fixed all my issues.

Solving Fizz Buzz more generally

The entire blogosphere is abuzz this week with the whole FizzBuzz thing. Basically, it’s a very simple programming problem that some guy gave to people who interviewed for a programming job, and most of the applicants were unable to write this simple program. Here’s the problem:

Write a program that prints the numbers from 1 to 100. But for multiples of three print “Fizz” instead of the number and for the multiples of five print “Buzz”. For numbers which are multiples of both three and five print “FizzBuzz”.

Not terribly complicated, a for loop and a few if statements using the modulo operator. Here’s how this can be solved in Python:

for i in xrange(1, 101):
    if i % 3 == 0 and i % 5 == 0:
        print "FizzBuzz"
    elif i % 3 == 0:
        print "Fizz"
    elif i % 5 == 0:
        print "Buzz"
    else:
        print i

Pretty simple. To give myself a challenge, I decided to write it in a function where I could specify the range of numbers and what to print for certain multiples. Here’s how it works, I give the function a hash table with a mapping between a multiple and a string to print. In the FizzBuzz case, that hash would be { 3 => “Fizz”, 5 => “Buzz” }. The function will find which keys of the hash the current number is divisible by. The values of those multiples will then be joined together and printed. If the number isn’t divisible by any of the keys, it will be printed instead.

Here’s the implementation in Ruby and in Perl 6.

# Ruby
def fizz_buzz(start, stop, multiples)
    start.upto(stop) do |x|
        string = multiples.values_at(*multiples.keys.sort.select { |m| x % m == 0 }).join
        if string.empty?
            puts x
        else
            puts string
        end
    end
end

fizz_buzz(1, 100, {3 => "Fizz", 5 => "Buzz"})


# Perl 6
sub fizz_buzz($start, $end, %multiples) {
    for $start .. $end -> $x {
        my @words = %multiples{ grep -> $m { $x % $m == 0}, %multiples.keys.sort };
        if @words {
            say @words;
        }
        else {
            say $x;
        }
    }
}

fizz_buzz(1, 100, (3 => "Fizz", 5 => "Buzz"));

Debating the insignificant things

This weekend, I read a pretty long thread on the forums of ArsTechnica. The thread was about a comment DHH of Ruby on Rails made on Perl. He said that “Perl burned a lot of people.” A member, most likely unfamiliar with Perl, wondered what David meant with that remark. A few people mentionned some quirks of Perl that could puzzle a developer if he or she wasn’t familiar with Perl.

At one point, the discussion turned to Perl’s handling of the undef value. There were some people who were stunned that Perl converted undef to 0 in integer context and to the empty string in string context. This caused flames back and forth between a few members over the “logic” of such a conversion. I skimmed pretty quickly, so I don’t know if they mentionned that Ruby has the exact same behavior with regards to nil or that NULL in C and C++ is 0. The basic premise of the people arguing that the handling of undef was incorrect was that 0 or the empty string were not undefined values, they were in fact defined. The other group said that in the world of numbers, 0 was the closest thing to undefinedness and the same was true of the empty string in the world of strings.

Regardless, I thought it was a waste of time to debate such a minor issue. Perl has a lot of other more controversial topics that would’ve probably done more to explain to the original poster how “Perl burned many people”:

  • No formal parameters
  • Barewords
  • References (the fact that they are explicit)
  • The object model
  • Sigils
  • Arrays are one dimension only and can only contain scalars

First contribution to pugs

Today, I made my first commit to pugs. For those who don’t know, pugs is an implementation of the Perl 6 specification. Commit access is given very liberally by audreyt++ and I got mine a few weeks ago. I didn’t know exactly what to contribute, but it is recommended to newcomers to start with unit tests.

After playing with the pugs interpreter for a while, I found a problem with the range operator. In Perl 5, when using .., both sides of the operator are truncated if they are floats. This is not what happens in pugs. In fact, the current behaviour is most likely incorrect:

pugs> ~(1 .. 4.4) # Rounded down to 4
"1 2 3 4"

pugs> ~(1 .. 4.5) # Rounded up to 5
"1 2 3 4 5"

pugs> ~(1.4 .. 5)
"1.4 2.4 3.4 4.4 5.4"

As you can see, the right side of the range operator is rounded, not truncated when it’s a float and when the left side is a float, the operator returns a range beyond the upper limit.

So I wrote the tests to make sure that both sides of the operator would be truncated. I also added tests to check that both sides of the operators were forced to scalar context (pugs passes those, but more tests == better software, so I put them in anyway.)

Hopefully, this was the first of many contributions.

Too many dependencies?

As I learn more about Perl, the more I want to use it. Recently, I wanted to install XML::RSS to use in a script that would generate an RSS feed. This package depends on XML::Parser which in its turn depends on an external C library called libexpat. Since libexpat was not installed on the target computer, I had to use strings to generate the XML myself.

It’s not the first time I’ve tried to install a Perl module and that I’ve had to download many other modules to satisfy some dependencies. And I’ve often ran into a wall; on my iBook, I cannot install WWW::Mechanize, the installation just dies. I’ve also had modules failing to install because they fail their unit tests. It’s possible to skip the tests, but it’s not the kind of thing that makes me want to trust a module.

Looking around, I found that one very bad offender is Plagger, an RSS/Atom feed aggregator, similar to the Python planetplanet. In one presentation, you can see the following slide when the authors lists some of the problems of Plagger:

Horrible lots of
CPAN deps.

% grep requires Makefile.PL| wc–l
29
% grep recommends Makefile.PL| wc–l
73

That’s 29 required modules that you need to install before a bare Plagger installation works. And who knows how many of these modules have other dependencies. This is completely ridiculous. Compare with planetplanet where you can read on their home page:

Planet requires Python 2.2 or greater installed on your system. It requires only python-bdb, everything else is included in the Planet distribution.

I know that Python 2.3, 2.4 and 2.5 all ship with the bdb module. So basically, with a plain, vanilla installation of Python, one can install planetplanet. Is it any wonder that Planet Perl uses planetplanet and not Plagger?

The Perl people often say that nothing compares with CPAN and cite it as one of the bigger reasons to pick Perl for a project: most tasks that one wants to do have already modules written for them. This is a definite advantage, but could CPAN also be viewed as a problem? When a new user tries to install a package from CPAN and that the installation fails because of a C library that is not installed or because some tests fail, the user can become a bit confused.

Hopefully future version of Perl, either 5.10 or Perl 6, will adopt the “batteries included” philosophy of Python and ship the standard distribution with a wealth of powerful libraries to reduce the CPAN dependency hell.