code: def process_substitutions(mapping, substitutions):
      
      def parse_val(v):
          # TODO: do we need to recognize numbers,lists,dicts?
          if v.lower() == 'true':
              return True
          elif v.lower() == 'false':
              return False
          elif v.lower() == 'none':
              return None
          return v
  
      if isinstance(substitutions, list):
          # if we got a list, get a {key: val} dict out of it
          substitutions = dict([s.split('=', 1) for s in substitutions])
  
      for key, val in substitutions.items():
          cur_dict = mapping
          key_parts = key.split('.')
          if key_parts[0].endswith('_plugins'):
              _process_plugin_substitution(mapping, key_parts, val)
          else:
              key_parts_without_last = key_parts[:-1]
  
              # now go down mapping, following the dotted path; create empty dicts on way
              for k in key_parts_without_last:
                  if k in cur_dict:
                      if not isinstance(cur_dict[k], dict):
                          cur_dict[k] = {}
                  else:
                      cur_dict[k] = {}
                  cur_dict = cur_dict[k]
              cur_dict[key_parts[-1]] = parse_val(val)
docs: Process `substitutions` for given `mapping` (modified in place)
  
      :param mapping: a dict
      :param substitutions: either a dict {key: value} or a list of ["key=value"] strings
          keys can use dotted notation to change to nested dicts
  
      Note: Plugin substitutions are processed differently - they are accepted in form of
          plugin_type.plugin_name.arg_name, even though that doesn't reflect the actual
          structure of given mapping.
      Also note: For non-plugin substitutions, additional dicts/key/value pairs
          are created on the way if they're missing. For plugin substitutions, only
          existing values can be changed (TODO: do we want to change this behaviour?).