3
\$\begingroup\$

The following code extracts command line parameters and modifies argc and argv parameters of the. After all parameters are read remaining values (especially after --) are remaining.

Please keep following questions in mind while reviewing:

  • Is it good practice to modify argc and argv?
  • How to make clear to the caller they change?
  • Are the bool and std::string specialisations obvious?
  • Is there a shorter/compacter way than using the currently used mixin?

Condensed:

/**
* Basic functions shared by general template and specialisations
*/
template <typename T>
class CliArgMixin
{
protected:
    /**
    * Helper function to delete arguments.
    */
    static void deleteArg(int& argc, char* argv[], int pos)
    {
        assert(argv);
        assert(argc > 0);

        if(pos >= argc) return;

        for(int i=pos; i<argc-1; ++i)
        {
            argv[i] = argv[i+1];
        }

        argv[argc] = 0;
        argc--;
        argv[argc] = 0;
    }

    /**
    * Search by an alternative argument name.
    * Use this to use long and short arguments e.g. '-l 10' and '--list 10'
    */
    bool search(int& argc, char* argv[], const std::string& name)
    {
        assert(argv);
        assert(argc > 0);

        if(name.empty()) return false;

        std::string entry;
        for(int i=0; i<argc; ++i)
        {
            assert(argv[i]);
            if(argv[i][0] == '-' && argv[i][1] != '-')
            {
                entry.assign(argv[i] + 1);
            }
            else if(argv[i][0] == '-' && argv[i][1] == '-')
            {
                // skip on --
                if(argv[i][2] == 0) return false;
                entry.assign(argv[i] + 2);
            }
            else
            {
                continue;
            }

            if(entry == name)
            {
                extract(argc, argv, i);
                m_extracted = true;
                return true;
            }
        }
        return false;
    }

    /**
    * Search condesed POSIX short options like '-x12'
    */
    void searchCondensed(int& argc, char* argv[], const std::string& name)
    {
        assert(argc > 0);
        assert(argv);
        if(name.empty()) return;

        std::string entry;
        for(int i=0; i<argc; ++i)
        {
            if(argv[i][0] == '-' && argv[i][1] != '-')
            {
                entry = argv[i][1];
            }
            else if(argv[i][0] == '-' && argv[i][1] == '-')
            {
                // skip on --
                if(argv[i][2] == 0) return;
            }
            else
            {
                continue;
            }

            if(entry == name)
            {
                extractCondensed(argc, argv, i);
                m_extracted = true;
                return;
            }
        }
    }

    /**
    * Returns true if an argument value from the command line was extracted
    * false otherwise.
    */
    bool extractedValue() const { return m_extracted; }

    CliArgMixin(const T& dflt)
    : m_value(dflt)
    , m_extracted(false)
    {}

    virtual ~CliArgMixin() {}

    virtual void extract(int& argc, char* argv[], int index) = 0;
    virtual void extractCondensed(int& argc, char* argv[], int index) = 0;

    T       m_value;
    bool    m_extracted;
};

/**
* General template.
*/
template <typename T>
class CliArg : public CliArgMixin<T>
{
    typedef CliArgMixin<T> Mixin;
public:
    CliArg(int& argc, char* argv[], const std::string& opt, const std::string& longOpt, const T& d = T())
    : Mixin(d)
    {
        if(opt.length() > 1)
        {
            throw std::invalid_argument("Short option must be one character or empty");
        }

        if(longOpt.length() == 1)
        {
            throw std::invalid_argument("Long option must not be one character");
        }

        if( Mixin::search(argc, argv, longOpt) == false )
        {
            if( Mixin::search(argc, argv, opt) == false )
            {
                Mixin::searchCondensed(argc, argv, opt);
            }
        }
    }

    ~CliArg() {}

    const T& value() const { return CliArgMixin<T>::m_value; }
    operator const T&() const { return CliArgMixin<T>::m_value; }

private:
    CliArg() = delete;

    virtual void extract(int& argc, char* argv[], int index)
    {
        if(index+1 < argc)
        {
            std::istringstream(argv[index+1]) >> Mixin::m_value;
            Mixin::deleteArg(argc, argv, index);
            // TRICKY: after the i-th was a delete index is now the old i+1;
            Mixin::deleteArg(argc, argv, index);
        }
        else
        {
            std::ostringstream msg;
            msg << "Missing value for parameter: " << argv[index];
            throw std::runtime_error( msg.str() );
        }
    }

    virtual void extractCondensed(int& argc, char** argv, int index)
    {
        // skip '-' and opt
        std::istringstream(argv[index]+2) >> Mixin::m_value;
        Mixin::deleteArg(argc, argv, index);
    }
};

/**
* Specialisation for std::string values
*/
template <>
class CliArg< std::string > : public CliArgMixin< std::string >
{
    typedef CliArgMixin<std::string> Mixin;
public:
    CliArg(int& argc, char* argv[], const std::string& opt, const std::string& longOpt, const std::string& d = "")
    : Mixin(d)
    {
        if(opt.length() > 1)
        {
            throw std::invalid_argument("Short option must be one character or empty");
        }

        if(longOpt.length() == 1)
        {
            throw std::invalid_argument("Long option must not be one character");
        }

        if( Mixin::search(argc, argv, longOpt) == false)
        {
            if( Mixin::search(argc, argv, opt) == false)
            {
                Mixin::searchCondensed(argc, argv, opt);
            }
        }
    }

    ~CliArg() {}

    const std::string& value() const { return Mixin::m_value; }
    operator const std::string&() const  { return Mixin::m_value; }

private:
    CliArg() = delete;

    virtual void extract(int& argc, char** argv, int index)
    {
        if(index+1 < argc)
        {
            Mixin::m_value = argv[index+1];
            Mixin::deleteArg(argc, argv, index);
            // TRICKY: after the i-th was a delete i is now the old i+1;
            Mixin::deleteArg(argc, argv, index);
        }
        else
        {
            std::ostringstream msg;
            msg << "Missing value for parameter: " << argv[index];
            throw std::runtime_error( msg.str() );
        }
    }
    virtual void extractCondensed(int& argc, char** argv, int index)
    {
        // skip '-' and opt
        Mixin::m_value = argv[index]+2;
        Mixin::deleteArg(argc, argv, index);
    }
};

/**
* Specialisation for bool flags with no following value.
*/
template <>
class CliArg< bool > : public CliArgMixin< bool >
{
    typedef CliArgMixin< bool > Mixin;
public:
    CliArg(int& argc, char* argv[], const std::string& opt, const std::string& longOpt)
    : Mixin(false)
    {
        assert(argc > 0);
        assert(argv);
        if(opt.length() > 1)
        {
            throw std::invalid_argument("Short option must be one character or empty");
        }

        if(longOpt.length() == 1)
        {
            throw std::invalid_argument("Long option must not be one character");
        }

        if( Mixin::search(argc, argv, longOpt) == false )
        {
            if( Mixin::search(argc, argv, opt) == false )
            {
                searchCondensed(argc, argv, opt);
            }
        }
    }

    ~CliArg() {}

    bool value() const { return Mixin::m_value; }

    operator bool() const  { return Mixin::m_value; }

private:

    CliArg() = delete;

    virtual void extract(int& argc, char** argv, int index)
    {
        Mixin::m_value = true;
        Mixin::deleteArg(argc, argv, index);
    }

    /**
    * There are no condensed value for short options
    */
    virtual void extractCondensed(int&, char**, int) {}

    /**
    * Only condensed bool are of type '-asd'
    */
    void searchCondensed(int& argc, char* argv[], const std::string& opt)
    {
        if(opt.empty()) return;

        for(int index=0; index<argc; ++index)
        {
            const size_t len = ::strlen(argv[index]);

            if(len < 2) continue;
            if( argv[index][0] != '-' ) continue;
            if( argv[index][1] == '-' ) return;

            for(uint i=1; i<len; ++i)
            {
                if( argv[index][i] == opt[0] )
                {
                    Mixin::m_value = true;
                    Mixin::m_extracted = true;

                    // remove the opt from the string.
                    argv[index][i] = argv[index][len-1];
                    argv[index][len-1] = 0;

                    break;
                }
            }
        }
    }
};

Usage

a.out --verbose -n 2 input1 input2

bodhi::CliArg< bool > isVerbose(argc, argv, "v", "verbose");
bodhi::CliArg< int > count(argc, argv, "n", "--number");

if( verbose == true ) // be verbose

argc is reduced by 2 and input1 is the next in argv

Update:

fyi I reworked my code to adhere all points made here: https://gist.github.com/aggsol/6008239

\$\endgroup\$
  • 1
    \$\begingroup\$ PPS. If I want to manipulate the command line arguments I usually do this: std::vector<std::string> args(argv + 1, argv + argc); \$\endgroup\$ – Martin York Apr 13 '17 at 16:25
  • 2
    \$\begingroup\$ What you can do rather than modify argc and argv is create a copy and then modify the copy. @LokiAstari is more than correct. \$\endgroup\$ – pacmaninbw Apr 13 '17 at 17:02
  • 2
    \$\begingroup\$ @aggsol I believe they are not const because of legacy reasons. This is no reason for you not to follow best practices. \$\endgroup\$ – Vorac Apr 18 '17 at 15:02
  • 1
    \$\begingroup\$ PS. I will gladly accept any answer stating above mentioned points and citing a good source for not messing with argc and argv. \$\endgroup\$ – aggsol Apr 19 '17 at 6:44
  • 1
    \$\begingroup\$ @Loki - why is it illegal to modify argc and/or argv? There's nothing magical about them. Even if you were talking about the arguments to main(), it's still perfectly valid, and that's the normal pattern for argument parsing in Qt, for example. \$\endgroup\$ – Toby Speight May 4 '17 at 12:17

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.