From 135f303051e9b75dde7c32f56c5b428b8a630b40 Mon Sep 17 00:00:00 2001 From: ReinerBRO <593493640@qq.com> Date: Thu, 26 Mar 2026 14:31:58 +0800 Subject: [PATCH] Fix environment variable reuse in config parsing --- supervisor/datatypes.py | 23 ++++++++++++++++------- supervisor/options.py | 24 +++++++++++++++--------- supervisor/tests/test_options.py | 26 ++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 16 deletions(-) diff --git a/supervisor/datatypes.py b/supervisor/datatypes.py index 7fa0dc2fe..5a5850b55 100644 --- a/supervisor/datatypes.py +++ b/supervisor/datatypes.py @@ -65,17 +65,19 @@ def list_of_exitcodes(arg): except: raise ValueError("not a valid list of exit codes: " + repr(arg)) -def dict_of_key_value_pairs(arg): - """ parse KEY=val,KEY2=val2 into {'KEY':'val', 'KEY2':'val2'} - Quotes can be used to allow commas in the value +def list_of_key_value_pairs(arg): + """Parse KEY=val,KEY2=val2 into a list of (KEY, val) tuples. + + Quotes can be used to allow commas in the value. The original order is + preserved so callers can expand values incrementally. """ lexer = shlex.shlex(str(arg), posix=shlex_posix_works) - lexer.wordchars += '/.+-():' + lexer.wordchars += '/.+-():%' tokens = list(lexer) tokens_len = len(tokens) - D = {} + pairs = [] i = 0 while i < tokens_len: k_eq_v = tokens[i:i+3] @@ -87,9 +89,16 @@ def dict_of_key_value_pairs(arg): if not shlex_posix_works: v = v.strip('\'"') - D[k] = v + pairs.append((k, v)) i += 4 - return D + return pairs + + +def dict_of_key_value_pairs(arg): + """ parse KEY=val,KEY2=val2 into {'KEY':'val', 'KEY2':'val2'} + Quotes can be used to allow commas in the value + """ + return dict(list_of_key_value_pairs(arg)) class Automatic: pass diff --git a/supervisor/options.py b/supervisor/options.py index 897d07876..c50f26766 100644 --- a/supervisor/options.py +++ b/supervisor/options.py @@ -34,7 +34,7 @@ from supervisor.datatypes import byte_size from supervisor.datatypes import signal_number from supervisor.datatypes import list_of_exitcodes -from supervisor.datatypes import dict_of_key_value_pairs +from supervisor.datatypes import list_of_key_value_pairs from supervisor.datatypes import logfile_name from supervisor.datatypes import list_of_strings from supervisor.datatypes import octal_type @@ -647,8 +647,8 @@ def get(opt, default, **kwargs): section.strip_ansi = boolean(get('strip_ansi', 'false')) environ_str = get('environment', '', do_expand=False) - environ_str = expand(environ_str, expansions, 'environment') - section.environment = dict_of_key_value_pairs(environ_str) + section.environment = expand_key_value_pairs( + environ_str, expansions, 'environment') # extend expansions for global from [supervisord] environment definition for k, v in section.environment.items(): @@ -958,12 +958,8 @@ def get(section, opt, *args, **kwargs): expansions.update({'process_num': process_num, 'numprocs': numprocs}) expansions.update(self.environ_expansions) - environment = dict_of_key_value_pairs( - expand(environment_str, expansions, 'environment')) - - # extend expansions for process from [program:x] environment definition - for k, v in environment.items(): - expansions['ENV_%s' % k] = v + environment = expand_key_value_pairs( + environment_str, expansions, 'environment') directory = get(section, 'directory', None) @@ -2213,6 +2209,16 @@ def expand(s, expansions, name): (s, name, str(ex)) ) + +def expand_key_value_pairs(arg, expansions, name): + """Parse and expand KEY=val pairs in order.""" + pairs = [] + for k, v in list_of_key_value_pairs(arg): + v = expand(v, expansions, name) + pairs.append((k, v)) + expansions['ENV_%s' % k] = v + return dict(pairs) + def make_namespec(group_name, process_name): # we want to refer to the process by its "short name" (a process named # process1 in the group process1 has a name "process1"). This is for diff --git a/supervisor/tests/test_options.py b/supervisor/tests/test_options.py index d27cb0c6b..c23316883 100644 --- a/supervisor/tests/test_options.py +++ b/supervisor/tests/test_options.py @@ -1796,6 +1796,20 @@ def test_processes_from_section_expands_env_in_environment(self): expected = "/foo/bar:%s" % os.environ['PATH'] self.assertEqual(pconfigs[0].environment['PATH'], expected) + def test_processes_from_section_environment_can_reuse_earlier_vars(self): + instance = self._makeOne() + text = lstrip("""\ + [program:foo] + command = /bin/foo --value=%(ENV_B)s + environment = A="1",B="%(ENV_A)s" + """) + from supervisor.options import UnhosedConfigParser + config = UnhosedConfigParser() + config.read_string(text) + pconfigs = instance.processes_from_section(config, 'program:foo', 'bar') + self.assertEqual(pconfigs[0].environment, {'A': '1', 'B': '1'}) + self.assertEqual(pconfigs[0].command, '/bin/foo --value=1') + def test_processes_from_section_redirect_stderr_with_filename(self): instance = self._makeOne() text = lstrip("""\ @@ -3414,6 +3428,18 @@ def test_options_environment_of_supervisord_with_escaped_chars(self): options = instance.configroot.supervisord self.assertEqual(options.environment, dict(VAR_WITH_P="some_value_%_end")) + def test_options_environment_of_supervisord_can_reuse_earlier_vars(self): + text = lstrip(""" + [supervisord] + environment=A="1",B="%(ENV_A)s" + """) + + instance = self._makeOne() + instance.configfile = StringIO(text) + instance.realize(args=[]) + options = instance.configroot.supervisord + self.assertEqual(options.environment, dict(A="1", B="1")) + class ProcessConfigTests(unittest.TestCase): def _getTargetClass(self):