/*~********************************************************************

File:	CmdLine.js, v1.1

A working demo of a J(ava)Script Command-Line parser

Charles Harrison, April 2009

***********************************************************************

Charles Harrison's Work is licensed under a Creative Commons Licence -
Attribution-Non-Commercial-No Derivative Works 2.0 UK: England & Wales
http://creativecommons.org/licenses/by-nc-nd/2.0/uk/

For further details, please see:
http://www.macfh.co.uk/JavaJive/JavaJive.html

***********************************************************************

This script is a useful set of objects to solve a common problem  -
parsing the command-line passed to console run scripts.

However it is also a working demo of object inheritance in JavaScript,
based on the techniques explained in an excellent tutorial:
http://www.cs.rit.edu/~atk/JavaScript/manuals/jsobj/

Although it is not intended to explain object inheritance here, as a
non-trivial working implementation of it, it may prove a useful guide.

***********************************************************************

This script expects to be run by Windows Scripting Host, CScript.exe
This may not be installed by default on a Windows machine.  To install
WSH (you will likely need administrator rights), see ...
http://www.microsoft.com/downloads/details.aspx?FamilyId=C717D943-7E4B-4622-86EB-95A22B832CAA

For other OSs or interpreters, search for comments which begin ...
Non WSH use:
... currently there are 6, and make appropriate alterations.

(For best layout Tabs expected every 4 columns)

***********************************************************************

Updates:
13/06/2010	Safer Array for() loops if the Array.prototype is altered.

**********************************************************************/

//	Non WSH use: delete or change appropriately these 5 lines
//	Create normal WSH support objects for the wider purpose of the program
//	var	wshNetwork	= new ActiveXObject( "WScript.Network" );
//	var	wshShell	= new ActiveXObject( "WScript.Shell" );
	var	wshFileSys	= new ActiveXObject( "Scripting.FileSystemObject" );
	var interpreter	= WScript.FullName;			//	The name of the script interpreter
	var thisScript	= WScript.ScriptName;		//	The name of the script

//	Create the required command-line object heirarchy and debug variable
	initObjects();
	var	debug		= false;

//	Create some command-line parameter objects
//	(there is at least one of each recognised type here)
	var	keyPar		= new KeyPar	( "K", "-K\tAn example of a KeyPar to switch (make NOT) a Boolean" );
	var	keyLstPar	= new KeyLstPar	( "L", "-L:<l>\tAn example of a KeyLstPar to choose from a list, here days of the week", ["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"] );
	var	keyTogPar	= new KeyTogPar	( "T", "-T[+-]\tAn example of a KeyTogPar to switch or set a Boolean" );
	var	keyStrPar	= new KeyStrPar	( "S", "-S:<s>\tAn example of a KeyStrPar to set a String" );
//	This example demonstrates the use of a listener to alter the behaviour of a subsequent command-line parameter
	var	signPar		= new KeyPar	( "B", "-B\tA KeyPar which sets the following KeyIntPar to expect a signed byte" );
	signPar.addListen( signChange );
	var	keyIntPar	= new KeyIntPar	( "I", 0, 255, 0, "-I:<n>\tAn example of a KeyIntPar, here setting an unsigned byte" );
	var	keyFltPar	= new KeyFltPar	( "F", null, null, null, "-F:<n>\tAn example of a KeyFltPar setting a floating point number" );
	var	hlpPar		= new KeyPar	( "{0,2}(\\?|H|Help)?", "?\tShow Help (a more flexible example of a KeyPar)", null, null, null, KeyPar.prototype.tags + "?" );
	var	filPar		= new FileSpecPar( "<file>\tAn example of a FileSpecPar to set a file name (required)", true );

//	Create the command-line object
	var	cmdLine		= new CmdLine(
									[ keyPar, keyStrPar, keyLstPar, keyTogPar, signPar, keyIntPar, keyFltPar, hlpPar, /* Put this type last */ filPar ],
									true, null, null,
									"A simple program to demonstrate command-line parsing.\n"
								);
