Repository: pgbackrest/pgbackrest
Pull request #2625: Fix defaults in command-line help.
Author: login: dwsteele
Labels: name: module [core]
name: enhancement
Created: 2025-05-08T14:02:56Z | Merged: 2025-05-12T17:33:42Z

This is another attempt to fix broken defaults in the command-line help. PR #2395 tried to do this by removing current and default values (at least from the list) but this removed much of the value of command-line help being context sensitive. Improvements were made to current values in #2453 but defaults were left as-is, i.e. broken in some cases.

This new approach handles defaults much the same way as current values. If there are multiple defaults for an option then it is displayed in the option list as <multi> and enumerated in the specific option help. For example, if repo1-type=s3 and repo2-type=azure then the defaults for repo1-storage-upload-chunk-size and repo2-storage-upload-chunk-size will differ.

However, getting the above right means we can no longer display defaults for options that are not valid in the current configuration. For instance, if repo1-type=posix then none of the repo1-s3-* options are valid and therefore their defaults are not displayed in help. In this case they don't have defaults and if you tried to set one of these options you would get an error. Another option here would be to not display the option at all (probably confusing) or mark it [inactive] in the list. Marking options inactive would require more work than I've done here due to some behind-the-scenes defaulting we do for programmatic convenience, but it is a possibility.


Diff:
diff --git a/doc/xml/release/2025/2.56.0.xml b/doc/xml/release/2025/2.56.0.xml
index 7e608fa964..45bb8b59bc 100644
--- a/doc/xml/release/2025/2.56.0.xml
+++ b/doc/xml/release/2025/2.56.0.xml
@@ -1,2 +1,17 @@
 <release date="XXXX-XX-XX" version="2.56.0dev" title="UNDER DEVELOPMENT">
+    <release-core-list>
+        <release-improvement-list>
+            <release-item>
+                <github-pull-request id="2625"/>
+
+                <release-item-contributor-list>
+                    <release-item-contributor id="david.steele"/>
+                    <release-item-reviewer id="david.christensen"/>
+                    <release-item-reviewer id="chris.bandy"/>
+                </release-item-contributor-list>
+
+                <p>Fix defaults in command-line help.</p>
+            </release-item>
+        </release-improvement-list>
+    </release-core-list>
 </release>
diff --git a/src/command/help/help.c b/src/command/help/help.c
index d9cd4bb2e4..08b0d0ed65 100644
--- a/src/command/help/help.c
+++ b/src/command/help/help.c
@@ -261,6 +261,45 @@ helpRenderValue(const ConfigOption optionId)
     FUNCTION_TEST_RETURN(STRING, result);
 }
 
+/***********************************************************************************************************************************
+Helper function for helpRender() to output defaults
+***********************************************************************************************************************************/
+static String *
+helpRenderDefault(const ConfigOption optionId)
+{
+    FUNCTION_TEST_BEGIN();
+        FUNCTION_TEST_PARAM(ENUM, optionId);
+    FUNCTION_TEST_END();
+
+    String *result = strDup(cfgOptionIdxDefaultValue(optionId, 0));
+
+    if (cfgOptionGroup(optionId))
+    {
+        MEM_CONTEXT_TEMP_BEGIN()
+        {
+            for (unsigned int optionIdx = 1; optionIdx < cfgOptionGroupIdxTotal(cfgOptionGroupId(optionId)); optionIdx++)
+            {
+                const String *const defaultValue = cfgOptionIdxDefaultValue(optionId, optionIdx);
+
+                if (!strEq(result, defaultValue))
+                {
+                    MEM_CONTEXT_PRIOR_BEGIN()
+                    {
+                        strFree(result);
+                        result = strNewZ("<multi>");
+                    }
+                    MEM_CONTEXT_PRIOR_END();
+
+                    break;
+                }
+            }
+        }
+        MEM_CONTEXT_TEMP_END();
+    }
+
+    FUNCTION_TEST_RETURN(STRING, result);
+}
+
 /***********************************************************************************************************************************
 Determine if the first character of a summary should be lower-case
 ***********************************************************************************************************************************/
@@ -551,7 +590,7 @@ helpRender(const Buffer *const helpData)
                         String *const summary = helpRenderSummary(optionData[optionId].summary);
 
                         // Output current and default values if they exist
-                        const String *const defaultValue = cfgOptionDefault(optionId);
+                        const String *const defaultValue = helpRenderDefault(optionId);
                         const String *const value = helpRenderValue(optionId);
 
                         if (value != NULL || defaultValue != NULL)
@@ -619,7 +658,7 @@ helpRender(const Buffer *const helpData)
                             CONSOLE_WIDTH)));
 
                 // Output current and default values if they exist
-                const String *const defaultValue = cfgOptionDefault(option.id);
+                const String *const defaultValue = helpRenderDefault(option.id);
                 const String *const value = helpRenderValue(option.id);
 
                 if (value != NULL || defaultValue != NULL)
