diff --git a/docs/tutorial/multiple-values/multiple-options.md b/docs/tutorial/multiple-values/multiple-options.md
index 08c9e80ff2..d6c16b8a92 100644
--- a/docs/tutorial/multiple-values/multiple-options.md
+++ b/docs/tutorial/multiple-values/multiple-options.md
@@ -63,3 +63,49 @@ The sum is 9.5
```
+
+## Passing multiple values in a single argument
+
+**Typer** supports passing multiple arguments with a single option, by using the `separator` parameter in combination with `typing.List[T]` types.
+This feature makes it easy to parse multiple values from a single command-line argument into a list in your application.
+
+To use this feature, define a command-line option that accepts multiple values separated by a specific character (such as a comma). Here's an example of how to implement this:
+
+{* docs_src/multiple_values/multiple_options/tutorial003_an.py hl[10] *}
+
+Check it:
+
+
+
+```console
+// With no optional CLI argument
+$ python main.py
+
+The sum is 0
+
+// With one number argument
+$ python main.py --number 2
+
+The sum is 2.0
+
+// With several number arguments, split using the separator defined by the Option argument
+$ python main.py --number "2, 3, 4.5"
+
+The sum is 9.5
+
+// You can remove the quotes if no whitespace is added between the numbers
+$ python main.py --number 2,3,4.5
+
+The sum is 9.5
+
+// Supports passing the option multiple times. This joins all values to a single list
+$ python main.py --number 2,3,4.5 --number 5
+
+The sum is 14.5
+```
+
+
+
+/// warning
+
+Only single-character non-whitespace separators are supported.
diff --git a/docs_src/multiple_values/multiple_options/tutorial003.py b/docs_src/multiple_values/multiple_options/tutorial003.py
new file mode 100644
index 0000000000..ba973ad5f9
--- /dev/null
+++ b/docs_src/multiple_values/multiple_options/tutorial003.py
@@ -0,0 +1,14 @@
+from typing import List
+
+import typer
+
+app = typer.Typer()
+
+
+@app.command()
+def main(number: List[float] = typer.Option([], separator=",")):
+ print(f"The sum is {sum(number)}")
+
+
+if __name__ == "__main__":
+ app()
diff --git a/docs_src/multiple_values/multiple_options/tutorial003_an.py b/docs_src/multiple_values/multiple_options/tutorial003_an.py
new file mode 100644
index 0000000000..abdcf4e610
--- /dev/null
+++ b/docs_src/multiple_values/multiple_options/tutorial003_an.py
@@ -0,0 +1,15 @@
+from typing import List
+
+import typer
+from typing_extensions import Annotated
+
+app = typer.Typer()
+
+
+@app.command()
+def main(number: Annotated[List[float], typer.Option(separator=",")] = []):
+ print(f"The sum is {sum(number)}")
+
+
+if __name__ == "__main__":
+ app()
diff --git a/pyproject.toml b/pyproject.toml
index ac89bbd9bb..26e2c31634 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -184,6 +184,7 @@ ignore = [
# Default mutable data structure
"docs_src/options_autocompletion/tutorial006_an.py" = ["B006"]
"docs_src/multiple_values/multiple_options/tutorial002_an.py" = ["B006"]
+"docs_src/multiple_values/multiple_options/tutorial003_an.py" = ["B006"]
"docs_src/options_autocompletion/tutorial007_an.py" = ["B006"]
"docs_src/options_autocompletion/tutorial008_an.py" = ["B006"]
"docs_src/options_autocompletion/tutorial009_an.py" = ["B006"]
diff --git a/tests/test_others.py b/tests/test_others.py
index a8ba207a5f..f1846d3078 100644
--- a/tests/test_others.py
+++ b/tests/test_others.py
@@ -321,3 +321,33 @@ def test_split_opt():
prefix, opt = _split_opt("verbose")
assert prefix == ""
assert opt == "verbose"
+
+
+def test_multiple_options_separator_1_unsupported_separator():
+ app = typer.Typer()
+
+ @app.command()
+ def main(names: typing.List[str] = typer.Option(..., separator="\t \n")):
+ pass # pragma: no cover
+
+ with pytest.raises(typer.UnsupportedSeparatorError) as exc_info:
+ runner.invoke(app, [])
+ assert (
+ str(exc_info.value)
+ == "Error in definition of Option 'names'. Only single-character non-whitespace separators are supported, but got \"\t \n\"."
+ )
+
+
+def test_multiple_options_separator_2_non_list_type():
+ app = typer.Typer()
+
+ @app.command()
+ def main(names: str = typer.Option(..., separator=",")):
+ pass # pragma: no cover
+
+ with pytest.raises(typer.SeparatorForNonListTypeError) as exc_info:
+ runner.invoke(app, [])
+ assert (
+ str(exc_info.value)
+ == "Multiple values are supported for List[T] types only. Annotate 'names' as List[str] to support multiple values."
+ )
diff --git a/tests/test_tutorial/test_multiple_values/test_multiple_options/test_tutorial003.py b/tests/test_tutorial/test_multiple_values/test_multiple_options/test_tutorial003.py
new file mode 100644
index 0000000000..d09150eb8d
--- /dev/null
+++ b/tests/test_tutorial/test_multiple_values/test_multiple_options/test_tutorial003.py
@@ -0,0 +1,43 @@
+import subprocess
+import sys
+
+from typer.testing import CliRunner
+
+from docs_src.multiple_values.multiple_options import tutorial003 as mod
+
+app = mod.app
+
+runner = CliRunner()
+
+
+def test_main():
+ result = runner.invoke(app)
+ assert result.exit_code == 0
+ assert "The sum is 0" in result.output
+
+
+def test_1_number():
+ result = runner.invoke(app, ["--number", "2"])
+ assert result.exit_code == 0
+ assert "The sum is 2.0" in result.output
+
+
+def test_2_number():
+ result = runner.invoke(app, ["--number", "2,3,4.5"], catch_exceptions=False)
+ assert result.exit_code == 0
+ assert "The sum is 9.5" in result.output
+
+
+def test_3_number():
+ result = runner.invoke(app, ["--number", "2,3,4.5", "--number", "5"])
+ assert result.exit_code == 0
+ assert "The sum is 14.5" in result.output
+
+
+def test_script():
+ result = subprocess.run(
+ [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"],
+ capture_output=True,
+ encoding="utf-8",
+ )
+ assert "Usage" in result.stdout
diff --git a/tests/test_tutorial/test_multiple_values/test_multiple_options/test_tutorial003_an.py b/tests/test_tutorial/test_multiple_values/test_multiple_options/test_tutorial003_an.py
new file mode 100644
index 0000000000..e7ae10cb28
--- /dev/null
+++ b/tests/test_tutorial/test_multiple_values/test_multiple_options/test_tutorial003_an.py
@@ -0,0 +1,43 @@
+import subprocess
+import sys
+
+from typer.testing import CliRunner
+
+from docs_src.multiple_values.multiple_options import tutorial003_an as mod
+
+app = mod.app
+
+runner = CliRunner()
+
+
+def test_main():
+ result = runner.invoke(app)
+ assert result.exit_code == 0
+ assert "The sum is 0" in result.output
+
+
+def test_1_number():
+ result = runner.invoke(app, ["--number", "2"])
+ assert result.exit_code == 0
+ assert "The sum is 2.0" in result.output
+
+
+def test_2_number():
+ result = runner.invoke(app, ["--number", "2,3,4.5"])
+ assert result.exit_code == 0
+ assert "The sum is 9.5" in result.output
+
+
+def test_3_number():
+ result = runner.invoke(app, ["--number", "2,3,4.5", "--number", "5"])
+ assert result.exit_code == 0
+ assert "The sum is 14.5" in result.output
+
+
+def test_script():
+ result = subprocess.run(
+ [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"],
+ capture_output=True,
+ encoding="utf-8",
+ )
+ assert "Usage" in result.stdout
diff --git a/typer/__init__.py b/typer/__init__.py
index a2e897cdd7..726e03595a 100644
--- a/typer/__init__.py
+++ b/typer/__init__.py
@@ -37,3 +37,9 @@
from .models import FileTextWrite as FileTextWrite
from .params import Argument as Argument
from .params import Option as Option
+from .utils import (
+ SeparatorForNonListTypeError as SeparatorForNonListTypeError,
+)
+from .utils import (
+ UnsupportedSeparatorError as UnsupportedSeparatorError,
+)
diff --git a/typer/core.py b/typer/core.py
index e9631e56cf..aa22a638c6 100644
--- a/typer/core.py
+++ b/typer/core.py
@@ -3,6 +3,7 @@
import inspect
import os
import sys
+import typing as t
from difflib import get_close_matches
from enum import Enum
from gettext import gettext as _
@@ -26,6 +27,7 @@
import click.shell_completion
import click.types
import click.utils
+from click import Context
from ._typing import Literal
@@ -446,6 +448,7 @@ def __init__(
show_envvar: bool = False,
# Rich settings
rich_help_panel: Union[str, None] = None,
+ separator: Optional[str] = None,
):
super().__init__(
param_decls=param_decls,
@@ -475,6 +478,19 @@ def __init__(
)
_typer_param_setup_autocompletion_compat(self, autocompletion=autocompletion)
self.rich_help_panel = rich_help_panel
+ self.original_type = type
+ self.separator = separator
+
+ def _parse_separated_parameter_list(self, parameter_values: List[str]) -> List[str]:
+ values = []
+ for param_str_list in parameter_values:
+ values.extend(param_str_list.split(self.separator))
+ return values
+
+ def process_value(self, ctx: Context, value: t.Any) -> t.Any:
+ if self.separator is not None:
+ value = self._parse_separated_parameter_list(value)
+ return super().process_value(ctx, value)
def _get_default_string(
self,
diff --git a/typer/main.py b/typer/main.py
index 2e3b1223cc..4429893768 100644
--- a/typer/main.py
+++ b/typer/main.py
@@ -48,7 +48,11 @@
TyperInfo,
TyperPath,
)
-from .utils import get_params_from_function
+from .utils import (
+ SeparatorForNonListTypeError,
+ UnsupportedSeparatorError,
+ get_params_from_function,
+)
_original_except_hook = sys.excepthook
_typer_developer_exception_attr_name = "__typer_developer_exception__"
@@ -896,6 +900,18 @@ def get_click_param(
param_decls.extend(parameter_info.param_decls)
else:
param_decls.append(default_option_declaration)
+
+ # Check the multiple separator option for validity
+ separator = None
+ if parameter_info.separator:
+ separator = parameter_info.separator.strip()
+
+ if not is_list:
+ raise SeparatorForNonListTypeError(param.name, main_type)
+
+ if len(separator) != 1:
+ raise UnsupportedSeparatorError(param.name, parameter_info.separator)
+
return (
TyperOption(
# Option
@@ -928,6 +944,7 @@ def get_click_param(
autocompletion=get_param_completion(parameter_info.autocompletion),
# Rich settings
rich_help_panel=parameter_info.rich_help_panel,
+ separator=separator,
),
convertor,
)
diff --git a/typer/models.py b/typer/models.py
index e0bddb965b..8bbd7f6410 100644
--- a/typer/models.py
+++ b/typer/models.py
@@ -336,6 +336,7 @@ def __init__(
path_type: Union[None, Type[str], Type[bytes]] = None,
# Rich settings
rich_help_panel: Union[str, None] = None,
+ separator: Optional[str] = None,
):
super().__init__(
default=default,
@@ -398,6 +399,7 @@ def __init__(
self.hide_input = hide_input
self.count = count
self.allow_from_autoenv = allow_from_autoenv
+ self.separator = separator
class ArgumentInfo(ParameterInfo):
diff --git a/typer/params.py b/typer/params.py
index 66c2b32d3e..ac3c55962c 100644
--- a/typer/params.py
+++ b/typer/params.py
@@ -202,6 +202,8 @@ def Option(
path_type: Union[None, Type[str], Type[bytes]] = None,
# Rich settings
rich_help_panel: Union[str, None] = None,
+ # Multiple values
+ separator: Optional[str] = None,
) -> Any:
return OptionInfo(
# Parameter
@@ -257,6 +259,7 @@ def Option(
path_type=path_type,
# Rich settings
rich_help_panel=rich_help_panel,
+ separator=separator,
)
diff --git a/typer/utils.py b/typer/utils.py
index 81dc4dd61d..c9a47dbb54 100644
--- a/typer/utils.py
+++ b/typer/utils.py
@@ -188,3 +188,30 @@ def get_params_from_function(func: Callable[..., Any]) -> Dict[str, ParamMeta]:
name=param.name, default=default, annotation=annotation
)
return params
+
+
+class SeparatorForNonListTypeError(Exception):
+ argument_name: str
+ argument_type: Type[Any]
+
+ def __init__(self, argument_name: str, argument_type: Type[Any]):
+ self.argument_name = argument_name
+ self.argument_type = argument_type
+
+ def __str__(self) -> str:
+ return f"Multiple values are supported for List[T] types only. Annotate {self.argument_name!r} as List[{self.argument_type.__name__}] to support multiple values."
+
+
+class UnsupportedSeparatorError(Exception):
+ argument_name: str
+ separator: str
+
+ def __init__(self, argument_name: str, separator: str):
+ self.argument_name = argument_name
+ self.separator = separator
+
+ def __str__(self) -> str:
+ return (
+ f"Error in definition of Option {self.argument_name!r}. "
+ f'Only single-character non-whitespace separators are supported, but got "{self.separator}".'
+ )