//	Non WSH use: change appropriately WScript.Arguments
//	Scan/parse the command-line
	if( cmdLine.scan(WScript.Arguments) && !hlpPar.getScand() )
		//	Set your program values from the command-line and do the work here
		log( "Command-line scanned successfully!" );
	else
		//	Show a suitable error message and quit
		log	(
			cmdLine.getHelp()
				+ (cmdLine.getGood() != "" ? "\nCommand-line successfully scanned:\n" + cmdLine.getGood() : "")
				+ (cmdLine.getBad() != "" ? "\nCommand-line not scanned:\n" + cmdLine.getBad() : "")
			);

//	Non WSH use: change appropriately WScript.Quit
//	Quit
	WScript.Quit();

function signChange( nValue, oValue )
	{
	keyIntPar.lower = nValue ? -128 : 0;
	keyIntPar.upper = nValue ? 127 : 255;
	}

//	Non WSH use: change appropriately wshFileSys.GetFileName
//	Initialise the command-line object heirarchy
//
//	Note:	MUST be called before creating any *Par (parameter) or CmdLine (command-line) objects.
//
function initObjects()
	{
	FileSpecPar.prototype	= new Param;
	KeyPar.prototype		= new Param;
	KeyPar.prototype.tags	= "-";				//	Windows users may prefer "-/"
	KeyPar.prototype.cases	= false;
	KeyStrPar.prototype		= new KeyPar;
	KeyLstPar.prototype		= new KeyStrPar;
	KeyTogPar.prototype		= new KeyStrPar;
	KeyTogPar.prototype.vals = "-+01FT";
	KeyIntPar.prototype		= new KeyStrPar;
	KeyFltPar.prototype		= new KeyIntPar;
	CmdLine.prototype.tTitl	= thisScript;
	CmdLine.prototype.tSyn	= "Syntax:\n" + wshFileSys.GetFileName(interpreter) + " " + thisScript + " <parameter(s)>\n";
	CmdLine.prototype.tErr	= "Command-Line Error - Required parameter(s) missing or wrong syntax or value:\n";
	CmdLine.prototype.tUndl = "============================================================================";
	CmdLine.prototype.tPars = "Parameters:";
	}

//	A set of command-line parameters
//
//	Parameters:
//
//		aPars	An array of objects each derived from Param and holding one recognised command-line parameter
//		bHelp	Boolean for whether to give help when errors are encountered scanning script arguments
//		sHelp	Either: A full (console) page of help for the user constructed by the programmer ...
//		... 	Or:		Null, in which case console help will be constructed from the parameters' help and:
//		STitle	Program title
//		sDesc	Program description
//		sSyntax	A line describing the command-line syntax
//		sEg		An example command line
//		sPars	An optional header for the list of help-lines for the individual command-line parameters
//
function CmdLine( aPars, bHelp, sHelp, sTitle, sDesc, sSyntax, sEg, sPars )
	{
	this.pars		= aPars ? aPars : null;
	this.bHelpful	= bHelp ? bHelp : false;
	this.help		= sHelp ? sHelp : null;
	if(	sTitle )
		this.tTitl	= sTitle;
	this.desc		= sDesc ? sDesc : null;
	if(	sSyntax )
		this.tSyn	= sSyntax;
	this.eg			= sEg;
	if(	sPars )
		this.tPars	= sPars;
	this.goodScans	= "";
	this.badScans	= "";
	this.getGood	= gGood;
	this.getBad		= gBad;
	this.scan		= scanCmdLine;
	this.getHelp	= fHelp;
	}

