Parsing af kommandolinjeparametre med JCommander

1. Oversigt

I denne vejledning vi lærer at bruge JCommander til at analysere kommandolinjeparametre. Vi undersøger flere af dens funktioner, når vi bygger en simpel kommandolinjeapplikation.

2. Hvorfor JCommander?

"Fordi livet er for kort til at parse kommandolinjeparametre" - Cédric Beust

JCommander, oprettet af Cédric Beust, er en annoteringsbaseret bibliotek tilparsing af kommandolinjeparametre. Det kan reducere indsatsen for at opbygge kommandolinjeapplikationer og hjælpe os med at give dem en god brugeroplevelse.

Med JCommander kan vi aflaste vanskelige opgaver som parsing, validering og typekonvertering, så vi kan fokusere på vores applikationslogik.

3. Opsætning af JCommander

3.1. Maven-konfiguration

Lad os begynde med at tilføje jcommander afhængighed i vores pom.xml:

 com.beust jcommander 1.78 

3.2. Hej Verden

Lad os oprette en simpel HelloWorldApp der tager et enkelt input kaldet navn og udskriver en hilsen, "Hej ".

Siden JCommander binder kommandolinjeargumenter til felter i en Java-klasse, definerer vi først en HelloWorldArgs klasse med et felt navn kommenteret med @Parameter:

klasse HelloWorldArgs {@Parameter (names = "--name", beskrivelse = "Brugernavn", krævet = sand) privat Stringnavn; }

Lad os nu bruge JCommander klasse for at analysere kommandolinjeargumenterne og tildele felterne i vores HelloWorldArgs objekt:

HelloWorldArgs jArgs = ny HelloWorldArgs (); JCommander helloCmd = JCommander.newBuilder () .addObject (jArgs) .build (); hejCmd.parse (args); System.out.println ("Hej" + jArgs.getName ());

Lad os endelig påkalde hovedklassen med de samme argumenter fra konsollen:

$ java HelloWorldApp - navn JavaWorld Hello JavaWorld

4. Opbygning af en rigtig applikation i JCommander

Nu hvor vi er i gang, lad os overveje en mere kompleks brugssag - en kommandolinie-API-klient, der interagerer med en faktureringsapplikation såsom Stripe, især det målte (eller brugsbaserede) faktureringsscenarie. Denne tredjeparts faktureringstjeneste administrerer vores abonnementer og fakturering.

Lad os forestille os, at vi driver en SaaS-forretning, hvor vores kunder køber abonnementer på vores tjenester og faktureres for antallet af API-opkald til vores tjenester pr. Måned. Vi udfører to operationer i vores klient:

  • Indsend: Indsend mængde og enhedspris for en kunde mod et givet abonnement
  • hente: Hent gebyrer for en kunde baseret på forbruget af nogle eller alle deres abonnementer i den aktuelle måned - vi kan få disse gebyrer samlet over alle abonnementerne eller specificeret efter hvert abonnement

Vi bygger API-klienten, når vi gennemgår bibliotekets funktioner.

Lad os begynde!

5. Definition af en parameter

Lad os begynde med at definere de parametre, som vores applikation kan bruge.

5.1. Det @Parameter Kommentar

Annotering af et felt med @Parameter fortæller JCommander at binde et matchende kommandolinjeargument til det. @Parameter har attributter til at beskrive hovedparameteren, såsom:

  • navne - et eller flere navne på indstillingen, for eksempel “–navn” eller “-n”
  • beskrivelse - betydningen bag muligheden for at hjælpe slutbrugeren
  • krævet - om indstillingen er obligatorisk, er som standard falsk
  • arity - antal yderligere parametre, som indstillingen bruger

Lad os konfigurere en parameter Kunde ID i vores målte faktureringsscenarie:

@Parameter (names = {"--customer", "-C"}, beskrivelse = "Id for kunden, der bruger tjenesterne", arity = 1, krævet = true) String customerId; 

Lad os nu udføre vores kommando med den nye parameter “–kunde”:

$ java-app - kunde cust0000001A Læs CustomerId: cust0000001A. 

På samme måde kan vi bruge den kortere “-C” -parameter til at opnå den samme effekt:

$ java App -C cust0000001A Læs CustomerId: cust0000001A. 

5.2. Nødvendige parametre

Hvor en parameter er obligatorisk, afsluttes applikationen at kaste a ParameterUndtagelse hvis brugeren ikke angiver det:

$ java App-undtagelse i tråden "main" com.beust.jcommander.ParameterException: Følgende mulighed er påkrævet: [--customer | -C]

Vi skal bemærke, at enhver fejl i parsing af parametrene resulterer i a ParameterUndtagelse i JCommander.

6. Indbyggede typer

6.1. IStringConverter Interface

JCommander udfører typekonvertering fra kommandolinjen Snor input til Java-typerne i vores parameterklasser. Det IStringConverter interface håndterer typekonvertering af en parameter fra Snor til enhver vilkårlig type. Så alle JCommanders indbyggede konvertere implementerer denne grænseflade.

Uden for boksen kommer JCommander med support til almindelige datatyper som f.eks Snor, Heltal, Boolsk, BigDecimalog Enum.

6.2. Single Arity Typer

Arity relaterer til antallet af yderligere parametre, som en indstilling bruger. JCommander's indbyggede parametertyper har en standardaritet på en, med undtagelse af Boolsk og Liste. Derfor er almindelige typer som Snor, Heltal, BigDecimal, Lang, og Enum, er single-arity typer.

6.3. Boolsk Type

Felter af typen boolsk eller Boolsk behøver ingen yderligere parameter - disse muligheder har en arity af nul.

Lad os se på et eksempel. Måske vil vi hente gebyrer for en kunde, specificeret efter abonnement. Vi kan tilføje en boolsk Mark specificeret, som er falsk som standard:

@Parameter (names = {"--itemized"}) privat boolsk specificeret; 

Vores ansøgning returnerer samlede gebyrer med specificeret indstillet til falsk. Når vi påberåber sig kommandolinjen med specificeret parameter, indstiller vi feltet til rigtigt:

$ java App - genoplæst Læse flag specificeret: sandt. 

Dette fungerer godt, medmindre vi har en brugssag, hvor vi altid vil have specificerede afgifter, medmindre andet er angivet. Vi kunne ændre parameteren til at være ikke optaget, men det kan være klarere at kunne levere falsk som værdien af specificeret.

Lad os introducere denne adfærd ved hjælp af en standardværdi rigtigt for marken og indstilling af dens arity som en:

@Parameter (names = {"--itemized"}, arity = 1) privat boolsk itemized = true; 

Når vi nu angiver indstillingen, indstilles værdien til falsk:

$ java-app - godkendt falsk Læs flag specificeret: falsk. 

7. Liste Typer

JCommander giver et par måder at binde argumenter på Liste felter.

7.1. Angivelse af parameteren flere gange

Lad os antage, at vi kun ønsker at hente gebyrerne for et undersæt af en kundes abonnementer:

@Parameter (names = {"--subscription", "-S"}) private List-abonnementIds; 

Feltet er ikke obligatorisk, og applikationen vil hente gebyrer på tværs af alle abonnementer, hvis parameteren ikke leveres. Vi kan dog specificere flere abonnementer ved at bruge parameternavnet flere gange:

$ java App -S abonnementA001 -S abonnementA002 -S abonnementA003 Læs abonnementer: [abonnementA001, abonnementA002, abonnementA003]. 

7.2. Bindende Lister Brug af Splitter

I stedet for at angive indstillingen flere gange, lad os prøve at binde listen ved at sende en komma-adskilt Snor:

$ java App -S abonnementA001, abonnementA002, abonnementA003 Læs abonnementer: [abonnementA001, abonnementA002, abonnementA003]. 

Dette bruger en enkelt parameterværdi (arity = 1) til at repræsentere en liste. JCommander vil bruge klassen CommaParameterSplitter for at binde det kommaseparerede Snor til vores Liste.

7.3. Bindende Lister Brug af en brugerdefineret splitter

Vi kan tilsidesætte standardopdeleren ved at implementere IParameterSplitter grænseflade:

klasse ColonParameterSplitter implementerer IParameterSplitter {@Override public List split (String value) {return asList (value.split (":")); }}

Og derefter kortlægge implementeringen til splitter attribut i @Parameter:

@Parameter (names = {"--subscription", "-S"}, splitter = ColonParameterSplitter.class) private List-abonnementIds; 

Lad os prøve det:

$ java App -S "abonnementA001: abonnementA002: abonnementA003" Læs abonnementer: [abonnementA001, abonnementA002, abonnementA003]. 

7.4. Variabel Arity Lister

Variabel aritet giver os mulighed for at erklærelister, der kan tage ubestemte parametre, op til den næste mulighed. Vi kan indstille attributten variableArity som rigtigt for at specificere denne adfærd.

Lad os prøve dette for at analysere abonnementer:

@Parameter (names = {"--subscription", "-S"}, variableArity = true) private List subscriptionIds; 

