11from __future__ import annotations
22
3+ import argparse
34import logging
4- import re
55
66from contextlib import suppress
77from importlib import import_module
1616from cleo .exceptions import CleoCommandNotFoundError
1717from cleo .exceptions import CleoError
1818from cleo .formatters .style import Style
19+ from cleo .io .inputs .argv_input import ArgvInput
1920
2021from poetry .__version__ import __version__
2122from poetry .console .command_loader import CommandLoader
2627
2728if TYPE_CHECKING :
2829 from collections .abc import Callable
29- from typing import Any
3030
3131 from cleo .events .event import Event
32- from cleo .io .inputs .argv_input import ArgvInput
3332 from cleo .io .inputs .definition import Definition
3433 from cleo .io .inputs .input import Input
3534 from cleo .io .io import IO
@@ -243,7 +242,7 @@ def _run(self, io: IO) -> int:
243242 # to ensure the users are not exposed to a stack trace for providing invalid values to
244243 # the options --directory or --project, configuring the options here allow cleo to trap and
245244 # display the error cleanly unless the user uses verbose or debug
246- self ._configure_custom_application_options (io )
245+ self ._configure_global_options (io )
247246
248247 self ._load_plugins (io )
249248
@@ -265,40 +264,29 @@ def _run(self, io: IO) -> int:
265264
266265 return exit_code
267266
268- def _option_get_value (self , io : IO , name : str , default : Any ) -> Any :
269- option = self .definition .option (name )
267+ def _configure_global_options (self , io : IO ) -> None :
268+ """
269+ Configures global options for the application by setting up the relevant
270+ directories, disabling plugins or cache, and managing the working and
271+ project directories. This method ensures that all directories are valid
272+ paths and handles the resolution of the project directory relative to the
273+ working directory if necessary.
270274
271- if option is None :
272- return default
275+ :param io: The IO instance whose input and options are being read.
276+ :return: Nothing.
277+ """
278+ self ._sort_global_options (io )
273279
274- values = [f"--{ option .name } " ]
275-
276- if option .shortcut :
277- values .append (f"-{ option .shortcut } " )
278-
279- if not io .input .has_parameter_option (values ):
280- return default
281-
282- if option .is_flag ():
283- return True
284-
285- return io .input .parameter_option (values = values , default = default )
286-
287- def _configure_custom_application_options (self , io : IO ) -> None :
288- self ._disable_plugins = self ._option_get_value (
289- io , "no-plugins" , self ._disable_plugins
290- )
291- self ._disable_cache = self ._option_get_value (
292- io , "no-cache" , self ._disable_cache
293- )
280+ self ._disable_plugins = io .input .option ("no-plugins" )
281+ self ._disable_cache = io .input .option ("no-cache" )
294282
295283 # we use ensure_path for the directories to make sure these are valid paths
296284 # this will raise an exception if the path is invalid
297285 self ._working_directory = ensure_path (
298- self . _option_get_value ( io , "directory" , Path .cwd () ), is_directory = True
286+ io . input . option ( "directory" ) or Path .cwd (), is_directory = True
299287 )
300288
301- self ._project_directory = self . _option_get_value ( io , "project" , None )
289+ self ._project_directory = io . input . option ( "project" )
302290 if self ._project_directory is not None :
303291 self ._project_directory = Path (self ._project_directory )
304292 self ._project_directory = ensure_path (
@@ -310,40 +298,151 @@ def _configure_custom_application_options(self, io: IO) -> None:
310298 is_directory = True ,
311299 )
312300
313- def _configure_io (self , io : IO ) -> None :
314- # We need to check if the command being run
315- # is the "run" command.
316- definition = self .definition
301+ def _sort_global_options (self , io : IO ) -> None :
302+ """
303+ Sorts global options of the provided IO instance according to the
304+ definition of the available options, reordering and parsing arguments
305+ to ensure consistency in input handling.
306+
307+ The function interprets the options and their corresponding values
308+ using an argument parser, constructs a sorted list of tokens, and
309+ recreates the input with the rearranged sequence while maintaining
310+ compatibility with the initially provided input stream.
311+
312+ If using in conjunction with `_configure_run_command`, it is recommended that
313+ it be called first in order to correctly handling cases like
314+ `poetry run -V python -V`.
315+
316+ :param io: The IO instance whose input and options are being processed
317+ and reordered.
318+ :return: Nothing.
319+ """
320+ original_input = cast (ArgvInput , io .input )
321+ tokens : list [str ] = original_input ._tokens
322+
323+ parser = argparse .ArgumentParser (add_help = False )
324+
325+ for option in self .definition .options :
326+ parser .add_argument (
327+ f"--{ option .name } " ,
328+ * ([f"-{ option .shortcut } " ] if option .shortcut else []),
329+ action = "store_true" if option .is_flag () else "store" ,
330+ )
331+
332+ args , remaining_args = parser .parse_known_args (tokens )
333+
334+ tokens = []
335+ for option in self .definition .options :
336+ key = option .name .replace ("-" , "_" )
337+ value = getattr (args , key , None )
338+
339+ if value is not None :
340+ if value : # is truthy
341+ tokens .append (f"--{ option .name } " )
342+
343+ if option .accepts_value ():
344+ tokens .append (str (value ))
345+
346+ sorted_input = ArgvInput ([self ._name or "" , * tokens , * remaining_args ])
347+ sorted_input .set_stream (original_input .stream )
348+
349+ with suppress (CleoError ):
350+ sorted_input .bind (self .definition )
351+
352+ io .set_input (sorted_input )
353+
354+ def _configure_run_command (self , io : IO ) -> None :
355+ """
356+ Configures the input for the "run" command to properly handle cases where the user
357+ executes commands such as "poetry run -- <subcommand>". This involves reorganizing
358+ input tokens to ensure correct parsing and execution of the run command.
359+ """
317360 with suppress (CleoError ):
318- io .input .bind (definition )
319-
320- name = io .input .first_argument
321- if name == "run" :
322- from poetry .console .io .inputs .run_argv_input import RunArgvInput
323-
324- input = cast ("ArgvInput" , io .input )
325- run_input = RunArgvInput ([self ._name or "" , * input ._tokens ])
326- # For the run command reset the definition
327- # with only the set options (i.e. the options given before the command)
328- for option_name , value in input .options .items ():
329- if value :
330- option = definition .option (option_name )
331- run_input .add_parameter_option ("--" + option .name )
332- if option .shortcut :
333- shortcuts = re .split (r"\|-?" , option .shortcut .lstrip ("-" ))
334- shortcuts = [s for s in shortcuts if s ]
335- for shortcut in shortcuts :
336- run_input .add_parameter_option ("-" + shortcut .lstrip ("-" ))
361+ io .input .bind (self .definition )
362+
363+ command_name = io .input .first_argument
364+
365+ if command_name == "run" :
366+ original_input = cast (ArgvInput , io .input )
367+ tokens : list [str ] = original_input ._tokens
368+
369+ if "--" in tokens :
370+ # this means the user has done the right thing and used "poetry run -- echo hello"
371+ # in this case there is not much we need to do, we can skip the rest
372+ return
373+
374+ # find the correct command index, in some cases this might not be first occurrence
375+ # eg: poetry -C run run echo
376+ command_index = tokens .index (command_name )
377+
378+ while command_index < (len (tokens ) - 1 ):
379+ try :
380+ # try parsing the tokens so far
381+ _ = ArgvInput (
382+ [self ._name or "" , * tokens [: command_index + 1 ]],
383+ definition = self .definition ,
384+ )
385+ break
386+ except CleoError :
387+ # parsing failed, try finding the next "run" token
388+ try :
389+ command_index += (
390+ tokens [command_index + 1 :].index (command_name ) + 1
391+ )
392+ except ValueError :
393+ command_index = len (tokens )
394+ else :
395+ # looks like we reached the end of the road, let clea deal with it
396+ return
397+
398+ # fetch tokens after the "run" command
399+ tokens_without_command = tokens [command_index + 1 :]
400+
401+ # we create a new input for parsing the subcommand pretending
402+ # it is poetry command
403+ without_command = ArgvInput (
404+ [self ._name or "" , * tokens_without_command ], None
405+ )
337406
338407 with suppress (CleoError ):
339- run_input .bind (definition )
408+ # we want to bind the definition here so that cleo knows what should be
409+ # parsed, and how
410+ without_command .bind (self .definition )
411+
412+ # the first argument here is the subcommand
413+ subcommand = without_command .first_argument
414+ subcommand_index = (
415+ (tokens_without_command .index (subcommand ) if subcommand else 0 )
416+ + command_index
417+ + 1
418+ )
419+
420+ # recreate the original input reordering in the following order
421+ # - all tokens before "run" command
422+ # - all tokens after "run" command but before the subcommand
423+ # - the "run" command token
424+ # - the "--" token to normalise the form
425+ # - all remaining tokens starting with the subcommand
426+ run_input = ArgvInput (
427+ [
428+ self ._name or "" ,
429+ * tokens [:command_index ],
430+ * tokens [command_index + 1 : subcommand_index ],
431+ command_name ,
432+ "--" ,
433+ * tokens [subcommand_index :],
434+ ]
435+ )
436+ run_input .set_stream (original_input .stream )
340437
341- for option_name , value in input .options .items ():
342- if value :
343- run_input .set_option (option_name , value )
438+ with suppress (CleoError ):
439+ run_input .bind (self .definition )
344440
441+ # reset the input to our constructed form
345442 io .set_input (run_input )
346443
444+ def _configure_io (self , io : IO ) -> None :
445+ self ._configure_run_command (io )
347446 super ()._configure_io (io )
348447
349448 def register_command_loggers (
0 commit comments