Repository: ESMCI/cime

Issue:
E3SM replay.sh functionality: Missing quotes for xmlchange invocations
I noticed that replay.sh does not capture quotes around arguments for xmlchange invocations.

For instance,
although the case was created and modified using
```./xmlchange --append --id CAM_CONFIG_OPTS --val  " -nlev 276 "```
replay.sh has
```./xmlchange --append --id CAM_CONFIG_OPTS --val  -nlev 276```

It causes the following problem when you try running replay.sh:
```
$ ./xmlchange --append --id CAM_CONFIG_OPTS --val  -nlev 276
WARNING: The -nlev argument is deprecated. Multi-character arguments should begin with "--" and single character with "-"
  Use --help for a complete list of available options
usage: xmlchange [-h] [-d] [-v] [-s] [--caseroot CASEROOT] [--append] [--subgroup SUBGROUP] [--id ID] [--val VAL] [--file FILE] [--delimiter DELIMITER] [--dryrun] [--noecho] [-f] [-N] [-loglevel LOGLEVEL] [listofsettings]
xmlchange: error: argument --val/-val: expected one argument
$ ./xmlchange --append --id CAM_CONFIG_OPTS --val " -nlev 276 "
For your changes to take effect, run:
./case.build --clean-all
./case.build
```


Discussion:
cc @jasonb5 
i thought it was only scream behavior, apparently not https://github.com/E3SM-Project/scream/issues/1856
@sarats I'm looking into this.

@oksanaguba That definitely looks related to this.

Resolving patch:
diff --git a/CIME/case/case.py b/CIME/case/case.py
--- a/CIME/case/case.py
+++ b/CIME/case/case.py
@@ -2255,7 +2255,7 @@ def record_cmd(self, cmd=None, init=False):
         cimeroot = self.get_value("CIMEROOT")
 
         if cmd is None:
-            cmd = list(sys.argv)
+            cmd = self.fix_sys_argv_quotes(list(sys.argv))
 
         if init:
             ctime = time.strftime("%Y-%m-%d %H:%M:%S")
@@ -2294,6 +2294,37 @@ def record_cmd(self, cmd=None, init=False):
         except PermissionError:
             logger.warning("Could not write to 'replay.sh' script")
 
+    def fix_sys_argv_quotes(self, cmd):
+        """Fixes removed quotes from argument list.
+
+        Restores quotes to `--val` and `KEY=VALUE` from sys.argv.
+        """
+        # handle fixing quotes
+        # case 1: "--val", " -nlev 276 "
+        # case 2: "-val" , " -nlev 276 "
+        # case 3: CAM_CONFIG_OPTS=" -nlev 276 "
+        for i, item in enumerate(cmd):
+            if re.match("[-]{1,2}val", item) is not None:
+                if i + 1 >= len(cmd):
+                    continue
+
+                # only quote if value contains spaces
+                if " " in cmd[i + 1]:
+                    cmd[i + 1] = f'"{cmd[i + 1]}"'
+            else:
+                m = re.search("([^=]*)=(.*)", item)
+
+                if m is None:
+                    continue
+
+                g = m.groups()
+
+                # only quote if value contains spaces
+                if " " in g[1]:
+                    cmd[i] = f'{g[0]}="{g[1]}"'
+
+        return cmd
+
     def create(
         self,
         casename,


Test patch:
diff --git a/CIME/tests/test_unit_case.py b/CIME/tests/test_unit_case.py
--- a/CIME/tests/test_unit_case.py
+++ b/CIME/tests/test_unit_case.py
@@ -84,6 +84,75 @@ def setUp(self):
         self.srcroot = os.path.abspath(cime_utils.get_src_root())
         self.tempdir = tempfile.TemporaryDirectory()
 
+    @mock.patch("CIME.case.case.Case.read_xml")
+    def test_fix_sys_argv_quotes(self, read_xml):
+        input_data = ["./xmlquery", "--val", "PIO"]
+        expected_data = ["./xmlquery", "--val", "PIO"]
+
+        with tempfile.TemporaryDirectory() as tempdir:
+            make_valid_case(tempdir)
+
+            with Case(tempdir) as case:
+                output_data = case.fix_sys_argv_quotes(input_data)
+
+        assert output_data == expected_data
+
+    @mock.patch("CIME.case.case.Case.read_xml")
+    def test_fix_sys_argv_quotes_incomplete(self, read_xml):
+        input_data = ["./xmlquery", "--val"]
+        expected_data = ["./xmlquery", "--val"]
+
+        with tempfile.TemporaryDirectory() as tempdir:
+            make_valid_case(tempdir)
+
+            with Case(tempdir) as case:
+                output_data = case.fix_sys_argv_quotes(input_data)
+
+        assert output_data == expected_data
+
+    @mock.patch("CIME.case.case.Case.read_xml")
+    def test_fix_sys_argv_quotes_val(self, read_xml):
+        input_data = ["./xmlquery", "--val", "-test"]
+        expected_data = ["./xmlquery", "--val", "-test"]
+
+        with tempfile.TemporaryDirectory() as tempdir:
+            make_valid_case(tempdir)
+
+            with Case(tempdir) as case:
+                output_data = case.fix_sys_argv_quotes(input_data)
+
+        assert output_data == expected_data
+
+    @mock.patch("CIME.case.case.Case.read_xml")
+    def test_fix_sys_argv_quotes_val_quoted(self, read_xml):
+        input_data = ["./xmlquery", "--val", " -nlev 267 "]
+        expected_data = ["./xmlquery", "--val", '" -nlev 267 "']
+
+        with tempfile.TemporaryDirectory() as tempdir:
+            make_valid_case(tempdir)
+
+            with Case(tempdir) as case:
+                output_data = case.fix_sys_argv_quotes(input_data)
+
+        assert output_data == expected_data
+
+    @mock.patch("CIME.case.case.Case.read_xml")
+    def test_fix_sys_argv_quotes_kv(self, read_xml):
+        input_data = ["./xmlquery", "CAM_CONFIG_OPTS= -nlev 267", "OTHER_OPTS=-test"]
+        expected_data = [
+            "./xmlquery",
+            'CAM_CONFIG_OPTS=" -nlev 267"',
+            "OTHER_OPTS=-test",
+        ]
+
+        with tempfile.TemporaryDirectory() as tempdir:
+            make_valid_case(tempdir)
+
+            with Case(tempdir) as case:
+                output_data = case.fix_sys_argv_quotes(input_data)
+
+        assert output_data == expected_data
+
     @mock.patch("CIME.case.case.Case.read_xml")
     @mock.patch("sys.argv", ["/src/create_newcase", "--machine", "docker"])
     @mock.patch("time.strftime", return_value="00:00:00")