diff --git a/meson.build b/meson.build index 29c747c..b9818c2 100644 --- a/meson.build +++ b/meson.build @@ -51,6 +51,8 @@ subdir('src/uenv') # the uenv executable if uenv_cli uenv_src = [ + 'src/cli/color.cpp', + 'src/cli/help.cpp', 'src/cli/image.cpp', 'src/cli/ls.cpp', 'src/cli/run.cpp', diff --git a/src/cli/color.cpp b/src/cli/color.cpp new file mode 100644 index 0000000..3eb8278 --- /dev/null +++ b/src/cli/color.cpp @@ -0,0 +1,37 @@ +#include +#include + +#include + +#include "color.h" + +namespace color { + +namespace impl { +bool use = true; +} + +void default_color() { + // enable color by default + set_color(true); + + // disable color if NO_COLOR env. variable is set + if (std::getenv("NO_COLOR")) { + set_color(false); + } + + // disable color if stdout is not a terminal + if (!isatty(fileno(stdout))) { + set_color(false); + } +} + +void set_color(bool v) { + impl::use = v; +} + +bool use_color() { + return impl::use; +} + +} // namespace color diff --git a/src/cli/color.h b/src/cli/color.h new file mode 100644 index 0000000..92cfa8b --- /dev/null +++ b/src/cli/color.h @@ -0,0 +1,36 @@ +#pragma once + +#include + +#define MAKE_COLOR(color) \ + static auto color() { \ + return fmt::emphasis::bold | fg(fmt::terminal_color::color); \ + } \ + template constexpr auto color(const S& s) { \ + return use_color() ? fmt::format(color(), "{}", s) : std::string(s); \ + } + +namespace color { + +void default_color(); +void set_color(bool v); +bool use_color(); + +MAKE_COLOR(black) +MAKE_COLOR(red) +MAKE_COLOR(green) +MAKE_COLOR(yellow) +MAKE_COLOR(blue) +MAKE_COLOR(magenta) +MAKE_COLOR(cyan) +MAKE_COLOR(white) +MAKE_COLOR(bright_black) +MAKE_COLOR(bright_red) +MAKE_COLOR(bright_green) +MAKE_COLOR(bright_yellow) +MAKE_COLOR(bright_blue) +MAKE_COLOR(bright_magenta) +MAKE_COLOR(bright_cyan) +MAKE_COLOR(bright_white) + +} // namespace color diff --git a/src/cli/help.cpp b/src/cli/help.cpp new file mode 100644 index 0000000..03248c4 --- /dev/null +++ b/src/cli/help.cpp @@ -0,0 +1,55 @@ +#include + +#include "fmt/format.h" + +#include "color.h" +#include "help.h" + +namespace help { + +block::block(std::string msg) : kind(none), lines{std::move(msg)} { +} + +std::string render(const linebreak&) { + return ""; +} + +std::string render(const block& b) { + using enum help::block::admonition; + std::string result{}; + switch (b.kind) { + case none: + case code: + break; + case note: + result += fmt::format("{} - ", ::color::cyan("Note")); + break; + case xmpl: + result += fmt::format("{} - ", ::color::blue("Example")); + break; + case info: + result += fmt::format("{} - ", ::color::green("Info")); + break; + case warn: + result += fmt::format("{} - ", ::color::red("Warning")); + break; + case depr: + result += fmt::format("{} - ", ::color::red("Deprecated")); + } + bool first = true; + for (auto& l : b.lines) { + if (!first) { + result += "\n"; + } + if (b.kind == code) { + result += fmt::format(" {}", ::color::white(l)); + } else { + result += l; + } + first = false; + } + + return result; +} + +} // namespace help diff --git a/src/cli/help.h b/src/cli/help.h new file mode 100644 index 0000000..fbc4f3e --- /dev/null +++ b/src/cli/help.h @@ -0,0 +1,126 @@ +#pragma once + +#include +#include +#include + +#include +#include + +#include "color.h" + +namespace help { + +struct lst { + std::string content; +}; + +struct block { + enum class admonition : std::uint32_t { + none, + note, + xmpl, + code, + info, + warn, + depr + }; + using enum admonition; + admonition kind = none; + std::vector lines; + + block() = default; + block(std::string); + template + block(admonition k, Args&&... args) + : kind(k), lines{std::forward(args)...} { + } +}; +std::string render(const block&); + +struct linebreak {}; +std::string render(const linebreak&); + +// type erasure for items to print in a help message. +// any type T for which the following is provided will be supported +// std::string render(const T&) +template +concept Renderable = requires(T v) { + { render(v) } -> std::convertible_to; +}; + +class item { + public: + template + item(T impl) : impl_(std::make_unique>(std::move(impl))) { + } + + item(item&& other) = default; + + item(const item& other) : impl_(other.impl_->clone()) { + } + + item& operator=(item&& other) = default; + item& operator=(const item& other) { + return *this = item(other); + } + + std::string render() const { + return impl_->render(); + } + + private: + struct interface { + virtual ~interface() { + } + virtual std::unique_ptr clone() = 0; + virtual std::string render() const = 0; + }; + + std::unique_ptr impl_; + + template struct wrap : interface { + explicit wrap(const T& impl) : wrapped(impl) { + } + explicit wrap(T&& impl) : wrapped(std::move(impl)) { + } + + virtual std::unique_ptr clone() override { + return std::unique_ptr(new wrap(wrapped)); + } + + virtual std::string render() const override { + return help::render(wrapped); + } + + T wrapped; + }; +}; + +} // namespace help + +template <> class fmt::formatter { + public: + // parse format specification and store it: + constexpr auto parse(format_parse_context& ctx) { + return ctx.end(); + } + // format a value using stored specification: + template + constexpr auto format(help::item const& item, FmtContext& ctx) const { + return fmt::format_to(ctx.out(), "{}", item.render()); + } +}; + +template <> class fmt::formatter { + public: + // parse format specification and store it: + constexpr auto parse(format_parse_context& ctx) { + return ctx.end(); + } + // format a value using stored specification: + template + constexpr auto format(help::lst const& l, FmtContext& ctx) const { + return fmt::format_to(ctx.out(), "{}", ::color::yellow(l.content)); + } +}; diff --git a/src/cli/ls.cpp b/src/cli/ls.cpp index 0413c22..c3ec174 100644 --- a/src/cli/ls.cpp +++ b/src/cli/ls.cpp @@ -1,8 +1,9 @@ // vim: ts=4 sts=4 sw=4 et -// #include +#include #include +#include #include #include @@ -11,13 +12,12 @@ #include #include +#include "help.h" #include "ls.h" namespace uenv { -void image_ls_help() { - fmt::println("image ls help"); -} +std::string image_ls_footer(); void image_ls_args::add_cli(CLI::App& cli, [[maybe_unused]] global_settings& settings) { @@ -28,6 +28,8 @@ void image_ls_args::add_cli(CLI::App& cli, "print only the matching records, with no header."); ls_cli->callback( [&settings]() { settings.mode = uenv::cli_mode::image_ls; }); + + ls_cli->footer(image_ls_footer); } int image_ls(const image_ls_args& args, const global_settings& settings) { @@ -105,4 +107,53 @@ int image_ls(const image_ls_args& args, const global_settings& settings) { return 0; } +std::string image_ls_footer() { + using enum help::block::admonition; + std::vector items{ + // clang-format off + help::block{none, "Search for uenv that are available to run." }, + help::linebreak{}, + help::block{xmpl, "list all uenv"}, + help::block{code, "uenv image ls"}, + help::linebreak{}, + help::block{xmpl, "list all uenv"}, + help::block{code, "uenv image ls"}, + help::linebreak{}, + help::block{xmpl, "list all uenv with the name prgenv-gnu"}, + help::block{code, "uenv image ls prgenv-gnu"}, + help::linebreak{}, + help::block{xmpl, "list all uenv with the name prgenv-gnu and version 24.7"}, + help::block{code, "uenv image ls prgenv-gnu/24.7"}, + help::linebreak{}, + help::block{xmpl, "list all uenv with the name prgenv-gnu, version 24.7 and release v2"}, + help::block{code, "uenv image ls prgenv-gnu/24.7:v2"}, + help::linebreak{}, + help::block{xmpl, "use the @ symbol to specify a target system name"}, + help::block{code, "uenv image ls prgenv-gnu@todi"}, + help::block{none, "this feature is useful when using images that were built for a different system", + "than the one you are currently working on."}, + help::linebreak{}, + help::block{xmpl, "use the @ symbol to specify a target system name"}, + help::block{code, "uenv image ls prgenv-gnu@todi"}, + help::block{none, "this feature is useful when using images that were built for a different system", + "than the one you are currently working on."}, + help::linebreak{}, + help::block{xmpl, "use the % symbol to specify a target microarchitecture (uarch)"}, + help::block{code, "uenv image ls prgenv-gnu%gh200"}, + help::block{none, "this feature is useful on a system with multiple uarch."}, + help::linebreak{}, + help::block{xmpl, "list any uenv with a concrete sha256 checksum"}, + help::block{code, "uenv image ls 510094ddb3484e305cb8118e21cbb9c94e9aff2004f0d6499763f42" "bdafccfb5"}, + help::linebreak{}, + help::block{note, "more than one uenv might be listed if there are two uenv that refer", + "to the same underlying uenv sha256."}, + help::linebreak{}, + help::block{xmpl, "search for uenv by id (id is the first 16 characters of the sha256):"}, + help::block{code, "uenv image ls 510094ddb3484e30"}, + // clang-format on + }; + + return fmt::format("{}", fmt::join(items, "\n")); +} + } // namespace uenv diff --git a/src/cli/run.cpp b/src/cli/run.cpp index 22df5d5..074ad19 100644 --- a/src/cli/run.cpp +++ b/src/cli/run.cpp @@ -13,14 +13,13 @@ #include #include +#include "help.h" #include "run.h" #include "uenv.h" namespace uenv { -void run_help() { - fmt::println("run help"); -} +std::string run_footer(); void run_args::add_cli(CLI::App& cli, global_settings& settings) { auto* run_cli = cli.add_subcommand("run", "run a uenv session"); @@ -35,6 +34,7 @@ void run_args::add_cli(CLI::App& cli, global_settings& settings) { "the command to run, including with arguments") ->required(); run_cli->callback([&settings]() { settings.mode = uenv::cli_mode::run; }); + run_cli->footer(run_footer); } int run(const run_args& args, const global_settings& globals) { @@ -68,4 +68,46 @@ int run(const run_args& args, const global_settings& globals) { return util::exec(commands); } +std::string run_footer() { + using enum help::block::admonition; + using help::block; + using help::linebreak; + using help::lst; + std::vector items{ + // clang-format off + block{none, "Run a command in an environment."}, + linebreak{}, + block{xmpl, "run the script job.sh in an evironmnent"}, + block{code, "uenv run prgenv-gnu/24.2:v1 -- ./job.sh"}, + block{none, "This will mount prgenv-gnu, execute job.sh, then return to the calling shell."}, + block{note, "how the command to execute comes after the two dashes '--'."}, + linebreak{}, + block{xmpl, "run the script job.sh in an evironmnent with a view loaded"}, + block{code, "uenv run prgenv-gnu/24.2:v1 --view=default -- ./job.sh"}, + linebreak{}, + block{note, "the spec must uniquely identify the uenv. To ensure this, always use a"}, + block{none, "fully qualified spec in the form of name/version:tag, the unique 16 digit id,"}, + block{none, "or sha256 of a uenv. If more than one uenv match the spec, an error message"}, + block{none, "is printed."}, + linebreak{}, + block{xmpl, "run the job.sh script with two images mounted"}, + block{code, "uenv run prgenv-gnu/24.2:v1 ddt/23.1 -- ./job.sh"}, + linebreak{}, + block{xmpl, "run the job.sh script with two images mounted at specific mount points"}, + block{code, "uenv run prgenv-gnu/24.2:v1:$SCRATCH/pe ddt/23.1:/user-tools -- ./job.sh"}, + block{none, "Here the mount point for each image is specified using a ':'."}, + linebreak{}, + block{note, "uenv must be mounted at the mount point for which they were built."}, + block{none, "If mounted at the wrong location, a warning message will be printed, and"}, + block{none, "views will be disabled."}, + linebreak{}, + block{xmpl, "the run command can be used to execute workflow steps with", "separate environments"}, + block{code, "uenv run gromacs/23.1 -- ./simulation.sh"}, + block{code, "uenv run paraview/5.11 -- ./render.sh"}, + // clang-format on + }; + + return fmt::format("{}", fmt::join(items, "\n")); +} + } // namespace uenv diff --git a/src/cli/start.cpp b/src/cli/start.cpp index afe6996..8c1a2b6 100644 --- a/src/cli/start.cpp +++ b/src/cli/start.cpp @@ -13,14 +13,13 @@ #include #include +#include "help.h" #include "start.h" #include "uenv.h" namespace uenv { -void start_help() { - fmt::println("start help"); -} +std::string start_footer(); void start_args::add_cli(CLI::App& cli, [[maybe_unused]] global_settings& settings) { @@ -33,6 +32,7 @@ void start_args::add_cli(CLI::App& cli, ->required(); start_cli->callback( [&settings]() { settings.mode = uenv::cli_mode::start; }); + start_cli->footer(start_footer); } int start(const start_args& args, @@ -76,4 +76,52 @@ int start(const start_args& args, return util::exec(commands); } +std::string start_footer() { + using enum help::block::admonition; + using help::lst; + std::vector items{ + // clang-format off + help::block{none, "Start a new shell with a uenv environment. The shell will be", + fmt::format("the default shell set using the SHELL envronment variable ({}).", lst("echo $SHELL"))}, + help::linebreak{}, + help::block{note, + "the uenv must have been pulled before it can be used. See the list", + fmt::format("of available uenv using {}.", lst("uenv image ls")), + "If using a path to a squashfs file, you need to have read rights in", + "the path where the file is stored.", + }, + help::linebreak{}, + help::block{xmpl, "start a uenv"}, + help::block{code, "uenv start prgenv-gnu/24.7:v3"}, + help::block{none, "use the full name/version:tag format to disambiguate fully the image "}, + help::linebreak{}, + help::block{info, "uenv will mount the image at the correct location, which for most uenv", + "is /user-enviroment."}, + help::linebreak{}, + help::block{xmpl, fmt::format("start an image built for the system daint using {}", lst("@daint"))}, + help::block{code, "uenv start prgenv-gnu/24.7:v1@daint"}, + help::linebreak{}, + help::block{xmpl, "use the @ symbol to specify a target system name"}, + help::block{code, "uenv start prgenv-gnu@todi"}, + help::block{none, "this feature is useful when using images that were built for a different system", + "than the one you are currently working on."}, + help::linebreak{}, + help::block{xmpl, "two uenv images can be used at the same time"}, + help::block{code, "uenv start prgenv-gnu/24.7:v3,editors/24.7:v1"}, + help::linebreak{}, + help::block{info, "to start two uenv at the same time, they must be mounted at different mount.", + "points. uenv provided by CSCS are designed to be mounted at two locations:", + fmt::format(" - {} programming environments and applications", help::lst("/user-enviroment")), + fmt::format(" - {} tools like debuggers and profilers that are used", help::lst("/user-tools")), + " alongside PE and applications"}, + help::linebreak{}, + help::block{xmpl, "example of using the full specification:"}, + help::block{code, "uenv start prgenv-gnu/24.7:v3:/user-environemnt,editors/24.7:v1:user-tools \\", + " --view=prgenv-gnu:default,editors:modules"}, + help::linebreak{}, + // clang-format on + }; + + return fmt::format("{}", fmt::join(items, "\n")); +} } // namespace uenv diff --git a/src/cli/uenv.cpp b/src/cli/uenv.cpp index dda3dc5..7f90299 100644 --- a/src/cli/uenv.cpp +++ b/src/cli/uenv.cpp @@ -1,7 +1,6 @@ // vim: ts=4 sts=4 sw=4 et #include -#include #include #include @@ -12,21 +11,36 @@ #include #include +#include "color.h" +#include "help.h" #include "image.h" #include "run.h" #include "start.h" #include "uenv.h" +std::string help_footer(); + int main(int argc, char** argv) { uenv::global_settings settings; bool print_version = false; + // enable/disable color depending on NOCOLOR env. var + // and tty terminal status. + color::default_color(); + CLI::App cli(fmt::format("uenv {}", UENV_VERSION)); cli.add_flag("-v,--verbose", settings.verbose, "enable verbose output"); - cli.add_flag("--no-color", settings.no_color, "disable color output"); + cli.add_flag_callback( + "--no-color", []() -> void { color::set_color(false); }, + "disable color output"); + cli.add_flag_callback( + "--color", []() -> void { color::set_color(true); }, + "enable color output"); cli.add_flag("--repo", settings.repo_, "the uenv repository"); cli.add_flag("--version", print_version, "print version"); + cli.footer(help_footer); + uenv::start_args start; uenv::run_args run; uenv::image_args image; @@ -37,6 +51,8 @@ int main(int argc, char** argv) { CLI11_PARSE(cli, argc, argv); + // color::set_color(!settings.no_color); + // Warnings and errors are always logged. The verbosity level is increased // with repeated uses of --verbose. spdlog::level::level_enum console_log_level = spdlog::level::warn; @@ -103,3 +119,26 @@ int main(int argc, char** argv) { return 0; } + +std::string help_footer() { + using enum help::block::admonition; + using help::lst; + + // clang-format off + std::vector items{ + help::block{none, "Use the --help flag in with sub-commands for more information."}, + help::linebreak{}, + help::block{xmpl, fmt::format("use the {} flag to generate more verbose output", lst{"-v"})}, + help::block{code, "uenv -v image ls # info level logging"}, + help::block{code, "uenv -vv image ls # debug level logging"}, + help::linebreak{}, + help::block{xmpl, "get help with the run command"}, + help::block{code, "uenv run --help"}, + help::linebreak{}, + help::block{xmpl, fmt::format("get help with the {} command", lst("image ls"))}, + help::block{code, "uenv image ls --help"}, + }; + // clang-format on + + return fmt::format("{}", fmt::join(items, "\n")); +} diff --git a/src/cli/uenv.h b/src/cli/uenv.h index c4d9ace..a4015e7 100644 --- a/src/cli/uenv.h +++ b/src/cli/uenv.h @@ -18,8 +18,6 @@ struct global_settings { using enum cli_mode; int verbose = 0; - bool no_color = false; - // int mode = mode_none; cli_mode mode = unset; // repo_ is the unverified string description of the repo path that is