Custom Filename Completion in Term::ReadLine::Perl

When we released the last version of our product, I got an IM from a support guy saying that we had broken filename completion in the installer. My reaction was “but we don’t implement filename completion!” I ran the installer on the previous release, and sure enough, it supported filename completion. Wha?

Turns out we were getting filename completion as a side effect of using Term::ReadLine::Perl to do prompting. Support “knew” we supported it, and even had a list of known bugs they passed around, but no one in dev knew about the feature or the bug. Good times!

Anyway, fixing what we broke on completions, I started looking at the bugs support had been informally floating around. Essentially there two issues, which caused three separate problems:

1) After every completion, Term::ReadLine::Perl inserted a space character after the path
Problem 1: When auto-completing paths, this meant you had to hit delete after each path completion before you could type the next few characters.
Problem 2: If you didn’t manually delete the space from the end of the completed filename, our installer would treat the space as part of the filename and tell you the file didn’t exist.

2) By default, after every completion, Term::ReadLine::Perl “decorates” the completed file
Problem 3: If the file you were completing to was a symlink, it would have ‘@’ appended to it, and our installer would tell you the file didn’t exist.

Problems 2 & 3 were fixable with some logic after the user hit enter, and problem 1 wasn’t that annoying, but I went looking to see if I could fix the actual completion function in Term::ReadLine::Perl. I did figure it out, but since there seems to be little google-able documentation specific to addressing this in Term::ReadLine::Perl (as opposed to Term::ReadLine::Gnu) I thought I’d document what I did.

Here’s my toy app demonstrating the above issues:

$| = 1;
use Term::ReadLine;
my $Term = new Term::ReadLine 'Installer';

my $file = $Term->readline('Enter a file path: ');
print "$file = '$file'n";

The relevant library code is in Term/readline.pm. This appears to be a Perl5 wrapper around Perl4 code.

The default filename completion function is called rl_filename_list(). This function has a tunable to turn off the filename decoration (adding “@” to symlinks, etc) called $var_CompleteAddsuffix, set to “1” by default. I tried turning it off like this at the top of my app:

$readline::var_CompleteAddsuffix = 0;

This worked, in that it turned off decoration. Unfortunately, the adding of “/” to a completed path is part of decoration, and that’s functionality I wanted to keep. So, I guess I need to write my own filename expander. Here is my toy app with my custom filename expander routine:

$| = 1;
use Term::ReadLine;
my $Term = new Term::ReadLine 'Installer';

# tell Term::ReadLine::Perl to use my filename expander for completion
$readline::rl_completion_function = "main::rl_filename_list_lcl";

my $file = $Term->readline('Enter a file path: ');
print "$file = '$file'n";

exit;

sub rl_filename_list_lcl {
    my $pattern = $_[0];
    my @files = (<$pattern*>);
    foreach (@files) {
        if (-d $_) {
            $_ .= '/';
        }
    }
    return @files;
}

The subroutine rl_filename_list_lcl() is a slightly modified copy of readline::rl_filename_list(). I removed all the decoration except ‘/’ for dirs, and I removed the check of $var_CompleteAddsuffix before decorating (I want it to happen unconditionally).

That solved the decoration issue, but not the “adds a space after every completion” issue. This is controlled by $rl_completer_terminator_character being unconditionally set to ‘ ‘ in readline.pm’s complete_internal(). I experimented with setting it to '' unconditionally, which did fix my problem. However, I didn’t want to leave it like that unconditionally for fear of unexpected side effects. Then I came across this in the code:

##   $rl_completer_terminator_character -- what to insert to separate
##      a completed token from the rest.  Reset at beginning of
##      completion to ' ' so completion function can change it.

Perfect! I’m already defining a custom function, so all I have to do is set $readline::rl_completer_terminator_character to '' in it.

Et voila! My completed toy function with auto-complete behaving the way I want it to:

$| = 1;
use Term::ReadLine;
my $Term = new Term::ReadLine 'Installer';

# tell Term::ReadLine::Perl to use my filename expander for completion
$readline::rl_completion_function = "main::rl_filename_list_lcl";

my $file = $Term->readline('Enter a file path: ');
print "$file = '$file'n";

exit;

sub rl_filename_list_lcl {
    my $pattern = $_[0];
    my @files = (<$pattern*>);
    foreach (@files) {
        if (-d $_) {
            $_ .= '/';
        }
    }

    # The readline.pm library sets this to ' ' on every completion call, we can
    # force it to '' here but have to do it every time
    $readline::rl_completer_terminator_character = '';

    return @files;
}

Alternating Test Step Row Colors in TestLink