Og når vi kører vores kommando:

$ java App -S abonnementA001 abonnementA002 abonnementA003 - itemiserede læse abonnementer: [abonnementA001, abonnementA002, abonnementA003]. 

JCommander binder alle inputargumenter efter indstillingen “-S” til listefeltet, indtil den næste indstilling eller slutningen af ​​kommandoen.

7.5. Fast arity Lister

Indtil videre har vi set ubegrænsede lister, hvor vi kan videregive så mange listeelementer, som vi ønsker. Nogle gange vil vi måske begrænse antallet af varer, der sendes til en Liste Mark. For at gøre dette kan vi angiv en helhedsaritetsværdi for en Liste Markfor at gøre det afgrænset:

@Parameter (names = {"--subscription", "-S"}, arity = 2) private abonnements-lister; 

Fast arity tvinger en kontrol af antallet af parametre, der sendes til a Liste mulighed og kaster en ParameterUndtagelse i tilfælde af overtrædelse:

$ java App -S abonnementA001 abonnementA002 abonnementA003 Blev passeret hovedparameter 'abonnementA003', men der blev ikke defineret nogen hovedparameter i din arg-klasse 

Fejlmeddelelsen antyder, at da JCommander kun forventede to argumenter, forsøgte den at analysere den ekstra inputparameter “abonnementA003” som den næste mulighed.

8. Brugerdefinerede typer

Vi kan også binde parametre ved at skrive tilpassede konvertere. Ligesom indbyggede konvertere skal tilpassede konvertere implementere IStringConverter interface.

Lad os skrive en konverter til parsing af et ISO8601 tidsstempel:

klasse ISO8601TimestampConverter implementerer IStringConverter {privat statisk endelig DateTimeFormatter TS_FORMATTER = DateTimeFormatter.ofPattern ("uuuu-MM-dd'T'HH: mm: ss"); @ Override offentlig Øjeblikkelig konvertering (strengværdi) {prøv {return LocalDateTime .parse (værdi, TS_FORMATTER) .atOffset (ZoneOffset.UTC) .toInstant (); } catch (DateTimeParseException e) {throw new ParameterException ("Ugyldig tidsstempel"); }}} 

Denne kode analyserer input Snor og returner en Øjeblikkelig, kaster en ParameterUndtagelse hvis der er en konverteringsfejl. Vi kan bruge denne konverter ved at binde den til et typefelt Øjeblikkelig bruger konverter attribut i @Parameter:

@Parameter (names = {"--timestamp"}, converter = ISO8601TimestampConverter.class) privat Øjeblikkelig tidsstempel; 

Lad os se det i aktion:

$ java-app - tidsstempel 2019-10-03T10: 58: 00 Læs tidsstempel: 2019-10-03T10: 58: 00Z.

9. Validerende parametre

JCommander giver et par standardvalideringer:

  • om der kræves parametre
  • hvis antallet af angivne parametre stemmer overens med et felt
  • om hver Snor parameter kan konverteres til det tilsvarende felt type

Ud over, Vi ønsker muligvis at tilføje tilpassede valideringer. Lad os for eksempel antage, at kunde-id'erne skal være UUID'er.

Vi kan skrive en validator til kundefeltet, der implementerer grænsefladen IParameterValidator:

klasse UUIDValidator implementerer IParameterValidator {privat statisk endelig streng UUID_REGEX = "[0-9a-fA-F] {8} (- [0-9a-fA-F] {4}) {3} - [0-9a-fA- F] {12} "; @Override public void validate (String name, String value) throw ParameterException {if (! IsValidUUID (value)) {throw new ParameterException ("String parameter" + value + "is not a valid UUID."); }} privat boolsk isValidUUID (strengværdi) {return Pattern.compile (UUID_REGEX) .matcher (værdi) .matches (); }} 

Så kan vi tilslutte det med validateWith parameterens attribut:

@Parameter (names = {"--customer", "-C"}, validateWith = UUIDValidator.class) privat streng kunde-id; 

Hvis vi påkalder kommandoen med et ikke-UUID-kunde-id, afsluttes applikationen med en valideringsfejlmeddelelse:

$ java App --C customer001 Strengparameter customer001 er ikke et gyldigt UUID. 

10. Underkommandoer

Nu hvor vi har lært om parameterbinding, lad os trække alt sammen for at opbygge vores kommandoer.

I JCommander kan vi understøtte flere kommandoer, kaldet underkommandoer, hver med et særskilt sæt valgmuligheder.

10.1. @Parameters Kommentar

