diff --git a/apps/isar_post_rend.c b/apps/isar_post_rend.c index dd99da223f97fbb1df088aa84bd5b634760ab2d0..c67b21425c5e112cda6bba956d827fe81a9cf341 100644 --- a/apps/isar_post_rend.c +++ b/apps/isar_post_rend.c @@ -57,8 +57,7 @@ * Local constants *------------------------------------------------------------------------------------------*/ -#define POST_REND_MAX_CLI_ARG_LENGTH ( FILENAME_MAX ) -#define POST_REND_NUM_MANDATORY_CMD_LINE_PARAMS 3 +#define POST_REND_MAX_CLI_ARG_LENGTH ( FILENAME_MAX ) #define ISAR_MAX16B_FLT 32767.0f #define ISAR_MIN16B_FLT ( -32768.0f ) @@ -139,42 +138,52 @@ static const CmdLnParser_Option cliOptions[] = { .id = CmdLnOptionId_inputFile, .match = "input_file", .matchShort = "i", + .placeholder = "", .description = "Path to the input file (WAV or raw PCM with BINAURAL_SPLIT_PCM input format, coded ISAR file with BINAURAL_SPLIT_CODED)", + .isMandatory = true, }, { .id = CmdLnOptionId_inputFormat, .match = "input_format", .matchShort = "if", + .placeholder = "", .description = "Audio format of input file (e.g. BINAURAL_SPLIT_CODED, BINAURAL_SPLIT_PCM, ... Use -l for a list)", + .isMandatory = true, }, { .id = CmdLnOptionId_outputFile, .match = "output_file", .matchShort = "o", + .placeholder = "", .description = "Path to the output file", + .isMandatory = true, }, { .id = CmdLnOptionId_inputMetadata, .match = "input_metadata", .matchShort = "im", + .placeholder = "", .description = "Path to the input metadata file for BINAURAL_SPLIT_PCM input mode", }, { .id = CmdLnOptionId_sampleRate, .match = "sample_rate", .matchShort = "fs", + .placeholder = "", .description = "Input sampling rate in kHz (16, 32, 48) - required only with raw PCM inputs", }, { .id = CmdLnOptionId_trajFile, .match = "trajectory_file", .matchShort = "T", + .placeholder = "", .description = "Head rotation trajectory file for simulation of head tracking", }, { .id = CmdLnOptionId_SplitRendBFIFile, .match = "post_rend_bfi_file", .matchShort = "prbfi", + .placeholder = "", .description = "Split rendering BFI (Bad Frame Indicator) file", }, { @@ -187,6 +196,7 @@ static const CmdLnParser_Option cliOptions[] = { .id = CmdLnOptionId_complexityLevel, .match = "complexity_level", .matchShort = "level", + .placeholder = "", .description = "Complexity level, level = (1, 2, 3), will be defined after characterisation.", }, { @@ -205,12 +215,14 @@ static const CmdLnParser_Option cliOptions[] = { .id = CmdLnOptionId_framing, .match = "framing", .matchShort = "fr", - .description = "Set audio rendering frame size", + .placeholder = "", + .description = "Set audio rendering frame size in ms (5, 10, 20)", }, { .id = CmdLnOptionId_srParamsFile, .match = "sr_params", .matchShort = "s", + .placeholder = "", .description = "Path to the split rendering init params file", }, { @@ -394,41 +406,6 @@ static IVAS_AUDIO_CONFIG parseAudioConfig( } -static bool checkRequiredArgs( - CmdlnArgs args ) -{ - const CmdLnParser_Option *tmpOption; - - /* Check required arguments */ - bool missingRequiredArg = false; - if ( isEmptyString( args.inputFilePath ) ) - { - tmpOption = findOptionById( CmdLnOptionId_inputFile ); - fprintf( stderr, "Missing mandatory parameter: --%s/-%s\n", tmpOption->match, tmpOption->matchShort ); - missingRequiredArg = true; - } - - if ( args.inConfig.numBinBuses == 0 ) - { - tmpOption = findOptionById( CmdLnOptionId_inputFormat ); - fprintf( stderr, "Missing mandatory parameter: --%s/-%s\n", tmpOption->match, tmpOption->matchShort ); - missingRequiredArg = true; - } - - if ( isEmptyString( args.outputFilePath ) ) - { - tmpOption = findOptionById( CmdLnOptionId_outputFile ); - fprintf( stderr, "Missing mandatory parameter: --%s/-%s\n", tmpOption->match, tmpOption->matchShort ); - missingRequiredArg = true; - } - - if ( missingRequiredArg ) - { - CmdLnParser_printUsage( args.executableName, cliOptions, numCliOptions, POST_REND_NUM_MANDATORY_CMD_LINE_PARAMS ); - } - - return !missingRequiredArg; -} static CmdlnArgs defaultArgs( @@ -643,7 +620,7 @@ static int16_t parseOption( strncpy( args->srParamsFilePath, optionValues[0], POST_REND_MAX_CLI_ARG_LENGTH - 1 ); break; case CmdLnOptionId_help: - CmdLnParser_printUsage( args->executableName, cliOptions, numCliOptions, POST_REND_NUM_MANDATORY_CMD_LINE_PARAMS ); + CmdLnParser_printUsage( args->executableName, cliOptions, numCliOptions ); exit( 0 ); default: fprintf( stderr, "Error: Incorrect or invalid command-line usage!\n" ); @@ -661,12 +638,7 @@ static CmdlnArgs parseCmdlnArgs( { CmdlnArgs args = defaultArgs( argv[0] ); - if ( CmdLnParser_parseArgs( argc, argv, cliOptions, numCliOptions, &args, parseOption, POST_REND_NUM_MANDATORY_CMD_LINE_PARAMS ) != 0 ) - { - exit( -1 ); /* Error printout handled by failing function */ - } - - if ( !checkRequiredArgs( args ) ) + if ( CmdLnParser_parseArgs( argc, argv, cliOptions, numCliOptions, &args, parseOption ) != 0 ) { exit( -1 ); /* Error printout handled by failing function */ } diff --git a/apps/renderer.c b/apps/renderer.c index bba0c4c2313234f71589896d00541b58bd18b0ed..31693715601cd871c490384fbb8c8ddd7a65a3d9 100644 --- a/apps/renderer.c +++ b/apps/renderer.c @@ -65,10 +65,9 @@ * Local constants *------------------------------------------------------------------------------------------*/ -#define RENDERER_MAX_CLI_ARG_LENGTH ( FILENAME_MAX ) -#define RENDERER_MAX_METADATA_LENGTH 8192 -#define RENDERER_MAX_METADATA_LINE_LENGTH 1024 -#define RENDERER_NUM_MANDATORY_CMD_LINE_PARAMS 1000 +#define RENDERER_MAX_CLI_ARG_LENGTH ( FILENAME_MAX ) +#define RENDERER_MAX_METADATA_LENGTH 8192 +#define RENDERER_MAX_METADATA_LINE_LENGTH 1024 #define IVAS_MAX16B_FLT 32767.0f #define IVAS_MIN16B_FLT ( -32768.0f ) @@ -235,96 +234,116 @@ static const CmdLnParser_Option cliOptions[] = { .id = CmdLnOptionId_inputFile, .match = "input_file", .matchShort = "i", + .placeholder = "", .description = "Path to the input file (WAV, raw PCM or scene description file)", + .isMandatory = true, }, { .id = CmdLnOptionId_inputFormat, .match = "input_format", .matchShort = "if", + .placeholder = "", .description = "Audio format of input file (e.g. 5_1 or HOA3 or META,\nuse -l for a list)", + .isMandatory = true, }, { .id = CmdLnOptionId_inputMetadata, .match = "input_metadata", .matchShort = "im", + .placeholder = " [...]", .description = "Space-separated list of path to metadata files for ISM/MASA/OMASA/\nOSBA/BINAURAL_SPLIT_PCM inputs. \nFor OMASA, ISM files must be specified first.", }, { .id = CmdLnOptionId_outputFile, .match = "output_file", .matchShort = "o", + .placeholder = "", .description = "Path to the output file", + .isMandatory = true, }, { .id = CmdLnOptionId_outputFormat, .match = "output_format", .matchShort = "of", + .placeholder = "", .description = "Output format to render.\nAlternatively, can be a custom loudspeaker layout file", + .isMandatory = true, }, { .id = CmdLnOptionId_sampleRate, .match = "sample_rate", .matchShort = "fs", + .placeholder = "", .description = "Input sampling rate in kHz (16, 32, 48) - required only with raw\nPCM inputs", }, { .id = CmdLnOptionId_trajFile, .match = "trajectory_file", .matchShort = "T", + .placeholder = "", .description = "Head rotation trajectory file for simulation of head tracking\n(only for binaural outputs)", }, { .id = CmdLnOptionId_outputMetadata, .match = "output_metadata", .matchShort = "om", + .placeholder = "", .description = "coded metadata file for BINAURAL_SPLIT_PCM output mode", }, { .id = CmdLnOptionId_SplitRendBFIFile, .match = "post_rend_bfi_file", .matchShort = "prbfi", + .placeholder = "", .description = "Split rendering option: bfi file", }, { .id = CmdLnOptionId_refRotFile, .match = "reference_rotation_file", .matchShort = "rf", + .placeholder = "", .description = "Reference rotation trajectory file for simulation of head tracking\n(only for binaural outputs)", }, { .id = CmdLnOptionId_customHrtfFile, .match = "custom_hrtf", .matchShort = "hrtf", + .placeholder = "", .description = "Custom HRTF file for binaural rendering\n(only for binaural outputs)", }, { .id = CmdLnOptionId_renderConfigFile, .match = "render_config_parameters", .matchShort = "render_config", + .placeholder = "", .description = "Binaural renderer configuration parameters in file\n(only for binaural outputs)", }, { .id = CmdLnOptionId_nonDiegeticPan, .match = "non_diegetic_panning", .matchShort = "non_diegetic_pan", + .placeholder = "", .description = "Panning mono non diegetic sound to stereo -90<= pan <= 90\nleft or l or 90->left, right or r or -90->right,\ncenter or c or 0 ->middle", }, { .id = CmdLnOptionId_orientationTracking, .match = "tracking_type", .matchShort = "otr", + .placeholder = "", .description = "Head orientation tracking type: 'none', 'ref', 'avg' or `ref_vec`\nor `ref_vec_lev` (only for binaural outputs)", }, { .id = CmdlnOptionId_lfePosition, .match = "lfe_position", .matchShort = "lp", + .placeholder = ",,", .description = "Output LFE position. Comma-delimited triplet of [gain, azimuth,\nelevation] where gain is linear (like --gain, -g) and azimuth,\nelevation are in degrees. If specified, overrides the default\nbehavior which attempts to map input to output LFE channel(s)", }, { .id = CmdlnOptionId_lfeMatrix, .match = "lfe_matrix", .matchShort = "lm", + .placeholder = "", .description = "LFE panning matrix. File (CSV table) containing a matrix of\ndimensions [ num_input_lfe x num_output_channels ] with elements\nspecifying linear routing gain (like --gain, -g). If specified,\noverrides the output LFE position option and the default\nbehavior which attempts to map input to output LFE channel(s)", }, { @@ -337,6 +356,7 @@ static const CmdLnParser_Option cliOptions[] = { .id = CmdLnOptionId_complexityLevel, .match = "complexity_level", .matchShort = "level", + .placeholder = "", .description = "Complexity level, level = (1, 2, 3), will be defined after\ncharacterisation.", }, { @@ -349,6 +369,7 @@ static const CmdLnParser_Option cliOptions[] = { .id = CmdLnOptionId_inputGain, .match = "gain", .matchShort = "g", + .placeholder = "", .description = "Input gain (linear, not in dB) to be applied to input audio file", }, { @@ -361,43 +382,50 @@ static const CmdLnParser_Option cliOptions[] = { .id = CmdLnOptionId_referenceVectorFile, .match = "reference_vector_file", .matchShort = "rvf", + .placeholder = "", .description = "Reference vector trajectory file for simulation of head tracking\n(only for binaural outputs)", }, { .id = CmdLnOptionId_exteriorOrientationFile, .match = "exterior_orientation_file", .matchShort = "exof", + .placeholder = "", .description = "External orientation trajectory file for simulation of external\norientations", }, { .id = CmdLnOptionId_framing, .match = "framing", .matchShort = "fr", - .description = "Set Render audio framing.", + .placeholder = "", + .description = "Set render audio framing in ms", }, { .id = CmdLnOptionId_syncMdDelay, .match = "sync_md_delay", .matchShort = "smd", + .placeholder = "", .description = "Metadata Synchronization Delay in ms, Default is 0. Quantized by\n5ms subframes for TDRenderer (13ms -> 10ms -> 2subframes)", }, { .id = CmdLnOptionId_directivityPatternId, .match = "ism_directivity_pattern_id", .matchShort = "dpid", + .placeholder = " [...]", .description = "Directivity pattern ID(s) = [ID1, ID2, ID3, ID4]. Space-separated\nlist of up to 4 numbers (unsigned integers) can be specified for\nBINAURAL and BINAURAL_ROOM_REVERB output.\nID1, ID2, ID3, ID4 specify the directivity pattern IDs used for\nISMs 1,2,3 and 4 respectively. \nThis option needs to be accompanied by a render_config file,\notherwise a default directivity pattern is used.", }, { .id = CmdLnOptionId_acousticEnvironmentId, .match = "acoustic_environment_id", .matchShort = "aeid", + .placeholder = "", .description = "Acoustic environment ID (number > 0) alternatively, it can be\na text file where each line contains \"ID duration\" for\nBINAURAL_ROOM_REVERB output.", }, { .id = CmdLnOptionId_roomSize, .match = "room_size", .matchShort = "rsz", - .description = "Selects default reverb based on a room size (S - small | M - medium | L - large)", + .placeholder = "", + .description = "Selects default reverb based on a room size (S - small |\nM - medium | L - large)", } }; @@ -2596,53 +2624,6 @@ static bool parseReverbRoomSize( return true; } - -static bool checkRequiredArgs( - CmdlnArgs args ) -{ - const CmdLnParser_Option *tmpOption; - - /* Check required arguments */ - bool missingRequiredArg = false; - if ( isEmptyString( args.inputFilePath ) ) - { - tmpOption = findOptionById( CmdLnOptionId_inputFile ); - fprintf( stderr, "Missing required argument: %s (%s)\n", tmpOption->match, tmpOption->matchShort ); - missingRequiredArg = true; - } - - const bool singleInputSpecified = args.inConfig.numAudioObjects != 0 || - args.inConfig.numAmbisonicsBuses != 0 || - args.inConfig.numMultiChannelBuses != 0 || - args.inConfig.numMasaBuses != 0; - - if ( !args.sceneDescriptionInput && !singleInputSpecified ) - { - /* Neither scene description input nor single-type input was specified on command line */ - tmpOption = findOptionById( CmdLnOptionId_inputFormat ); - fprintf( stderr, "Missing required argument: %s (%s)\n", tmpOption->match, tmpOption->matchShort ); - missingRequiredArg = true; - } - if ( isEmptyString( args.outputFilePath ) ) - { - tmpOption = findOptionById( CmdLnOptionId_outputFile ); - fprintf( stderr, "Missing required argument: %s (%s)\n", tmpOption->match, tmpOption->matchShort ); - missingRequiredArg = true; - } - if ( args.outConfig.audioConfig == IVAS_AUDIO_CONFIG_INVALID ) - { - tmpOption = findOptionById( CmdLnOptionId_outputFormat ); - fprintf( stderr, "Missing required argument: %s (%s)\n", tmpOption->match, tmpOption->matchShort ); - missingRequiredArg = true; - } - if ( missingRequiredArg ) - { - CmdLnParser_printUsage( args.executableName, cliOptions, numCliOptions, 100 ); - } - - return !missingRequiredArg; -} - static CmdlnArgs defaultArgs( const char *executableName ) { @@ -2921,12 +2902,7 @@ static CmdlnArgs parseCmdlnArgs( { CmdlnArgs args = defaultArgs( argv[0] ); - if ( CmdLnParser_parseArgs( argc, argv, cliOptions, numCliOptions, &args, parseOption, RENDERER_NUM_MANDATORY_CMD_LINE_PARAMS ) != 0 ) - { - exit( -1 ); /* Error printout handled by failing function */ - } - - if ( !checkRequiredArgs( args ) ) + if ( CmdLnParser_parseArgs( argc, argv, cliOptions, numCliOptions, &args, parseOption ) != 0 ) { exit( -1 ); /* Error printout handled by failing function */ } diff --git a/lib_util/cmdl_tools.c b/lib_util/cmdl_tools.c index 625db1273342f1b56eb20e12c5e31ffbf5ec7bc5..1f551f8917cab5791fd2129031e3cfdc4c6f98bd 100644 --- a/lib_util/cmdl_tools.c +++ b/lib_util/cmdl_tools.c @@ -225,3 +225,28 @@ bool isEmptyString( { return str[0] == '\0'; } + + +/*---------------------------------------------------------------------* + * strLength() + * + * Get the length of a string up to a maximum length. + * + * Equivalent to strnlen, which is not part of the C standard and only provided by POSIX. + * + *---------------------------------------------------------------------*/ + +int32_t strLength( + const char *str, + int32_t maxlen ) +{ + int32_t len; + + len = 0; + while ( len < maxlen && str[len] != '\0' ) + { + len++; + } + + return len; +} diff --git a/lib_util/cmdl_tools.h b/lib_util/cmdl_tools.h index 327acb93d0b1d528874de8ef018a36686379b778..b331dea03df0fea8a0b43c682f940c3773fd4b6e 100644 --- a/lib_util/cmdl_tools.h +++ b/lib_util/cmdl_tools.h @@ -50,4 +50,6 @@ void clearString( char *str ); bool isEmptyString( const char *str ); +int32_t strLength( const char *str, int32_t maxlen ); + #endif /* CMDL_TOOLS_H */ diff --git a/lib_util/cmdln_parser.c b/lib_util/cmdln_parser.c index d51f1ff1eb770c246ef22f48d7c8aa9757edee1f..cd201a0cdc2f50dd23cec8fb4e5978b6f7c6e450 100644 --- a/lib_util/cmdln_parser.c +++ b/lib_util/cmdln_parser.c @@ -36,19 +36,30 @@ #include #include #include +#include +#include -#define MAX_SUPPORTED_OPTS ( 1024 ) -#define MAX_OPTION_LENGTH ( 1024 ) +#define MAX_SUPPORTED_OPTS ( 1024 ) +#define MAX_OPTION_MATCH_LENGTH ( 30 ) +#define MAX_OPTION_LENGTH ( MAX_OPTION_MATCH_LENGTH + 2 ) typedef CmdLnParser_Option OptionProps; typedef struct { OptionProps props; - int8_t hasBeenParsed; + bool hasBeenParsed; } Option; -static int16_t validateNoDuplicateIds( +/* Error enum for internal use */ +typedef enum +{ + CMDLN_PARSER_ERR_OK = 0, + CMDLN_PARSER_ERR_FAILED_PARSING = -1, + CMDLN_PARSER_ERR_MISCONFIGURED = -2, +} CmdLnParserError; + +static CmdLnParserError validateNoDuplicateIds( const OptionProps *props, int32_t numOpts ) { @@ -59,121 +70,136 @@ static int16_t validateNoDuplicateIds( if ( props[i].id == props[j].id ) { fprintf( stderr, "[dev] Duplicate ID == %d between options %s and %s\n", props[i].id, props[i].match, props[j].match ); - return -1; + return CMDLN_PARSER_ERR_MISCONFIGURED; } } } - return 0; + return CMDLN_PARSER_ERR_OK; } -static int16_t validateOptionProps( +static CmdLnParserError validateOptionProps( OptionProps props ) { /* Check required properties */ if ( props.match == NULL ) { - /* TODO(sgi): Don't print out usage after this - props.match is used there */ - fprintf( stderr, "[dev] Option with ID == %d - missing required property \"match\"\n", props.id ); - return -1; + fprintf( stderr, "[dev] Option with ID %d - missing required property \"match\"\n", props.id ); + return CMDLN_PARSER_ERR_MISCONFIGURED; } if ( props.id == 0 ) { fprintf( stderr, "[dev] Invalid ID for option %s. ID == %d is reserved.\n", props.match, props.id ); - return -1; + return CMDLN_PARSER_ERR_MISCONFIGURED; } - return 0; + /* Check match string length */ + if ( strLength( props.match, MAX_OPTION_MATCH_LENGTH + 1 ) > MAX_OPTION_MATCH_LENGTH ) + { + fprintf( stderr, "[dev] Option with ID %d - match string exceeds limit of %d characters.\n", props.id, MAX_OPTION_MATCH_LENGTH ); + return CMDLN_PARSER_ERR_MISCONFIGURED; + } + if ( props.matchShort != NULL && strLength( props.matchShort, MAX_OPTION_MATCH_LENGTH + 1 ) > MAX_OPTION_MATCH_LENGTH ) + { + fprintf( stderr, "[dev] Option with ID %d - matchShort string exceeds limit of %d characters.\n", props.id, MAX_OPTION_MATCH_LENGTH ); + return CMDLN_PARSER_ERR_MISCONFIGURED; + } + + return CMDLN_PARSER_ERR_OK; } /* Validate given OptionProps and use them to initialize array of Options */ -static int16_t initOpts( +static CmdLnParserError initOpts( const OptionProps *options, const int32_t numOpts, Option *opts ) { + CmdLnParserError error; + + if ( numOpts > MAX_SUPPORTED_OPTS ) + { + fprintf( stderr, "[dev] Number of defined options (%d) exceeds limit (%d).\n", numOpts, MAX_SUPPORTED_OPTS ); + return CMDLN_PARSER_ERR_MISCONFIGURED; + } + + /* Check for duplicate IDs */ + if ( ( error = validateNoDuplicateIds( options, numOpts ) ) != 0 ) + { + return error; + } + for ( int32_t i = 0; i < numOpts; ++i ) { - if ( validateOptionProps( options[i] ) != 0 ) + if ( ( error = validateOptionProps( options[i] ) ) != CMDLN_PARSER_ERR_OK ) { - return -1; + return error; } Option tmp = { - .hasBeenParsed = 0 + .hasBeenParsed = false }; tmp.props = options[i]; /* Cannot assign in aggregate initializer above - causes Visual Studio warning */ opts[i] = tmp; } - /* Check for duplicate IDs */ - if ( validateNoDuplicateIds( options, numOpts ) != 0 ) - { - return -1; - } - return 0; } -static int8_t stringLooksLikeOption( +static bool stringLooksLikeOption( const char *str ) { - if ( ( str[0] == '-' ) && is_number( str ) == false ) + if ( ( str[0] == '-' ) && !is_number( str ) ) { - return 1; + return true; } - return 0; + return false; } -static const char *stringToOptionName( - const char *str ) -{ - while ( ( *str == '-' ) && ( ( str[1] != '0' ) || ( str[1] != '1' ) ) ) - { - ++str; - } - - return str; -} - -static int8_t optionMatchesString( +static bool optionMatchesString( Option opt, const char *str ) { if ( !stringLooksLikeOption( str ) ) { - return 0; + return false; } - const char *optionName = stringToOptionName( str ); + if ( strLength( str, MAX_OPTION_LENGTH + 1 ) > MAX_OPTION_LENGTH ) + { + /* String longer than longest possible option - not a match */ + return false; + } - char optionName_to_upper[FILENAME_MAX]; - strncpy( optionName_to_upper, optionName, sizeof( optionName_to_upper ) - 1 ); - optionName_to_upper[sizeof( optionName_to_upper ) - 1] = '\0'; - to_upper( optionName_to_upper ); + char str_to_upper[MAX_OPTION_LENGTH + 1]; + snprintf( str_to_upper, sizeof( str_to_upper ), "%s", str ); + to_upper( str_to_upper ); - char match_to_upper[FILENAME_MAX]; - strncpy( match_to_upper, opt.props.match, sizeof( match_to_upper ) - 1 ); - optionName_to_upper[sizeof( match_to_upper ) - 1] = '\0'; + char match_to_upper[MAX_OPTION_LENGTH + 1]; + snprintf( match_to_upper, sizeof( match_to_upper ), "--%s", opt.props.match ); to_upper( match_to_upper ); + if ( strcmp( str_to_upper, match_to_upper ) == 0 ) + { + return true; + } - char matchShort_to_upper[FILENAME_MAX]; - strncpy( matchShort_to_upper, opt.props.matchShort, sizeof( matchShort_to_upper ) - 1 ); - optionName_to_upper[sizeof( matchShort_to_upper ) - 1] = '\0'; - to_upper( matchShort_to_upper ); - - if ( strncmp( optionName_to_upper, match_to_upper, MAX_OPTION_LENGTH ) == 0 || strncmp( optionName_to_upper, matchShort_to_upper, MAX_OPTION_LENGTH ) == 0 ) + if ( opt.props.matchShort != NULL ) { - return 1; + snprintf( match_to_upper, sizeof( match_to_upper ), "-%s", opt.props.matchShort ); + to_upper( match_to_upper ); + + if ( strcmp( str_to_upper, match_to_upper ) == 0 ) + { + return true; + } } - return 0; + return false; } -static int16_t parseOpts( +static CmdLnParserError parseOpts( int32_t argc, char **argv, Option *opts, @@ -202,8 +228,14 @@ static int16_t parseOpts( /* Check if already parsed */ if ( optToMatch->hasBeenParsed ) { - fprintf( stderr, "Duplicate option: %s (%s)\n", optToMatch->props.match, optToMatch->props.matchShort ); - return -1; + fprintf( stderr, "Duplicate option provided: --%s", optToMatch->props.match ); + if ( optToMatch->props.matchShort != NULL ) + { + fprintf( stderr, "/-%s", optToMatch->props.matchShort ); + } + fprintf( stderr, "\n" ); + + return CMDLN_PARSER_ERR_FAILED_PARSING; } break; @@ -216,8 +248,8 @@ static int16_t parseOpts( /* Invalid option */ if ( stringLooksLikeOption( argv[argIdx] ) ) { - fprintf( stderr, "Unknown option `%s`\n", stringToOptionName( argv[argIdx] ) ); - return -1; + fprintf( stderr, "Unknown option `%s`\n", argv[argIdx] ); + return CMDLN_PARSER_ERR_FAILED_PARSING; } /* Otherwise, value following current option. @@ -229,7 +261,7 @@ static int16_t parseOpts( else { fprintf( stderr, "Unexpected token `%s`\n", argv[argIdx] ); - return -1; + return CMDLN_PARSER_ERR_FAILED_PARSING; } } @@ -240,9 +272,9 @@ static int16_t parseOpts( { if ( parseOption( currOpt->props.id, &argv[currOptIdx + 1], numValues, pOutputStruct ) != 0 ) { - return -1; + return CMDLN_PARSER_ERR_FAILED_PARSING; } - currOpt->hasBeenParsed = 1; + currOpt->hasBeenParsed = true; } currOpt = nextOpt; @@ -258,12 +290,35 @@ static int16_t parseOpts( { if ( parseOption( currOpt->props.id, &argv[currOptIdx + 1], numValues, pOutputStruct ) != 0 ) { - return -1; + return CMDLN_PARSER_ERR_FAILED_PARSING; } - currOpt->hasBeenParsed = 1; + currOpt->hasBeenParsed = true; } - return 0; + /* Check mandatory options */ + bool missingMandatory = false; + for ( int32_t optIdx = 0; optIdx < numOpts; ++optIdx ) + { + Option opt = opts[optIdx]; + + if ( opt.props.isMandatory && !opt.hasBeenParsed ) + { + fprintf( stderr, "Missing mandatory parameter: --%s", opt.props.match ); + if ( opt.props.matchShort != NULL ) + { + fprintf( stderr, "/-%s", opt.props.matchShort ); + } + fprintf( stderr, "\n" ); + + missingMandatory = true; + } + } + if ( missingMandatory ) + { + return CMDLN_PARSER_ERR_FAILED_PARSING; + } + + return CMDLN_PARSER_ERR_OK; } static const char *getBasename( @@ -287,19 +342,22 @@ static const char *getBasename( return path; } -static int32_t totalOptionNameLength( +static int32_t totalNumOptChars( const OptionProps opt ) { - return (int32_t) ( strlen( opt.match ) + strlen( opt.matchShort ) ); -} + int32_t len = (int32_t) strlen( opt.match ); -static void printWhitespace( - const int32_t n ) -{ - for ( int32_t i = 0; i < n; ++i ) + if ( opt.matchShort != NULL ) { - fprintf( stderr, " " ); + len += (int32_t) strlen( opt.matchShort ); } + + if ( opt.placeholder != NULL ) + { + len += (int32_t) strlen( opt.placeholder ); + } + + return len; } static void printOptDescriptionAligned( @@ -323,7 +381,7 @@ static void printOptDescriptionAligned( fprintf( stderr, "%c", *descPtr ); if ( *descPtr == '\n' ) { - printWhitespace( descriptionColumnIdx ); + fprintf( stderr, "%*s", descriptionColumnIdx, "" ); } ++descPtr; } @@ -331,50 +389,92 @@ static void printOptDescriptionAligned( return; } -static void printUsage( - const char *argv0, +static void printOptions( const OptionProps *optionProps, const int32_t numOptions, - const int16_t numMandatoryCmdLineParams ) + const bool mandatory, + const int32_t maxNumOptChars ) { - int32_t optNameLength; + const int32_t descriptionColumnIdx = maxNumOptChars + 11 /* Additional chars we will print in the options column */; + int32_t numOptChars; - fprintf( stderr, "\n" ); - fprintf( stderr, "Usage: %s [options]\n", getBasename( argv0 ) ); - fprintf( stderr, "\n" ); - - /* Find option with longest name, used for pretty formatting */ - int32_t maxOptNameLength = 0; for ( int32_t i = 0; i < numOptions; ++i ) { - optNameLength = totalOptionNameLength( optionProps[i] ); - if ( maxOptNameLength < optNameLength ) + OptionProps opt = optionProps[i]; + + if ( opt.isMandatory != mandatory ) + { + continue; + } + + numOptChars = totalNumOptChars( optionProps[i] ); + + fprintf( stderr, " --%s", opt.match ); + numOptChars += 4; + + if ( opt.matchShort != NULL ) + { + fprintf( stderr, ", -%s", opt.matchShort ); + numOptChars += 3; + } + + if ( opt.placeholder != NULL ) + { + fprintf( stderr, " %s", opt.placeholder ); + numOptChars += 1; + } + + if ( opt.description != NULL ) + { + /* Done printing options column, fill with whitespace until description column */ + fprintf( stderr, "%*s", descriptionColumnIdx - numOptChars, ": " ); + printOptDescriptionAligned( opt.description, descriptionColumnIdx ); + } + else { - maxOptNameLength = optNameLength; + fprintf( stderr, "\n" ); } } +} - fprintf( stderr, "Mandatory parameters:\n" ); +static void printUsage( + const char *argv0, + const OptionProps *optionProps, + const int32_t numOptions ) +{ + fprintf( stderr, "\n" ); + fprintf( stderr, "Usage: %s [options]\n", getBasename( argv0 ) ); + fprintf( stderr, "\n" ); - const int32_t preDescriptionWhitespace = 8; - const int32_t leftColumnAdditionalChars = 7; + int32_t numOptChars; + int32_t maxNumOptChars = 0; + bool hasMandatoryOptions = false; for ( int32_t i = 0; i < numOptions; ++i ) { - OptionProps opt = optionProps[i]; - optNameLength = totalOptionNameLength( optionProps[i] ); - - if ( i == numMandatoryCmdLineParams ) + /* Find option with most characters when printed, used for pretty formatting */ + numOptChars = totalNumOptChars( optionProps[i] ); + if ( maxNumOptChars < numOptChars ) { - fprintf( stderr, "\nOptions:\n" ); + maxNumOptChars = numOptChars; } - /* TODO(sgi): make matchShort optional */ - fprintf( stderr, " --%s, -%s", opt.match, opt.matchShort ); + /* Check if mandatory parameters should be printed separately */ + if ( optionProps[i].isMandatory ) + { + hasMandatoryOptions = true; + } + } - printWhitespace( maxOptNameLength - optNameLength + preDescriptionWhitespace ); - printOptDescriptionAligned( opt.description, maxOptNameLength + preDescriptionWhitespace + leftColumnAdditionalChars ); + if ( hasMandatoryOptions ) + { + fprintf( stderr, "Mandatory parameters:\n---------------------\n" ); + printOptions( optionProps, numOptions, true, maxNumOptChars ); + fprintf( stderr, "\n" ); } + fprintf( stderr, "Options:\n--------\n" ); + printOptions( optionProps, numOptions, false, maxNumOptChars ); + return; } @@ -384,38 +484,66 @@ int16_t CmdLnParser_parseArgs( const OptionProps *optionProps, const int32_t numOptions, void *pOutputStruct, - CmdLnParser_FnPtr_ParseOption parseOption, - const int16_t numMandatoryCmdLineParams ) + CmdLnParser_FnPtr_ParseOption parseOption ) { - assert( numOptions <= MAX_SUPPORTED_OPTS ); + CmdLnParserError error; /* Prepare option array */ Option opts[MAX_SUPPORTED_OPTS]; - if ( initOpts( optionProps, numOptions, opts ) != 0 ) + if ( ( error = initOpts( optionProps, numOptions, opts ) ) != CMDLN_PARSER_ERR_OK ) { goto fail; } /* Iterate over argv and parse */ - if ( parseOpts( argc, argv, opts, numOptions, pOutputStruct, parseOption ) != 0 ) + if ( ( error = parseOpts( argc, argv, opts, numOptions, pOutputStruct, parseOption ) ) != CMDLN_PARSER_ERR_OK ) { goto fail; } - return 0; - fail: - printUsage( argv[0], optionProps, numOptions, numMandatoryCmdLineParams ); - return -1; + switch ( error ) + { + case CMDLN_PARSER_ERR_OK: + break; + case CMDLN_PARSER_ERR_MISCONFIGURED: + fprintf( stderr, "CmdLnParser is misconfigured.\n" ); + break; + case CMDLN_PARSER_ERR_FAILED_PARSING: + printUsage( argv[0], optionProps, numOptions ); + break; + } + + return error; } -void CmdLnParser_printUsage( +int16_t CmdLnParser_printUsage( char *executableName, const CmdLnParser_Option *options, - const int32_t numOptions, - const int16_t numMandatoryCmdLineParams ) + const int32_t numOptions ) { - printUsage( executableName, options, numOptions, numMandatoryCmdLineParams ); + CmdLnParserError error; - return; + /* Re-use initOpts for validation only */ + Option opts[MAX_SUPPORTED_OPTS]; + if ( ( error = initOpts( options, numOptions, opts ) ) != CMDLN_PARSER_ERR_OK ) + { + goto fail; + } + + printUsage( executableName, options, numOptions ); + +fail: + switch ( error ) + { + case CMDLN_PARSER_ERR_OK: + break; + case CMDLN_PARSER_ERR_MISCONFIGURED: + fprintf( stderr, "CmdLnParser is misconfigured.\n" ); + break; + case CMDLN_PARSER_ERR_FAILED_PARSING: + break; + } + + return error; } diff --git a/lib_util/cmdln_parser.h b/lib_util/cmdln_parser.h index 58deb4a41e40c1cd606d5b842588acac8c6d065d..c6848f79b81934854ecea336b3002b7d72b377c3 100644 --- a/lib_util/cmdln_parser.h +++ b/lib_util/cmdln_parser.h @@ -38,17 +38,21 @@ typedef struct { - int32_t id; - const char *match; - const char *matchShort; - const char *description; + int32_t id; /* Unique ID for the option */ + const char *match; /* String to match, e.g. "input" here will match "--input" on CLI */ + + /* Struct members below are optional and can be omitted in option definition. If omitted, C will implicitly set them to 0. */ + const char *matchShort; /* Short version of the string to match, e.g. "i" here will match "-i" on CLI */ + const char *placeholder; /* If not NULL, this will follow the match string in the usage printout, e.g. "" here will print "--input " on CLI */ + const char *description; /* Description of the option for the usage printout. May contain '\n' for line breaks. */ + bool isMandatory; /* Parsing will fail if an option with `isMandatory == true` is not given */ } CmdLnParser_Option; /* Function for parsing option values into an output struct, to be implemented by the user */ typedef int16_t ( *CmdLnParser_FnPtr_ParseOption )( int32_t optionId, char **optionValues, int16_t numOptionValues, void *pOutputStruct ); -int16_t CmdLnParser_parseArgs( int32_t argc, char **argv, const CmdLnParser_Option *options, const int32_t numOptions, void *pOutputStruct, CmdLnParser_FnPtr_ParseOption parseOption, const int16_t numMandatoryCmdLineParams ); +int16_t CmdLnParser_parseArgs( int32_t argc, char **argv, const CmdLnParser_Option *options, const int32_t numOptions, void *pOutputStruct, CmdLnParser_FnPtr_ParseOption parseOption ); -void CmdLnParser_printUsage( char *executableName, const CmdLnParser_Option *options, const int32_t numOptions, const int16_t numMandatoryCmdLineParams ); +int16_t CmdLnParser_printUsage( char *executableName, const CmdLnParser_Option *options, const int32_t numOptions ); #endif /* CMDLN_PARSER_H */