Working on implementing TestLink at work for better testing process flow. Very few complaints about the product as a whole – it can feel ponderous at times, but testing is a big problem, and TestLink is the best tool I’ve found for addressing them all.

The only real negative feedback I’ve received so far is regarding the display of steps from individual test cases. By default, the entire table is the same color, and it’s very difficult to visually track which row is which. It seems like such an obvious problem that I assumed I had missed a knob to tweak, but I can’t find anything about it in the docs, the forums, or the bug tracker.

I poked around a bit tonight and the solution turned out to be ridiculously (but pleasingly) simple.

Update 2012-05-08: There is a usable-with-the-patch-command patch available that incorporates all of these changes and more. See the README at the base of the github repo for more details on what’s in that patch.

First, add the following to gui/themes/default/css/custom.css:

.row_color_1 {
    background-color:   #DDDDDD;
}
.row_color_2 {
    background-color:   #CCCCCC;
}

Then, patch the following two files in gui/templates/testcases/ like so (original files were from 1.9.3):

--- inc_steps.tpl.dist  2011-08-22 21:08:21.000000000 -0500
+++ inc_steps.tpl       2011-08-22 21:13:02.000000000 -0500
@@ -45,7 +45,7 @@
        </tr>
        {* BUGID 3376 *}
        {foreach from=$steps item=step_info}
-       <tr id="step_row_{$step_info.step_number}">
+       <tr id="step_row_{$step_info.step_number}" class="{cycle values="row_color_1,row_color_2"}">
                <td style="text-align:left;">
                        <span class="order_info" style='display:none'>
                        <input type="text" name="step_set[{$step_info.id}]" id="step_set_{$step_info.id}"
--- tcStepEdit.tpl.dist 2011-09-22 10:27:23.000000000 -0500
+++ tcStepEdit.tpl      2011-09-22 10:39:55.000000000 -0500
@@ -197,7 +197,7 @@
   {* this means we have steps to display *}
   {if $gui->tcaseSteps != ''}
        {foreach from=$gui->tcaseSteps item=step_info}
-         <tr id="step_row_{$step_info.step_number}">
+         <tr id="step_row_{$step_info.step_number}" class="{cycle values="row_color_1,row_color_2"}">
       {if $step_info.step_number == $gui->step_number}
                    <td style="text-align:left;">{$gui->step_number}</td>
                  <td>{$steps}</td>

Blow away the cached .css files, and voila! Alternating background colors for test steps in TestLink.

Before: After:

I tried to post this as a hint in the TestLink forum but I honestly couldn’t figure out how to register a forum account. I did post a comment on a related bug in their issue tracker, ticket 3583

EDIT 2011-09-22: The original version of this post only included a change to inc_steps.tpl. Added the patch to tcStepsEdit.tpl to apply alternation to the rows in the test case step edit view also.

EDIT 2011-09-26: Updated to include the correct full path to custom.css

Enforcing a default owner for new tickets in Trac

We’ve been loving our Trac install at work, but we are still sanding down a few rough spots.  One particular spot that’s been bobbing up has been what happens when “new” tickets have an owner assigned.  I liked the ticket status being “new”, because it meant that I could easily look at my ticket list and see tickets that had been directly assigned to me that I hadn’t acknowledged yet by accepting them.  Someone else really disliked having to “accept” a ticket that was already assigned to him before he could move it to QA.

I actually managed to create a solution for this using the TicketConditionalCreationStatusPlugin from TracHacks.  This plugin allowed me to conditionally set the status of the ticket based on the contents of the “owner” field.  This was actually a pretty slick solution, but for various reasons it didn’t sit well with a lot of people.

The next idea we had was to prevent new tickets from being created with any owner besides the default value of “dev” (our dev team mailing list).  This solution had three main benefits – It worked the same for everyone; no one would ever have a “new” ticket assigned to them; and the “dev” alias was always associated with a new ticket, which has some value in relation to email notifications.

Easy enough, right?  Maybe it’s just me, but I could not find a good way to enforce this.  There’s no native ability to hide fields that aren’t drop-downs, and owner’s not a drop down by default.  It can be made a drop-down, but not in a manner which would allow it to be removed.  There are several owner-oriented plugins, but none of them differentiate between the “new ticket” screen and the “modify ticket” screen.

After a bit of fruitless experimentation I realized that I was overthinking it.  I made the following change:

- <body py:match="body" py:attrs="select('@*')">
+ <body onload='document.getElementById("field-owner").disabled = true;' py:match="body" py:attrs="select('@*')">

et voila!  My default owner value is visible but immutable!

I know this isn’t a rock-solid solution, but in our environment (restricted corporate access) I’m not worried about evil-doers and griefers, I’m just wanting to remind people that they’re not supposed to change the value…