Index: .gitignore
===================================================================
--- .gitignore	(revision b9c8d280039ad11469fe3bdb43469fc4d7773198)
+++ .gitignore	(revision 42ad91716a0f429f02385235acffa71546e28caa)
@@ -28,4 +28,5 @@
 owl_prototypes.h
 owl_prototypes.h.new
+perl_tester
 perlglue.c
 perlwrap.c
Index: Makefile.am
===================================================================
--- Makefile.am	(revision eddee7e563e9e9a0686e7a6fe792a7266fd3a9d0)
+++ Makefile.am	(revision 42ad91716a0f429f02385235acffa71546e28caa)
@@ -2,5 +2,5 @@
 
 bin_PROGRAMS = barnowl.bin
-check_PROGRAMS = tester
+check_PROGRAMS = tester perl_tester
 
 barnowl_bin_SOURCES = $(BASE_SRCS) \
@@ -17,4 +17,12 @@
 
 tester_LDADD = libfaim/libfaim.a
+
+perl_tester_SOURCES = $(BASE_SRCS) \
+     owl.h owl_perl.h config.h \
+     libzcrypt.a \
+     $(GEN_C) $(GEN_H) \
+     perl_tester.c
+
+perl_tester_LDADD = libfaim/libfaim.a libzcrypt.a
 
 TESTS=runtests.sh
Index: perl/lib/BarnOwl/Complete/Client.pm
===================================================================
--- perl/lib/BarnOwl/Complete/Client.pm	(revision d5ccf4e8272b63f9955a27f0fbc5a31d4abc3728)
+++ perl/lib/BarnOwl/Complete/Client.pm	(revision a3a9eb73bf0972305a6b4df90ea6e4d482d6b4e0)
@@ -7,4 +7,5 @@
 
 use BarnOwl::Completion::Util qw(complete_flags);
