Обработка на аргументи от командния ред в Java: Делото е затворено

Много Java приложения, стартирани от командния ред, вземат аргументи, за да контролират поведението си. Тези аргументи са налични в аргумента низ масив, предаден в статичния main()метод на приложението . Обикновено има два типа аргументи: опции (или превключватели) и действителни аргументи на данни. Приложението на Java трябва да обработва тези аргументи и да изпълнява две основни задачи:

  1. Проверете дали използваният синтаксис е валиден и поддържан
  2. Вземете действителните данни, необходими на приложението, за да изпълнява своите операции

Често кодът, който изпълнява тези задачи, е направен по поръчка за всяко приложение и по този начин изисква значителни усилия както за създаване, така и за поддържане, особено ако изискванията надхвърлят прости случаи само с една или две опции. В Optionsкласа описани в тази статия оръдия общ подход към лесно да се справят с най-сложните ситуации. Класът позволява проста дефиниция на необходимите опции и аргументи за данни и осигурява задълбочени проверки на синтаксиса и лесен достъп до резултатите от тези проверки. За този проект бяха използвани и нови функции на Java 5 като генерични и безопасни преброявания.

Типове аргументи от командния ред

През годините написах няколко инструмента на Java, които вземат аргументи от командния ред, за да контролират поведението си. В началото ми се стори досадно ръчно да създавам и поддържам кода за обработка на различните опции. Това доведе до разработването на прототип клас за улесняване на тази задача, но този клас несъмнено имаше своите ограничения, тъй като при внимателна проверка броят на възможните различни разновидности на аргументите на командния ред се оказа значителен. В крайна сметка реших да разработя общо решение на този проблем.

При разработването на това решение трябваше да реша два основни проблема:

  1. Идентифицирайте всички разновидности, в които могат да се появят опции на командния ред
  2. Намерете лесен начин да позволите на потребителите да изразят тези разновидности, когато използват все още неразработения клас

Анализът на проблем 1 доведе до следните наблюдения:

  • Опции на командния ред в противоречие с аргументите за данни на командния ред - започнете с префикс, който ги идентифицира уникално. Примерите за префикс включват тире ( -) на платформи на Unix за опции като -aили наклонена черта ( /) на платформи на Windows.
  • Опциите могат да бъдат прости превключватели (т.е. -aмогат да присъстват или не) или да вземат стойност. Пример за това е:

    java MyTool -a -b logfile.inp 
  • Опциите, които приемат стойност, могат да имат различни разделители между действителния ключ за опции и стойността. Такива разделители могат да бъдат празно място, двоеточие ( :) или знак за равенство ( =):

    java MyTool -a -b logfile.inp java MyTool -a -b: logfile.inp java MyTool -a -b = logfile.inp 
  • Опциите, приемащи стойност, могат да добавят още едно ниво на сложност. Помислете за начина, по който Java поддържа дефиницията на свойства на околната среда като пример:

    java -Djava.library.path = / usr / lib ... 
  • Така че, освен действителния ключ за опция ( D), разделителя ( =) и действителната стойност на опцията ( /usr/lib), допълнителен параметър ( java.library.path) може да приеме произволен брой стойности (в горния пример могат да бъдат посочени множество свойства на средата, използвайки този синтаксис ). В тази статия този параметър се нарича „детайл“.
  • Опциите също имат свойство за множественост: те могат да бъдат задължителни или незадължителни, а броят пъти, в които са разрешени, също може да варира (например точно веднъж, веднъж или повече или други възможности).
  • Аргументите на данните са всички аргументи на командния ред, които не започват с префикс. Тук приемливият брой такива аргументи на данни може да варира между минимален и максимален брой (които не са непременно еднакви). В допълнение, обикновено приложението изисква тези аргументи с данни да са последни в командния ред, но не винаги трябва да е така. Например:

    java MyTool -a -b = logfile.inp data1 data2 data3 // Всички данни в края 

    или

    java MyTool -a data1 data2 -b = logfile.inp data3 // Може да е приемливо за приложение 
  • По-сложните приложения могат да поддържат повече от един набор от опции:

    java MyTool -a -b datafile.inp java MyTool -k [-verbose] foo bar duh java MyTool -check -verify logfile.out 
  • И накрая, приложението може да реши да игнорира неизвестни опции или може да счете тези опции за грешка.

