diff --git a/docs/tutorial/multiple-values/multiple-options.md b/docs/tutorial/multiple-values/multiple-options.md
index 0c876eab52..ae59013b29 100644
--- a/docs/tutorial/multiple-values/multiple-options.md
+++ b/docs/tutorial/multiple-values/multiple-options.md
@@ -103,3 +103,61 @@ 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:
+
+=== "Python 3.7+"
+
+ ```Python hl_lines="7"
+ {!> ../docs_src/multiple_values/multiple_options/tutorial003_an.py!}
+ ```
+
+=== "Python 3.7+ non-Annotated"
+
+ !!! tip
+ Prefer to use the `Annotated` version if possible.
+
+ ```Python hl_lines="6"
+ {!> ../docs_src/multiple_values/multiple_options/tutorial003.py!}
+ ```
+
+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..a8dcfc6927
--- /dev/null
+++ b/docs_src/multiple_values/multiple_options/tutorial003.py
@@ -0,0 +1,11 @@
+from typing import List
+
+import typer
+
+
+def main(number: List[float] = typer.Option([], separator=",")):
+ print(f"The sum is {sum(number)}")
+
+
+if __name__ == "__main__":
+ typer.run(main)
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..d29e01c514
--- /dev/null
+++ b/docs_src/multiple_values/multiple_options/tutorial003_an.py
@@ -0,0 +1,12 @@
+from typing import List
+
+import typer
+from typing_extensions import Annotated
+
+
+def main(number: Annotated[List[float], typer.Option(separator=",")] = []):
+ print(f"The sum is {sum(number)}")
+
+
+if __name__ == "__main__":
+ typer.run(main)
diff --git a/pyproject.toml b/pyproject.toml
index c69e7c190c..ade8e654b4 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -177,6 +177,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 8c78520029..6e5bee2386 100644
--- a/tests/test_others.py
+++ b/tests/test_others.py
@@ -256,3 +256,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..638b80b4c5
--- /dev/null
+++ b/tests/test_tutorial/test_multiple_values/test_multiple_options/test_tutorial003.py
@@ -0,0 +1,44 @@
+import subprocess
+import sys
+
+import typer
+from typer.testing import CliRunner
+
+from docs_src.multiple_values.multiple_options import tutorial003 as mod
+
+runner = CliRunner()
+app = typer.Typer()
+app.command()(mod.main)
+
+
+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..45dd8c7242
--- /dev/null
+++ b/tests/test_tutorial/test_multiple_values/test_multiple_options/test_tutorial003_an.py
@@ -0,0 +1,44 @@
+import subprocess
+import sys
+
+import typer
+from typer.testing import CliRunner
+
+from docs_src.multiple_values.multiple_options import tutorial003 as mod
+
+runner = CliRunner()
+app = typer.Typer()
+app.command()(mod.main)
+
+
+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 d4ac56d0ba..e11ce98efc 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 31fece5a76..a6e8dadd0c 100644
--- a/typer/core.py
+++ b/typer/core.py
@@ -2,6 +2,7 @@
import inspect
import os
import sys
+import typing as t
from enum import Enum
from gettext import gettext as _
from typing import (
@@ -25,6 +26,7 @@
import click.shell_completion
import click.types
import click.utils
+from click import Context
if sys.version_info >= (3, 8):
from typing import Literal
@@ -419,6 +421,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,
@@ -449,6 +452,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 9db26975ca..cb5dda7738 100644
--- a/typer/main.py
+++ b/typer/main.py
@@ -34,7 +34,11 @@
Required,
TyperInfo,
)
-from .utils import get_params_from_function
+from .utils import (
+ SeparatorForNonListTypeError,
+ UnsupportedSeparatorError,
+ get_params_from_function,
+)
try:
import rich
@@ -884,6 +888,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
@@ -917,6 +933,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 9bbe2a36d2..271f8a38ad 100644
--- a/typer/models.py
+++ b/typer/models.py
@@ -331,6 +331,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,
@@ -386,6 +387,7 @@ def __init__(
self.flag_value = flag_value
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 710a4cf136..d44a6158ff 100644
--- a/typer/params.py
+++ b/typer/params.py
@@ -195,6 +195,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
@@ -250,6 +252,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 2ba7bace45..e114b273cf 100644
--- a/typer/utils.py
+++ b/typer/utils.py
@@ -190,3 +190,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}".'
+ )