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