def decode_cf_variables(
    variables: T_Variables,
    attributes: T_Attrs,
    concat_characters: bool | Mapping[str, bool] = True,
    mask_and_scale: bool | Mapping[str, bool] = True,
    decode_times: bool | CFDatetimeCoder | Mapping[str, bool | CFDatetimeCoder] = True,
    decode_coords: bool | Literal["coordinates", "all"] = True,
    drop_variables: T_DropVariables = None,
    use_cftime: bool | Mapping[str, bool] | None = None,
    decode_timedelta: bool
    | CFTimedeltaCoder
    | Mapping[str, bool | CFTimedeltaCoder]
    | None = None,
) -> tuple[T_Variables, T_Attrs, set[Hashable]]:
    """
    Decode several CF encoded variables.

    See: decode_cf_variable
    """
    # Only emit one instance of the decode_timedelta default change
    # FutureWarning. This can be removed once this change is made.
    warnings.filterwarnings("once", "decode_timedelta", FutureWarning)

    dimensions_used_by = defaultdict(list)
    for v in variables.values():
        for d in v.dims:
            dimensions_used_by[d].append(v)

    def stackable(dim: Hashable) -> bool:
        # figure out if a dimension can be concatenated over
        if dim in variables:
            return False
        for v in dimensions_used_by[dim]:
            if v.dtype.kind != "S" or dim != v.dims[-1]:
                return False
        return True

    coord_names = set()

    if isinstance(drop_variables, str):
        drop_variables = [drop_variables]
    elif drop_variables is None:
        drop_variables = []
    drop_variables = set(drop_variables)

    # Time bounds coordinates might miss the decoding attributes
    if decode_times:
        _update_bounds_attributes(variables)

    new_vars = {}
    for k, v in variables.items():
        if k in drop_variables:
            continue
        stack_char_dim = (
            _item_or_default(concat_characters, k, True)
            and v.dtype == "S1"
            and v.ndim > 0
            and stackable(v.dims[-1])
        )
        try:
            new_vars[k] = decode_cf_variable(
                k,
                v,
                concat_characters=_item_or_default(concat_characters, k, True),
                mask_and_scale=_item_or_default(mask_and_scale, k, True),
                decode_times=_item_or_default(decode_times, k, True),
                stack_char_dim=stack_char_dim,
                use_cftime=_item_or_default(use_cftime, k, None),
                decode_timedelta=_item_or_default(decode_timedelta, k, None),
            )
        except Exception as e:
            raise type(e)(f"Failed to decode variable {k!r}: {e}") from e
        if decode_coords in [True, "coordinates", "all"]:
            var_attrs = new_vars[k].attrs
            if "coordinates" in var_attrs:
                var_coord_names = [
                    c for c in var_attrs["coordinates"].split() if c in variables
                ]
                # propagate as is
                new_vars[k].encoding["coordinates"] = var_attrs["coordinates"]
                del var_attrs["coordinates"]
                # but only use as coordinate if existing
                if var_coord_names:
                    coord_names.update(var_coord_names)

        if decode_coords == "all":
            for attr_name in CF_RELATED_DATA:
                if attr_name in var_attrs:
                    # fixes stray colon
                    attr_val = var_attrs[attr_name].replace(" :", ":")
                    var_names = attr_val.split()
                    # if grid_mapping is a single string, do not enter here
                    if (
                        attr_name in CF_RELATED_DATA_NEEDS_PARSING
                        and len(var_names) > 1
                    ):
                        # map the keys to list of strings
                        # "A: b c d E: f g" returns
                        # {"A": ["b", "c", "d"], "E": ["f", "g"]}
                        roles_and_names = defaultdict(list)
                        key = None
                        for vname in var_names:
                            if ":" in vname:
                                key = vname.strip(":")
                            else:
                                if key is None:
                                    raise ValueError(
                                        f"First element {vname!r} of [{attr_val!r}] misses ':', "
                                        f"cannot decode {attr_name!r}."
                                    )
                                roles_and_names[key].append(vname)
                        # for grid_mapping keys are var_names
                        if attr_name == "grid_mapping":
                            var_names = list(roles_and_names.keys())
                        else:
                            # for cell_measures and formula_terms values are var names
                            var_names = list(itertools.chain(*roles_and_names.values()))
                            # consistency check (one element per key)
                            if len(var_names) != len(roles_and_names.keys()):
                                emit_user_level_warning(
                                    f"Attribute {attr_name!r} has malformed content [{attr_val!r}], "
                                    f"decoding {var_names!r} to coordinates."
                                )
                    if all(var_name in variables for var_name in var_names):
                        new_vars[k].encoding[attr_name] = attr_val
                        coord_names.update(var_names)
                    else:
                        referenced_vars_not_in_variables = [
                            proj_name
                            for proj_name in var_names
                            if proj_name not in variables
                        ]
                        emit_user_level_warning(
                            f"Variable(s) referenced in {attr_name} not in variables: {referenced_vars_not_in_variables}",
                        )
                    del var_attrs[attr_name]

    if decode_coords and isinstance(attributes.get("coordinates", None), str):
        attributes = dict(attributes)
        crds = attributes.pop("coordinates")
        coord_names.update(crds.split())

    return new_vars, attributes, coord_names

    Decode several CF encoded variables.

    See: decode_cf_variable
    
xarray/conventions.py