feat(argtype): :alone - the value of an option written without one
An optional argument has three values: the default (the name is absent), the argument type's :alone value (the name is written alone), and a written value. :alone is declared by the argument type only, never by a klammer's parameter declaration -- a default is what one klammer means by silence, but a bare option name must read the same way in every klammer. The bool type declares :alone true, which is the whole of the convention that a bare boolean option means true; there is no boolean special case in the engine. A type whose pattern matches running text cannot declare :alone, since an option's value runs to the next bar or option name and would swallow the following text. kdesc and `ktext -m` now show [default: X] and [alone: Y] per argument type. In the Standard Klammer Set: @code :number becomes a bool (it was an untyped string tested only for truthiness, so a bare :number was a no-op); decimal_mark declares :alone comma; table_hline and table_vline declare :alone all. The 35 bools that default false gained the bare form for free. Tests: tst/alone_test.sh (21 cases) joins the shipped suite. (from dev 6024f49c2859)
This commit is contained in:
@@ -41,7 +41,7 @@ are regenerated on each release — patches cannot be merged directly.
|
|||||||
Report problems (or send patches) to the author; accepted changes are
|
Report problems (or send patches) to the author; accepted changes are
|
||||||
applied to the development tree and appear in a following snapshot.
|
applied to the development tree and appear in a following snapshot.
|
||||||
|
|
||||||
This snapshot was assembled from development commit `bc7cd62b6f68`.
|
This snapshot was assembled from development commit `6024f49c2859`.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -7,13 +7,15 @@
|
|||||||
#include "util.h"
|
#include "util.h"
|
||||||
|
|
||||||
Argtype::Argtype(std::string name, std::string desc, std::string symbolic_pattern, std::string pattern,
|
Argtype::Argtype(std::string name, std::string desc, std::string symbolic_pattern, std::string pattern,
|
||||||
std::string default_value, std::string python_cast, modify_string_f python_format,
|
std::string default_value, std::string alone_value,
|
||||||
|
std::string python_cast, modify_string_f python_format,
|
||||||
const Locator& loc)
|
const Locator& loc)
|
||||||
: m_name(name)
|
: m_name(name)
|
||||||
, m_desc(desc)
|
, m_desc(desc)
|
||||||
, m_symbolic_pattern(symbolic_pattern)
|
, m_symbolic_pattern(symbolic_pattern)
|
||||||
, m_pattern(pattern)
|
, m_pattern(pattern)
|
||||||
, m_default(default_value)
|
, m_default(default_value)
|
||||||
|
, m_alone(alone_value)
|
||||||
, m_python_cast(python_cast)
|
, m_python_cast(python_cast)
|
||||||
, m_python_format(python_format)
|
, m_python_format(python_format)
|
||||||
, m_regex(std::regex(pattern))
|
, m_regex(std::regex(pattern))
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ public:
|
|||||||
{};
|
{};
|
||||||
|
|
||||||
Argtype(std::string name, std::string desc, std::string symbolic_pattern, std::string pattern,
|
Argtype(std::string name, std::string desc, std::string symbolic_pattern, std::string pattern,
|
||||||
std::string default_value, std::string python_cast, modify_string_f python_format,
|
std::string default_value, std::string alone_value,
|
||||||
|
std::string python_cast, modify_string_f python_format,
|
||||||
const Locator& loc);
|
const Locator& loc);
|
||||||
|
|
||||||
std::string python_value(const std::string& var_name, std::vector<std::string> value, size_t name_size);
|
std::string python_value(const std::string& var_name, std::vector<std::string> value, size_t name_size);
|
||||||
@@ -48,6 +49,18 @@ public:
|
|||||||
// (cell_hpos, column_width); general types (bool, float) have no
|
// (cell_hpos, column_width); general types (bool, float) have no
|
||||||
// sensible universal default and leave it empty.
|
// sensible universal default and leave it empty.
|
||||||
std::string m_default {};
|
std::string m_default {};
|
||||||
|
// Value for an optional argument whose name is written alone, with no
|
||||||
|
// value after it (:number rather than :number 10). Declared by the
|
||||||
|
// argument type, never by a klammer's parameter declaration: the
|
||||||
|
// default is what a klammer means by silence and is properly
|
||||||
|
// per-klammer, but a bare option name must read the same way in every
|
||||||
|
// klammer or the writer cannot know what it means without consulting
|
||||||
|
// each signature. bool declares "true", which is where the
|
||||||
|
// presence-means-true convention comes from; a type whose pattern
|
||||||
|
// matches everything cannot declare it (the value would swallow the
|
||||||
|
// following text instead). Empty means the type has no alone value,
|
||||||
|
// and a bare option name yields the empty string as before.
|
||||||
|
std::string m_alone {};
|
||||||
// Type parameter for parameterized types like rest(2): the value N
|
// Type parameter for parameterized types like rest(2): the value N
|
||||||
// is bound around the python cast as (lambda N: <cast>)(2)(...).
|
// is bound around the python cast as (lambda N: <cast>)(2)(...).
|
||||||
// Empty means unparameterized; a type with a default parameter
|
// Empty means unparameterized; a type with a default parameter
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
Parameter_set& Argtype_set::parameters()
|
Parameter_set& Argtype_set::parameters()
|
||||||
{
|
{
|
||||||
static Parameter_set instance("name | desc :pattern .* :python_cast str :default");
|
static Parameter_set instance("name | desc :pattern .* :python_cast str :default :alone");
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,11 +22,15 @@ Argtype_set::Argtype_set()
|
|||||||
(void)(void)K::log(2);
|
(void)(void)K::log(2);
|
||||||
Locator loc = current_locator();
|
Locator loc = current_locator();
|
||||||
for (auto [name, desc, pattern, python_cast, python_format] : base_argtypes) {
|
for (auto [name, desc, pattern, python_cast, python_format] : base_argtypes) {
|
||||||
add(name, desc, pattern, "", python_cast, python_format, loc);
|
add(name, desc, pattern, "", "", python_cast, python_format, loc);
|
||||||
}
|
}
|
||||||
// rest is a parameterized type (rest(N)); an unparameterized use is
|
// rest is a parameterized type (rest(N)); an unparameterized use is
|
||||||
// one-dimensional.
|
// one-dimensional.
|
||||||
m_types["rest"].m_parameter = "1";
|
m_types["rest"].m_parameter = "1";
|
||||||
|
// A bool option written alone is true: :number means :number true.
|
||||||
|
// This is a declaration like any other type's :alone, not an engine
|
||||||
|
// special case for booleans.
|
||||||
|
m_types["bool"].m_alone = "true";
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string Argtype_set::replace_symbols(const std::string& pattern, const Locator& loc)
|
std::string Argtype_set::replace_symbols(const std::string& pattern, const Locator& loc)
|
||||||
@@ -63,6 +67,7 @@ void Argtype_set::check_for_existing_definition(
|
|||||||
|
|
||||||
void Argtype_set::add(const std::string& name, const std::string& desc,
|
void Argtype_set::add(const std::string& name, const std::string& desc,
|
||||||
const std::string& pattern, const std::string& default_value,
|
const std::string& pattern, const std::string& default_value,
|
||||||
|
const std::string& alone_value,
|
||||||
const std::string& python_cast, modify_string_f python_format,
|
const std::string& python_cast, modify_string_f python_format,
|
||||||
const Locator& loc)
|
const Locator& loc)
|
||||||
{
|
{
|
||||||
@@ -73,7 +78,8 @@ void Argtype_set::add(const std::string& name, const std::string& desc,
|
|||||||
m_pattern_size = std::max(m_pattern_size, expanded_pattern.size());
|
m_pattern_size = std::max(m_pattern_size, expanded_pattern.size());
|
||||||
try {
|
try {
|
||||||
m_types[name] = Argtype(name, desc, pattern, expanded_pattern,
|
m_types[name] = Argtype(name, desc, pattern, expanded_pattern,
|
||||||
default_value, python_cast, python_format, loc);
|
default_value, alone_value,
|
||||||
|
python_cast, python_format, loc);
|
||||||
} catch (const std::regex_error& e) {
|
} catch (const std::regex_error& e) {
|
||||||
std::stringstream ss {};
|
std::stringstream ss {};
|
||||||
ss << "The pattern for argument type \"" << name
|
ss << "The pattern for argument type \"" << name
|
||||||
@@ -92,6 +98,35 @@ void Argtype_set::add(const std::string& name, const std::string& desc,
|
|||||||
<< " " << pattern << "\n";
|
<< " " << pattern << "\n";
|
||||||
throw Definition_error(ss.str(), loc, false);
|
throw Definition_error(ss.str(), loc, false);
|
||||||
}
|
}
|
||||||
|
if (!alone_value.empty()) {
|
||||||
|
// A pattern that matches running text cannot delimit a bare option
|
||||||
|
// name from the text after it: an option's value runs to the next
|
||||||
|
// bar or option name, so ":opt some words" would silently take
|
||||||
|
// "some words" as the value and the alone value would never be
|
||||||
|
// reached. A pattern that rejects multi-word text raises a clean
|
||||||
|
// argument error there instead. The test is behavioral rather
|
||||||
|
// than a comparison against matches_all()'s one literal pattern,
|
||||||
|
// because an @@@argtype written without a :pattern gets ".*",
|
||||||
|
// which is equally unable to delimit.
|
||||||
|
if (std::regex_match(std::string("one two"), m_types[name].m_regex)) {
|
||||||
|
std::stringstream ss {};
|
||||||
|
ss << "The argument type \"" << name << "\" cannot declare an :alone value "
|
||||||
|
<< "because its pattern matches running text:\n"
|
||||||
|
<< " " << pattern << "\n\n"
|
||||||
|
<< "An :alone value is used when an option name is written without a "
|
||||||
|
<< "value. A type that matches running text cannot tell a bare option "
|
||||||
|
<< "name from one whose value follows it, so the text after the name "
|
||||||
|
<< "would be taken as the value instead.\n";
|
||||||
|
throw Definition_error(ss.str(), loc, false);
|
||||||
|
}
|
||||||
|
if (!std::regex_match(alone_value, m_types[name].m_regex)) {
|
||||||
|
std::stringstream ss {};
|
||||||
|
ss << "The alone value \"" << alone_value << "\" for argument type \""
|
||||||
|
<< name << "\" does not match its own pattern:\n"
|
||||||
|
<< " " << pattern << "\n";
|
||||||
|
throw Definition_error(ss.str(), loc, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
m_names.push_back(name);
|
m_names.push_back(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,7 +143,7 @@ void Argtype_set::add(std::vector<Katom>::iterator begin, std::vector<Katom>::it
|
|||||||
// std::for_each(begin, end+1, [](Katom& k) { k.m_type = katom_t::replaced; });
|
// std::for_each(begin, end+1, [](Katom& k) { k.m_type = katom_t::replaced; });
|
||||||
|
|
||||||
add(values["name"], values["desc"], values["pattern"], values["default"],
|
add(values["name"], values["desc"], values["pattern"], values["default"],
|
||||||
values["python_cast"], modify_string_f{},
|
values["alone"], values["python_cast"], modify_string_f{},
|
||||||
begin->m_loc);
|
begin->m_loc);
|
||||||
|
|
||||||
modify_type(katom_t::replaced, begin, end);
|
modify_type(katom_t::replaced, begin, end);
|
||||||
@@ -143,6 +178,21 @@ std::string Argtype_set::eval(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The values a type supplies when an argument does not give one: the
|
||||||
|
// default (the option was not written at all) and the alone value (the
|
||||||
|
// option name was written without a value). Shown only when declared.
|
||||||
|
static std::string values_note(const Argtype& type)
|
||||||
|
{
|
||||||
|
std::stringstream note {};
|
||||||
|
if (!type.m_default.empty()) {
|
||||||
|
note << " [default: " << type.m_default << "]";
|
||||||
|
}
|
||||||
|
if (!type.m_alone.empty()) {
|
||||||
|
note << " [alone: " << type.m_alone << "]";
|
||||||
|
}
|
||||||
|
return note.str();
|
||||||
|
}
|
||||||
|
|
||||||
std::string Argtype_set::describe(bool long_form, int indent_width) const
|
std::string Argtype_set::describe(bool long_form, int indent_width) const
|
||||||
{
|
{
|
||||||
std::string indent(' ', indent_width);
|
std::string indent(' ', indent_width);
|
||||||
@@ -162,7 +212,8 @@ std::string Argtype_set::describe(bool long_form, int indent_width) const
|
|||||||
result << "\n";
|
result << "\n";
|
||||||
} else {
|
} else {
|
||||||
// result << abbrev(m_types.at(name).m_desc) << "\n";
|
// result << abbrev(m_types.at(name).m_desc) << "\n";
|
||||||
result << regex_split(m_types.at(name).m_desc, std::regex("\\n"), true)[0] << "\n";
|
result << regex_split(m_types.at(name).m_desc, std::regex("\\n"), true)[0]
|
||||||
|
<< values_note(m_types.at(name)) << "\n";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (long_form) {
|
if (long_form) {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ public:
|
|||||||
void check_for_existing_definition(const std::string& name, const Locator& loc);
|
void check_for_existing_definition(const std::string& name, const Locator& loc);
|
||||||
void add(const std::string& name, const std::string& desc,
|
void add(const std::string& name, const std::string& desc,
|
||||||
const std::string& pattern, const std::string& default_value,
|
const std::string& pattern, const std::string& default_value,
|
||||||
|
const std::string& alone_value,
|
||||||
const std::string& python_cast, modify_string_f python_format,
|
const std::string& python_cast, modify_string_f python_format,
|
||||||
const Locator& loc);
|
const Locator& loc);
|
||||||
void add(std::vector<Katom>::iterator begin, std::vector<Katom>::iterator end, std::vector<Katom>& katoms);
|
void add(std::vector<Katom>::iterator begin, std::vector<Katom>::iterator end, std::vector<Katom>& katoms);
|
||||||
|
|||||||
@@ -417,6 +417,16 @@ Parameter_set::value_map(
|
|||||||
}
|
}
|
||||||
auto optional_values = check_optional(optional, loc);
|
auto optional_values = check_optional(optional, loc);
|
||||||
for (auto [key, value] : optional_values) {
|
for (auto [key, value] : optional_values) {
|
||||||
|
// check_optional returns only the options that were actually
|
||||||
|
// written, so an empty value here means the name was written alone
|
||||||
|
// (":number" rather than ":number 10") — distinct from the option
|
||||||
|
// being absent, which is filled from the default below. The
|
||||||
|
// argument type supplies the alone value; bool declares "true",
|
||||||
|
// which is what makes a bare boolean option mean true.
|
||||||
|
const Parameter* parameter = find(key);
|
||||||
|
if (value.empty() && parameter && !parameter->m_argtype.m_alone.empty()) {
|
||||||
|
value = parameter->m_argtype.m_alone;
|
||||||
|
}
|
||||||
values[key] = value;
|
values[key] = value;
|
||||||
}
|
}
|
||||||
for (auto opt : m_optional) {
|
for (auto opt : m_optional) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
@@code.k :filename :pattern :caption :number | text.literal :
|
@@code.k :filename :pattern :caption :number.bool | text.literal :
|
||||||
A source file displayed verbatim
|
A source file displayed verbatim
|
||||||
@@
|
@@
|
||||||
|
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ def tex_list(K, list_type, items):
|
|||||||
body = "\n\n".join([f"\\item {e}" for e in items])
|
body = "\n\n".join([f"\\item {e}" for e in items])
|
||||||
command = {'ol' : 'enumerate', 'ul' : 'itemize'}[list_type]
|
command = {'ol' : 'enumerate', 'ul' : 'itemize'}[list_type]
|
||||||
topsep = '[topsep=0pt]'
|
topsep = '[topsep=0pt]'
|
||||||
listsep = '\\setlist{nolistsep}' if K.cmp != 'false' else ''
|
listsep = '\\setlist{nolistsep}' if K.cmp else ''
|
||||||
result = f'{listsep}\n\\begin{{{command}}}{topsep}\n{body}\n\\end{{{command}}}\n'
|
result = f'{listsep}\n\\begin{{{command}}}{topsep}\n{body}\n\\end{{{command}}}\n'
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|||||||
@@ -75,6 +75,7 @@
|
|||||||
|
|
||||||
:pattern ((?^:top^|head^|inner^|bottom^|all^|none)(?^:'index_subsets')?^|'indexed_range'^|\s+)+
|
:pattern ((?^:top^|head^|inner^|bottom^|all^|none)(?^:'index_subsets')?^|'indexed_range'^|\s+)+
|
||||||
:python_cast (lambda s : s.split())
|
:python_cast (lambda s : s.split())
|
||||||
|
:alone all
|
||||||
@@@
|
@@@
|
||||||
|
|
||||||
@@@argtype table_vline |
|
@@@argtype table_vline |
|
||||||
@@ -94,6 +95,7 @@
|
|||||||
|
|
||||||
:pattern ((?^:outer^|inner^|all^|none)(?^:'index_subsets')?^|'indexed_range'^|\s+)+
|
:pattern ((?^:outer^|inner^|all^|none)(?^:'index_subsets')?^|'indexed_range'^|\s+)+
|
||||||
:python_cast (lambda s : s.split())
|
:python_cast (lambda s : s.split())
|
||||||
|
:alone all
|
||||||
@@@
|
@@@
|
||||||
|
|
||||||
@@@argtype table_span |
|
@@@argtype table_span |
|
||||||
@@ -157,6 +159,7 @@
|
|||||||
calculated values.
|
calculated values.
|
||||||
:pattern period^|comma
|
:pattern period^|comma
|
||||||
:default period
|
:default period
|
||||||
|
:alone comma
|
||||||
@@@
|
@@@
|
||||||
|
|
||||||
@@@argtype table_justify |
|
@@@argtype table_justify |
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
# Klammertext distribution test suite (subset).
|
# Klammertext distribution test suite (subset).
|
||||||
#
|
#
|
||||||
# Runs the six shell regression suites:
|
# Runs the seven shell regression suites:
|
||||||
# cond_test.sh — @cond argument delimitation
|
# cond_test.sh — @cond argument delimitation
|
||||||
# deftype_test.sh — the four klammer definition modes + redefinition table
|
# deftype_test.sh — the four klammer definition modes + redefinition table
|
||||||
# escape_test.sh — target character escaping and quoted specials
|
# escape_test.sh — target character escaping and quoted specials
|
||||||
# filename_test.sh — filenames with spaces (quoting, " / " lists, rescue)
|
# filename_test.sh — filenames with spaces (quoting, " / " lists, rescue)
|
||||||
|
# alone_test.sh — an optional argument's three values (default, the
|
||||||
|
# argument type's :alone value, a written value)
|
||||||
# modulepath_test.sh — @eval finds modules beside the file that names them
|
# modulepath_test.sh — @eval finds modules beside the file that names them
|
||||||
# editor_test.sh — editor support (doc/edit): indentation and table
|
# editor_test.sh — editor support (doc/edit): indentation and table
|
||||||
# alignment; needs python3, uses Emacs when installed
|
# alignment; needs python3, uses Emacs when installed
|
||||||
@@ -17,5 +19,6 @@ test:
|
|||||||
./deftype_test.sh
|
./deftype_test.sh
|
||||||
./escape_test.sh
|
./escape_test.sh
|
||||||
./filename_test.sh
|
./filename_test.sh
|
||||||
|
./alone_test.sh
|
||||||
./modulepath_test.sh
|
./modulepath_test.sh
|
||||||
./editor_test.sh
|
./editor_test.sh
|
||||||
|
|||||||
240
tst/alone_test.sh
Executable file
240
tst/alone_test.sh
Executable file
@@ -0,0 +1,240 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# alone_test.sh — Regression tests for an argument type's :alone value.
|
||||||
|
#
|
||||||
|
# An optional argument has three possible values, not two:
|
||||||
|
#
|
||||||
|
# 1. the option name is not written at all -> the default
|
||||||
|
# 2. the name is written alone, with no value -> the type's :alone value
|
||||||
|
# 3. the name is written with a value -> that value
|
||||||
|
#
|
||||||
|
# The :alone value is declared by the argument type (@@@argtype ... :alone),
|
||||||
|
# never by a klammer's parameter declaration. A default is what a klammer
|
||||||
|
# means by silence and is properly per-klammer; a bare option name must read
|
||||||
|
# the same way in every klammer, or a writer cannot know what it means
|
||||||
|
# without consulting each signature.
|
||||||
|
#
|
||||||
|
# The bool type declares :alone true, which is where the convention that a
|
||||||
|
# bare boolean option means true comes from. This is a declaration like any
|
||||||
|
# other type's, not an engine special case for booleans.
|
||||||
|
#
|
||||||
|
# A type whose pattern matches running text cannot declare an :alone value:
|
||||||
|
# an option's value runs to the next bar or option name, so ":opt some words"
|
||||||
|
# would take "some words" as the value and the alone value would never be
|
||||||
|
# reached. That is a definition-time error.
|
||||||
|
#
|
||||||
|
# Engine tier: these tests run with -k none and define their own argtypes
|
||||||
|
# and klammers inline, so they do not depend on the Standard Klammer Set.
|
||||||
|
#
|
||||||
|
# Usage: ./alone_test.sh (LSan suppressions come from env/runtime.env)
|
||||||
|
# Exit code: 0 if all tests pass, 1 otherwise.
|
||||||
|
|
||||||
|
PASS=0
|
||||||
|
FAIL=0
|
||||||
|
KTEXT=ktext
|
||||||
|
K=${KLAMMERTEXT_HOME:?KLAMMERTEXT_HOME must be set}
|
||||||
|
|
||||||
|
red=$'\033[31m'
|
||||||
|
green=$'\033[32m'
|
||||||
|
bold=$'\033[1m'
|
||||||
|
reset=$'\033[0m'
|
||||||
|
|
||||||
|
# strip leading/trailing blank lines and surrounding whitespace
|
||||||
|
trim() { awk '{ sub(/[ \t\r]+$/, "") } { line[NR]=$0 } END { f=1; while (f<=NR && line[f]=="") f++; l=NR; while (l>=1 && line[l]=="") l--; for (i=f;i<=l;i++) print line[i] }'; }
|
||||||
|
|
||||||
|
# check_eq TEST_NAME EXPECTED KTEXT_ARGS...
|
||||||
|
# Runs ktext, expects exit status 0, and compares trimmed stdout to EXPECTED.
|
||||||
|
check_eq() {
|
||||||
|
local test_name="$1"
|
||||||
|
local expected="$2"
|
||||||
|
shift 2
|
||||||
|
|
||||||
|
local output status
|
||||||
|
output=$("$KTEXT" "$@" 2>/tmp/alone_test_err.$$)
|
||||||
|
status=$?
|
||||||
|
output=$(printf '%s' "$output" | trim)
|
||||||
|
|
||||||
|
if [ $status -ne 0 ]; then
|
||||||
|
echo "${red}FAIL${reset} $test_name — ktext exited $status"
|
||||||
|
echo " stderr: $(head -3 /tmp/alone_test_err.$$)"
|
||||||
|
FAIL=$((FAIL + 1))
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if [ "$output" = "$expected" ]; then
|
||||||
|
echo "${green}PASS${reset} $test_name"
|
||||||
|
PASS=$((PASS + 1))
|
||||||
|
else
|
||||||
|
echo "${red}FAIL${reset} $test_name"
|
||||||
|
echo " expected: [$expected]"
|
||||||
|
echo " got: [$output]"
|
||||||
|
FAIL=$((FAIL + 1))
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# check_error TEST_NAME PATTERN KTEXT_ARGS...
|
||||||
|
# Runs ktext, expects a NONZERO exit status and PATTERN in the message.
|
||||||
|
check_error() {
|
||||||
|
local test_name="$1"
|
||||||
|
local pattern="$2"
|
||||||
|
shift 2
|
||||||
|
|
||||||
|
local output status
|
||||||
|
output=$("$KTEXT" "$@" 2>&1)
|
||||||
|
status=$?
|
||||||
|
|
||||||
|
if [ $status -eq 0 ]; then
|
||||||
|
echo "${red}FAIL${reset} $test_name — expected an error but ktext succeeded"
|
||||||
|
FAIL=$((FAIL + 1))
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if echo "$output" | grep -qF "$pattern"; then
|
||||||
|
echo "${green}PASS${reset} $test_name"
|
||||||
|
PASS=$((PASS + 1))
|
||||||
|
else
|
||||||
|
echo "${red}FAIL${reset} $test_name — expected error to contain [$pattern]"
|
||||||
|
echo " output: $(echo "$output" | head -5)"
|
||||||
|
FAIL=$((FAIL + 1))
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# A type with both a default and an alone value, and a klammer using it.
|
||||||
|
DEPTH='@@@argtype depth | a table of contents depth :pattern \d+ :default 0 :alone 3 @@@'
|
||||||
|
DK='@@d :n.depth : [*n*] @@'
|
||||||
|
|
||||||
|
# A type with an alone value but no default.
|
||||||
|
MARK='@@@argtype mark | a mark character :pattern [-*+] :alone * @@@'
|
||||||
|
MK='@@m :c.mark : [*c*] @@'
|
||||||
|
|
||||||
|
echo "${bold}Argument type :alone value tests${reset}"
|
||||||
|
echo "================================"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# --- The three values of an optional argument ---
|
||||||
|
|
||||||
|
check_eq \
|
||||||
|
" 1. name absent — the default" \
|
||||||
|
"[0]" \
|
||||||
|
-k none -s "$DEPTH $DK @d@" -d
|
||||||
|
|
||||||
|
check_eq \
|
||||||
|
" 2. name written alone — the type's alone value" \
|
||||||
|
"[3]" \
|
||||||
|
-k none -s "$DEPTH $DK @d :n @" -d
|
||||||
|
|
||||||
|
check_eq \
|
||||||
|
" 3. name written with a value — that value" \
|
||||||
|
"[7]" \
|
||||||
|
-k none -s "$DEPTH $DK @d :n 7 @" -d
|
||||||
|
|
||||||
|
# --- A type with an alone value but no default ---
|
||||||
|
|
||||||
|
check_eq \
|
||||||
|
" 4. no default declared — absent is empty" \
|
||||||
|
"[]" \
|
||||||
|
-k none -s "$MARK $MK @m@" -d
|
||||||
|
|
||||||
|
check_eq \
|
||||||
|
" 5. no default declared — alone still applies" \
|
||||||
|
"[*]" \
|
||||||
|
-k none -s "$MARK $MK @m :c @" -d
|
||||||
|
|
||||||
|
# --- The alone value belongs to the type, so every parameter of that
|
||||||
|
# type gets it, and a parameter default does not disturb it ---
|
||||||
|
|
||||||
|
check_eq \
|
||||||
|
" 6. two parameters of one type share the alone value" \
|
||||||
|
"[3][3]" \
|
||||||
|
-k none -s "$DEPTH @@d2 :a.depth :b.depth : [*a*][*b*] @@ @d2 :a :b @" -d
|
||||||
|
|
||||||
|
check_eq \
|
||||||
|
" 7. a parameter default overrides the type default, not the alone value" \
|
||||||
|
"[5]|[3]" \
|
||||||
|
-k none -s "$DEPTH @@d3 :n.depth 5 : [*n*] @@ @d3@|@d3 :n @" -d
|
||||||
|
|
||||||
|
# --- bool: the convention that a bare boolean option means true ---
|
||||||
|
|
||||||
|
check_eq \
|
||||||
|
" 8. bool written alone is true" \
|
||||||
|
"[true]" \
|
||||||
|
-k none -s '@@b :f.bool : [*f*] @@ @b :f @' -d
|
||||||
|
|
||||||
|
check_eq \
|
||||||
|
" 9. bool written with false stays false" \
|
||||||
|
"[false]" \
|
||||||
|
-k none -s '@@b :f.bool : [*f*] @@ @b :f false @' -d
|
||||||
|
|
||||||
|
check_eq \
|
||||||
|
"10. bool absent with no default is empty" \
|
||||||
|
"[]" \
|
||||||
|
-k none -s '@@b :f.bool : [*f*] @@ @b@' -d
|
||||||
|
|
||||||
|
check_eq \
|
||||||
|
"11. bool absent with a true parameter default" \
|
||||||
|
"[true]" \
|
||||||
|
-k none -s '@@b :f.bool true : [*f*] @@ @b@' -d
|
||||||
|
|
||||||
|
check_eq \
|
||||||
|
"12. bool false explicitly against a true default" \
|
||||||
|
"[false]" \
|
||||||
|
-k none -s '@@b :f.bool true : [*f*] @@ @b :f false @' -d
|
||||||
|
|
||||||
|
# --- The value reaching @eval ---
|
||||||
|
|
||||||
|
check_eq \
|
||||||
|
"13. python value of a bool written alone" \
|
||||||
|
"True" \
|
||||||
|
-k none -s '@@b :f.bool : @eval repr(K.f) @ @@ @b :f @' -d
|
||||||
|
|
||||||
|
check_eq \
|
||||||
|
"14. python value of an absent bool" \
|
||||||
|
"None" \
|
||||||
|
-k none -s '@@b :f.bool : @eval repr(K.f) @ @@ @b@' -d
|
||||||
|
|
||||||
|
check_eq \
|
||||||
|
"15. python value of a user type written alone" \
|
||||||
|
"'3'" \
|
||||||
|
-k none -s "$DEPTH @@d4 :n.depth : @eval repr(K.n) @ @@ @d4 :n @" -d
|
||||||
|
|
||||||
|
# --- Types that cannot delimit a bare option name ---
|
||||||
|
|
||||||
|
check_error \
|
||||||
|
"16. :alone refused on a type matching running text" \
|
||||||
|
"cannot declare an :alone value" \
|
||||||
|
-k none -s '@@@argtype loose | anything at all :alone x @@@' -d
|
||||||
|
|
||||||
|
check_error \
|
||||||
|
"17. :alone refused on an explicit match-everything pattern" \
|
||||||
|
"cannot declare an :alone value" \
|
||||||
|
-k none -s '@@@argtype loose | anything :pattern (?:.^|\n)* :alone x @@@' -d
|
||||||
|
|
||||||
|
check_error \
|
||||||
|
"18. an alone value must match its own type's pattern" \
|
||||||
|
"does not match its own pattern" \
|
||||||
|
-k none -s '@@@argtype depth | a depth :pattern \d+ :alone many @@@' -d
|
||||||
|
|
||||||
|
# --- Delimitation: an option value still runs to the next bar or option
|
||||||
|
# name, so text after a bare name is taken as the value and rejected
|
||||||
|
# by the type. This is what makes the guard above necessary.
|
||||||
|
|
||||||
|
check_error \
|
||||||
|
"19. text after a bare option name is taken as its value" \
|
||||||
|
"does not match" \
|
||||||
|
-k none -s "$DEPTH $DK @d :n some words @" -d
|
||||||
|
|
||||||
|
check_eq \
|
||||||
|
"20. a bar separates a bare option name from following text" \
|
||||||
|
"[3]two" \
|
||||||
|
-k none -s "$DEPTH @@d5 :n.depth | t : [*n*]*t* @@ @d5 :n | two @" -d
|
||||||
|
|
||||||
|
# --- A type with no alone value is unchanged: a bare name is empty ---
|
||||||
|
|
||||||
|
check_eq \
|
||||||
|
"21. bare option of a type with no alone value is empty" \
|
||||||
|
"[]" \
|
||||||
|
-k none -s '@@s :t.word : [*t*] @@ @s :t @' -d
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "================================"
|
||||||
|
echo "Passed: $PASS Failed: $FAIL"
|
||||||
|
rm -f /tmp/alone_test_err.$$
|
||||||
|
[ $FAIL -eq 0 ]
|
||||||
Reference in New Issue
Block a user