1 /*
2 mkvmerge -- utility for splicing together matroska files
3 from component media subtypes
4
5 Distributed under the GPL v2
6 see the file COPYING for details
7 or visit https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
8
9 definitions used in all programs, helper functions
10
11 Written by Moritz Bunkus <moritz@bunkus.org>.
12 */
13
14 #include "common/common_pch.h"
15
16 #include <algorithm>
17 #include <cstring>
18 #ifdef SYS_WINDOWS
19 # include <windows.h>
20 #endif
21
22 #include "common/command_line.h"
23 #if defined(SYS_APPLE)
24 # include "common/fs_sys_helpers.h"
25 #endif
26 #include "common/hacks.h"
27 #include "common/json.h"
28 #include "common/mm_io_x.h"
29 #include "common/mm_file_io.h"
30 #include "common/mm_proxy_io.h"
31 #include "common/mm_text_io.h"
32 #include "common/mm_write_buffer_io.h"
33 #include "common/strings/editing.h"
34 #include "common/strings/utf8.h"
35 #include "common/translation.h"
36 #include "common/version.h"
37
38 namespace mtx::cli {
39
40 bool g_gui_mode = false;
41 bool g_abort_on_warnings = false;
42
43 static void
read_args_from_json_file(std::vector<std::string> & args,std::string const & filename)44 read_args_from_json_file(std::vector<std::string> &args,
45 std::string const &filename) {
46 std::string buffer;
47
48 try {
49 auto io = std::make_shared<mm_text_io_c>(std::make_shared<mm_file_io_c>(filename));
50 io->read(buffer, io->get_size());
51
52 } catch (mtx::mm_io::exception &ex) {
53 mxerror(fmt::format(Y("The file '{0}' could not be opened for reading: {1}.\n"), filename, ex));
54 }
55
56 try {
57 auto doc = mtx::json::parse(buffer);
58 auto skip_next = false;
59
60 if (!doc.is_array())
61 throw std::domain_error{Y("JSON option files must contain a JSON array consisting solely of JSON strings")};
62
63 for (auto const &val : doc) {
64 if (!val.is_string())
65 throw std::domain_error{Y("JSON option files must contain a JSON array consisting solely of JSON strings")};
66
67 if (skip_next) {
68 skip_next = false;
69 continue;
70 }
71
72 auto string = val.get<std::string>();
73 if (string == "--command-line-charset")
74 skip_next = true;
75
76 else
77 args.push_back(string);
78 }
79
80 } catch (std::exception const &ex) {
81 mxerror(fmt::format("The JSON option file '{0}' contains an error: {1}.\n", filename, ex.what()));
82 }
83 }
84
85 /** \brief Expand the command line parameters
86
87 Takes each command line paramter, converts it to UTF-8, and reads more
88 commands from command files if the argument starts with '@'. Puts all
89 arguments into a new array.
90 On Windows it uses the \c GetCommandLineW() function. That way it can
91 also handle multi-byte input like Japanese file names.
92
93 \param argc The number of arguments. This is the same argument that
94 \c main normally receives.
95 \param argv The arguments themselves. This is the same argument that
96 \c main normally receives.
97 \return An array of strings converted to UTF-8 containing all the
98 command line arguments and any arguments read from option files.
99 */
100 #if !defined(SYS_WINDOWS)
101 std::vector<std::string>
args_in_utf8(int argc,char ** argv)102 args_in_utf8(int argc,
103 char **argv) {
104 int i;
105 std::vector<std::string> args;
106
107 charset_converter_cptr cc_command_line = g_cc_stdio;
108
109 for (i = 1; i < argc; i++)
110 if (argv[i][0] == '@')
111 read_args_from_json_file(args, &argv[i][1]);
112 else {
113 if (!strcmp(argv[i], "--command-line-charset")) {
114 if ((i + 1) == argc)
115 mxerror(Y("'--command-line-charset' is missing its argument.\n"));
116 cc_command_line = charset_converter_c::init(!argv[i + 1] ? "" : argv[i + 1]);
117 i++;
118 } else
119 args.push_back(cc_command_line->utf8(argv[i]));
120 }
121
122 #if defined(SYS_APPLE)
123 // Always use NFD on macOS, no matter which normalization form the
124 // command-line arguments used.
125 for (auto &arg : args)
126 arg = mtx::sys::normalize_unicode_string(arg, mtx::sys::unicode_normalization_form_e::d);
127 #endif // SYS_APPLE
128
129 return args;
130 }
131
132 #else // !defined(SYS_WINDOWS)
133
134 std::vector<std::string>
args_in_utf8(int,char **)135 args_in_utf8(int,
136 char **) {
137 std::vector<std::string> args;
138 std::string utf8;
139
140 int num_args = 0;
141 LPWSTR *arg_list = CommandLineToArgvW(GetCommandLineW(), &num_args);
142
143 if (!arg_list)
144 return args;
145
146 int i;
147 for (i = 1; i < num_args; i++) {
148 auto arg = to_utf8(std::wstring{arg_list[i]});
149
150 if (arg[0] == '@')
151 read_args_from_json_file(args, arg.substr(1));
152
153 else
154 args.push_back(arg);
155 }
156
157 LocalFree(arg_list);
158
159 return args;
160 }
161 #endif // !defined(SYS_WINDOWS)
162
163 std::string g_usage_text;
164
165 /** Handle command line arguments common to all programs
166
167 Iterates over the list of command line arguments and handles the ones
168 that are common to all programs. These include --output-charset,
169 --redirect-output, --help, --version and --verbose along with their
170 short counterparts.
171
172 \param args A vector of strings containing the command line arguments.
173 The ones that have been handled are removed from the vector.
174 \param redirect_output_short The name of the short option that is
175 recognized for --redirect-output. If left empty then no short
176 version is accepted.
177 \returns \c true if the locale has changed and the function should be
178 called again and \c false otherwise.
179 */
180 bool
handle_common_args(std::vector<std::string> & args,const std::string & redirect_output_short)181 handle_common_args(std::vector<std::string> &args,
182 const std::string &redirect_output_short) {
183 size_t i = 0;
184
185 while (args.size() > i) {
186 if (args[i] == "--debug") {
187 if ((i + 1) == args.size())
188 mxerror("Missing argument for '--debug'.\n");
189
190 debugging_c::request(args[i + 1]);
191 args.erase(args.begin() + i, args.begin() + i + 2);
192
193 } else if (args[i] == "--engage") {
194 if ((i + 1) == args.size())
195 mxerror(Y("'--engage' lacks its argument.\n"));
196
197 mtx::hacks::engage(args[i + 1]);
198 args.erase(args.begin() + i, args.begin() + i + 2);
199
200 } else if (args[i] == "--gui-mode") {
201 g_gui_mode = true;
202 args.erase(args.begin() + i, args.begin() + i + 1);
203
204 } else if (args[i] == "--flush-on-close") {
205 mm_file_io_c::enable_flushing_on_close(true);
206 args.erase(args.begin() + i, args.begin() + i + 1);
207
208 } else if (args[i] == "--abort-on-warnings") {
209 g_abort_on_warnings = true;
210 args.erase(args.begin() + i, args.begin() + i + 1);
211
212 } else
213 ++i;
214 }
215
216 // First see if there's an output charset given.
217 i = 0;
218 while (args.size() > i) {
219 if (args[i] == "--output-charset") {
220 if ((i + 1) == args.size())
221 mxerror(Y("Missing argument for '--output-charset'.\n"));
222 set_cc_stdio(args[i + 1]);
223 args.erase(args.begin() + i, args.begin() + i + 2);
224 } else
225 ++i;
226 }
227
228 // Now let's see if the user wants the output redirected.
229 i = 0;
230 while (args.size() > i) {
231 if ((args[i] == "--redirect-output") || (args[i] == "-r") ||
232 ((redirect_output_short != "") &&
233 (args[i] == redirect_output_short))) {
234 if ((i + 1) == args.size())
235 mxerror(fmt::format(Y("'{0}' is missing the file name.\n"), args[i]));
236 try {
237 if (!stdio_redirected()) {
238 mm_io_cptr file = mm_write_buffer_io_c::open(args[i + 1], 128 * 1024);
239 file->write_bom(g_stdio_charset);
240 redirect_stdio(file);
241 }
242 args.erase(args.begin() + i, args.begin() + i + 2);
243 } catch(mtx::mm_io::exception &) {
244 mxerror(fmt::format(Y("Could not open the file '{0}' for directing the output.\n"), args[i + 1]));
245 }
246 } else
247 ++i;
248 }
249
250 // Check for the translations to use (if any).
251 i = 0;
252 while (args.size() > i) {
253 if (args[i] == "--ui-language") {
254 if ((i + 1) == args.size())
255 mxerror(Y("Missing argument for '--ui-language'.\n"));
256
257 if (args[i + 1] == "list") {
258 mxinfo(Y("Available translations:\n"));
259 auto translation = translation_c::ms_available_translations.begin(), end = translation_c::ms_available_translations.end();
260 while (translation != end) {
261 mxinfo(fmt::format(" {0} ({1})\n", translation->get_locale(), translation->m_english_name));
262 ++translation;
263 }
264 mxexit();
265 }
266
267 if (-1 == translation_c::look_up_translation(args[i + 1]))
268 mxerror(fmt::format(Y("There is no translation available for '{0}'.\n"), args[i + 1]));
269
270 init_locales(args[i + 1]);
271
272 args.erase(args.begin() + i, args.begin() + i + 2);
273
274 return true;
275 } else
276 ++i;
277 }
278
279 // Last find the --help, --version, --check-for-updates arguments.
280 i = 0;
281 while (args.size() > i) {
282 if ((args[i] == "-V") || (args[i] == "--version")) {
283 mxinfo(fmt::format("{0}\n", get_version_info(get_program_name(), vif_full)));
284 mxexit();
285
286 } else if ((args[i] == "-v") || (args[i] == "--verbose")) {
287 ++verbose;
288 args.erase(args.begin() + i, args.begin() + i + 1);
289
290 } else if ((args[i] == "-q") || (args[i] == "--quiet")) {
291 verbose = 0;
292 g_suppress_info = true;
293 args.erase(args.begin() + i, args.begin() + i + 1);
294
295 } else if ((args[i] == "-h") || (args[i] == "-?") || (args[i] == "--help"))
296 display_usage();
297
298 else
299 ++i;
300 }
301
302 return false;
303 }
304
305 void
display_usage(int exit_code)306 display_usage(int exit_code) {
307 if (!g_usage_text.empty()) {
308 mxinfo(g_usage_text);
309 if (g_usage_text.at(g_usage_text.size() - 1) != '\n')
310 mxinfo("\n");
311 }
312 mxexit(exit_code);
313 }
314
315 }
316