问:当给定路径包含空格时,使用 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的内容为:
- argv[0] = Exepath
- argv[1] = -o
- argv[2] = c:\temp\options 1.txt
这是完整代码:
#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::path
的 operator>>
不遵守 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::quoted
quoting/escaping。
现场演示
概念验证:
#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
内部
我有一个使用 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的内容为:
- argv[0] = Exepath
- argv[1] = -o
- argv[2] = c:\temp\options 1.txt
这是完整代码:
#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::path
的 operator>>
不遵守 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::quoted
quoting/escaping。
现场演示
概念验证:
#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
内部