diff --git a/doc/FAQ.md b/doc/FAQ.md index 9244df78..308a5d08 100644 --- a/doc/FAQ.md +++ b/doc/FAQ.md @@ -6,7 +6,7 @@ This is a work in progress. More questions coming soon! * [How do I change the bot trigger?](#how-do-i-change-the-bot-trigger) * [How do I whitelist a user?](#how-do-i-whitelist-a-user) * [How do I change how the bot outputs multi-line messages?](#how-do-i-change-how-the-bot-outputs-multi-line-messages) -* [I made a command. It's supposed to output formatting with spaces and tabs?](#i-made-a-command-its-supposed-to-output-formatting-with-spaces-and-tabs) +* [How can I remove excessive spaces and tabs from command output?](#how-can-i-remove-excessive-spaces-and-tabs-from-command-output) * [How do I change my password?](#how-do-i-change-my-password) * [How do I make PBot remember my `date` timezone?](#how-do-i-make-pbot-remember-my-date-timezone) * [How do I make PBot remember my `weather` location?](#how-do-i-make-pbot-remember-my-weather-location) diff --git a/doc/Factoids.md b/doc/Factoids.md index 98b7b2a2..7bce4c96 100644 --- a/doc/Factoids.md +++ b/doc/Factoids.md @@ -28,6 +28,7 @@ * [$0](#0) * [List variables](#list-variables) * [Expansion modifiers](#expansion-modifiers) +* [Default values for variables and arguments](#default-values-for-variables-and-arguments) * [action_with_args](#action_with_args) * [add_nick](#add_nick) * [Channel namespaces](#channel-namespaces) @@ -437,6 +438,30 @@ Text Modifier | Description `:ucfirst` | Uppercases the first letter in the expansion. `:title` | Lowercases the expansion and then uppercases the initial letter of each word. +## Default values for variables and arguments +Factoid variables and argument variables may be provided with a default value to be used when the variable is undefined. + + !factadd cookie /me gives a cookie to ${args:-nobody. What a shame}! + cookie added to the global channel. + + !cookie Bob + * PBot gives a cookie to Bob! + + !cookie + * PBot gives a cookie to nobody. What a shame! + + !factadd sum /call calc $arg[0]:-1 + $arg[1]:-2 + sum added to the global channel. + + !sum + 1 + 2 = 3 + + !sum 3 + 3 + 2 = 5 + + !sum 4 6 + 4 + 6 = 10 + ## action_with_args You can use the [`factset`](#factset) command to set a special [factoid metadata](#factoid-metadata) key named `action_with_args` to trigger an alternate message if an argument has been supplied. diff --git a/doc/README.md b/doc/README.md index 178c1a94..f30937f5 100644 --- a/doc/README.md +++ b/doc/README.md @@ -200,6 +200,7 @@ * [$0](Factoids.md#0) * [List variables](Factoids.md#list-variables) * [Expansion modifiers](Factoids.md#expansion-modifiers) + * [Default values for variables and arguments](Factoids.md#default-values-for-variables-and-arguments) * [action_with_args](Factoids.md#action_with_args) * [add_nick](Factoids.md#add_nick) * [Channel namespaces](Factoids.md#channel-namespaces) @@ -363,7 +364,7 @@ * [How do I change the bot trigger?](FAQ.md#how-do-i-change-the-bot-trigger) * [How do I whitelist a user?](FAQ.md#how-do-i-whitelist-a-user) * [How do I change how the bot outputs multi-line messages?](FAQ.md#how-do-i-change-how-the-bot-outputs-multi-line-messages) - * [I made a command. It's supposed to output formatting with spaces and tabs?](FAQ.md#i-made-a-command-its-supposed-to-output-formatting-with-spaces-and-tabs) + * [How can I remove excessive spaces and tabs from command output?](FAQ.md#how-can-i-remove-excessive-spaces-and-tabs-from-command-output) * [How do I change my password?](FAQ.md#how-do-i-change-my-password) * [How do I make PBot remember my `date` timezone?](FAQ.md#how-do-i-make-pbot-remember-my-date-timezone) * [How do I make PBot remember my `weather` location?](FAQ.md#how-do-i-make-pbot-remember-my-weather-location) diff --git a/lib/PBot/Core/Factoids/Code.pm b/lib/PBot/Core/Factoids/Code.pm index 19b72d05..9b2e4aab 100644 --- a/lib/PBot/Core/Factoids/Code.pm +++ b/lib/PBot/Core/Factoids/Code.pm @@ -39,9 +39,9 @@ sub execute($self, $context) { # expand factoid action $args if ($factoids->get_data($context->{channel}, $context->{keyword}, 'allow_empty_args')) { - $context->{code} = $variables->expand_action_arguments($context->{code}, $context->{arguments}, ''); + $context->{code} = $variables->expand_action_arguments($context, $context->{code}, $context->{arguments}, ''); } else { - $context->{code} = $variables->expand_action_arguments($context->{code}, $context->{arguments}, $context->{nick}); + $context->{code} = $variables->expand_action_arguments($context, $context->{code}, $context->{arguments}, $context->{nick}); } } else { # otherwise allow nick overriding diff --git a/lib/PBot/Core/Factoids/Interpreter.pm b/lib/PBot/Core/Factoids/Interpreter.pm index 440e085b..df9cadc5 100644 --- a/lib/PBot/Core/Factoids/Interpreter.pm +++ b/lib/PBot/Core/Factoids/Interpreter.pm @@ -283,11 +283,10 @@ sub handle_action($self, $context, $action) { # trace context and context's contents if ($self->{pbot}->{registry}->get_value('general', 'debugcontext')) { use Data::Dumper; - $Data::Dumper::Sortkeys = sub { [sort grep { not /(?:cmdlist|arglist)/ } keys %$context] }; + $Data::Dumper::Sortkeys = 1; $Data::Dumper::Indent = 2; $self->{pbot}->{logger}->log("Factoids::handle_action [$action]\n"); $self->{pbot}->{logger}->log(Dumper $context); - $Data::Dumper::Sortkeys = 1; } if (not length $action) { @@ -341,7 +340,7 @@ sub handle_action($self, $context, $action) { if ($action =~ m/\$\{?args/ or $action =~ m/\$\{?arg\[/) { # factoid has $args, replace them if ($context->{interpolate}) { - $action = $self->{pbot}->{factoids}->{variables}->expand_action_arguments($action, $context->{arguments}, $context->{nick}); + $action = $self->{pbot}->{factoids}->{variables}->expand_action_arguments($context, $action, $context->{arguments}, $context->{nick}); } $context->{arguments} = ''; @@ -369,9 +368,9 @@ sub handle_action($self, $context, $action) { $context->{alldone} = 1; } else { if ($self->{pbot}->{factoids}->{data}->{storage}->get_data($channel, $keyword, 'allow_empty_args')) { - $action = $self->{pbot}->{factoids}->{variables}->expand_action_arguments($action, undef, ''); + $action = $self->{pbot}->{factoids}->{variables}->expand_action_arguments($context, $action, undef, ''); } else { - $action = $self->{pbot}->{factoids}->{variables}->expand_action_arguments($action, undef, $context->{nick}); + $action = $self->{pbot}->{factoids}->{variables}->expand_action_arguments($context, $action, undef, $context->{nick}); } } @@ -441,9 +440,9 @@ sub handle_action($self, $context, $action) { $action = $self->{pbot}->{factoids}->{variables}->expand_factoid_vars($context, $action); if ($self->{pbot}->{factoids}->{data}->{storage}->get_data($channel, $keyword, 'allow_empty_args')) { - $action = $self->{pbot}->{factoids}->{variables}->expand_action_arguments($action, $context->{arguments}, ''); + $action = $self->{pbot}->{factoids}->{variables}->expand_action_arguments($context, $action, $context->{arguments}, ''); } else { - $action = $self->{pbot}->{factoids}->{variables}->expand_action_arguments($action, $context->{arguments}, $context->{nick}); + $action = $self->{pbot}->{factoids}->{variables}->expand_action_arguments($context, $action, $context->{arguments}, $context->{nick}); } } diff --git a/lib/PBot/Core/Factoids/Modifiers.pm b/lib/PBot/Core/Factoids/Modifiers.pm index 5a545371..3d8da456 100644 --- a/lib/PBot/Core/Factoids/Modifiers.pm +++ b/lib/PBot/Core/Factoids/Modifiers.pm @@ -13,12 +13,23 @@ use PBot::Imports; sub initialize { } -sub parse($self, $modifier) { +sub parse($self, $modifier, $bracketed = 0) { my %modifiers; my $interp = $self->{pbot}->{interpreter}; - while ($$modifier =~ s/^:(?=\w)//) { + my $modregex; + my $defregex; + + if ($bracketed) { + $modregex = qr/^:(?=.+?:?)/; + $defregex = qr/^-([^:]+)/; + } else { + $modregex = qr/^:(?=[\w+-]+)/; + $defregex = qr/^-([\w]+)/; + } + + while ($$modifier =~ s/$modregex//) { if ($$modifier =~ s/^join\s*(?=\(.*?(?=\)))//) { my ($params, $rest) = $interp->extract_bracketed($$modifier, '(', ')', '', 1); $$modifier = $rest; @@ -37,6 +48,11 @@ sub parse($self, $modifier) { next; } + if ($$modifier =~ s/$defregex//) { + $modifiers{'default'} = $1; + next; + } + if ($$modifier=~ s/^pick_unique\s*(?=\(.*?(?=\)))//) { my ($params, $rest) = $interp->extract_bracketed($$modifier, '(', ')', '', 1); $$modifier = $rest; @@ -92,7 +108,7 @@ sub parse($self, $modifier) { next; } - if ($$modifier =~ s/^(enumerate|comma|ucfirst|lcfirst|title|uc|lc)//) { + if ($$modifier =~ s/^(enumerate|comma|ucfirst|lcfirst|title|uc|lc|json)//) { $modifiers{$1} = 1; next; } diff --git a/lib/PBot/Core/Factoids/Variables.pm b/lib/PBot/Core/Factoids/Variables.pm index 11d859ef..faaa3adf 100644 --- a/lib/PBot/Core/Factoids/Variables.pm +++ b/lib/PBot/Core/Factoids/Variables.pm @@ -62,7 +62,7 @@ sub expand_factoid_vars($self, $context, $action, %opts) { $result .= $1; my ($var, $orig_var); - my $modifiers; + my $modifiers = ''; my $extract_method; if ($rest =~ /^\{.*?\}/) { @@ -102,44 +102,66 @@ sub expand_factoid_vars($self, $context, $action, %opts) { my @factoids = $self->{pbot}->{factoids}->{data}->find($from, $var, exact_channel => 2, exact_trigger => 2); - if (not @factoids or not $factoids[0]) { - $result .= $extract_method eq 'bracket' ? '${' . $orig_var . '}' : '$' . $orig_var; - next; + my $var_chan; + if (@factoids && $factoids[0]) { + ($var_chan, $var) = ($factoids[0]->[0], $factoids[0]->[1]); + + if ($self->{pbot}->{factoids}->{data}->{storage}->get_data($var_chan, $var, 'action') =~ m{^/call (.*)}ms) { + $var = $1; + + if (++$recurse > 100) { + $self->{pbot}->{logger}->log("Factoids: variable expansion recursion limit reached\n"); + $result .= $extract_method eq 'bracket' ? '${' . $orig_var . '}' : '$' . $orig_var; + next; + } + + goto ALIAS; + } } - my $var_chan; - ($var_chan, $var) = ($factoids[0]->[0], $factoids[0]->[1]); + my $copy; + my $bracketed = 0; - if ($self->{pbot}->{factoids}->{data}->{storage}->get_data($var_chan, $var, 'action') =~ m{^/call (.*)}ms) { - $var = $1; + if ($extract_method eq 'bracket') { + $copy = $modifiers; + $bracketed = 1; + } else { + $copy = $rest; + } - if (++$recurse > 100) { - $self->{pbot}->{logger}->log("Factoids: variable expansion recursion limit reached\n"); + my %settings = $self->{pbot}->{factoids}->{modifiers}->parse(\$copy, $bracketed); + + my $change; + + if (not @factoids or not $factoids[0]) { + if (exists $settings{default} && length $settings{default}) { + $change = $settings{default}; + } else { $result .= $extract_method eq 'bracket' ? '${' . $orig_var . '}' : '$' . $orig_var; next; } - - goto ALIAS; + } else { + if ($self->{pbot}->{factoids}->{data}->{storage}->get_data($var_chan, $var, 'type') eq 'text') { + $change = $self->{pbot}->{factoids}->{data}->{storage}->get_data($var_chan, $var, 'action'); + } } - if ($modifiers) { - $rest = $modifiers . $rest; - } - - my $copy = $rest; - my %settings = $self->{pbot}->{factoids}->{modifiers}->parse(\$copy); - - if ($self->{pbot}->{factoids}->{data}->{storage}->get_data($var_chan, $var, 'type') eq 'text') { - my $change = $self->{pbot}->{factoids}->{data}->{storage}->get_data($var_chan, $var, 'action'); - my @list = $self->{pbot}->{interpreter}->split_line($change); - + if (defined $change) { + my @list = $self->{pbot}->{interpreter}->split_line($change); my @replacements; + my $ref; + + if ($extract_method eq 'bracket') { + $ref = \$modifiers; + } else { + $ref = \$rest; + } if (wantarray) { - @replacements = $self->{pbot}->{factoids}->{selectors}->select_item($context, join ('|', @list), \$rest, %opts); + @replacements = $self->{pbot}->{factoids}->{selectors}->select_item($context, join ('|', @list), $ref, %opts); return @replacements; } else { - push @replacements, scalar $self->{pbot}->{factoids}->{selectors}->select_item($context, join ('|', @list), \$rest, %opts); + push @replacements, scalar $self->{pbot}->{factoids}->{selectors}->select_item($context, join ('|', @list), $ref, %opts); } my $replacement = $opts{nested} ? join('|', @replacements) : "@replacements"; @@ -210,104 +232,218 @@ sub expand_factoid_vars($self, $context, $action, %opts) { return validate_string($result, $self->{pbot}->{registry}->get_value('factoids', 'max_content_length')); } -sub expand_action_arguments($self, $action, $input, $nick) { +sub expand_action_arguments($self, $context, $action, $input = '', $nick = '') { $action = validate_string($action, $self->{pbot}->{registry}->get_value('factoids', 'max_content_length')); $input = validate_string($input, $self->{pbot}->{registry}->get_value('factoids', 'max_content_length')); - my %h; - - if (not defined $input or $input eq '') { - $input = ''; - %h = (args => $nick); - } else { - %h = (args => $input); - } - - my $jsonargs = to_json \%h; - $jsonargs =~ s/^{".*":"//; - $jsonargs =~ s/"}$//; + my %opts = ( + nested => 0, + ); my @args = $self->{pbot}->{interpreter}->split_line($input); $action =~ s/\$arglen\b|\$\{arglen\}/scalar @args/eg; - $action =~ s/\$args:json|\$\{args:json\}/$jsonargs/g; - if ($input eq '') { - $action =~ s/\$p?args(?![[\w])|\$\{p?args(?![[\w])\}/$nick/g; - } else { - $action =~ s/\$args(?![[\w])|\$\{args(?![[\w])\}/$input/g; + my $root_keyword = $context->{keyword_override} ? $context->{keyword_override} : $context->{root_keyword}; + + if ($action =~ m/^\/call --keyword-override=([^ ]+)/i) { + $root_keyword = $1; } my $depth = 0; my $const_action = $action; - while ($const_action =~ m/\$arg\[([^]]+)]|\$\{arg\[([^]]+)]\}/g) { - my $arg = defined $2 ? $2 : $1; + my $result = ''; + my $rest = $action; - last if ++$depth >= 100; + while (++$depth < 100) { + $rest =~ s/(?{pbot}->{interpreter}->extract_bracketed($rest, '{', '}'); + $orig_var = $var; + + if ($var =~ /:/) { + my @stuff = split /:/, $var, 2; + $var = $stuff[0]; + $modifiers = ':' . $stuff[1]; + } + + $extract_method = 'bracket'; } else { - $action =~ s/\$arg\[\*\]|\$\{arg\[\*\]\}/$input/; + $rest =~ s/^(\w+)//; + $var = $orig_var = $1; + $extract_method = 'regex'; } - next; - } - - if ($arg =~ m/([^:]*):(.*)/) { - my $arg1 = $1; - my $arg2 = $2; - - my $arg1i = $arg1; - my $arg2i = $arg2; - - $arg1i = 0 if $arg1i eq ''; - $arg2i = $#args if $arg2i eq ''; - $arg2i = $#args if $arg2i > $#args; - - my @values = eval { - local $SIG{__WARN__} = sub { }; - return @args[$arg1i .. $arg2i]; - }; - - if ($@) { + if ($var ne 'args' && $var ne 'arg') { + $result .= $extract_method eq 'bracket' ? '${' . $orig_var . '}' : '$' . $orig_var; next; - } else { - my $string = join(' ', @values); + } - if ($string eq '') { - $action =~ s/\s*\$\{arg\[$arg1:$arg2\]\}// || $action =~ s/\s*\$arg\[$arg1:$arg2\]//; + $matches++; + + my $value; + my $use_nick = 0; + + if ($var eq 'args') { + if (!defined $input || !length $input) { + $value = undef; + $use_nick = 1; } else { - $action =~ s/\$\{arg\[$arg1:$arg2\]\}/$string/ || $action =~ s/\$arg\[$arg1:$arg2\]/$string/; + $value = $input; + } + } else { + my $index; + if ($rest =~ s/\[(.*?)\]//) { + $index = $1; + } else { + $result .= $extract_method eq 'bracket' ? '${' . $orig_var . '}' : '$' . $orig_var; + next; + } + + if ($index eq '*') { + if (!defined $input || !length $input) { + $value = undef; + $use_nick = 1; + } else { + $value = $input; + } + } elsif ($index =~ m/([^:]*):(.*)/) { + my $arg1 = $1; + my $arg2 = $2; + + my $arg1i = $arg1; + my $arg2i = $arg2; + + $arg1i = 0 if $arg1i eq ''; + $arg2i = $#args if $arg2i eq ''; + $arg2i = $#args if $arg2i > $#args; + + my @values = eval { + local $SIG{__WARN__} = sub { }; + return @args[$arg1i .. $arg2i]; + }; + + if ($@) { + $value = undef; + } else { + $value = join(' ', @values); + } + } else { + $value = eval { + local $SIG{__WARN__} = sub { }; + return $args[$index]; + }; + + if ($@) { + if ($index == 0) { + $value = $nick; + } else { + $value = undef; + } + } + + if (!defined $value) { + if ($index == 0) { + $use_nick = 1; + } + } } } - next; + my %settings; + if ($extract_method eq 'bracket') { + %settings = $self->{pbot}->{factoids}->{modifiers}->parse(\$modifiers, 1); + } else { + %settings = $self->{pbot}->{factoids}->{modifiers}->parse(\$rest); + } + + my $change; + + if (!defined $value || !length $value) { + if (exists $settings{default} && length $settings{default}) { + $change = $settings{default}; + } else { + if ($use_nick) { + $change = $nick; + } else { + $change = ''; + } + } + } else { + $change = $value; + } + + if (defined $change) { + if (not length $change) { + $result =~ s/\s+$//; + } + + if ($settings{'uc'}) { + $change = uc $change; + } + + if ($settings{'lc'}) { + $change = lc $change; + } + + if ($settings{'ucfirst'}) { + $change = ucfirst $change; + } + + if ($settings{'title'}) { + $change = ucfirst lc $change; + $change =~ s/ (\w)/' ' . uc $1/ge; + } + + if ($settings{'json'}) { + $change = $self->escape_json($change); + } + + if ($result =~ s/\b(a|an)(\s+)$//i) { + my ($article, $trailing) = ($1, $2); + my $fixed_article = select_indefinite_article $change; + + if ($article eq 'AN') { + $fixed_article = uc $fixed_article; + } elsif ($article eq 'An' or $article eq 'A') { + $fixed_article = ucfirst $fixed_article; + } + + $change = $fixed_article . $trailing . $change; + } + + $result .= $change; + + $expansions++; + } } - my $value = eval { - local $SIG{__WARN__} = sub { }; - return $args[$arg]; - }; + last if $matches == 0 or $expansions == 0; - if ($@) { - next; - } else { - if (not defined $value) { - if ($arg == 0) { - $action =~ s/\$\{arg\[$arg\]\}/$nick/ || $action =~ s/\$arg\[$arg\]/$nick/; - } else { - $action =~ s/\s*\$\{arg\[$arg\]\}// || $action =~ s/\s*\$arg\[$arg\]//; - } - } else { - $action =~ s/\$arg\{\[$arg\]\}/$value/ || $action =~ s/\$arg\[$arg\]/$value/; - } + if (not length $rest) { + $rest = $result; + $result = ''; } } - return $action; + $result .= $rest; + + # unescape certain symbols + $result =~ s/(?{pbot}->{registry}->get_value('factoids', 'max_content_length')); } sub escape_json($self, $text) { diff --git a/lib/PBot/VERSION.pm b/lib/PBot/VERSION.pm index cf987f02..9854b8ec 100644 --- a/lib/PBot/VERSION.pm +++ b/lib/PBot/VERSION.pm @@ -25,8 +25,8 @@ use PBot::Imports; # These are set by the /misc/update_version script use constant { BUILD_NAME => "PBot", - BUILD_REVISION => 4858, - BUILD_DATE => "2024-11-22", + BUILD_REVISION => 4859, + BUILD_DATE => "2024-11-27", }; sub initialize {}