1# --
2# Copyright (C) 2001-2020 OTRS AG, https://otrs.com/
3# --
4# This software comes with ABSOLUTELY NO WARRANTY. For details, see
5# the enclosed file COPYING for license information (GPL). If you
6# did not receive this file, see https://www.gnu.org/licenses/gpl-3.0.txt.
7# --
8
9package Kernel::System::Console::BaseCommand;
10
11use strict;
12use warnings;
13
14use Getopt::Long();
15use Term::ANSIColor();
16use IO::Interactive();
17use Encode::Locale();
18use Kernel::System::VariableCheck qw(:all);
19
20our @ObjectDependencies = (
21    'Kernel::Config',
22    'Kernel::System::Cache',
23    'Kernel::System::Encode',
24    'Kernel::System::Main',
25);
26
27our $SuppressANSI = 0;
28
29=head1 NAME
30
31Kernel::System::Console::BaseCommand - command base class
32
33=head1 DESCRIPTION
34
35Base class for console commands.
36
37=head1 PUBLIC INTERFACE
38
39=head2 new()
40
41constructor for new objects. You should not need to reimplement this in your command,
42override L</Configure()> instead if you need to.
43
44=cut
45
46sub new {
47    my ( $Type, %Param ) = @_;
48
49    my $Self = {};
50    bless( $Self, $Type );
51
52    # for usage help
53    $Self->{Name} = $Type;
54    $Self->{Name} =~ s{Kernel::System::Console::Command::}{}smx;
55
56    $Self->{ANSI} = 1;
57
58    # Check if we are in an interactive terminal, disable colors otherwise.
59    if ( !IO::Interactive::is_interactive() ) {
60        $Self->{ANSI} = 0;
61    }
62
63    # Force creation of the EncodeObject as it initialzes STDOUT/STDERR for unicode output.
64    $Kernel::OM->Get('Kernel::System::Encode');
65
66    # Call object specific configure method. We use an eval to trap any exceptions
67    #   that might occur here. Only if everything was ok we set the _ConfigureSuccessful
68    #   flag.
69    eval {
70        $Self->Configure();
71        $Self->{_ConfigureSuccessful} = 1;
72    };
73
74    $Self->{_GlobalOptions} = [
75        {
76            Name        => 'help',
77            Description => 'Display help for this command.',
78        },
79        {
80            Name        => 'no-ansi',
81            Description => 'Do not perform ANSI terminal output coloring.',
82        },
83        {
84            Name        => 'quiet',
85            Description => 'Suppress informative output, only retain error messages.',
86        },
87        {
88            Name => 'allow-root',
89            Description =>
90                'Allow root user to execute the command. This might damage your system; use at your own risk.',
91            Invisible => 1,    # hide from usage screen
92        },
93    ];
94
95    return $Self;
96}
97
98=head2 Configure()
99
100initializes this object. Override this method in your commands.
101
102This method might throw exceptions.
103
104=cut
105
106sub Configure {
107    return;
108}
109
110=head2 Name()
111
112get the Name of the current Command, e. g. 'Admin::User::SetPassword'.
113
114=cut
115
116sub Name {
117    my ($Self) = @_;
118
119    return $Self->{Name};
120}
121
122=head2 Description()
123
124get/set description for the current command. Call this in your Configure() method.
125
126=cut
127
128sub Description {
129    my ( $Self, $Description ) = @_;
130
131    $Self->{Description} = $Description if defined $Description;
132
133    return $Self->{Description};
134}
135
136=head2 AdditionalHelp()
137
138get/set additional help text for the current command. Call this in your Configure() method.
139
140You can use color tags (see L</Print()>) in your help tags.
141
142=cut
143
144sub AdditionalHelp {
145    my ( $Self, $AdditionalHelp ) = @_;
146
147    $Self->{AdditionalHelp} = $AdditionalHelp if defined $AdditionalHelp;
148
149    return $Self->{AdditionalHelp};
150}
151
152=head2 AddArgument()
153
154adds an argument that can/must be specified on the command line.
155This function must be called during Configure() by the command to
156indicate which arguments it can process.
157
158    $CommandObject->AddArgument(
159        Name         => 'filename',
160        Description  => 'name of the file to be loaded',
161        Required     => 1,
162        ValueRegex   => qr{a-zA-Z0-9]\.txt},
163    );
164
165Please note that required arguments have to be specified before any optional ones.
166
167The information about known arguments and options (see below) will be used to generate
168usage help and also to automatically verify the data provided by the user on the command line.
169
170=cut
171
172sub AddArgument {
173    my ( $Self, %Param ) = @_;
174
175    for my $Key (qw(Name Description ValueRegex)) {
176        if ( !$Param{$Key} ) {
177            $Self->PrintError("Need $Key.");
178            die;
179        }
180    }
181
182    for my $Key (qw(Required)) {
183        if ( !defined $Param{$Key} ) {
184            $Self->PrintError("Need $Key.");
185            die;
186        }
187    }
188
189    if ( $Self->{_ArgumentSeen}->{ $Param{Name} }++ ) {
190        $Self->PrintError("Cannot register argument '$Param{Name}' twice.");
191        die;
192    }
193
194    if ( $Self->{_OptionSeen}->{ $Param{Name} } ) {
195        $Self->PrintError("Cannot add argument '$Param{Name}', because it is already registered as an option.");
196        die;
197    }
198
199    $Self->{_Arguments} //= [];
200    push @{ $Self->{_Arguments} }, \%Param;
201
202    return;
203}
204
205=head2 GetArgument()
206
207fetch an argument value as provided by the user on the command line.
208
209    my $Filename = $CommandObject->GetArgument('filename');
210
211=cut
212
213sub GetArgument {
214    my ( $Self, $Argument ) = @_;
215
216    if ( !$Self->{_ArgumentSeen}->{$Argument} ) {
217        $Self->PrintError("Argument '$Argument' was not configured and cannot be accessed.");
218        return;
219    }
220
221    return $Self->{_ParsedARGV}->{Arguments}->{$Argument};
222}
223
224=head2 AddOption()
225
226adds an option that can/must be specified on the command line.
227This function must be called during L</Configure()> by the command to
228indicate which arguments it can process.
229
230    $CommandObject->AddOption(
231        Name         => 'iterations',
232        Description  => 'number of script iterations to perform',
233        Required     => 1,
234        HasValue     => 0,
235        ValueRegex   => qr{\d+},
236        Multiple     => 0,  # optional, allow more than one occurrence (only possible if HasValue is true)
237    );
238
239B<Option Naming Conventions>
240
241If there is a source and a target involved in the command, the related options should start
242with C<--source> and C<--target>, for example C<--source-path>.
243
244For specifying filesystem locations, C<--*-path> should be used for directory/filename
245combinations (e.g. C<mydirectory/myfile.pl>), C<--*-filename> for filenames without directories,
246and C<--*-directory> for directories.
247
248Example: C<--target-path /tmp/test.out --source-filename test.txt --source-directory /tmp>
249
250=cut
251
252sub AddOption {
253    my ( $Self, %Param ) = @_;
254
255    for my $Key (qw(Name Description)) {
256        if ( !$Param{$Key} ) {
257            $Self->PrintError("Need $Key.");
258            die;
259        }
260    }
261
262    for my $Key (qw(Required HasValue)) {
263        if ( !defined $Param{$Key} ) {
264            $Self->PrintError("Need $Key.");
265            die;
266        }
267    }
268
269    if ( $Param{HasValue} ) {
270        if ( !$Param{ValueRegex} ) {
271            $Self->PrintError("Need ValueRegex.");
272            die;
273        }
274    }
275
276    if ( $Param{Multiple} && !$Param{HasValue} ) {
277        $Self->PrintError("Multiple can only be specified if HasValue is true.");
278        die;
279    }
280
281    if ( $Self->{_OptionSeen}->{ $Param{Name} }++ ) {
282        $Self->PrintError("Cannot register option '$Param{Name}' twice.");
283        die;
284    }
285
286    if ( $Self->{_ArgumentSeen}->{ $Param{Name} } ) {
287        $Self->PrintError("Cannot add option '$Param{Name}', because it is already registered as an argument.");
288        die;
289    }
290
291    $Self->{_Options} //= [];
292    push @{ $Self->{_Options} }, \%Param;
293
294    return;
295}
296
297=head2 GetOption()
298
299fetch an option as provided by the user on the command line.
300
301    my $Iterations = $CommandObject->GetOption('iterations');
302
303If the option was specified with HasValue => 1, the user provided value will be
304returned. Otherwise 1 will be returned if the option was present.
305
306In case of an option with C<Multiple => 1>, an array reference will be returned
307if the option was specified, and undef otherwise.
308
309=cut
310
311sub GetOption {
312    my ( $Self, $Option ) = @_;
313
314    if ( !$Self->{_OptionSeen}->{$Option} ) {
315        $Self->PrintError("Option '--$Option' was not configured and cannot be accessed.");
316        return;
317    }
318
319    return $Self->{_ParsedARGV}->{Options}->{$Option};
320
321}
322
323=head2 PreRun()
324
325perform additional validations/preparations before Run(). Override this method in your commands.
326
327If this method returns, execution will be continued. If it throws an exception with die(), the program aborts at this point, and Run() will not be called.
328
329=cut
330
331sub PreRun {
332    return 1;
333}
334
335=head2 Run()
336
337runs the command. Override this method in your commands.
338
339This method needs to return the exit code to be used for the whole program.
340For this, the convenience methods ExitCodeOk() and ExitCodeError() can be used.
341
342In case of an exception, the error code will be set to 1 (error).
343
344=cut
345
346sub Run {
347    my $Self = shift;
348
349    return $Self->ExitCodeOk();
350}
351
352=head2 PostRun()
353
354perform additional cleanups after Run(). Override this method in your commands.
355
356The return value of this method will be ignored. It will be called after Run(), even
357if Run() caused an exception or returned an error exit code.
358
359In case of an exception in this method, the exit code will be set to 1 (error) if
360Run() returned 0 (ok).
361
362=cut
363
364sub PostRun {
365    return;
366}
367
368=head2 Execute()
369
370this method will parse/validate the command line arguments supplied by the user.
371If that was ok, the Run() method of the command will be called.
372
373=cut
374
375sub Execute {
376    my ( $Self, @CommandlineArguments ) = @_;
377
378    # Normally, nothing was logged until this point, so the LogObject does not exist yet.
379    #   Change the LogPrefix so that it indicates which command causes the log entry.
380    #   In future we might need to check if it was created and update it on the fly.
381    $Kernel::OM->ObjectParamAdd(
382        'Kernel::System::Log' => {
383            LogPrefix => 'OTRS-otrs.Console.pl-' . $Self->Name(),
384        },
385    );
386
387    my $ParsedGlobalOptions = $Self->_ParseGlobalOptions( \@CommandlineArguments );
388
389    # Don't allow to run these scripts as root.
390    if ( !$ParsedGlobalOptions->{'allow-root'} && $> == 0 ) {    # $EFFECTIVE_USER_ID
391        $Self->PrintError(
392            "You cannot run otrs.Console.pl as root. Please run it as the 'otrs' user or with the help of su:"
393        );
394        $Self->Print("  <yellow>su -c \"bin/otrs.Console.pl MyCommand\" -s /bin/bash otrs</yellow>\n");
395        return $Self->ExitCodeError();
396    }
397
398    # Disable in-memory cache to avoid problems with long running scripts.
399    $Kernel::OM->Get('Kernel::System::Cache')->Configure(
400        CacheInMemory => 0,
401    );
402
403    # Only run if the command was setup ok.
404    if ( !$Self->{_ConfigureSuccessful} ) {
405        $Self->PrintError("Aborting because the command was not successfully configured.");
406        return $Self->ExitCodeError();
407    }
408
409    # First handle the optional global options.
410    if ( $ParsedGlobalOptions->{'no-ansi'} ) {
411        $Self->ANSI(0);
412    }
413
414    if ( $ParsedGlobalOptions->{help} ) {
415        print "\n" . $Self->GetUsageHelp();
416        return $Self->ExitCodeOk();
417    }
418
419    if ( $ParsedGlobalOptions->{quiet} ) {
420        $Self->{Quiet} = 1;
421    }
422
423    # Parse command line arguments and bail out in case of error,
424    # of course with a helpful usage screen.
425    $Self->{_ParsedARGV} = $Self->_ParseCommandlineArguments( \@CommandlineArguments );
426    if ( !%{ $Self->{_ParsedARGV} // {} } ) {
427        print STDERR "\n" . $Self->GetUsageHelp();
428        return $Self->ExitCodeError();
429    }
430
431    # If we have an interactive console, make sure that the output can handle UTF-8.
432    if (
433        IO::Interactive::is_interactive()
434        && !$Kernel::OM->Get('Kernel::Config')->Get('SuppressConsoleEncodingCheck')
435        )
436    {
437        my $ConsoleEncoding = lc $Encode::Locale::ENCODING_CONSOLE_OUT;    ## no critic
438
439        if ( $ConsoleEncoding ne 'utf-8' ) {
440            $Self->PrintError(
441                "The terminal encoding should be set to 'utf-8', but is '$ConsoleEncoding'. Some characters might not be displayed correctly."
442            );
443        }
444    }
445
446    eval { $Self->PreRun(); };
447    if ($@) {
448        $Self->PrintError($@);
449        return $Self->ExitCodeError();
450    }
451
452    # Make sure we get a proper exit code to return to the shell.
453    my $ExitCode;
454    eval {
455        # Make sure that PostRun() works even if a user presses ^C.
456        local $SIG{INT} = sub {
457            $Self->PostRun();
458            exit $Self->ExitCodeError();
459        };
460        $ExitCode = $Self->Run();
461    };
462    if ($@) {
463        $Self->PrintError($@);
464        $ExitCode = $Self->ExitCodeError();
465    }
466
467    eval { $Self->PostRun(); };
468    if ($@) {
469        $Self->PrintError($@);
470        $ExitCode ||= $Self->ExitCodeError();    # switch from 0 (ok) to error
471    }
472
473    if ( !defined $ExitCode ) {
474        $Self->PrintError("Command $Self->{Name} did not return a proper exit code.");
475        $ExitCode = $Self->ExitCodeError();
476    }
477
478    return $ExitCode;
479}
480
481=head2 ExitCodeError()
482
483returns an exit code to signal something went wrong (mostly for better
484code readability).
485
486    return $CommandObject->ExitCodeError();
487
488You can also provide a custom error code for special use cases:
489
490    return $CommandObject->ExitCodeError(255);
491
492=cut
493
494sub ExitCodeError {
495    my ( $Self, $CustomExitCode ) = @_;
496
497    return $CustomExitCode // 1;
498}
499
500=head2 ExitCodeOk()
501
502returns 0 as exit code to indicate everything went fine in the command
503(mostly for better code readability).
504
505=cut
506
507sub ExitCodeOk {
508    return 0;
509}
510
511=head2 GetUsageHelp()
512
513generates usage / help screen for this command.
514
515    my $UsageHelp = $CommandObject->GetUsageHelp();
516
517=cut
518
519sub GetUsageHelp {
520    my $Self = shift;
521
522    my $UsageText = "<green>$Self->{Description}</green>\n";
523    $UsageText .= "\n<yellow>Usage:</yellow>\n";
524    $UsageText .= " otrs.Console.pl $Self->{Name}";
525
526    my $OptionsText   = "<yellow>Options:</yellow>\n";
527    my $ArgumentsText = "<yellow>Arguments:</yellow>\n";
528
529    OPTION:
530    for my $Option ( @{ $Self->{_Options} // [] } ) {
531        my $OptionShort = "--$Option->{Name}";
532        if ( $Option->{HasValue} ) {
533            $OptionShort .= " ...";
534            if ( $Option->{Multiple} ) {
535                $OptionShort .= " ($OptionShort)";
536            }
537        }
538        if ( !$Option->{Required} ) {
539            $OptionShort = "[$OptionShort]";
540        }
541        $UsageText   .= " $OptionShort";
542        $OptionsText .= sprintf " <green>%-30s</green> - %s", $OptionShort, $Option->{Description} . "\n";
543    }
544
545    # Global options only show up at the end of the options section, but not in the command line string as
546    #   they don't actually belong to the current command (only).
547    GLOBALOPTION:
548    for my $Option ( @{ $Self->{_GlobalOptions} // [] } ) {
549        next GLOBALOPTION if $Option->{Invisible};
550        my $OptionShort = "[--$Option->{Name}]";
551        $OptionsText .= sprintf " <green>%-30s</green> - %s", $OptionShort, $Option->{Description} . "\n";
552    }
553
554    ARGUMENT:
555    for my $Argument ( @{ $Self->{_Arguments} // [] } ) {
556        my $ArgumentShort = $Argument->{Name};
557        if ( !$Argument->{Required} ) {
558            $ArgumentShort = "[$ArgumentShort]";
559        }
560        $UsageText     .= " $ArgumentShort";
561        $ArgumentsText .= sprintf " <green>%-30s</green> - %s", $ArgumentShort,
562            $Argument->{Description} . "\n";
563    }
564
565    $UsageText .= "\n";
566
567    $UsageText .= "\n$OptionsText";    # Always present because of global options
568
569    if ( @{ $Self->{_Arguments} // [] } ) {
570        $UsageText .= "\n$ArgumentsText";
571    }
572
573    if ( $Self->AdditionalHelp() ) {
574        $UsageText .= "\n<yellow>Help:</yellow>\n";
575        $UsageText .= $Self->AdditionalHelp();
576    }
577
578    $UsageText .= "\n";
579
580    return $Self->_ReplaceColorTags($UsageText);
581}
582
583=head2 ANSI()
584
585get/set support for colored text.
586
587=cut
588
589sub ANSI {
590    my ( $Self, $ANSI ) = @_;
591
592    $Self->{ANSI} = $ANSI if defined $ANSI;
593    return $Self->{ANSI};
594}
595
596=head2 PrintError()
597
598shorthand method to print an error message to STDERR.
599
600It will be prefixed with 'Error: ' and colored in red,
601if the terminal supports it (see L</ANSI()>).
602
603=cut
604
605sub PrintError {
606    my ( $Self, $Text ) = @_;
607
608    chomp $Text;
609    print STDERR $Self->_Color( 'red', "Error: $Text\n" );
610    return;
611}
612
613=head2 Print()
614
615this method will print the given text to STDOUT.
616
617You can add color tags (C<< <green>...</green>, <yellow>...</yellow>, <red>...</red> >>)
618to your text, and they will be replaced with the corresponding terminal escape sequences
619if the terminal supports it (see L</ANSI()>).
620
621=cut
622
623sub Print {
624    my ( $Self, $Text ) = @_;
625
626    if ( !$Self->{Quiet} ) {
627        print $Self->_ReplaceColorTags($Text);
628    }
629    return;
630}
631
632=head2 TableOutput()
633
634this method generates an ascii table of headers and column content
635
636    my $FormattedOutput = $Command->TableOutput(
637        TableData => {
638            Header => [
639                'First Header',
640                'Second Header',
641                'Third Header'
642            ],
643            Body => [
644                [ 'FirstItem 1', 'SecondItem 1', 'ThirdItem 1' ],
645                [ 'FirstItem 2', 'SecondItem 2', 'ThirdItem 2' ],
646                [ 'FirstItem 3', 'SecondItem 3', 'ThirdItem 3' ],
647                [ 'FirstItem 4', 'SecondItem 4', 'ThirdItem 4' ],
648            ],
649        },
650        Indention => 2, # Spaces to indent (ltr), default 0;
651        EvenOdd   => 'yellow', # add even and odd line coloring (green|yellow|red)
652                               # (overwrites existing coloring), # default 0
653    );
654
655    Returns:
656
657    +--------------+---------------+--------------+
658    | First Header | Second Header | Third Header |
659    +--------------+---------------+--------------+
660    | FirstItem 1  | SecondItem 1  | ThirdItem 1  |
661    | FirstItem 2  | SecondItem 2  | ThirdItem 1  |
662    | FirstItem 3  | SecondItem 3  | ThirdItem 1  |
663    | FirstItem 4  | SecondItem 4  | ThirdItem 1  |
664    +--------------+---------------+--------------+
665
666=cut
667
668sub TableOutput {
669    my ( $Self, %Param ) = @_;
670
671    return if $Param{TableData}->{Header} && !IsArrayRefWithData( $Param{TableData}->{Header} );
672    return if $Param{TableData}->{Body}   && !IsArrayRefWithData( $Param{TableData}->{Body} );
673
674    my @MaxColumnLength;
675
676    # check for available header row and determine lengths
677    my $ShowHeader = IsArrayRefWithData( $Param{TableData}->{Header} ) ? 1 : 0;
678
679    if ($ShowHeader) {
680
681        my $HeaderCount = 0;
682
683        for my $Header ( @{ $Param{TableData}->{Header} } ) {
684
685            # detect coloring
686            my $PreparedHeader = $Header;
687
688            if ( $PreparedHeader =~ m/<.+?>.+?<\/.+?>/smx ) {
689                $PreparedHeader =~ s{ (<.+?>)(.+?)(<\/.+?>) }{$2}xmsg;
690            }
691
692            # detect header value length
693            if ( !$MaxColumnLength[$HeaderCount] || $MaxColumnLength[$HeaderCount] < length $PreparedHeader ) {
694                $MaxColumnLength[$HeaderCount] = length $PreparedHeader;
695            }
696            $HeaderCount++;
697        }
698    }
699
700    Row:
701    for my $Row ( @{ $Param{TableData}->{Body} } ) {
702
703        next ROW if !$Row;
704        next ROW if !IsArrayRefWithData($Row);
705
706        # determine maximum length of every column
707        my $ColumnCount = 0;
708
709        for my $Column ( @{$Row} ) {
710
711            # detect coloring
712            my $PreparedColumn = $Column || ' ';
713
714            if ( $PreparedColumn =~ m/<.+?>.+?<\/.+?>/smx ) {
715                $PreparedColumn =~ s{ (<.+?>)(.+?)(<\/.+?>) }{$2}xmsg;
716            }
717
718            # detect column value length
719            if ( !$MaxColumnLength[$ColumnCount] || $MaxColumnLength[$ColumnCount] < length $PreparedColumn ) {
720                $MaxColumnLength[$ColumnCount] = length $PreparedColumn;
721            }
722            $ColumnCount++;
723        }
724    }
725
726    # generate horizontal border
727    my $HorizontalBorder = '';
728
729    my $ColumnCount = 0;
730
731    for my $ColumnLength (@MaxColumnLength) {
732
733        # add space character before and after column content
734        $ColumnLength += 2;
735
736        # save new column length in maximum column length array
737        $MaxColumnLength[$ColumnCount] = $ColumnLength;
738
739        # save border part
740        $HorizontalBorder .= '+' . ( '-' x $ColumnLength );
741
742        $ColumnCount++;
743    }
744
745    $HorizontalBorder .= '+';
746
747    if ( $Param{Indention} ) {
748        my $Indention = ' ' x $Param{Indention};
749        $HorizontalBorder = $Indention . $HorizontalBorder;
750    }
751
752    # add first border to output
753    my $Output = $HorizontalBorder . "\n";
754
755    # add header row if available
756    if ($ShowHeader) {
757
758        my $HeaderContent = '';
759        my $HeaderCount   = 0;
760
761        if ( $Param{Indention} ) {
762            my $Indention = ' ' x $Param{Indention};
763            $HeaderContent = $Indention . $HeaderContent;
764        }
765
766        for my $Header ( @{ $Param{TableData}->{Header} } ) {
767
768            # prepare header content
769            $HeaderContent .= '| ' . $Header;
770
771            # detect coloring
772            if ( $Header =~ m/<.+?>.+?<\/.+?>/smx ) {
773                $Header =~ s{ (<.+?>)(.+?)(<\/.+?>) }{$2}xmsg;
774            }
775
776            # determine difference between current header content and maximum content length
777            my $HeaderContentDiff = ( $MaxColumnLength[$HeaderCount] ) - ( length $Header );
778
779            # fill up with spaces
780            if ($HeaderContentDiff) {
781                $HeaderContent .= ' ' x ( $HeaderContentDiff - 1 );
782            }
783
784            $HeaderCount++;
785        }
786
787        # save the result as output
788        $Output .= $HeaderContent . "|\n";
789
790        # add horizontal border
791        $Output .= $HorizontalBorder . "\n";
792    }
793
794    my $EvenOddIndicator = 0;
795
796    # add body rows
797    Row:
798    for my $Row ( @{ $Param{TableData}->{Body} } ) {
799
800        next ROW if !$Row;
801        next ROW if !IsArrayRefWithData($Row);
802
803        my $RowContent  = '';
804        my $ColumnCount = 0;
805
806        if ( $Param{Indention} ) {
807            my $Indention = ' ' x $Param{Indention};
808            $RowContent = $Indention . $RowContent;
809        }
810
811        for my $Column ( @{$Row} ) {
812
813            $Column = IsStringWithData($Column) ? $Column : ' ';
814
815            # even and odd coloring
816            if ( $Param{EvenOdd} ) {
817
818                if ( $Column =~ m/<.+?>.+?<\/.+?>/smx ) {
819                    $Column =~ s{ (<.+?>)(.+?)(<\/.+?>) }{$2}xmsg;
820                }
821
822                if ($EvenOddIndicator) {
823                    $Column = "<$Param{EvenOdd}>" . $Column . "</$Param{EvenOdd}>";
824                }
825            }
826
827            # prepare header content
828            $RowContent .= '| ' . $Column;
829
830            # detect coloring
831            if ( $Column =~ m/<.+?>.+?<\/.+?>/smx ) {
832                $Column =~ s{ (<.+?>)(.+?)(<\/.+?>) }{$2}xmsg;
833            }
834
835            # determine difference between current column content and maximum content length
836            my $RowContentDiff = ( $MaxColumnLength[$ColumnCount] ) - ( length $Column );
837
838            # fill up with spaces
839            if ($RowContentDiff) {
840                $RowContent .= ' ' x ( $RowContentDiff - 1 );
841            }
842
843            $ColumnCount++;
844        }
845
846        # toggle even odd indicator
847        $EvenOddIndicator = $EvenOddIndicator ? 0 : 1;
848
849        # save the result as output
850        $Output .= $RowContent . "|\n";
851    }
852
853    # add trailing horizontal border
854    $Output .= $HorizontalBorder . "\n";
855
856    return $Output // '';
857}
858
859=begin Internal:
860
861=head2 _ParseGlobalOptions()
862
863parses any global options possibly provided by the user.
864
865Returns a hash with the option values.
866
867=cut
868
869sub _ParseGlobalOptions {
870    my ( $Self, $Arguments ) = @_;
871
872    Getopt::Long::Configure('pass_through');
873    Getopt::Long::Configure('no_auto_abbrev');
874
875    my %OptionValues;
876
877    OPTION:
878    for my $Option ( @{ $Self->{_GlobalOptions} } ) {
879        my $Value;
880        my $Lookup = $Option->{Name};
881
882        Getopt::Long::GetOptionsFromArray(
883            $Arguments,
884            $Lookup => \$Value,
885        );
886
887        $OptionValues{ $Option->{Name} } = $Value;
888    }
889
890    return \%OptionValues;
891}
892
893=head2 _ParseCommandlineArguments()
894
895parses and validates the command line arguments provided by the user according to
896the configured arguments and options of the command.
897
898Returns a hash with argument and option values if all needed values were supplied
899and correct, or undef otherwise.
900
901=cut
902
903sub _ParseCommandlineArguments {
904    my ( $Self, $Arguments ) = @_;
905
906    Getopt::Long::Configure('pass_through');
907    Getopt::Long::Configure('no_auto_abbrev');
908
909    my %OptionValues;
910
911    OPTION:
912    for my $Option ( @{ $Self->{_Options} // [] }, @{ $Self->{_GlobalOptions} } ) {
913        my $Lookup = $Option->{Name};
914        if ( $Option->{HasValue} ) {
915            $Lookup .= '=s';
916            if ( $Option->{Multiple} ) {
917                $Lookup .= '@';
918            }
919        }
920
921        # Option with multiple values
922        if ( $Option->{HasValue} && $Option->{Multiple} ) {
923
924            my @Values;
925
926            Getopt::Long::GetOptionsFromArray(
927                $Arguments,
928                $Lookup => \@Values,
929            );
930
931            if ( !@Values ) {
932                if ( !$Option->{Required} ) {
933                    next OPTION;
934                }
935
936                $Self->PrintError("please provide option '--$Option->{Name}'.");
937                return;
938            }
939
940            for my $Value (@Values) {
941                if ( $Option->{HasValue} && $Value !~ $Option->{ValueRegex} ) {
942                    $Self->PrintError("please provide a valid value for option '--$Option->{Name}'.");
943                    return;
944                }
945            }
946
947            $OptionValues{ $Option->{Name} } = \@Values;
948        }
949
950        # Option with no or a single value
951        else {
952
953            my $Value;
954
955            Getopt::Long::GetOptionsFromArray(
956                $Arguments,
957                $Lookup => \$Value,
958            );
959
960            if ( !defined $Value ) {
961                if ( !$Option->{Required} ) {
962                    next OPTION;
963                }
964
965                $Self->PrintError("please provide option '--$Option->{Name}'.");
966                return;
967            }
968
969            if ( $Option->{HasValue} && $Value !~ $Option->{ValueRegex} ) {
970                $Self->PrintError("please provide a valid value for option '--$Option->{Name}'.");
971                return;
972            }
973
974            $OptionValues{ $Option->{Name} } = $Value;
975        }
976    }
977
978    my %ArgumentValues;
979
980    ARGUMENT:
981    for my $Argument ( @{ $Self->{_Arguments} // [] } ) {
982        if ( !@{$Arguments} ) {
983            if ( !$Argument->{Required} ) {
984                next ARGUMENT;
985            }
986
987            $Self->PrintError("please provide a value for argument '$Argument->{Name}'.");
988            return;
989        }
990
991        my $Value = shift @{$Arguments};
992
993        if ( $Value !~ $Argument->{ValueRegex} ) {
994            $Self->PrintError("please provide a valid value for argument '$Argument->{Name}'.");
995            return;
996        }
997
998        $ArgumentValues{ $Argument->{Name} } = $Value;
999    }
1000
1001    # check for superfluous arguments
1002    if ( @{$Arguments} ) {
1003        my $Error = "found unknown arguments on the command line ('";
1004        $Error .= join "', '", @{$Arguments};
1005        $Error .= "').\n";
1006        $Self->PrintError($Error);
1007        return;
1008    }
1009
1010    return {
1011        Options   => \%OptionValues,
1012        Arguments => \%ArgumentValues,
1013    };
1014}
1015
1016=head2 _Color()
1017
1018this will color the given text (see Term::ANSIColor::color()) if
1019ANSI output is available and active, otherwise the text stays unchanged.
1020
1021    my $PossiblyColoredText = $CommandObject->_Color('green', $Text);
1022
1023=cut
1024
1025sub _Color {
1026    my ( $Self, $Color, $Text ) = @_;
1027
1028    return $Text if !$Self->{ANSI};
1029    return $Text if $SuppressANSI;
1030    return Term::ANSIColor::color($Color) . $Text . Term::ANSIColor::color('reset');
1031}
1032
1033sub _ReplaceColorTags {
1034    my ( $Self, $Text ) = @_;
1035    $Text =~ s{<(green|yellow|red)>(.*?)</\1>}{$Self->_Color($1, $2)}gsmxe;
1036    return $Text;
1037}
1038
10391;
1040
1041=end Internal:
1042
1043=head1 TERMS AND CONDITIONS
1044
1045This software is part of the OTRS project (L<https://otrs.org/>).
1046
1047This software comes with ABSOLUTELY NO WARRANTY. For details, see
1048the enclosed file COPYING for license information (GPL). If you
1049did not receive this file, see L<https://www.gnu.org/licenses/gpl-3.0.txt>.
1050
1051=cut
1052