+use BarnOwl::Complete::Filter qw(complete_filter_name complete_filter_expr);
 
 my @all_colors = qw(default
@@ -42,134 +43,6 @@
 sub complete_command { return sort @BarnOwl::all_commands; }
 sub complete_color { return @all_colors; }
-sub complete_filter_name { return @{BarnOwl::all_filters()}; }
 sub complete_variable    { return @{BarnOwl::all_variables()}; }
 sub complete_style       { return @{BarnOwl::all_styles()}; }
-
-my %filter_cmds = (
-    sender    => \&BarnOwl::Complete::Zephyr::complete_user,
-    recipient => \&BarnOwl::Complete::Zephyr::complete_user,
-    class     => \&BarnOwl::Complete::Zephyr::complete_class,
-    instance  => \&BarnOwl::Complete::Zephyr::complete_instance,
-    opcode    => \&BarnOwl::Complete::Zephyr::complete_opcode,
-    realm     => \&BarnOwl::Complete::Zephyr::complete_realm,
-    body      => undef,
-    hostname  => undef,
-    type      => sub { qw(zephyr aim admin); },
-    direction => sub { qw(in out none); },
-    login     => sub { qw(login logout none); },
-    filter    => \&complete_filter_name,
-    perl      => undef,
-);
-
-# Returns:
-# - where to look next after pulling out an expression
-# - $INCOMPLETE if this cannot form a complete expression (or w/e)
-# - pushes to completion list as it finds valid completions
-
-my $INCOMPLETE = -1;
-sub _complete_filter_expr {
-    # Takes as arguments context and the index into $ctx->words where the
-    # filter expression starts
-    my $ctx = shift;
-    my $start = shift;
-    my $o_comp = shift;
-    my $end = $ctx->word;
-
-    # Grab an expression; we don't allow empty
-    my $i = $start;
-    $i = _complete_filter_primitive_expr($ctx, $start, $o_comp);
-    return $INCOMPLETE if $start == $INCOMPLETE;
-
-    while ($i <= $end) {
-        if ($i == $end) {
-            # We could and/or another clause
-            push @$o_comp, qw(and or);
-            return $end; # Or we could let the parent do his thing
-        }
-
-        if ($ctx->words->[$i] ne 'and' && $ctx->words->[$i] ne 'or') {
-            return $i; # We're done. Let the parent do his thing
-        }
-
-        # Eat the and/or
-        $i++;
-
-        # Grab an expression
-        $i = _complete_filter_primitive_expr($ctx, $i, $o_comp);
-        return $INCOMPLETE if $i == $INCOMPLETE;
-    }
-    
-    return $i; # Well, it looks like we're happy
-    # (Actually, I'm pretty sure this never happens...)
-}
-
-sub _complete_filter_primitive_expr {
-    my $ctx = shift;
-    my $start = shift;
-    my $o_comp = shift;
-    my $end = $ctx->word;
-
-    if ($start >= $end) {
-        push @$o_comp, "(";
-        push @$o_comp, qw(true false not);
-        push @$o_comp, keys %filter_cmds;
-        return $INCOMPLETE;
-    }
-
-    my $word = $ctx->words->[$start];
-    if ($word eq "(") {
-        $start = _complete_filter_expr($ctx, $start+1, $o_comp);
-        return $INCOMPLETE if $start == $INCOMPLETE;
-
-        # Now, we expect a ")"
-        if ($start >= $end) {
-            push @$o_comp, ")";
-            return $INCOMPLETE;
-        }
-        if ($ctx->words->[$start] ne ')') {
-            # User is being confusing. Give up.
-            return $INCOMPLETE;
-        }
-        return $start+1; # Eat the )
-    } elsif ($word eq "not") {
-        # We just want another primitive expression
-        return _complete_filter_primitive_expr($ctx, $start+1, $o_comp);
-    } elsif ($word eq "true" || $word eq "false") {
-        # No arguments
-        return $start+1; # Eat the boolean. Mmmm, tasty.
-    } else {
-        # It's of the form 'CMD ARG'
-        return $start+2 if ($start+1 < $end); # The user supplied the argument
-
-        # complete the argument
-        my $arg_func = $filter_cmds{$word};
-        push @$o_comp, ($arg_func ? ($arg_func->()) : ());
-        return $INCOMPLETE;
-    }
-}
-
-sub complete_filter_expr {
-    my $ctx = shift;
-    my $start = shift;
-    my @completions = ();
-    _complete_filter_expr($ctx, $start, \@completions);
-    # Get rid of duplicates and sort
-    my %hash = ();
-    @hash{@completions} = ();
-    @completions = sort keys %hash;
-    return @completions;
-}
-
-sub complete_filter_args {
-    my $ctx = shift;
-    my $arg = shift;
-    return complete_filter_name() unless $arg;
-    my $idx = 2; # skip the filter name
-    while ($idx < $ctx->word) {
-        last unless ($ctx->words->[$idx] =~ m{^-});
-        $idx += 2; # skip the flag and the argument
-    }
-    return complete_filter_expr($ctx, $idx);
-}
 
 sub complete_help {
@@ -194,4 +67,11 @@
 sub complete_filter {
     my $ctx = shift;
+    # Syntax: filter FILTERNAME FLAGS EXPR
+
+    # FILTERNAME
+    return complete_filter_name() if $ctx->word == 1;
+
+    # FLAGS
+    $ctx = $ctx->shift_words(1); # complete_flags starts at the second word
     return complete_flags($ctx,
         [qw()],
@@ -200,5 +80,15 @@
            "-b" => \&complete_color,
         },
-         \&complete_filter_args
+        # EXPR
+        sub {
+            my $ctx = shift;
+            my $arg = shift;
+
+            # We pass stop_at_nonflag, so we can rewind to the start
+            my $idx = $ctx->word - $arg;
+            $ctx = $ctx->shift_words($idx);
+            return complete_filter_expr($ctx);
+        },
+        stop_at_nonflag => 1
         );
 }
@@ -213,5 +103,6 @@
     }
     if ($ctx->words->[1] eq "-d") {
-        return complete_filter_expr($ctx, 2);
+        $ctx = $ctx->shift_words(2);
+        return complete_filter_expr($ctx);
     }
     if ($ctx->words->[1] eq "-s") {
@@ -243,4 +134,10 @@
 }
 