function scanCmdLine( args )
	{
	var	result	= true;
	var	subResult;
	var	msg		= this.tErr;
	for( var a = 0, A = args.length; a < A; a++ )
		{
//	Non WSH use: change appropriately to args[a]
		thisArg	= args(a);
		subResult = false;
		for( var p = 0, P = this.pars.length; p < P; p++ )
			{
			if( debug )
				log( "Parameter " + p + ": " + (this.pars[p].getHelp() ? this.pars[p].getHelp() : "") );
			if( this.pars[p].scan( thisArg ) )
				{
				subResult = true;
				this.goodScans += (this.goodScans != "" ? " " : "") + thisArg;
				if( debug )
					log( "Argument $" + a  + " " + thisArg + " scanned as " + this.pars[p].getValue() );
				break;
				}
			}
		if( (!subResult) && (this.bHelpful || debug) )
			{
			this.badScans += (this.badScans != "" ? " " : "") + thisArg;
			if( debug )
				log( "Argument $" + (a+1)  + " " + thisArg + " not scanned" );
			}
		result &= subResult;
		}
	subResult = true;
	for( p = 0, P = this.pars.length; p < P; p++ )
		if( this.pars[p].getReqd() && !this.pars[p].getScand() )
			{
			msg += this.pars[p].getHelp() + "\n";
			subResult = false;
			}
	if( (!subResult) && (this.bHelpful || debug) )
		log( msg + "\n" );
	result &= subResult;
	return result;
	}

function fHelp()
	{
	var	result = null;
	if( this.help != null )
		result = this.help;
	else
		{
		result = ( this.tTitl ? this.tTitl + "\n" + this.tUndl.substr(0, this.tTitl.length) + "\n\n" : "" )
					+ ( this.desc ? this.desc + "\n" : "" )
					+ ( this.tSyn ? this.tSyn + "\n" : "" )
					+ ( this.eg ? this.eg + "\n" : "" )
					+ ( this.tPars && this.pars && this.pars.length ? this.tPars + "\n" : "" );
		for( var p = 0, P = this.pars.length; p < P; p++ )
			if( this.pars[p].getHelp() )
				result += this.pars[p].getHelp() + "\n";
		}
	return result;
	}

function gGood()
	{
	return this.goodScans;
	}

function gBad()
	{
	return this.badScans;
	}

//	Base object for all command-line parameters
//
//	Parameters:
//
//		sHelp	Help text (preferably < 79 characters) to prompt user
//		bReqd	Boolean for whether the parameter must be present, default is false
//		oValue	The initial value, defaults to null
//
function Param( sHelp, bReqd, oValue )
	{
	this.help		= sHelp ? sHelp : null;
	this.reqd		= bReqd ? true : false;
	this.scand		= false;
	this.value		= oValue ? oValue : null;
	this.getHelp	= gHelp;
	this.getReqd	= gReqd;
	this.getScand	= gScand;
	this.getValue	= gVal;
	this.setValue	= sVal;
	this.addListen	= aList;
	this.delListen	= dList;
	this.listeners	= new Array(0);
	}

function gHelp()
	{
	return this.help;
	}

function gReqd()
	{
	return this.reqd;
	}

function gScand()
	{
	return this.scand;
	}

function gVal()
	{
	return this.value;
	}

function sVal( nValue )
	{
	var	oValue = this.value;
	this.value = nValue;
	if( nValue != oValue )
		for( var l = 0, L = this.listeners.length; l < L; l++ )
			this.listeners[l]( nValue, oValue );
	return oValue;
	}

function aList( fListener )
	{
	this.listeners.push( fListener );
	}

function dList( fListener )
	{
	for( var l = this.listeners.length - 1; l >= 0; l-- )
		if( this.listeners[l] == fListener )
			this.listeners.splice( l , 1 );
	}

//	A file spec command-line parameter object
//
//	Format:	<filespec>
//
//	Note:	Untagged/unkeyed string parameters such as FileSpecPars must follow all other parameter types
//			in the aPars array passed to the CmdLine constructor.
//
//	Parameters:
//
//		sHelp	Help text (preferably < 79 characters) to prompt user
//		bReqd	Boolean for whether the parameter must be present, default is false
//		sValue	The initial value, defaults to null
//		bStrict	Whether the parser should perform strict scanning, default is false
//				(would accept legal filenames that look like other parameters, such as -A)
//
function FileSpecPar( sHelp, bReqd, sValue, bStrict )
	{
	//	Object inheritance
	this.base	= Param;
	this.base( sHelp, bReqd, sValue );

	this.strict	= bStrict ? true : false;
	this.scan	= scanFileSpec;
	}