@@ -651,7 +690,26 @@ helpRender(const Buffer *const helpData)
                     }
 
                     if (defaultValue != NULL)
-                        strCatFmt(result, "default: %s\n", strZ(defaultValue));
+                    {
+                        strCatZ(result, "default:");
+
+                        if (!strEqZ(defaultValue, "<multi>"))
+                            strCatFmt(result, " %s\n", strZ(defaultValue));
+                        else
+                        {
+                            const unsigned int groupId = cfgOptionGroupId(option.id);
+
+                            strCatChr(result, '\n');
+
+                            for (unsigned int optionIdx = 0; optionIdx < cfgOptionGroupIdxTotal(groupId); optionIdx++)
+                            {
+                                const String *const defaultValue = cfgOptionIdxDefaultValue(option.id, optionIdx);
+
+                                if (defaultValue != NULL)
+                                    strCatFmt(result, "  %s: %s\n", cfgOptionGroupName(groupId, optionIdx), strZ(defaultValue));
+                            }
+                        }
+                    }
                 }
 
                 // Output alternate name (call it deprecated so the user will know not to use it)
diff --git a/src/config/config.c b/src/config/config.c
index 6ab9959dfd..25cf81e170 100644
--- a/src/config/config.c
+++ b/src/config/config.c
@@ -410,21 +410,20 @@ cfgOptionIdxTotal(const ConfigOption optionId)
 
 /**********************************************************************************************************************************/
 FN_EXTERN const String *
-cfgOptionDefault(const ConfigOption optionId)
+cfgOptionIdxDefaultValue(const ConfigOption optionId, const unsigned int optionIdx)
 {
     FUNCTION_TEST_BEGIN();
         FUNCTION_TEST_PARAM(ENUM, optionId);
+        FUNCTION_TEST_PARAM(UINT, optionIdx);
     FUNCTION_TEST_END();
 
     ASSERT(cfgInited());
     ASSERT(optionId < CFG_OPTION_TOTAL);
+    ASSERT_DECLARE(const bool group = configLocal->option[optionId].group);
+    ASSERT_DECLARE(const unsigned int indexTotal = configLocal->optionGroup[configLocal->option[optionId].groupId].indexTotal);
+    ASSERT((!group && optionIdx == 0) || (group && optionIdx < indexTotal));
 
-    ConfigOptionData *const option = &configLocal->option[optionId];
-
-    if (option->defaultValue == NULL)
-        option->defaultValue = cfgParseOptionDefault(cfgCommand(), optionId, cfgBin());
-
-    FUNCTION_TEST_RETURN_CONST(STRING, option->defaultValue);
+    FUNCTION_TEST_RETURN_CONST(STRING, configLocal->option[optionId].index[optionIdx].defaultValue);
 }
 
 /**********************************************************************************************************************************/
@@ -927,6 +926,21 @@ cfgOptionIdxSet(
         optionValueType->string = NULL;
     }
 
+    // Set the default when specified
+    if (source == cfgSourceDefault)
+    {
+        if (value != NULL)
+        {
+            MEM_CONTEXT_BEGIN(configLocal->memContext)
+            {
+                optionValue->defaultValue = cfgOptionDisplayVar(value, cfgParseOptionType(optionId));
+            }
+            MEM_CONTEXT_END();
+        }
+        else
+            optionValue->defaultValue = NULL;
+    }
+
     // Clear the display value, which will be generated when needed
     optionValue->display = NULL;
 
diff --git a/src/config/config.intern.h b/src/config/config.intern.h
index 45a20c51c0..d0829da252 100644
--- a/src/config/config.intern.h
+++ b/src/config/config.intern.h
@@ -46,6 +46,7 @@ typedef struct ConfigOptionValue
     bool reset;                                                     // Is the option reset?
     unsigned int source;                                            // Where the option came from, i.e. ConfigSource enum
     const String *display;                                          // Current display value, if any. Used for messages, etc.
+    const String *defaultValue;                                     // Default value
     ConfigOptionValueType value;                                    // Option value
 } ConfigOptionValue;
 
@@ -56,7 +57,6 @@ typedef struct ConfigOptionData
     bool group;                                                     // In a group?
     unsigned int groupId;                                           // Id if in a group
     ConfigOptionDataType dataType;                                  // Underlying data type
-    const String *defaultValue;                                     // Default value
     ConfigOptionValue *index;                                       // List of indexed values (only 1 unless the option is indexed)
     const String **indexName;                                       // Index names (e.g. repo1-path, repo2-path)
 } ConfigOptionData;
@@ -110,7 +110,7 @@ FN_EXTERN unsigned int cfgOptionGroupId(ConfigOption optionId);
 Option Functions
 ***********************************************************************************************************************************/
 // Option default - should only be called by the help command
