Change option lists to be specified as separate arguments on commands line

Option lists and maps are specified using separate arguments, avoiding
the need for additional escaping of their separator and reusing the
existing command line spliting logic instead.

As discussed on #2087, this should make it much easier to work with
list options, and make the general option system feel cleaner.
This commit is contained in:
Maxime Coste 2018-05-30 23:23:38 +10:00
parent 5eeec8bd4d
commit b548dd3a6f
8 changed files with 168 additions and 85 deletions

View File

@ -253,9 +253,31 @@ Token parse_percent_token(Reader& reader, bool throw_on_unterminated)
} }
} }
String expand_token(const Token& token, const Context& context, auto expand_option(Option& opt, std::true_type)
const ShellContext& shell_context)
{ {
return opt.get_as_string();
}
auto expand_option(Option& opt, std::false_type)
{
return opt.get_as_strings();
}
String expand_arobase(ConstArrayView<String> params, std::true_type)
{
return join(params, ' ', false);
}
Vector<String> expand_arobase(ConstArrayView<String> params, std::false_type)
{
return {params.begin(), params.end()};
}
template<bool single>
std::conditional_t<single, String, Vector<String>>
expand_token(const Token& token, const Context& context, const ShellContext& shell_context)
{
using IsSingle = std::integral_constant<bool, single>;
auto& content = token.content; auto& content = token.content;
switch (token.type) switch (token.type)
{ {
@ -273,35 +295,35 @@ String expand_token(const Token& token, const Context& context,
++trailing_eol_count; ++trailing_eol_count;
} }
str.resize(str.length() - trailing_eol_count, 0); str.resize(str.length() - trailing_eol_count, 0);
return str; return {str};
} }
case Token::Type::RegisterExpand: case Token::Type::RegisterExpand:
return context.main_sel_register_value(content).str(); return {context.main_sel_register_value(content).str()};
case Token::Type::OptionExpand: case Token::Type::OptionExpand:
return context.options()[content].get_as_string(); return expand_option(context.options()[content], IsSingle{});
case Token::Type::ValExpand: case Token::Type::ValExpand:
{ {
auto it = shell_context.env_vars.find(content); auto it = shell_context.env_vars.find(content);
if (it != shell_context.env_vars.end()) if (it != shell_context.env_vars.end())
return it->value; return {it->value};
return ShellManager::instance().get_val(content, context); return {ShellManager::instance().get_val(content, context)};
} }
case Token::Type::ArgExpand: case Token::Type::ArgExpand:
{ {
auto& params = shell_context.params; auto& params = shell_context.params;
if (content == '@') if (content == '@')
return join(params, ' '); return expand_arobase(params, IsSingle{});
const int arg = str_to_int(content)-1; const int arg = str_to_int(content)-1;
if (arg < 0) if (arg < 0)
throw runtime_error("invalid argument index"); throw runtime_error("invalid argument index");
return arg < params.size() ? params[arg] : String{}; return {arg < params.size() ? params[arg] : String{}};
} }
case Token::Type::RawEval: case Token::Type::RawEval:
return expand(content, context, shell_context); return {expand(content, context, shell_context)};
case Token::Type::Raw: case Token::Type::Raw:
case Token::Type::RawQuoted: case Token::Type::RawQuoted:
return content; return {content};
default: kak_assert(false); default: kak_assert(false);
} }
return {}; return {};
@ -378,8 +400,7 @@ String expand_impl(StringView str, const Context& context,
else else
{ {
res += reader.substr_from(beg); res += reader.substr_from(beg);
res += postprocess(expand_token(parse_percent_token(reader, true), res += postprocess(expand_token<true>(parse_percent_token(reader, true), context, shell_context));
context, shell_context));
beg = reader.pos; beg = reader.pos;
} }
} }
@ -483,7 +504,12 @@ void CommandManager::execute(StringView command_line,
params.insert(params.end(), shell_context.params.begin(), params.insert(params.end(), shell_context.params.begin(),
shell_context.params.end()); shell_context.params.end());
else else
params.push_back(expand_token(token, context, shell_context)); {
auto tokens = expand_token<false>(token, context, shell_context);
params.insert(params.end(),
std::make_move_iterator(tokens.begin()),
std::make_move_iterator(tokens.end()));
}
} }
execute_single_command(params, context, shell_context, command_coord); execute_single_command(params, context, shell_context, command_coord);
} }
@ -640,7 +666,7 @@ Completions CommandManager::complete(const Context& context,
context, flags, params, tokens.size() - 2, context, flags, params, tokens.size() - 2,
cursor_pos_in_token), start); cursor_pos_in_token), start);
if (token.type != Token::Type::RawQuoted) if (not completions.quoted and token.type != Token::Type::RawQuoted)
{ {
StringView to_escape = token.type == Token::Type::Raw ? "% \t;" : "%"; StringView to_escape = token.type == Token::Type::Raw ? "% \t;" : "%";
for (auto& candidate : completions.candidates) for (auto& candidate : completions.candidates)

View File

@ -1318,7 +1318,7 @@ const CommandDesc set_option_cmd = {
"scope the option is set in", "scope the option is set in",
ParameterDesc{ ParameterDesc{
{ { "add", { false, "add to option rather than replacing it" } } }, { { "add", { false, "add to option rather than replacing it" } } },
ParameterDesc::Flags::SwitchesOnlyAtStart, 3, 3 ParameterDesc::Flags::SwitchesOnlyAtStart, 2, (size_t)-1
}, },
CommandFlags::None, CommandFlags::None,
option_doc_helper, option_doc_helper,
@ -1337,13 +1337,11 @@ const CommandDesc set_option_cmd = {
else if (token_to_complete == start + 1) else if (token_to_complete == start + 1)
return { 0_byte, params[start + 1].length(), return { 0_byte, params[start + 1].length(),
GlobalScope::instance().option_registry().complete_option_name(params[start + 1], pos_in_token) }; GlobalScope::instance().option_registry().complete_option_name(params[start + 1], pos_in_token) };
else if (not add and token_to_complete == start + 2 and else if (not add and token_to_complete == start + 2 and params[start + 2].empty() and
GlobalScope::instance().option_registry().option_exists(params[start + 1])) GlobalScope::instance().option_registry().option_exists(params[start + 1]))
{ {
OptionManager& options = get_scope(params[start], context).options(); OptionManager& options = get_scope(params[start], context).options();
String val = options[params[start + 1]].get_as_string(); return { 0_byte, params[start + 2].length(), { options[params[start + 1]].get_as_string() }, true };
if (prefix_match(val, params[start + 2]))
return { 0_byte, params[start + 2].length(), { std::move(val) } };
} }
return Completions{}; return Completions{};
}, },
@ -1351,9 +1349,9 @@ const CommandDesc set_option_cmd = {
{ {
Option& opt = get_options(parser[0], context, parser[1]).get_local_option(parser[1]); Option& opt = get_options(parser[0], context, parser[1]).get_local_option(parser[1]);
if (parser.get_switch("add")) if (parser.get_switch("add"))
opt.add_from_string(parser[2]); opt.add_from_strings(parser.positionals_from(2));
else else
opt.set_from_string(parser[2]); opt.set_from_strings(parser.positionals_from(2));
} }
}; };
@ -1427,7 +1425,7 @@ const CommandDesc declare_option_cmd = {
ParameterDesc{ ParameterDesc{
{ { "hidden", { false, "do not display option name when completing" } }, { { "hidden", { false, "do not display option name when completing" } },
{ "docstring", { true, "specify option description" } } }, { "docstring", { true, "specify option description" } } },
ParameterDesc::Flags::SwitchesOnlyAtStart, 2, 3 ParameterDesc::Flags::SwitchesOnlyAtStart, 2, (size_t)-1
}, },
CommandFlags::None, CommandFlags::None,
CommandHelper{}, CommandHelper{},
@ -1470,8 +1468,8 @@ const CommandDesc declare_option_cmd = {
else else
throw runtime_error(format("no such option type: '{}'", parser[0])); throw runtime_error(format("no such option type: '{}'", parser[0]));
if (parser.positional_count() == 3) if (parser.positional_count() > 2)
opt->set_from_string(parser[2]); opt->set_from_strings(parser.positionals_from(2));
} }
}; };

View File

@ -21,6 +21,7 @@ struct Completions
CandidateList candidates; CandidateList candidates;
ByteCount start; ByteCount start;
ByteCount end; ByteCount end;
bool quoted = false;
Completions() Completions()
: start(0), end(0) {} : start(0), end(0) {}
@ -28,8 +29,8 @@ struct Completions
Completions(ByteCount start, ByteCount end) Completions(ByteCount start, ByteCount end)
: start(start), end(end) {} : start(start), end(end) {}
Completions(ByteCount start, ByteCount end, CandidateList candidates) Completions(ByteCount start, ByteCount end, CandidateList candidates, bool quoted = false)
: candidates(std::move(candidates)), start(start), end(end) {} : candidates(std::move(candidates)), start(start), end(end), quoted{quoted} {}
}; };
enum class CompletionFlags enum class CompletionFlags
@ -56,7 +57,7 @@ Completions shell_complete(const Context& context, CompletionFlags,
inline Completions offset_pos(Completions completion, ByteCount offset) inline Completions offset_pos(Completions completion, ByteCount offset)
{ {
return { completion.start + offset, completion.end + offset, return { completion.start + offset, completion.end + offset,
std::move(completion.candidates) }; std::move(completion.candidates), completion.quoted };
} }
template<typename Container> template<typename Container>

View File

@ -16,6 +16,33 @@ inline String option_to_string(int opt);
inline String option_to_string(size_t opt); inline String option_to_string(size_t opt);
inline String option_to_string(bool opt); inline String option_to_string(bool opt);
// Default fallback to single value functions
template<typename T>
decltype(option_from_string(Meta::Type<T>{}, StringView{}))
option_from_strings(Meta::Type<T>, ConstArrayView<String> strs)
{
if (strs.size() != 1)
throw runtime_error("expected a single value for option");
return option_from_string(Meta::Type<T>{}, strs[0]);
}
template<typename T>
Vector<decltype(option_to_string(std::declval<T>()))>
option_to_strings(const T& opt)
{
return Vector<String>{option_to_string(opt)};
}
template<typename T>
decltype(option_add(std::declval<T>(), std::declval<String>()))
option_add_from_strings(T& opt, ConstArrayView<String> strs)
{
if (strs.size() != 1)
throw runtime_error("expected a single value for option");
return option_add(opt, strs[0]);
}
template<typename P, typename T> template<typename P, typename T>
struct PrefixedList struct PrefixedList
{ {

View File

@ -54,9 +54,10 @@ public:
template<typename T> bool is_of_type() const; template<typename T> bool is_of_type() const;
virtual String get_as_string() const = 0; virtual String get_as_string() const = 0;
virtual void set_from_string(StringView str) = 0; virtual Vector<String> get_as_strings() const = 0;
virtual void add_from_string(StringView str) = 0; virtual void set_from_strings(ConstArrayView<String> strs) = 0;
virtual void update(const Context& context) = 0; virtual void add_from_strings(ConstArrayView<String> strs) = 0;
virtual void update(const Context& context) = 0;
virtual Option* clone(OptionManager& manager) const = 0; virtual Option* clone(OptionManager& manager) const = 0;
OptionManager& manager() const { return m_manager; } OptionManager& manager() const { return m_manager; }
@ -141,19 +142,27 @@ public:
const T& get() const { return m_value; } const T& get() const { return m_value; }
T& get_mutable() { return m_value; } T& get_mutable() { return m_value; }
Vector<String> get_as_strings() const override
{
return option_to_strings(m_value);
}
String get_as_string() const override String get_as_string() const override
{ {
return option_to_string(m_value); return option_to_string(m_value);
} }
void set_from_string(StringView str) override
void set_from_strings(ConstArrayView<String> strs) override
{ {
set(option_from_string(Meta::Type<T>{}, str)); set(option_from_strings(Meta::Type<T>{}, strs));
} }
void add_from_string(StringView str) override
void add_from_strings(ConstArrayView<String> strs) override
{ {
if (option_add(m_value, str)) if (option_add_from_strings(m_value, strs))
m_manager.on_option_changed(*this); m_manager.on_option_changed(*this);
} }
void update(const Context& context) override void update(const Context& context) override
{ {
option_update(m_value, context); option_update(m_value, context);

View File

@ -5,19 +5,20 @@ namespace Kakoune
{ {
UnitTest test_option_parsing{[]{ UnitTest test_option_parsing{[]{
auto check = [](auto&& value, StringView str) auto check = [](auto&& value, ConstArrayView<String> strs)
{ {
auto repr = option_to_string(value); auto repr = option_to_strings(value);
kak_assert(repr == str); kak_assert(strs == ConstArrayView<String>{repr});
auto parsed = option_from_string(Meta::Type<std::decay_t<decltype(value)>>{}, str); auto parsed = option_from_strings(Meta::Type<std::decay_t<decltype(value)>>{}, strs);
kak_assert(parsed == value); kak_assert(parsed == value);
}; };
check(123, "123"); check(123, {"123"});
check(true, "true"); check(true, {"true"});
check(Vector<String>{"foo", "bar:", "baz"}, "foo:bar\\::baz"); check(Vector<String>{"foo", "bar:", "baz"}, {"foo", "bar:", "baz"});
check(HashMap<String, int>{{"foo", 10}, {"b=r", 20}, {"b:z", 30}}, "foo=10:b\\=r=20:b\\:z=30"); check(Vector<int>{10, 20, 30}, {"10", "20", "30"});
check(DebugFlags::Keys | DebugFlags::Hooks, "hooks|keys"); check(HashMap<String, int>{{"foo", 10}, {"b=r", 20}, {"b:z", 30}}, {"foo=10", "b\\=r=20", "b:z=30"});
check(DebugFlags::Keys | DebugFlags::Hooks, {"hooks|keys"});
}}; }};
} }

View File

@ -18,6 +18,12 @@
namespace Kakoune namespace Kakoune
{ {
inline String quote(StringView s)
{
return format("'{}'", replace(s, "'", "''"));
}
template<typename T> template<typename T>
constexpr decltype(T::option_type_name) option_type_name(Meta::Type<T>) constexpr decltype(T::option_type_name) option_type_name(Meta::Type<T>)
{ {
@ -67,13 +73,16 @@ inline Codepoint option_from_string(Meta::Type<Codepoint>, StringView str)
} }
constexpr StringView option_type_name(Meta::Type<Codepoint>) { return "codepoint"; } constexpr StringView option_type_name(Meta::Type<Codepoint>) { return "codepoint"; }
constexpr char list_separator = ':'; template<typename T, MemoryDomain domain>
Vector<String> option_to_strings(const Vector<T, domain>& opt)
{
return opt | transform([](const T& t) { return option_to_string(t); }) | gather<Vector<String>>();
}
template<typename T, MemoryDomain domain> template<typename T, MemoryDomain domain>
String option_to_string(const Vector<T, domain>& opt) String option_to_string(const Vector<T, domain>& opt)
{ {
return join(opt | transform([](const T& t) { return option_to_string(t); }), return join(opt | transform([](const T& t) { return quote(option_to_string(t)); }), ' ', false);
list_separator);
} }
template<typename T, MemoryDomain domain> template<typename T, MemoryDomain domain>
@ -81,20 +90,18 @@ void option_list_postprocess(Vector<T, domain>& opt)
{} {}
template<typename T, MemoryDomain domain> template<typename T, MemoryDomain domain>
Vector<T, domain> option_from_string(Meta::Type<Vector<T, domain>>, StringView str) Vector<T, domain> option_from_strings(Meta::Type<Vector<T, domain>>, ConstArrayView<String> strs)
{ {
auto res = str | split<StringView>(list_separator, '\\') auto res = strs | transform([](auto&& s) { return option_from_string(Meta::Type<T>{}, s); })
| transform(unescape<list_separator, '\\'>) | gather<Vector<T, domain>>();
| transform([](auto&& s) { return option_from_string(Meta::Type<T>{}, s); })
| gather<Vector<T, domain>>();
option_list_postprocess(res); option_list_postprocess(res);
return res; return res;
} }
template<typename T, MemoryDomain domain> template<typename T, MemoryDomain domain>
bool option_add(Vector<T, domain>& opt, StringView str) bool option_add_from_strings(Vector<T, domain>& opt, ConstArrayView<String> strs)
{ {
auto vec = option_from_string(Meta::Type<Vector<T, domain>>{}, str); auto vec = option_from_strings(Meta::Type<Vector<T, domain>>{}, strs);
opt.insert(opt.end(), opt.insert(opt.end(),
std::make_move_iterator(vec.begin()), std::make_move_iterator(vec.begin()),
std::make_move_iterator(vec.end())); std::make_move_iterator(vec.end()));
@ -109,31 +116,35 @@ String option_type_name(Meta::Type<Vector<T, D>>)
} }
template<typename Key, typename Value, MemoryDomain domain> template<typename Key, typename Value, MemoryDomain domain>
String option_to_string(const HashMap<Key, Value, domain>& opt) Vector<String> option_to_strings(const HashMap<Key, Value, domain>& opt)
{ {
String res; return opt | transform([](auto&& item) {
for (auto it = opt.begin(); it != opt.end(); ++it) return format("{}={}",
{ escape(option_to_string(item.key), '=', '\\'),
if (it != opt.begin()) escape(option_to_string(item.value), '=', '\\'));
res += list_separator; }) | gather<Vector<String>>();
String elem = escape(option_to_string(it->key), '=', '\\') + "=" +
escape(option_to_string(it->value), '=', '\\');
res += escape(elem, list_separator, '\\');
}
return res;
} }
template<typename Key, typename Value, MemoryDomain domain> template<typename Key, typename Value, MemoryDomain domain>
bool option_add(HashMap<Key, Value, domain>& opt, StringView str) String option_to_string(const HashMap<Key, Value, domain>& opt)
{
return join(opt | transform([](auto&& item) {
return quote(format("{}={}",
escape(option_to_string(item.key), '=', '\\'),
escape(option_to_string(item.value), '=', '\\')));
}), ' ', false);
}
template<typename Key, typename Value, MemoryDomain domain>
bool option_add_from_strings(HashMap<Key, Value, domain>& opt, ConstArrayView<String> strs)
{ {
bool changed = false; bool changed = false;
for (auto&& elem : str | split<StringView>(list_separator, '\\') for (auto&& str : strs)
| transform(unescape<list_separator, '\\'>))
{ {
struct error : runtime_error { error(size_t) : runtime_error{"map option expects key=value"} {} }; struct error : runtime_error { error(size_t) : runtime_error{"map option expects key=value"} {} };
auto key_value = elem | split<StringView>('=', '\\') auto key_value = str | split<StringView>('=', '\\')
| transform(unescape<'=', '\\'>) | transform(unescape<'=', '\\'>)
| static_gather<error, 2>(); | static_gather<error, 2>();
opt[option_from_string(Meta::Type<Key>{}, key_value[0])] = option_from_string(Meta::Type<Value>{}, key_value[1]); opt[option_from_string(Meta::Type<Key>{}, key_value[0])] = option_from_string(Meta::Type<Value>{}, key_value[1]);
changed = true; changed = true;
@ -142,10 +153,10 @@ bool option_add(HashMap<Key, Value, domain>& opt, StringView str)
} }
template<typename Key, typename Value, MemoryDomain domain> template<typename Key, typename Value, MemoryDomain domain>
HashMap<Key, Value, domain> option_from_string(Meta::Type<HashMap<Key, Value, domain>>, StringView str) HashMap<Key, Value, domain> option_from_strings(Meta::Type<HashMap<Key, Value, domain>>, ConstArrayView<String> str)
{ {
HashMap<Key, Value, domain> res; HashMap<Key, Value, domain> res;
option_add(res, str); option_add_from_strings(res, str);
return res; return res;
} }
@ -304,29 +315,32 @@ EnableIfWithBitOps<Flags, bool> option_add(Flags& opt, StringView str)
return res != (Flags)0; return res != (Flags)0;
} }
template<typename P, typename T>
inline Vector<String> option_to_strings(const PrefixedList<P, T>& opt)
{
Vector<String> res{option_to_string(opt.prefix)};
auto list = option_to_strings(opt.list);
res.insert(res.end(), std::make_move_iterator(list.begin()), std::make_move_iterator(list.end()));
return res;
}
template<typename P, typename T> template<typename P, typename T>
inline String option_to_string(const PrefixedList<P, T>& opt) inline String option_to_string(const PrefixedList<P, T>& opt)
{ {
if (opt.list.empty()) return option_to_string(opt.prefix) + " " + option_to_string(opt.list);
return format("{}", escape(option_to_string(opt.prefix), list_separator, '\\'));
else
return format("{}{}{}", escape(option_to_string(opt.prefix), list_separator, '\\'),
list_separator, option_to_string(opt.list));
} }
template<typename P, typename T> template<typename P, typename T>
inline PrefixedList<P, T> option_from_string(Meta::Type<PrefixedList<P, T>>, StringView str) inline PrefixedList<P, T> option_from_strings(Meta::Type<PrefixedList<P, T>>, ConstArrayView<String> strs)
{ {
using VecType = Vector<T, MemoryDomain::Options>; return {option_from_string(Meta::Type<P>{}, strs[0]),
auto it = find(str, list_separator); option_from_strings(Meta::Type<Vector<T, MemoryDomain::Options>>{}, strs.subrange(1))};
return {option_from_string(Meta::Type<P>{}, StringView{str.begin(), it}),
it != str.end() ? option_from_string(Meta::Type<VecType>{}, {it+1, str.end()}) : VecType{}};
} }
template<typename P, typename T> template<typename P, typename T>
inline bool option_add(PrefixedList<P, T>& opt, StringView str) inline bool option_add_from_strings(PrefixedList<P, T>& opt, ConstArrayView<String> str)
{ {
return option_add(opt.list, str); return option_add_from_strings(opt.list, str);
} }
} }

View File

@ -6,6 +6,7 @@
#include "meta.hh" #include "meta.hh"
#include "array_view.hh" #include "array_view.hh"
#include "optional.hh" #include "optional.hh"
#include "flags.hh"
#include "string.hh" #include "string.hh"
#include "string_utils.hh" #include "string_utils.hh"
@ -115,6 +116,12 @@ struct ParametersParser
return m_params[m_positional_indices[index]]; return m_params[m_positional_indices[index]];
} }
ConstArrayView<String> positionals_from(size_t first) const
{
kak_assert(m_desc.flags & (ParameterDesc::Flags::SwitchesOnlyAtStart | ParameterDesc::Flags::SwitchesAsPositional));
return m_params.subrange(m_positional_indices[first]);
}
iterator begin() const { return iterator(*this, 0); } iterator begin() const { return iterator(*this, 0); }
iterator end() const { return iterator(*this, m_positional_indices.size()); } iterator end() const { return iterator(*this, m_positional_indices.size()); }