Bash++
Bash++ compiler internal documentation
parse_arguments.h
Go to the documentation of this file.
1
6#pragma once
7
8#include <iostream>
9#include <string>
10#include <cstring>
11#include <vector>
12#include <optional>
13#include <filesystem>
14#include <memory>
15#include <getopt.h>
16#include <unistd.h>
17#include <algorithm>
18
19#include <include/FixedString.h>
20#include <include/BashVersion.h>
21
22#include <version.h>
23#include <updated_year.h>
24
25constexpr const char* program_name = "Bash++";
26
27constexpr const char* copyright = "Copyright (C) 2024-"
29 " Andrew S. Rightenburg\n\n"
30 "This program is free software; you can redistribute it and/or modify\n"
31 "it under the terms of the GNU General Public License as published by\n"
32 "the Free Software Foundation; either version 3 of the License, or\n"
33 "(at your option) any later version.\n\n"
34 "This program is distributed in the hope that it will be useful,\n"
35 "but WITHOUT ANY WARRANTY; without even the implied warranty of\n"
36 "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n"
37 "GNU General Public License for more details.\n\n"
38 "You should have received a copy of the GNU General Public License\n"
39 "along with this program. If not, see http://www.gnu.org/licenses/.\n";
40
50struct Option {
51 const char shortopt;
52 const char* longopt;
53 const char* description;
54 const bool takes_argument;
55
56 constexpr Option(
57 char shortopt,
58 const char* longopt,
59 const char* description,
60 bool takes_argument)
65};
66
67constexpr Option options[] = {
68 {'o', "output", "Specify output file (default: run on exit)", true},
69 {'b', "target-bash", "Target Bash version (default: 5.2)", true},
70 {'s', "no-warnings", "Suppress warnings", false},
71 {'I', "include", "Add directory to include path", true},
72 {'p', "parse-tree", "Display parse tree (do not compile program)", false},
73 {'t', "tokens", "Display tokens (do not compile program)", false},
74 {'v', "version", "Display version information", false},
75 {'h', "help", "Display this help message", false},
76 {0, nullptr, nullptr, false} // Sentinel
77};
78
79consteval size_t option_label_length(const Option& opt) {
80 size_t length = 0;
81 if (opt.shortopt) {
82 length += 2; // '-x'
83 }
84
85 if (opt.shortopt && opt.longopt) {
86 length += 2; // ', '
87 }
88
89 if (opt.longopt) {
90 length += 2 + string_length(opt.longopt); // '--longopt'
91 }
92
93 if (opt.takes_argument) {
94 length += 6; // ' <arg>'
95 }
96
97 return length;
98}
99
100consteval size_t max_option_label_length() {
101 size_t max_length = 0;
102 for (size_t i = 0; options[i].shortopt || options[i].longopt; i++) {
103 size_t length = option_label_length(options[i]);
104 if (length > max_length) {
105 max_length = length;
106 }
107 }
108
109 return max_length;
110}
111
112constexpr const char* help_intro =
113 "Usage: bpp [options] [file] ...\n"
114 "If no file is specified, read from stdin\n"
115 "All arguments after the file are passed to the compiled program\n"
116 "Options:\n";
117
119 size_t total_length = string_length(help_intro);
120 constexpr size_t max_label_length = max_option_label_length();
121
122 for (size_t i = 0; options[i].shortopt || options[i].longopt; i++) {
123 const auto& opt = options[i];
124
125 // Base line: " -x, --longopt <arg>"
126 total_length += 2; // " "
127 if (opt.shortopt) {
128 total_length += 2; // "-x"
129 }
130
131 if (opt.shortopt && opt.longopt) {
132 total_length += 2; // ", "
133 }
134
135 if (opt.longopt) {
136 total_length += 2 + string_length(opt.longopt); // "--longopt"
137 }
138
139 if (opt.takes_argument) {
140 total_length += 6; // " <arg>"
141 }
142
143 // Padding + space + description + newline
144 size_t label_length = option_label_length(opt);
145 size_t padding_amount = max_label_length - label_length;
146 total_length += padding_amount + 1 + string_length(opt.description) + 1;
147 }
148
149 return total_length + 1; // +1 for null terminator
150}
151
152// Generate the help string at compile-time
153consteval auto generate_help_string() {
154 constexpr size_t total_size = calculate_help_string_length();
156
157 result.append(help_intro);
158
159 constexpr size_t max_label_length = max_option_label_length();
160
161 for (size_t i = 0; options[i].shortopt || options[i].longopt; i++) {
162 const auto& opt = options[i];
163
164 result.append(" ");
165
166 if (opt.shortopt) {
167 result.append('-');
168 result.append(opt.shortopt);
169 }
170
171 if (opt.shortopt && opt.longopt) {
172 result.append(", ");
173 }
174
175 if (opt.longopt) {
176 result.append("--");
177 result.append(opt.longopt);
178 }
179
180 if (opt.takes_argument) {
181 result.append(" <arg>");
182 }
183
184 // Add padding
185 size_t label_length = option_label_length(opt);
186 size_t padding_amount = max_label_length - label_length;
187 result.append(' ', padding_amount + 1); // Padding + space
188 result.append(opt.description);
189 result.append('\n');
190 }
191
192 result.append('\0'); // Null-terminate the string
193 return result;
194}
195
197
207struct Arguments {
208 std::vector<char*> program_arguments;
209 std::optional<std::string> input_file;
210 std::optional<std::string> output_file;
211 BashVersion target_bash_version = {5, 2}; // Default to Bash 5.2
212 std::shared_ptr<std::vector<std::string>> include_paths = std::make_shared<std::vector<std::string>>();
213 bool suppress_warnings = false;
214 bool display_tokens = false;
215 bool display_parse_tree = false;
216
217 bool exit_early = false; // Exit early if the request is just -h/--help or -v/--version
218};
219
220inline std::pair<std::vector<char*>, std::vector<char*>> separate_compiler_and_program_options(Arguments* args, int argc, char* argv[]) {
221 std::vector<char*> program_arguments;
222 std::vector<char*> compiler_arguments;
223
224 program_arguments.reserve(static_cast<std::vector<char*>::size_type>(argc));
225 compiler_arguments.reserve(static_cast<std::vector<char*>::size_type>(argc));
226 compiler_arguments.push_back(argv[0]);
227
228 // Scan through each argument:
229 // The first non-option argument should be interpreted as the source file to compile
230 // Everything before it should be interpreted as arguments to the compiler
231 // Everything after it should be interpreted as arguments to the compiled program
232
233 std::vector<std::pair<char, const char*>> options_with_arguments;
234
235 for (const auto& opt : options) {
236 if (opt.takes_argument) {
237 options_with_arguments.emplace_back(opt.shortopt, opt.longopt);
238 }
239 }
240
241 bool expecting_argument = false;
242 bool received_filename = false;
243 for (int i = 1; i < argc; i++) {
244 // If we've already received a filename, everything else is a program argument
245 if (received_filename) {
246 program_arguments.push_back(argv[i]);
247 continue;
248 }
249
250 // If the last option we processed expected an argument,
251 // Then this one is the argument for that option
252 if (expecting_argument) {
253 compiler_arguments.push_back(argv[i]);
254 expecting_argument = false;
255 continue;
256 }
257
258 // If this argument is an option, check if it takes an argument
259 // If it does, set expecting_argument to true so we can process that next
260 if (argv[i][0] == '-') {
261 for (const auto& opt : options_with_arguments) {
262 // Make sure we handle attached arguments to options, like -o- or --output=file.sh
263 if (argv[i][1] == opt.first && argv[i][2] == '\0') {
264 // Expect next opt to be the argument for this option
265 expecting_argument = true;
266 } else if (opt.second
267 && strcmp(argv[i] + 2, opt.second) == 0
268 && argv[i][2 + std::char_traits<char>::length(opt.second)] == '\0'
269 ) {
270 // Expect next opt to be the argument for this option
271 expecting_argument = true;
272 }
273 }
274 }
275
276 // Add the argument to the compiler_arguments array
277 compiler_arguments.push_back(argv[i]);
278 // If this argument is not an option, and we haven't received a filename yet,
279 // Then this is the source file to compile
280 if (!received_filename && argv[i][0] != '-') {
281 args->input_file = argv[i];
282 received_filename = true;
283 }
284 }
285
286 return {compiler_arguments, program_arguments};
287}
288
289inline Arguments parse_arguments(int argc, char* argv[]) {
290 Arguments args;
291
292 args.include_paths->push_back("/usr/lib/bpp/stdlib/");
293
294 auto [compiler_arguments, program_arguments] = separate_compiler_and_program_options(&args, argc, argv);
295 args.program_arguments = std::move(program_arguments);
296
297 // Getopt
298 int c;
299 opterr = 0;
300 int option_index = 0;
301
302 std::vector<char> options_with_arguments;
303
304 std::string short_options;
305 for (const auto& opt : options) {
306 if (opt.shortopt) {
307 short_options += opt.shortopt;
308 if (opt.takes_argument) {
309 short_options += ":";
310 options_with_arguments.push_back(opt.shortopt);
311 }
312 }
313 }
314 short_options += '\0'; // Null-terminate the string
315
316 // Long options
317 static struct option long_options[(sizeof(options) / sizeof(options[0])) + 1];
318
319 for (const auto& opt : options) {
320 if (opt.longopt) {
321 long_options[option_index++] = {opt.longopt, opt.takes_argument ? required_argument : no_argument, nullptr, opt.shortopt};
322 }
323 }
324 long_options[option_index] = {nullptr, 0, nullptr, 0}; // Sentinel
325
326 option_index = 0;
327
328 bool received_output_filename = false;
329
330 while (
331 (c = getopt_long(
332 static_cast<int>(compiler_arguments.size()),
333 compiler_arguments.data(),
334 short_options.c_str(),
335 long_options,
336 &option_index)
337 ) != -1
338 ) {
339 switch (c) {
340 case 'b':
341 // Parse the target Bash version
342 {
343 std::istringstream version_stream(optarg);
344 uint16_t major, minor;
345 char dot;
346 if (!(version_stream >> major >> dot >> minor) || dot != '.') {
347 throw std::runtime_error("Invalid Bash version format: " + std::string(optarg) +
348 "\nExpected format: <major>.<minor> (e.g., 5.2)");
349 }
350 args.target_bash_version = {major, minor};
351 }
352 break;
353 case 'h':
354 std::cout << program_name << " " << bpp_compiler_version << std::endl << help_string;
355 args.exit_early = true;
356 return args;
357 break;
358 case 'I':
359 // Verify the given include path is a directory
360 if (!std::filesystem::exists(optarg) || !std::filesystem::is_directory(optarg)) {
361 throw std::runtime_error("Include path '" + std::string(optarg) + "' does not exist or is not a directory");
362 }
363 args.include_paths->push_back(std::string(optarg));
364 break;
365 case 'o':
366 if (received_output_filename) {
367 throw std::runtime_error("Multiple output files specified");
368 }
369
370 if (std::string(optarg) == "-") {
371 args.output_file = optarg;
372 break;
373 }
374
375 {
376 std::filesystem::path output_path(optarg);
377 if (output_path.is_absolute()) {
378 args.output_file = output_path.string();
379 } else {
380 args.output_file = std::filesystem::current_path() / output_path;
381 }
382 }
383
384 // Check if we have permission to write to the specified output file
385 // If the file exists, verify write access; if it doesn't, verify write access on its parent directory
386 try {
387 if (std::filesystem::exists(args.output_file.value())) {
388 if (!std::filesystem::is_regular_file(args.output_file.value())) {
389 throw std::runtime_error("Output file '" + args.output_file.value() + "' is not a regular file");
390 }
391 if (access(args.output_file.value().c_str(), W_OK) != 0) {
392 throw std::runtime_error("No write permission for output file '" + args.output_file.value() + "'");
393 }
394 } else {
395 std::filesystem::path parent_path = std::filesystem::path(args.output_file.value()).parent_path();
396 if (!std::filesystem::exists(parent_path) || !std::filesystem::is_directory(parent_path)) {
397 throw std::runtime_error("Parent directory of output file '" + args.output_file.value() + "' does not exist or is not a directory");
398 }
399 if (access(parent_path.c_str(), W_OK) != 0) {
400 throw std::runtime_error("No write permission for parent directory of output file '" + args.output_file.value() + "'");
401 }
402 }
403 } catch (const std::exception& e) {
404 throw std::runtime_error(std::string("Could not verify write permission for output file '") + args.output_file.value() + "': " + e.what());
405 }
406 break;
407 case 'p':
408 args.display_parse_tree = true;
409 break;
410 case 's':
411 args.suppress_warnings = true;
412 break;
413 case 't':
414 args.display_tokens = true;
415 break;
416 case 'v':
417 std::cout << program_name << " " << bpp_compiler_version << std::endl << copyright;
418 args.exit_early = true;
419 return args;
420 break;
421 case '?':
422 if (std::find(options_with_arguments.begin(), options_with_arguments.end(), optopt) != options_with_arguments.end()) {
423 throw std::runtime_error(std::string("Option -") + static_cast<char>(optopt) + " requires an argument\nUse -h for help");
424 } else {
425 throw std::runtime_error("Unknown option: -" + std::string(1, optopt));
426 }
427 break;
428 }
429 }
430
431 return args;
432}
consteval size_t string_length(const char *str)
Definition FixedString.h:34
consteval size_t max_option_label_length()
Definition parse_arguments.h:100
consteval size_t option_label_length(const Option &opt)
Definition parse_arguments.h:79
constexpr auto help_string
Definition parse_arguments.h:196
Arguments parse_arguments(int argc, char *argv[])
Definition parse_arguments.h:289
consteval size_t calculate_help_string_length()
Definition parse_arguments.h:118
constexpr const char * help_intro
Definition parse_arguments.h:112
constexpr Option options[]
Definition parse_arguments.h:67
std::pair< std::vector< char * >, std::vector< char * > > separate_compiler_and_program_options(Arguments *args, int argc, char *argv[])
Definition parse_arguments.h:220
constexpr const char * copyright
Definition parse_arguments.h:27
consteval auto generate_help_string()
Definition parse_arguments.h:153
constexpr const char * program_name
Definition parse_arguments.h:25
Represents the parsed command-line arguments given to the compiler.
Definition parse_arguments.h:207
BashVersion target_bash_version
Definition parse_arguments.h:211
bool display_parse_tree
Definition parse_arguments.h:215
bool exit_early
Definition parse_arguments.h:217
bool display_tokens
Definition parse_arguments.h:214
std::optional< std::string > output_file
Definition parse_arguments.h:210
bool suppress_warnings
Definition parse_arguments.h:213
std::shared_ptr< std::vector< std::string > > include_paths
Definition parse_arguments.h:212
std::optional< std::string > input_file
Definition parse_arguments.h:209
std::vector< char * > program_arguments
Definition parse_arguments.h:208
Represents a Bash version to target for code generation.
Definition BashVersion.h:20
Definition FixedString.h:8
consteval void append(std::string_view str)
Definition FixedString.h:12
Represents a command-line option for the Bash++ compiler.
Definition parse_arguments.h:50
const char shortopt
Definition parse_arguments.h:51
const char * description
Definition parse_arguments.h:53
const bool takes_argument
Definition parse_arguments.h:54
const char * longopt
Definition parse_arguments.h:52
constexpr Option(char shortopt, const char *longopt, const char *description, bool takes_argument)
Definition parse_arguments.h:56
#define bpp_compiler_updated_year
Definition updated_year.h:1
#define bpp_compiler_version
Definition version.h:1