xaizek / dit (License: GPLv3) (since 2018-12-07)
Command-line task keeper that remembers all old values and is meant to combine several orthogonal features to be rather flexible in managing items.
<root> / src / Invocation.cpp (8a9020742a220ab5ea88e405ed0d0b1c64bf1128) (8,194B) (mode 100644) [raw]
// Copyright (C) 2015 xaizek <xaizek@posteo.net>
//
// This file is part of dit.
//
// dit is free software: you can redistribute it and/or modify
// it under the terms of version 3 of the GNU Affero General Public
// License as published by the Free Software Foundation.
//
// dit is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with dit.  If not, see <http://www.gnu.org/licenses/>.

#include "Invocation.hpp"

#include <algorithm>
#include <functional>
#include <regex>
#include <string>
#include <tuple>
#include <utility>
#include <vector>

#include <boost/program_options/errors.hpp>
#include <boost/program_options/options_description.hpp>
#include <boost/program_options/parsers.hpp>
#include <boost/program_options/variables_map.hpp>
#include <boost/range/adaptor/reversed.hpp>

#include "utils/args.hpp"
#include "utils/containers.hpp"
#include "utils/contains.hpp"
#include "utils/strings.hpp"

namespace po = boost::program_options;

static std::vector<std::string> applyAlias(
    const std::vector<std::string> &alias, const std::vector<std::string> &args,
    bool completion);

void
Invocation::setCmdLine(std::vector<std::string> args)
{
    if (!args.empty() && !args[0].empty() && args[0].front() == '.') {
        prjName = args[0].substr(1);
        args.erase(args.begin());
    }

    std::vector<std::string> assigns;
    std::tie(assigns, cmdLine) =
        span(args,
             [](const std::string &s) {
                 return contains(s, '=');
             });

    confs.reserve(confs.size() + assigns.size());
    for (const std::string &assign : assigns) {
        confs.emplace_back(splitAt(assign, '='));
    }
}

void
Invocation::setDefCmdLine(std::string cmdLine)
{
    defCmdLine = std::move(cmdLine);
}

void
Invocation::setDefPrjName(std::string prjName)
{
    defPrjName = std::move(prjName);
}

void
Invocation::setAliasResolver(aliasResolverFunc resolver)
{
    aliasResolver = std::move(resolver);
}

void
Invocation::parse(bool completion)
{
    processOptions(completion);

    const bool takeComposition = !cmdLine.empty();
    if (takeComposition) {
        composition = cmdLine[0];
        cmdName = composition;
        cmdLine.erase(cmdLine.begin());

        if (composition.empty()) {
            // "." and "" are treated equivalently, but the former one can also
            // be distinguished from missing composition.
            composition = ".";
        }
    }

    std::vector<std::string> names;

    // Treat "." as an alias for "".
    if (takeComposition && (composition.empty() || composition == ".")) {
        names.assign({ std::string() });
    } else {
        names = split(composition, '.');
        if (std::count_if(names.cbegin(), names.cend(),
                        std::mem_fn(&std::string::empty)) > 1U) {
            names.assign({ composition });
        }
    }

    auto processAlias = [this, completion](const std::string &name) {
        const std::string rhsString = aliasResolver(name);
        if (rhsString.empty()) {
            cmdLine.insert(cmdLine.begin(), name);
            return;
        }

        const std::vector<std::string> &rhs = breakIntoArgs(rhsString);
        setCmdLine(applyAlias(rhs, cmdLine, completion));
    };

    // Break composition by a '.' and apply aliases from right to left, commands
    // that are not aliases are just prepended to current command-line.
    namespace adaptors = boost::adaptors;
    for (const std::string &name : adaptors::reverse(names)) {
        if (!name.empty()) {
            processAlias(name);
            continue;
        }

        if (defCmdLine.empty()) {
            continue;
        }

        std::vector<std::string> args = breakIntoArgs(defCmdLine);
        cmdLine.insert(cmdLine.cbegin(), args.cbegin(), args.cend());
        setCmdLine(cmdLine);

        if (!cmdLine.empty()) {
            std::vector<std::string> names = split(cmdLine.front(), '.');
            cmdLine.erase(cmdLine.begin());
            for (const std::string &name : adaptors::reverse(names)) {
                processAlias(name);
            }
        }
    }

    if (cmdLine.empty()) {
        cmdName.clear();
    } else {
        cmdName = cmdLine[0];
        cmdLine.erase(cmdLine.begin());
    }
}