-FN_EXTERN const String *cfgOptionDefault(ConfigOption optionId);
+FN_EXTERN const String *cfgOptionIdxDefaultValue(ConfigOption optionId, unsigned int optionIdx);
 
 // Format a variant for display using the supplied option type. cfgOptionDisplay()/cfgOptionIdxDisplay() should be used whenever
 // possible, but sometimes the variant needs to be manipulated before being formatted.
diff --git a/src/config/parse.c b/src/config/parse.c
index 824bb00d2f..87f8de96f3 100644
--- a/src/config/parse.c
+++ b/src/config/parse.c
@@ -2672,23 +2672,29 @@ cfgParse(const Storage *const storage, const unsigned int argListSize, const cha
                     {
                         configOptionValue->source = parseOptionValue->source;
                     }
-                    // Else try to set a default
-                    else
+
+                    if ((!configOptionValue->set && !parseOptionValue->negate) || config->help)
                     {
                         // If the option has a default
                         if (cfgParseOptionalRule(&optionalRules, parseRuleOptionalTypeDefault, config->command, optionId))
                         {
-                            configOptionValue->set = true;
-                            configOptionValue->value = optionalRules.defaultValue;
+                            if (!configOptionValue->set)
+                            {
+                                configOptionValue->set = true;
+                                configOptionValue->value = optionalRules.defaultValue;
+                                configOptionValue->display = optionalRules.defaultRaw;
+                            }
+
+                            configOptionValue->defaultValue = optionalRules.defaultRaw;
                         }
                         // Else error if option is required and help was not requested
-                        else
+                        else if (!config->help)
                         {
                             const bool required =
                                 cfgParseOptionalRule(&optionalRules, parseRuleOptionalTypeRequired, config->command, optionId) ?
                                     optionalRules.required : ruleOption->required;
 
-                            if (required && !config->help)
+                            if (required)
                             {
                                 THROW_FMT(
                                     OptionRequiredError, "%s command requires option: %s%s",
@@ -2707,6 +2713,8 @@ cfgParse(const Storage *const storage, const unsigned int argListSize, const cha
                 {
                     configOptionValue->set = true;
                     configOptionValue->value.boolean = dependResult.defaultValue;
+                    configOptionValue->defaultValue = optionalRules.defaultRaw;
+                    configOptionValue->display = optionalRules.defaultRaw;
                 }
 
                 pckReadFree(optionalRules.pack);
diff --git a/test/src/module/command/helpTest.c b/test/src/module/command/helpTest.c
index b99a235e77..9a19c29e9f 100644
--- a/test/src/module/command/helpTest.c
+++ b/test/src/module/command/helpTest.c
@@ -242,9 +242,9 @@ testRun(void)
             "                                      specified directory\n"
             "  --target                            recovery target\n"
             "  --target-action                     action to take when recovery target is\n"
-            "                                      reached [default=pause]\n"
+            "                                      reached\n"
             "  --target-exclusive                  stop just before the recovery target is\n"
-            "                                      reached [default=n]\n"
+            "                                      reached\n"
             "  --target-timeline                   recover along a timeline\n"
             "  --type                              recovery type [default=default]\n"
             "\n"
@@ -298,19 +298,17 @@ testRun(void)
             "  --repo-azure-account                azure repository account\n"
             "  --repo-azure-container              azure repository container\n"
             "  --repo-azure-endpoint               azure repository endpoint\n"
-            "                                      [default=blob.core.windows.net]\n"
             "  --repo-azure-key                    azure repository key\n"
-            "  --repo-azure-key-type               azure repository key type [default=shared]\n"
-            "  --repo-azure-uri-style              azure URI Style [default=host]\n"
+            "  --repo-azure-key-type               azure repository key type\n"
+            "  --repo-azure-uri-style              azure URI Style\n"
             "  --repo-cipher-pass                  repository cipher passphrase\n"
             "                                      [current=<redacted>]\n"
             "  --repo-cipher-type                  cipher used to encrypt the repository\n"
             "                                      [current=<multi>, default=none]\n"
             "  --repo-gcs-bucket                   GCS repository bucket\n"
             "  --repo-gcs-endpoint                 GCS repository endpoint\n"
-            "                                      [default=storage.googleapis.com]\n"
             "  --repo-gcs-key                      GCS repository key\n"
-            "  --repo-gcs-key-type                 GCS repository key type [default=service]\n"
+            "  --repo-gcs-key-type                 GCS repository key type\n"
             "  --repo-gcs-user-project             GCS project ID\n"
             "  --repo-host                         repository host when operating remotely\n"
             "                                      [current=<multi>]\n"
@@ -339,19 +337,19 @@ testRun(void)
             "  --repo-s3-endpoint                  S3 repository endpoint\n"
             "  --repo-s3-key                       S3 repository access key\n"
             "  --repo-s3-key-secret                S3 repository secret access key\n"
-            "  --repo-s3-key-type                  S3 repository key type [default=shared]\n"
+            "  --repo-s3-key-type                  S3 repository key type\n"
             "  --repo-s3-kms-key-id                S3 repository KMS key\n"
             "  --repo-s3-region                    S3 repository region\n"
-            "  --repo-s3-requester-pays            S3 repository requester pays [default=n]\n"
+            "  --repo-s3-requester-pays            S3 repository requester pays\n"
             "  --repo-s3-role                      S3 repository role\n"
             "  --repo-s3-sse-customer-key          S3 repository SSE customer key\n"
             "  --repo-s3-token                     S3 repository security token\n"
-            "  --repo-s3-uri-style                 S3 URI Style [default=host]\n"
+            "  --repo-s3-uri-style                 S3 URI Style\n"
             "  --repo-sftp-host                    SFTP repository host\n"
             "  --repo-sftp-host-fingerprint        SFTP repository host fingerprint\n"
-            "  --repo-sftp-host-key-check-type     SFTP host key check type [default=strict]\n"
+            "  --repo-sftp-host-key-check-type     SFTP host key check type\n"
             "  --repo-sftp-host-key-hash-type      SFTP repository host key hash type\n"
-            "  --repo-sftp-host-port               SFTP repository host port [default=22]\n"
+            "  --repo-sftp-host-port               SFTP repository host port\n"
             "  --repo-sftp-host-user               SFTP repository host user\n"
             "  --repo-sftp-known-host              SFTP known hosts file\n"
             "  --repo-sftp-private-key-file        SFTP private key file\n"
@@ -360,11 +358,11 @@ testRun(void)
             "  --repo-storage-ca-file              repository storage CA file\n"
             "  --repo-storage-ca-path              repository storage CA path\n"
             "  --repo-storage-host                 repository storage host\n"
-            "  --repo-storage-port                 repository storage port [default=443]\n"
+            "  --repo-storage-port                 repository storage port\n"
             "  --repo-storage-tag                  repository storage tag(s)\n"
             "  --repo-storage-upload-chunk-size    repository storage upload chunk size\n"
+            "                                      [default=4194304]\n"
             "  --repo-storage-verify-tls           repository storage certificate verify\n"
-            "                                      [default=y]\n"
             "  --repo-target-time                  target time for repository\n"
             "  --repo-type                         type of storage used for the repository\n"
             "                                      [default=posix]\n"
@@ -639,6 +637,61 @@ testRun(void)
         strLstAddZ(argList, "repo-host");
         TEST_RESULT_VOID(testCfgLoad(argList), "help for restore command, repo-host option");
         TEST_RESULT_STR_Z(helpRender(helpData), optionHelp, "check text");
+
+        // -------------------------------------------------------------------------------------------------------------------------
+        TEST_TITLE("multiple default values");
+
+        #define HELP_OPTION_CHUNK                                                                                                  \
+            "%s - 'restore' command - 'repo-storage-upload-chunk-size' option help\n"                                              \
+            "\n"                                                                                                                   \
+            "Repository storage upload chunk size.\n"                                                                              \
+            "\n"                                                                                                                   \
+            "Object stores such as S3 allow files to be uploaded in chunks when the file is\n"                                     \
+            "too large to be stored in memory. Even if the file can be stored in memory, it\n"                                     \
+            "is more memory efficient to limit the amount of memory used for uploads.\n"                                           \
+            "\n"                                                                                                                   \
+            "A larger chunk size will generally lead to better performance because it will\n"                                      \
+            "minimize upload requests and allow more files to be uploaded in a single\n"                                           \
+            "request rather than in chunks. The disadvantage is that memory usage will be\n"                                       \
+            "higher and because the chunk buffer must be allocated per process, larger\n"                                          \
+            "process-max values will lead to more memory being consumed overall.\n"                                                \
+            "\n"                                                                                                                   \
+            "Default chunk sizes by repo type:\n"                                                                                  \
+            "\n"                                                                                                                   \
+            "* azure - 4MiB\n"                                                                                                     \
+            "* gcs - 4MiB\n"                                                                                                       \
+            "* s3 - 5MiB\n"                                                                                                        \
+            "\n"                                                                                                                   \
+            "Note that valid chunk sizes vary by storage type and by platform. For example,\n"                                     \
+            "AWS S3 has a minimum chunk size of 5MiB but S3 clones may accept lower values.\n"                                     \
+            "Terminology for chunk size varies by storage type, so when searching min/max\n"                                       \
+            "values use \"part size\" for AWS S3, \"chunk size\" for GCS, and \"block size\" for\n"                                \
+            "Azure. No attempt is made to validate configured chunk sizes so selecting an\n"                                       \
+            "invalid value will lead to errors from the storage service or undefined\n"                                            \
+            "behavior.\n"
+
+        optionHelp = zNewFmt(
+            HELP_OPTION_CHUNK
+            "\n"
+            "default:\n"
+            "  repo1: 5242880\n"
+            "  repo2: 4194304\n",
+            helpVersion);
+
+        argList = strLstNew();
+        strLstAddZ(argList, "/path/to/pgbackrest");
+        hrnCfgArgKeyRawZ(argList, cfgOptRepoType, 1, "s3");
+        hrnCfgArgKeyRawZ(argList, cfgOptRepoType, 2, "azure");
+        hrnCfgArgKeyRawZ(argList, cfgOptRepoType, 3, "gcs");
+        strLstAddZ(argList, "help");
+        strLstAddZ(argList, "restore");
+        strLstAddZ(argList, "repo-storage-upload-chunk-size");
+        TEST_RESULT_VOID(testCfgLoad(argList), "help for restore command, repo-storage-upload-chunk-size option");
+
+        // There is currently no way for some defaults to be NULL and others not but it could happen in the future
+        cfgOptionIdxSet(cfgOptRepoStorageUploadChunkSize, 2, cfgSourceDefault, NULL);
+
+        TEST_RESULT_STR_Z(helpRender(helpData), optionHelp, "check text");
     }
 
     // *****************************************************************************************************************************
diff --git a/test/src/module/config/parseTest.c b/test/src/module/config/parseTest.c
index e3924e4c48..0e76c25475 100644
--- a/test/src/module/config/parseTest.c
+++ b/test/src/module/config/parseTest.c
@@ -1945,7 +1945,7 @@ testRun(void)
         TEST_RESULT_INT(cfgOptionInt64(cfgOptBufferSize), 65536, "buffer-size is set");
         TEST_RESULT_INT(cfgOptionSource(cfgOptBufferSize), cfgSourceConfig, "backup-standby is source config");
         TEST_RESULT_UINT(cfgOptionUInt64(cfgOptDbTimeout), 1800000, "db-timeout is set");
-        TEST_RESULT_STR_Z(cfgOptionDisplay(cfgOptDbTimeout), "1800", "db-timeout display");
+        TEST_RESULT_STR_Z(cfgOptionDisplay(cfgOptDbTimeout), "30m", "db-timeout display");
         TEST_RESULT_UINT(cfgOptionUInt64(cfgOptProtocolTimeout), 3600000, "protocol-timeout is set");
         TEST_RESULT_UINT(cfgOptionIdxUInt(cfgOptPgPort, 1), 5432, "pg2-port is set");
         TEST_RESULT_UINT(cfgOptionIdxUInt64(cfgOptPgPort, 1), 5432, "pg2-port is set");
@@ -1957,13 +1957,18 @@ testRun(void)
         TEST_RESULT_BOOL(cfgParseOptionRequired(cfgCmdBackup, cfgOptPgHost), false, "pg-host is not required for backup");
         TEST_RESULT_BOOL(cfgParseOptionRequired(cfgCmdInfo, cfgOptStanza), false, "stanza is not required for info");
 
-        TEST_RESULT_STR_Z(cfgOptionDefault(cfgOptBackupStandby), "n", "backup-standby default is false");
-        TEST_RESULT_STR_Z(cfgOptionDefault(cfgOptBackupStandby), "n", "backup-standby default is false (again)");
-        TEST_RESULT_PTR(cfgOptionDefault(cfgOptPgHost), NULL, "pg-host default is NULL");
-        TEST_RESULT_STR_Z(cfgOptionDefault(cfgOptLogLevelConsole), "warn", "log-level-console default is warn");
-        TEST_RESULT_STR_Z(cfgOptionDefault(cfgOptPgPort), "5432", "pg-port default is 5432");
-        TEST_RESULT_STR_Z(cfgOptionDisplay(cfgOptPgPort), "5432", "pg-port display is 5432");
-        TEST_RESULT_STR_Z(cfgOptionDefault(cfgOptDbTimeout), "30m", "db-timeout default is 30m");
+        TEST_RESULT_STR_Z(cfgOptionIdxDefaultValue(cfgOptBackupStandby, 0), "n", "backup-standby default is false");
+        TEST_RESULT_STR_Z(cfgOptionIdxDefaultValue(cfgOptBackupStandby, 0), "n", "backup-standby default is false (again)");
+        TEST_RESULT_PTR(cfgOptionIdxDefaultValue(cfgOptPgHost, 0), NULL, "pg-host default is NULL");
+        TEST_RESULT_STR_Z(cfgOptionIdxDefaultValue(cfgOptLogLevelConsole, 0), "warn", "log-level-console default is warn");
+        TEST_RESULT_STR_Z(cfgOptionIdxDefaultValue(cfgOptPgPort, 0), "5432", "pg-port default is 5432");
+        TEST_RESULT_STR_Z(cfgOptionIdxDisplay(cfgOptPgPort, 0), "5432", "pg-port display is 5432");
+        TEST_RESULT_VOID(cfgOptionSet(cfgOptDbTimeout, cfgSourceDefault, VARINT64(30000)), "set db-timeout default");
+        TEST_RESULT_STR_Z(cfgOptionIdxDefaultValue(cfgOptDbTimeout, 0), "30", "db-timeout default is 30m");
+
+        TEST_RESULT_STR_Z(cfgParseOptionDefault(cfgCmdBackup, cfgOptPgHost, STRDEF("pgbackrest")), NULL, "default not found");
+        TEST_RESULT_STR_Z(
+            cfgParseOptionDefault(cfgCmdBackup, cfgOptRepoPath, STRDEF("pgbackrest")), "/var/lib/pgbackrest", "default found");
 
         TEST_RESULT_VOID(
             cfgOptionIdxSet(cfgOptPgSocketPath, 1, cfgSourceDefault, VARSTRDEF("/default")), "set pg-socket-path default");


Reviews:
user: login: pgguru
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T17:07:09Z
user: login: dwsteele
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T19:26:04Z
user: login: dwsteele
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T19:26:14Z
user: login: dwsteele
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T19:28:32Z
user: login: cbandy
body: LGTM
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T15:34:50Z
user: login: pgguru
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T15:36:04Z
user: login: pgguru
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T15:36:21Z

Review comments:
diff_hunk: @@ -2672,17 +2672,23 @@ cfgParse(const Storage *const storage, const unsigned int argListSize, const cha
                     {
                         configOptionValue->source = parseOptionValue->source;
                     }
-                    // Else try to set a default
-                    else
+
+                    if ((!configOptionValue->set && !parseOptionValue->negate) || config->help)
                     {
                         // If the option has a default
                         if (cfgParseOptionalRule(&optionalRules, parseRuleOptionalTypeDefault, config->command, optionId))
                         {
-                            configOptionValue->set = true;
-                            configOptionValue->value = optionalRules.defaultValue;
+                            if (!configOptionValue->set)
+                            {
+                                configOptionValue->set = true;
+                                configOptionValue->value = optionalRules.defaultValue;
+                                configOptionValue->display = optionalRules.defaultRaw;
+                            }
+
+                            configOptionValue->defaultValue = optionalRules.defaultRaw;
                         }
                         // Else error if option is required and help was not requested
-                        else
+                        else if (!configOptionValue->set)
path: src/config/parse.c
user: login: pgguru
body: Maybe the indentation is off here, but it looks like this condition would always be true given the outer conditional at :2676 (so indistinguishable from an `else` block, along with no-oping any theoretical else at the end of this construct).
created_at: 2025-05-08T16:58:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -410,21 +410,20 @@ cfgOptionIdxTotal(const ConfigOption optionId)
 
 /**********************************************************************************************************************************/
 FN_EXTERN const String *
-cfgOptionDefault(const ConfigOption optionId)
+cfgOptionIdxDefaultValue(const ConfigOption optionId, const unsigned int optionIdx)
 {
     FUNCTION_TEST_BEGIN();
         FUNCTION_TEST_PARAM(ENUM, optionId);
+        FUNCTION_TEST_PARAM(UINT, optionIdx);
     FUNCTION_TEST_END();
 
     ASSERT(cfgInited());
     ASSERT(optionId < CFG_OPTION_TOTAL);
+    ASSERT_DECLARE(const bool group = configLocal->option[optionId].group);
+    ASSERT_DECLARE(const unsigned int indexTotal = configLocal->optionGroup[configLocal->option[optionId].groupId].indexTotal);
path: src/config/config.c
user: login: pgguru
body: This is a little hard to follow; not sure if there's anything to do to make it a little more clear, but maybe some sort of macro?
created_at: 2025-05-08T17:00:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -410,21 +410,20 @@ cfgOptionIdxTotal(const ConfigOption optionId)
 
 /**********************************************************************************************************************************/
 FN_EXTERN const String *
-cfgOptionDefault(const ConfigOption optionId)
+cfgOptionIdxDefaultValue(const ConfigOption optionId, const unsigned int optionIdx)
 {
     FUNCTION_TEST_BEGIN();
         FUNCTION_TEST_PARAM(ENUM, optionId);
+        FUNCTION_TEST_PARAM(UINT, optionIdx);
     FUNCTION_TEST_END();
 
     ASSERT(cfgInited());
     ASSERT(optionId < CFG_OPTION_TOTAL);
+    ASSERT_DECLARE(const bool group = configLocal->option[optionId].group);
+    ASSERT_DECLARE(const unsigned int indexTotal = configLocal->optionGroup[configLocal->option[optionId].groupId].indexTotal);
path: src/config/config.c
user: login: pgguru
body: (Though at that point, you're just hiding that complexity somewhere else for me to complain about, so maybe this is already as isolated as we can hope for...)
created_at: 2025-05-08T17:02:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -639,6 +637,61 @@ testRun(void)
         strLstAddZ(argList, "repo-host");
         TEST_RESULT_VOID(testCfgLoad(argList), "help for restore command, repo-host option");
         TEST_RESULT_STR_Z(helpRender(helpData), optionHelp, "check text");
+
+        // -------------------------------------------------------------------------------------------------------------------------
+        TEST_TITLE("multiple default values");
+
+        #define HELP_OPTION_CHUNK                                                                                                  \
+            "%s - 'restore' command - 'repo-storage-upload-chunk-size' option help\n"                                              \
+            "\n"                                                                                                                   \
+            "Repository storage upload chunk size.\n"                                                                              \
+            "\n"                                                                                                                   \
+            "Object stores such as S3 allow files to be uploaded in chunks when the file is\n"                                     \
+            "too large to be stored in memory. Even if the file can be stored in memory, it\n"                                     \
+            "is more memory efficient to limit the amount of memory used for uploads.\n"                                           \
+            "\n"                                                                                                                   \
+            "A larger chunk size will generally lead to better performance because it will\n"                                      \
+            "minimize upload requests and allow more files to be uploaded in a single\n"                                           \
+            "request rather than in chunks. The disadvantage is that memory usage will be\n"                                       \
+            "higher and because the chunk buffer must be allocated per process, larger\n"                                          \
+            "process-max values will lead to more memory being consumed overall.\n"                                                \
+            "\n"                                                                                                                   \
+            "Default chunk sizes by repo type:\n"                                                                                  \
+            "\n"                                                                                                                   \
+            "* azure - 4MiB\n"                                                                                                     \
+            "* gcs - 4MiB\n"                                                                                                       \
+            "* s3 - 5MiB\n"                                                                                                        \
+            "\n"                                                                                                                   \
+            "Note that valid chunk sizes vary by storage type and by platform. For example,\n"                                     \
+            "AWS S3 has a minimum chunk size of 5MiB but S3 clones may accept lower values.\n"                                     \
+            "Terminology for chunk size varies by storage type, so when searching min/max\n"                                       \
+            "values use \"part size\" for AWS S3, \"chunk size\" for GCS, and \"block size\" for\n"                                \
+            "Azure. No attempt is made to validate configured chunk sizes so selecting an\n"                                       \
+            "invalid value will lead to errors from the storage service or undefined\n"                                            \
+            "behavior.\n"
+
+        optionHelp = zNewFmt(
+            HELP_OPTION_CHUNK
+            "\n"
+            "default:\n"
+            "  repo1: 5242880\n"
+            "  repo2: 4194304\n",
path: test/src/module/command/helpTest.c
user: login: pgguru
body: So this is the meat of what we're actually testing in this test; otherwise, the rest of it is just prepping the expected output test so it can do (more-or-less) a strcmp to verify that we get the exact help output for this command given the later option setup, yes?
created_at: 2025-05-08T17:06:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -410,21 +410,20 @@ cfgOptionIdxTotal(const ConfigOption optionId)
 
 /**********************************************************************************************************************************/
 FN_EXTERN const String *
-cfgOptionDefault(const ConfigOption optionId)
+cfgOptionIdxDefaultValue(const ConfigOption optionId, const unsigned int optionIdx)
 {
     FUNCTION_TEST_BEGIN();
         FUNCTION_TEST_PARAM(ENUM, optionId);
+        FUNCTION_TEST_PARAM(UINT, optionIdx);
     FUNCTION_TEST_END();
 
     ASSERT(cfgInited());
     ASSERT(optionId < CFG_OPTION_TOTAL);
+    ASSERT_DECLARE(const bool group = configLocal->option[optionId].group);
+    ASSERT_DECLARE(const unsigned int indexTotal = configLocal->optionGroup[configLocal->option[optionId].groupId].indexTotal);
path: src/config/config.c
user: login: dwsteele
body: Yeah, that was my thinking. I originally tried to create wrapper macros but there are subtle differences per function so they were impossible to maintain or understand.
created_at: 2025-05-08T19:26:04Z
author_association: MEMBER
diff_hunk: @@ -639,6 +637,61 @@ testRun(void)
         strLstAddZ(argList, "repo-host");
         TEST_RESULT_VOID(testCfgLoad(argList), "help for restore command, repo-host option");
         TEST_RESULT_STR_Z(helpRender(helpData), optionHelp, "check text");
+
+        // -------------------------------------------------------------------------------------------------------------------------
+        TEST_TITLE("multiple default values");
+
+        #define HELP_OPTION_CHUNK                                                                                                  \
+            "%s - 'restore' command - 'repo-storage-upload-chunk-size' option help\n"                                              \
+            "\n"                                                                                                                   \
+            "Repository storage upload chunk size.\n"                                                                              \
+            "\n"                                                                                                                   \
+            "Object stores such as S3 allow files to be uploaded in chunks when the file is\n"                                     \
+            "too large to be stored in memory. Even if the file can be stored in memory, it\n"                                     \
+            "is more memory efficient to limit the amount of memory used for uploads.\n"                                           \
+            "\n"                                                                                                                   \
+            "A larger chunk size will generally lead to better performance because it will\n"                                      \
+            "minimize upload requests and allow more files to be uploaded in a single\n"                                           \
+            "request rather than in chunks. The disadvantage is that memory usage will be\n"                                       \
+            "higher and because the chunk buffer must be allocated per process, larger\n"                                          \
+            "process-max values will lead to more memory being consumed overall.\n"                                                \
+            "\n"                                                                                                                   \
+            "Default chunk sizes by repo type:\n"                                                                                  \
+            "\n"                                                                                                                   \
+            "* azure - 4MiB\n"                                                                                                     \
+            "* gcs - 4MiB\n"                                                                                                       \
+            "* s3 - 5MiB\n"                                                                                                        \
+            "\n"                                                                                                                   \
+            "Note that valid chunk sizes vary by storage type and by platform. For example,\n"                                     \
+            "AWS S3 has a minimum chunk size of 5MiB but S3 clones may accept lower values.\n"                                     \
+            "Terminology for chunk size varies by storage type, so when searching min/max\n"                                       \
+            "values use \"part size\" for AWS S3, \"chunk size\" for GCS, and \"block size\" for\n"                                \
+            "Azure. No attempt is made to validate configured chunk sizes so selecting an\n"                                       \
+            "invalid value will lead to errors from the storage service or undefined\n"                                            \
+            "behavior.\n"
+
+        optionHelp = zNewFmt(
+            HELP_OPTION_CHUNK
+            "\n"
+            "default:\n"
+            "  repo1: 5242880\n"
+            "  repo2: 4194304\n",
path: test/src/module/command/helpTest.c
user: login: dwsteele
body: Correct.
created_at: 2025-05-08T19:26:14Z
author_association: MEMBER
diff_hunk: @@ -2672,17 +2672,23 @@ cfgParse(const Storage *const storage, const unsigned int argListSize, const cha
                     {
                         configOptionValue->source = parseOptionValue->source;
                     }
-                    // Else try to set a default
-                    else
+
+                    if ((!configOptionValue->set && !parseOptionValue->negate) || config->help)
                     {
                         // If the option has a default
                         if (cfgParseOptionalRule(&optionalRules, parseRuleOptionalTypeDefault, config->command, optionId))
                         {
-                            configOptionValue->set = true;
-                            configOptionValue->value = optionalRules.defaultValue;
+                            if (!configOptionValue->set)
+                            {
+                                configOptionValue->set = true;
+                                configOptionValue->value = optionalRules.defaultValue;
+                                configOptionValue->display = optionalRules.defaultRaw;
+                            }
+
+                            configOptionValue->defaultValue = optionalRules.defaultRaw;
                         }
                         // Else error if option is required and help was not requested
-                        else
+                        else if (!configOptionValue->set)
path: src/config/parse.c
user: login: dwsteele
body: Well, it works fine and gets full coverage, but I agree that it looks half baked. The idea was to gate this section of code and I think that should be done like 54f5771a64.
created_at: 2025-05-08T19:28:32Z
author_association: MEMBER
diff_hunk: @@ -2672,17 +2672,23 @@ cfgParse(const Storage *const storage, const unsigned int argListSize, const cha
                     {
                         configOptionValue->source = parseOptionValue->source;
                     }
-                    // Else try to set a default
-                    else
+
+                    if ((!configOptionValue->set && !parseOptionValue->negate) || config->help)
                     {
                         // If the option has a default
                         if (cfgParseOptionalRule(&optionalRules, parseRuleOptionalTypeDefault, config->command, optionId))
                         {
-                            configOptionValue->set = true;
-                            configOptionValue->value = optionalRules.defaultValue;
+                            if (!configOptionValue->set)
+                            {
+                                configOptionValue->set = true;
+                                configOptionValue->value = optionalRules.defaultValue;
+                                configOptionValue->display = optionalRules.defaultRaw;
+                            }
+
+                            configOptionValue->defaultValue = optionalRules.defaultRaw;
                         }
                         // Else error if option is required and help was not requested
-                        else
+                        else if (!configOptionValue->set)
path: src/config/parse.c
user: login: pgguru
body: No strong feelings on that; looks fine.

created_at: 2025-05-12T15:36:04Z
author_association: CONTRIBUTOR

PR comments: