diff --git a/args.hxx b/args.hxx index 85671f9..d1bd637 100644 --- a/args.hxx +++ b/args.hxx @@ -1499,6 +1499,12 @@ namespace args if (!failed) { std::istringstream ss(raw); + // Use the C locale so that the cword index parses + // consistently regardless of any std::locale::global call + // elsewhere in the process. A locale with a non-empty + // grouping facet would otherwise reject digit-only inputs + // like "12" when grouping rules expect separators. + ss.imbue(std::locale::classic()); ss >> parsed; if (ss.fail()) { @@ -3820,6 +3826,12 @@ namespace args ParseNumericValue(const std::string &value, T &destination) { std::istringstream ss(value); + // Pin parsing to the C locale so that the decimal separator and + // thousands grouping behavior do not silently depend on whatever + // std::locale::global was last set to elsewhere in the process. + // Without this, e.g. "3.14" parses as 3 (with ".14" trailing) in + // any locale whose numpunct facet treats ',' as the decimal point. + ss.imbue(std::locale::classic()); ss >> destination; if (ss.fail()) { diff --git a/test/locale_independent_parsing.cxx b/test/locale_independent_parsing.cxx new file mode 100644 index 0000000..0b089d1 --- /dev/null +++ b/test/locale_independent_parsing.cxx @@ -0,0 +1,71 @@ +#include "test_common.hxx" +#include +#include "test_helpers.hxx" + +#include + +// Custom numpunct facet that uses ',' as the decimal point and ';' as +// the thousands separator. Installing this in the global C++ locale +// reproduces what happens on systems where std::locale::global has been +// set to a locale with non-"C" numeric formatting (e.g. de_DE), without +// requiring any specific system locale to be installed on the test host. +struct CommaDecimalPunct : std::numpunct +{ +protected: + char do_decimal_point() const override { return ','; } + char do_thousands_sep() const override { return ';'; } + std::string do_grouping() const override { return "\3"; } +}; + +int main() +{ + // Save the original global locale so the test does not leak state. + const std::locale original_global = std::locale(); + + // Install an anonymous global locale derived from the classic locale, + // overriding numpunct only. This anonymous-locale form does not call + // setlocale() under the hood, so it cannot perturb anything outside + // the C++ locale machinery. + std::locale::global(std::locale(std::locale::classic(), new CommaDecimalPunct)); + + // Regardless of the global locale's decimal point, command-line + // numeric arguments must keep using '.' as the decimal separator. + // Before the fix, this would parse "3.14" as 3 (with ".14" trailing) + // and either silently truncate or be rejected. + { + args::ArgumentParser parser("locale-independent parsing test"); + args::ValueFlag rate(parser, "RATE", "rate", {'r', "rate"}); + + std::vector a{"--rate", "3.14"}; + parser.ParseArgs(a); + const double v = args::get(rate); + test::require(v > 3.139 && v < 3.141); + } + + // The '.'-as-decimal contract also holds for float. + { + args::ArgumentParser parser("locale-independent parsing test"); + args::ValueFlag rate(parser, "RATE", "rate", {'r', "rate"}); + + std::vector a{"--rate", "2.5"}; + parser.ParseArgs(a); + const float v = args::get(rate); + test::require(v > 2.49f && v < 2.51f); + } + + // The ',' character is not a decimal separator on the command line, + // so it must be rejected as trailing garbage regardless of locale. + test::require_throws_as([] { + args::ArgumentParser parser("locale-independent parsing test"); + args::ValueFlag rate(parser, "RATE", "rate", {'r', "rate"}); + std::vector a{"--rate", "3,14"}; + parser.ParseArgs(a); + }); + + // Restore the original global locale so we do not perturb later + // tests sharing the process (defensive; tests are independent + // executables under CTest but this is cheap and safe). + std::locale::global(original_global); + + return 0; +}