1 # ho_tfind.pl
  2 # $Id: ho_tfind.pl,v 1.18 2005/03/27 13:13:34 jvunder Exp $
  3 #
  4 # Provides extended search functionality for the /TRACE command.
  5 #
  6 # Part of the Hybrid Oper Script Collection.
  7 #
  8 # Based on BlackJac's /TFIND and morrow's stat.pl script.
  9 #
 10 # Known bugs:
 11 # * If the output window is closed halfway, the script crashes.
 12 
 13 use strict;
 14 use vars qw($VERSION %IRSSI $SCRIPT_NAME);
 15 
 16 use Irssi;
 17 use Irssi::Irc;
 18 use HOSC::again;
 19 use HOSC::again 'HOSC::Base';
 20 use HOSC::again 'HOSC::Tools';
 21 import HOSC::Tools qw(test_regexps get_equality glob_to_regexp);
 22 use Getopt::Long;
 23 
 24 $SCRIPT_NAME = "Trace Find";
 25 ($VERSION) = '$Revision: 1.18 $' =~ / (\d+\.\d+) /;
 26 %IRSSI = (
 27     authors     => 'Garion',
 28     contact     => 'garion@irssi.org',
 29     name        => 'ho_tfind',
 30     description => 'Provides extended search functionality for the /TRACE command.',
 31     license     => 'Public Domain',
 32     url         => 'http://www.garion.org/irssi/hosc/',
 33     changed     => '04 April 2004 12:34:38',
 34 );
 35 
 36 my ($stats, $args);
 37 my @found_clients; # Storage place for found clients in case of sorting
 38 my @cache;
 39 my $cache_time;
 40 my $cache_tag;  # the network tag for which the cache is active.
 41 
 42 # ---------------------------------------------------------------------
 43 # /TFIND 
 44 
 45 sub cmd_tfind {
 46     my ($arguments, $server, $item) = @_;
 47 
 48     $args = parse_arguments($arguments);
 49 
 50     if ($args->{help}) {
 51         Irssi::command_runsub ('tfind', 'help', $server, $item);
 52         return;
 53     }
 54     
 55     if ($args->{error} || length $arguments == 0) {
 56         return print_usage();
 57     }   
 58 
 59     if (!$server) {
 60         return ho_print_error("No server in this window.");
 61     }
 62 
 63     $stats->{num_clients} = 0;
 64     $stats->{num_found}   = 0;
 65     $stats->{window}      = Irssi::active_win();
 66     @found_clients        = ();
 67     
 68     if ($stats->{busy}) {
 69         ho_print_error("Sorry, already performing a TFIND. Please wait.");
 70         return;
 71     }
 72 
 73     if ($args->{equality} && $args->{equality} !~ /^(n|nu|nr|ur|nur)$/) {
 74         ho_print_error("TFIND: Invalid equality " . $args->{equality} . ".");
 75         return;
 76     }
 77 
 78     for my $property (qw[ rnick ruser rhost rgecos ]) {
 79         if (defined $args->{$property} && !test_regexps($args->{$property})) {
 80             return ho_print_error("TFIND: Invalid regexp in $property.");
 81         }
 82     }
 83 
 84     my $search_param_text = get_param_text($args);
 85     ho_print("Searching TRACE output with params $search_param_text");
 86 
 87     my $use_cache = Irssi::settings_get_bool('ho_tfind_use_cache');
 88     if ($use_cache && !$args->{nocache}) {
 89         my $cache_expiry_time = 
 90             Irssi::settings_get_int('ho_tfind_cache_expiry_time');
 91         my $cache_age = time() - $cache_time;
 92             
 93         if (!$use_cache) {
 94             ho_print_warning('Cache is not enabled. Use "/set ' .
 95                 'ho_tfind_use_cache ON" to enable it.');
 96         } elsif (@cache == 0) {
 97             ho_print_warning('Cache empty. Not using it.');
 98         } elsif ($cache_age > $cache_expiry_time) {
 99             ho_print_warning('Cache expired. Not using it.');
100         } elsif ($cache_tag ne $server->{tag}) {
101             ho_print_warning('Cache contents are for diffent tag. Replacing cache.');
102             undef @cache;
103         } else {
104             # Phew, can use the cache.
105             ho_print("Using cache: " . scalar @cache . " clients. ".
106                 "Age is $cache_age/$cache_expiry_time.");
107             trace_from_cache($server);
108             return;
109         }
110     }
111 
112     $server->redirect_event(
113         'command cmd_tfind', 0, '', (split(/\s+/, $arguments) > 2), undef, {
114                 "event 203", "redir event_trace_line",
115                 "event 204", "redir event_trace_line",
116                 "event 205", "redir event_trace_line",
117                 "event 709", "redir event_trace_line",
118                 "event 206", "redir event_stop",
119                 "event 207", "redir event_stop",
120                 "event 208", "redir event_stop",
121                 "event 209", "redir event_stop",
122                 "event 262", "redir event_trace_end",
123                 "event 421", "redir event_unknown_command",
124         } 
125     );
126 
127     $stats->{busy} = 1;
128     $cache_tag = $server->{tag};
129     undef @cache;
130     $cache_time = time;
131     if ($args->{etrace} || $server->{version} =~ /ircd-ratbox/) {
132         $server->send_raw("ETRACE");
133     } else {
134         $server->send_raw("TRACE");
135     }
136 }
137 
138 # ---------------------------------------------------------------------
139 
140 sub cmd_tfind_help {
141     my ($arguments, $server, $item) = @_;
142     print_help();
143 }
144 
145 # ---------------------------------------------------------------------
146 
147 sub parse_arguments {
148     my ($arguments) = @_;
149     my $opt;
150 
151     # Irssi works with -argument, Getopt::Long expects --argument. Fix.
152     $arguments =~ s/-/--/g;
153     
154     # Smart splitting of $arguments: understands "multi word argument".
155     local @ARGV;
156     my @tempargv = split / /, $arguments;
157     my ($in_multi_token, $delimiter, $index) = (0, undef, 0);
158     while (@tempargv) {
159         my $token = shift @tempargv;
160         if ($in_multi_token) {
161             if ($token =~ /$delimiter$/) {
162                 # End of multi token argument
163                 $token =~ s/$delimiter$//;
164                 $ARGV[$index] .= " " . $token;
165                 $in_multi_token = 0;
166                 $index++;
167             } else {
168                 # Continue multi token argument
169                 $ARGV[$index] .= " " . $token;
170             }
171         } elsif ($token =~ /^(['"])/ && $token !~ /['"]$/) {
172             # New multi token argument
173             $delimiter = $1;
174             $token =~ s/^['"]//;
175             $ARGV[$index] = $token;
176             $in_multi_token = 1;
177         } else {
178             # Single token argument
179             $ARGV[$index] = $token;
180             $index++;
181         }
182     }
183 
184     # Prevent GetOptions frow screwing up the layout in case of errors.
185     # Thanks xmath. :)
186     local $SIG{__WARN__} = sub { 
187         my ($msg) = @_;
188         $msg = '/TFIND: ' . $msg;
189         ho_print_error($msg); 
190     };
191 
192     my $res = GetOptions(
193         'nocache'   => \$opt->{nocache},
194         'sort=s'    => \$opt->{sort},
195         'nick=s'    => \$opt->{nick},
196         'user=s'    => \$opt->{user},
197         'host=s'    => \$opt->{host},
198         'ip=s'      => \$opt->{ip},
199         'gecos=s'   => \$opt->{gecos},
200         'rnick=s'   => \$opt->{rnick},    
201         'ruser=s'   => \$opt->{ruser},
202         'rhost=s'   => \$opt->{rhost},
203         'rip=s'     => \$opt->{rip},
204         'rgecos=s'  => \$opt->{rgecos},
205         'equality=s'=> \$opt->{equality},
206         'spoof'     => \$opt->{spoof},
207         'nospoof'   => \$opt->{nospoof},
208         'oper'      => \$opt->{oper},
209         'nooper'    => \$opt->{nooper},
210         'etrace'    => \$opt->{etrace},
211         'help'      => \$opt->{help},
212         '4'         => \$opt->{ipv4},
213         '6'         => \$opt->{ipv6},
214         'rawcmd=s'  => \$opt->{rawcmd},
215         'count'     => \$opt->{count},
216     );
217 
218     $opt->{error} = 1 unless $res;
219 
220     for my $arg (@ARGV) {
221         # If any args left, read them.
222         if (lc $arg eq "help") {
223             $opt->{help} = 1;
224         } else {
225             $opt->{error} = 1;
226         }
227     }
228 
229     # Change glob patterns into regexp patterns.
230     for my $var (qw[ nick user host gecos ip ]) {
231         if (defined $opt->{$var}) {
232             # Store the original glob request for displaying later on.
233             $opt->{"glob$var"} = $opt->{$var};
234             # Convert glob to regexp for matching later on.
235             $opt->{$var}       = glob_to_regexp($opt->{$var});
236         }
237     }
238 
239     # Now restore any -- in the values back to -.
240     for my $key (keys %$opt) {
241         $opt->{$key} =~ s/--/-/g;
242     }
243 
244     return $opt;
245 }
246 
247 # ---------------------------------------------------------------------
248 
249 sub get_param_text {
250     my ($args) = @_;
251     my $text;
252 
253     for my $var (qw[ nick user host gecos ip ]) {
254         if (defined $args->{$var} && length $args->{$var}) {
255             $text .= "($var is " . $args->{"glob$var"} . ") ";
256         }
257         if (defined $args->{"r$var"} && length $args->{"r$var"}) {
258             $text .= "($var regexp " . $args->{"r$var"} . ") ";
259         }
260     }
261 
262     $text .= "(spoof) "     if $args->{spoof};
263     $text .= "(not spoof) " if $args->{nospoof};
264     $text .= "(oper) "      if $args->{oper};
265     $text .= "(not oper) "  if $args->{nooper};
266     $text .= "(only ipv4) " if $args->{ipv4};
267     $text .= "(only ipv6) " if $args->{ipv6};
268     $text .= "(equality " . $args->{equality} . ") " if $args->{equality};
269 
270     $text .= "using ETRACE " if $args->{etrace};
271 
272     $text =~ s/\) \(/) and (/g;
273     $text =~ s/ $//;
274     return $text;
275 }
276  
277 # ---------------------------------------------------------------------
278 # Catching and processing output of the ircd.
279 
280 sub event_trace_line {
281     my ($server, $data, $nick, $address) = @_;
282     my ($ownnick, $line) = $data =~ /^(\S*)\s+(.*)$/;
283 
284     my $details = get_trace_line_details($line);
285     return if $details->{crap};
286 
287     if (Irssi::settings_get_bool('ho_tfind_use_cache')) {
288         push @cache, $details;
289     }
290     process_line($details, $server);
291 }
292 
293 # ---------------------------------------------------------------------
294 
295 sub trace_from_cache {
296     my ($server) = @_;
297     for my $details (@cache) {
298         process_line($details, $server);
299     }
300     trace_end();
301 }
302 
303 # ---------------------------------------------------------------------
304 # Processes one line of TRACE output, whether retrieved from TRACE output
305 # or from the cache.
306 
307 sub process_line {
308     my ($details, $server) = @_;
309 
310     $stats->{num_clients}++;
311 
312     for my $check (qw[ nick user host gecos ip ]) {
313         # Glob check
314         return if defined $args->{$check} && length $args->{$check} && 
315             $details->{$check} !~ /$args->{$check}/i;
316 
317         # Regexp check
318         return if defined $args->{"r$check"} && length $args->{"r$check"} &&
319             $details->{$check} !~ /$args->{"r$check"}/;
320     }
321 
322     return if $args->{spoof}   && $details->{ip} ne "255.255.255.255";
323     return if $args->{nospoof} && $details->{ip} eq "255.255.255.255";
324     return if $args->{oper}    && !$details->{is_oper};
325     return if $args->{nooper}  && $details->{is_oper};
326     return if $args->{ipv4}    && !$details->{ipv4};
327     return if $args->{ipv6}    && !$details->{ipv6};
328 
329     if ($args->{equality}) {
330         my $eq = get_equality($details->{nick}, $details->{user},
331             $details->{gecos});
332         return if $eq ne $args->{equality};
333     }
334 
335     if (defined $args->{rawcmd}) {
336         execute_raw_command($details, $server);
337     } elsif (defined $args->{sort}) {
338         push @found_clients, $details;
339     } else {
340         if ($args->{'count'}) {
341             # Don't print the client, only list the number of clients found
342             # at the end.
343         } else {
344            print_client($details);
345         }
346     }
347     $stats->{num_found}++;
348 }
349 
350 # ---------------------------------------------------------------------
351 
352 sub print_client {
353     my ($details) = @_;
354 
355     my $format = 'ho_tfind_line';
356     $format = 'ho_tfind_line_v6' if $details->{ipv6};
357 
358     $stats->{window}->printformat(MSGLEVEL_CRAP, $format, 
359         $details->{nick}, $details->{user}, $details->{host}, 
360         $details->{gecos}, $details->{ip});
361 }
362 
363 # ---------------------------------------------------------------------
364 
365 sub execute_raw_command {
366     my ($details, $server) = @_;
367 
368     my $cmd = $args->{rawcmd};
369     for (qw[ nick user host gecos ip ]) {
370         $cmd =~ s/%$_%/$details->{$_}/g;
371     }
372     $server->send_raw_now($cmd);
373 }
374 
375 # ---------------------------------------------------------------------
376 # Processes a single /TRACE output line and returns a hashref with the
377 # relevant data.
378 
379 sub get_trace_line_details {
380     my ($line) = @_;
381     
382     my $details;
383     
384     # TRACE
385     if ($line =~ /(User|Oper)\s+(\S+)\s+(\S+)\[([^@]+)@(\S+)\]\s+\(([^)]+)\)/) {
386         $details->{is_user} = 1 if $1 eq "User";
387         $details->{is_oper} = 1 if $1 eq "Oper";
388         $details->{class}   = $2;
389         $details->{nick}    = $3;
390         $details->{user}    = $4;
391         $details->{host}    = $5;
392         $details->{ip}      = $6;
393         $details->{ipv4}    = 1 if $details->{ip} !~ /:/;
394         $details->{ipv6}    = 1 if $details->{ip} =~ /:/;
395         return $details;
396     }
397 
398     # ETRACE
399     if ($line =~ /(User|Oper)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+:(.*)$/) {
400         $details->{is_user} = 1 if $1 eq "User";
401         $details->{is_oper} = 1 if $1 eq "Oper";
402         $details->{class}   = $2;
403         $details->{nick}    = $3;
404         $details->{user}    = $4;
405         $details->{host}    = $5;
406         $details->{ip}      = $6;
407         $details->{gecos}   = $7;
408         $details->{ipv4}    = 1 if $details->{ip} !~ /:/;
409         $details->{ipv6}    = 1 if $details->{ip} =~ /:/;
410         return $details;
411     }
412     
413     $details->{crap} = 1;
414     return $details;
415 }
416 
417 # ---------------------------------------------------------------------
418 
419 sub signal_stop {
420     my ($server, $data, $nick, $address) = @_;
421     Irssi::signal_stop();
422 }
423 
424 # ---------------------------------------------------------------------
425 
426 sub event_trace_end {
427     my ($server, $data, $nick, $address) = @_;
428     
429     trace_end();
430 }
431 
432 # ---------------------------------------------------------------------
433 
434 sub trace_end {
435     if (defined $args->{sort}) {
436         my @sorted_clients = sort_clients(@found_clients);
437         print_client($_) for @sorted_clients;
438     }
439 
440     ho_print("Found " . $stats->{num_found} . " match" . 
441         ($stats->{num_found} == 1 ? "" : "es") . 
442         " in " . $stats->{num_clients} . " client" .
443         ($stats->{num_clients} == 1 ? "" : "s") . ".");
444     $stats->{busy} = 0;
445 }
446 
447 # ---------------------------------------------------------------------
448 
449 sub sort_clients {
450     my @clients = @_;
451 
452     my %sort_params = (
453         n => {
454             field    => 'nick',
455             order    => 'normal',
456         },
457         N => {
458             field    => 'nick',
459             order    => 'reverse',
460         },
461         u => {
462             field    => 'user',
463             order    => 'normal',
464         },
465         U => {
466             field    => 'user',
467             order    => 'reverse',
468         },
469         h => {
470             field    => 'host',
471             order    => 'normal',
472         },
473         H => {
474             field    => 'host',
475             order    => 'reverse',
476         },
477         g => {
478             field    => 'gecos',
479             order    => 'normal',
480         },
481         G => {
482             field    => 'gecos',
483             order    => 'reverse',
484         },
485     );
486     
487     if (exists $sort_params{$args->{sort}}) {
488         return 
489             map { $_->[0] }
490             sort { 
491                 text_compare(
492                     $a->[1], $b->[1], 
493                     $sort_params{$args->{sort}}->{order},
494                     Irssi::settings_get_bool('ho_tfind_sort_case_sensitive')
495                 )
496             }
497             map { [ $_, $_->{ $sort_params{ $args->{sort} }->{field} } ] } 
498             @clients;
499     } elsif ($args->{sort} eq 'i') {
500         return 
501             map { $_->[0] }
502             sort { ip_compare($a->[1], $b->[1]) }
503             map { [ $_, $_->{ip} ] } 
504             @clients;
505     }
506 
507     return @clients;
508 }
509 
510 # ---------------------------------------------------------------------
511 
512 sub text_compare {
513     my ($first, $second, $reverse, $case_sensitive) = @_;
514 
515     if ($reverse eq 'normal') {
516         if ($case_sensitive) {
517             return ($first cmp $second);
518         } else {
519             return ((lc $first) cmp (lc $second));
520         }
521     } else {
522         if ($case_sensitive) {
523             return ((reverse $first) cmp (reverse $second));
524         } else {
525             return ((reverse lc $first) cmp (reverse lc $second));
526         }
527     }
528 }
529 
530 # ---------------------------------------------------------------------
531 # This is not an exact ip comparison, but it's Good Enough[tm].
532 # It treats the ip as a number and sorts that.
533 
534 sub ip_compare {
535     my ($first, $second) = @_;
536 
537     if ($first =~ /\./) {
538         return -1 if $second =~ /:/;
539         return $first <=> $second;
540     } else {
541         return 1 if $second =~ /\./;
542         return $first <=> $second;
543     }
544 }
545 
546 # ---------------------------------------------------------------------
547 
548 sub event_unknown_command {
549     my ($server, $data, $nick, $address) = @_;
550 
551     ho_print_error("This server does not support ETRACE.");
552     $stats->{busy} = 0;
553 }
554 
555 # ---------------------------------------------------------------------
556 # Initialisation
557 
558 ho_print_init_begin($SCRIPT_NAME);
559 
560 Irssi::settings_add_bool('ho', 'ho_tfind_use_cache', 0);
561 Irssi::settings_add_int('ho',  'ho_tfind_cache_expiry_time', 60);
562 Irssi::settings_add_bool('ho', 'ho_tfind_sort_case_sensitive', 0);
563 
564 Irssi::command_bind('tfind',       'cmd_tfind');
565 Irssi::command_bind('tfind help',  'cmd_tfind_help');
566 
567 Irssi::signal_add({
568     "redir event_trace_line"       => \&event_trace_line,
569     "redir event_trace_end"        => \&event_trace_end,
570     "redir event_stop"             => \&signal_stop,
571     "redir event_unknown_command"  => \&event_unknown_command,
572 });
573 
574 
575 # ok this is IMO realy ugly, if anyone has a suggestion please let me know
576 Irssi::Irc::Server::redirect_register("command cmd_tfind", 0, 0,
577     {
578         "event 203" => 1, # RPL_TRACEUNKNOWN
579         "event 204" => 1, # RPL_TRACEOPERATOR
580         "event 205" => 1, # RPL_TRACEUSER
581         "event 206" => 1, # RPL_TRACESERVER
582         "event 207" => 1, # RPL_TRACESERVICE
583         "event 208" => 1, # RPL_TRACENEWTYPE
584         "event 209" => 1, # RPL_TRACECLASS
585         "event 709" => 1, # ratbox ETRACE output 
586     },
587     {
588         "event 219" => 1,  # end of stats
589         "event 262" => 1,  # end of trace
590         "event 263" => 1,  # tryagain
591         "event 401" => 1,  # no such server
592         "event 421" => 1,  # unknow command (missing ETRACE)
593     },
594     undef,
595 );
596 
597 # Register format.
598 # nick, user, host, gecos, ip
599 
600 Irssi::theme_register([
601     'ho_tfind_line',
602     '{nick $[-9]0}{comment %g$[!15]4}{chanhost_hilight $[-11]1@$[!38]2}{comment $3}',
603     
604     'ho_tfind_line_v6',
605     '{nick $[-9]0}{comment %g$[!24]4}{chanhost_hilight $[-11]1@$[!38]2}{comment $3}',
606 ]);
607 
608 ho_print_init_end($SCRIPT_NAME);
609 
610 # ---------------------------------------------------------------------
611 # Help.
612 
613 sub print_usage {
614     ho_print_help('section', 'Syntax');
615     ho_print_help('syntax', '/TFIND [-]HELP');
616     ho_print_help('syntax', "/TFIND -<switch> [<arg>] -<switch> [<arg>] ..");
617     #ho_print_help('syntax', "/TFIND -<switch> [<arg>] [action [arguments]]\n");
618     #ho_print_help("Use /TFIND -help for help\n");
619 }
620 
621 sub print_help {
622     ho_print_help('head', $SCRIPT_NAME);
623     
624     print_usage();
625     
626     ho_print_help('section', 'Introduction');
627     ho_print_help("Script to search through /TRACE output.\n");
628     ho_print_help("Glob search has * and ? as wildcards.\n");
629     ho_print_help('This script has a cache built in, which is disabled by '.
630         'default. It can be enabled by setting ho_tfind_use_cache to ON. '.
631         'When a TRACE is sent to the server and caching is enabled, the '.
632         'TRACE output is stored internally. If another /TFIND is done '.
633         'shortly after the previous one, the cache data is used instead of '.
634         "sending another TRACE to the server.\n");
635     
636     ho_print_help('section', 'Arguments');
637     ho_print_help('argument', '-nocache',   
638         'Does not use cache, even if available.');
639 
640     my @args = (
641         [qw(nick Glob nickname)],
642         [qw(user Glob username)],
643         [qw(host Glob hostname)],
644         [qw(ip Glob ip)],
645         [qw(gecos Glob gecos)],
646         [qw(rnick Regexp nickname)],
647         [qw(ruser Regexp username)],
648         [qw(rhost Regexp hostname)],
649         [qw(rip Regexp ip)],
650         [qw(rgecos Regexp gecos)],
651     );
652     for my $arg (@args) {
653         ho_print_help('argument', '-' . $arg->[0] . ' <pattern>',
654         $arg->[1] . ' searches for <pattern> in the ' . $arg->[2] . '.');
655     }
656 
657     ho_print_help('argument', '-4',       'Searches for ipv4 clients.');
658     ho_print_help('argument', '-6',       'Searches for ipv6 clients.');
659     ho_print_help('argument', '-oper',    'Searches for opers.');
660     ho_print_help('argument', '-nooper',  'Excludes opers.');
661     ho_print_help('argument', '-spoof',   'Searches for spoofs.');
662     ho_print_help('argument', '-nospoof', 'Excludes spoofs.');
663 
664     ho_print_help('argument', '-equality <equality>', 
665         'Requires this equality. See below.');
666     ho_print_help('argument', '-sort <criterium>', 
667         'Sorts by criterium. See below.');
668     ho_print_help('argument', '-rawcmd "<cmd>"', 
669         'Executes the given command. See below.');
670 
671     ho_print_help('argument', '-etrace',  'Use ETRACE instead of TRACE.');
672     
673     ho_print_help('section', 'Settings');
674     ho_print_help('setting', 'ho_tfind_use_cache',
675         'Boolean indicating whether to cache /TRACE output.');
676     ho_print_help('setting', 'ho_tfind_cache_expiry_time',
677         'Time, in seconds, after which the cache becomes invalid.');
678     ho_print_help('setting', 'ho_tfind_sort_case_sensitive',
679         'Whether output sorting is case sensitive.');
680 #    ho_print_help('setting', 'ho_tfind_kline_time',
681 #        'Time (in minutes) for -kline option. [not functional yet]');
682 #    ho_print_help('setting', 'ho_tfind_kline_reason',
683 #        'Reason for -kline option. [not functional yet]');
684 #    ho_print_help('setting', 'ho_tfind_log_file',
685 #        'File to log found clients to. [not functional yet]');
686 
687     ho_print_help('section', 'Equality');
688     ho_print_help("Equality is a term which describes the relationship ".
689         "between a client's nick, user and realname (gecos). The following ".
690         "equalities exist:");
691     ho_print_help("  n   - all three are different");
692     ho_print_help("  nu  - nick equals username, realname is different");
693     ho_print_help("  nr  - nick equals realname, username is different");
694     ho_print_help("  ur  - username equals realname, nick is different");
695     ho_print_help("  nur - all three are equal\n");
696 
697     ho_print_help('section', 'Sorting');
698     ho_print_help("The output of clients can be sorted using the -sort ".
699         "option. The following search criteria are allowed:");
700     ho_print_help("  n - sort by nick");
701     ho_print_help("  N - sort by reversed nick");
702     ho_print_help("  u - sort by username");
703     ho_print_help("  U - sort by reversed username");
704     ho_print_help("  h - sort by host");
705     ho_print_help("  H - sort by reversed host");
706     ho_print_help("  g - sort by gecos");
707     ho_print_help("  G - sort by reversed gecos");
708     ho_print_help("  i - sort by ip");
709     ho_print_help("By default, sorting is done case insensitive. To " .
710         "change this, use the setting ho_tfind_sort_case_sensitive.\n");
711 
712     ho_print_help('section', 'Raw command');
713     ho_print_help("Using the -rawcmd option, you can make this script " .
714         "execute a raw IRCD command on all the found clients. Do not " .
715         "forget to put the double quotes around the command.");
716     ho_print_help("To make this feature actually useful, several " .
717         "strings are automatically replaced by the found client's " .
718         "properties before the raw command is sent. These are:");
719     ho_print_help("  %nick%  - the client's nick");
720     ho_print_help("  %user%  - the client's username");
721     ho_print_help("  %host%  - the client's hostname");
722     ho_print_help("  %ip%    - the client's ip");
723     ho_print_help("  %gecos% - the client's gecos");
724     ho_print_help("Remember: do not forget the quotes around the command!\n");
725 
726     ho_print_help('section', 'Examples');
727     ho_print_help('argument', '/tfind -spoof -nooper -sort H',
728         'Finds all spoofed, non-opered clients, sorted by their '.
729         'reversed hostname.');
730     ho_print_help('argument', '/tfind -rnick ^\[..+\].?[0-9]+$ -equality nu',
731         'Finds all clients with a [abc]-123 kind of nickname, whose ' .
732         'nickname is equal to their username.');
733     ho_print_help('argument', '/tfind -gecos "w3 rul3 j00r 4ss" -rawcmd '.
734         '"PRIVMSG %nick% :.die"', 'Finds all clients with the "we rule" '.
735         'gecos, and sends them a message ".die".');
736     ho_print_help('argument', '/tfind -rnick [A-Z]{4} -rawcmd '.
737         '"DLINE %ip% :drone"', 'Places a D-line on the ip of each client ' .
738         'with at least 4 consecutive uppercase letters in their nick.');
739 }


syntax highlighted by Code2HTML, v. 0.9.1