Welcome!

By registering with us, you'll be able to discuss, share and private message with other members of our community.

SignUp Now!

FUNCTION with variable number of arguments

May
238
2
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.
 
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.
 
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
 
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)
 
I think the issue is you mix commands and strings.
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?
Within @EXECSTR the grouping is still needed.
 
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
 
Code:
function switchargs `%@execstr[do a in /l %$ ( echo %a )]`
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".
 
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?".
 
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?
 
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).
 
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.
 

Attachments

  • test.btm
    486 bytes · Views: 270
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


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.
 
Thanks for the replies everyone!

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.


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
 
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
 
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
 
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.

@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.
 
You can just put the do loop directly within your script.

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.

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

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!
 
@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.

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. )
 

Similar threads

Back
Top