Index: src/OFOptionsParser.h ================================================================== --- src/OFOptionsParser.h +++ src/OFOptionsParser.h @@ -13,75 +13,154 @@ * LICENSE.GPLv2 or LICENSE.GPLv3 respectively included in the packaging of this * file. */ #import "OFObject.h" -#import "OFConstantString.h" +#import "OFString.h" + +@class OFMapTable; OF_ASSUME_NONNULL_BEGIN +/*! + * @struct of_options_parser_option_t OFOptionsParser.h ObjFW/OFOptionsParser.h + * + * @brief An option which can be parsed by an @ref OFOptionsParser. + */ +typedef struct of_options_parser_option_t { + /*! The short version (e.g. `-v`) of the option or `'\0'` for none */ + of_unichar_t shortOption; + + /*! + * The long version (e.g. `--verbose`) of the option or `nil` for none + */ + OFString *_Nullable longOption; + + /*! + * Whether the option takes an argument + * + * 0 means it takes no argument.@n + * 1 means it takes a required argument.@n + * -1 means it takes an optional argument.@n + * + * All other values are invalid and will throw an + * @ref OFInvalidArgumentException. + */ + signed char hasArgument; + + /*! + * An optional pointer to a bool that is set to whether the option has + * been specified + */ + bool *_Nullable isSpecifiedPtr; + + /*! + * An optional pointer to an @ref OFString* that is set to the argument + * specified for the option or `nil` for no argument + */ + OFString *__autoreleasing _Nullable *_Nullable argumentPtr; +} of_options_parser_option_t; + /*! * @class OFOptionsParser OFOptionsParser.h ObjFW/OFOptionsParser.h * * @brief A class for parsing the program options specified on the command line. */ @interface OFOptionsParser: OFObject { - of_unichar_t *_options; + of_options_parser_option_t *_options; + OFMapTable *_longOptions; OFArray OF_GENERIC(OFString*) *_arguments; size_t _index, _subIndex; of_unichar_t _lastOption; - OFString *_argument; + OFString *_lastLongOption, *_argument; bool _done; } + +#ifdef OF_HAVE_PROPERTIES +@property (readonly) of_unichar_t lastOption; +@property OF_NULLABLE_PROPERTY (readonly) OFString *lastLongOption; +@property OF_NULLABLE_PROPERTY (readonly) OFString *argument; +@property (readonly) OFArray OF_GENERIC(OFString*) *remainingArguments; +#endif /*! * @brief Creates a new OFOptionsParser which accepts the specified options. * - * @param options A string listing the acceptable options.@n - * Options that require an argument are immediately followed by - * ':'. + * @param options An array of @ref of_options_parser_option_t specifying all + * accepted options, terminated with an option whose short + * option is `'\0'` and long option is `nil`. * * @return A new, autoreleased OFOptionsParser */ -+ (instancetype)parserWithOptions: (OFString*)options; ++ (instancetype)parserWithOptions: (const of_options_parser_option_t*)options; /*! * @brief Initializes an already allocated OFOptionsParser so that it accepts * the specified options. * - * @param options A string listing the acceptable options.@n - * Options that require an argument are immediately followed by - * ':'. + * @param options An array of @ref of_options_parser_option_t specifying all + * accepted options, terminated with an option whose short + * option is `'\0'` and long option is `nil`. * * @return An initialized OFOptionsParser */ -- initWithOptions: (OFString*)options; +- initWithOptions: (const of_options_parser_option_t*)options; /*! * @brief Returns the next option. * + * If the option is only available as a long option, '-' is returned. + * Otherwise, the short option is returned, even if it was specified as a long + * option.@n * If an unknown option is specified, '?' is returned.@n * If the argument for the option is missing, ':' is returned.@n - * If all options have been parsed, '\0' is returned. + * If there is an argument for the option even though it takes none, '=' is + * returned.@n + * If all options have been parsed, `'\0'` is returned. + * + * @note You need to call @ref nextOption repeatedly until it returns `'\0'` to + * make sure all options have been parsed, even if you only rely on the + * optional pointers specified and don't do any parsing yourself. * * @return The next option */ - (of_unichar_t)nextOption; /*! * @brief Returns the last parsed option. * * If @ref nextOption returned '?' or ':', this returns the option which was - * unknown or for which the argument was missing. + * unknown or for which the argument was missing.@n + * If this returns '-', the last option is only available as a long option (see + * @ref lastLongOption). * * @return The last parsed option */ - (of_unichar_t)lastOption; /*! - * @brief Returns the argument for the last parsed option, or nil if the last + * @brief Returns the long option for the last parsed option, or `nil` if the + * last parsed option was not passed as a long option by the user. + * + * In case @ref nextOption returned '?', this contains the unknown long + * option.@n + * In case it returned ':', this contains the long option which is missing an + * argument.@n + * In case it returned '=', this contains the long option for which an argument + * was specified even though the option takes no argument. + * + * @warning Unlike @ref lastOption, which returns the short option even if the + * user specified a long option, this only returns the long option if + * it was actually specified as a long option by the user. + * + * @return The last parsed long option or `nil` + */ +- (nullable OFString*)lastLongOption; + +/*! + * @brief Returns the argument for the last parsed option, or `nil` if the last * parsed option takes no argument. * * @return The argument for the last parsed option */ - (nullable OFString*)argument; Index: src/OFOptionsParser.m ================================================================== --- src/OFOptionsParser.m +++ src/OFOptionsParser.m @@ -17,32 +17,112 @@ #include "config.h" #import "OFOptionsParser.h" #import "OFApplication.h" #import "OFArray.h" +#import "OFMapTable.h" + +#import "OFInvalidArgumentException.h" + +static uint32_t +stringHash(void *value) +{ + return [(OFString*)value hash]; +} + +static bool +stringEqual(void *value1, void *value2) +{ + return [(OFString*)value1 isEqual: (OFString*)value2]; +} @implementation OFOptionsParser -+ (instancetype)parserWithOptions: (OFString*)options ++ (instancetype)parserWithOptions: (const of_options_parser_option_t*)options { return [[[self alloc] initWithOptions: options] autorelease]; } - init { OF_INVALID_INIT_METHOD } -- initWithOptions: (OFString*)options +- initWithOptions: (const of_options_parser_option_t*)options { self = [super init]; @try { - _options = [self allocMemoryWithSize: sizeof(of_unichar_t) - count: [options length] + 1]; - [options getCharacters: _options - inRange: of_range(0, [options length])]; - _options[[options length]] = 0; + size_t count = 0; + const of_options_parser_option_t *iter; + of_options_parser_option_t *iter2; + const of_map_table_functions_t keyFunctions = { + .hash = stringHash, + .equal = stringEqual + }; + const of_map_table_functions_t valueFunctions = { NULL }; + + /* Count, sanity check and initialize pointers */ + for (iter = options; + iter->shortOption != '\0' || iter->longOption != nil; + iter++) { + if (iter->hasArgument < -1 || iter->hasArgument > 1) + @throw [OFInvalidArgumentException exception]; + + if (iter->shortOption != '\0' && + iter->hasArgument == -1) + @throw [OFInvalidArgumentException exception]; + + if (iter->hasArgument == 0 && iter->argumentPtr != NULL) + @throw [OFInvalidArgumentException exception]; + + if (iter->isSpecifiedPtr) + *iter->isSpecifiedPtr = false; + if (iter->argumentPtr) + *iter->argumentPtr = nil; + + count++; + } + + _longOptions = [[OFMapTable alloc] + initWithKeyFunctions: keyFunctions + valueFunctions: valueFunctions]; + _options = [self + allocMemoryWithSize: sizeof(*_options) + count: count + 1]; + + for (iter = options, iter2 = _options; + iter->shortOption != '\0' || iter->longOption != nil; + iter++, iter2++) { + iter2->shortOption = iter->shortOption; + iter2->longOption = nil; + iter2->hasArgument = iter->hasArgument; + iter2->isSpecifiedPtr = iter->isSpecifiedPtr; + iter2->argumentPtr = iter->argumentPtr; + + @try { + iter2->longOption = [iter->longOption copy]; + + if ([_longOptions + valueForKey: iter2->longOption] != NULL) + @throw [OFInvalidArgumentException + exception]; + + [_longOptions setValue: iter2 + forKey: iter2->longOption]; + } @catch (id e) { + /* + * Make sure we are in a consistent state where + * dealloc works. + */ + [iter2->longOption release]; + + iter2->shortOption = '\0'; + iter2->longOption = nil; + + @throw e; + } + } _arguments = [[OFApplication arguments] retain]; } @catch (id e) { [self release]; @throw e; @@ -51,23 +131,38 @@ return self; } - (void)dealloc { + of_options_parser_option_t *iter; + + [_longOptions release]; + + if (_options != NULL) + for (iter = _options; + iter->shortOption != '\0' || iter->longOption != nil; + iter++) + [iter->longOption release]; + [_arguments release]; [_argument release]; [super dealloc]; } - (of_unichar_t)nextOption { - of_unichar_t *options; + of_options_parser_option_t *iter; OFString *argument; if (_done || _index >= [_arguments count]) return '\0'; + + [_lastLongOption release]; + [_argument release]; + _lastLongOption = nil; + _argument = nil; argument = [_arguments objectAtIndex: _index]; if (_subIndex == 0) { if ([argument length] < 2 || @@ -79,10 +174,53 @@ if ([argument isEqual: @"--"]) { _done = true; _index++; return '\0'; } + + if ([argument hasPrefix: @"--"]) { + void *pool = objc_autoreleasePoolPush(); + size_t pos; + of_options_parser_option_t *option; + + _lastOption = '-'; + _index++; + + if ((pos = [argument rangeOfString: @"="].location) != + OF_NOT_FOUND) { + of_range_t range = of_range(pos + 1, + [argument length] - pos - 1); + _argument = [[argument + substringWithRange: range] copy]; + } else + pos = [argument length]; + + _lastLongOption = [[argument substringWithRange: + of_range(2, pos - 2)] copy]; + + objc_autoreleasePoolPop(pool); + + option = [_longOptions valueForKey: _lastLongOption]; + if (option == nil) + return '?'; + + if (option->hasArgument == 1 && _argument == nil) + return ':'; + if (option->hasArgument == 0 && _argument != nil) + return '='; + + if (option->isSpecifiedPtr != NULL) + *option->isSpecifiedPtr = true; + if (option->argumentPtr != NULL) + *option->argumentPtr = + [[_argument copy] autorelease]; + + if (option->shortOption != '\0') + _lastOption = option->shortOption; + + return _lastOption; + } _subIndex = 1; } _lastOption = [argument characterAtIndex: _subIndex++]; @@ -90,28 +228,31 @@ if (_subIndex >= [argument length]) { _index++; _subIndex = 0; } - for (options = _options; *options != 0; options++) { - if (_lastOption == *options) { - if (options[1] != ':') { - [_argument release]; - _argument = nil; + for (iter = _options; + iter->shortOption != '\0' || iter->longOption != nil; iter++) { + if (iter->shortOption == _lastOption) { + if (iter->hasArgument == 0) return _lastOption; - } if (_index >= [_arguments count]) return ':'; argument = [_arguments objectAtIndex: _index]; argument = [argument substringWithRange: of_range(_subIndex, [argument length] - _subIndex)]; - [_argument release]; _argument = [argument copy]; + if (iter->isSpecifiedPtr != NULL) + *iter->isSpecifiedPtr = true; + if (iter->argumentPtr != NULL) + *iter->argumentPtr = + [[_argument copy] autorelease]; + _index++; _subIndex = 0; return _lastOption; } @@ -122,17 +263,22 @@ - (of_unichar_t)lastOption { return _lastOption; } + +- (OFString*)lastLongOption +{ + OF_GETTER(_lastLongOption, true) +} - (OFString*)argument { - return [[_argument copy] autorelease]; + OF_GETTER(_argument, true) } - (OFArray*)remainingArguments { return [_arguments objectsInRange: of_range(_index, [_arguments count] - _index)]; } @end Index: utils/ofhttp/OFHTTP.m ================================================================== --- utils/ofhttp/OFHTTP.m +++ utils/ofhttp/OFHTTP.m @@ -211,61 +211,85 @@ } } - (void)applicationDidFinishLaunching { - OFOptionsParser *optionsParser = - [OFOptionsParser parserWithOptions: @"b:chH:m:o:OP:qv"]; + OFString *outputPath; + const of_options_parser_option_t options[] = { + { 'b', @"body", 1, NULL, NULL }, + { 'c', @"continue", 0, &_continue, NULL }, + { 'h', @"help", 0, NULL, NULL }, + { 'H', @"header", 1, NULL, NULL }, + { 'm', @"method", 1, NULL, NULL }, + { 'o', @"output", 1, NULL, &outputPath }, + { 'O', @"detect-filename", 0, &_detectFileName, NULL }, + { 'P', @"socks5-proxy", 1, NULL, NULL }, + { 'q', @"quiet", 0, &_quiet, NULL }, + { 'v', @"verbose", 0, &_verbose, NULL }, + { '\0', nil, 0, NULL, NULL } + }; + OFOptionsParser *optionsParser = [OFOptionsParser + parserWithOptions: options]; of_unichar_t option; while ((option = [optionsParser nextOption]) != '\0') { switch (option) { case 'b': [self setBody: [optionsParser argument]]; break; - case 'c': - _continue = true; - break; case 'h': help(of_stdout, true, 0); break; case 'H': [self addHeader: [optionsParser argument]]; break; case 'm': [self setMethod: [optionsParser argument]]; break; - case 'o': - [_outputPath release]; - _outputPath = [[optionsParser argument] retain]; - break; - case 'O': - _detectFileName = true; - break; case 'P': [self setProxy: [optionsParser argument]]; break; - case 'q': - _quiet = true; - break; - case 'v': - _verbose = true; - break; - case ':': - [of_stderr writeFormat: @"%@: Argument for option -%C " - @"missing\n", - [OFApplication programName], - [optionsParser lastOption]]; - [OFApplication terminateWithStatus: 1]; - default: - [of_stderr writeFormat: @"%@: Unknown option: -%C\n", - [OFApplication programName], - [optionsParser lastOption]]; - [OFApplication terminateWithStatus: 1]; - } - } - + case ':': + if ([optionsParser lastLongOption] != nil) + [of_stderr writeFormat: + @"%@: Argument for option --%@ missing\n", + [OFApplication programName], + [optionsParser lastLongOption]]; + else + [of_stderr writeFormat: + @"%@: Argument for option -%C missing\n", + [OFApplication programName], + [optionsParser lastOption]]; + + [OFApplication terminateWithStatus: 1]; + break; + case '=': + [of_stderr writeFormat: @"%@: Option --%@ takes no " + @"argument\n", + [OFApplication programName], + [optionsParser lastLongOption]]; + + [OFApplication terminateWithStatus: 1]; + break; + case '?': + if ([optionsParser lastLongOption] != nil) + [of_stderr writeFormat: + @"%@: Unknown option: --%@\n", + [OFApplication programName], + [optionsParser lastLongOption]]; + else + [of_stderr writeFormat: + @"%@: Unknown option: -%C\n", + [OFApplication programName], + [optionsParser lastOption]]; + + [OFApplication terminateWithStatus: 1]; + break; + } + } + + _outputPath = [outputPath copy]; _URLs = [[optionsParser remainingArguments] retain]; if ([_URLs count] < 1) help(of_stderr, false, 1); Index: utils/ofzip/OFZIP.m ================================================================== --- utils/ofzip/OFZIP.m +++ utils/ofzip/OFZIP.m @@ -106,12 +106,22 @@ } @implementation OFZIP - (void)applicationDidFinishLaunching { + const of_options_parser_option_t options[] = { + { 'f', @"force", 0, NULL, NULL }, + { 'h', @"help", 0, NULL, NULL }, + { 'l', @"list", 0, NULL, NULL }, + { 'n', @"no-clobber", 0, NULL, NULL }, + { 'q', @"quiet", 0, NULL, NULL }, + { 'v', @"verbose", 0, NULL, NULL }, + { 'x', @"extract", 0, NULL, NULL }, + { '\0', nil, 0, NULL, NULL } + }; OFOptionsParser *optionsParser = - [OFOptionsParser parserWithOptions: @"fhlnqvx"]; + [OFOptionsParser parserWithOptions: options]; of_unichar_t option, mode = '\0'; OFArray OF_GENERIC(OFString*) *remainingArguments, *files; OFZIPArchive *archive; while ((option = [optionsParser nextOption]) != '\0') { @@ -148,14 +158,30 @@ mode = option; break; case 'h': help(of_stdout, true, 0); break; - default: - [of_stderr writeFormat: @"%@: Unknown option: -%C\n", + case '=': + [of_stderr writeFormat: @"%@: Option --%@ takes no " + @"argument!\n", [OFApplication programName], - [optionsParser lastOption]]; + [optionsParser lastLongOption]]; + + [OFApplication terminateWithStatus: 1]; + break; + default: + if ([optionsParser lastLongOption] != nil) + [of_stderr writeFormat: + @"%@: Unknown option: --%@\n", + [OFApplication programName], + [optionsParser lastLongOption]]; + else + [of_stderr writeFormat: + @"%@: Unknown option: -%C\n", + [OFApplication programName], + [optionsParser lastOption]]; + [OFApplication terminateWithStatus: 1]; } } remainingArguments = [optionsParser remainingArguments]; @@ -357,12 +383,12 @@ directory = [outFileName stringByDeletingLastPathComponent]; if (![fileManager directoryExistsAtPath: directory]) [fileManager createDirectoryAtPath: directory createParents: true]; - if ([fileManager fileExistsAtPath: outFileName] && - _override != 1) { + if (_override != 1 && + [fileManager fileExistsAtPath: outFileName]) { OFString *line; if (_override == -1) { if (_outputLevel >= 0) [of_stdout writeLine: @" skipped"];