+sub complete_startup {
+    my $ctx = shift;
+    my $new_ctx = $ctx->shift_words(1);
+    return BarnOwl::Completion::get_completions($new_ctx);
+}
+
 BarnOwl::Completion::register_completer(help    => \&complete_help);
 BarnOwl::Completion::register_completer(filter  => \&complete_filter);
@@ -250,4 +147,5 @@
 BarnOwl::Completion::register_completer(set     => \&complete_set);
 BarnOwl::Completion::register_completer(unset   => \&complete_set);
+BarnOwl::Completion::register_completer(startup => \&complete_startup);
 
 1;
Index: perl/lib/BarnOwl/Complete/Filter.pm
===================================================================
--- perl/lib/BarnOwl/Complete/Filter.pm	(revision 5fdc56247c5d5b114e51cd431657f3c187fddc5d)
+++ perl/lib/BarnOwl/Complete/Filter.pm	(revision 5fdc56247c5d5b114e51cd431657f3c187fddc5d)
@@ -0,0 +1,129 @@
+use strict;
+use warnings;
+
+# Helper completers for filters
+package BarnOwl::Complete::Filter;
+
+use base qw(Exporter);
+our @EXPORT_OK = qw(complete_filter_name complete_filter_expr);
+
+# Completes the name of a filter
+sub complete_filter_name { return @{BarnOwl::all_filters()}; }
+
+# Completes a filter expression
+sub complete_filter_expr {
+    my $ctx = shift;
+
+    my @completions = ();
+    _complete_filter_expr($ctx, 0, \@completions);
+    # Get rid of duplicates and sort
+    my %hash = ();
+    @hash{@completions} = ();
+    @completions = sort keys %hash;
+    return @completions;
+}
+
+### Private
+
+my %filter_cmds = (
+    sender    => \&BarnOwl::Complete::Zephyr::complete_user,
+    recipient => \&BarnOwl::Complete::Zephyr::complete_user,
+    class     => \&BarnOwl::Complete::Zephyr::complete_class,
+    instance  => \&BarnOwl::Complete::Zephyr::complete_instance,
+    opcode    => \&BarnOwl::Complete::Zephyr::complete_opcode,
+    realm     => \&BarnOwl::Complete::Zephyr::complete_realm,
+    body      => undef,
+    hostname  => undef,
+    type      => sub { qw(zephyr aim admin); },
+    direction => sub { qw(in out none); },
+    login     => sub { qw(login logout none); },
+    filter    => \&complete_filter_name,
+    perl      => undef,
+);
+
+# Returns:
+# - where to look next after pulling out an expression
+# - $INCOMPLETE if this cannot form a complete expression (or w/e)
+# - pushes to completion list as it finds valid completions
+
+my $INCOMPLETE = -1;
+sub _complete_filter_expr {
+    # Takes as arguments context and the index into $ctx->words where the
+    # filter expression starts
+    my $ctx = shift;
+    my $start = shift;
+    my $o_comp = shift;
+    my $end = $ctx->word;
+
+    # Grab an expression; we don't allow empty
+    my $i = $start;
+    $i = _complete_filter_primitive_expr($ctx, $start, $o_comp);
+    return $INCOMPLETE if $i == $INCOMPLETE;
+
+    while ($i <= $end) {
+        if ($i == $end) {
+            # We could and/or another clause
+            push @$o_comp, qw(and or);
+            return $end; # Or we could let the parent do his thing
+        }
+
+        if ($ctx->words->[$i] ne 'and' && $ctx->words->[$i] ne 'or') {
+            return $i; # We're done. Let the parent do his thing
+        }
+
+        # Eat the and/or
+        $i++;
+
+        # Grab an expression
+        $i = _complete_filter_primitive_expr($ctx, $i, $o_comp);
+        return $INCOMPLETE if $i == $INCOMPLETE;
+    }
+    
+    return $i; # Well, it looks like we're happy
+    # (Actually, I'm pretty sure this never happens...)
+}
+
+sub _complete_filter_primitive_expr {
+    my $ctx = shift;
+    my $start = shift;
+    my $o_comp = shift;
+    my $end = $ctx->word;
+
+    if ($start >= $end) {
+        push @$o_comp, "(";
+        push @$o_comp, qw(true false not);
+        push @$o_comp, keys %filter_cmds;
+        return $INCOMPLETE;
+    }
+
+    my $word = $ctx->words->[$start];
+    if ($word eq "(") {
+        $start = _complete_filter_expr($ctx, $start+1, $o_comp);
+        return $INCOMPLETE if $start == $INCOMPLETE;
+
+        # Now, we expect a ")"
+        if ($start >= $end) {
+            push @$o_comp, ")";
+            return $INCOMPLETE;
+        }
+        if ($ctx->words->[$start] ne ')') {
+            # User is being confusing. Give up.
+            return $INCOMPLETE;
+        }
+        return $start+1; # Eat the )
+    } elsif ($word eq "not") {
+        # We just want another primitive expression
+        return _complete_filter_primitive_expr($ctx, $start+1, $o_comp);
+    } elsif ($word eq "true" || $word eq "false") {
+        # No arguments
+        return $start+1; # Eat the boolean. Mmmm, tasty.
+    } else {
+        # It's of the form 'CMD ARG'
+        return $start+2 if ($start+1 < $end); # The user supplied the argument
+
+        # complete the argument
+        my $arg_func = $filter_cmds{$word};
+        push @$o_comp, ($arg_func ? ($arg_func->()) : ());
+        return $INCOMPLETE;
+    }
+}
Index: perl/lib/BarnOwl/Completion/Context.pm
===================================================================
--- perl/lib/BarnOwl/Completion/Context.pm	(revision 7be5d8b6e180b287462defa361f336639cd5e84e)
+++ perl/lib/BarnOwl/Completion/Context.pm	(revision e97c5d051441fc88462b69a71e396bcbabef078e)
@@ -40,4 +40,5 @@
 
 use base qw(Class::Accessor::Fast);
