blob: 8215b2973eb11220e36b66a85ad94a2d7bc7fdba [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* License); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
#include "cli/cli_args.h"
#include <algorithm>
#include <cerrno>
#include <cstdlib>
namespace tsfile_cli {
namespace {
bool has_strict_decimal_body(const std::string& s, size_t start) {
if (start >= s.size()) {
return false;
}
if (s[start] == '0' && start + 1 != s.size()) {
return false;
}
for (size_t i = start; i < s.size(); ++i) {
if (s[i] < '0' || s[i] > '9') {
return false;
}
}
return true;
}
bool parse_strict_i64(const std::string& s, long long& out) {
if (s.empty()) {
return false;
}
size_t start = (s[0] == '-') ? 1 : 0;
if (s[0] == '-' && start + 1 == s.size() && s[start] == '0') {
return false;
}
if (!has_strict_decimal_body(s, start)) {
return false;
}
char* endp = nullptr;
errno = 0;
long long v = std::strtoll(s.c_str(), &endp, 10);
if (endp == nullptr || *endp != '\0' || errno == ERANGE) {
return false;
}
out = v;
return true;
}
bool parse_strict_non_negative(const std::string& s, long long& out) {
if (s.empty()) {
return false;
}
if (!has_strict_decimal_body(s, 0)) {
return false;
}
char* endp = nullptr;
errno = 0;
long long v = std::strtoll(s.c_str(), &endp, 10);
if (endp == nullptr || *endp != '\0' || errno == ERANGE || v < 0) {
return false;
}
out = v;
return true;
}
bool parse_format(const std::string& s, ParsedArgs::Format& out) {
if (s == "csv") {
out = ParsedArgs::Format::kCsv;
} else if (s == "ndjson") {
out = ParsedArgs::Format::kJson;
} else if (s == "table") {
out = ParsedArgs::Format::kTable;
} else {
return false;
}
return true;
}
bool parse_tag_filter_op(const std::string& s, ParsedArgs::TagFilterOp& out) {
if (s == "eq") {
out = ParsedArgs::TagFilterOp::kEq;
} else if (s == "neq") {
out = ParsedArgs::TagFilterOp::kNeq;
} else if (s == "regexp") {
out = ParsedArgs::TagFilterOp::kRegexp;
} else if (s == "is-null") {
out = ParsedArgs::TagFilterOp::kIsNull;
} else if (s == "not-null") {
out = ParsedArgs::TagFilterOp::kNotNull;
} else {
return false;
}
return true;
}
bool tag_filter_op_needs_value(ParsedArgs::TagFilterOp op) {
return op == ParsedArgs::TagFilterOp::kEq ||
op == ParsedArgs::TagFilterOp::kNeq ||
op == ParsedArgs::TagFilterOp::kRegexp;
}
} // namespace
ParsedArgs parse_args(const std::vector<std::string>& args) {
ParsedArgs p;
if (args.empty()) {
return p;
}
p.command = args[0];
if (p.command == "--version") {
p.version = true;
}
if (p.command == "--help" || p.command == "-h") {
p.help = true;
}
// The subcommand must come first. A leading option means it was omitted;
// say so explicitly instead of failing later with a confusing message about
// the first real positional argument.
if (p.command.size() > 1 && p.command[0] == '-' && !p.version && !p.help) {
p.error = "the command must come before options (got option '" +
p.command + "'); run with --help for usage";
return p;
}
size_t i = 1;
auto need_value = [&](const std::string& flag, std::string& dst) -> bool {
if (i + 1 >= args.size()) {
p.error = "Missing value for " + flag;
return false;
}
dst = args[++i];
return true;
};
auto append_column = [&](const std::string& name, const std::string& type,
bool is_tag) {
p.columns.emplace_back(name, type, is_tag);
};
bool positional_file_set = false;
bool limit_set = false;
bool offset_set = false;
bool output_set = false;
bool output_dir_set = false;
bool force_set = false;
for (; i < args.size(); ++i) {
const std::string& a = args[i];
std::string val;
if (positional_file_set) {
p.error = "Unexpected argument after file: " + a;
return p;
}
if (a == "--") {
if (i + 1 >= args.size()) {
p.error = "Missing value after --";
return p;
}
if (!p.file.empty() || i + 2 != args.size()) {
p.error = "Unexpected argument after file: " + args[i + 1];
return p;
}
p.file = args[++i];
positional_file_set = true;
continue;
}
if (a == "-f" || a == "--format") {
if (p.format_set) {
p.error = "--format specified more than once";
return p;
}
if (!need_value(a, val)) {
return p;
}
if (!parse_format(val, p.format)) {
p.error = "unsupported format '" + val +
"'; expected table, ndjson, or csv";
return p;
}
p.format_set = true;
} else if (a == "-d" || a == "--device") {
if (!need_value(a, val)) {
return p;
}
p.device = val;
p.devices.push_back(val);
} else if (a == "-t" || a == "--table") {
if (!need_value(a, val)) {
return p;
}
p.table = val;
p.tables.push_back(val);
} else if (a == "-m" || a == "--measurements") {
if (!need_value(a, val)) {
return p;
}
if (val.find(',') != std::string::npos) {
p.error =
"--measurements accepts one column per option; repeat -m";
return p;
}
if (std::find(p.measurements.begin(), p.measurements.end(), val) !=
p.measurements.end()) {
p.error = "measurement '" + val + "' specified more than once";
return p;
}
p.measurements.push_back(val);
} else if (a == "-n" || a == "--limit") {
if (limit_set) {
p.error = "--limit specified more than once";
return p;
}
if (!need_value(a, val)) {
return p;
}
if (!parse_strict_non_negative(val, p.limit)) {
p.error = "Invalid -n/--limit: " + val;
return p;
}
limit_set = true;
} else if (a == "--offset") {
if (offset_set) {
p.error = "--offset specified more than once";
return p;
}
if (!need_value(a, val)) {
return p;
}
if (!parse_strict_non_negative(val, p.offset)) {
p.error = "Invalid --offset: " + val;
return p;
}
offset_set = true;
} else if (a == "--start") {
if (p.has_start) {
p.error = "--start specified more than once";
return p;
}
if (!need_value(a, val)) {
return p;
}
if (!parse_strict_i64(val, p.start)) {
p.error = "Invalid --start: " + val;
return p;
}
p.has_start = true;
} else if (a == "--end") {
if (p.has_end) {
p.error = "--end specified more than once";
return p;
}
if (!need_value(a, val)) {
return p;
}
if (!parse_strict_i64(val, p.end)) {
p.error = "Invalid --end: " + val;
return p;
}
p.has_end = true;
} else if (a == "--seed") {
if (!need_value(a, val)) {
return p;
}
if (!parse_strict_i64(val, p.seed)) {
p.error = "Invalid --seed: " + val;
return p;
}
p.has_seed = true;
} else if (a == "--type") {
if (p.export_format_set) {
p.error = "--type specified more than once";
return p;
}
if (!need_value(a, val)) {
return p;
}
if (!parse_format(val, p.export_format)) {
p.error = "unsupported export type '" + val +
"'; expected table, ndjson, or csv";
return p;
}
p.export_format_set = true;
} else if (a == "-o" || a == "--output") {
if (output_set) {
p.error = "--output specified more than once";
return p;
}
if (!need_value(a, p.output)) {
return p;
}
output_set = true;
} else if (a == "--output-dir") {
if (output_dir_set) {
p.error = "--output-dir specified more than once";
return p;
}
if (!need_value(a, p.output_dir)) {
return p;
}
output_dir_set = true;
} else if (a == "--columns") {
p.error = "Unknown flag: --columns";
return p;
} else if (a == "--field" || a == "--tag") {
if (i + 2 >= args.size()) {
p.error = "Missing value for " + a;
return p;
}
std::string name = args[++i];
std::string type = args[++i];
append_column(name, type, a == "--tag");
} else if (a == "-i" || a == "--input") {
if (p.input_set) {
p.error = "choose exactly one of --input or --stdin";
return p;
}
if (!need_value(a, p.file)) {
return p;
}
p.input_set = true;
} else if (a == "--stdin") {
if (p.input_set) {
p.error = "choose exactly one of --input or --stdin";
return p;
}
p.file = "-";
p.input_set = true;
} else if (a == "--encoding" || a == "--compression") {
if (i + 2 >= args.size()) {
p.error = "Missing value for " + a;
return p;
}
ParsedArgs::PhysicalOverride override;
override.kind =
a == "--encoding"
? ParsedArgs::PhysicalOverride::Kind::kEncoding
: ParsedArgs::PhysicalOverride::Kind::kCompression;
override.data_type = args[++i];
override.value = args[++i];
p.physical_overrides.push_back(override);
} else if (a == "--force") {
if (force_set) {
p.error = "--force specified more than once";
return p;
}
p.force = true;
force_set = true;
} else if (a == "-v" || a == "--verbose") {
p.verbose = true;
} else if (a == "--header-match") {
p.header_match = true;
} else if (a == "--tag-filter") {
if (i + 2 >= args.size()) {
p.error = "Missing value for " + a;
return p;
}
ParsedArgs::TagFilterSpec spec;
spec.column = args[++i];
std::string op = args[++i];
if (!parse_tag_filter_op(op, spec.op)) {
p.error = "Invalid --tag-filter operator: " + op +
" (use eq|neq|regexp|is-null|not-null)";
return p;
}
if (tag_filter_op_needs_value(spec.op)) {
if (i + 1 >= args.size()) {
p.error = "Missing value for " + a;
return p;
}
spec.value = args[++i];
}
p.has_tag_filter = true;
p.tag_filters.push_back(spec);
} else if (a == "--tag-match") {
if (!need_value(a, val)) {
return p;
}
if (val != "all" && val != "any") {
p.error = "Invalid --tag-match: " + val + " (use all or any)";
return p;
}
if (!p.tag_match.empty()) {
p.error = "--tag-match specified more than once";
return p;
}
p.tag_match = val;
} else if (a == "--model") {
p.error = "Unknown flag: --model";
return p;
} else if (a == "--no-header") {
p.no_header = true;
} else if (a == "-h" || a == "--help") {
if (i != 1 || args.size() != 2) {
p.error =
"--help must appear by itself or immediately after a "
"command";
return p;
}
p.help = true;
return p;
} else if (a == "--version") {
p.error = "--version must appear by itself";
return p;
} else if (a.size() > 1 && a[0] == '-') {
p.error = "Unknown flag: " + a;
return p;
} else {
if (p.file.empty()) {
p.file = a;
positional_file_set = true;
} else {
p.error = "Unexpected argument: " + a;
return p;
}
}
}
return p;
}
} // namespace tsfile_cli