App-karr
view release on metacpan or search on metacpan
lib/App/karr/Dispatch.pm view on Meta::CPAN
}
# An option name with a dash in it does not survive standing behind a boolean
# flag, so the flags are respelled with underscores here, before MooX::Cmd and
# MooX::Options ever look at argv (ticket #256).
#
# App::karr::Role::CliArgs/normalize_option_argv carries the whole diagnosis and
# the condition under which this call may go again. What belongs here is only
# why the CALL is here: the rewrite has to know which token is a flag and which
# is the value of the flag in front of it, that answer comes out of the option
# table of the command being run, and this is the last point at which the whole
# argv is still in one piece.
#
# Which command that is, is decided exactly the way MooX::Cmd decides it half a
# millisecond later: the first argv token that names a command
# (MooX::Cmd::Role::_initialize_from_cmd, `first_index` across the WHOLE argv --
# see the %COMMAND_ALIASES comment in App::karr for why the table is asked and
# argv is not rewritten). Everything before that token is the root's own argv
# and is normalized against App::karr, everything after it against the command
# class. The command name itself is passed through untouched, which is what
# keeps the dashed command spellings (`get-refs`) findable in the table.
sub _normalize_option_argv {
my $commands = App::karr->_build_command_commands( {} );
my ( $at, $class );
for my $i ( 0 .. $#ARGV ) {
next unless defined $commands->{ $ARGV[$i] };
( $at, $class ) = ( $i, $commands->{ $ARGV[$i] } );
last;
}
my @root = defined $at ? @ARGV[ 0 .. $at - 1 ] : @ARGV;
my @name = defined $at ? ( $ARGV[$at] ) : ();
my @rest = defined $at ? @ARGV[ $at + 1 .. $#ARGV ] : ();
# Both calls are class-method calls: normalize_option_argv reads the option
# table and nothing else, so it needs no instance -- and no instance exists
# yet, which is the point of doing this here. Every App::karr::Cmd::* class
# composes App::karr::Role::CliArgs (t/256 pins that, because a command that
# forgot to would silently keep the defect for its own dashed options), but
# the guard stays: MooX::Cmd's plugin scan decides what a command class is,
# not this file.
@root = App::karr->normalize_option_argv( \@root );
if ( defined $class ) {
use_module($class);
@rest = $class->normalize_option_argv( \@rest )
if $class->can('normalize_option_argv');
}
@ARGV = ( @root, @name, @rest );
return;
}
sub dispatch {
my (@argv) = @_;
# dispatch operates on the global @ARGV, exactly as bin/karr did inline:
# the two rewrites above and MooX::Cmd::new_with_cmd all read and write it.
# Localising it lets an embedding host call dispatch repeatedly, and lets
# bin/karr pass its own @ARGV in unchanged.
local @ARGV = @argv;
# The character/octet boundary (ticket #53). Everything the OS hands in is
# bytes; everything a command body sees is Perl characters. @ARGV comes in
# decoded, STDOUT and STDERR encode on the way out, and no command body
# encodes anything itself. STDIN stays raw on purpose -- every reader of it
# decodes its own payload (#246).
enable_std_utf8();
decode_argv();
# The caller's own words, kept for the suggestion line an option-parse error
# ends on (ticket k263). Recorded HERE because both rewrites below change
# argv and neither leaves what anyone typed: _refuse_empty_argument's
# diagnosis reads the raw line, and _normalize_option_argv respells
# --claimed-by as --claimed_by and folds a flag-shaped value onto its option
# with an `=`. App::karr::Role::ExitCodes reads it back through
# App::karr::Error, and prints no suggestion at all where nothing was
# recorded.
set_original_argv(@ARGV);
# Inside the eval on purpose: the "Usage error:" marker is what
# App::karr::Error::is_usage_error keys on, so the handler below turns this
# into exit 2 through the same path as every other usage error.
my $ran = eval { _refuse_empty_argument(); _normalize_option_argv(); App::karr->new_with_cmd; 1 };
if ( !$ran ) {
my $err = $@;
# An embedding host's exit-signal (see EMBEDDING) is not a command that
# died: hand it back rather than classifying it. Nothing in karr's own
# code answers this, so under bin/karr -- where exit() really exits and
# never reaches this eval -- it never fires.
die $err if blessed($err) && $err->can('__karr_dispatch_exit');
# Exit-code contract (ADR 0002): 0 success / 1 runtime failure / 2 usage
# error. This is the central handler the ADR calls for: it catches every
# uncaught die from a command body and turns it into a deterministic 1
# or 2, replacing the accidental 255 an uncaught die used to leak.
#
# Usage-error dies carry one of these stable leading markers:
# "Unknown command:" the dispatch guard in App::karr
# "unexpected extra argument" surplus positionals (Role::CliArgs)
# "Usage:" a missing required positional
# "Usage error:" anything a command rejects as misuse
# that is not one of the shapes above --
# raised via App::karr::Role::ExitCodes'
# usage_error(), the generic entry point
# for e.g. an out-of-range option value
# Any new usage-error die must start with one of these; prefer
# usage_error() over inventing a fifth marker. Everything else -- task
# not found, board missing, a Git/sync failure, a refused destructive
# operation -- is a runtime failure (1).
#
# The markers themselves live in App::karr::Error::is_usage_error,
# because this handler is no longer their only reader: the batch runner
# in App::karr::Role::TaskMutation asks the same question to decide
# whether a failure belongs to one id or to the whole invocation (#61).
#
# Option-parse errors (unknown option, unparseable option value) never
# reach here: MooX::Options exits 2 directly via
# App::karr::Role::ExitCodes (and the root's _print_help), so those
# exits bypass this eval.
lib/App/karr/Dispatch.pm view on Meta::CPAN
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
App::karr::Dispatch - The one dispatch path shared by bin/karr and the in-process test runner
=head1 VERSION
version 0.600
=head1 SYNOPSIS
use App::karr::Dispatch qw( dispatch );
dispatch(@ARGV);
=head1 DESCRIPTION
Everything that sits between a program's start and C<< App::karr->new_with_cmd >>
lived inline in F<bin/karr>: the character/octet boundary setup, the record of
the caller's own argv, the two argv rewrites (empty-argument refusal and
dashed-option normalisation), and the central handler that turns an uncaught
C<die> into the exit-code contract (ADR 0002). This module is that code, moved
out whole so the F<karr> executable and the in-process test runner
(F<t/lib/TestKarr.pm>) share B<one> dispatch path rather than two copies that
drift.
L</dispatch> does exactly what F<bin/karr> used to do inline. F<bin/karr> is now
a thin wrapper that calls it (and keeps the END block that flushes
L<App::karr::SyncGuard>, which is a process-lifecycle concern -- see there).
=head1 EMBEDDING
An embedding host may run L</dispatch> many times in one interpreter -- the
in-process test runner does, to skip ~0.3s of Perl startup per C<karr> call.
Such a host installs, in a C<BEGIN> block before L<App::karr> is compiled, an
override of C<CORE::GLOBAL::exit> that raises an exception instead of tearing
the whole interpreter down. karr reaches C<exit> from three places -- this
module's handler, L<App::karr::Role::ExitCodes>, and L<App::karr/_print_help> --
and the override catches all three.
So that L</dispatch>'s own handler does not mistake such an exit-signal for a
command that died, it re-raises any caught exception that answers true to a
C<__karr_dispatch_exit> method, leaving it for whoever installed the override.
Nothing in karr's own code blesses such an object, so under F<bin/karr> -- which
installs no override and lets C<exit> exit -- this never fires and the handler
behaves exactly as it always did.
=head2 dispatch
dispatch(@ARGV);
Runs one C<karr> invocation: sets up the character/octet boundary, records the
caller's argv, applies the empty-argument (#243) and dashed-option (#256)
rewrites, and calls C<< App::karr->new_with_cmd >> inside the central
exit-code handler (ADR 0002). Operates on a localised C<@ARGV>. Returns nothing
on success; on failure it prints the message to C<STDERR> and C<exit>s C<1> or
C<2> -- which, under an embedding host that overrides C<exit> (see
L</EMBEDDING>), becomes the host's exit-signal instead.
=head1 SEE ALSO
L<karr>, L<App::karr>, L<App::karr::Error>, L<App::karr::Encoding>,
L<App::karr::SyncGuard>
=head1 SUPPORT
=head2 Issues
Please report bugs and feature requests on GitHub at
L<https://github.com/Getty/karr/issues>.
=head2 IRC
Join C<#langertha> on C<irc.perl.org> or message Getty directly.
=head1 CONTRIBUTING
Contributions are welcome! Please fork the repository and submit a pull request.
=head1 AUTHOR
Torsten Raudssus <getty@cpan.org>
=head1 COPYRIGHT AND LICENSE
This software is Copyright (c) 2026 by Torsten Raudssus <torsten@raudssus.de> L<https://raudssus.de/>.
This is free software, licensed under:
The Artistic License 2.0 (GPL Compatible)
=cut
( run in 0.287 second using v1.01-cache-2.11-cpan-ad19def0cd9 )