blob: 27ca751f07972b20d549617ef4cf5ebd8a51de22 [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/run_cli.h"
#include <cstdio>
#include <set>
#include "cli/cli_args.h"
#include "cli/exit_codes.h"
#include "commands/commands.h"
#include "format/output_format.h"
#include "reader/tsfile_reader.h"
#ifdef _WIN32
#include <io.h>
#define TSFILE_ISATTY _isatty
#define TSFILE_FILENO _fileno
#else
#include <unistd.h>
#define TSFILE_ISATTY isatty
#define TSFILE_FILENO fileno
#endif
#ifndef TSFILE_CLI_VERSION
#define TSFILE_CLI_VERSION "unknown"
#endif
namespace tsfile_cli {
namespace {
void print_usage(std::ostream& os) {
os << "Usage: tsfile-cli <command> [options] <file.tsfile>\n"
"Commands:\n"
" ls list devices (tree) or tables (table)\n"
" schema per-measurement data type/encoding/compression\n"
" meta file metadata summary\n"
" stats per-series count, time range, "
"min/max/first/last/sum\n"
" head first N rows (use -n)\n"
" cat all rows of a device/table\n"
" count number of rows (per series, plus a total)\n"
" sample deterministic sample rows (use -n and --seed)\n"
" write import CSV/TSV rows into a new table tsfile "
"(--table, --columns, -o)\n"
"Options:\n"
" -f, --format csv|tsv|json|table output format "
"(default: table on a TTY, tsv when piped)\n"
" -d, --device <name> restrict to one device (tree model)\n"
" -t, --table <name> restrict to one table (table model)\n"
" -m, --measurements a,b project only these measurements\n"
" -n, --limit N max rows (head/cat/sample)\n"
" --offset N skip N rows before emitting\n"
" --start <ms> inclusive lower time bound\n"
" --end <ms> inclusive upper time bound\n"
" --seed N RNG seed for sample\n"
" --tag-filter C OP V table TAG predicate; OP is "
"eq|neq|lt|lteq|gt|gteq|regexp|not-regexp\n"
" --tag-between C L U table TAG predicate: L <= C <= U\n"
" --tag-not-between C L U table TAG predicate outside [L,U]\n"
" --no-header omit the header row\n"
" --model tree|table force the data model (else auto)\n"
" -h, --help print this help\n"
" --version print version\n"
"Write options:\n"
" --table <name> target table name (required)\n"
" --columns SPEC column spec name:TYPE:tag|field,... "
"(required)\n"
" -o, --output <file> destination .tsfile (required)\n"
" --header-match require the input header to match "
"--columns\n"
" -v, --verbose report rows written to stderr\n";
}
bool is_known_command(const std::string& c) {
static const std::set<std::string> kCmds = {"ls", "schema", "meta",
"stats", "head", "cat",
"count", "sample", "write"};
return kCmds.find(c) != kCmds.end();
}
bool validate_command_flags(const ParsedArgs& p, std::ostream& err) {
if (p.has_seed && p.command != "sample") {
err << "Error: --seed is only valid for sample\n";
return false;
}
if (p.command == "sample" && p.offset != 0) {
err << "Error: --offset is not valid for sample\n";
return false;
}
if (!p.device.empty() && !p.table.empty()) {
err << "Error: -d/--device and -t/--table cannot be used together\n";
return false;
}
if (p.limit < -1) {
err << "Error: -n/--limit must be >= -1\n";
return false;
}
if (p.offset < 0) {
err << "Error: --offset must be >= 0\n";
return false;
}
if (p.has_start && p.has_end && p.start > p.end) {
err << "Error: --start must be <= --end\n";
return false;
}
return true;
}
bool validate_write_flags(const ParsedArgs& p, std::ostream& err) {
if (p.table.empty()) {
err << "Error: write requires -t/--table\n";
return false;
}
if (p.columns.empty()) {
err << "Error: write requires --columns\n";
return false;
}
if (p.output.empty()) {
err << "Error: write requires -o/--output\n";
return false;
}
if (p.format == ParsedArgs::Format::kJson ||
p.format == ParsedArgs::Format::kTable) {
err << "Error: write input format must be csv or tsv\n";
return false;
}
if (p.no_header && p.header_match) {
err << "Error: --header-match cannot be combined with --no-header\n";
return false;
}
if (p.has_tag_filter) {
err << "Error: tag filter flags are not valid for write\n";
return false;
}
// Name the offending flag so the user does not have to guess which of
// the read-only options triggered the rejection.
if (!p.measurements.empty()) {
err << "Error: -m/--measurements is not valid for write\n";
return false;
}
if (!p.device.empty()) {
err << "Error: -d/--device is not valid for write\n";
return false;
}
if (p.has_start || p.has_end) {
err << "Error: --start/--end are not valid for write\n";
return false;
}
if (p.has_seed) {
err << "Error: --seed is not valid for write\n";
return false;
}
if (p.limit != -1) {
err << "Error: -n/--limit is not valid for write\n";
return false;
}
if (p.offset != 0) {
err << "Error: --offset is not valid for write\n";
return false;
}
if (!p.model.empty()) {
err << "Error: --model is not valid for write\n";
return false;
}
return true;
}
// Reject flags that have no effect for the given read command, instead of
// silently ignoring them, so misuse is caught rather than producing surprising
// output. Only called for non-write commands; write has its own validation.
bool validate_read_flag_applicability(const ParsedArgs& p, std::ostream& err) {
const std::string& c = p.command;
const bool is_row = (c == "head" || c == "cat" || c == "sample");
const bool scoped =
is_row || c == "schema" || c == "stats" || c == "count";
if (!p.output.empty()) {
err << "Error: -o/--output is only valid for write\n";
return false;
}
if (!p.columns.empty()) {
err << "Error: --columns is only valid for write\n";
return false;
}
if (p.header_match) {
err << "Error: --header-match is only valid for write\n";
return false;
}
if (p.verbose) {
err << "Error: -v/--verbose is only valid for write\n";
return false;
}
if (!is_row && p.limit != -1) {
err << "Error: -n/--limit is only valid for head/cat/sample\n";
return false;
}
if (!is_row && p.offset != 0) {
err << "Error: --offset is only valid for head/cat\n";
return false;
}
if (!is_row && (p.has_start || p.has_end)) {
err << "Error: --start/--end are only valid for head/cat/sample\n";
return false;
}
if (p.has_tag_filter && !is_row) {
err << "Error: tag filter flags are only valid for head/cat/sample\n";
return false;
}
if (p.has_tag_filter && p.model == "tree") {
err << "Error: tag filter flags are only valid for table model\n";
return false;
}
if (p.has_tag_filter && !p.device.empty()) {
err << "Error: tag filter flags cannot be combined with -d/--device\n";
return false;
}
if (!scoped && !p.device.empty()) {
err << "Error: -d/--device is not valid for " << c << "\n";
return false;
}
if (!scoped && !p.table.empty()) {
err << "Error: -t/--table is not valid for " << c << "\n";
return false;
}
if (!scoped && !p.measurements.empty()) {
err << "Error: -m/--measurements is not valid for " << c << "\n";
return false;
}
return true;
}
} // namespace
int run_cli(const std::vector<std::string>& args, std::ostream& out,
std::ostream& err) {
ParsedArgs p = parse_args(args);
if (p.version) {
out << "tsfile-cli (Apache TsFile C++) " << TSFILE_CLI_VERSION << "\n";
return kExitOk;
}
if (args.empty()) {
print_usage(err);
return kExitUsage;
}
if (p.command == "help" || p.command == "--help" || p.command == "-h" ||
p.help) {
print_usage(out);
return kExitOk;
}
if (!p.error.empty()) {
err << "Error: " << p.error << "\n";
print_usage(err);
return kExitUsage;
}
if (!is_known_command(p.command)) {
err << "Unknown command: " << p.command << "\n";
print_usage(err);
return kExitUsage;
}
if (p.command != "write" && p.file.empty()) {
err << "Error: missing <file.tsfile> argument\n";
return kExitUsage;
}
if (!validate_command_flags(p, err)) {
print_usage(err);
return kExitUsage;
}
if (p.command == "write") {
if (!validate_write_flags(p, err)) {
print_usage(err);
return kExitUsage;
}
storage::libtsfile_init();
return cmd_write(p, out, err);
}
if (!validate_read_flag_applicability(p, err)) {
print_usage(err);
return kExitUsage;
}
storage::libtsfile_init();
storage::TsFileReader reader;
int open_ret = reader.open(p.file);
if (open_ret != 0) {
err << "Error: cannot open " << p.file << ": "
<< error_code_message(open_ret) << " (code " << open_ret << ")\n";
return kExitFile;
}
// head/cat/sample/schema dispatch on the data model and would silently
// ignore the scope flag of the other model; reject that instead.
if (p.command == "head" || p.command == "cat" || p.command == "sample" ||
p.command == "schema") {
const bool table_model = is_table_model(p, reader);
if (table_model && !p.device.empty()) {
err << "Error: -d/--device does not apply to the table model; "
"use -t/--table (or force --model tree)\n";
reader.close();
return kExitUsage;
}
if (!table_model && !p.table.empty()) {
err << "Error: -t/--table does not apply to the tree model; "
"use -d/--device (or force --model table)\n";
reader.close();
return kExitUsage;
}
}
bool stdout_tty = TSFILE_ISATTY(TSFILE_FILENO(stdout)) != 0;
OutputFormat fmt = resolve_format(p.format, stdout_tty);
int code;
if (p.command == "ls") {
code = cmd_ls(p, reader, fmt, out, err);
} else if (p.command == "schema") {
code = cmd_schema(p, reader, fmt, out, err);
} else if (p.command == "meta") {
code = cmd_meta(p, reader, fmt, out, err);
} else if (p.command == "stats") {
code = cmd_stats(p, reader, fmt, out, err);
} else if (p.command == "head") {
code = cmd_head(p, reader, fmt, out, err);
} else if (p.command == "cat") {
code = cmd_cat(p, reader, fmt, out, err);
} else if (p.command == "count") {
code = cmd_count(p, reader, fmt, out, err);
} else if (p.command == "sample") {
code = cmd_sample(p, reader, fmt, out, err);
} else {
err << "Unknown command: " << p.command << "\n";
code = kExitUsage;
}
reader.close();
return code;
}
} // namespace tsfile_cli