Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ check: ## Static analysis
--check-level=exhaustive --project=$(BUILD_DIR)/compile_commands.json \
--suppress=missingIncludeSystem -i$(BUILD_DIR)

check-all: format lint check ## Run all checks
check-all: test format lint check ## Run all checks

fix: ## Fix code formatting and linting issues
@test -n "$(CLANG_FORMAT)" || { echo "error: clang-format not found"; exit 1; }
Expand Down
14 changes: 8 additions & 6 deletions include/tl_flag.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,12 @@ typedef struct {
/**
* @brief Parses the given command line arguments.
*
* Parses argv into flags. A flag is anything starting with "--". It can carry
* a value written as --name=value, or as --name value in the next entry.
* A bare "--" ends flag parsing; everything after it is a positional, even if
* it starts with dashes. Any previously parsed state is thrown away first.
* Parses argv into flags and positionals. A flag is anything starting
* with "-" or "--" (e.g. "-h", "--help"). It can carry a value written as
* --name=value, or as --name value in the next entry. A bare "-" is a
* positional. A bare "--" ends flag parsing; everything after it is a
* positional, even if it starts with dashes. Any previously parsed state
* is thrown away first.
*
* @param argc The number of command line arguments.
* @param argv The command line arguments.
Expand Down Expand Up @@ -124,8 +126,8 @@ const char *tl_get_flag_at(const char *flag, size_t index);
/**
* @brief Returns the number of positional arguments.
*
* Positionals are bare arguments (not starting with `--`) and everything
* after a bare `--` terminator, in the order they appeared.
* Positionals are bare arguments (not starting with `-` or `--`) and
* everything after a bare `--` terminator, in the order they appeared.
*
* @return The positional argument count.
*/
Expand Down
100 changes: 69 additions & 31 deletions src/tl_flag.c
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,39 @@ static char *line_buf = NULL;
static char **line_tokens = NULL;

/**
* @brief Returns whether the token is a long flag (starts with "--" and has content).
* @brief Returns whether the token is a flag.
*
* A flag starts with "-" or "--" and is not a bare "-" or "--".
* A bare "-" is not a flag (it's a common stdin placeholder).
* A bare "--" is the positional terminator and is handled separately.
*/
static bool is_long_flag(const char *s) {
return s != NULL && s[0] == '-' && s[1] == '-' && s[2] != '\0';
static bool is_flag(const char *s) {
if (s == NULL || s[0] != '-' || s[1] == '\0') {
return false;
}
if (s[1] == '-' && s[2] == '\0') {
return false;
}
return true;
}

/**
* @brief Returns whether the token is the bare "--" terminator.
*/
static bool is_dash_dash(const char *s) {
return s != NULL && s[0] == '-' && s[1] == '-' && s[2] == '\0';
if (s == NULL) {
return false;
}
if (s[0] != '-') {
return false;
}
if (s[1] != '-') {
return false;
}
if (s[2] != '\0') {
return false;
}
return true;
}

/**
Expand All @@ -40,7 +62,7 @@ static bool flag_matches(const tl_flag_t *f, const char *name, size_t name_len)
* @brief Fills the flag and positional tables from a token list.
*
* The first token is the program name and is skipped. The rest are
* sorted into flags (anything starting with "--") and positionals
* sorted into flags (anything starting with "-" or "--") and positionals
* (everything else, plus anything after a bare "--").
*/
static bool parse_tokens(char **tokens, int count) {
Expand Down Expand Up @@ -70,8 +92,8 @@ static bool parse_tokens(char **tokens, int count) {
after_dd = true;
continue;
}
// Long flag
if (is_long_flag(tok)) {
// Flag
if (is_flag(tok)) {
char *eq = strchr(tok, '=');
if (eq) {
flags[flag_count].name = tok;
Expand All @@ -81,7 +103,7 @@ static bool parse_tokens(char **tokens, int count) {
const char *value = NULL;
// Consume the next token as the value if it is not another flag
// and not the "--" terminator
if (i + 1 < count && !is_long_flag(tokens[i + 1]) && !is_dash_dash(tokens[i + 1])) {
if (i + 1 < count && !is_flag(tokens[i + 1]) && !is_dash_dash(tokens[i + 1])) {
value = tokens[i + 1];
i++;
}
Expand All @@ -98,6 +120,43 @@ static bool parse_tokens(char **tokens, int count) {
return true;
}

/**
* @brief Reads one token from `line` starting at `*i` into `line_buf` at `*bi`.
*
* Stops at unquoted whitespace or end of line. Writes the NUL terminator.
* Returns true on success, false if a quoted string was never closed.
*/
static bool read_one_token(const char *line, size_t len, size_t *i, size_t *bi) {
bool in_quote = false;
while (*i < len) {
char c = line[*i];
if (!in_quote && (c == ' ' || c == '\t')) {
break;
}
if (c == '"') {
if (in_quote) {
in_quote = false;
} else {
in_quote = true;
}
(*i)++;
continue;
}
if (c == '\\' && *i + 1 < len) {
line_buf[(*bi)++] = line[*i + 1];
*i += 2;
continue;
}
line_buf[(*bi)++] = c;
(*i)++;
}
if (in_quote) {
return false;
}
line_buf[(*bi)++] = '\0';
return true;
}

/**
* @brief Splits a command line string into tokens stored in line_tokens.
*
Expand Down Expand Up @@ -131,31 +190,10 @@ static int tokenize_line(const char *line) {
if (i >= len) {
break;
}
// Start a new token at the current buffer position
line_tokens[n++] = &line_buf[bi];
bool in_quote = false;
while (i < len) {
char c = line[i];
if (!in_quote && (c == ' ' || c == '\t')) {
break;
}
if (c == '"') {
in_quote = in_quote ? false : true;
i++;
continue;
}
if (c == '\\' && i + 1 < len) {
line_buf[bi++] = line[i + 1];
i += 2;
continue;
}
line_buf[bi++] = c;
i++;
}
if (in_quote) {
return -1; // unterminated quoted string
if (!read_one_token(line, len, &i, &bi)) {
return -1;
}
line_buf[bi++] = '\0';
}
return (int)n;
}
Expand Down
Loading