Така че, измисляйки начин да позволя на потребителите да изразят всички тези разновидности, измислих следния общ формуляр за опции, който се използва като основа за тази статия:

[[]] 

Този формуляр трябва да се комбинира със свойството множественост, както е описано по-горе.

В рамките на ограниченията на общата форма на опция, описана по-горе, Optionsкласът, описан в тази статия, е проектиран да бъде общото решение за всякакви нужди от обработка на командния ред, които приложението на Java може да има.

Помощните класове

Най- Optionsкласа, която е основен клас за разтвора, описан в тази статия, се предлага с два помощни класове:

  1. OptionData: Този клас съдържа цялата информация за една конкретна опция
  2. OptionSet: Този клас съдържа набор от опции. Optionsсамата тя може да побере произволен брой такива комплекти

Преди да се опишат подробностите за тези класове, Optionsтрябва да се въведат други важни понятия от класа.

Typesafe преброявания

Префиксът, разделителят и свойството за множественост са заснети от enums, функция, предоставена за първи път от Java 5:

публичен префикс на преброяване {DASH ('-'), SLASH ('/'); частна char c; частен префикс (char c) {this.c = c; } char getName () {return c; }} публичен разделител на изброяване {COLON (':'), EQUALS ('='), BLANK (''), NONE ('D'); частна char c; частен разделител (char c) {this.c = c; } char getName () {return c; }} public enum Множество {ONCE, ONCE_OR_MORE, ZERO_OR_ONE, ZERO_OR_MORE; }

Използването на enums има някои предимства: повишена безопасност на типа и строг, лесен контрол върху набора от допустими стойности. Enums също могат удобно да се използват с генерирани колекции.

Обърнете внимание, че Prefixи Separatorenum имат свои собствени конструктори, позволяващи дефинирането на действителен символ, представляващ този екземпляр на enum (спрямо името, използвано за препратка към конкретния екземпляр на enum). Тези символи могат да бъдат извлечени с помощта на getName()методите на тези изброявания и символите се използват за java.util.regexсинтаксиса на шаблона на пакета. Този пакет се използва за извършване на някои от проверките на синтаксиса в Optionsкласа, подробности за които ще последват.

В Multiplicityмомента enum поддържа четири различни стойности:

  1. ONCE: Опцията трябва да се появи точно веднъж
  2. ONCE_OR_MORE: The option has to occur at least once
  3. ZERO_OR_ONCE: The option can either be absent or present exactly once
  4. ZERO_OR_MORE: The option can either be absent or present any number of times

More definitions can easily be added should the need arise.

The OptionData class

The OptionData class is basically a data container: firstly, for the data describing the option itself, and secondly, for the actual data found on the command line for that option. This design is already reflected in the constructor:

OptionData(Options.Prefix prefix, String key, boolean detail, Options.Separator separator, boolean value, Options.Multiplicity multiplicity) 

The key is used as the unique identifier for this option. Note that these arguments directly reflect the findings described earlier: a full option description must have at least a prefix, a key, and multiplicity. Options taking a value also have a separator and might accept details. Note also that this constructor has package access, so applications cannot directly use it. Class OptionSet's addOption() method adds the options. This design principle has the advantage that we have much better control on the actual possible combinations of arguments used to create OptionData instances. For example, if this constructor were public, you could create an instance with detail set to true and value set to false, which is of course nonsense. Rather than having elaborate checks in the constructor itself, I decided to provide a controlled set of addOption() methods.

The constructor also creates an instance of java.util.regex.Pattern, which is used for this option's pattern-matching process. One example would be the pattern for an option taking a value, no details, and a nonblank separator:

pattern = java.util.regex.Pattern.compile(prefix.getName() + key + separator.getName() + "(.+)$"); 

