5f33ed552faba4ce7864812aaf0b11031daa241d
[alexxy/gromacs.git] / src / gromacs / commandline / cmdlineparser.cpp
1 /*
2  * This file is part of the GROMACS molecular simulation package.
3  *
4  * Copyright (c) 2010-2018, The GROMACS development team.
5  * Copyright (c) 2019,2020, by the GROMACS development team, led by
6  * Mark Abraham, David van der Spoel, Berk Hess, and Erik Lindahl,
7  * and including many others, as listed in the AUTHORS file in the
8  * top-level source directory and at http://www.gromacs.org.
9  *
10  * GROMACS is free software; you can redistribute it and/or
11  * modify it under the terms of the GNU Lesser General Public License
12  * as published by the Free Software Foundation; either version 2.1
13  * of the License, or (at your option) any later version.
14  *
15  * GROMACS is distributed in the hope that it will be useful,
16  * but WITHOUT ANY WARRANTY; without even the implied warranty of
17  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
18  * Lesser General Public License for more details.
19  *
20  * You should have received a copy of the GNU Lesser General Public
21  * License along with GROMACS; if not, see
22  * http://www.gnu.org/licenses, or write to the Free Software Foundation,
23  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA.
24  *
25  * If you want to redistribute modifications to GROMACS, please
26  * consider that scientific software is very special. Version
27  * control is crucial - bugs must be traceable. We will be happy to
28  * consider code for inclusion in the official distribution, but
29  * derived work must not be called official GROMACS. Details are found
30  * in the README & COPYING files - if they are missing, get the
31  * official version at http://www.gromacs.org.
32  *
33  * To help us fund GROMACS development, we humbly ask that you cite
34  * the research papers on the package. Check out http://www.gromacs.org.
35  */
36 /*! \internal \file
37  * \brief
38  * Implements gmx::CommandLineParser.
39  *
40  * \author Teemu Murtola <teemu.murtola@gmail.com>
41  * \ingroup module_commandline
42  */
43 #include "gmxpre.h"
44
45 #include "cmdlineparser.h"
46
47 #include <cstdlib>
48
49 #include <string>
50 #include <vector>
51
52 #include "gromacs/options/optionsassigner.h"
53 #include "gromacs/utility/basedefinitions.h"
54 #include "gromacs/utility/exceptions.h"
55
56 namespace gmx
57 {
58
59 /********************************************************************
60  * CommandLineParser::Impl
61  */
62
63 /*! \internal \brief
64  * Private implementation class for CommandLineParser.
65  *
66  * \ingroup module_commandline
67  */
68 class CommandLineParser::Impl
69 {
70 public:
71     //! Sets the options object to parse to.
72     explicit Impl(Options* options);
73
74     /*! \brief
75      * Determines whether a cmdline parameter starts an option and the name
76      * of that option.
77      *
78      * \param[in] arg  Individual argument from \c argv.
79      * \returns The beginning of the option name in \p arg, or NULL if
80      *     \p arg does not look like an option.
81      */
82     static const char* toOptionName(const char* arg);
83
84     //! Helper object for assigning the options.
85     OptionsAssigner assigner_;
86     //! Whether to allow and skip unknown options.
87     bool bSkipUnknown_;
88     /*! \brief Whether to allow positional arguments
89      *
90      * These are not options (no leading hyphen), and come before
91      * all options. */
92     bool bAllowPositionalArguments_;
93 };
94
95 CommandLineParser::Impl::Impl(Options* options) :
96     assigner_(options),
97     bSkipUnknown_(false),
98     bAllowPositionalArguments_(false)
99 {
100     assigner_.setAcceptBooleanNoPrefix(true);
101 }
102
103 const char* CommandLineParser::Impl::toOptionName(const char* arg)
104 {
105     // Lone '-' or '--' is not an option.
106     if (arg[0] != '-' || arg[1] == '\0' || (arg[1] == '-' && arg[2] == '\0'))
107     {
108         return nullptr;
109     }
110     // Something starting with '--' is always an option.
111     if (arg[1] == '-')
112     {
113         return arg + 2;
114     }
115     // Don't return numbers as option names.
116     char* endptr;
117     // We are only interested in endptr, not in the actual value.
118     GMX_IGNORE_RETURN_VALUE(std::strtod(arg, &endptr));
119     if (*endptr == '\0')
120     {
121         return nullptr;
122     }
123     return arg + 1;
124 }
125
126 /********************************************************************
127  * CommandLineParser
128  */
129
130 CommandLineParser::CommandLineParser(Options* options) : impl_(new Impl(options)) {}
131
132 CommandLineParser::~CommandLineParser() {}
133
134 CommandLineParser& CommandLineParser::skipUnknown(bool bEnabled)
135 {
136     impl_->bSkipUnknown_ = bEnabled;
137     return *this;
138 }
139
140 CommandLineParser& CommandLineParser::allowPositionalArguments(bool bEnabled)
141 {
142     impl_->bAllowPositionalArguments_ = bEnabled;
143     return *this;
144 }
145
146 void CommandLineParser::parse(int* argc, char* argv[])
147 {
148     ExceptionInitializer errors("Invalid command-line options");
149     std::string          currentContext;
150     bool                 bInOption = false;
151
152     // Note that this function gets called multiple times in typical
153     // cases of calling gmx. Command lines like "gmx -hidden mdrun -h"
154     // work because the first call has argv[0] == "gmx" and skips
155     // unknown things, and the second has argv[0] == "mdrun".
156     int i = 1, newi = 1;
157
158     // First, process any permitted leading positional arguments.
159     for (; i < *argc; ++i)
160     {
161         const char* const arg = argv[i];
162         if (impl_->toOptionName(arg) != nullptr)
163         {
164             // If we find an option, no more positional arguments
165             // can be handled.
166             break;
167         }
168
169         if (!impl_->bAllowPositionalArguments_)
170         {
171             GMX_THROW(
172                     InvalidInputError(
173                             "Positional argument '" + std::string(arg)
174                             + "' cannot be accepted. "
175                               "Perhaps you forgot to put a hyphen before an option name."));
176         }
177         // argv[i] is not an option, so preserve it in the argument list
178         // by incrementing newi. There's no need to copy argv contents
179         // because they cannot have changed yet.
180         ++newi;
181     }
182
183     // Now handle the option arguments.
184     impl_->assigner_.start();
185     for (; i < *argc; ++i)
186     {
187         const char* const arg        = argv[i];
188         const char* const optionName = impl_->toOptionName(arg);
189         if (optionName != nullptr)
190         {
191             if (bInOption)
192             {
193                 try
194                 {
195                     impl_->assigner_.finishOption();
196                 }
197                 catch (UserInputError& ex)
198                 {
199                     ex.prependContext(currentContext);
200                     errors.addCurrentExceptionAsNested();
201                 }
202             }
203             currentContext = "In command-line option " + std::string(arg);
204             try
205             {
206                 bInOption = impl_->assigner_.tryStartOption(optionName);
207                 if (!bInOption)
208                 {
209                     currentContext.clear();
210                     if (!impl_->bSkipUnknown_)
211                     {
212                         std::string message = "Unknown command-line option " + std::string(arg);
213                         GMX_THROW(InvalidInputError(message));
214                     }
215                 }
216             }
217             catch (UserInputError& ex)
218             {
219                 // If tryStartOption() throws, make sure that the rest gets
220                 // ignored.
221                 // TODO: Consider whether we should remove the option from the
222                 // command line nonetheless, as it is recognized, but just
223                 // invalid.
224                 bInOption = false;
225                 ex.prependContext(currentContext);
226                 errors.addCurrentExceptionAsNested();
227                 currentContext.clear();
228             }
229         }
230         else if (bInOption)
231         {
232             try
233             {
234                 impl_->assigner_.appendValue(arg);
235             }
236             // TODO: Consider if some types of exceptions would be better left
237             // unhandled.
238             catch (GromacsException& ex)
239             {
240                 ex.prependContext(currentContext);
241                 errors.addCurrentExceptionAsNested();
242             }
243         }
244         // Retain unrecognized options if applicable.
245         if (!bInOption && impl_->bSkipUnknown_)
246         {
247             argv[newi] = argv[i];
248             ++newi;
249         }
250     }
251     // Update the args if argv was modified.
252     if (impl_->bSkipUnknown_)
253     {
254         *argc      = newi;
255         argv[newi] = nullptr;
256     }
257
258     // Finish the last option.
259     if (bInOption)
260     {
261         try
262         {
263             impl_->assigner_.finishOption();
264         }
265         catch (UserInputError& ex)
266         {
267             ex.prependContext(currentContext);
268             errors.addCurrentExceptionAsNested();
269         }
270     }
271     impl_->assigner_.finish();
272     if (errors.hasNestedExceptions())
273     {
274         // TODO: This exception type may not always be appropriate.
275         GMX_THROW(InvalidInputError(errors));
276     }
277 }
278
279 } // namespace gmx