diff --git a/apps/cat.cpp b/apps/cat.cpp index 5ac525fb..7236fbf9 100644 --- a/apps/cat.cpp +++ b/apps/cat.cpp @@ -30,6 +30,7 @@ Result luna_main(int argc, char** argv) StringView filename; os::ArgumentParser parser; + parser.add_description("Concatenate files to standard output."_sv); parser.add_positional_argument(filename, "file"_sv, "-"_sv); Vector extra_files = TRY(parser.parse(argc, argv)); diff --git a/apps/chmod.cpp b/apps/chmod.cpp index 8440c8bf..e1f3f206 100644 --- a/apps/chmod.cpp +++ b/apps/chmod.cpp @@ -9,6 +9,7 @@ int main(int argc, char** argv) StringView path; os::ArgumentParser parser; + parser.add_description("Change the permissions of a file."_sv); parser.add_positional_argument(mode_string, "mode"_sv, true); parser.add_positional_argument(path, "path"_sv, true); parser.parse(argc, argv); diff --git a/apps/chown.cpp b/apps/chown.cpp index cf7e64c2..36f88b7e 100644 --- a/apps/chown.cpp +++ b/apps/chown.cpp @@ -9,6 +9,7 @@ int main(int argc, char** argv) StringView path; os::ArgumentParser parser; + parser.add_description("Change the owner and group of a file."_sv); parser.add_positional_argument(user, "user"_sv, true); parser.add_positional_argument(path, "path"_sv, true); parser.parse(argc, argv); diff --git a/apps/date.cpp b/apps/date.cpp index 3019f69d..e20a2d5b 100644 --- a/apps/date.cpp +++ b/apps/date.cpp @@ -10,7 +10,9 @@ int main(int argc, char** argv) StringView date; os::ArgumentParser parser; - parser.add_value_argument(date, 'd', "date"_sv, true); + parser.add_description("Display the current (or another) date and time."_sv); + parser.add_value_argument(date, 'd', "date"_sv, true, + "the UNIX timestamp to display instead of the current time"_sv); parser.parse(argc, argv); time_t now; diff --git a/apps/edit.cpp b/apps/edit.cpp index 1bfe0c9e..88039dcb 100644 --- a/apps/edit.cpp +++ b/apps/edit.cpp @@ -9,6 +9,7 @@ Result luna_main(int argc, char** argv) StringView pathname; os::ArgumentParser parser; + parser.add_description("Edit a file using basic line-based shell editing."_sv); parser.add_positional_argument(pathname, "path"_sv, true); parser.parse(argc, argv); diff --git a/apps/ls.cpp b/apps/ls.cpp index 2b848022..4053a9e9 100644 --- a/apps/ls.cpp +++ b/apps/ls.cpp @@ -12,9 +12,10 @@ Result luna_main(int argc, char** argv) bool show_almost_all { false }; os::ArgumentParser parser; + parser.add_description("List files contained in a directory (defaults to '.', the current directory)"_sv); parser.add_positional_argument(pathname, "directory"_sv, "."_sv); - parser.add_switch_argument(show_all, 'a', "all"_sv); - parser.add_switch_argument(show_almost_all, 'A', "almost-all"_sv); + parser.add_switch_argument(show_all, 'a', "all"_sv, "also list hidden files (whose filename begins with a dot)"_sv); + parser.add_switch_argument(show_almost_all, 'A', "almost-all"_sv, "list all files except '.' and '..'"_sv); parser.parse(argc, argv); DIR* dp = opendir(pathname.chars()); diff --git a/apps/mkdir.cpp b/apps/mkdir.cpp index 8927e660..e4a1ef8d 100644 --- a/apps/mkdir.cpp +++ b/apps/mkdir.cpp @@ -30,9 +30,11 @@ Result luna_main(int argc, char** argv) bool recursive; os::ArgumentParser parser; + parser.add_description("Create directories."_sv); parser.add_positional_argument(path, "path"_sv, true); parser.add_positional_argument(mode_string, "mode"_sv, "755"_sv); - parser.add_switch_argument(recursive, 'p', "parents"_sv); + parser.add_switch_argument(recursive, 'p', "parents"_sv, + "if parent directories do not exist, create them as well"_sv); parser.parse(argc, argv); mode_t mode = (mode_t)parse_unsigned_integer(mode_string.chars(), nullptr, 8); diff --git a/apps/rm.cpp b/apps/rm.cpp index 096336e1..494c89a2 100644 --- a/apps/rm.cpp +++ b/apps/rm.cpp @@ -7,8 +7,10 @@ Result luna_main(int argc, char** argv) bool recursive; os::ArgumentParser parser; + parser.add_description("Remove a path from the file system."_sv); parser.add_positional_argument(path, "path"_sv, true); - parser.add_switch_argument(recursive, 'r', "recursive"_sv); + parser.add_switch_argument(recursive, 'r', "recursive"_sv, + "remove a directory recursively (by default, rm removes only empty directories)"_sv); parser.parse(argc, argv); if (!recursive) TRY(os::FileSystem::remove(path)); diff --git a/apps/sh.cpp b/apps/sh.cpp index 0e16bcc8..42a73c63 100644 --- a/apps/sh.cpp +++ b/apps/sh.cpp @@ -37,8 +37,9 @@ Result luna_main(int argc, char** argv) SharedPtr input_file; os::ArgumentParser parser; + parser.add_description("The Luna system's command shell."_sv); parser.add_positional_argument(path, "path"_sv, "-"_sv); - parser.add_value_argument(command, 'c', "command"_sv, true); + parser.add_value_argument(command, 'c', "command"_sv, true, "execute a single command and then exit"_sv); parser.parse(argc, argv); if (!command.is_empty()) TRY(execute_command(command)); diff --git a/apps/su.cpp b/apps/su.cpp index f699dc0e..60c990fc 100644 --- a/apps/su.cpp +++ b/apps/su.cpp @@ -62,6 +62,7 @@ Result luna_main(int argc, char** argv) } os::ArgumentParser parser; + parser.add_description("Switch to a different user."_sv); parser.add_positional_argument(name, "name"_sv, true); parser.parse(argc, argv); diff --git a/libos/include/os/ArgumentParser.h b/libos/include/os/ArgumentParser.h index 8be7aa56..5f04382a 100644 --- a/libos/include/os/ArgumentParser.h +++ b/libos/include/os/ArgumentParser.h @@ -7,15 +7,19 @@ namespace os class ArgumentParser { public: - ArgumentParser() = default; + ArgumentParser(bool add_help = true); + + void add_description(StringView description); Result add_positional_argument(StringView& out, StringView name, bool required); Result add_positional_argument(StringView& out, StringView name, StringView fallback); - Result add_switch_argument(bool& out, char short_flag, StringView long_flag); + Result add_switch_argument(bool& out, char short_flag, StringView long_flag, StringView help = {}); - Result add_value_argument(StringView& out, char short_flag, StringView long_flag, bool value_required); - Result add_value_argument(StringView& out, char short_flag, StringView long_flag, StringView fallback); + Result add_value_argument(StringView& out, char short_flag, StringView long_flag, bool value_required, + StringView help = {}); + Result add_value_argument(StringView& out, char short_flag, StringView long_flag, StringView fallback, + StringView help = {}); Result> parse(int argc, char* const* argv); @@ -33,6 +37,7 @@ namespace os bool* out; char short_flag; StringView long_flag; + StringView help; }; struct ValueArgument @@ -42,10 +47,17 @@ namespace os StringView long_flag; bool required; StringView fallback; + StringView help; }; + Result usage(StringView program_name); + void short_usage(StringView program_name); + Vector m_positional_args; Vector m_switch_args; Vector m_value_args; + StringView m_description = {}; + bool m_add_short_help_flag { false }; + bool m_add_long_help_flag { false }; }; } diff --git a/libos/src/ArgumentParser.cpp b/libos/src/ArgumentParser.cpp index 09022fdd..f7a0ea0f 100644 --- a/libos/src/ArgumentParser.cpp +++ b/libos/src/ArgumentParser.cpp @@ -1,9 +1,19 @@ +#include #include #include #include namespace os { + ArgumentParser::ArgumentParser(bool add_help) : m_add_short_help_flag(add_help), m_add_long_help_flag(add_help) + { + } + + void ArgumentParser::add_description(StringView description) + { + m_description = description; + } + Result ArgumentParser::add_positional_argument(StringView& out, StringView name, bool required) { PositionalArgument arg { &out, name, required, {} }; @@ -18,25 +28,34 @@ namespace os return m_positional_args.try_append(move(arg)); } - Result ArgumentParser::add_switch_argument(bool& out, char short_flag, StringView long_flag) + Result ArgumentParser::add_switch_argument(bool& out, char short_flag, StringView long_flag, StringView help) { - SwitchArgument arg { &out, short_flag, long_flag }; + SwitchArgument arg { &out, short_flag, long_flag, help }; + + if (short_flag == 'h') m_add_short_help_flag = false; + if (long_flag == "help"_sv) m_add_long_help_flag = false; return m_switch_args.try_append(move(arg)); } Result ArgumentParser::add_value_argument(StringView& out, char short_flag, StringView long_flag, - bool value_required) + bool value_required, StringView help) { - ValueArgument arg { &out, short_flag, long_flag, value_required, {} }; + ValueArgument arg { &out, short_flag, long_flag, value_required, {}, help }; + + if (short_flag == 'h') m_add_short_help_flag = false; + if (long_flag == "help"_sv) m_add_long_help_flag = false; return m_value_args.try_append(move(arg)); } Result ArgumentParser::add_value_argument(StringView& out, char short_flag, StringView long_flag, - StringView fallback) + StringView fallback, StringView help) { - ValueArgument arg { &out, short_flag, long_flag, false, fallback }; + ValueArgument arg { &out, short_flag, long_flag, false, fallback, help }; + + if (short_flag == 'h') m_add_short_help_flag = false; + if (long_flag == "help"_sv) m_add_long_help_flag = false; return m_value_args.try_append(move(arg)); } @@ -87,6 +106,8 @@ namespace os bool found = false; + if (m_add_long_help_flag && flag == "help"_sv) { TRY(usage(program_name)); } + for (const auto& current : m_switch_args) { if (current.long_flag == flag) @@ -111,7 +132,7 @@ namespace os if (found) continue; fprintf(stderr, "%s: unrecognized option '%s'\n", program_name.chars(), arg.chars()); - exit(1); + short_usage(program_name); } else if (looks_like_short_flag(arg)) { @@ -122,6 +143,8 @@ namespace os char c = flags[j]; bool found = false; + if (m_add_short_help_flag && c == 'h') { TRY(usage(program_name)); } + // Last flag, this could be a value flag if (j + 1 == flags.length()) { @@ -156,7 +179,7 @@ namespace os if (found) continue; fprintf(stderr, "%s: invalid option -- '%c'\n", program_name.chars(), c); - exit(1); + short_usage(program_name); } continue; @@ -179,7 +202,7 @@ namespace os { fprintf(stderr, "%s: option '--%s' requires an argument\n", program_name.chars(), current_value_argument->long_flag.chars()); - exit(1); + short_usage(program_name); } else { *current_value_argument->out = current_value_argument->fallback; } } @@ -190,11 +213,86 @@ namespace os if (arg.required) { fprintf(stderr, "%s: required argument '%s' not provided\n", program_name.chars(), arg.name.chars()); - exit(1); + short_usage(program_name); } else { *arg.out = arg.fallback; } } return leftovers; } + + Result ArgumentParser::usage(StringView program_name) + { + StringBuilder sb; + TRY(sb.format("Usage: %s "_sv, program_name.chars())); + + for (const auto& arg : m_positional_args) + { + if (arg.required) { TRY(sb.format(" %s", arg.name.chars())); } + else { TRY(sb.format(" [%s]", arg.name.chars())); } + } + + TRY(sb.add('\n')); + + auto usage_line = TRY(sb.string()); + fputs(usage_line.chars(), stdout); + + if (!m_description.is_empty()) { puts(m_description.chars()); } + + if (m_switch_args.size() || m_value_args.size()) + { + putchar('\n'); + puts("Options:"); + } + + for (const auto& arg : m_switch_args) + { + if (arg.long_flag.is_empty()) + { + if (arg.short_flag == ' ') continue; + + printf(" -%-25c %s\n", arg.short_flag, arg.help.chars()); + } + else if (arg.short_flag == ' ') { printf(" --%-20s %s\n", arg.long_flag.chars(), arg.help.chars()); } + else { printf(" -%c, --%-20s %s\n", arg.short_flag, arg.long_flag.chars(), arg.help.chars()); } + } + + for (const auto& arg : m_value_args) + { + StringView value_name; + if (arg.required) value_name = "VALUE"_sv; + else + value_name = "[VALUE]"_sv; + + int field_size = 20 - (int)(arg.long_flag.length() + 1); + if (field_size < 0) field_size = 0; + + if (arg.long_flag.is_empty()) + { + if (arg.short_flag == ' ') continue; + + printf(" -%c %-20s %s\n", arg.short_flag, value_name.chars(), arg.help.chars()); + } + else if (arg.short_flag == ' ') + { + printf(" --%s %-*s %s\n", arg.long_flag.chars(), field_size, value_name.chars(), arg.help.chars()); + } + else + { + printf(" -%c, --%s %-*s %s\n", arg.short_flag, arg.long_flag.chars(), field_size, value_name.chars(), + arg.help.chars()); + } + } + + exit(0); + } + + void ArgumentParser::short_usage(StringView program_name) + { + if (m_add_short_help_flag || m_add_long_help_flag) + fprintf(stderr, "Try running '%s %s' for more information.\n", program_name.chars(), + m_add_long_help_flag ? "--help" : "-h"); + + exit(1); + } }