diff options
author | AnotherTest <ali.mpfard@gmail.com> | 2020-05-27 17:13:40 +0430 |
---|---|---|
committer | Andreas Kling <kling@serenityos.org> | 2020-05-28 11:01:08 +0200 |
commit | 0b98924ee776518b4cfa0c1ae9e0f12c6f233af8 (patch) | |
tree | bdaef4f3fec4574266e446735f8b94acd28a3719 /Userland/xargs.cpp | |
parent | dd2f9658f321b28bdd78a31dd23e786d84a20416 (diff) | |
download | serenity-0b98924ee776518b4cfa0c1ae9e0f12c6f233af8.zip |
Userland: Add a basic xargs
This adds a basic implementation of xargs.
The implemenation is missing quite a few options, and is not entirely
POSIX-compliant, but it gets the job done :^)
Diffstat (limited to 'Userland/xargs.cpp')
-rw-r--r-- | Userland/xargs.cpp | 298 |
1 files changed, 298 insertions, 0 deletions
diff --git a/Userland/xargs.cpp b/Userland/xargs.cpp new file mode 100644 index 0000000000..88628dc4aa --- /dev/null +++ b/Userland/xargs.cpp @@ -0,0 +1,298 @@ +/* + * Copyright (c) 2020, The SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <AK/Function.h> +#include <AK/StdLibExtras.h> +#include <AK/StringBuilder.h> +#include <LibCore/ArgsParser.h> +#include <errno.h> +#include <fcntl.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/wait.h> + +bool run_command(Vector<char*>&& child_argv, bool verbose, bool is_stdin, int devnull_fd); + +enum Decision { + Unget, + Continue, + Stop, +}; +bool read_items(FILE* fp, char entry_separator, Function<Decision(StringView)>); + +class ParsedInitialArguments { +public: + ParsedInitialArguments(Vector<const char*>&, const StringView& placeholder); + + void for_each_joined_argument(const StringView&, Function<void(const String&)>) const; + + size_t size() const { return m_all_parts.size(); } + +private: + Vector<Vector<StringView>> m_all_parts; +}; + +int main(int argc, char** argv) +{ + if (pledge("stdio rpath proc exec", nullptr) < 0) { + perror("pledge"); + return 1; + } + + const char* placeholder = nullptr; + bool split_with_nulls = false; + const char* specified_delimiter = "\n"; + Vector<const char*> arguments; + bool verbose = false; + const char* file_to_read = "-"; + int max_lines_for_one_command = 0; + int max_bytes_for_one_command = ARG_MAX; + + Core::ArgsParser args_parser; + args_parser.add_option(placeholder, "Placeholder string to be replaced in arguments", "replace", 'I', "placeholder"); + args_parser.add_option(split_with_nulls, "Split input items with the null character instead of newline", "null", '0'); + args_parser.add_option(specified_delimiter, "Split the input items with the specified character", "delimiter", 'd', "delim"); + args_parser.add_option(verbose, "Display each command before executing it", "verbose", 'v'); + args_parser.add_option(file_to_read, "Read arguments from the specified file instead of stdin", "arg-file", 'a', "file"); + args_parser.add_option(max_lines_for_one_command, "Use at most max-lines lines to create a command", "line-limit", 'L', "max-lines"); + args_parser.add_option(max_bytes_for_one_command, "Use at most max-chars characters to create a command", "char-limit", 's', "max-chars"); + args_parser.add_positional_argument(arguments, "Command and any initial arguments for it", "command", Core::ArgsParser::Required::No); + args_parser.parse(argc, argv); + + size_t max_bytes = min(ARG_MAX, max_bytes_for_one_command); + size_t max_lines = max(max_lines_for_one_command, 0); + + if (!split_with_nulls && strlen(specified_delimiter) > 1) { + fprintf(stderr, "xargs: the delimiter must be a single byte\n"); + return 1; + } + + char entry_separator = split_with_nulls ? '\0' : *specified_delimiter; + + StringView placeholder_view { placeholder }; + + if (!placeholder_view.is_empty()) + max_lines = 1; + + if (arguments.is_empty()) + arguments.append("echo"); + + ParsedInitialArguments initial_arguments(arguments, placeholder_view); + + FILE* fp = stdin; + bool is_stdin = true; + + if (StringView { "-" } != file_to_read) { + // A file was specified, try to open it. + fp = fopen(file_to_read, "re"); + if (!fp) { + perror("fopen"); + return 1; + } + is_stdin = false; + } + + StringBuilder builder; + Vector<char*> child_argv; + + int devnull_fd = 0; + + if (is_stdin) { + devnull_fd = open("/dev/null", O_RDONLY | O_CLOEXEC); + if (devnull_fd < 0) { + perror("open"); + return 1; + } + } + + size_t total_command_length = 0; + size_t items_used_for_this_command = 0; + + auto fail = read_items(fp, entry_separator, [&](StringView item) { + if (item.ends_with('\n')) + item = item.substring_view(0, item.length() - 1); + + if (item.is_empty()) + return Continue; + + // The first item is processed differently, as all the initial-arguments are processed _with_ that item + // as their substitution target (assuming that substitution is enabled). + // Note that if substitution is not enabled, we manually insert a substitution target at the end of initial-arguments, + // so this item has a place to go. + if (items_used_for_this_command == 0) { + child_argv.ensure_capacity(initial_arguments.size()); + + initial_arguments.for_each_joined_argument(item, [&](const String& string) { + total_command_length += string.length(); + child_argv.append(strdup(string.characters())); + }); + + ++items_used_for_this_command; + } else { + if ((max_lines > 0 && items_used_for_this_command + 1 > max_lines) || total_command_length + item.length() + 1 >= max_bytes) { + // Note: This `move' does not actually move-construct a new Vector at the callsite, it only allows perfect-forwarding + // and does not invalidate `child_argv' in this scope. + // The same applies for the one below. + if (!run_command(move(child_argv), verbose, is_stdin, devnull_fd)) + return Stop; + items_used_for_this_command = 0; + total_command_length = 0; + return Unget; + } else { + child_argv.append(strndup(item.characters_without_null_termination(), item.length())); + total_command_length += item.length(); + ++items_used_for_this_command; + } + } + + return Continue; + }); + + if (!fail && !child_argv.is_empty()) + fail = !run_command(move(child_argv), verbose, is_stdin, devnull_fd); + + if (!is_stdin) + fclose(fp); + + return fail ? 1 : 0; +} + +bool read_items(FILE* fp, char entry_separator, Function<Decision(StringView)> callback) +{ + bool fail = false; + + for (;;) { + char* item = nullptr; + size_t buffer_size = 0; + + auto item_size = getdelim(&item, &buffer_size, entry_separator, fp); + + if (item_size < 0) { + // getdelim() will return -1 and set errno to 0 on EOF. + if (errno != 0) { + perror("getdelim"); + fail = true; + } + break; + } + + Decision decision; + do { + decision = callback(item); + if (decision == Stop) { + free(item); + return true; + } + } while (decision == Unget); + + free(item); + } + + return fail; +} + +bool run_command(Vector<char*>&& child_argv, bool verbose, bool is_stdin, int devnull_fd) +{ + child_argv.append(nullptr); + + if (verbose) { + StringBuilder builder; + builder.join(" ", child_argv); + fprintf(stderr, "xargs: %s\n", builder.to_string().characters()); + fflush(stderr); + } + + auto pid = fork(); + if (pid < 0) { + perror("fork"); + return false; + } + + if (pid == 0) { + if (is_stdin) + dup2(devnull_fd, STDIN_FILENO); + + execvp(child_argv[0], child_argv.data()); + exit(1); + } + + for (auto* ptr : child_argv) + free(ptr); + + child_argv.clear_with_capacity(); + + int wstatus = 0; + if (waitpid(pid, &wstatus, 0) < 0) { + perror("waitpid"); + return false; + } + + if (WIFEXITED(wstatus)) { + if (WEXITSTATUS(wstatus) != 0) + return false; + } else { + return false; + } + + return true; +} + +ParsedInitialArguments::ParsedInitialArguments(Vector<const char*>& arguments, const StringView& placeholder) +{ + m_all_parts.ensure_capacity(arguments.size()); + bool some_argument_has_placeholder = false; + + for (auto argument : arguments) { + StringView arg { argument }; + + if (placeholder.is_empty()) { + m_all_parts.append({ arg }); + } else { + auto parts = arg.split_view(placeholder, true); + some_argument_has_placeholder = some_argument_has_placeholder || parts.size() > 1; + m_all_parts.append(move(parts)); + } + } + + // Append an implicit placeholder at the end if no argument has any placeholders. + if (!some_argument_has_placeholder) { + Vector<StringView> parts; + parts.append(""); + parts.append(""); + m_all_parts.append(move(parts)); + } +} + +void ParsedInitialArguments::for_each_joined_argument(const StringView& separator, Function<void(const String&)> callback) const +{ + StringBuilder builder; + for (auto& parts : m_all_parts) { + builder.clear(); + builder.join(separator, parts); + callback(builder.to_string()); + } +} |