void
Invocation::processOptions(bool completion)
{
    po::options_description opts("dit options");
    opts.add_options()
        ("help,h", "display help message")
        ("version,v", "display version information");
    std::ostringstream oss;
    oss << opts;
    helpMsg = oss.str();

    using opt_t = boost::shared_ptr<po::option_description>;
    optNames.clear();
    for (const opt_t &opt : opts.options()) {
        optNames.push_back("--" + opt->long_name());
    }
    optNames.push_back("-h");
    optNames.push_back("-v");

    po::options_description all;
    all.add(opts);

    std::vector<std::string> prefix;
    std::tie(prefix, cmdLine) = span(cmdLine, [](const std::string &a) {
                                         return !a.empty() && a.front() == '-';
                                     });

    po::variables_map vm;
    try {
        po::store(po::command_line_parser(prefix).options(all).run(), vm);
        po::notify(vm);
    } catch (const boost::program_options::error &) {
        // Allow unknown options for completion.
        if (!completion) {
            throw;
        }
    }

    help = vm.count("help");
    version = vm.count("version");

    if (cmdLine.empty()) {
        if (prefix.empty()) {
            cmdLine = breakIntoArgs(defCmdLine);
        } else {
            composition.clear();
            cmdName.clear();
        }
    }
}

/**
 * @brief Performs alias argument substitution.
 *
 * @param alias Right-hand side of the alias.
 * @param args Arguments passed to the alias.
 * @param completion Whether we're performing completion.
 *
 * @returns Expanded command-line.
 */
static std::vector<std::string>
applyAlias(const std::vector<std::string> &alias,
           const std::vector<std::string> &args, bool completion)
{
    // TODO: maybe display error to the user if not all alias arguments were
    //       supplied

    static std::regex argRegex(R"(\$\{(\d+)\})");

    std::vector<bool> argUsed(args.size());

    std::vector<std::string> substituted;

    std::smatch match;
    for (const std::string &arg : alias) {
        if (std::regex_match(arg, match, argRegex)) {
            const unsigned int argN = std::stoi(match[1].str());
            if (argN == 0) {
                substituted.push_back(arg);
            } else if (argN <= args.size()) {
                substituted.push_back(args[argN - 1U]);
                argUsed[argN - 1U] = true;

                // Stop after inserting last argument during completion.
                if (completion && argN == args.size()) {
                    return substituted;
                }
            } else {
                substituted.emplace_back();
            }
        } else {
            substituted.push_back(arg);
        }
    }

    // Append all unused elements.
    for (unsigned int i = 0; i < args.size(); ++i) {
        if (!argUsed[i]) {
            substituted.push_back(args[i]);
        }
    }

    return substituted;
}

bool
Invocation::shouldPrintHelp() const
{
    return help;
}

std::string
Invocation::getHelp() const
{
    return helpMsg;
}

std::vector<std::string>
Invocation::getOpts() const
{
    return optNames;
}

bool
Invocation::shouldPrintVersion() const
{
    return version;
}

std::string
Invocation::getPrjName() const
{
    return prjName.empty() ? defPrjName : prjName;
}

std::string
Invocation::getCmdName() const
{
    return cmdName;
}

std::string
Invocation::getComposition() const
{
    return composition;
}

std::vector<std::string>
Invocation::getCmdArgs() const
{
    return cmdLine;
}

std::vector<std::pair<std::string, std::string>>
Invocation::getConfs() const
{
    return confs;
}
Hints

Before first commit, do not forget to setup your git environment:
git config --global user.name "your_name_here"
git config --global user.email "your@email_here"

Clone this repository using HTTP(S):
git clone https://code.reversed.top/user/xaizek/dit

Clone this repository using ssh (do not forget to upload a key first):
git clone ssh://rocketgit@code.reversed.top/user/xaizek/dit

You are allowed to anonymously push to this repository.
This means that your pushed commits will automatically be transformed into a pull request:
... clone the repository ...
... make some changes and some commits ...
git push origin master