From 73a6f2e9edc57ccf24d22b9f68fbdb15a8cd3d6f Mon Sep 17 00:00:00 2001 From: Tim Ledbetter Date: Wed, 12 Jul 2023 17:42:51 +0100 Subject: [PATCH] ps: Add the `-o` option to specify a user-defined column format This option allows the user to change which colums are displayed by giving comma or space separated list of column format specifiers. A column format specifier is of the form: `COLUMN_NAME[=COLUMN_TITLE]`. Where `COLUMN_NAME` is any of: uid, pid, ppid, pgid, sid, state, tty, or cmd. Specifying a `COLUMN_TITLE` will change the name shown in the column header. `COLUMN_TITLE` may be blank. If all given column titles are blank, the header is omitted. --- Base/usr/share/man/man1/ps.md | 30 ++++- Userland/Utilities/ps.cpp | 213 ++++++++++++++++++++++++---------- 2 files changed, 178 insertions(+), 65 deletions(-) diff --git a/Base/usr/share/man/man1/ps.md b/Base/usr/share/man/man1/ps.md index e1b235ea26e..742b37e8b3f 100644 --- a/Base/usr/share/man/man1/ps.md +++ b/Base/usr/share/man/man1/ps.md @@ -5,7 +5,7 @@ ps - list currently running processes ## Synopsis ```**sh -$ ps [--version] [-a] [-A] [-e] [-f] [-p pid-list] [--ppid pid-list] [-q pid-list] [-t tty-list] [-u user-list] +$ ps [--version] [-a] [-A] [-e] [-f] [-o column-format] [-p pid-list] [--ppid pid-list] [-q pid-list] [-t tty-list] [-u user-list] ``` ## Description @@ -18,6 +18,14 @@ For each process, print its PID (process ID), to which TTY it belongs, and invok * `-a`: Consider all processes that are associated with a TTY. * `-A` or `-e`: Consider all processes, not just those in the current TTY. * `-f`: Also print for each process: UID (as resolved username), PPID (parent PID), and STATE (Runnable, Sleeping, Selecting, Reading, etc.) +* `-o column-format`: Specify a user-defined format, as a list of column format specifiers separated by commas or spaces. + + A column format specifier is of the form: `COLUMN_NAME[=COLUMN_TITLE]`. + Where `COLUMN_NAME` is any of the following: `uid`, `pid`, `ppid`, `pgid`, `sid`, `state`, `tty`, or `cmd`. + + Specifying a `COLUMN_TITLE` will change the name shown in the column header. `COLUMN_TITLE` may be blank. + If all given column titles are blank, the column header is omitted. + * `-p pid-list`: Select processes matching any of the given PIDs. `pid-list` is a list of PIDs, separated by commas or spaces. * `--ppid pid-list`: Select processes whose PPID matches any of the given PIDs. `pid-list` is a list of PIDs, separated by commas or spaces. * `-q pid-list`: Only consider the given PIDs, if they exist. Output the processes in the order provided by `pid-list`. `pid-list` is a list of PIDs, separated by commas or spaces. @@ -26,10 +34,30 @@ For each process, print its PID (process ID), to which TTY it belongs, and invok ## Examples +Show all processes (full format): + ```sh $ ps -ef ``` +Show the PID, state and name of all processes + +```sh +$ ps -eo pid,state,cmd +``` + +Show the name and state of PID 42 and rename the first column from CMD to Command: + +```sh +$ ps -q 42 -o cmd=Command,state +``` + +Show name of PID 42 and omit the header entirely + +```sh +$ ps -q 42 -o cmd= +``` + ## See Also * [`pmap`(1)](help://man/1/pmap) * [`lsof`(1)](help://man/1/lsof) diff --git a/Userland/Utilities/ps.cpp b/Userland/Utilities/ps.cpp index ca84b472a2d..ebfa5c0b9cf 100644 --- a/Userland/Utilities/ps.cpp +++ b/Userland/Utilities/ps.cpp @@ -16,6 +16,113 @@ #include #include +#define ENUMERATE_COLUMN_DESCRIPTIONS \ + COLUMN(UserId, "uid", "UID", Alignment::Left) \ + COLUMN(ProcessId, "pid", "PID", Alignment::Right) \ + COLUMN(ParentProcessId, "ppid", "PPID", Alignment::Right) \ + COLUMN(ProcessGroupId, "pgid", "PGID", Alignment::Right) \ + COLUMN(SessionId, "sid", "SID", Alignment::Right) \ + COLUMN(State, "state", "STATE", Alignment::Left) \ + COLUMN(TTY, "tty", "TTY", Alignment::Left) \ + COLUMN(Command, "cmd", "CMD", Alignment::Left) + +enum class ColumnId { +#define COLUMN(column_id, lookup_name, default_title, alignment) column_id, + ENUMERATE_COLUMN_DESCRIPTIONS +#undef COLUMN + __Count +}; + +enum class Alignment { + Left, + Right, +}; + +struct ColumnDescription { + StringView lookup_name; + StringView default_title; + Alignment alignment; +}; + +struct Column { + ColumnId id; + String title; + Alignment alignment; + int width { 0 }; + String buffer {}; +}; + +static Optional column_name_to_id(StringView column_name) +{ +#define COLUMN(column_id, lookup_name, default_title, alignment) \ + if (column_name == lookup_name) \ + return ColumnId::column_id; + ENUMERATE_COLUMN_DESCRIPTIONS +#undef COLUMN + + return {}; +} + +static ErrorOr column_from_id(ColumnId column_id, Optional const& custom_title = {}) +{ + constexpr Array(ColumnId::__Count)> available_columns { { +#define COLUMN(column_id, lookup_name, default_title, alignment) { lookup_name##sv, default_title##sv, alignment }, + ENUMERATE_COLUMN_DESCRIPTIONS +#undef COLUMN + } }; + + auto const& column_description = available_columns[static_cast(column_id)]; + auto title = custom_title.has_value() + ? custom_title.value() + : TRY(String::from_utf8(column_description.default_title)); + + return Column { column_id, title, column_description.alignment }; +} + +static ErrorOr column_to_string(ColumnId column_id, Core::ProcessStatistics process) +{ + switch (column_id) { + case ColumnId::UserId: + return String::from_deprecated_string(process.username); + case ColumnId::ProcessId: + return String::number(process.pid); + case ColumnId::ParentProcessId: + return String::number(process.ppid); + case ColumnId::ProcessGroupId: + return String::number(process.pgid); + case ColumnId::SessionId: + return String::number(process.sid); + case ColumnId::TTY: + return process.tty == "" ? "n/a"_short_string : String::from_deprecated_string(process.tty); + case ColumnId::State: + return process.threads.is_empty() + ? "Zombie"_short_string + : String::from_deprecated_string(process.threads.first().state); + case ColumnId::Command: + return String::from_deprecated_string(process.name); + default: + VERIFY_NOT_REACHED(); + } +} + +static ErrorOr parse_column_format_specifier(StringView column_format_specifier) +{ + auto column_specification_parts = column_format_specifier.split_view('=', SplitBehavior::KeepEmpty); + if (column_specification_parts.size() > 2) + return Error::from_string_literal("Invalid column specifier format"); + + auto column_name = column_specification_parts[0]; + auto maybe_column_id = column_name_to_id(column_name); + if (!maybe_column_id.has_value()) + return Error::from_string_literal("Unknown column"); + + Optional column_title; + if (column_specification_parts.size() == 2) + column_title = TRY(String::from_utf8(column_specification_parts[1])); + + return column_from_id(maybe_column_id.value(), column_title); +} + static ErrorOr> tty_stat_to_pseudo_name(struct stat tty_stat) { int tty_device_major = major(tty_stat.st_rdev); @@ -103,23 +210,12 @@ ErrorOr serenity_main(Main::Arguments arguments) TRY(Core::System::unveil("/dev/", "r")); TRY(Core::System::unveil(nullptr, nullptr)); - enum class Alignment { - Left, - Right, - }; - - struct Column { - String title; - Alignment alignment { Alignment::Left }; - int width { 0 }; - String buffer; - }; - bool every_process_flag = false; bool every_terminal_process_flag = false; bool full_format_flag = false; bool provided_filtering_option = false; bool provided_quick_pid_list = false; + Vector columns; Vector pid_list; Vector parent_pid_list; Vector tty_list; @@ -130,6 +226,14 @@ ErrorOr serenity_main(Main::Arguments arguments) args_parser.add_option(every_process_flag, "Show every process", nullptr, 'A'); args_parser.add_option(every_process_flag, "Show every process (Equivalent to -A)", nullptr, 'e'); args_parser.add_option(full_format_flag, "Full format", nullptr, 'f'); + args_parser.add_option(make_list_option(columns, "Specify a user-defined format.", nullptr, 'o', "column-format", [&](StringView column_format_specifier) -> Optional { + auto column_or_error = parse_column_format_specifier(column_format_specifier); + if (column_or_error.is_error()) { + warnln("Could not parse '{}' as a column format specifier", column_format_specifier); + return {}; + } + return column_or_error.release_value(); + })); args_parser.add_option(make_list_option(pid_list, "Show processes with a matching PID. (Comma- or space-separated list)", nullptr, 'p', "pid-list", [&](StringView pid_string) { provided_filtering_option = true; auto pid = pid_string.to_int(); @@ -180,37 +284,29 @@ ErrorOr serenity_main(Main::Arguments arguments) return 1; } - Vector columns; + if (columns.is_empty()) { + auto add_default_column = [&](ColumnId column_id) -> ErrorOr { + auto column = TRY(column_from_id(column_id)); + columns.unchecked_append(move(column)); + return {}; + }; - Optional uid_column; - Optional pid_column; - Optional ppid_column; - Optional pgid_column; - Optional sid_column; - Optional state_column; - Optional tty_column; - Optional cmd_column; - - auto add_column = [&](auto title, auto alignment) { - columns.unchecked_append({ title, alignment, 0, {} }); - return columns.size() - 1; - }; - - if (full_format_flag) { - TRY(columns.try_ensure_capacity(8)); - uid_column = add_column("UID"_short_string, Alignment::Left); - pid_column = add_column("PID"_short_string, Alignment::Right); - ppid_column = add_column("PPID"_short_string, Alignment::Right); - pgid_column = add_column("PGID"_short_string, Alignment::Right); - sid_column = add_column("SID"_short_string, Alignment::Right); - state_column = add_column("STATE"_short_string, Alignment::Left); - tty_column = add_column("TTY"_short_string, Alignment::Left); - cmd_column = add_column("CMD"_short_string, Alignment::Left); - } else { - TRY(columns.try_ensure_capacity(3)); - pid_column = add_column("PID"_short_string, Alignment::Right); - tty_column = add_column("TTY"_short_string, Alignment::Left); - cmd_column = add_column("CMD"_short_string, Alignment::Left); + if (full_format_flag) { + TRY(columns.try_ensure_capacity(8)); + TRY(add_default_column(ColumnId::UserId)); + TRY(add_default_column(ColumnId::ProcessId)); + TRY(add_default_column(ColumnId::ParentProcessId)); + TRY(add_default_column(ColumnId::ProcessGroupId)); + TRY(add_default_column(ColumnId::SessionId)); + TRY(add_default_column(ColumnId::State)); + TRY(add_default_column(ColumnId::TTY)); + TRY(add_default_column(ColumnId::Command)); + } else { + TRY(columns.try_ensure_capacity(3)); + TRY(add_default_column(ColumnId::ProcessId)); + TRY(add_default_column(ColumnId::TTY)); + TRY(add_default_column(ColumnId::Command)); + } } auto all_processes = TRY(Core::ProcessStatisticsReader::get_all()); @@ -252,32 +348,21 @@ ErrorOr serenity_main(Main::Arguments arguments) Vector header; TRY(header.try_ensure_capacity(columns.size())); - for (auto& column : columns) + auto header_is_empty = true; + for (auto& column : columns) { + if (!column.title.is_empty()) + header_is_empty = false; header.unchecked_append(column.title); - rows.unchecked_append(move(header)); + } + + if (!header_is_empty) + rows.unchecked_append(move(header)); for (auto const& process : processes) { Vector row; - TRY(row.try_resize(columns.size())); - - if (uid_column.has_value()) - row[*uid_column] = TRY(String::from_deprecated_string(process.username)); - if (pid_column.has_value()) - row[*pid_column] = TRY(String::number(process.pid)); - if (ppid_column.has_value()) - row[*ppid_column] = TRY(String::number(process.ppid)); - if (pgid_column.has_value()) - row[*pgid_column] = TRY(String::number(process.pgid)); - if (sid_column.has_value()) - row[*sid_column] = TRY(String::number(process.sid)); - if (tty_column.has_value()) - row[*tty_column] = process.tty == "" ? "n/a"_short_string : TRY(String::from_deprecated_string(process.tty)); - if (state_column.has_value()) - row[*state_column] = process.threads.is_empty() - ? "Zombie"_short_string - : TRY(String::from_deprecated_string(process.threads.first().state)); - if (cmd_column.has_value()) - row[*cmd_column] = TRY(String::from_deprecated_string(process.name)); + TRY(row.try_ensure_capacity(columns.size())); + for (auto const& column : columns) + row.unchecked_append(TRY(column_to_string(column.id, process))); rows.unchecked_append(move(row)); }