Repository: pdm-project/pdm

Issue:
Task verbosity change introduce in 2.21 breaks some scripts
### Describe the bug

In 2.21, in https://github.com/pdm-project/pdm/pull/3278 more precisely, default scripts verbosity has been changed, and it breaks some existing scripts relying on output:
- all scripts that are output-dependants now needs to run with `-q` (can represent some volume to migrate)
- script called using `pdm <command>` have to migrate to `pdm run -q <command>` to preserve output, `-q` not being available on the shortcut form
- script full command is outputted in addition to the task title, resulting in lots of line lost and broken terminal output when relying on some multiline scripts
- it makes the `-v` flag do nothing more
- It kind of break verbosity consistency as everywhere else command echo/feedback is only available with detailed verbosity (ex: https://github.com/pdm-project/pdm/blob/main/src/pdm/cli/commands/venv/backends.py#L72-L75)

### To reproduce

- create a script outputting some formatted data (JSON, CSV, ...) > not parsable anymore without `-q`
- create a script spanning on multiple lines > same amount of lines is prepended in addition to the title
- create a composite script, with optionally pre or post hooks > output is now having 2 extra lines by task hook (assuming they are single line scripts)

### Expected Behavior

One of:
1. default script behavior just run the script without decoration, echoing script name and command line being by definition verbosity
2. this behavior is opt-in, aka. no output as default, a user setting allows enabling this when not verbose, but remain whatever the setting value in verbose mode
3. this behavior is opt-in and progressive, aka. no output as default, a user setting allows disabling the task name only in normal, and the full output remain in verbose mode whatever the setting value

(The third one is having my preference)

### Environment Information

PDM version:
  2.21.0
Python Interpreter:
  <redacted workdir>/.venv/bin/python (3.11)
Project Root:
  <redacted workdir>
Local Packages:

{
  "implementation_name": "cpython",
  "implementation_version": "3.12.7",
  "os_name": "posix",
  "platform_machine": "x86_64",
  "platform_release": "6.11.9-arch1-1",
  "platform_system": "Linux",
  "platform_version": "#1 SMP PREEMPT_DYNAMIC Sun, 17 Nov 2024 16:06:17 +0000",
  "python_full_version": "3.11.9",
  "platform_python_implementation": "CPython",
  "python_version": "3.11",
  "sys_platform": "linux"
}

### Additional Context

Given I also understand some people like having tasks feedback by default, this PR is to start discussing what should be the solution, choose a setting name (`scripts.echo` ? `scripts.display_name` ? `scripts.always_show_name` ?) if a setting-based solution is accepted.

### Are you willing to submit a PR to fix this bug?

- [X] Yes, I would like to submit a PR.

Discussion:
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/pdm-project/pdm/pull/3315?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 85.33%. Comparing base [(`2631439`)](https://app.codecov.io/gh/pdm-project/pdm/commit/263143935b8ed378d9fd72a717b1b16d54a7fef3?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project) to head [(`ac0f02b`)](https://app.codecov.io/gh/pdm-project/pdm/commit/ac0f02be3e6b9a235e3c21c5ed1761130e880fc0?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project).

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #3315      +/-   ##
==========================================
+ Coverage   85.32%   85.33%   +0.01%     
==========================================
  Files         112      112              
  Lines       11406    11417      +11     
  Branches     2489     2492       +3     
==========================================
+ Hits         9732     9743      +11     
  Misses       1144     1144              
  Partials      530      530              
```

| [Flag](https://app.codecov.io/gh/pdm-project/pdm/pull/3315/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project) | Coverage Δ | |
|---|---|---|
| [unittests](https://app.codecov.io/gh/pdm-project/pdm/pull/3315/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project) | `85.13% <100.00%> (+0.01%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/pdm-project/pdm/pull/3315?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project).

frostming: Adding two configurations for this feature seems a bit finicky to me. People may be overwhelmed by this large number of configs

Patch:
diff --git a/news/3313.feature.md b/news/3313.feature.md
new file mode 100644
index 00000000..a55571e1
--- /dev/null
+++ b/news/3313.feature.md
@@ -0,0 +1,1 @@
+Allow customizing scripts display with `scripts.show_header` settings.
diff --git a/src/pdm/cli/commands/run.py b/src/pdm/cli/commands/run.py
index 5ed21ded..2765f0c9 100644
--- a/src/pdm/cli/commands/run.py
+++ b/src/pdm/cli/commands/run.py
@@ -341,11 +341,8 @@ class TaskRunner:
         elif kind == "composite":
             assert isinstance(value, list)
 
-        self.project.core.ui.echo(
-            f"Running {task}: [success]{args}[/]\n",
-            err=True,
-            verbosity=termui.Verbosity.NORMAL,
-        )
+        self.display_task(task, args)
+
         if kind == "composite":
             args = list(args)
             should_interpolate = any(RE_ARGS_PLACEHOLDER.search(script) for script in value)
@@ -366,6 +363,19 @@ class TaskRunner:
             return composite_code
         return self._run_process(args, chdir=True, shell=shell, **merge_options(self.global_options, options, opts))  # type: ignore[misc]
 
+    def display_task(self, task: Task, args: Sequence[str]) -> None:
+        """Display a task given current verbosity and settings"""
+        is_verbose = self.project.core.ui.verbosity >= termui.Verbosity.DETAIL
+        if not (is_verbose or self.project.config.get("scripts.show_header")):
+            return
+        args = task.args if task.kind == "composite" else args
+        content = args if is_verbose else task.short_description
+        self.project.core.ui.echo(
+            f"Running {task}: [success]{content}[/]",
+            err=True,
+            verbosity=termui.Verbosity.NORMAL,
+        )
+
     def run(
         self,
         command: str,
diff --git a/src/pdm/project/config.py b/src/pdm/project/config.py
index 31a79af7..afb51abb 100644
--- a/src/pdm/project/config.py
+++ b/src/pdm/project/config.py
@@ -217,6 +217,12 @@ class Config(MutableMapping[str, str]):
             env_var="PDM_PYPI_JSON_API",
             coerce=ensure_boolean,
         ),
+        "scripts.show_header": ConfigItem(
+            "Display script name and help before running",
+            default=False,
+            env_var="PDM_SCRIPTS_SHOW_HEADER",
+            coerce=ensure_boolean,
+        ),
         "venv.location": ConfigItem(
             "Parent directory for virtualenvs",
             os.path.join(platformdirs.user_data_dir("pdm"), "venvs"),


Test patch:
diff --git a/tests/cli/test_run.py b/tests/cli/test_run.py
index 92d6e451..80800728 100644
--- a/tests/cli/test_run.py
+++ b/tests/cli/test_run.py
@@ -1028,3 +1028,109 @@ def test_run_script_pass_run_cwd_to_original_working_dir_when_working_dir_of_scr
     capfd.readouterr()
     pdm(["run", "test_script"], obj=project, strict=True)
     assert capfd.readouterr()[0].strip() == str(Path.cwd())
+
+
+def test_run_script_default_verbosity(project, pdm):
+    project.pyproject.settings["scripts"] = {"test": {"cmd": "python -V", "help": "help"}}
+    project.pyproject.write()
+
+    result = pdm(["run", "test"], strict=True, obj=project)
+
+    assert "task test" not in result.stderr
+    assert "['python', '-V']" not in result.stderr
+    assert "help" not in result.stderr
+
+
+def test_run_script_default_verbosity_with_show_header(project, pdm):
+    project.project_config.update({"scripts.show_header": True})
+    project.pyproject.settings["scripts"] = {"test": {"cmd": "python -V", "help": "help"}}
+    project.pyproject.write()
+
+    result = pdm(["run", "test"], strict=True, obj=project)
+
+    assert "task test" in result.stderr
+    assert "['python', '-V']" not in result.stderr
+    assert "python -V" not in result.stderr
+    assert "help" in result.stderr
+
+
+def test_run_script_default_verbosity_with_show_header_no_help(project, pdm):
+    project.project_config.update({"scripts.show_header": True})
+    project.pyproject.settings["scripts"] = {"test": {"cmd": "python -V"}}
+    project.pyproject.write()
+
+    result = pdm(["run", "test"], strict=True, obj=project)
+
+    assert "task test" in result.stderr
+    assert "python -V" in result.stderr
+    assert "['python', '-V']" not in result.stderr
+
+
+def test_run_script_verbose(project, pdm):
+    project.pyproject.settings["scripts"] = {"test": {"cmd": "python -V", "help": "help"}}
+    project.pyproject.write()
+
+    result = pdm(["run", "-v", "test"], strict=True, obj=project)
+
+    assert "task test" in result.stderr
+    assert "['python', '-V']" in result.stderr
+    assert "python -V" not in result.stderr
+    assert "help" not in result.stderr
+
+
+def test_run_composite_script_default_verbosity_with_show_header(project, pdm):
+    project.project_config.update({"scripts.show_header": True})
+    project.pyproject.settings["scripts"] = {
+        "test": {"cmd": "python -V", "help": "help"},
+        "parent": {"composite": ["test", "test"]},
+    }
+    project.pyproject.write()
+
+    result = pdm(["run", "parent"], strict=True, obj=project)
+
+    assert "task parent" in result.stderr
+    assert f"test {termui.Emoji.ARROW_SEPARATOR} test" in result.stderr
+    assert "['test', 'test']" not in result.stderr
+    assert "task test" in result.stderr
+    assert "python -V" not in result.stderr
+    assert "['python', '-V']" not in result.stderr
+    assert "help" in result.stderr
+
+
+def test_run_composite_script_default_verbosity_with_show_header_and_help(project, pdm):
+    project.project_config.update({"scripts.show_header": True})
+    project.pyproject.settings["scripts"] = {
+        "test": {"cmd": "python -V", "help": "help"},
+        "parent": {"composite": ["test", "test"], "help": "composite"},
+    }
+    project.pyproject.write()
+
+    result = pdm(["run", "parent"], strict=True, obj=project)
+
+    assert "task parent" in result.stderr
+    assert "composite" in result.stderr
+    assert f"test {termui.Emoji.ARROW_SEPARATOR} test" not in result.stderr
+    assert "['test', 'test']" not in result.stderr
+    assert "task test" in result.stderr
+    assert "['python', '-V']" not in result.stderr
+    assert "python -V" not in result.stderr
+    assert "help" in result.stderr
+
+
+def test_run_composite_script_verbose(project, pdm):
+    project.pyproject.settings["scripts"] = {
+        "test": {"cmd": "python -V", "help": "help"},
+        "parent": {"composite": ["test", "test"], "help": "composite"},
+    }
+    project.pyproject.write()
+
+    result = pdm(["run", "-v", "parent"], strict=True, obj=project)
+
+    assert "task parent" in result.stderr
+    assert "composite" not in result.stderr
+    assert f"test {termui.Emoji.ARROW_SEPARATOR} test" not in result.stderr
+    assert "['test', 'test']" in result.stderr
+    assert "task test" in result.stderr
+    assert "['python', '-V']" in result.stderr
+    assert "python -V" not in result.stderr
+    assert "help" not in result.stderr