Maak een Java-opdrachtregelprogramma met Picocli

1. Inleiding

In deze tutorial zullen we de picocli bibliotheek, waarmee we eenvoudig opdrachtregelprogramma's in Java kunnen maken.

We gaan eerst aan de slag door een Hello World-commando te maken. We duiken dan diep in de belangrijkste functies van de bibliotheek door het git opdracht.

2. Hallo Wereldcommando

Laten we beginnen met iets eenvoudigs: een Hello World-commando!

Allereerst moeten we de afhankelijkheid toevoegen aan het picocli project:

 info.picocli picocli 3.9.6 

Zoals we kunnen zien, gebruiken we de 3.9.6 versie van de bibliotheek, hoewel een 4.0.0 versie is in aanbouw (momenteel beschikbaar in alfatest).

Nu de afhankelijkheid is ingesteld, gaan we onze Hello World-opdracht maken. Om dat te doen, we zullen de @Opdracht annotatie uit de bibliotheek:

@Command (name = "hallo", description = "Says hallo") openbare klasse HelloWorldCommand {}

Zoals we kunnen zien, kan de annotatie parameters aannemen. We gebruiken er hier maar twee. Hun doel is om informatie te geven over de huidige opdracht en tekst voor het automatische helpbericht.

Op dit moment kunnen we niet veel doen met dit commando. Om het iets te laten doen, moeten we een hoofd methode aanroepen het gemak CommandLine.run (Runnable, String []) methode. Hiervoor zijn twee parameters nodig: een instantie van onze opdracht, die dus het Runnable interface, en een Draad matrix die de opdrachtargumenten (opties, parameters en subopdrachten) vertegenwoordigt:

openbare klasse HelloWorldCommand implementeert Runnable {openbare statische leegte hoofd (String [] args) {CommandLine.run (nieuw HelloWorldCommand (), args); } @Override public void run () {System.out.println ("Hallo wereld!"); }}

Nu, wanneer we het hoofd methode, zullen we zien dat de console output "Hallo Wereld!"

Wanneer verpakt in een jar, kunnen we onze Hello World-opdracht uitvoeren met behulp van de Java opdracht:

java -cp "pathToPicocliJar; pathToCommandJar" com.baeldung.picoli.helloworld.HelloWorldCommand

Het is geen verrassing dat dat ook het "Hallo Wereld!" string naar de console.

3. Een concrete use case

Nu we de basis hebben gezien, gaan we diep in op het picocli bibliotheek. Om dat te doen, gaan we een populair commando gedeeltelijk reproduceren: git.

Het doel is natuurlijk niet om het git commandogedrag, maar om de mogelijkheden van de git commando - welke subcommando's er zijn en welke opties beschikbaar zijn voor een eigenaardig subcommando.

Eerst moeten we een GitCommand klasse zoals we deden voor ons Hello World-commando:

@Command public class GitCommand implementeert Runnable {public static void main (String [] args) {CommandLine.run (new GitCommand (), args); } @Override public void run () {System.out.println ("Het populaire git-commando"); }}

4. Subopdrachten toevoegen

De git commando biedt veel subcommando's - add, commit, remote, en nog veel meer. We zullen ons hier concentreren op toevoegen en plegen.

Ons doel hier is dus om die twee subcommando's aan het hoofdcommando te geven. Picocli biedt drie manieren om dit te bereiken.

4.1. De ... gebruiken @Opdracht Annotatie over klassen

De @Opdracht annotatie biedt de mogelijkheid om subopdrachten te registreren via de subopdrachten parameter:

@Command (subcommands = {GitAddCommand.class, GitCommitCommand.class})

In ons geval voegen we twee nieuwe klassen toe: GitAddCommand en GitCommitCommand. Beide zijn geannoteerd met @Opdracht en implementeren Runnable. Het is belangrijk om ze een naam te geven, aangezien de namen worden gebruikt door picocli om te herkennen welke subopdracht (en) moeten worden uitgevoerd:

@Command (name = "add") public class GitAddCommand implementeert Runnable {@Override public void run () {System.out.println ("Sommige bestanden aan het staging-gebied toevoegen"); }}
@Command (name = "commit") public class GitCommitCommand implementeert Runnable {@Override public void run () {System.out.println ("Bestanden vastleggen in het staging-gebied, hoe geweldig?"); }}

Dus als we ons hoofdcommando uitvoeren met toevoegen als argument zal de console uitvoeren "Sommige bestanden toevoegen aan het verzamelgebied".

