问:当给定路径包含空格时,使用 std::filesystem::path 作为选项的 Boost Program Options 失败

Q: Boost Program Options using std::filesystem::path as option fails when the given path contains spaces

我有一个使用 Boost.Program_Options 的 windows 命令行程序。一种选择使用 std::filesystem::path 变量。

namespace fs = std::filesystem;
namespace po = boost::program_options;

fs::path optionsFile;

po::options_description desc( "Options" );
desc.add_options()
        ("help,h", "Help screen")
        ("options,o", po::value<fs::path>( &optionsFile ), "file with options");

用 -o c:\temp\options.txt 或 -o "c:\temp\options.txt" 调用程序工作正常,但用 -o "c:\temp\options 1.txt" 失败并出现此错误:

error: the argument( 'c:\temp\options 1.txt' ) for option '--options' is invalid

本例中argv的内容为:

这是完整代码:

#include <boost/program_options.hpp>
#include <filesystem>
#include <iostream>

namespace fs = std::filesystem;
namespace po = boost::program_options;

int wmain( int argc, wchar_t * argv[] )
{
    try
    {
        fs::path optionsFile;

        po::options_description desc( "Options" );
        desc.add_options()
            ("help,h", "Help screen")
            ("options,o", po::value<fs::path>( &optionsFile ), "File containing the command and the arguments");

        po::wcommand_line_parser parser{ argc, argv };
        parser.options( desc ).allow_unregistered().style(
            po::command_line_style::default_style |
            po::command_line_style::allow_slash_for_short );
        po::wparsed_options parsed_options = parser.run();

        po::variables_map vm;
        store( parsed_options, vm );
        notify( vm );

        if( vm.count( "help" ) )
        {
            std::cout << desc << '\n';
            return 0;
        }

        std::cout << "optionsFile = " << optionsFile << "\n";
    }
    catch( const std::exception & e )
    {
        std::cerr << "error: " << e.what() << "\n";
        return 1;
    }

    return 0;
}

如何正确处理包含空格的路径?甚至可以使用 std::filesystem::path 还是我必须使用 std::wstring?

我确实可以重现这个。将 fs::path 替换为 std::string 修复了它。

这是一个并排的复制器: Live On Coliru

#include <boost/program_options.hpp>
#include <filesystem>
#include <iostream>

namespace po = boost::program_options;

template <typename Path> static constexpr auto Type           = "[unknown]";
template <> constexpr auto Type<std::string>           = "std::string";
template <> constexpr auto Type<std::filesystem::path> = "fs::path";

template <typename Path>
bool do_test(int argc, char const* argv[]) try {
    Path optionsFile;

    po::options_description desc("Options");
    desc.add_options()            //
        ("help,h", "Help screen") //
        ("options,o", po::value<Path>(&optionsFile),
         "File containing the command and the arguments");

    po::command_line_parser parser{argc, argv};
    parser.options(desc).allow_unregistered().style(
            po::command_line_style::default_style |
            po::command_line_style::allow_slash_for_short);
    auto parsed_options = parser.run();

    po::variables_map vm;
    store(parsed_options, vm);
    notify(vm);

    if (vm.count("help")) {
        std::cout << desc << '\n';
        return true;
    }

    std::cout << "Using " << Type<Path> << "\toptionsFile = " << optionsFile << "\n";
    return true;
} catch (const std::exception& e) {
    std::cout << "Using " << Type<Path> << "\terror: " << e.what() << "\n";
    return false;
}

int main() {
    for (auto args : {
             std::vector{"Exepath", "-o", "c:\temp\options1.txt"},
             std::vector{"Exepath", "-o", "c:\temp\options 1.txt"},
         })
    {
        std::cout << "\n -- Input: ";
        for (auto& arg : args) {
            std::cout << " " << std::quoted(arg);
        }
        std::cout << "\n";
        int argc = args.size();
        args.push_back(nullptr);
        do_test<std::string>(argc, args.data());
        do_test<std::filesystem::path>(argc, args.data());
    }
} 

版画

 -- Input:  "Exepath" "-o" "c:\temp\options1.txt"
Using std::string   optionsFile = c:\temp\options1.txt
Using fs::path  optionsFile = "c:\temp\options1.txt"

 -- Input:  "Exepath" "-o" "c:\temp\options 1.txt"
Using std::string   optionsFile = c:\temp\options 1.txt
Using fs::path  error: the argument ('c:\temp\options 1.txt') for option '--options' is invalid

最有可能的原因是从命令行参数中提取默认使用 operator>> 字符串流¹。如果设置了 skipws(默认情况下所有 C++ istream 都这样做),那么空格会停止“解析”并且参数会被拒绝,因为它没有被完全消耗。

但是,修改代码以包含 path 秒触发的 validate 重载,添加 std::noskipws 没有帮助!

template <class CharT>
void validate(boost::any& v, std::vector<std::basic_string<CharT>> const& s,
              std::filesystem::path* p, int)
{
    assert(s.size() == 1);
    std::basic_stringstream<CharT> ss;

    for (auto& el : s)
        ss << el;

    path converted;
    ss >> std::noskipws >> converted;

    if (!ss.eof())
        throw std::runtime_error("Invalid path format");

    v = std::move(converted);
}

显然,fs::pathoperator>> 不遵守 noskipws。一看at the docs confirms:

Performs stream input or output on the path p. std::quoted is used so that spaces do not cause truncation when later read by stream input operator.

这为我们提供了解决方法:

解决方法

template <class CharT>
void validate(boost::any& v, std::vector<std::basic_string<CharT>> const& s,
              std::filesystem::path* p, int)
{
    assert(s.size() == 1);
    std::basic_stringstream<CharT> ss;

    for (auto& el : s)
        ss << std::quoted(el);

    path converted;
    ss >> std::noskipws >> converted;

    if (ss.peek(); !ss.eof())
        throw std::runtime_error("excess path characters");

    v = std::move(converted);
}

这里我们根据需要平衡std::quotedquoting/escaping。

现场演示

概念验证:

Live On Coliru

#include <boost/program_options.hpp>
#include <filesystem>
#include <iostream>

namespace std::filesystem {
    template <class CharT>
    void validate(boost::any& v, std::vector<std::basic_string<CharT>> const& s,
                  std::filesystem::path* p, int)
    {
        assert(s.size() == 1);
        std::basic_stringstream<CharT> ss;

        for (auto& el : s)
            ss << std::quoted(el);

        path converted;
        ss >> std::noskipws >> converted;

        if (ss.peek(); !ss.eof())
            throw std::runtime_error("excess path characters");

        v = std::move(converted);
    }
}

namespace po = boost::program_options;

template <typename Path> static constexpr auto Type    = "[unknown]";
template <> constexpr auto Type<std::string>           = "std::string";
template <> constexpr auto Type<std::filesystem::path> = "fs::path";

template <typename Path>
bool do_test(int argc, char const* argv[]) try {
    Path optionsFile;

    po::options_description desc("Options");
    desc.add_options()            //
        ("help,h", "Help screen") //
        ("options,o", po::value<Path>(&optionsFile),
         "File containing the command and the arguments");

    po::command_line_parser parser{argc, argv};
    parser.options(desc).allow_unregistered().style(
            po::command_line_style::default_style |
            po::command_line_style::allow_slash_for_short);
    auto parsed_options = parser.run();

    po::variables_map vm;
    store(parsed_options, vm);
    notify(vm);

    if (vm.count("help")) {
        std::cout << desc << '\n';
        return true;
    }

    std::cout << "Using " << Type<Path> << "\toptionsFile = " << optionsFile << "\n";
    return true;
} catch (const std::exception& e) {
    std::cout << "Using " << Type<Path> << "\terror: " << e.what() << "\n";
    return false;
}

int main() {
    for (auto args : {
             std::vector{"Exepath", "-o", "c:\temp\options1.txt"},
             std::vector{"Exepath", "-o", "c:\temp\options 1.txt"},
         })
    {
        std::cout << "\n -- Input: ";
        for (auto& arg : args) {
            std::cout << " " << std::quoted(arg);
        }
        std::cout << "\n";
        int argc = args.size();
        args.push_back(nullptr);
        do_test<std::string>(argc, args.data());
        do_test<std::filesystem::path>(argc, args.data());
    }
} 

现在打印

 -- Input:  "Exepath" "-o" "c:\temp\options1.txt"
Using std::string   optionsFile = c:\temp\options1.txt
Using fs::path  optionsFile = "c:\temp\options1.txt"

 -- Input:  "Exepath" "-o" "c:\temp\options 1.txt"
Using std::string   optionsFile = c:\temp\options 1.txt
Using fs::path  optionsFile = "c:\temp\options 1.txt"

¹ 这实际上发生在来自 Boost Conversion

boost::lexical_cast 内部