function scanFileSpec( aValue )
	{
	var	result 	= false;
	var	re 		= new RegExp( "[" + KeyPar.prototype.tags + "]" );
	if( !this.scand )
		{
		if( this.strict || !re.test(aValue.substr(0, 1)) )
			{
			this.setValue( aValue );
			this.scand	= true;
			result = true;
			}
		}
	return result;
	}

//	A keyed command-line parameter to toggle (switch or logically NOT) a boolean value
//
//	Format: [-]<key>
//
//	Parameters:
//
//		sKey	The string, usually a single character but can be a regular expression, to use as key
//		sHelp	Help text (preferably < 79 characters) to prompt user
//		bReqd	Boolean for whether the parameter must be present, default is false
//		bValue	The initial value, defaults to false
//		bCase	Boolean for whether the key is case-sensitive, default is false
//		sTag	String of allowable tag characters, default is "-"
//
function KeyPar( sKey, sHelp, bReqd, bValue, bCase, sTag )
	{
	//	Object inheritance
	this.base	= Param;
	this.base( sHelp, bReqd, bValue ? bValue : false );

	this.key	= sKey;
	if( bCase )
		this.cases = true;
	if( sTag )
		this.tags	= sTag;
	this.scan	= scanKey;
	this.re		= new RegExp("^[" + this.tags + "]" + this.key + "$", this.cases ? "g" : "ig" );
	}

function scanKey( aValue )
	{
	var	result = false;
	if( (!this.scand) && this.re.test(aValue) )
		{
		this.setValue( !this.getValue() );
		this.scand	= true;
		result = true;
		}
	return result;
	}

//	A keyed command-line string parameter
//
//	Format: [-]<key>:<string>
//
//	Parameters:
//
//		sKey	The string, usually a single character but can be a regular expression, to use as key
//		sHelp	Help text (preferably < 79 characters) to prompt user
//		bReqd	Boolean for whether the parameter must be present, default is false
//		sValue	The initial value, defaults to null
//		bCase	Boolean for whether the key is case-sensitive, default is false
//		sTag	String of allowable tag characters, default is "-"
//
function KeyStrPar( sKey, sHelp, bReqd, sValue, bCase, sTag )
	{
	//	Object inheritance
	this.base	= KeyPar;
	this.base( sKey, sHelp, bReqd, sValue ? sValue : null, bCase, sTag );

	this.scan	= scanKeyStr;
	this.re		= new RegExp("^[" + this.tags + "]" + this.key + ":?(.+)$", this.cases ? "g" : "ig" );
	}

function scanKeyStr( aValue )
	{
	var	result = false;
	if( !this.scand )
		{
		try
			{
			this.setValue( this.re.exec(aValue)[1] );
			this.scand	= true;
			result = true;
			}
		catch(e)
			{
			if( debug )
				log( "scanKeyStr - exception caught: 0x" + ((e.number+0xFFFFFFFF)%0xFFFFFFFF).toString(16).toUpperCase() + "\n\t" + e.description );
			}
		}
	return result;
	}

//	A keyed command-line parameter to set a boolean value
//
//	Format: [-]<key>:<-+01>
//
//	Parameters:
//
//		sKey	The string, usually a single character but can be a regular expression, to use as key
//		sHelp	Help text (preferably < 79 characters) to prompt user
//		bReqd	Boolean for whether the parameter must be present, default is false
//		bValue	The initial value, defaults to false
//		bCase	Boolean for whether the key and the toggle characters are case-sensitive, default is false
//		sTag	String of allowable tag characters, default is "-"
//		sVals	String of allowable toggle characters, should be in pairs "<False><True>", as in default "-+01FT"
//
function KeyTogPar( sKey, sHelp, bReqd, bValue, bCase, sTag, sVals )
	{
	//	Object inheritance
	this.base	= KeyStrPar;
	this.base( sKey, sHelp, bReqd, bValue ? bValue : false, bCase, sTag );

	if( sVals )
		this.vals = sVals;
	this.scan	= scanKeyTog;
	this.re		= new RegExp("^[" + this.tags + "]" + this.key + ":?([" + this.vals + "])?$", this.cases ? "g" : "ig" );
	}

function scanKeyTog( aValue )
	{
	var	result = false;
	if( !this.scand )
		{
		try
			{
			var	tog = this.re.exec(aValue);
			if( tog != null )
				{
				tog = tog[tog.length - 1];
				switch( tog ? this.vals.indexOf( this.cases ? tog : tog.toUpperCase() ) % 2 : -1 )
					{
					case -1:	this.setValue( !this.getValue() );
								this.scand	= true;
								result = true;
								break;

					case 0:		this.setValue( false );
								this.scand	= true;
								result = true;
								break;

					case 1:		this.setValue( true );
								this.scand	= true;
								result = true;
								break;

					default:	break;
					}
				}
			}
		catch(e)
			{
			if( debug )
				log( "scanKeyTog - exception caught: 0x" + ((e.number+0xFFFFFFFF)%0xFFFFFFFF).toString(16).toUpperCase() + "\n\t" + e.description );
			}
		}
	return result;
	}

//	A keyed command-line parameter to choose from a list of allowed strings
//
//	Format: [-]<key>:<string>
//
//	Parameters:
//
//		sKey	The string, usually a single character but can be a regular expression, to use as key
//		sHelp	Help text (preferably < 79 characters) to prompt user
//		sVals	Array of allowed string choices
//		bReqd	Boolean for whether the parameter must be present, default is false
//		bValue	The initial value, defaults to false
//		bCase	Boolean for whether the key and the string values are case-sensitive, default is false
//		sTag	String of allowable tag characters, default is "-"
//
function KeyLstPar( sKey, sHelp, sVals, bReqd, bValue, bCase, sTag )
	{
	//	Object inheritance
	this.base	= KeyStrPar;
	this.base( sKey, sHelp, bReqd, bValue ? bValue : false, bCase, sTag );

	if( sVals )
		this.vals = sVals;
	this.scan	= scanKeyLst;
	this.re		= new RegExp("^[" + this.tags + "]" + this.key + ":?(" + this.vals.join("|") + ")$", this.cases ? "g" : "ig" );
	}

function scanKeyLst( aValue )
	{
	var	result = false;
	if( !this.scand )
		{
		try
			{
			this.setValue( this.re.exec(aValue)[1] );
			this.scand	= true;
			result = true;
			}
		catch(e)
			{
			if( debug )
				log( "scanKeyStr - exception caught: 0x" + ((e.number+0xFFFFFFFF)%0xFFFFFFFF).toString(16).toUpperCase() + "\n\t" + e.description );
			}
		}
	return result;
	}

//	A keyed command-line bounded integer parameter
//
//	Format: [-]<key>:<decimal>|B<binary>|O<octal>|H<hexadecimal>
//
//	Parameters:
//
//		sKey	The string, usually a single character but can be a regular expression, to use as key
//		iLower	The lower bound, defaults to javascript's smallest integer -2^52
//		iUpper	The upper bound, defaults to javascript's largest integer +2^52
//		iValue	The initial value, defaults to null
//		sHelp	Help text (preferably < 79 characters) to prompt user
//		bReqd	Boolean for whether the parameter must be present, default is false
//		bCase	Boolean for whether the key, B, O, H are case-sensitive, default is false
//		sTag	String of allowable tag characters, default is "-"
//
function KeyIntPar( sKey, iLower, iUpper, iValue, sHelp, bReqd, bCase, sTag )
	{
	//	Object inheritance
	this.base	= KeyStrPar;
	this.base( sKey, sHelp, bReqd, iValue, bCase, sTag );

	this.lower		= iLower != null ? iLower : -Math.pow(2,52);
	this.upper		= iUpper != null ? iUpper : Math.pow(2,52);
	this.getLower	= gLower;
	this.setLower	= sLower;
	this.getUpper	= gUpper;
	this.setUpper	= sUpper;
	this.scan		= scanKeyInt;
	this.re			= new RegExp("^[" + this.tags + "]" + this.key + ":?(([-+]?[0-9]+)|(B[-+]?[0-1]+)|(O[-+]?[0-7]+)|(H[-+]?[0-9A-F]+))$", this.cases ? "g" : "ig" );
	}

function scanKeyInt( aValue )
	{
	var	result = false;
	if( !this.scand )
		{
		try
			{
			var	base;
			var	numb = this.re.exec(aValue)[1];
			switch( numb.charAt(0).toUpperCase() )
				{
				default:	base = 10;
							break;

				case "B":	numb = numb.substr( 1 );
							base = 2;
							break;

				case "O":	numb = numb.substr( 1 );
							base = 8;
							break;

				case "H":	numb = numb.substr( 1 );
							base = 16;
							break;
				}
			numb = parseInt( numb, base );
			if( (numb >= this.lower) && (numb <= this.upper) )
				{
				this.setValue( numb );
				this.scand	= true;
				result = true;
				}
			}
		catch(e)
			{
			if( debug )
				log( "scanKeyInt - exception caught: 0x" + ((e.number+0xFFFFFFFF)%0xFFFFFFFF).toString(16).toUpperCase() + "\n\t" + e.description );
			}
		}
	return result;
	}

function gLower()
	{
	return this.lower;
	}

function sLower( nLower )
	{
	var	oLower = this.lower;
	this.lower = nLower;
	return oLower;
	}

function gUpper()
	{
	return this.upper;
	}

function sUpper( nUpper )
	{
	var	oUpper = this.upper;
	this.upper = nUpper;
	return oUpper;
	}

//	A keyed command-line bounded floating point parameter accepting a number in decimal or scientific format
//
//	Format: [-]<key>:<fp decimal>
//
//	Parameters:
//
//		sKey	The string, usually a single character but can be a regular expression, to use as key
//		iLower	The lower bound, defaults to javascript's smallest number -2^52
//		iUpper	The upper bound, defaults to javascript's largest number +2^52
//		iValue	The initial value, defaults to null
//		sHelp	Help text (preferably < 79 characters) to prompt user
//		bReqd	Boolean for whether the parameter must be present, default is false
//		bCase	Boolean for whether the key and the E denoting an exponent are case-sensitive, default is false
//		sTag	String of allowable tag characters, default is "-"
//
function KeyFltPar( sKey, iLower, iUpper, iValue, sHelp, bReqd, bCase, sTag )
	{
	//	Object inheritance
	this.base	= KeyIntPar;
	this.base( sKey, iLower, iUpper, iValue, sHelp, bReqd, bCase, sTag );

	this.scan		= scanKeyFlt;
	this.re			= new RegExp("^[" + this.tags + "]" + this.key + ":?([-+]?[.0-9]+)(?:E([-+]?[.0-9]+))?$", this.cases ? "g" : "ig" );
	}

function scanKeyFlt( aValue )
	{
	var	result = false;
	if( !this.scand )
		{
		try
			{
			var	comps = this.re.exec(aValue);
			var	numb = comps[2] ? parseFloat( comps[1] )*Math.pow( 10, parseFloat(comps[2]) ) : parseFloat( comps[1] );
			if( (numb >= this.lower) && (numb <= this.upper) )
				{
				this.setValue( numb );
				this.scand	= true;
				result = true;
				}
			}
		catch(e)
			{
			if( debug )
				log( "scanKeyFlt - exception caught: 0x" + ((e.number+0xFFFFFFFF)%0xFFFFFFFF).toString(16).toUpperCase() + "\n\t" + e.description );
			}
		}
	return result;
	}

//	Non WSH use: change appropriately WScript.Echo, TextStream.WriteLine
//	Output logging messages
function log( _msg, _logfile )
	{
	WScript.Echo( _msg );
	if( _logfile != null )
		_logfile.WriteLine( _msg );
	return;
	}