4.2. De ... gebruiken @Opdracht Annotatie over methoden

Een andere manier om subopdrachten te declareren, is door creëren @Opdracht-geannoteerde methoden die deze opdrachten in de GitCommand klasse:

@Command (name = "add") public void addCommand () {System.out.println ("Sommige bestanden aan het verzamelgebied toevoegen"); } @Command (name = "commit") public void commitCommand () {System.out.println ("Bestanden vastleggen in het staging-gebied, hoe geweldig?"); }

Op die manier kunnen we onze bedrijfslogica rechtstreeks in de methoden implementeren en geen afzonderlijke klassen maken om deze af te handelen.

4.3. Programmatisch subopdrachten toevoegen

Tenslotte, picocli biedt ons de mogelijkheid om onze subcommando's programmatisch te registreren. Deze is een beetje lastiger, omdat we een Opdrachtregel object omwikkelt onze opdracht en voegt er vervolgens de subopdrachten aan toe:

CommandLine commandLine = nieuwe CommandLine (nieuwe GitCommand ()); commandLine.addSubcommand ("add", nieuwe GitAddCommand ()); commandLine.addSubcommand ("commit", nieuwe GitCommitCommand ());

Daarna moeten we nog steeds onze opdracht uitvoeren, maar we kunnen geen gebruik maken van de CommandLine.run () methode meer. Nu moeten we de parseWithHandler () methode op onze nieuw gecreëerde CommandLine voorwerp:

commandLine.parseWithHandler (nieuwe RunLast (), args);

We moeten rekening houden met het gebruik van de RunLast klasse, die vertelt picocli om de meest specifieke subopdracht uit te voeren. Er zijn twee andere opdrachthandlers die worden geleverd door picocli: RunFirst en RunAll. De eerste voert het bovenste commando uit, terwijl de laatste ze allemaal uitvoert.

Bij gebruik van de gemaksmethode CommandLine.run (), de RunLast handler wordt standaard gebruikt.

5. Opties beheren met de @Keuze Annotatie

5.1. Optie zonder argument

Laten we nu kijken hoe we enkele opties aan onze opdrachten kunnen toevoegen. Inderdaad, we zouden het onze willen vertellen toevoegen opdracht dat het alle gewijzigde bestanden moet toevoegen. Om dat te bereiken, we voegen een veld toe dat is geannoteerd met de @Keuze annotatie naar onze GitAddCommand klasse:

@Option (names = {"-A", "--all"}) private boolean allFiles; @Override public void run () {if (allFiles) {System.out.println ("Alle bestanden aan het verzamelgebied worden toegevoegd"); } else {System.out.println ("Sommige bestanden aan het staging-gebied toevoegen"); }}

Zoals we kunnen zien, duurt de annotatie een namen parameter, die de verschillende namen van de optie geeft. Daarom belt u het toevoegen commando met een van beide -EEN of -alle zal de alle bestanden veld naar waar. Dus als we de opdracht met de optie uitvoeren, wordt de console weergegeven "Alle bestanden toevoegen aan het verzamelgebied".

5.2. Optie met een argument

Zoals we zojuist hebben gezien, wordt voor opties zonder argumenten hun aanwezigheid of afwezigheid altijd geëvalueerd als a boolean waarde.

Het is echter mogelijk om opties te registreren die argumenten accepteren. We kunnen dit eenvoudig doen door te verklaren dat ons veld van een ander type is. Laten we een bericht optie voor onze plegen opdracht:

@Option (names = {"-m", "--message"}) privé String-bericht; @Override public void run () {System.out.println ("Bestanden vastleggen in het staging-gebied, hoe geweldig?"); if (bericht! = null) {System.out.println ("Het vastlegbericht is" + bericht); }}

Het is niet verwonderlijk dat de bericht optie, zal het commando het vastlegbericht op de console tonen. Later in dit artikel bespreken we welke typen door de bibliotheek worden verwerkt en hoe u met andere typen kunt omgaan.

5.3. Optie met meerdere argumenten

Maar nu, wat als we willen dat onze opdracht meerdere berichten opneemt, zoals wordt gedaan met de echte git commit opdracht? Geen zorgen, laten we ons veld een array of een Verzameling, en we zijn zo ongeveer klaar:

@Option (names = {"-m", "--message"}) privé String [] berichten; @Override public void run () {System.out.println ("Bestanden vastleggen in het staging-gebied, hoe geweldig?"); if (messages! = null) {System.out.println ("Het vastlegbericht is"); voor (String bericht: berichten) {System.out.println (bericht); }}}

Nu kunnen we de bericht optie meerdere keren:

commit -m "Mijn commit is geweldig" -m "Mijn commit is prachtig"

Het is echter mogelijk dat we de optie maar één keer willen geven en de verschillende parameters door een regex-scheidingsteken van elkaar willen scheiden. Daarom kunnen we de splitsen parameter van de @Keuze annotatie:

@Option (names = {"-m", "--message"}, split = ",") private String [] berichten;

Nu kunnen we slagen -m "Mijn commitment is geweldig", "Mijn commitment is mooi" om hetzelfde resultaat te bereiken als hierboven.

5.4. Vereiste optie

Soms hebben we misschien een optie die vereist is. De verplicht argument, dat standaard is ingesteld op false, stelt ons in staat om dat te doen:

@Option (names = {"-m", "--message"}, required = true) private String [] berichten;

Nu is het onmogelijk om de plegen commando zonder het bericht keuze. Als we dat proberen, picocli zal een foutmelding afdrukken:

Ontbrekende vereiste optie '--message =' Gebruik: git commit -m = [-m =] ... -m, --message =

6. Positieparameters beheren

6.1. Leg positionele parameters vast

Laten we ons nu concentreren op onze toevoegen commando omdat het nog niet erg krachtig is. We kunnen alleen besluiten om alle bestanden toe te voegen, maar wat als we specifieke bestanden willen toevoegen?

We zouden een andere optie kunnen gebruiken om dat te doen, maar een betere keuze zou zijn om positionele parameters te gebruiken. Inderdaad, positionele parameters zijn bedoeld om commando-argumenten vast te leggen die specifieke posities innemen en zijn geen subcommando's of opties.

In ons voorbeeld zou dit ons in staat stellen om zoiets te doen als:

voeg file1 file2

Om positionele parameters vast te leggen, we maken gebruik van de @Parameters annotatie:

@Parameters privélijstbestanden; @Override public void run () {if (allFiles) {System.out.println ("Alle bestanden aan het verzamelgebied worden toegevoegd"); } if (files! = null) {files.forEach (pad -> System.out.println ("Toevoegen" + pad + "aan het verzamelgebied")); }}

Nu zou ons commando van eerder afdrukken:

Bestand1 aan het verzamelgebied toevoegen Bestand2 aan het verzamelgebied toevoegen

6.2. Leg een subset van positionele parameters vast

Dankzij de inhoudsopgave parameter van de annotatie. De index is gebaseerd op nul. Dus als we definiëren:

@Parameters (index = "2 .. *")

Dit zou argumenten vastleggen die niet overeenkomen met opties of subopdrachten, van de derde tot het einde.

De index kan een bereik of een enkel getal zijn, dat een enkele positie vertegenwoordigt.

7. Een woord over typeconversie

Zoals we eerder in deze tutorial hebben gezien, picocli voert zelf een typeconversie uit. Het wijst bijvoorbeeld meerdere waarden toe aan arrays of Collecties, maar het kan ook argumenten aan specifieke typen toewijzen, zoals wanneer we de Pad klasse voor de toevoegen opdracht.

Eigenlijk, picocli wordt geleverd met een heleboel voorbewerkte soorten. Dit betekent dat we die typen direct kunnen gebruiken zonder dat we er zelf over na hoeven te denken om ze om te zetten.

Het is echter mogelijk dat we onze opdrachtargumenten moeten toewijzen aan andere typen dan degene die al zijn afgehandeld. Gelukkig voor ons, dit is mogelijk dankzij de ITypeConverter interface en de CommandLine # registerConverter methode, die een type aan een converter koppelt.

Laten we ons voorstellen dat we de config subcommando aan onze git commando, maar we willen niet dat gebruikers een configuratie-element wijzigen dat niet bestaat. Dus besluiten we om die elementen toe te wijzen aan een enum:

openbare opsomming ConfigElement {USERNAME ("user.name"), EMAIL ("user.email"); private laatste String-waarde; ConfigElement (String-waarde) {this.value = value; } public String value () {retourwaarde; } public static ConfigElement from (String value) {return Arrays.stream (values ​​()) .filter (element -> element.value.equals (waarde)) .findFirst () .orElseThrow (() -> new IllegalArgumentException ("De argument "+ waarde +" komt niet overeen met een ConfigElement ")); }}

