Repository: iterative/dvc

Issue:
Nested Looping in Parameterization
This feature request is in regards to the experimental parameterization feature here: https://github.com/iterative/dvc/wiki/Parametrization

Currently if in `params.yaml` if one has

```yaml
  models:
    a:
      features: 1
    a:
      features: 2
    b:
      features: 1
    b:
      features: 2
```

we can have `dvc.yaml` as so

```yaml
stages:
  build:
    foreach: ${models}
    do:
      cmd: >- 
          python script.py
          --model ${item}
          --features ${item.features}
```

Currently dvc.yaml can only loop over one thing e.g models in this case. I am wondering if something like this would be possible:

```yaml
# params.yaml

models: 
  - a
  - b
features: 
  - 1
  - 2
```
and
```yaml
# dvc.yaml

stages:
  build:
    foreach: ${models}
    foreach: ${features}
    do:
      cmd: >- 
          python script.py
          --model ${item_0}
          --features ${item_1}
```

This would be great as one wouldn't have to specify all possible configurations in params.yaml. 
@skshetry tagging you as I believe you have been working on parameterization.




Comments:
@bharathc346, we cannot have two `foreach` as it's not supported in YAML format.
We could support nested `foreach` as following:
```yaml

stages:
  build:
    foreach: ${models}
    do:
      foreach: ${features}
      do:
        cmd: >- 
            python script.py
            --model ${item_0}
            --features ${item_1}
```

But, as you can see, this requires too much indentation. Though, this is what we have in mind right now.

@johnnychen94 suggested introducing something similar to Github's `matrix` or `travis`'s `env` in https://github.com/iterative/dvc/issues/3633#issuecomment-736508018. But, we are not sure if `matrix` is more intuitive to our users than the loops. With matrix, it'd look something like following:
```yaml

stages:
  build:
    matrix:
      - ${models}
      - ${features}
    do:
      cmd: >- 
          python script.py
          --model ${item_0}
          --features ${item_1}
```

Regarding `item_0`/`item_1` naming, I think it might be better to do similar to what `Ansible` does.
So, `item_0` will be accessible as `item[0]` and `item_1` as `item[1]`. `item` will just be a list. Similarly, `key[0]`, `key[1]`, etc. This might make it confusing with `item` and `key` as they are now a list, but I don't have any good ideas for now.

Anyway, @bharathc346 thanks for creating the issue. Let's hear some feedback for 2.0 change and given enough interests, we'll come to it. Thanks.
I see. I like @johnnychen94 comment about introducing the github matrix. But I think any solution for nested interpolation would be great. Thanks for the detailed comment
@bharathc346, ahh, looks like I used the wrong term. It should be nested `foreach`. 
Upvoted, that definitely something that would be nice to have.

Intuitively I was doing something like that:
```yaml
stages:
  build:
    foreach:
      - fr:
        - a
        - b
      - en:
        - a
        - b
    do:
    ...
```

However, I feel the `matrix` is more readable. But I'd be ok with a nested `foreach` too.
I am definitely interested in such a feature. If I were to vote on the naming convention, I would go for `matrix` as `foreach` means to me some sort of a parallelizable loop (for - each).

For the keys and items, I would stick with what is well done with the current foreach loop, meaning:

 - if a list is provided, then the var is `item` and since there is a matrix, `item` is a list:

```yaml
stages:
  build:
    matrix:
      - ["us", "fr", "gb"]
      - [128, 256, 512]
    do:
      cmd: >-
          python script.py
          --model ${item[0]}
          --features ${item[1]}
```

 - if a dict is provided, `item` is a dict to access the values by their names (this second option improves readability and I could even vote to enforce it)

```yaml
stages:
  build:
    matrix:
      model: ["us", "fr", "gb"]
      features: [128, 256, 512]
    do:
      cmd: >-
          python script.py
          --model ${item.model}
          --features ${item.features}
```
Hi, any updates on this feature? I would be really interested in this
I am glad to work on it as well if we have a go from the dvc team 
The matrix loop is a very trivial case in that it introduces a dense parameter grid. There will always be a need to run a "filter" on this grid to reduce unnecessary tasks.

It would be nice if DVC defines an interchangeable protocol for multiple parameterization tasks so that people can write their own task generation script when needed. The "params.yaml" example I provide in https://github.com/iterative/dvc/issues/3633#issuecomment-736508018 is a very naive version of the interchangeable protocol. It would be very trivial work to dump "matrix" definition into something like this, as long as the protocol is well-defined.

https://github.com/iterative/dvc/issues/5795 should be also fixed to enable wide usage of this requested feature.
### Overlap with dvc exp run 

There is some overlap between this kind of parametrized loop and `dvc exp run --set-param`. The current `foreach` approach creates many separate stages, while `dvc exp run` creates many variations of the same stage. @johnnychen94 mentioned concurrent execution in another thread, which `dvc exp run` is already starting to tackle. If it were possible to programmatically define the set of experiments to run with `dvc exp run`, would people still need `matrix`?

### Nested loop syntax

There seems to be consensus that the `matrix` syntax makes sense. Personally, I agree with @ClementWalter that enforcing a dict structure is most readable.

Adding `matrix` might make it challenging to explain the difference between `foreach` and `matrix`. This also relates generally to how to balance complexity and flexibility in defining DAGs (see https://github.com/iterative/dvc/discussions/5646#discussioncomment-874316).
@dberenbaum, For myself, the `foreach` is a useful functionality when I'm doing some text classifications. In this work, two billion items need to be classified into about 5000 child categories inside 50 parent categories. The project has two levels, in the first one I trained a model to classify items into parent categories, and in the second I use `foreach` to train a separate model for the classification of the child categories for each parent category. It could not be replaced by `exp` or other functionalities.

So, If we are facing a project with an even more level of structure, then a nested loop might be an unavoidable solution.
@dberenbaum I think `matrix` or nested `foreach` still make sense, even with more dynamic parameter definitions in `dvc exp`. We use "foreach stages" to define different models (similar model but for different countries and products for example), while using dvc experiments for trying out different parameters for those models. There is a logical difference, and it would not be optimal to start mixing those concepts.

And of course upvoting this feature request 👍🏻 
Here is another upvote.

Either `matrix` or nested `foreach` would help me align the structure of the pipeline to the natural structure of the experiment.

Although I am fairly new to DVC, I keep running into limitations. I wonder if DVC might get more mileage by leveraging existing workflow systems (such as Snakemake and NextFlow) and focusing on what DVC does best, which is managing data versions. I guess I could trigger a whole Snakemake workflow as one stage of a DVC pipeline, and then specify which parameters, dependencies and outputs I want to track with DVC. I'm just wondering out loud if it might be more efficient to better integrate DVC with other workflow systems (such as automatically detecting parameters, dependencies and outputs) rather than focus on making DVC pipelines more flexible.
This is my most wanted feature. Just adding my perspective, as I have a collection of models I want to run on a collection of datasets. If I could nest the `foreach`, my config size would literally lose an order of magnitude in terms of line count.

As for whether we should use `matrix` instead, I think although it's visually cleaner it would have to be documented as a separate feature, which feels bloated. Whereas uses nested `foreach`, although less visually appealing, is a more intuitive extension of the concept (i.e. the first thing I tried after ready about DVC's looping). There would be no way someone would naturally change their `foreach` to `matrix`.
Just to comment here, that the "nested loop" is just "one way", of tuning the hyper parameters automatically.
There are others (random search or sobol sequences) and ideally DVC should allow each of them.

