1. This site uses cookies. By continuing to use this site, you are agreeing to our use of cookies. Learn More.

FUNCTION with variable number of arguments

Discussion in 'Support' started by nikbackm, Apr 22, 2013.

  1. nikbackm

    Joined:
    May 30, 2008
    Messages:
    194
    Likes Received:
    1
    Can you define a function using FUNCTION that takes a variable number of arguments and then in the function definition process all of these arguments in some way?

    I'd like to define functions like these:

    - Will return all params starting with "/" and ignore the rest.
    @switchargs[params]

    - Will return all params NOT starting with "/" and ignore the rest.
    @actionargs[params]

    Example use:

    echo %@switchargs[/n /e a1 a2 a3]
    /n /e

    echo %@actionargs[/n /e /y a1 /x a2]
    a1 a2

    Can that be done using a FUNCTION defined function?

    I can't find an obvious way to loop over the arguments at least, and it does not seem as if TCC offers any filter-like function for the purpose.
     
  2. JohnQSmith

    Joined:
    Jan 19, 2011
    Messages:
    559
    Likes Received:
    7
    Here's what I've come up with. I haven't been able to get the ECHO part to work right as it keeps wanting to echo the first part of the function rather than the final return value even though I have the whole thing grouped.

    Code:
    function switchargs=`(unset /q _switchargs %+ do ii = 0 to %@eval[%#-1] (iff "%@left[1,%@field[%ii,%$]]" EQ "/" then set _switchargs=%@trim[%_switchargs %@field[%ii,%$]]) %+ %_switchargs)`
    function actionargs=`(unset /q _actionargs %+ do ii = 0 to %@eval[%#-1] (iff "%@left[1,%@field[%ii,%$]]" NE "/" then set _actionargs=%@trim[%_actionargs %@field[%ii,%$]]) %+ %_actionargs)`
    Edit: Taking out the lines in the following doesn't work right since it keeps appending to the variable.
    If you change them by taking out the first and third commands of each group, it works a little better, but still not right.
     
  3. samintz

    samintz Scott Mintz

    Joined:
    May 20, 2008
    Messages:
    1,190
    Likes Received:
    11
    This is what I came up with:
    Code:
    function switchargs=`%@execstr[do i in /l "%$" (if not %%@index[%%@unquote[%%i],/] == -1 echos ` `%%@unquote[%%i])]`
    function actionargs=`%@execstr[do i in /l "%$" (if %%@index[%%@unquote[%%i],/] == -1 echos ` `%%@unquote[%%i])]`
     
    echo %@switchargs[/n /e a1 a2 a3]
     /n /e
     
    echo %@actionargs[/n /e a1 a2 a3]
     a1 a2 a3
    
     
  4. Steve Fabian

    Joined:
    May 20, 2008
    Messages:
    3,520
    Likes Received:
    4
    I think the issue is you mix commands and strings. This is the skeleton I use in many of my functions:

    function switchargs=`%@execstr[( commands_to_build_string %+ echo %string)]`

    It performs the action, and explicitly echoes the result string. Within @EXECSTR the grouping is still needed. Beware! The commands building the string should NOT have any output to stdout, otherwise the first of those lines would be returned as the value.

    BTW, @TRIM is not needed:
    1/ you know string to be appended has no leading space
    2/ SET truncates trailing spaces (unless explicitly protected, a COMMAND.COM legacy)
     
  5. JohnQSmith

    Joined:
    Jan 19, 2011
    Messages:
    559
    Likes Received:
    7
    Thanks for the pointers. I'm going to have to check out @EXECSTR and see what unfolds. Good info on the @TRIM and the trailing spaces too.

    Edit:
    Here are the modified functions (with Steve's helpful suggestions). After they run, the results are stored in variables _switchargs and _actionargs, respectively, for future use, if needed.
    Code:
    switchargs=%@execstr[do ii = 0 to %@eval[%#-1] (iff "%@left[1,%@field[%ii,%$]]" EQ "/" then set _switchargs=%_switchargs %@field[%ii,%$]) %+ echo %_switchargs]
    actionargs=%@execstr[do ii = 0 to %@eval[%#-1] (iff "%@left[1,%@field[%ii,%$]]" NE "/" then set _actionargs=%_actionargs %@field[%ii,%$]) %+ echo %_actionargs]
    Edit 2:
    Steve, after I reread your post, could you explain this?
     
  6. vefatica

    Joined:
    May 20, 2008
    Messages:
    7,952
    Likes Received:
    30
    Just as a starting point, I tried this simple one:
    Code:
    function switchargs `%@execstr[do a in /l %$ ( echo %a )]`
    And I couldn't get anything out of it.
    Code:
    v:\> echo %@switchargs[a /b /c]
    ECHO is OFF
     
  7. JohnQSmith

    Joined:
    Jan 19, 2011
    Messages:
    559
    Likes Received:
    7
    Even more interesting...
    Code:
    v:\> %@switchargs[a /b /c]
    is OFF
    Which looks like it's actually running and returning the command "echo ECHO is OFF". So without the initial echo, as in your example, it's running the command "ECHO is OFF" which returns "is OFF".
     
  8. vefatica

    Joined:
    May 20, 2008
    Messages:
    7,952
    Likes Received:
    30
    Oops! That was my select/copy mistake. It really said "ECHO is OFF" but I block-selected (in the console) from the right of the prompt. My real question was "Why doesn't %a have a value to be echoed?".
     
  9. vefatica

    Joined:
    May 20, 2008
    Messages:
    7,952
    Likes Received:
    30
    There must be something wrong somewhere. This doesn't make sense:
    Code:
    v:\> function switchargs `%@execstr[do a=1 to %# ( echo %a )]`
     
    v:\> echo %@switchargs[a /b /c]
    ECHO is OFF
     
    v:\> function switchargs `%@execstr[do a in /l "%$" ( echo %a )]`
     
    v:\> echo %@switchargs[a /b /c]
    4
     
    v:\> echo %@switchargs[a /b /c]
    ECHO is OFF
    Why doesn't the first ECHO have anything to echo?
    Why does the second ECHO produce "4"?
    Why doesn't the third ECHO prodice the same thing as the one before it?
     
  10. vefatica

    Joined:
    May 20, 2008
    Messages:
    7,952
    Likes Received:
    30
    I can answer my last two questions ... because the envvar a was previously set to 4. So it's back to the first question. Here's a slightly simplified version. What's the difference below?
    Code:
    v:\> do a in /l a /b /c ( echo %a )
    a
    /b
    /c
     
    v:\> echo %@execstr[do a in /l a /b /c ( echo %a )]
    ECHO is OFF
    ... answer ... because the %a is evaluated before the DO loop (makes some sense because EXECSTR doesn't know there's a DO inside it).
     
  11. nikbackm

    Joined:
    May 30, 2008
    Messages:
    194
    Likes Received:
    1
    Thanks for the replies everyone!

    The solution using @exestr[] is succinct and also generalizable to many other function definitions.

    It has one big drawback however, it has to (by necessity?) spawn a new instance of TCC to execute the function.
    This does not matter much for one-off functions called only once or a few times, but if you want to make general-purpose functions that might be called a large number of times (e.g. for each line when processing a file) then the cost of creating a new TCC process will quickly add up.

    For example, on my system running the function @switchargs by samintz once took 20-30 msecs, but running it a 100 times took almost 3 seconds. In comparison, running the body of the function inlined instead only took 10-20 msecs for a 100 times. (See test.btm)

    Might there be another way to define functions like these (that can in some way "loop" over the input argument(s)) but does not use @execstr? I have looked around in the Functions section of the help file but nothing really sticks out.

    Would also work if you could somehow make @execstr execute the function body in the current TCC instance without extra overhead. Such an optimization could only be applied if it does not call any external programs of course. But this seems like a non-trivial extension to TCC and not too likely to happen.
     

    Attached Files:

    • test.btm
      File size:
      486 bytes
      Views:
      3
  12. samintz

    samintz Scott Mintz

    Joined:
    May 20, 2008
    Messages:
    1,190
    Likes Received:
    11

    Vince, I found I had to double the percent's within the parentheses. I also had to quote %$ because when I specified a command of "/n /e a1 a2 a3" TCC hung.

    If you type echo %@switchargs[a /b /c] and then press Ctrl+X, the command expands and you can see why/where the %'s get evaluated.
     
  13. samintz

    samintz Scott Mintz

    Joined:
    May 20, 2008
    Messages:
    1,190
    Likes Received:
    11

    You can just put the do loop directly within your script.

    switchargs.btm:
    Code:
    do l in /l %*
    if not %@index[%l,/] == -1 echos ` `%l
    enddo
     
  14. samintz

    samintz Scott Mintz

    Joined:
    May 20, 2008
    Messages:
    1,190
    Likes Received:
    11
    I assume that ultimately, you want to parse your command line. You can use a SWITCH statement along with the DO loop.
    Code:
    set NoExec=0
    set EchoOn=0
     
    do i in /l %*
      iff not %@index[%i,/] == -1 then
        switch %i
        case /n
          set NoExec=1
        case /e
          set EchoOn=1
        default
          echo usage: foo [/n] [/e]
        endswitch
      else
        rem handle non-switched arguments
      endiff
    enddo
    
     
  15. Fross

    Joined:
    May 30, 2008
    Messages:
    223
    Likes Received:
    1
    I'm not sure if your example is just an example, but if you are interested in parsing the command line, I posted a GetOpt script some time ago that I still use on a daily basis.

    If you use it, please let me know if you have any questions or ideas to enhance. The documentation is at the top of the batch file.

    https://bitbucket.org/frossm/getopt.btm/downloads

    Regards,

    Fross
     
  16. rconn

    rconn Administrator
    Staff Member

    Joined:
    May 14, 2008
    Messages:
    9,859
    Likes Received:
    83
    @EXECSTR does not spawn a new instance of TCC, unless you pass a negative index (to read from the end of the output). In that case, it pipes the output to another instance of TCC.

    Otherwise, @EXECSTR redirects the output of the command to a file, and then reads the specified line from that file.
     
  17. nikbackm

    Joined:
    May 30, 2008
    Messages:
    194
    Likes Received:
    1
    Of course, and that is what I do as well most of the time.

    But being able to re-use chunks of code without performance suffering noticeably would be neat.

    Functions are simple to understand and call which makes them ideal to use in e.g. ALIAS:es. You generally don't want to include too many lines of code in those.

    It served as both an example for defining functions and an actual use-case.

    I am interested in techniques to build powerful functions -- those that TCC already offers but which I don't know about, and possibly getting enhancements in future TCC releases -- but it was this specific example that got me thinking about it.

    My intended use-case was making some ALIAS:es to call a batch file. The ALIAS:es would provide fixed arguments for calling a batch-file and would also allow the caller of the alias to give further arguments. The idea was to use @switchargs to make sure that all switch arguments was passed before the actual/data arguments (requirement of the specific batch-file).

    So GETOPT.BTM will not help with that, but it still looks interesting for other purposes. Would make sure all your batch-files uses consistent argument definitions if nothing else. Thanks for the tip!
     
  18. nikbackm

    Joined:
    May 30, 2008
    Messages:
    194
    Likes Received:
    1
    Oops. That's true, I confused this case with the one where you have a pipe with an internal command on the right side (I assume that will still need a new TCC process to be spawned).

    In that case it seems as if perhaps it should be possible to improve the performance of @execstr.

    Or it seems to me at least that this operation

    Code:
    10:15 (2013-04-25) C:\User>*timer on & *do 100 (*echo %@execstr[*echo 1] > NUL) & *timer off
    Timer 1 on: 10:15:39
    Timer 1 off: 10:15:41  Elapsed: 0:00:02,71
    

    is quite slow in comparison to these that also involve similar (or more) file I/O.

    Code:
    10:16 (2013-04-25) C:\User>*timer on & *do 100 (*echo 1 >> testfile.txt) & *timer off
    Timer 1 on: 10:16:12
    Timer 1 off: 10:16:12  Elapsed: 0:00:00,08
     
    10:16 (2013-04-25) C:\User>*timer on & do 100 (dir > NUL) & *timer off
    Timer 1 on: 10:16:55
    Timer 1 off: 10:16:55  Elapsed: 0:00:00,23
     
    10:16 (2013-04-25) C:\User>*timer on & do 100 (dir > testfile2.txt) & *timer off
    Timer 1 on: 10:17:17
    Timer 1 off: 10:17:17  Elapsed: 0:00:00,43
    

    Perhaps the difference is that the temp-file used by @execstr is also read from later, and this requires(?) it to be flushed to disk first. Might not be much to do in that case.

    Hm, and I assume @execstr is also using a different (uniquely named) temp file each time, which will take some time to setup, as opposed to the other tests which use the same file for each DO iteration.

    Edit: Yes, that is indeed the issue.

    Code:
    10:41 (2013-04-25) C:\Users\backnikl\AppData\Local\Temp>timer on & do 100 (echo %@unique[] > NUL) & timer off
    Timer 1 on: 10:42:43
    Timer 1 off: 10:42:46  Elapsed: 0:00:02,56
    
    So not much room for improvement then unless @execstr was more drastically changed. (maybe it could take the tempfile path as an optional argument? :) Or not. )
     

Share This Page