Plus, in onze nieuw gecreëerde GitConfigCommand klasse, laten we twee positionele parameters toevoegen:

@Parameters (index = "0") privé ConfigElement-element; @Parameters (index = "1") private String-waarde; @Override public void run () {System.out.println ("Setting" + element.value () + "to" + value); }

Op deze manier zorgen we ervoor dat gebruikers niet-bestaande configuratie-elementen niet kunnen wijzigen.

Ten slotte moeten we onze converter registreren. Wat mooi is, is dat als we Java 8 of hoger gebruiken, we niet eens een klasse hoeven te maken die het ITypeConverter koppel. We kunnen gewoon een lambda- of methodeverwijzing doorgeven aan de registerConverter () methode:

CommandLine commandLine = nieuwe CommandLine (nieuwe GitCommand ()); commandLine.registerConverter (ConfigElement.class, ConfigElement :: from); commandLine.parseWithHandler (nieuwe RunLast (), args);

Dit gebeurt in de GitCommand hoofd() methode. Merk op dat we het gemak moesten loslaten CommandLine.run () methode.

Bij gebruik met een niet-afgehandeld configuratie-element, zou de opdracht het helpbericht weergeven plus een stukje informatie dat ons vertelt dat het niet mogelijk was om de parameter naar een ConfigElement:

Ongeldige waarde voor positionele parameter bij index 0 (): kan 'user.phone' niet converteren naar ConfigElement (java.lang.IllegalArgumentException: het argument user.phone komt niet overeen met een ConfigElement) 

8. Integratie met Spring Boot

Laten we tot slot eens kijken hoe we dat allemaal kunnen Springify!

Inderdaad, we werken misschien in een Spring Boot-omgeving en willen hiervan profiteren in ons opdrachtregelprogramma. Om dat te doen, we moeten een SpringBootApplicationimplementatie van de CommandLineRunner koppel:

@SpringBootApplication openbare klasse Toepassing implementeert CommandLineRunner {openbare statische leegte hoofd (String [] args) {SpringApplication.run (Application.class, args); } @Override public void run (String ... args) {}}

Plus, laten we al onze commando's en subcommando's annoteren met de Spring @Component annotatie en autowire dat alles in onze Toepassing:

privé GitCommand gitCommand; privé GitAddCommand addCommand; privé GitCommitCommand commitCommand; private GitConfigCommand configCommand; openbare toepassing (GitCommand gitCommand, GitAddCommand addCommand, GitCommitCommand commitCommand, GitConfigCommand configCommand) {this.gitCommand = gitCommand; this.addCommand = addCommand; this.commitCommand = commitCommand; this.configCommand = configCommand; }

Merk op dat we elk subcommando automatisch moesten bedraden. Helaas komt dit omdat, voorlopig, picocli is nog niet in staat om subopdrachten uit de Spring-context op te halen wanneer ze declaratief worden gedeclareerd (met annotaties). We zullen die bedrading dus zelf moeten doen, op een programmatische manier:

@Override public void run (String ... args) {CommandLine commandLine = nieuwe CommandLine (gitCommand); commandLine.addSubcommand ("add", addCommand); commandLine.addSubcommand ("commit", commitCommand); commandLine.addSubcommand ("config", configCommand); commandLine.parseWithHandler (nieuwe CommandLine.RunLast (), args); }

En nu werkt ons opdrachtregelprogramma als een zonnetje met Spring-componenten. Daarom zouden we een aantal serviceklassen kunnen maken en deze in onze opdrachten kunnen gebruiken, en Spring de afhankelijkheidsinjectie laten verzorgen.

9. Conclusie

In dit artikel hebben we enkele belangrijke kenmerken van de picocli bibliotheek. We hebben geleerd hoe we een nieuwe opdracht kunnen maken en er enkele subopdrachten aan kunnen toevoegen. We hebben veel manieren gezien om met opties en positionele parameters om te gaan. Bovendien hebben we geleerd hoe we onze eigen typeconverters kunnen implementeren om onze opdrachten sterk getypt te maken. Eindelijk hebben we gezien hoe we Spring Boot in onze opdrachten kunnen brengen.

Er valt natuurlijk nog veel meer over te ontdekken. De bibliotheek biedt volledige documentatie.

Wat betreft de volledige code van dit artikel, deze is te vinden op onze GitHub.