Repository: ESMCI/cime

Issue:
Provide error if .cime/config contains invalid entry
My current `.cime/config` file is pretty sparse:

```
$ cat ~/.cime/config
[main]
CIME_MODEL=cesm
```

If I add a variable to this file, though, there is no indication of whether CIME recognizes the addition. For example, if I update to

```
[main]
CIME_MODEL=cesm
MAILTYPE=end
```

Then nothing will change when I run `create_newcase` because `MAILTYPE` is not a recognized variable (the valid variable has an underscore: `MAIL_TYPE`). It would be better if `create_newcase` / `create_test` / other python scripts that parse `config` report an error if an unknown setting is specified. Note that [manage_externals](https://github.com/ESMCI/manage_externals) does this:

```
$ head -n 7 Externals.cfg
[cime]
tag = cime_cesm2_0_rel_02
protocol = git
repo_url = https://github.com/ESMCI/cime
local_path = cime
required = True
foo = bar
$ ./manage_externals/checkout_externals
Processing externals description file : Externals.cfg
ERROR:root:Invalid input: "cime" contains unknown item "foo".

ERROR: Invalid input: "cime" contains unknown item "foo".
```


Discussion:

Resolving patch:
diff --git a/scripts/lib/CIME/XML/machines.py b/scripts/lib/CIME/XML/machines.py
--- a/scripts/lib/CIME/XML/machines.py
+++ b/scripts/lib/CIME/XML/machines.py
@@ -4,7 +4,7 @@
 from CIME.XML.standard_module_setup import *
 from CIME.XML.generic_xml import GenericXML
 from CIME.XML.files import Files
-from CIME.utils import convert_to_unknown_type
+from CIME.utils import convert_to_unknown_type, get_cime_config
 
 import socket
 
@@ -46,7 +46,11 @@ def __init__(self, infile=None, files=None, machine=None):
             if "CIME_MACHINE" in os.environ:
                 machine = os.environ["CIME_MACHINE"]
             else:
-                machine = self.probe_machine_name()
+                cime_config = get_cime_config()
+                if cime_config.has_option("main", "machine"):
+                    machine = cime_config.get("main", "machine")
+                if machine is None:
+                    machine = self.probe_machine_name()
 
         expect(machine is not None, "Could not initialize machine object from {} or {}".format(infile, local_infile))
         self.set_machine(machine)
@@ -229,7 +233,13 @@ def get_default_compiler(self):
         """
         Get the compiler to use from the list of COMPILERS
         """
-        return self.get_field_from_list("COMPILERS")
+        cime_config = get_cime_config()
+        if cime_config.has_option('main','COMPILER'):
+            value = cime_config.get('main', 'COMPILER')
+            expect(self.is_valid_compiler(value), "User-selected compiler {} is not supported on machine {}".format(value, self.machine))
+        else:
+            value = self.get_field_from_list("COMPILERS")
+        return value
 
     def get_default_MPIlib(self, attributes=None):
         """
diff --git a/scripts/lib/CIME/utils.py b/scripts/lib/CIME/utils.py
--- a/scripts/lib/CIME/utils.py
+++ b/scripts/lib/CIME/utils.py
@@ -174,12 +174,30 @@ def _read_cime_config_file():
     CIME_MODEL=e3sm,cesm
     PROJECT=someprojectnumber
     """
+    allowed_sections = ("main", "create_test")
+
+    allowed_in_main = ("cime_model", "project", "charge_account", "srcroot", "mail_type",
+                       "mail_user", "machine", "mpilib", "compiler", "input_dir")
+    allowed_in_create_test = ("mail_type", "mail_user", "save_timing", "single_submit",
+                              "test_root", "output_root", "baseline_root", "clean",
+                              "machine", "mpilib", "compiler", "parallel_jobs", "proc_pool",
+                              "walltime", "job_queue", "allow_baseline_overwrite", "wait",
+                              "force_procs", "force_threads", "input_dir", "pesfile", "retry",
+                              "walltime")
 
     cime_config_file = os.path.abspath(os.path.join(os.path.expanduser("~"),
                                                   ".cime","config"))
     cime_config = configparser.SafeConfigParser()
     if(os.path.isfile(cime_config_file)):
         cime_config.read(cime_config_file)
+        for section in cime_config.sections():
+            expect(section in allowed_sections,"Unknown section {} in .cime/config\nallowed sections are {}".format(section, allowed_sections))
+        if cime_config.has_section('main'):
+            for item,_ in cime_config.items('main'):
+                expect(item in allowed_in_main,"Unknown option in config section \"main\": \"{}\"\nallowed options are {}".format(item, allowed_in_main))
+        if cime_config.has_section('create_test'):
+            for item,_ in cime_config.items('create_test'):
+                expect(item in allowed_in_create_test,"Unknown option in config section \"test\": \"{}\"\nallowed options are {}".format(item, allowed_in_create_test))
     else:
         logger.debug("File {} not found".format(cime_config_file))
         cime_config.add_section('main')
@@ -231,6 +249,8 @@ def set_model(model):
     Set the model to be used in this session
     """
     cime_config = get_cime_config()
+    if not cime_config.has_section('main'):
+        cime_config.add_section('main')
     cime_config.set('main','CIME_MODEL',model)
 
 def get_model():
@@ -257,7 +277,11 @@ def get_model():
 
     # One last try
     if (model is None):
-        srcroot = os.path.dirname(os.path.abspath(get_cime_root()))
+        srcroot = None
+        if cime_config.has_section('main') and cime_config.has_option('main', 'SRCROOT'):
+            srcroot = cime_config.get('main','SRCROOT')
+        if srcroot is None:
+            srcroot = os.path.dirname(os.path.abspath(get_cime_root()))
         if os.path.isfile(os.path.join(srcroot, "SVN_EXTERNAL_DIRECTORIES")) \
            or os.path.isdir(os.path.join(srcroot, "manage_externals")):
             model = 'cesm'
@@ -995,7 +1019,8 @@ def parse_args_and_handle_standard_logging_options(args, parser=None):
 
     # scripts_regression_tests is the only thing that should pass a None argument in parser
     if parser is not None:
-        _check_for_invalid_args(args[1:])
+        if "--help" not in args[1:]:
+            _check_for_invalid_args(args[1:])
         args = parser.parse_args(args[1:])
 
     # --verbose adds to the message format but does not impact the log level


Test patch:
diff --git a/scripts/create_test b/scripts/create_test
--- a/scripts/create_test
+++ b/scripts/create_test
@@ -41,11 +41,11 @@ logger = logging.getLogger(__name__)
 def parse_command_line(args, description):
 ###############################################################################
 
-    model = CIME.utils.get_model()
-
     parser = argparse.ArgumentParser(description=description,
                                      formatter_class=RawTextHelpFormatter)
 
+    model = CIME.utils.get_model()
+
     CIME.utils.setup_standard_logging_options(parser)
 
     config = get_cime_config()
diff --git a/scripts/tests/scripts_regression_tests.py b/scripts/tests/scripts_regression_tests.py
--- a/scripts/tests/scripts_regression_tests.py
+++ b/scripts/tests/scripts_regression_tests.py
@@ -15,9 +15,10 @@
 sys.path.append(LIB_DIR)
 # Remove all pyc files to ensure we're testing the right things
 import subprocess, argparse
-subprocess.call('/bin/rm $(find . -name "*.pyc")', shell=True, cwd=LIB_DIR)
-from six import assertRaisesRegex
+subprocess.call('/bin/rm -f $(find . -name "*.pyc")', shell=True, cwd=LIB_DIR)
 import six
+from six import assertRaisesRegex
+
 
 import collections