Serenity Operating System
1/*
2 * Copyright (c) 2020, the SerenityOS developers.
3 *
4 * SPDX-License-Identifier: BSD-2-Clause
5 */
6
7#include <AK/Function.h>
8#include <AK/StdLibExtras.h>
9#include <AK/StringBuilder.h>
10#include <LibCore/ArgsParser.h>
11#include <LibCore/System.h>
12#include <errno.h>
13#include <fcntl.h>
14#include <stdio.h>
15#include <stdlib.h>
16#include <string.h>
17#include <sys/wait.h>
18#include <unistd.h>
19
20bool run_command(Vector<char*>&& child_argv, bool verbose, bool is_stdin, int devnull_fd);
21
22enum Decision {
23 Unget,
24 Continue,
25 Stop,
26};
27bool read_items(FILE* fp, char entry_separator, Function<Decision(StringView)>);
28
29class ParsedInitialArguments {
30public:
31 ParsedInitialArguments(Vector<DeprecatedString>&, StringView placeholder);
32
33 void for_each_joined_argument(StringView, Function<void(DeprecatedString const&)>) const;
34
35 size_t size() const { return m_all_parts.size(); }
36
37private:
38 Vector<Vector<StringView>> m_all_parts;
39};
40
41ErrorOr<int> serenity_main(Main::Arguments main_arguments)
42{
43 TRY(Core::System::pledge("stdio rpath proc exec"));
44
45 StringView placeholder;
46 bool split_with_nulls = false;
47 DeprecatedString specified_delimiter = "\n"sv;
48 Vector<DeprecatedString> arguments;
49 bool verbose = false;
50 DeprecatedString file_to_read = "-"sv;
51 int max_lines_for_one_command = 0;
52 int max_bytes_for_one_command = ARG_MAX;
53
54 Core::ArgsParser args_parser;
55 args_parser.set_stop_on_first_non_option(true);
56 args_parser.set_general_help("Read arguments from stdin and interpret them as command-line arguments for another program. See also: 'man xargs'.");
57 args_parser.add_option(placeholder, "Placeholder string to be replaced in arguments", "replace", 'I', "placeholder");
58 args_parser.add_option(split_with_nulls, "Split input items with the null character instead of newline", "null", '0');
59 args_parser.add_option(specified_delimiter, "Split the input items with the specified character", "delimiter", 'd', "delim");
60 args_parser.add_option(verbose, "Display each command before executing it", "verbose", 'v');
61 args_parser.add_option(file_to_read, "Read arguments from the specified file instead of stdin", "arg-file", 'a', "file");
62 args_parser.add_option(max_lines_for_one_command, "Use at most max-lines lines to create a command", "line-limit", 'L', "max-lines");
63 args_parser.add_option(max_bytes_for_one_command, "Use at most max-chars characters to create a command", "char-limit", 's', "max-chars");
64 args_parser.add_positional_argument(arguments, "Command and any initial arguments for it", "command", Core::ArgsParser::Required::No);
65 args_parser.parse(main_arguments);
66
67 size_t max_bytes = min(ARG_MAX, max_bytes_for_one_command);
68 size_t max_lines = max(max_lines_for_one_command, 0);
69
70 if (!split_with_nulls && specified_delimiter.length() > 1) {
71 warnln("xargs: the delimiter must be a single byte");
72 return 1;
73 }
74
75 char entry_separator = split_with_nulls ? '\0' : specified_delimiter[0];
76
77 if (!placeholder.is_empty())
78 max_lines = 1;
79
80 if (arguments.is_empty())
81 arguments.append("echo");
82
83 ParsedInitialArguments initial_arguments(arguments, placeholder);
84
85 FILE* fp = stdin;
86 bool is_stdin = true;
87
88 if ("-"sv != file_to_read) {
89 // A file was specified, try to open it.
90 fp = fopen(file_to_read.characters(), "re");
91 if (!fp) {
92 perror("fopen");
93 return 1;
94 }
95 is_stdin = false;
96 }
97
98 StringBuilder builder;
99 Vector<char*> child_argv;
100
101 int devnull_fd = 0;
102
103 if (is_stdin) {
104 devnull_fd = TRY(Core::System::open("/dev/null"sv, O_RDONLY | O_CLOEXEC));
105 }
106
107 size_t total_command_length = 0;
108 size_t items_used_for_this_command = 0;
109
110 auto fail = read_items(fp, entry_separator, [&](StringView item) {
111 if (item.ends_with('\n'))
112 item = item.substring_view(0, item.length() - 1);
113
114 if (item.is_empty())
115 return Continue;
116
117 // The first item is processed differently, as all the initial-arguments are processed _with_ that item
118 // as their substitution target (assuming that substitution is enabled).
119 // Note that if substitution is not enabled, we manually insert a substitution target at the end of initial-arguments,
120 // so this item has a place to go.
121 if (items_used_for_this_command == 0) {
122 child_argv.ensure_capacity(initial_arguments.size());
123
124 initial_arguments.for_each_joined_argument(item, [&](const DeprecatedString& string) {
125 total_command_length += string.length();
126 child_argv.append(strdup(string.characters()));
127 });
128
129 ++items_used_for_this_command;
130 } else {
131 if ((max_lines > 0 && items_used_for_this_command + 1 > max_lines) || total_command_length + item.length() + 1 >= max_bytes) {
132 // Note: This `move' does not actually move-construct a new Vector at the callsite, it only allows perfect-forwarding
133 // and does not invalidate `child_argv' in this scope.
134 // The same applies for the one below.
135 if (!run_command(move(child_argv), verbose, is_stdin, devnull_fd))
136 return Stop;
137 items_used_for_this_command = 0;
138 total_command_length = 0;
139 return Unget;
140 } else {
141 child_argv.append(strndup(item.characters_without_null_termination(), item.length()));
142 total_command_length += item.length();
143 ++items_used_for_this_command;
144 }
145 }
146
147 return Continue;
148 });
149
150 if (!fail && !child_argv.is_empty())
151 fail = !run_command(move(child_argv), verbose, is_stdin, devnull_fd);
152
153 if (!is_stdin)
154 fclose(fp);
155
156 return fail ? 1 : 0;
157}
158
159bool read_items(FILE* fp, char entry_separator, Function<Decision(StringView)> callback)
160{
161 bool fail = false;
162
163 for (;;) {
164 char* item = nullptr;
165 size_t buffer_size = 0;
166
167 auto item_size = getdelim(&item, &buffer_size, entry_separator, fp);
168
169 if (item_size < 0) {
170 // getdelim() will return -1 and set errno to 0 on EOF.
171 if (errno != 0) {
172 perror("getdelim");
173 fail = true;
174 }
175 free(item);
176 break;
177 }
178
179 Decision decision;
180 do {
181 decision = callback({ item, strlen(item) });
182 if (decision == Stop) {
183 free(item);
184 return true;
185 }
186 } while (decision == Unget);
187
188 free(item);
189 }
190
191 return fail;
192}
193
194bool run_command(Vector<char*>&& child_argv, bool verbose, bool is_stdin, int devnull_fd)
195{
196 child_argv.append(nullptr);
197
198 if (verbose) {
199 StringBuilder builder;
200 builder.join(' ', child_argv);
201 warnln("xargs: {}", builder.to_deprecated_string());
202 }
203
204 auto pid = fork();
205 if (pid < 0) {
206 perror("fork");
207 return false;
208 }
209
210 if (pid == 0) {
211 if (is_stdin)
212 dup2(devnull_fd, STDIN_FILENO);
213
214 execvp(child_argv[0], child_argv.data());
215 exit(1);
216 }
217
218 for (auto* ptr : child_argv)
219 free(ptr);
220
221 child_argv.clear_with_capacity();
222
223 int wstatus = 0;
224 if (waitpid(pid, &wstatus, 0) < 0) {
225 perror("waitpid");
226 return false;
227 }
228
229 if (WIFEXITED(wstatus)) {
230 if (WEXITSTATUS(wstatus) != 0)
231 return false;
232 } else {
233 return false;
234 }
235
236 return true;
237}
238
239ParsedInitialArguments::ParsedInitialArguments(Vector<DeprecatedString>& arguments, StringView placeholder)
240{
241 m_all_parts.ensure_capacity(arguments.size());
242 bool some_argument_has_placeholder = false;
243
244 for (auto arg : arguments) {
245 if (placeholder.is_empty()) {
246 m_all_parts.append({ arg });
247 } else {
248 auto parts = arg.view().split_view(placeholder, SplitBehavior::KeepEmpty);
249 some_argument_has_placeholder = some_argument_has_placeholder || parts.size() > 1;
250 m_all_parts.append(move(parts));
251 }
252 }
253
254 // Append an implicit placeholder at the end if no argument has any placeholders.
255 if (!some_argument_has_placeholder) {
256 Vector<StringView> parts;
257 parts.append(""sv);
258 parts.append(""sv);
259 m_all_parts.append(move(parts));
260 }
261}
262
263void ParsedInitialArguments::for_each_joined_argument(StringView separator, Function<void(DeprecatedString const&)> callback) const
264{
265 StringBuilder builder;
266 for (auto& parts : m_all_parts) {
267 builder.clear();
268 builder.join(separator, parts);
269 callback(builder.to_deprecated_string());
270 }
271}