+use Carp qw(croak);
 
 __PACKAGE__->mk_ro_accessors(qw(line point words word word_point
@@ -64,4 +65,27 @@
        };
     return bless($self, $class);
+}
+
+=head2 shift_words N
+
+Returns a new C<Context> object, with the leading C<N> words
+stripped. All fields are updated as appopriate. If C<N> > C<<
+$self->word >>, C<croak>s with an error message.
+
+=cut
+
+sub shift_words {
+    my $self = shift;
+    my $n    = shift;
+
+    if($n > $self->word) {
+        croak "Context::shift: Unable to shift $n words";
+    }
+
+    my $before = substr($self->line, 0, $self->point);
+    my $after  = substr($self->line, $self->point);
+
+    return BarnOwl::Completion::Context->new(BarnOwl::skiptokens($before, $n),
+                                             $after);
 }
 
Index: perl/lib/BarnOwl/Completion/Util.pm
===================================================================
--- perl/lib/BarnOwl/Completion/Util.pm	(revision 94ef58c98f9248815a27a3addd8a7d03e61b9d9b)
+++ perl/lib/BarnOwl/Completion/Util.pm	(revision 69c27e6211624978c76b0f196e79d063e4d143d3)
@@ -24,4 +24,6 @@
     my $optsdone = 0;
 
+    my %flags_seen;
+
     while($idx < $ctx->word) {
         my $word = $ctx->words->[$idx];
@@ -37,4 +39,5 @@
         } elsif ($word =~ m{^-}) {
             $word = "-" . substr($word, -1);
+            $flags_seen{$word} = 1; # record flag
             $flag = $word if(exists $args->{$word});
         } else {
@@ -58,6 +61,8 @@
         return;
     } else {
-        return ($optsdone ? () : (@$no_args, keys %$args),
-                $default ? ($default->($ctx, $argct)) : ());
+        my @opts = $optsdone ? () : (@$no_args, keys %$args);
+        # filter out flags we've seen if needbe
+        @opts = grep {!$flags_seen{$_}} @opts unless $options{repeat_flags};
+        return (@opts, $default ? ($default->($ctx, $argct)) : ());
     }
 }