Vi kan bruge @Parameters for at definere underkommandoer. @Parameters indeholder attributten commandNames for at identificere en kommando.

Lad os modellere Indsend og hente som underkommandoer:

@Parameters (commandNames = {"send"}, commandDescription = "Indsend brug for en given kunde og abonnement," + "accepterer et brugselement") klasse SubmitUsageCommand {// ...} @Parameters (commandNames = {"fetch" }, commandDescription = "Hent gebyrer for en kunde i den aktuelle måned," + "kan specificeres eller aggregeres") klasse FetchCurrentChargesCommand {// ...} 

JCommander bruger attributterne i @Parameters for at konfigurere underkommandoerne, såsom:

  • commandNames - navnet på underkommandoen binder kommandolinjeargumenterne til den klasse, der er kommenteret med @Parameters
  • commandDescription - dokumenterer formålet med underkommandoen

10.2. Tilføjelse af underkommandoer til JCommander

Vi tilføjer underkommandoerne til JCommander med addCommand metode:

SubmitUsageCommand submitUsageCmd = ny SubmitUsageCommand (); FetchCurrentChargesCommand fetchChargesCmd = ny FetchCurrentChargesCommand (); JCommander jc = JCommander.newBuilder () .addCommand (submitUsageCmd) .addCommand (fetchChargesCmd) .build (); 

Det addCommand metode registrerer underkommandoerne med deres respektive navne som specificeret i commandNames attribut for @Parameters kommentar.

10.3. Analyse af underkommandoer

For at få adgang til brugerens valg af kommando skal vi først analysere argumenterne:

jc.parse (args); 

Dernæst kan vi udtrække underkommandoen med getParsedCommand:

Streng parsedCmdStr = jc.getParsedCommand (); 

Ud over at identificere kommandoen binder JCommander resten af ​​kommandolinjeparametrene til deres felter i underkommandoen. Nu skal vi bare kalde den kommando, vi vil bruge:

switch (parsedCmdStr) {case "send": submitUsageCmd.submit (); pause; sag "fetch": fetchChargesCmd.fetch (); pause; standard: System.err.println ("Ugyldig kommando:" + parsedCmdStr); } 

11. Hjælp til JCommander Usage

Vi kan påberåbe os brug at gengive en brugsanvisning. Dette er et resumé af alle de muligheder, som vores applikation bruger. I vores ansøgning kan vi påberåbe os brug på hovedkommandoen eller alternativt på hver af de to kommandoer "indsend" og "hent" separat.

En brugsdisplay kan hjælpe os på et par måder: vise hjælpemuligheder og under fejlhåndtering.

11.1. Viser hjælpemuligheder

Vi kan binde en hjælpemulighed i vores kommandoer ved hjælp af a boolsk parameter sammen med attributten Hjælp indstillet til rigtigt:

@Parameter (names = "--help", help = true) privat boolsk hjælp; 

Derefter kan vi registrere, om “–hjælp” er videregivet i argumenterne, og ringe brug:

hvis (cmd.help) {jc.usage (); } 

Lad os se hjælpeproduktionen til vores "indsend" underkommando:

$ java App indsende - hjælp Anvendelse: indsend [valgmuligheder] Indstillinger: * - kunde, -C id for kunden, der bruger tjenesterne * - abonnement, -S id for abonnementet, der blev købt * - mængde brugt mængde ; rapporteret mængde tilføjes i løbet af faktureringsperioden * - pris-type, -P Prissætningstype for den rapporterede brug (værdier: [PRE_RATED, UNRATED]) * - tidsstempel Tidsstempel for brugshændelsen, skal ligge i den aktuelle faktureringsperiode - -pris Hvis PRE_RATED, skal enhedsprisen anvendes pr. rapporteret enhed af brugsmængde 

Det brug metoden bruger @Parameter attributter som f.eks beskrivelse for at vise et nyttigt resume. Parametre markeret med en stjerne (*) er obligatoriske.

11.2. Fejlhåndtering

Vi kan fange ParameterUndtagelse og ring brug for at hjælpe brugeren med at forstå, hvorfor deres input var forkert. ParameterUndtagelse indeholder JCommander instans for at få vist hjælp:

prøv {jc.parse (args); } fange (ParameterException e) {System.err.println (e.getLocalizedMessage ()); jc.usage (); } 

12. Konklusion

I denne vejledning brugte vi JCommander til at opbygge en kommandolinjeapplikation. Mens vi dækkede mange af de vigtigste funktioner, er der mere i den officielle dokumentation.

Som normalt er kildekoden til alle eksemplerne tilgængelig på GitHub.