368 lines
16 KiB
C++
368 lines
16 KiB
C++
#include "prx/libc/include/FileStream.hpp"
|
|
#include "prx/libc/include/general/VabiMacros.hpp"
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
struct GuestOption {
|
|
const char* name;
|
|
int hasArgument;
|
|
int* flag;
|
|
int value;
|
|
};
|
|
|
|
extern "C" {
|
|
int APS5_VABI getopt_nid_postfix(int, char* const*, const char*);
|
|
int APS5_VABI getopt_long_nid_postfix(int, char* const*, const char*, const GuestOption*, int*);
|
|
int APS5_VABI setenv_nid_postfix(const char*, const char*, int);
|
|
int APS5_VABI unsetenv_nid_postfix(const char*);
|
|
extern int opterr_nid_postfix;
|
|
extern int optind_nid_postfix;
|
|
extern int optopt_nid_postfix;
|
|
extern int optreset_nid_postfix;
|
|
extern char* optarg_nid_postfix;
|
|
extern FileStream* __stderrp_nid_postfix;
|
|
}
|
|
|
|
namespace {
|
|
|
|
void Expect(bool value, const std::string& what) {
|
|
if (value) return;
|
|
std::fprintf(stderr, "failed: %s\n", what.c_str());
|
|
std::abort();
|
|
}
|
|
|
|
void ExpectText(const std::string& actual, const std::string& expected, const std::string& what) {
|
|
Expect(actual == expected, what + ": `" + actual + "' != `" + expected + "'");
|
|
}
|
|
|
|
class ArgumentVector {
|
|
std::vector<std::string> storage;
|
|
std::vector<char*> pointers;
|
|
|
|
public:
|
|
explicit ArgumentVector(const std::string& line) {
|
|
std::istringstream words(line);
|
|
for (std::string word; words >> word;) storage.push_back(word);
|
|
for (auto& word : storage) pointers.push_back(word.data());
|
|
pointers.push_back(nullptr);
|
|
}
|
|
int Count() const { return static_cast<int>(storage.size()); }
|
|
char* const* Values() { return pointers.data(); }
|
|
std::string Joined() const {
|
|
std::string result;
|
|
for (std::size_t i = 0; i + 1 < pointers.size(); ++i) result += (i ? " " : "") + std::string(pointers[i]);
|
|
return result;
|
|
}
|
|
};
|
|
|
|
class StderrCapture {
|
|
FileStream* previous;
|
|
FileStream stream;
|
|
|
|
public:
|
|
StderrCapture() : previous(__stderrp_nid_postfix), stream(std::tmpfile()) { __stderrp_nid_postfix = &stream; }
|
|
~StderrCapture() {
|
|
__stderrp_nid_postfix = previous;
|
|
stream.Close();
|
|
}
|
|
std::string Text() {
|
|
std::FILE* handle = stream.GetHandle();
|
|
std::fflush(handle);
|
|
std::rewind(handle);
|
|
std::string text;
|
|
for (int character; (character = std::fgetc(handle)) != EOF;) text.push_back(static_cast<char>(character));
|
|
return text;
|
|
}
|
|
};
|
|
|
|
void Reset() {
|
|
optind_nid_postfix = optreset_nid_postfix = 1;
|
|
opterr_nid_postfix = 1;
|
|
}
|
|
|
|
std::string Finish(std::string result, int count) {
|
|
if (result.empty()) result = "|";
|
|
else result.back() = '|';
|
|
return result + std::to_string(count - optind_nid_postfix);
|
|
}
|
|
|
|
std::string RunGetopt(const char* options, ArgumentVector& arguments) {
|
|
Reset();
|
|
std::string result;
|
|
for (int character; (character = getopt_nid_postfix(arguments.Count(), arguments.Values(), options)) != -1;) {
|
|
Expect(character != ':', "getopt returned `:'");
|
|
const char* found = std::strchr(options, character);
|
|
if (!found) result += "!" + std::string(1, static_cast<char>(character)) + ",";
|
|
else if (found[1] != ':') result += std::string(1, static_cast<char>(character)) + ",";
|
|
else result += std::string(1, static_cast<char>(character)) + "=" + optarg_nid_postfix + ",";
|
|
}
|
|
return Finish(result, arguments.Count());
|
|
}
|
|
|
|
std::string RunGetoptLong(const char* options, const GuestOption* longOptions, ArgumentVector& arguments) {
|
|
Reset();
|
|
std::string result;
|
|
int index = -2;
|
|
for (int character; (character = getopt_long_nid_postfix(arguments.Count(), arguments.Values(), options, longOptions, &index)) != -1;) {
|
|
Expect(character != ':', "getopt_long returned `:'");
|
|
if (index == -2) {
|
|
const char* found = std::strchr(options, character);
|
|
if (!found) result += "!" + std::string(1, static_cast<char>(character)) + ",";
|
|
else if (found[1] != ':') result += std::string(1, static_cast<char>(character)) + ",";
|
|
else result += std::string(1, static_cast<char>(character)) + "=" + optarg_nid_postfix + ",";
|
|
} else {
|
|
const GuestOption& option = longOptions[index];
|
|
result += "-" + std::string(option.name);
|
|
if (option.hasArgument == 1) result += "=" + std::string(optarg_nid_postfix);
|
|
if (option.hasArgument == 2 && optarg_nid_postfix) result += "=" + std::string(optarg_nid_postfix);
|
|
result += ",";
|
|
}
|
|
index = -2;
|
|
}
|
|
return Finish(result, arguments.Count());
|
|
}
|
|
|
|
struct GetoptCase {
|
|
const char* options;
|
|
const char* arguments;
|
|
const char* result;
|
|
};
|
|
|
|
void FreeBsdGetoptCases() {
|
|
const GetoptCase cases[] = {
|
|
{"c:d", "foo -c 1 -d foo", "c=1,d|1"},
|
|
{"c:d", "foo -d foo bar", "d|2"},
|
|
{"c:d", "foo -c 2 foo bar", "c=2|2"},
|
|
{"c:d", "foo -e 1 foo bar", "!?|3"},
|
|
{"c:d", "foo -d -- -c 1", "d|2"},
|
|
{"c:d", "foo -c- 1", "c=-|1"},
|
|
{"c:d", "foo -d - 1", "d|2"},
|
|
};
|
|
for (const auto& test : cases) {
|
|
ArgumentVector arguments(test.arguments);
|
|
StderrCapture capture;
|
|
ExpectText(RunGetopt(test.options, arguments), test.result, std::string("getopt ") + test.arguments);
|
|
const std::string expectedError = std::strcmp(test.result, "!?|3") == 0 ? "eboot.bin: illegal option -- e\n" : "";
|
|
ExpectText(capture.Text(), expectedError, std::string("getopt stderr ") + test.arguments);
|
|
}
|
|
}
|
|
|
|
void FreeBsdGetoptLongCases() {
|
|
const GuestOption gnu[] = {
|
|
{"required", 1, nullptr, 'r'},
|
|
{"optional", 2, nullptr, 'o'},
|
|
{"none", 0, nullptr, 'n'},
|
|
{"color", 0, nullptr, 'C'},
|
|
{"colour", 0, nullptr, 'C'},
|
|
{nullptr, 0, nullptr, 0},
|
|
};
|
|
const GetoptCase cases[] = {
|
|
{"abc:", "foo --req foobar", "-required=foobar|0"},
|
|
{"abc:", "foo --opt=bazbug", "-optional=bazbug|0"},
|
|
{"abc:", "foo --col", "-color|0"},
|
|
{"abc:", "foo --colour", "-colour|0"},
|
|
{"abc:", "foo -a -b -cfoobar --required foobar --optional=bazbug --none random --col --color --colour",
|
|
"a,b,c=foobar,-required=foobar,-optional=bazbug,-none,-color,-color,-colour|1"},
|
|
};
|
|
for (const auto& test : cases) {
|
|
ArgumentVector arguments(test.arguments);
|
|
StderrCapture capture;
|
|
ExpectText(RunGetoptLong(test.options, gnu, arguments), test.result, std::string("getopt_long ") + test.arguments);
|
|
ExpectText(capture.Text(), "", std::string("getopt_long stderr ") + test.arguments);
|
|
}
|
|
|
|
int flags[3] {};
|
|
const GuestOption lists[] = {
|
|
{"list-foobar", 0, &flags[0], 'f'},
|
|
{"list-goobar", 0, &flags[1], 'g'},
|
|
{"list", 0, &flags[2], 'l'},
|
|
{nullptr, 0, nullptr, 0},
|
|
};
|
|
ArgumentVector arguments("foo --list");
|
|
ExpectText(RunGetoptLong("fgl", lists, arguments), "-list|0", "exact match among prefixes");
|
|
Expect(flags[0] == 0 && flags[1] == 0 && flags[2] == 'l', "flag set by an exact match");
|
|
}
|
|
|
|
void ShortOptions() {
|
|
ArgumentVector grouped("foo -dcvalue -dd rest");
|
|
ExpectText(RunGetopt("c:d", grouped), "d,c=value,d,d|1", "grouped options");
|
|
|
|
ArgumentVector optional("foo -afoo -a bar");
|
|
Reset();
|
|
Expect(getopt_nid_postfix(optional.Count(), optional.Values(), "a::") == 'a' && std::strcmp(optarg_nid_postfix, "foo") == 0, "optional attached");
|
|
Expect(getopt_nid_postfix(optional.Count(), optional.Values(), "a::") == 'a' && optarg_nid_postfix == nullptr && optind_nid_postfix == 3, "optional absent");
|
|
Expect(getopt_nid_postfix(optional.Count(), optional.Values(), "a::") == -1 && optind_nid_postfix == 3, "optional stops at operand");
|
|
|
|
ArgumentVector dash("foo - -x");
|
|
Reset();
|
|
Expect(getopt_nid_postfix(dash.Count(), dash.Values(), "-x") == '-' && optind_nid_postfix == 2, "lone dash as option");
|
|
Expect(getopt_nid_postfix(dash.Count(), dash.Values(), "-x") == 'x' && optind_nid_postfix == 3, "after lone dash");
|
|
|
|
ArgumentVector unknown("foo -x");
|
|
{
|
|
StderrCapture capture;
|
|
Reset();
|
|
opterr_nid_postfix = 0;
|
|
Expect(getopt_nid_postfix(unknown.Count(), unknown.Values(), "ab") == '?', "unknown option returns ?");
|
|
Expect(optopt_nid_postfix == 'x' && optind_nid_postfix == 2, "optopt of unknown option");
|
|
ExpectText(capture.Text(), "", "opterr 0 is silent");
|
|
}
|
|
{
|
|
StderrCapture capture;
|
|
Reset();
|
|
Expect(getopt_nid_postfix(unknown.Count(), unknown.Values(), ":ab") == '?' && optopt_nid_postfix == 'x', "leading colon unknown");
|
|
ExpectText(capture.Text(), "", "leading colon is silent");
|
|
}
|
|
|
|
ArgumentVector missing("foo -c");
|
|
{
|
|
StderrCapture capture;
|
|
Reset();
|
|
Expect(getopt_nid_postfix(missing.Count(), missing.Values(), "c:") == '?' && optopt_nid_postfix == 'c' && optind_nid_postfix == 2, "missing argument");
|
|
ExpectText(capture.Text(), "eboot.bin: option requires an argument -- c\n", "missing argument message");
|
|
}
|
|
{
|
|
StderrCapture capture;
|
|
Reset();
|
|
Expect(getopt_nid_postfix(missing.Count(), missing.Values(), ":c:") == ':' && optopt_nid_postfix == 'c', "missing argument with leading colon");
|
|
ExpectText(capture.Text(), "", "leading colon missing argument is silent");
|
|
}
|
|
|
|
ArgumentVector first("foo -ab");
|
|
ArgumentVector second("bar -c");
|
|
Reset();
|
|
Expect(getopt_nid_postfix(first.Count(), first.Values(), "abc") == 'a', "first parse");
|
|
optind_nid_postfix = optreset_nid_postfix = 1;
|
|
Expect(getopt_nid_postfix(second.Count(), second.Values(), "abc") == 'c', "optreset restarts getopt");
|
|
Expect(getopt_nid_postfix(second.Count(), second.Values(), "abc") == -1 && optind_nid_postfix == 2, "restarted parse ends");
|
|
|
|
ArgumentVector permuted("foo a -b c");
|
|
ExpectText(RunGetopt("bc", permuted), "|3", "getopt does not permute");
|
|
}
|
|
|
|
void LongOptions() {
|
|
int flag = 0;
|
|
const GuestOption options[] = {
|
|
{"required", 1, nullptr, 'r'},
|
|
{"optional", 2, nullptr, 'o'},
|
|
{"none", 0, nullptr, 'n'},
|
|
{"verbose", 0, nullptr, 'v'},
|
|
{"version", 0, nullptr, 'V'},
|
|
{"flag", 0, &flag, 7},
|
|
{nullptr, 0, nullptr, 0},
|
|
};
|
|
|
|
ArgumentVector kinds("foo --required=1 --required 2 --optional=3 --optional 4 --none --flag");
|
|
ExpectText(RunGetoptLong("", options, kinds), "-required=1,-required=2,-optional=3,-optional,-none,-flag|1", "three argument kinds");
|
|
Expect(flag == 7, "flag pointer set");
|
|
ExpectText(kinds.Joined(), "foo --required=1 --required 2 --optional=3 --optional --none --flag 4", "operand permuted to the end");
|
|
|
|
ArgumentVector abbreviations("foo --verb --vers --req=x --opt");
|
|
ExpectText(RunGetoptLong("", options, abbreviations), "-verbose,-version,-required=x,-optional|0", "unique abbreviations");
|
|
|
|
struct ErrorCase {
|
|
const char* options;
|
|
const char* arguments;
|
|
int result;
|
|
int optopt;
|
|
const char* message;
|
|
};
|
|
const ErrorCase errors[] = {
|
|
{"", "foo --ver", '?', 0, "eboot.bin: option `--ver' is ambiguous\n"},
|
|
{"", "foo --none=1", '?', 'n', "eboot.bin: option `--none' doesn't allow an argument\n"},
|
|
{"", "foo --flag=1", '?', 0, "eboot.bin: option `--flag' doesn't allow an argument\n"},
|
|
{"", "foo --required", '?', 'r', "eboot.bin: option `--required' requires an argument\n"},
|
|
{":", "foo --required", ':', 'r', ""},
|
|
{"", "foo --nope=1", '?', 0, "eboot.bin: unrecognized option `--nope=1'\n"},
|
|
{"", "foo -x", '?', 'x', "eboot.bin: invalid option -- x\n"},
|
|
{"c:", "foo -c", '?', 'c', "eboot.bin: option requires an argument -- c\n"},
|
|
{":c:", "foo -c", ':', 'c', ""},
|
|
{"W;", "foo -W ver", '?', 0, "eboot.bin: option `-W ver' is ambiguous\n"},
|
|
{"W;", "foo -W", '?', 'W', "eboot.bin: option requires an argument -- W\n"},
|
|
};
|
|
for (const auto& test : errors) {
|
|
ArgumentVector arguments(test.arguments);
|
|
StderrCapture capture;
|
|
Reset();
|
|
optopt_nid_postfix = -1;
|
|
const int result = getopt_long_nid_postfix(arguments.Count(), arguments.Values(), test.options, options, nullptr);
|
|
Expect(result == test.result, std::string("getopt_long result ") + test.arguments);
|
|
Expect(optopt_nid_postfix == test.optopt, std::string("getopt_long optopt ") + test.arguments);
|
|
ExpectText(capture.Text(), test.message, std::string("getopt_long message ") + test.arguments);
|
|
}
|
|
{
|
|
ArgumentVector arguments("foo --ver -x");
|
|
StderrCapture capture;
|
|
Reset();
|
|
opterr_nid_postfix = 0;
|
|
Expect(getopt_long_nid_postfix(arguments.Count(), arguments.Values(), "", options, nullptr) == '?' && optopt_nid_postfix == 0, "silent ambiguity");
|
|
Expect(getopt_long_nid_postfix(arguments.Count(), arguments.Values(), "", options, nullptr) == '?' && optopt_nid_postfix == 'x', "silent unknown");
|
|
ExpectText(capture.Text(), "", "opterr 0 silences getopt_long");
|
|
}
|
|
|
|
ArgumentVector wOption("foo -W verbose -Wversion");
|
|
ExpectText(RunGetoptLong("W;", options, wOption), "-verbose,-version|0", "W; long options");
|
|
|
|
ArgumentVector terminated("foo a -b -- -d c");
|
|
ExpectText(RunGetoptLong("bd", options, terminated), "b|3", "-- ends getopt_long");
|
|
ExpectText(terminated.Joined(), "foo -b -- a -d c", "-- permutes skipped operands");
|
|
|
|
ArgumentVector interleaved("foo a -b c -d");
|
|
ExpectText(RunGetoptLong("bd", options, interleaved), "b,d|2", "permutation");
|
|
ExpectText(interleaved.Joined(), "foo -b -d a c", "permuted order");
|
|
|
|
ArgumentVector plus("foo a -b");
|
|
ExpectText(RunGetoptLong("+b", options, plus), "|2", "+ stops at the first operand");
|
|
|
|
ArgumentVector inOrder("foo a -b c");
|
|
Reset();
|
|
std::string sequence;
|
|
for (int character; (character = getopt_long_nid_postfix(inOrder.Count(), inOrder.Values(), "-b", options, nullptr)) != -1;)
|
|
sequence += character == 1 ? std::string("1:") + optarg_nid_postfix + "," : std::string(1, static_cast<char>(character)) + ",";
|
|
ExpectText(sequence, "1:a,b,1:c,", "- returns operands in order");
|
|
|
|
ArgumentVector restartFirst("foo -bd");
|
|
ArgumentVector restartSecond("bar -b");
|
|
Reset();
|
|
Expect(getopt_long_nid_postfix(restartFirst.Count(), restartFirst.Values(), "bd", options, nullptr) == 'b', "getopt_long first parse");
|
|
optind_nid_postfix = 0;
|
|
Expect(getopt_long_nid_postfix(restartSecond.Count(), restartSecond.Values(), "bd", options, nullptr) == 'b', "optind 0 restarts getopt_long");
|
|
Expect(getopt_long_nid_postfix(restartSecond.Count(), restartSecond.Values(), "bd", options, nullptr) == -1 && optind_nid_postfix == 2, "restarted getopt_long ends");
|
|
Reset();
|
|
Expect(getopt_long_nid_postfix(restartFirst.Count(), restartFirst.Values(), "bd", options, nullptr) == 'b', "getopt_long parse before optreset");
|
|
optind_nid_postfix = optreset_nid_postfix = 1;
|
|
Expect(getopt_long_nid_postfix(restartSecond.Count(), restartSecond.Values(), "bd", options, nullptr) == 'b', "optreset restarts getopt_long");
|
|
|
|
Expect(setenv_nid_postfix("POSIXLY_CORRECT", "1", 1) == 0, "setenv POSIXLY_CORRECT");
|
|
{
|
|
ArgumentVector posix("foo a -b");
|
|
ExpectText(RunGetoptLong("b", options, posix), "|2", "POSIXLY_CORRECT stops at the first operand");
|
|
ArgumentVector illegal("foo -x");
|
|
StderrCapture capture;
|
|
Reset();
|
|
Expect(getopt_long_nid_postfix(illegal.Count(), illegal.Values(), "b", options, nullptr) == '?', "POSIXLY_CORRECT unknown option");
|
|
ExpectText(capture.Text(), "eboot.bin: illegal option -- x\n", "POSIXLY_CORRECT message");
|
|
}
|
|
Expect(unsetenv_nid_postfix("POSIXLY_CORRECT") == 0, "unsetenv POSIXLY_CORRECT");
|
|
ArgumentVector gnu("foo a -b");
|
|
ExpectText(RunGetoptLong("b", options, gnu), "b|1", "permutation after POSIXLY_CORRECT is unset");
|
|
|
|
Reset();
|
|
Expect(getopt_long_nid_postfix(gnu.Count(), gnu.Values(), nullptr, options, nullptr) == -1, "null option string");
|
|
}
|
|
|
|
}
|
|
|
|
int main() {
|
|
Expect(opterr_nid_postfix == 1 && optind_nid_postfix == 1 && optopt_nid_postfix == 0 &&
|
|
optreset_nid_postfix == 0 && optarg_nid_postfix == nullptr, "initial values");
|
|
FreeBsdGetoptCases();
|
|
FreeBsdGetoptLongCases();
|
|
ShortOptions();
|
|
LongOptions();
|
|
}
|