Allowing "multiple param files" in one go, might be a middle ground. (#7891 )

Chiming in with support for this feature! For context, see [this trickery](https://github.com/iterative/dvc/issues/7093#issuecomment-1581417045) I'm currently doing with YAML anchors to serve this same purpose, and which is at risk of being broken after 3.0 release.

I think it's definitely possible that some version of `matrix` could serve my needs (what I'm trying to do is really not that complicated), thought it would depend on the details of the implementation. For example, would each "coordinate" in the constructed matrix need to be a simple scalar, or could one construct a matrix from e.g.

```yaml
matrix:
  - [foo, bar]
  - - feature: a
      type: json
    - feature: b
      type: npz
```

I use maps like that a lot, so that feels pretty important.

As an aside, when combined with hydra, this could possibly allow for "toggling" certain pipeline stages. In the example below, you could set `is_enabled` to an empty list to disable that stage—assuming that `matrix` doesn't require non-empty lists—or a list of a single value to turn it on. I actually do this currently with `foreach` and empty lists/maps.
```yaml
stage_name:
  matrix:
    - ${features}
    - ${is_enabled}
  do:
    ...
```
> ```yaml
> matrix:
>   - [foo, bar]
>   - - feature: a
>       type: json
>     - feature: b
>       type: npz
> ```

Would you mind continuing that example with the `do:` part? It's not entirely clear to me how it would look. I'd prefer to use the convention suggested by @ClementWalter above where each of those sections has a key to identify it, so maybe it would look more like:

```yaml
matrix:
  - name: [foo, bar]
  - config: 
    - feature: a
      type: json
    - feature: b
      type: npz
```

I _think_ that will also make it easier to reference those items in the `do:` section, but that's where I'd like to know how you think that would look.
> Would you mind continuing that example with the `do:` part?

I like the explicit naming, though you then don't need a list at the top level:
```yaml
stage_foo:
  matrix:
    name: [foo, bar]
    config:
    - feature: a
      type: json
    - feature: b
      type: npz
  do:
    cmd: echo ${item.name} ${item.config.feature}
```
@skshetry Do you think this could eventually replace `foreach` altogether, or do you think we would need to keep both?

Patch:
diff --git a/dvc/parsing/__init__.py b/dvc/parsing/__init__.py
--- a/dvc/parsing/__init__.py
+++ b/dvc/parsing/__init__.py
@@ -2,6 +2,7 @@
 import os
 from collections.abc import Mapping, Sequence
 from copy import deepcopy
+from itertools import product
 from typing import (
     TYPE_CHECKING,
     Any,
@@ -51,6 +52,7 @@
 WDIR_KWD = "wdir"
 PARAMS_KWD = "params"
 FOREACH_KWD = "foreach"
+MATRIX_KWD = "matrix"
 DO_KWD = "do"
 
 DEFAULT_PARAMS_FILE = "params.yaml"
@@ -105,7 +107,7 @@ def is_map_or_seq(data: Any) -> bool:
     return not isinstance(data, str) and _is_map_or_seq(data)
 
 
-def split_foreach_name(name: str) -> Tuple[str, Optional[str]]:
+def split_group_name(name: str) -> Tuple[str, Optional[str]]:
     group, *keys = name.rsplit(JOIN, maxsplit=1)
     return group, first(keys)
 
@@ -120,13 +122,15 @@ def func(s: "DictStrAny") -> None:
     return recurse(func)(data)
 
 
-Definition = Union["ForeachDefinition", "EntryDefinition"]
+Definition = Union["ForeachDefinition", "EntryDefinition", "MatrixDefinition"]
 
 
 def make_definition(
     resolver: "DataResolver", name: str, definition: "DictStrAny", **kwargs
 ) -> Definition:
     args = resolver, resolver.context, name, definition
+    if MATRIX_KWD in definition:
+        return MatrixDefinition(*args, **kwargs)
     if FOREACH_KWD in definition:
         return ForeachDefinition(*args, **kwargs)
     return EntryDefinition(*args, **kwargs)
@@ -159,7 +163,8 @@ def __init__(self, repo: "Repo", wdir: str, d: dict):
         self.tracked_vars: Dict[str, Mapping] = {}
 
         stages_data = d.get(STAGES_KWD, {})
-        # we wrap the definitions into ForeachDefinition and EntryDefinition,
+        # we wrap the definitions into:
+        # ForeachDefinition, MatrixDefinition, and EntryDefinition
         # that helps us to optimize, cache and selectively load each one of
         # them as we need, and simplify all of this DSL/parsing logic.
         self.definitions: Dict[str, Definition] = {
@@ -168,12 +173,13 @@ def __init__(self, repo: "Repo", wdir: str, d: dict):
         }
 
     def resolve_one(self, name: str):
-        group, key = split_foreach_name(name)
+        group, key = split_group_name(name)
 
         if not self._has_group_and_key(group, key):
             raise EntryNotFound(f"Could not find '{name}'")
 
-        # all of the checks for `key` not being None for `ForeachDefinition`
+        # all of the checks for `key` not being None for
+        # `ForeachDefinition`/`MatrixDefinition`
         # and/or `group` not existing in the `interim`, etc. should be
         # handled by the `self.has_key()` above.
         definition = self.definitions[group]
@@ -190,7 +196,7 @@ def resolve(self):
         return {STAGES_KWD: data}
 
     def has_key(self, key: str):
-        return self._has_group_and_key(*split_foreach_name(key))
+        return self._has_group_and_key(*split_group_name(key))
 
     def _has_group_and_key(self, group: str, key: Optional[str] = None):
         try:
@@ -198,16 +204,14 @@ def _has_group_and_key(self, group: str, key: Optional[str] = None):
         except KeyError:
             return False
 
-        if key:
-            return isinstance(definition, ForeachDefinition) and definition.has_member(
-                key
-            )
-        return not isinstance(definition, ForeachDefinition)
+        if not isinstance(definition, (ForeachDefinition, MatrixDefinition)):
+            return key is None
+        return key is not None and definition.has_member(key)
 
     @collecting
     def get_keys(self):
         for name, definition in self.definitions.items():
-            if isinstance(definition, ForeachDefinition):
+            if isinstance(definition, (ForeachDefinition, MatrixDefinition)):
                 yield from definition.get_generated_names()
                 continue
             yield name
@@ -256,8 +260,9 @@ def resolve_stage(self, skip_checks: bool = False) -> "DictStrAny":
         name = self.name
         if not skip_checks:
             # we can check for syntax errors as we go for interpolated entries,
-            # but for foreach-generated ones, once is enough, which it does
-            # that itself. See `ForeachDefinition.do_definition`.
+            # but for foreach and matrix generated ones, once is enough, which it does
+            # that itself. See `ForeachDefinition.template`
+            # and `MatrixDefinition.template`.
             check_syntax_errors(self.definition, name, self.relpath)
 
         # we need to pop vars from generated/evaluated data
@@ -331,17 +336,18 @@ def __init__(
         self.name = name
 
         assert DO_KWD in definition
+        assert MATRIX_KWD not in definition
         self.foreach_data = definition[FOREACH_KWD]
-        self._do_definition = definition[DO_KWD]
+        self._template = definition[DO_KWD]
 
         self.pair = IterationPair()
         self.where = where
 
     @cached_property
-    def do_definition(self):
+    def template(self):
         # optimization: check for syntax errors only once for `foreach` stages
-        check_syntax_errors(self._do_definition, self.name, self.relpath)
-        return self._do_definition
+        check_syntax_errors(self._template, self.name, self.relpath)
+        return self._template
 
     @cached_property
     def resolved_iterable(self):
@@ -444,16 +450,111 @@ def _each_iter(self, key: str) -> "DictStrAny":
             # i.e. quadratic complexity).
             generated = self._generate_name(key)
             entry = EntryDefinition(
-                self.resolver, self.context, generated, self.do_definition
+                self.resolver, self.context, generated, self.template
             )
             try:
                 # optimization: skip checking for syntax errors on each foreach
-                # generated stages. We do it once when accessing do_definition.
+                # generated stages. We do it once when accessing template.
                 return entry.resolve_stage(skip_checks=True)
             except ContextError as exc:
                 format_and_raise(exc, f"stage '{generated}'", self.relpath)
 
-            # let mypy know that this state is unreachable as format_and_raise
-            # does not return at all (it's not able to understand it for some
-            # reason)
-            raise AssertionError("unreachable")
+
+class MatrixDefinition:
+    def __init__(
+        self,
+        resolver: DataResolver,
+        context: Context,
+        name: str,
+        definition: "DictStrAny",
+        where: str = STAGES_KWD,
+    ):
+        self.resolver = resolver
+        self.relpath = self.resolver.relpath
+        self.context = context
+        self.name = name
+
+        assert MATRIX_KWD in definition
+        assert DO_KWD not in definition
+        assert FOREACH_KWD not in definition
+
+        self._template = definition.copy()
+        self.matrix_data = self._template.pop(MATRIX_KWD)
+
+        self.pair = IterationPair()
+        self.where = where
+
+    @cached_property
+    def template(self) -> "DictStrAny":
+        # optimization: check for syntax errors only once for `matrix` stages
+        check_syntax_errors(self._template, self.name, self.relpath)
+        return self._template
+
+    @cached_property
+    def resolved_iterable(self) -> Dict[str, List]:
+        return self._resolve_matrix_data()
+
+    def _resolve_matrix_data(self) -> Dict[str, List]:
+        try:
+            iterable = self.context.resolve(self.matrix_data, unwrap=False)
+        except (ContextError, ParseError) as exc:
+            format_and_raise(exc, f"'{self.where}.{self.name}.matrix'", self.relpath)
+        return iterable
+
+    @cached_property
+    def normalized_iterable(self) -> Dict[str, "DictStrAny"]:
+        """Convert sequence to Mapping with keys normalized."""
+        iterable = self.resolved_iterable
+        assert isinstance(iterable, Mapping)
+
+        ret: Dict[str, "DictStrAny"] = {}
+        matrix = {key: enumerate(v) for key, v in iterable.items()}
+        for combination in product(*matrix.values()):
+            d: "DictStrAny" = {}
+            fragments: List[str] = []
+            for k, (i, v) in zip(matrix.keys(), combination):
+                d[k] = v
+                fragments.append(f"{k}{i}" if is_map_or_seq(v) else to_str(v))
+
+            key = "-".join(fragments)
+            ret[key] = d
+        return ret
+
+    def has_member(self, key: str) -> bool:
+        return key in self.normalized_iterable
+
+    def get_generated_names(self) -> List[str]:
+        return list(map(self._generate_name, self.normalized_iterable))
+
+    def _generate_name(self, key: str) -> str:
+        return f"{self.name}{JOIN}{key}"
+
+    def resolve_all(self) -> "DictStrAny":
+        return join(map(self.resolve_one, self.normalized_iterable))
+
+    def resolve_one(self, key: str) -> "DictStrAny":
+        return self._each_iter(key)
+
+    def _each_iter(self, key: str) -> "DictStrAny":
+        err_message = f"Could not find '{key}' in matrix group '{self.name}'"
+        with reraise(KeyError, EntryNotFound(err_message)):
+            value = self.normalized_iterable[key]
+
+        temp_dict = {self.pair.value: value}
+        with self.context.set_temporarily(temp_dict, reserve=True):
+            # optimization: item and key can be removed on __exit__() as they
+            # are top-level values, and are not merged recursively.
+            # This helps us avoid cloning context, which is slower
+            # (increasing the size of the context might increase
+            # the no. of items to be generated which means more cloning,
+            # i.e. quadratic complexity).
+            generated = self._generate_name(key)
+            entry = EntryDefinition(
+                self.resolver, self.context, generated, self.template
+            )
+            try:
+                # optimization: skip checking for syntax errors on each matrix
+                # generated stages. We do it once when accessing template.
+                return entry.resolve_stage(skip_checks=True)
+            except ContextError as exc:
+                format_and_raise(exc, f"stage '{generated}'", self.relpath)
diff --git a/dvc/repo/stage.py b/dvc/repo/stage.py
--- a/dvc/repo/stage.py
+++ b/dvc/repo/stage.py
@@ -243,7 +243,7 @@ def _get_keys(
     ) -> Iterable[str]:
         if not name:
             return stages.keys()
-        if accept_group and stages.is_foreach_generated(name):
+        if accept_group and stages.is_foreach_or_matrix_generated(name):
             return self._get_group_keys(stages, name)
         if glob:
             return fnmatch.filter(stages.keys(), name)
diff --git a/dvc/schema.py b/dvc/schema.py
--- a/dvc/schema.py
+++ b/dvc/schema.py
@@ -11,7 +11,7 @@
     META_SCHEMA,
     Output,
 )
-from dvc.parsing import DO_KWD, FOREACH_KWD, VARS_KWD
+from dvc.parsing import DO_KWD, FOREACH_KWD, MATRIX_KWD, VARS_KWD
 from dvc.stage.params import StageParams
 
 STAGES = "stages"
@@ -77,6 +77,7 @@
 VARS_SCHEMA = [str, dict]
 
 STAGE_DEFINITION = {
+    MATRIX_KWD: {str: Any(str, list)},
     Required(StageParams.PARAM_CMD): Any(str, list),
     Optional(StageParams.PARAM_WDIR): str,
     Optional(StageParams.PARAM_DEPS): [str],
diff --git a/dvc/stage/loader.py b/dvc/stage/loader.py
--- a/dvc/stage/loader.py
+++ b/dvc/stage/loader.py
@@ -7,7 +7,7 @@
 from funcy import get_in, lcat, once, project
 
 from dvc import dependency, output
-from dvc.parsing import FOREACH_KWD, JOIN, EntryNotFound
+from dvc.parsing import FOREACH_KWD, JOIN, MATRIX_KWD, EntryNotFound
 from dvc.utils.objects import cached_property
 from dvc_data.hashfile.meta import Meta
 
@@ -167,8 +167,11 @@ def __len__(self):
     def __contains__(self, name):
         return self.resolver.has_key(name)  # noqa: W601
 
-    def is_foreach_generated(self, name: str):
-        return name in self.stages_data and FOREACH_KWD in self.stages_data[name]
+    def is_foreach_or_matrix_generated(self, name: str) -> bool:
+        return (
+            name in self.stages_data
+            and {FOREACH_KWD, MATRIX_KWD} & self.stages_data[name].keys()
+        )
 
 
 class SingleStageLoader(Mapping):


Test patch:
diff --git a/tests/func/parsing/test_matrix.py b/tests/func/parsing/test_matrix.py
new file mode 100644
--- /dev/null
+++ b/tests/func/parsing/test_matrix.py
@@ -0,0 +1,52 @@
+import pytest
+
+from dvc.parsing import DataResolver, MatrixDefinition
+
+MATRIX_DATA = {
+    "os": ["win", "linux"],
+    "pyv": [3.7, 3.8],
+    "dict": [{"arg1": 1}, {"arg2": 2}],
+    "list": [["out1", "out11"], ["out2", "out22"]],
+}
+
+
+@pytest.mark.parametrize(
+    "matrix",
+    [
+        MATRIX_DATA,
+        {
+            "os": "${os}",
+            "pyv": "${pyv}",
+            "dict": "${dict}",
+            "list": "${list}",
+        },
+    ],
+)
+def test_matrix_interpolated(tmp_dir, dvc, matrix):
+    (tmp_dir / "params.yaml").dump(MATRIX_DATA)
+    resolver = DataResolver(dvc, tmp_dir.fs_path, {})
+    data = {
+        "matrix": matrix,
+        "cmd": "echo ${item.os} ${item.pyv} ${item.dict}"
+        " -- ${item.list.0} ${item.list.1}",
+    }
+    definition = MatrixDefinition(resolver, resolver.context, "build", data)
+
+    assert definition.resolve_all() == {
+        "build@win-3.7-dict0-list0": {"cmd": "echo win 3.7 --arg1 1 -- out1 out11"},
+        "build@win-3.7-dict0-list1": {"cmd": "echo win 3.7 --arg1 1 -- out2 out22"},
+        "build@win-3.7-dict1-list0": {"cmd": "echo win 3.7 --arg2 2 -- out1 out11"},
+        "build@win-3.7-dict1-list1": {"cmd": "echo win 3.7 --arg2 2 -- out2 out22"},
+        "build@win-3.8-dict0-list0": {"cmd": "echo win 3.8 --arg1 1 -- out1 out11"},
+        "build@win-3.8-dict0-list1": {"cmd": "echo win 3.8 --arg1 1 -- out2 out22"},
+        "build@win-3.8-dict1-list0": {"cmd": "echo win 3.8 --arg2 2 -- out1 out11"},
+        "build@win-3.8-dict1-list1": {"cmd": "echo win 3.8 --arg2 2 -- out2 out22"},
+        "build@linux-3.7-dict0-list0": {"cmd": "echo linux 3.7 --arg1 1 -- out1 out11"},
+        "build@linux-3.7-dict0-list1": {"cmd": "echo linux 3.7 --arg1 1 -- out2 out22"},
+        "build@linux-3.7-dict1-list0": {"cmd": "echo linux 3.7 --arg2 2 -- out1 out11"},
+        "build@linux-3.7-dict1-list1": {"cmd": "echo linux 3.7 --arg2 2 -- out2 out22"},
+        "build@linux-3.8-dict0-list0": {"cmd": "echo linux 3.8 --arg1 1 -- out1 out11"},
+        "build@linux-3.8-dict0-list1": {"cmd": "echo linux 3.8 --arg1 1 -- out2 out22"},
+        "build@linux-3.8-dict1-list0": {"cmd": "echo linux 3.8 --arg2 2 -- out1 out11"},
+        "build@linux-3.8-dict1-list1": {"cmd": "echo linux 3.8 --arg2 2 -- out2 out22"},
+    }