Repository: Sceptre/sceptre

Issue:
Stack outputs in stack groups hide the stack names
### An export format for stack outputs that shows stack name is needed
The main purpose of stack outputs is to consume them in other stacks. Example:

```yaml
VpcId: !stack_output network.yaml::VpcId
```

or in a var file:

```yaml
VpcId: '!stack_output_external my-network-stack::VpcId'
```

In the case of stack groups however, there is no easy way to know which stack exports which outputs without either reading the source, going to the AWS Console, or using AWS CLI commands. Furthermore, even knowing what the stack name of any stack is requires use of Sceptre's `list stacks` command as Sceptre's logic of setting the stack name has never been obvious.

The options we have are:

1. list outputs 
```
% sceptre --output=yaml list outputs
---                       
- direct-connect:                     
  - OutputKey: VPGWId     
    OutputValue: vgw-1fb1e1e1af11da11b
- vpc:                             
  - ExportName: nonprod-DataSubnetCIDRa
    OutputKey: DataSubnetCIDRa     
    OutputValue: 11.11.111.11/11      
  - ExportName: nonprod-PublicSubnetCIDRa
    OutputKey: PublicSubnetCIDRa      
    OutputValue: 11.11.111.111/11
...
```

This command is flawed, in that it does not report true stack names, but names like `direct-connect` and `vpc` that only have meaning inside Sceptre itself.

2. list outputs --export=envvar
```
% sceptre --output=yaml list outputs --export=envvar
export SCEPTRE_VPGWId='vgw-1fb1e1e1af11da11b'
export SCEPTRE_DataSubnetCIDRa='11.11.111.11/11'
export SCEPTRE_PublicSubnetCIDRa='11.11.111.111/11'
export SCEPTRE_DataSubnetCIDRb='11.11.111.11/11'
export SCEPTRE_VPCId='vpc-111111ab111a11e11'
...
```

This command is also flawed, as it does not report the stack name at all.

Note that this same CLI behaviour of listing outputs originates in the initial commit of this project back in 2017. In that original version of the code we had:

```python
@cli.command(name="describe-stack-outputs")
@stack_options
@click.option("--export", type=click.Choice(["envvar"]))
@click.pass_context
@catch_exceptions
def describe_stack_outputs(ctx, environment, stack, export):
    """
    Describes stack outputs.

    Describes ENVIRONMENT/STACK's stack outputs.
    """
    env = get_env(ctx.obj["sceptre_dir"], environment, ctx.obj["options"])
    response = env.stacks[stack].describe_outputs()

    if export == "envvar":
        write("\n".join(
            [
                "export SCEPTRE_{0}={1}".format(
                    output["OutputKey"], output["OutputValue"]
                )
                for output in response
            ]
        ))
    else:
        write(response, ctx.obj["output_format"])
```

### Proposal export=stackoutput and export=stackoutputexternal

In addition to `--export=envvar` options like `--export=stackoutput` and `--export=stackoutputexternal` are needed. They might look like this:

```
% sceptre list outputs --export=stackoutput
!stack_output direct-connect.yaml::VPGWId --- vgw-1fb1e1e1af11da111
!stack_output vpc.yaml::DataSubnetCIDRa --- 11.11.111.11/11
!stack_output vpc.yaml::PublicSubnetCIDRa --- 11.11.111.111/11
!stack_output vpc.yaml::DataSubnetCIDRb --- 11.11.111.11/11
!stack_output vpc.yaml::VPCId --- vpc-111111ab111a11e11
...
```
and

```
% sceptre list outputs --export=stackoutputexternal
!stack_output_external nonprod-direct-connect::VPGWId --- vgw-1fb1e1e1af11da111
!stack_output_external nonprod-vpc::DataSubnetCIDRa --- 11.11.111.11/11
!stack_output_external nonprod-vpc::PublicSubnetCIDRa --- 11.11.111.111/11
!stack_output_external nonprod-vpc::DataSubnetCIDRb --- 11.11.111.11/11
!stack_output_external nonprod-vpc::VPCId --- vpc-111111ab111a11e11
...
```

Discussion:

Patch:
diff --git a/sceptre/cli/list.py b/sceptre/cli/list.py
index 6f11bd2..c9b7b0c 100644
--- a/sceptre/cli/list.py
+++ b/sceptre/cli/list.py
@@ -5,6 +5,8 @@ from sceptre.context import SceptreContext
 from sceptre.cli.helpers import catch_exceptions, write
 from sceptre.plan.plan import SceptrePlan
 
+from typing import List, Dict
+
 logger = logging.getLogger(__name__)
 
 
@@ -46,12 +48,67 @@ def list_resources(ctx, path):
     write(responses, context.output_format)
 
 