The OptionData class, as already mentioned, also holds the results of the checks performed by the Options class. It provides the following public methods to access these results:

int getResultCount() String getResultValue(int index) String getResultDetail(int index) 

The first method, getResultCount(), returns the number of times an option was found. This method design directly ties in with the multiplicity defined for the option. For options taking a value, this value can be retrieved using the getResultValue(int index) method, where the index can range between 0 and getResultCount() - 1. For value options that also accept details, these can be similarly accessed using the getResultDetail(int index) method.

The OptionSet class

The OptionSet class is basically a container for a set of OptionData instances and also the data arguments found on the command line.

The constructor has the form:

OptionSet(Options.Prefix prefix, Options.Multiplicity defaultMultiplicity, String setName, int minData, int maxData) 

Again, this constructor has package access. Option sets can only be created through the Options class's different addSet() methods. The default multiplicity for the options specified here can be overridden when adding an option to the set. The set name specified here is a unique identifier used to refer to the set. minData and maxData are the minimum and maximum number of acceptable data arguments for this set.

The public API for OptionSet contains the following methods:

General access methods:

String getSetName() int getMinData() int getMaxData() 

Methods to add options:

OptionSet addOption(String key) OptionSet addOption(String key, Multiplicity multiplicity) OptionSet addOption(String key, Separator separator) OptionSet addOption(String key, Separator separator, Multiplicity multiplicity) OptionSet addOption(String key, boolean details, Separator separator) OptionSet addOption(String key, boolean details, Separator separator, Multiplicity multiplicity) 

Methods to access check result data:

java.util.ArrayList getOptionData() OptionData getOption(String key) boolean isSet(String key) java.util.ArrayList getData() java.util.ArrayList getUnmatched() 

Note that the methods for adding options that take a Separator argument create an OptionData instance accepting a value. The addOption() methods return the set instance itself, which allows invocation chaining:

Options options = new Options(args); options.addSet("MySet").addOption("a").addOption("b"); 

After the checks have been performed, their results are available through the remaining methods. getOptionData() returns a list of all OptionData instances, while getOption() allows direct access to a specific option. isSet(String key) is a convenience method that checks whether an options was found at least once on the command line. getData() provides access to the data arguments found, while getUnmatched() lists all options found on the command line for which no matching OptionData instances were found.

The Options class

Options is the core class with which applications will interact. It provides several constructors, all of which take the command line argument string array that the main() method provides as the first argument:

Options(String args[]) Options(String args[], int data) Options(String args[], int defMinData, int defMaxData) Options(String args[], Multiplicity defaultMultiplicity) Options(String args[], Multiplicity defaultMultiplicity, int data) Options(String args[], Multiplicity defaultMultiplicity, int defMinData, int defMaxData) Options(String args[], Prefix prefix) Options(String args[], Prefix prefix, int data) Options(String args[], Prefix prefix, int defMinData, int defMaxData) Options(String args[], Prefix prefix, Multiplicity defaultMultiplicity) Options(String args[], Prefix prefix, Multiplicity defaultMultiplicity, int data) Options(String args[], Prefix prefix, Multiplicity defaultMultiplicity, int defMinData, int defMaxData) 

The first constructor in this list is the simplest one using all the default values, while the last one is the most generic.

Table 1: Arguments for the Options() constructors and their meaning

Value Description Default
prefix This constructor argument is the only place where a prefix can be specified. This value is passed on to any option set and any option created subsequently. The idea behind this approach is that within a given application, it proves unlikely that different prefixes will need to be used. Prefix.DASH
defaultMultiplicity This default multiplicity is passed to each option set and used as the default for options added to a set without specifying a multiplicity. Of course, this multiplicity can be overridden for each option added. Multiplicity.ONCE
defMinData defMinData is the default minimum number of supported data arguments passed to each option set, but it can of course be overridden when adding a set. 0
defMaxData defMaxData е максималният брой поддържани аргументи за данни, предадени на всеки набор от опции, но разбира се може да бъде заменен при добавяне на набор. 0