Index: perl_tester.c
===================================================================
--- perl_tester.c	(revision 42ad91716a0f429f02385235acffa71546e28caa)
+++ perl_tester.c	(revision 42ad91716a0f429f02385235acffa71546e28caa)
@@ -0,0 +1,35 @@
+#define OWL_PERL
+#include "owl.h"
+
+owl_global g;
+
+extern XS(boot_BarnOwl);
+extern XS(boot_DynaLoader);
+/* extern XS(boot_DBI); */
+
+static void owl_perl_xs_init(pTHX)
+{
+  const char *file = __FILE__;
+  dXSUB_SYS;
+  {
+    newXS("BarnOwl::bootstrap", boot_BarnOwl, file);
+    newXS("DynaLoader::boot_DynaLoader", boot_DynaLoader, file);
+  }
+}
+
+static PerlInterpreter *my_perl;  /***    The Perl interpreter    ***/
+
+int main(int argc, char **argv, char **env)
+{
+  /* Code from perldoc perlembed */
+  PERL_SYS_INIT3(&argc,&argv,&env);
+  my_perl = perl_alloc();
+  perl_construct(my_perl);
+  PL_exit_flags |= PERL_EXIT_DESTRUCT_END;
+  perl_parse(my_perl, owl_perl_xs_init, argc, argv, (char **)NULL);
+  perl_run(my_perl);
+  perl_destruct(my_perl);
+  perl_free(my_perl);
+  PERL_SYS_TERM();
+}
+
Index: perlglue.xs
===================================================================
--- perlglue.xs	(revision 1a5db78acbe1e35cc374e0cd7048894b36f2d7d9)
+++ perlglue.xs	(revision 9e5b5fdef1dac8b4eeaeaf49810ab65f616de7c0)
@@ -424,4 +424,15 @@
 		RETVAL
 
+const char *
+skiptokens(str, n)
+	const char *str;
+	int n;
+	CODE:
+		RETVAL = skiptokens(str, n);
+	OUTPUT:
+		RETVAL
+
+
+
 MODULE = BarnOwl		PACKAGE = BarnOwl::Internal
 
Index: runtests.sh
===================================================================
--- runtests.sh	(revision 2fa9a1a0ff2563754c3bf1bf659ef683993fe125)
+++ runtests.sh	(revision 42ad91716a0f429f02385235acffa71546e28caa)
@@ -1,2 +1,2 @@
 #!/bin/sh
-prove t/
+prove t/ --perl ./perl_tester
Index: t/completion.t
===================================================================
--- t/completion.t	(revision b017b0309025b72c830864f20f5dd923659f981c)
+++ t/completion.t	(revision 1167bf1098ad511a9390361dd5b88a090b872377)
@@ -8,4 +8,6 @@
 BEGIN {require (dirname($0) . "/mock.pl");};
 
+use BarnOwl::Complete::Filter qw(complete_filter_expr);
+
 =head1 DESCRIPTION
 
@@ -20,4 +22,17 @@
     my $after_point = shift;
     
+    my $ctx = BarnOwl::Completion::Context->new($before_point,
+                                                $after_point);
+    is($ctx->line, $before_point . $after_point);
+    is($ctx->point, length $before_point);
+
+    test_ctx($ctx, @_);
+}
+
+sub test_ctx {
+    local $Test::Builder::Level = $Test::Builder::Level + 1;
+
+    my $ctx = shift;
+
     my $words = shift;
     my $word = shift;
@@ -27,9 +42,4 @@
     my $word_end   = shift;
 
-    my $ctx = BarnOwl::Completion::Context->new($before_point,
-                                                $after_point);
-
-    is($ctx->line, $before_point . $after_point);
-    is($ctx->point, length $before_point);
     is_deeply($ctx->words, $words);
     if (defined($word)) {
@@ -41,4 +51,18 @@
 }
 
+sub test_shift {
+    local $Test::Builder::Level = $Test::Builder::Level + 1;
+
+    my $before_point = shift;
+    my $after_point = shift;
+    my $shift = shift;
+    
+    my $ctx = BarnOwl::Completion::Context->new($before_point,
+                                                $after_point);
+    $ctx = $ctx->shift_words($shift);
+
+    test_ctx($ctx, @_);
+}
+
 
 isa_ok(BarnOwl::Completion::Context->new('Hello, W', 'orld'), 'BarnOwl::Completion::Context');
@@ -108,4 +132,31 @@
               1, -1, 7, 12);
 
+## Test Context::shift
+test_shift('lorem ipsum dolor ', 'sit amet', 0,
+           [qw(lorem ipsum dolor sit amet)],
+           3, 0, 18, 21);
+
+test_shift('lorem ipsum dolor ', 'sit amet', 1,
+           [qw(ipsum dolor sit amet)],
+           2, 0, 12, 15);
+
+test_shift('lorem ipsum dolor ', 'sit amet', 2,
+           [qw(dolor sit amet)],
+           1, 0, 6, 9);
+
+test_shift('lorem ipsum dolor ', 'sit amet', 3,
+           [qw(sit amet)],
+           0, 0, 0, 3);
+
+eval {
+    my $before_point = 'lorem ipsum dolor';
+    my $after_point = 'sit amet';
+    my $shift = 4;
+
+    my $ctx = BarnOwl::Completion::Context->new($before_point,
+                                                $after_point);
+    $ctx = $ctx->shift_words($shift);
+};
+like($@, qr/^Context::shift: Unable to shift /, "Correctly die when shifting away the point");
 
 ## Test common_prefix
@@ -163,8 +214,8 @@
 
 test_complete('zwrite -c nelhage ', '',
-              [qw(-n -C -m -c -i -r -O nelhage asedeno geofft)]);
+              [qw(-n -C -m -i -r -O nelhage asedeno geofft)]);
 
 test_complete('zwrite -c nelhage ', '-',
-              [qw(-n -C -m -c -i -r -O nelhage asedeno geofft)]);
+              [qw(-n -C -m -i -r -O nelhage asedeno geofft)]);
 
 test_complete('zwrite -c nelhage -- ', '',
@@ -178,5 +229,6 @@
                               "-d" => sub {qw(some words for completing)},
                           },
-                          sub {$_[1]});
+                          sub {$_[1]},
+                          repeat_flags => 1);
 }
 
@@ -203,4 +255,38 @@
               [qw(2)], \&complete_word);
 
+
+# Test the filter expression completer
+test_complete('', '',
+              [qw[( body class direction false filter hostname instance login not opcode perl realm recipient sender true type]],
+              \&complete_filter_expr);
+
+test_complete('not ', '',
+              [qw[( body class direction false filter hostname instance login not opcode perl realm recipient sender true type]],
+              \&complete_filter_expr);
+
+test_complete('true ', '',
+              [qw[and or]],
+              \&complete_filter_expr);
+
+test_complete('( true ', '',
+              [qw[and or )]],
+              \&complete_filter_expr);
+
+test_complete('( body static and body analysis and not false and class davidben and ( instance python or instance hotd ', '',
+              [qw[and or )]],
+              \&complete_filter_expr);
+
+test_complete('type ', '',
+              [qw[admin aim zephyr]],
+              \&complete_filter_expr);
+
+test_complete('direction ', '',
+              [qw[in out none]],
+              \&complete_filter_expr);
+
+test_complete('login ', '',
+              [qw[login logout none]],
+              \&complete_filter_expr);
+
 1;
 
Index: t/mock.pl
===================================================================
--- t/mock.pl	(revision 776c4bb86d71f449fb9843bccde95ebc3a31bb5f)
+++ t/mock.pl	(revision 42ad91716a0f429f02385235acffa71546e28caa)
@@ -7,5 +7,4 @@
 use Carp;
 
-sub bootstrap {}
 sub get_data_dir {"."}
 sub get_config_dir {"."}
