Compare commits

...

5 Commits

Author SHA1 Message Date
Robert Bendun
719e8d4f26 Report similar names for builtins that user is searching for 2023-03-05 01:10:43 +01:00
Robert Bendun
92e5c3169c Started musique man implementation: structure of a page implemented 2023-03-04 21:23:13 +01:00
Robert Bendun
a2d8f28cd1 More colorfull error messages 2023-03-04 20:15:58 +01:00
Robert Bendun
20a6779e2f Usage generated from parameters short descriptions
Additionally printing short descriptions with list of proposed commands
2023-03-04 20:09:58 +01:00
Robert Bendun
c0f021e57f Introduce internal parameters; better condition for enabling repl 2023-03-04 19:00:15 +01:00
8 changed files with 384 additions and 119 deletions

View File

@ -9,7 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Builtin documentation for builtin functions display from repl and command line.
- Builtin documentation for builtin functions display from repl and command line (`musique doc <builtin>`)
- Man documentation for commandline interface builtin (`musique man`)
- Suggestions which command line parameters user may wanted to use
### Changed

View File

@ -1,16 +1,22 @@
#include <algorithm>
#include <array>
#include <chrono>
#include <edit_distance.hh>
#include <iomanip>
#include <iostream>
#include <limits>
#include <musique/cmd.hh>
#include <musique/common.hh>
#include <musique/errors.hh>
#include <musique/interpreter/builtin_function_documentation.hh>
#include <musique/pretty.hh>
#include <set>
#include <unordered_set>
#include <utility>
#include <variant>
// TODO: Command line parameters full documentation in other then man pages format. Maybe HTML generation?
#ifdef _WIN32
extern "C" {
#include <io.h>
@ -29,75 +35,174 @@ using namespace cmd;
// from musique/main.cc:
extern bool enable_repl;
extern bool ast_only_mode;
extern void usage();
static constexpr std::array all_parameters = [] {
Defines_Code provide_function = [](std::string_view fname) -> cmd::Run {
return { .type = Run::Deffered_File, .argument = fname };
};
static Defines_Code provide_function = [](std::string_view fname) -> cmd::Run {
return { .type = Run::Deffered_File, .argument = fname };
};
Defines_Code provide_inline_code = [](std::string_view code) -> cmd::Run {
return { .type = Run::Argument, .argument = code };
};
static Defines_Code provide_inline_code = [](std::string_view code) -> cmd::Run {
return { .type = Run::Argument, .argument = code };
};
Defines_Code provide_file = [](std::string_view fname) -> cmd::Run {
return { .type = Run::File, .argument = fname };
};
static Defines_Code provide_file = [](std::string_view fname) -> cmd::Run {
return { .type = Run::File, .argument = fname };
};
static Requires_Argument show_docs = [](std::string_view builtin) {
if (auto maybe_docs = find_documentation_for_builtin(builtin); maybe_docs) {
std::cout << *maybe_docs << std::endl;
return;
}
std::cerr << pretty::begin_error << "musique: error:" << pretty::end;
std::cerr << " cannot find documentation for given builtin" << std::endl;
Requires_Argument show_docs = [](std::string_view builtin) {
if (auto maybe_docs = find_documentation_for_builtin(builtin); maybe_docs) {
std::cout << *maybe_docs << std::endl;
return;
}
std::cerr << "musique: error: cannot find documentation for given builtin" << std::endl;
std::exit(1);
};
std::cerr << "Similar ones are:" << std::endl;
for (auto similar : similar_names_to_builtin(builtin)) {
std::cerr << " " << similar << '\n';
}
std::exit(1);
};
Empty_Argument set_interactive_mode = [] { enable_repl = true; };
Empty_Argument set_ast_only_mode = [] { ast_only_mode = true; };
static Empty_Argument set_interactive_mode = [] { enable_repl = true; };
static Empty_Argument set_ast_only_mode = [] { ast_only_mode = true; };
Empty_Argument print_version = [] { std::cout << Musique_Version << std::endl; };
Empty_Argument print_help = usage;
static Empty_Argument print_version = [] { std::cout << Musique_Version << std::endl; };
static Empty_Argument print_help = usage;
using Entry = std::pair<std::string_view, Parameter>;
[[noreturn]]
static void print_manpage();
// First entry for given action type should always be it's cannonical name
return std::array {
Entry { "fun", provide_function },
Entry { "def", provide_function },
Entry { "f", provide_function },
Entry { "func", provide_function },
Entry { "function", provide_function },
struct Entry
{
std::string_view name;
Parameter handler;
bool internal = false;
Entry { "run", provide_file },
Entry { "r", provide_file },
Entry { "exec", provide_file },
void* handler_ptr() const
{
return std::visit([](auto p) { return reinterpret_cast<void*>(p); }, handler);
}
Entry { "repl", set_interactive_mode },
Entry { "i", set_interactive_mode },
Entry { "interactive", set_interactive_mode },
size_t arguments() const
{
return std::visit([]<typename R, typename ...A>(R(*)(A...)) { return sizeof...(A); }, handler);
}
};
Entry { "doc", show_docs },
Entry { "d", show_docs },
Entry { "docs", show_docs },
// First entry for given action type should always be it's cannonical name
static auto all_parameters = std::array {
Entry { "run", provide_file },
Entry { "r", provide_file },
Entry { "exec", provide_file },
Entry { "load", provide_file },
Entry { "inline", provide_inline_code },
Entry { "c", provide_inline_code },
Entry { "code", provide_inline_code },
Entry { "fun", provide_function },
Entry { "def", provide_function },
Entry { "f", provide_function },
Entry { "func", provide_function },
Entry { "function", provide_function },
Entry { "help", print_help },
Entry { "?", print_help },
Entry { "/?", print_help },
Entry { "h", print_help },
Entry { "repl", set_interactive_mode },
Entry { "i", set_interactive_mode },
Entry { "interactive", set_interactive_mode },
Entry { "version", print_version },
Entry { "v", print_version },
Entry { "doc", show_docs },
Entry { "d", show_docs },
Entry { "docs", show_docs },
Entry { "ast", set_ast_only_mode },
};
}();
Entry { "man", print_manpage },
Entry { "inline", provide_inline_code },
Entry { "c", provide_inline_code },
Entry { "code", provide_inline_code },
Entry { "help", print_help },
Entry { "?", print_help },
Entry { "/?", print_help },
Entry { "h", print_help },
Entry { "version", print_version },
Entry { "v", print_version },
Entry {
.name = "ast",
.handler = set_ast_only_mode,
.internal = true,
},
};
struct Documentation_For_Handler_Entry
{
void *handler;
std::string_view short_documentation{};
std::string_view long_documentation{};
};
static auto documentation_for_handler = std::array {
Documentation_For_Handler_Entry {
.handler = reinterpret_cast<void*>(provide_function),
.short_documentation = "load file as function",
.long_documentation =
"Loads given file, placing it inside a function. The main use for this mechanism is\n"
"to delay execution of a file e.g. to play it using synchronization infrastructure.\n"
"Name of function is derived from file name, replacing special characters with underscores.\n"
"New name is reported when entering interactive mode."
},
Documentation_For_Handler_Entry {
.handler = reinterpret_cast<void*>(provide_file),
.short_documentation = "execute given file",
.long_documentation =
"Run provided Musique source file."
},
Documentation_For_Handler_Entry {
.handler = reinterpret_cast<void*>(set_interactive_mode),
.short_documentation = "enable interactive mode",
.long_documentation =
"Enables interactive mode. It's enabled by default when provided without arguments or\n"
"when all arguments are files loaded as functions."
},
Documentation_For_Handler_Entry {
.handler = reinterpret_cast<void*>(print_help),
.short_documentation = "print help",
.long_documentation =
"Prints short version of help, to provide version easy for quick lookup by the user."
},
Documentation_For_Handler_Entry {
.handler = reinterpret_cast<void*>(print_version),
.short_documentation = "print version information",
.long_documentation =
"Prints version of Musique, following Semantic Versioning.\n"
"It's either '<major>.<minor>.<patch>' for official releases or\n"
"'<major>.<minor>.<patch>-dev+gc<commit hash>' for self-build releases."
},
Documentation_For_Handler_Entry {
.handler = reinterpret_cast<void*>(show_docs),
.short_documentation = "print documentation for given builtin",
.long_documentation =
"Prints documentation for given builtin function (function predefined by language).\n"
"Documentation is in Markdown format and can be passed to render."
},
Documentation_For_Handler_Entry {
.handler = reinterpret_cast<void*>(provide_inline_code),
.short_documentation = "run code from an argument",
.long_documentation =
"Runs code passed as next argument. Same rules apply as for code inside a file."
},
Documentation_For_Handler_Entry {
.handler = reinterpret_cast<void*>(set_ast_only_mode),
.short_documentation = "don't run code, print AST of it",
.long_documentation =
"Parameter made for internal usage. Instead of executing provided code,\n"
"prints program syntax tree."
},
Documentation_For_Handler_Entry {
.handler = reinterpret_cast<void*>(print_manpage),
.short_documentation = "print man page source code to standard output",
.long_documentation =
"Prints Man page document to standard output of Musique full command line interface.\n"
"One can view it with 'musique man > /tmp/musique.1; man /tmp/musique.1'"
},
};
// Supported types of argument input:
// With arity = 0
@ -151,8 +256,8 @@ std::optional<std::string_view> cmd::accept_commandline_argument(std::vector<cmd
state.m_value = args[1];
}
for (auto const& [name, handler] : all_parameters) {
if (name != state.name()) {
for (auto const& p : all_parameters) {
if (p.name != state.name()) {
continue;
}
std::visit(Overloaded {
@ -160,29 +265,48 @@ std::optional<std::string_view> cmd::accept_commandline_argument(std::vector<cmd
state.mark_success();
h();
},
[&state, name=name](Requires_Argument const& h) {
[&state, p](Requires_Argument const& h) {
auto arg = state.value();
if (!arg) {
std::cerr << "musique: error: option " << std::quoted(name) << " requires an argument" << std::endl;
std::cerr << pretty::begin_error << "musique: error:" << pretty::end;
std::cerr << " option " << std::quoted(p.name) << " requires an argument" << std::endl;
std::exit(1);
}
h(*arg);
},
[&state, &runnables, name=name](Defines_Code const& h) {
[&state, &runnables, p](Defines_Code const& h) {
auto arg = state.value();
if (!arg) {
std::cerr << "musique: error: option " << std::quoted(name) << " requires an argument" << std::endl;
std::cerr << pretty::begin_error << "musique: error:" << pretty::end;
std::cerr << " option " << std::quoted(p.name) << " requires an argument" << std::endl;
std::exit(1);
}
runnables.push_back(h(*arg));
}
}, handler);
}, p.handler);
return std::nullopt;
}
return state.name();
}
Documentation_For_Handler_Entry find_documentation_for_handler(void *handler)
{
auto it = std::find_if(documentation_for_handler.begin(), documentation_for_handler.end(),
[=](Documentation_For_Handler_Entry const& e) { return e.handler == handler; });
ensure(it != documentation_for_handler.end(), "Parameter handler doesn't have matching documentation");
return *it;
}
Documentation_For_Handler_Entry find_documentation_for_parameter(std::string_view param)
{
auto entry = std::find_if(all_parameters.begin(), all_parameters.end(),
[=](auto const& e) { return e.name == param; });
ensure(entry != all_parameters.end(), "Cannot find parameter that maches given name");
return find_documentation_for_handler(entry->handler_ptr());
}
void cmd::print_close_matches(std::string_view arg)
{
@ -194,24 +318,149 @@ void cmd::print_close_matches(std::string_view arg)
all_parameters.begin(), all_parameters.end(),
closest.begin(), closest.end(),
[&minimum_distance, arg](auto const& lhs, auto const& rhs) {
auto const lhs_score = edit_distance(arg, lhs.first);
auto const rhs_score = edit_distance(arg, rhs.first);
auto const lhs_score = edit_distance(arg, lhs.name);
auto const rhs_score = edit_distance(arg, rhs.name);
minimum_distance = std::min({ minimum_distance, lhs_score, rhs_score });
return lhs_score < rhs_score;
}
);
std::cout << "The most similar commands are:\n";
std::unordered_set<void*> shown;
std::vector<std::string> shown;
if (minimum_distance <= 3) {
for (auto const& [ name, handler ] : closest) {
auto const handler_p = std::visit([](auto *v) { return reinterpret_cast<void*>(v); }, handler);
if (!shown.contains(handler_p)) {
std::cout << " " << name << std::endl;
shown.insert(handler_p);
for (auto const& p : closest) {
if (std::find(shown.begin(), shown.end(), std::string(p.name)) == shown.end()) {
shown.push_back(std::string(p.name));
}
}
}
if (shown.empty()) {
void *previous = nullptr;
std::cout << "Available subcommands are:\n";
for (auto const& p : all_parameters) {
auto handler_p = p.handler_ptr();
if (std::exchange(previous, handler_p) == handler_p || p.internal) {
continue;
}
std::cout << " " << p.name << " - " << find_documentation_for_handler(handler_p).short_documentation << '\n';
}
} else {
std::cout << "The most similar commands are:\n";
for (auto const& name : shown) {
std::cout << " " << name << " - " << find_documentation_for_parameter(name).short_documentation << '\n';
}
}
std::cout << "\nInvoke 'musique help' to read more about available commands\n";
}
static inline void iterate_over_documentation(
std::ostream& out,
std::string_view Documentation_For_Handler_Entry::* handler,
std::string_view prefix,
std::ostream&(*first)(std::ostream&, std::string_view name))
{
decltype(std::optional(all_parameters.begin())) previous = std::nullopt;
for (auto it = all_parameters.begin();; ++it) {
if (it != all_parameters.end() && it->internal)
continue;
if (it == all_parameters.end() || (previous && it->handler_ptr() != (*previous)->handler_ptr())) {
auto &e = **previous;
switch (e.arguments()) {
break; case 0: out << '\n';
break; case 1: out << " ARG\n";
break; default: unreachable();
}
out << prefix << find_documentation_for_handler(e.handler_ptr()).*handler << "\n\n";
}
if (it == all_parameters.end()) {
break;
}
if (previous && (**previous).handler_ptr() == it->handler_ptr()) {
out << ", " << it->name;
} else {
first(out, it->name);
}
previous = it;
}
}
void cmd::usage()
{
std::cerr << "usage: " << pretty::begin_bold << "musique" << pretty::end << " [subcommand]...\n";
std::cerr << " where available subcommands are:\n";
iterate_over_documentation(std::cerr, &Documentation_For_Handler_Entry::short_documentation, " ",
[](std::ostream& out, std::string_view name) -> std::ostream&
{
return out << " " << pretty::begin_bold << name << pretty::end;
});
std::exit(2);
}
void print_manpage()
{
auto const ymd = std::chrono::year_month_day(
std::chrono::floor<std::chrono::days>(
std::chrono::system_clock::now()
)
);
std::cout << ".TH MUSIQUE 1 "
<< int(ymd.year()) << '-'
<< std::setfill('0') << std::setw(2) << unsigned(ymd.month()) << '-'
<< std::setfill('0') << std::setw(2) << unsigned(ymd.day())
<< " Linux Linux\n";
std::cout << R"troff(.SH NAME
musique \- interactive, musical programming language
.SH SYNOPSIS
.B musique
[
SUBCOMMANDS
]
.SH DESCRIPTION
Musique is an interpreted, interactive, musical domain specific programming language
that allows for algorythmic music composition, live-coding and orchestra performing.
.SH SUBCOMMANDS
All subcommands can be expressed in three styles: -i arg -j -k
.I or
--i=arg --j --k
.I or
i arg j k
)troff";
iterate_over_documentation(std::cout, &Documentation_For_Handler_Entry::long_documentation, {},
[](std::ostream& out, std::string_view name) -> std::ostream&
{
return out << ".TP\n" << name;
});
std::cout << R"troff(.SH ENVIROMENT
.TP
NO_COLOR
This enviroment variable overrides standard Musique color behaviour.
When it's defined, it disables colors and ensures they are not enabled.
.SH FILES
.TP
History file
History file for interactive mode is kept in XDG_DATA_HOME (or similar on other operating systems).
.SH EXAMPLES
.TP
musique \-c "play (c5 + up 12)"
Plays all semitones in 5th octave
.TP
musique run examples/ode-to-joy.mq
Play Ode to Joy written as Musique source code in examples/ode-to-joy.mq
)troff";
std::exit(0);
}
bool cmd::is_tty()

View File

@ -30,6 +30,9 @@ namespace cmd
/// Recognize if stdout is connected to terminal
bool is_tty();
[[noreturn]]
void usage();
}
#endif // MUSIQUE_CMD_HH

View File

@ -3,7 +3,12 @@
#include <optional>
#include <string_view>
#include <vector>
std::optional<std::string_view> find_documentation_for_builtin(std::string_view builtin_name);
/// Returns top 4 similar names to required
std::vector<std::string_view> similar_names_to_builtin(std::string_view builtin_name);
#endif

View File

@ -35,39 +35,6 @@ static unsigned repl_line_number = 1;
#define Ignore(Call) do { auto const ignore_ ## __LINE__ = (Call); (void) ignore_ ## __LINE__; } while(0)
/// Print usage and exit
[[noreturn]] void usage()
{
std::cerr <<
"usage: musique [subcommand]...\n"
" where available subcommands are:\n"
" run FILENAME\n"
" executes given file\n"
"\n"
" fun FILENAME\n"
" load file as function\n"
"\n"
" repl\n"
" enter interactive enviroment\n"
"\n"
" version\n"
" prints Musique interpreter version\n"
"\n"
" doc BUILTIN\n"
" print documentation for given builtin function\n"
"\n"
" help\n"
" prints this message\n"
"\n"
"Thanks to:\n"
" Sy Brand, https://sybrand.ink/, creator of tl::expected https://github.com/TartanLlama/expected\n"
" Justine Tunney, https://justinetunney.com, creator of bestline readline library https://github.com/jart/bestline\n"
" Gary P. Scavone, http://www.music.mcgill.ca/~gary/, creator of rtmidi https://github.com/thestk/rtmidi\n"
" Creators of ableton/link, https://github.com/Ableton/link\n"
;
std::exit(1);
}
void print_repl_help()
{
std::cout <<
@ -334,7 +301,8 @@ static std::optional<Error> Main(std::span<char const*> args)
while (args.size()) if (auto failed = cmd::accept_commandline_argument(runnables, args)) {
std::cerr << "musique: error: Failed to recognize parameter " << std::quoted(*failed) << std::endl;
std::cerr << pretty::begin_error << "musique: error:" << pretty::end;
std::cerr << " Failed to recognize parameter " << std::quoted(*failed) << std::endl;
cmd::print_close_matches(args.front());
std::exit(1);
}
@ -353,7 +321,8 @@ static std::optional<Error> Main(std::span<char const*> args)
eternal_sources.emplace_back(std::istreambuf_iterator<char>(std::cin), std::istreambuf_iterator<char>());
} else {
if (not fs::exists(path)) {
std::cerr << "musique: error: couldn't open file: " << path << std::endl;
std::cerr << pretty::begin_error << "musique: error:" << pretty::end;
std::cerr << " couldn't open file: " << path << std::endl;
std::exit(1);
}
std::ifstream source_file{fs::path(path)};
@ -368,14 +337,11 @@ static std::optional<Error> Main(std::span<char const*> args)
}
}
enable_repl = enable_repl || std::all_of(runnables.begin(), runnables.end(),
[](cmd::Run const& run) {
return run.type == cmd::Run::Deffered_File;
});
enable_repl = enable_repl || (!runnables.empty() && std::all_of(runnables.begin(), runnables.end(),
[](cmd::Run const& run) { return run.type == cmd::Run::Deffered_File; }));
if (runnables.empty() || enable_repl) {
if (enable_repl) {
repl_line_number = 1;
enable_repl = true;
#ifndef _WIN32
bestlineSetCompletionCallback(completion);
#else
@ -417,7 +383,8 @@ static std::optional<Error> Main(std::span<char const*> args)
if (command.starts_with(':')) {
command.remove_prefix(1);
if (!Try(handle_repl_session_commands(command, runner))) {
std::cerr << "musique: error: unrecognized REPL command '" << command << '\'' << std::endl;
std::cerr << pretty::begin_error << "musique: error:" << pretty::end;
std::cerr << " unrecognized REPL command '" << command << '\'' << std::endl;
}
continue;
}

View File

@ -8,10 +8,11 @@ extern "C" {
namespace starters
{
static std::string_view Error;
static std::string_view Path;
static std::string_view Bold;
static std::string_view Comment;
static std::string_view End;
static std::string_view Error;
static std::string_view Path;
}
std::ostream& pretty::begin_error(std::ostream& os)
@ -29,6 +30,11 @@ std::ostream& pretty::begin_comment(std::ostream& os)
return os << starters::Comment;
}
std::ostream& pretty::begin_bold(std::ostream& os)
{
return os << starters::Bold;
}
std::ostream& pretty::end(std::ostream& os)
{
return os << starters::End;
@ -56,16 +62,18 @@ void pretty::terminal_mode()
#endif
starters::Error = "\x1b[31;1m";
starters::Path = "\x1b[34;1m";
starters::Bold = "\x1b[1m";
starters::Comment = "\x1b[30;1m";
starters::End = "\x1b[0m";
starters::Error = "\x1b[31;1m";
starters::Path = "\x1b[34;1m";
}
void pretty::no_color_mode()
{
starters::Error = {};
starters::Path = {};
starters::Bold = {};
starters::Comment = {};
starters::End = {};
starters::Error = {};
starters::Path = {};
}

View File

@ -15,6 +15,9 @@ namespace pretty
/// Mark start of printing a comment
std::ostream& begin_comment(std::ostream&);
/// Mark start of printing with bold face
std::ostream& begin_bold(std::ostream&);
/// Mark end of any above
std::ostream& end(std::ostream&);

View File

@ -70,6 +70,28 @@ std::optional<std::string_view> find_documentation_for_builtin(std::string_view
}
return std::nullopt;
}
std::vector<std::string_view> similar_names_to_builtin(std::string_view builtin_name)
{
auto minimum_distance = std::numeric_limits<int>::max();
std::array<std::pair<std::string_view, std::string_view>, 4> closest;
std::partial_sort_copy(
names_to_documentation.begin(), names_to_documentation.end(),
closest.begin(), closest.end(),
[&minimum_distance, builtin_name](auto const& lhs, auto const& rhs) {
auto const lhs_score = edit_distance(builtin_name, lhs.first);
auto const rhs_score = edit_distance(builtin_name, rhs.first);
minimum_distance = std::min({ minimum_distance, lhs_score, rhs_score });
return lhs_score < rhs_score;
}
);
std::vector<std::string_view> result;
result.resize(4);
std::transform(closest.begin(), closest.end(), result.begin(), [](auto const& p) { return p.first; });
return result;
}
"""
def warning(*args, prefix: str | None = None):
@ -238,8 +260,15 @@ def generate_cpp_documentation(builtins: typing.Iterable[Builtin], output_path:
return f"{builtin.implementation}_doc"
with open(output_path, "w") as out:
print("#include <array>", file=out)
print("#include <musique/interpreter/builtin_function_documentation.hh>\n", file=out)
includes = [
"algorithm",
"array",
"edit_distance.hh",
"musique/interpreter/builtin_function_documentation.hh",
"vector",
]
for include in includes:
print(f"#include <{include}>", file=out)
# 1. Generate strings with documentation
for builtin in builtins: