1

I'm not a Windows guy, come from Linux side of things. My question is, how do I write a batch file that takes an optional switch as a boolean or value argument? For example:

ssh.cmd /X /l login remotehost

Where /X is a boolean switch meaning "turn on feature X" and /l is a "value switch" meaning that the next token on the command line is to be used in a special way. For example, in unix, you might do this:

ssh -X -l login remotehost

Here, -X is a boolean telling ssh to forward X ports, and -l is a "value switch" which tells ssh that my username is "login". remotehost is a mandatory argument. I know how to handle only the last one of these in command files, and cannot find the solution anywhere in Googlespace.

I'm thinking that the only way to do this is to painfully wade through the arguments looking for strings that fit the right format and say "aha! found a switch!" Is that correct?

Thanks in advance.

Rich
  • 926
  • 8
  • 17

2 Answers2

3

Here's another option that denies unrecognized switches, accepts both / and - as valid switch prefixes, performs case-insensitive switch checking, and pings remotehost to check for validity.

@echo off
setlocal enabledelayedexpansion

if not "%~1"=="" goto :switches

:usage
echo Usage: %~nx0 [/X] [/L loginname] remotehost
goto :EOF

:switches
rem // make sure variables are undefined
for %%I in (forward login host switch valid) do set "%%I="

rem // loop through all arguments
for %%I in (%*) do (

    rem // if this iteration is a value for the preceding switch
    if defined switch (

        rem // if that switch was -L or /L (case-insensitive)
        if /i "!switch:~1!"=="L" set "login=%%~I"

        rem // clear switch variable
        set "switch="

    ) else (

        rem // if this iteration is a switch
        echo(%%~I | >NUL findstr "^[-/]" && (

            set "switch=%%~I"

            rem // if this is a valid switch
            for %%x in (X L) do (
                if /i "!switch:~1!"=="%%x" set "valid=true"
            )

            if not defined valid (
                echo Unrecognized switch: %%~I
                goto usage
            )

            set "valid="

            if /i "!switch:~1!"=="X" (
                set "forward=true"
                set "switch="
            )
        ) || (

            rem // not a switch.  Must be a host.
            ping -n 1 %%~I | findstr /i "^Reply.*time.*[0-9]" >NUL && (
                set "host=%%~I"
            ) || (
                echo Host %%~I is unreachable.
            )
        )
    )
)

if not defined host goto usage

<NUL set /P "=Args passed: "
if defined forward <NUL set /P "=FORWARD "
if defined login <NUL set /P "=LOGIN %login% "
echo %host%

And you know, if you're not comfortable scripting Windows Batch scripts, you might consider another scripting language. Many will suggest PowerShell, and it is a worthwhile choice. But if you'd rather not learn an entire new language and format, if you're familiar with JavaScript, JScript is very similar. I'll demonstrate, and I hope you'll be able to see the similarities. Save this with a .js extension. To run it, do cscript /nologo scriptname.js switches here:

// convert WSH.Arguments object to a JS array
for (var args=[], i=0; i<WSH.Arguments.Length; i++)
    args.push(WSH.Arguments(i));

function error(txt, code) {
    WSH.StdErr.WriteLine(txt);
    WSH.StdErr.WriteLine('Usage: '+WSH.ScriptName+' [/X] [/L loginname] remotehost');
    WSH.Quit(code);
}

for (var i=0; i<args.length; i++) {
    switch (args[i].toLowerCase().replace("-","/")) {

        case "/x":
            var forward = true;
            break;

        case "/l":
            if (++i == args.length || /^\//.test(args[i]))
                error('Invalid login name.', 1);
            else var login = args[i];
            break;

        default:
            if (/^\//.test(args[i]))
                error('Unrecognized option: ' + args[i], 2);
            else var host = args[i];
    }
}

if (!host) error('No host specified.', 3);

WSH.Echo([
    'Command: ',
    (forward ? 'FORWARD ' : ''),
    (login ? 'LOGIN ' + login + ' ' : ''),
    host
].join(''));

JScript and JavaScript have lots in common. You can invoke the same RegExp object methods and Array object methods, modify prototypes, coerce data into compatible data types or test their truthiness, and so on. JScript is JavaScript, just with a root ancestor object of "WScript" (or "WSH" for the lazy) instead of "Window".

// convert WSH.Arguments proprietary object to a JS array
for (var args=[], i=0; i<WSH.Arguments.Length; i++)
    args.push(WSH.Arguments(i));

function error(txt, code) {
    WSH.StdErr.WriteLine(txt);
    WSH.StdErr.WriteLine('Usage: '+WSH.ScriptName+' [/X] [/L loginname] remotehost');
    WSH.Quit(code);
}

// returns idx of array element matching rxp (or -1 if no match)
Array.prototype.match = function(rxp) {
    for (var i=this.length; i--;) if (rxp.test(this[i])) return i;
    return -1;
}

var X = args.match(/^[/-]X$/i);
if (X > -1) var forward = args.splice(X, 1)[0];

var L = args.match(/^[/-]L$/i);
if (L > -1) var login = args.splice(L, 2)[1];

for (var invalid=[], i=args.length; i--;)
    if (/^[/-]/.test(args[i])) invalid.push(args[i]);

if (invalid.length) error('Unrecognized option: ' + invalid.join(), 1);
if (!args.length) error('No host specified.', 2);

WSH.Echo([
    'Params: ',
    (forward ? 'FORWARD ' : ''),
    (login ? 'LOGIN ' + login + ' ' : ''),
    args[0]
].join(''));

And if you want to use these arguments to launch an external program, look into objShell.Run.


If you want to deploy your script to other users, making it into a Batch + JScript hybrid format and saving it with a .bat extension makes it more user-friendly to execute. Just put this at the top of either of those solutions above, and save the script with a .bat extension.

@If (@CodeSection == @Batch) @then
@echo off & setlocal
cscript /nologo /e:JScript "%~f0" %*
goto :EOF
@end // end Batch / begin JScript hybrid chimera
// JScript code here

The way it works is that, first, the script loads with the cmd batch interpreter. Since (@CodeSection does not equal @Batch), the rest of the line is never attempted. Continue until the cscript line, which reloads this script using the JScript interpreter, passing the command line arguments along.

JScript determines that @CodeSection in fact does not equal @Batch, and skips to @end. At the end of the JScript execution, cscript.exe exits and the Batch thread resumes until goto :EOF or exit /b.

With the hybrid code added, now the syntax is scriptname.bat arguments here. If you want to bypass the batch code and execute only the JScript portion, cscript /nologo /e:JScript scriptname.bat argumentss here also works.

rojo
  • 24,000
  • 5
  • 55
  • 101
  • Thanks! I have to be able to rely on every user having the ability to run my little hacky script without installing any extra tools. Is jscript a standard DOS tool, such that i can give users a .jscript file and they can just run it without further ado? And if so, what versions of Windows support it? – Rich Jan 22 '16 at 16:00
  • JScript is a Windows Scripting Host language, like VB Script, but with ECMAScript methods and syntax. Every version of Windows supports it. There's a neat trick you can use to deploy the script for other users, making a Batch + JScript hybrid chimera. I'll edit my answer and explain. – rojo Jan 22 '16 at 16:08
  • Thanks, that's very interesting and useful information. – Rich Jan 22 '16 at 19:36
2
@echo off
setlocal EnableDelayedExpansion

rem Define the "value switches"
set "valSwitch=L M N"

rem Load parameters in "switch" and "argv" vectors
set "argc=0"

:nextParam
   set "param=%~1"
   if "%param:~0,1%" equ "/" (

      rem Is a switch
      for %%s in (%param:~1,1%) do (
         if "!valSwitch:%%s=!" equ "%valSwitch%" (
            rem Is a boolean switch
            set "switch[%%s]=True"
         ) else (
            rem Is a value switch
            set "switch[%%s]=%~2"
            shift
         )
      )

   ) else (

      rem Is a standard parameter
      set /A argc+=1
      set "argv[!argc!]=%~1"

   )

   shift
if "%~1" neq "" goto nextParam

SET SWITCH
SET ARGV

Output example:

C:\Users\Antonio\Test> test.bat /X /l login remotehost
switch[l]=login
switch[X]=True
argv[1]=remotehost

C:\Users\Antonio\Test> test.bat /X remotehost /l login
switch[l]=login
switch[X]=True
argv[1]=remotehost

C:\Users\Antonio\Test> test.bat remotehost /l login /X
switch[l]=login
switch[X]=True
argv[1]=remotehost

C:\Users\Antonio\Test> test.bat /Y /A /M "login1 login2" remotehost "last parameter"
switch[A]=True
switch[M]=login1 login2
switch[Y]=True
argv[1]=remotehost
argv[2]=last parameter
Aacini
  • 65,180
  • 12
  • 72
  • 108
  • Thanks, this is a perfectly acceptable solution, I just can only accept one at a time apparently! :-) Cheers. – Rich Jan 22 '16 at 16:00
  • Yes, but you may uncheck your former selection in order to make a new one, if you want! **`;)`** – Aacini Jan 22 '16 at 17:09
  • Sadly, you were second on the scene, and you have to admit rojo is giving quite a complete and useful answer. Thanks though! – Rich Jan 22 '16 at 19:36
  • And of course, feel free to mark my question as being interesting. LOL, from one point-slut to another. – Rich Jan 22 '16 at 19:37