+# flake8: noqa: C901
+def write_outputs(export, responses, plan, context):
+    """
+    Helper function for list outputs.
+    """
+    # Legacy. This option was added in the initial commit of the project,
+    # although its intended use case is unclear. It may relate to a feature
+    # that had been removed prior to the initial commit.
+    if export == "envvar":
+        for response in responses:
+            for stack in response.values():
+                for output in stack:
+                    write(
+                        "export SCEPTRE_{0}='{1}'".format(
+                            output.get("OutputKey"), output.get("OutputValue")
+                        ),
+                        "text",
+                    )
+
+    # Format outputs as !stack_output references.
+    elif export == "stackoutput":
+        for response in responses:
+            for stack_name, stack in response.items():
+                for output in stack:
+                    write(
+                        "!stack_output {0}.yaml::{1} [{2}]".format(
+                            stack_name,
+                            output.get("OutputKey"),
+                            output.get("OutputValue"),
+                        ),
+                        "text",
+                    )
+
+    # Format outputs as !stack_output_external references.
+    elif export == "stackoutputexternal":
+        stack_names = {stack.name: stack.external_name for stack in plan.graph}
+        for response in responses:
+            for stack_name, stack in response.items():
+                for output in stack:
+                    write(
+                        "!stack_output_external {0}::{1} [{2}]".format(
+                            stack_names[stack_name],
+                            output.get("OutputKey"),
+                            output.get("OutputValue"),
+                        ),
+                        "text",
+                    )
+
+    # Legacy. The output here is somewhat confusing in that
+    # outputs are organised in keys that only have meaning inside
+    # Sceptre.
+    else:
+        write(responses, context.output_format)
+
+
 @list_group.command(name="outputs")
 @click.argument("path")
 @click.option(
     "-e",
     "--export",
-    type=click.Choice(["envvar"]),
+    type=click.Choice(["envvar", "stackoutput", "stackoutputexternal"]),
     help="Specify the export formatting.",
 )
 @click.pass_context
@@ -60,7 +117,6 @@ def list_outputs(ctx, path, export):
     """
     List outputs for stack.
     \f
-
     :param path: Path to execute the command on.
     :type path: str
     :param export: Specify the export formatting.
@@ -79,18 +135,7 @@ def list_outputs(ctx, path, export):
     plan = SceptrePlan(context)
     responses = [response for response in plan.describe_outputs().values() if response]
 
-    if export == "envvar":
-        for response in responses:
-            for stack in response.values():
-                for output in stack:
-                    write(
-                        "export SCEPTRE_{0}='{1}'".format(
-                            output.get("OutputKey"), output.get("OutputValue")
-                        ),
-                        "text",
-                    )
-    else:
-        write(responses, context.output_format)
+    write_outputs(export, responses, plan, context)
 
 
 @list_group.command(name="change-sets")


Test patch:
diff --git a/tests/test_cli/test_cli_commands.py b/tests/test_cli/test_cli_commands.py
index c3b4fd5..b5b8c88 100644
--- a/tests/test_cli/test_cli_commands.py
+++ b/tests/test_cli/test_cli_commands.py
@@ -636,8 +636,8 @@ class TestCli:
         expected_output = "StackOutputKeyOutputValue\n\nStackNameKeyValue\n"
         assert result.output.replace(" ", "") == expected_output
 
-    def test_list_outputs_with_export(self):
-        outputs = {"stack": [{"OutputKey": "Key", "OutputValue": "Value"}]}
+    def test_list_outputs_with_export__envvar(self):
+        outputs = {"mock-stack": [{"OutputKey": "Key", "OutputValue": "Value"}]}
         self.mock_stack_actions.describe_outputs.return_value = outputs
         result = self.runner.invoke(
             cli, ["list", "outputs", "dev/vpc.yaml", "-e", "envvar"]
@@ -645,6 +645,26 @@ class TestCli:
         assert result.exit_code == 0
         assert result.output == "export SCEPTRE_Key='Value'\n"
 
+    def test_list_outputs_with_export__stackoutput(self):
+        outputs = {"mock-stack": [{"OutputKey": "Key", "OutputValue": "Value"}]}
+        self.mock_stack_actions.describe_outputs.return_value = outputs
+        result = self.runner.invoke(
+            cli, ["list", "outputs", "dev/vpc.yaml", "-e", "stackoutput"]
+        )
+        assert result.exit_code == 0
+        assert result.output == "!stack_output mock-stack.yaml::Key [Value]\n"
+
+    def test_list_outputs_with_export__stackoutputexternal(self):
+        outputs = {"mock-stack": [{"OutputKey": "Key", "OutputValue": "Value"}]}
+        self.mock_stack_actions.describe_outputs.return_value = outputs
+        result = self.runner.invoke(
+            cli, ["list", "outputs", "dev/vpc.yaml", "-e", "stackoutputexternal"]
+        )
+        assert result.exit_code == 0
+        assert (
+            result.output == "!stack_output_external mock-stack-external::Key [Value]\n"
+        )
+
     @pytest.mark.parametrize(
         "path,output_format,expected_output",
         [