@@ -461,7 +461,7 @@ def parse_command_line_template(
461461 return template , inputs , outputs
462462 executable , args_str = parts
463463 tokens = re .split (r"\s+" , args_str .strip ())
464- arg_pattern = r"<([:a-zA-Z0-9_,\|\-\.\/\+]+\? ?)>"
464+ arg_pattern = r"<([:a-zA-Z0-9_,\|\-\.\/\+]+(?:\?|=[^>]+) ?)>"
465465 opt_pattern = r"--?[a-zA-Z0-9_]+"
466466 arg_re = re .compile (arg_pattern )
467467 opt_re = re .compile (opt_pattern )
@@ -470,10 +470,8 @@ def parse_command_line_template(
470470 arguments = []
471471 option = None
472472
473- def add_arg (name , field_type , kwds , is_option = False ):
473+ def add_arg (name , field_type , kwds ):
474474 """Merge the typing information with an existing field if it exists"""
475- if is_option and kwds ["type" ] is not bool :
476- kwds ["type" ] |= None
477475 if issubclass (field_type , Out ):
478476 dct = outputs
479477 else :
@@ -497,7 +495,8 @@ def add_arg(name, field_type, kwds, is_option=False):
497495 for k , v in kwds .items ():
498496 setattr (field , k , v )
499497 dct [name ] = field
500- arguments .append (field )
498+ if issubclass (field_type , Arg ):
499+ arguments .append (field )
501500
502501 def from_type_str (type_str ) -> type :
503502 types = []
@@ -528,9 +527,14 @@ def from_type_str(type_str) -> type:
528527 for token in tokens :
529528 if match := arg_re .match (token ):
530529 name = match .group (1 )
530+ modify = False
531531 if name .startswith ("out|" ):
532532 name = name [4 :]
533533 field_type = outarg
534+ elif name .startswith ("modify|" ):
535+ name = name [7 :]
536+ field_type = arg
537+ modify = True
534538 else :
535539 field_type = arg
536540 # Identify type after ':' symbols
@@ -539,14 +543,22 @@ def from_type_str(type_str) -> type:
539543 optional = True
540544 else :
541545 optional = False
546+ kwds = {}
547+ if "=" in name :
548+ name , default = name .split ("=" )
549+ kwds ["default" ] = eval (default )
542550 if ":" in name :
543551 name , type_str = name .split (":" )
544552 type_ = from_type_str (type_str )
545553 else :
546554 type_ = generic .FsObject if option is None else str
547555 if optional :
548556 type_ |= None # Make the arguments optional
549- kwds = {"type" : type_ }
557+ kwds ["type" ] = type_
558+ if modify :
559+ kwds ["copy_mode" ] = generic .File .CopyMode .copy
560+ # Add field to outputs with the same name as the input
561+ add_arg (name , out , {"type" : type_ , "callable" : _InputPassThrough (name )})
550562 # If name contains a '.', treat it as a file template and strip it from the name
551563 if field_type is outarg :
552564 path_template = name
@@ -566,6 +578,7 @@ def from_type_str(type_str) -> type:
566578 kwds ["argstr" ] = option
567579 add_arg (name , field_type , kwds )
568580 option = None
581+
569582 elif match := bool_arg_re .match (token ):
570583 argstr , var = match .groups ()
571584 add_arg (var , arg , {"type" : bool , "argstr" : argstr , "default" : False })
@@ -626,3 +639,13 @@ def remaining_positions(
626639 f"Multiple fields have the overlapping positions: { multiple_positions } "
627640 )
628641 return [i for i in range (start , num_args ) if i not in positions ]
642+
643+
644+ @attrs .define
645+ class _InputPassThrough :
646+ """A class that can be used to pass through an input to the output"""
647+
648+ name : str
649+
650+ def __call__ (self , inputs : ShellSpec ) -> ty .Any :
651+ return getattr (inputs , self .name )
0 commit comments