Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Constructing Command Line Applications in Perl

Constructing Command Line Applications in Perl

From one off scripts to fully-featured command line application suites, see how best to build your CLI in Perl.

Jason A. Crome

March 27, 2019
Tweet

More Decks by Jason A. Crome

Other Decks in Programming

Transcript

  1. The Command Line? Really? • Yes, really! • Easy and

    fast to manage • Easy to automate • Not always one-off scripts, often apps in their own right • We’ll learn everything from quick-and-dirty to full-blown CLI application Copyright 2019, Jason A. Crome
  2. Roll Your Own! • TIMTOWTDI! • Just like building your

    own template engine… • What could go wrong??? Copyright 2019, Jason A. Crome
  3. !#!/usr/bin/env perl use v5.26; my $verbose = 0; my $force

    = 0; foreach my $opt( @ARGV ) { if( $opt eq "-h" ) { say help(); exit 0; } elsif( $opt eq "-f" ) { $force = 1; } elsif( $opt eq "-v" ) { $verbose = 1; } else { say "Invalid option: $opt"; exit 1; } } exit blort(); sub blort { say "blorting with the following options:"; say "- Force: $force"; say "- Verbose: $verbose"; return 0; } sub help { return qq{ USAGE: blort.pl [-h] [-v] [-f] WHERE: -h: this help -f: force -v: verbose }; } blort, version 1 Copyright 2019, Jason A. Crome
  4. Let’s Upset the Apple Cart! • Let’s expand this to

    accept short and long arguments • Once we get that right, let’s accept arguments that accept arguments (i.e., blort.pl -i 5 -v -f ) Copyright 2019, Jason A. Crome
  5. !#!/usr/bin/env perl use v5.26; my $verbose = 0; my $force

    = 0; foreach my $opt( @ARGV ) { if( $opt eq "-h" or $opt eq "!--help" ) { say help(); exit 0; } elsif( $opt =~ /^-f|!--force$/ ) { $force = 1; } elsif( $opt =~ /^-v|!--verbose$/ ) { $verbose = 1; } else { say "Invalid option: $opt"; exit 1; } } exit blort(); sub blort { say "blorting with the following options:"; say "- Force: $force"; say "- Verbose: $verbose"; return 0; } sub help { return qq{ USAGE: blort.pl [-h | !--help] [-v | !--verbose] [-f | !--force] WHERE: -h / !--help: this help -f / !--force: force -v / !--verbose: verbose }; } blort, version 2 Copyright 2019, Jason A. Crome
  6. !#!/usr/bin/env perl use v5.26; my $verbose = 0; my $force

    = 0; my $iters = 0; foreach my $opt( @ARGV ) { if( $opt =~ /^-h|!--help$/ ) { say help(); exit 0; } elsif( $opt =~ /^-f|!--force$/ ) { $force = 1; } elsif( $opt =~ /^-v|!--verbose$/ ) { $verbose = 1; } elsif( $opt =~ /^-i|!--iters$/ ) { $iters = pop @ARGV; say "iters is not a decimal!" unless $iters =~ /^\d+$/; } else { say "Invalid option: $opt"; exit 1; } } exit blort(); sub blort { say "blorting with the following options:"; say "- Force: $force"; say "- Verbose: $verbose"; say "- Iters: $iters"; return 0; } sub help { return qq{ USAGE: blort.pl [-h | !--help] [-v | !--verbose] [-f | !--force] [-i | !--iters #] WHERE: -h / !--help: this help -f / !--force: force -v / !--verbose: verbose -i # / !--iters #: number of iterations }; } blort, version 3 Copyright 2019, Jason A. Crome
  7. Problems! • It doesn’t read well • It doesn’t maintain

    well • It’s not easy to expand • Frankly, it just sucks! Copyright 2019, Jason A. Crome
  8. Getopt::Long • The old tried-and-true • It’s practically part of

    Perl core • Fast and flexible • Parses arguments only • Most other CLI argument parsers build on this Copyright 2019, Jason A. Crome
  9. !#!/usr/bin/env perl use v5.26; use Getopt!::Long; use Pod!::Usage; my $verbose

    = 0; my $force = 0; my $iters = 0; GetOptions( 'v|verbose' !=> \$verbose, 'f|force' !=> \$force, 'i|iters=i' !=> \$iters, 'h|help' !=> sub{ pod2usage(1); }, ) or pod2usage(2); exit blort(); sub blort { say "blorting with the following options:"; say "- Force: $force"; say "- Verbose: $verbose"; say "- Iters: $iters"; return 0; } blort, version 4 Copyright 2019, Jason A. Crome
  10. =pod =head1 NAME blort-4.pl - All your blort are belong

    to us! =head1 SYNOPSIS blort.pl [-h | !--help] [-v | !--verbose] [-f | !--force] [-i | !--iters #] =head1 DESCRIPTION F<blort-4.pl> is for all your blorting needs. =head1 OPTIONS =over 4 =item * -h / !--help Get some help. =item * -f / !--force Forcibly blort. =item * -v / !--verbose Verbosely blort. =item * -i / !--iters How many times should we blort? =back =cut blort, version 4 Copyright 2019, Jason A. Crome
  11. More fun with Getopt::Long • Other argument types args •

    Inline variable declaration • My hack • Multiple occurrences of same argument • Comma separated options Copyright 2019, Jason A. Crome
  12. !#!/usr/bin/env perl use v5.26; use Getopt!::Long; use Pod!::Usage; GetOptions( 'v|verbose'

    !=> \my $verbose, # declare var inline 'f|force' !=> \my $force, 'i|iters=i' !=> \my $iters, 'd|devs=s' !=> \my @devs, # no default value! 'h|help' !=> sub{ pod2usage(1); }, ) or pod2usage(2); exit blort(); sub blort { @devs = split( /,/, join( ',', @devs )); say "blorting with the following options:"; say "- Force: $force"; say "- Verbose: $verbose"; say "- Iters: $iters"; say '- Devs: ' . join( ', ', @devs ); return 0; } blort, version 5 Copyright 2019, Jason A. Crome
  13. Getopt::Long Criticisms • A bit tricky to get right for

    newcomers • Don’t forget to check return value of GetOptions(). Copyright 2019, Jason A. Crome
  14. MooseX::App • Build CLI like you would an app •

    Makes it easy to create well structured, documented code • If you know Moose, you know MooseX::App • Anything you do with Moose, you can do in MooseX::App • If your using code that uses Moose, using this costs you nothing extra Copyright 2019, Jason A. Crome
  15. MooseX::App • Build a base class with common functionality •

    Create roles that contain discrete units of functionality • Create command classes that use roles, add new functionality Copyright 2019, Jason A. Crome
  16. package Blort; use v5.26; use MooseX!::App qw( Color Term );

    with qw( Blort!::Role!::WithDebug Blort!::Role!::WithVerbose Blort!::Role!::WithTerm ); use feature 'signatures'; no warnings 'experimental!::signatures'; option target !=> ( is !=> 'ro', isa !=> 'Str', lazy !=> 1, default !=> 'test', documentation !=> 'Target environment', ); # NOTE TO SELF: cmd_env specifies an env var that can be set instead of a parameter # Display a message in verbose mode sub _say( $self, $message ) { return unless $self!->verbose; say $message; } # Display a message in debug mode sub _say_debug ( $self, $message ) { return unless $self!->debug; say $message; } # Translate package name from camel case into hyphenated name app_command_name { my @parts = split( /[_\s]+|\b|(?<![A-Z])(!?=[A-Z])|(?!<=[A-Z])(!?=[A-Z][a-z])/, shift ); return lc(join('-',@parts)); }; app_namespace 'Blort!::Commands'; sub BUILD { my $self = shift; $self!->_say_debug( "Using Target: " . $self!->target ); } 1; blort, version 6 (Base Class) Copyright 2019, Jason A. Crome
  17. !#!/usr/bin/env perl use lib './lib'; use v5.26; use Blort; Blort!->new_with_command!->run;

    blort, version 6 (Instance Script) Copyright 2019, Jason A. Crome
  18. package Blort!::Role!::WithPassword; use v5.26; use MooseX!::App!::Role; use feature 'signatures'; no

    warnings 'experimental!::signatures'; option 'password' !=> ( is !=> 'rw', isa !=> 'Str', required !=> 1, documentation !=> 'Password for the new user', cmd_aliases !=> [qw(p)], ); 1; blort, version 6 (WithPassword) Copyright 2019, Jason A. Crome
  19. package Blort!::Commands!::AddUser; use v5.26; use MooseX!::App!::Command; extends qw( Blort );

    with qw( Blort!::Role!::WithUsername Blort!::Role!::WithPassword Blort!::Role!::WithEmail ); use feature 'signatures'; no warnings 'experimental!::signatures'; option 'godmode' !=> ( is !=> 'rw', isa !=> 'Bool', required !=> 0, documentation !=> 'Enable Doom-like God-mode for this user?', cmd_aliases !=> [qw(g)], default !=> 0, ); sub run ( $self ) { my $user = $self!->username; $self!->_say_debug( "Running AddUser!!..." ); $self!->_say( "Creating $user!!...done!" ); } 1; blort, version 6 (AddUser Command) Copyright 2019, Jason A. Crome
  20. . ├── bin │ └── blort-6.pl └── lib ├── Blort

    │ ├── Commands │ │ ├── AddUser.pm │ │ └── SetPassword.pm │ └── Role │ ├── WithDebug.pm │ ├── WithEmail.pm │ ├── WithNoPrompt.pm │ ├── WithPassword.pm │ ├── WithSlugArg.pm │ ├── WithTerm.pm │ ├── WithUsername.pm │ └── WithVerbose.pm └── Blort.pm 5 directories, 12 files blort, version 6 (Project Tree) Copyright 2019, Jason A. Crome
  21. MooseX::App Drawbacks • S - L - O - W

    - ! - ! - ! • Maximum overkill for many situations Copyright 2019, Jason A. Crome
  22. App::Cmd • Older OO-framework for CLI apps • Predates Moose

    • Many of the same benefits, but lightweight • Nice compromise between speed and functionality • Stable and proven Copyright 2019, Jason A. Crome
  23. package YourApp::Command::blort; use YourApp -command; use strict; use warnings; sub

    abstract { "blortex algorithm" } sub description { "Long description on blortex algorithm" } sub opt_spec { return ( [ "blortex|X", "use the blortex algorithm" ], [ "recheck|r", "recheck all results" ], ); } sub validate_args { my ($self, $opt, $args) = @_; # no args allowed but options! $self->usage_error("No args allowed") if @$args; } sub execute { my ($self, $opt, $args) = @_; my $result = $opt->{blortex} ? blortex() : blort(); recheck($result) if $opt->{recheck}; print $result; } App::Cmd Command Class Copyright 2019, Jason A. Crome
  24. App::Cmd Drawbacks • Classic Perl OO • Not seeing much

    (any?) development Copyright 2019, Jason A. Crome
  25. Other Options • Getopt::Std • Getopt::Long::Descriptive • App::Cmd::Simple • MooseX::App::Cmd

    • (MooseX::)Getopt::Kingpin • MooseX::Getopt::Usage • MooseX::Getopt::Defanged • MooseX::Getopt::Strict • Getopt::EX • Getopt::Alt Copyright 2019, Jason A. Crome