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