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