(view source code of batcodecheck.cs as plain text)
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
namespace RobvanderWoude
{
public partial class BatCodeCheck
{
public static string progver = "0.41";
#region Global Variables and Constants
public const int maxvarlen = 32; // Some day, we may have to adjust the accepted variable name length if 32 characters is no longer enough ([\w\.-]{1,32})
public const RegexOptions ignorecase = RegexOptions.IgnoreCase;
public const string dynvarpattern = @"__APPDIR__|__CD__|CD|CMDCMDLINE|CMDEXTVESION|DATE|ERRORLEVEL|HIGHESTNUMANODENUMBER|RANDOM|TIME";
public const string intcmdpattern = @"ASSOC|BREAK|CALL|CD|CHDIR|CLS|COLOR|COPY|DATE|DEL|DIR|DPATH|ECHO|ENDLOCAL|ERASE|EXIT|FOR|FTYPE|GOTO|IF|KEYS|MD|MKDIR|MKLINK|MOVE|PATH|PAUSE|POPD|PROMPT|PUSHD|RD|REM|REN|RENAME|RMDIR|SET|SETLOCAL|SHIFT|START|TIME|TITLE|TYPE|VER|VERIFY|VOL";
public const string notx32pattern = @"BREAK|CTTY|FASTOPEN|FASTOPEN\.COM|FASTOPEN\.EXE|KEYS|SHARE|SHARE\.EXE";
public const string notx64pattern = @"APPEND|APPEND\.COM|APPEND\.EXE|DEBUG|DEBUG\.COM|DEBUG\.EXE";
public const string sysvarpattern = @"AllUsersProfile|AppData|CommonProgramFiles|CommonProgramFiles\(x86\)|CommonProgramW6432|ComputerName|ComSpec|HomeDrive|HomePath|LocalAppData|LogonServer|NUMBER_OF_PROCESSORS|OS|PATH|PATHEXT|PROCESSOR_ARCHITECTURE|PROCESSOR_IDENTIFIER|PROCESSOR_LEVEL|PROCESSOR_REVISION|ProgramData|ProgramFiles|ProgramFiles\(x86\)|ProgramW6432|PROMPT|SystemDrive|SystemRoot|TEMP|TMP|UserDomain|UserName|UserProfile|windir";
public static int commentlines = 0;
public static int lasterrline = -1;
public static int lastline = -1;
public static int linenum = 0;
public static int lineswitherrors = 0;
public static int mysteryvars = 0;
public static int obsoletecmd = 0;
public static int parenthesis = 0;
public static int rc = 1;
public static int setlocal = 0;
public static int unterminatedexclamations = 0;
public static int unterminateddoublequotes = 0;
public static int unterminatedpercentsigns = 0;
public static int unterminatedsinglequotes = 0;
public static bool addtimestamp = false;
public static bool checkall = true;
public static bool checkbadpr = false;
public static bool checkbestpr = false;
public static bool checkcmdargs = false;
public static bool checkcommerr = false;
public static bool checkenvvars = false;
public static bool checklabels = false;
public static bool checksubs = false;
public static bool checkundef = false;
public static bool checkupdates = false;
public static bool checkvulnr = false;
public static bool debugmode = false;
public static bool delayedvarexp = false;
public static bool delayedvarglobal = false;
public static bool dispnums = true;
public static bool errinline = false;
public static bool htmlopen = false;
public static bool includelocalvars = false;
public static bool incomment = false;
public static bool interactive = false;
public static bool licensed = true;
public static bool log2html = false;
public static bool logging = false;
public static bool openalllogs = false;
public static bool openlog = false;
public static bool openhtml = false;
public static bool replacehtml = true;
public static bool replacelog = true;
public static bool spanopen = false;
public static bool splitoneliners = false;
public static bool verbosesummary = true;
public static bool wrap = false;
public static string allargs;
public static string allsrc;
public static string allsrc_nocomments;
public static string allerrors = string.Empty;
public static string allbadpr = string.Empty;
public static string alltips = string.Empty;
public static string allwarnings = string.Empty;
public static string allerrsum = string.Empty;
public static string checks = "ABELMSUV";
public static string encoding;
public static string html = string.Empty;
public static string htmlfile = string.Empty;
public static string logfile = string.Empty;
public static string newline = Environment.NewLine;
public static string source = string.Empty;
public static string timestamp = string.Empty;
public static string[] path = ( Environment.CurrentDirectory + ";" + Environment.GetEnvironmentVariable( "PATH" ) ).Split( ';' );
public static string[] pathext = ( ";" + Environment.GetEnvironmentVariable( "PATHEXT" ).ToLower( ) ).Split( ';' );
public static Dictionary<string, string> tests;
public static SortedList<string, int> labels = new SortedList<string, int>( );
public static SortedList<string, int> envvars = new SortedList<string, int>( );
public static SortedList<string, int> subroutines = new SortedList<string, int>( );
public static List<string> upcaselabels = new List<string>( );
public static List<string> gotoerrors = new List<string>( );
public static List<string> callerrors = new List<string>( );
public static List<string> upcaseenvvars = new List<string>( );
public static List<string> envvarerrors = new List<string>( );
public static List<string> localenvvars = new List<string>( );
public static List<int> incorrectlinefeeds = new List<int>( );
public static StreamWriter htmlstream;
public static ConsoleColor badprbgcolor = ConsoleColor.Yellow;
public static ConsoleColor badprfgcolor = ConsoleColor.Red;
public static ConsoleColor commentfgscolor = ConsoleColor.DarkYellow;
public static ConsoleColor doublequotedfgcolor = ConsoleColor.Yellow;
public static ConsoleColor echoedtextfgcolor = ConsoleColor.Cyan;
public static ConsoleColor errorbgcolor = ConsoleColor.Red;
public static ConsoleColor errorfgcolor = ConsoleColor.White;
public static ConsoleColor extcmdbgcolor = ConsoleColor.Black;
public static ConsoleColor extcmdfgcolor = ConsoleColor.White;
public static ConsoleColor intcmdbgcolor = ConsoleColor.Black;
public static ConsoleColor intcmdfgcolor = ConsoleColor.Yellow;
public static ConsoleColor lastbgc = Console.BackgroundColor;
public static ConsoleColor lastfgc = Console.ForegroundColor;
public static ConsoleColor linenumbgcolor = ConsoleColor.Gray;
public static ConsoleColor linenumfgcolor = ConsoleColor.DarkMagenta;
public static ConsoleColor parenthesisfgcolor = ConsoleColor.Red;
public static ConsoleColor redirectionfgcolor = ConsoleColor.Red;
public static ConsoleColor reportbgcolor = Console.BackgroundColor;
public static ConsoleColor reportfgcolor = ConsoleColor.Red;
public static ConsoleColor singlequotedfgcolor = ConsoleColor.DarkGray;
public static ConsoleColor tipsbgcolor = ConsoleColor.Blue;
public static ConsoleColor tipsfgcolor = ConsoleColor.White;
public static ConsoleColor variablesfgcolor = ConsoleColor.Green;
public static ConsoleColor warningbgcolor = ConsoleColor.DarkYellow;
public static ConsoleColor warningfgcolor = ConsoleColor.White;
public static ConsoleColor windowbgcolor = Console.BackgroundColor;
public static ConsoleColor windowfgcolor = Console.ForegroundColor;
// Inline variables
public static string badprmsg = string.Empty;
public static string errormsg = string.Empty;
public static string tipsmsg = string.Empty;
public static string warnmsg = string.Empty;
public static string message = string.Empty;
public static string part = string.Empty;
public static string buildword = string.Empty;
public static string lastword = string.Empty;
#endregion Global Variables and Constants
[STAThread]
static int Main( string[] args )
{
// Check for updates (licensed version only)
CheckUpdate( );
#region Initialize List of Tests
tests = new Dictionary<string, string>( );
tests["A"] = "command line Arguments for batch commands";
tests["B"] = "Best practice tips";
tests["E"] = "Environment variables";
tests["L"] = "Labels";
tests["M"] = "common Mistakes";
tests["S"] = "verbose Summary (variables, labels, subroutines)";
tests["U"] = "Undefined environment variables";
tests["V"] = "Vulnerabilities";
#endregion Initialize List of Tests
#region Command Line Parsing
allargs = string.Join( " ", args );
#region Command Line Arguments
if ( args.Length == 0 || args.Length > 8 )
{
return ShowHelp( );
}
foreach ( string arg in args )
{
if ( arg[0] == '/' )
{
switch ( arg.ToUpper( ).Substring( 0, Math.Min( 2, arg.Length ) ) )
{
case "/?":
return ShowHelp( );
case "/C":
if ( !checkall )
{
return ShowHelp( "Duplicate command line argument /C" );
}
if ( arg.Length < 4 || arg[2] != ':' )
{
return ShowHelp( "Invalid command line argument \"{0}\"", arg );
}
checks = arg.Substring( 3 ).ToUpper( );
foreach ( char check in checks )
{
if ( !tests.Keys.Contains( check.ToString( ).ToUpper( ) ) )
{
return ShowHelp( "Invalid check \"{1}\" in command line argument \"{0}\"", arg, check.ToString( ) );
}
}
checkall = false;
checkcmdargs = ( checks.IndexOf( 'A' ) > -1 ); // command line Arguments for batch commands
checkbestpr = ( checks.IndexOf( 'B' ) > -1 ); // Best practice tips
checkenvvars = ( checks.IndexOf( 'E' ) > -1 ); // Environment variables
checklabels = ( checks.IndexOf( 'L' ) > -1 ); // Labels
checkcommerr = ( checks.IndexOf( 'M' ) > -1 ); //' common Mistakes
verbosesummary = ( checks.IndexOf( 'S' ) > -1 ); // verbose Summary (variables, labels, subroutines)
checkundef = ( checks.IndexOf( 'U' ) > -1 ); // Undefined environment variables
checkvulnr = ( checks.IndexOf( 'V' ) > -1 ); // Vulnerabilities
break;
case "/D":
if ( debugmode )
{
return ShowHelp( "Duplicate command line argument /D" );
}
debugmode = true;
break;
case "/E":
if ( includelocalvars )
{
return ShowHelp( "Duplicate command line argument /E" );
}
includelocalvars = true;
break;
case "/H":
if ( log2html )
{
return ShowHelp( "Duplicate command line argument /H" );
}
log2html = true;
if ( arg.Length > 3 && arg.ToUpper( ).Substring( 2, 1 ) == ":" )
{
htmlfile = arg.Substring( 3 );
}
break;
case "/I":
if ( interactive )
{
return ShowHelp( "Duplicate command line argument /I" );
}
interactive = true;
break;
case "/L":
if ( logging )
{
return ShowHelp( "Duplicate command line argument /L" );
}
logging = true;
if ( arg.Length > 3 && arg.ToUpper( ).Substring( 2, 1 ) == ":" )
{
logfile = arg.Substring( 3 );
}
break;
case "/O":
if ( openalllogs )
{
return ShowHelp( "Duplicate command line argument /O" );
}
openalllogs = true;
break;
case "/S":
if ( splitoneliners )
{
return ShowHelp( "Duplicate command line argument /S" );
}
splitoneliners = true;
break;
case "/T":
if ( addtimestamp )
{
return ShowHelp( "Duplicate command line argument /T" );
}
addtimestamp = true;
break;
case "/U":
if ( checkupdates )
{
return ShowHelp( "Duplicate command line argument /U" );
}
checkupdates = true;
break;
case "/W":
if ( wrap )
{
return ShowHelp( "Duplicate command line argument /W" );
}
wrap = true;
break;
default:
return ShowHelp( "Invalid command line argument \"{0}\"", arg );
}
}
else
{
if ( string.IsNullOrEmpty( source ) )
{
source = arg;
}
else
{
return ShowHelp( "Invalid command line argument \"{0}\"", arg );
}
}
}
if ( checkupdates )
{
string updatecheckurl = "http://www.robvanderwoude.com/getlatestver.php?progfile=BatCodeCheck.exe&version=" + progver;
Process.Start( updatecheckurl );
}
#endregion Command Line Arguments
#region Validate paths
if ( !interactive )
{
if ( string.IsNullOrEmpty( source ) )
{
return ShowHelp( "Please specify a source file" );
}
if ( !addtimestamp && source.IndexOfAny( "/?*".ToCharArray( ) ) > -1 )
{
return ShowHelp( );
}
if ( !File.Exists( source ) )
{
return ShowHelp( "Source file not found" );
}
}
if ( !string.IsNullOrWhiteSpace( source ) )
{
source = Path.GetFullPath( source );
}
if ( !string.IsNullOrEmpty( logfile ) )
{
if ( !Directory.Exists( Directory.GetParent( logfile ).FullName ) )
{
return ShowHelp( "Invalid path to log file" );
}
logfile = Path.GetFullPath( logfile );
}
if ( !string.IsNullOrEmpty( htmlfile ) )
{
if ( !Directory.Exists( Directory.GetParent( htmlfile ).FullName ) )
{
return ShowHelp( "Invalid path to HTML file" );
}
htmlfile = Path.GetFullPath( htmlfile );
}
#endregion Validate paths
#region BatCodeCheck Environment Variable
// Local environment variable %BatCodeCheck% can be used to set defaults, but it is overruled by command line arguments
try
{
string envargs = Environment.GetEnvironmentVariable( "BatCodeCheck" );
if ( !string.IsNullOrWhiteSpace( envargs ) )
{
envargs = envargs.ToUpper( );
}
if ( new Regex( @"^\s*(/[EHILOSTW]\s*)+$" ).IsMatch( envargs ) )
{
foreach ( Match match in new Regex( "/[DEHILOSTW]" ).Matches( envargs ) )
{
switch ( match.ToString( ) )
{
case "/D":
if ( !debugmode )
{
debugmode = true;
allargs += " /D";
}
break;
case "/E":
if ( !includelocalvars )
{
includelocalvars = true;
allargs += " /E";
}
break;
case "/H":
if ( !log2html )
{
log2html = true;
allargs += " /H";
}
break;
case "/I":
if ( !interactive )
{
interactive = true;
allargs += " /I";
}
break;
case "/L":
if ( !logging )
{
logging = true;
allargs += " /L";
}
break;
case "/O":
if ( !openalllogs )
{
openalllogs = true;
allargs += " /O";
}
break;
case "/S":
if ( !splitoneliners )
{
splitoneliners = true;
allargs += " /S";
}
break;
case "/T":
if ( !addtimestamp )
{
addtimestamp = true;
allargs += " /T";
}
break;
case "/W":
if ( !wrap )
{
wrap = true;
allargs += " /W";
}
break;
}
}
}
}
catch ( Exception )
{
// Environment variable BatCodeCheck not found
}
#endregion BatCodeCheck Environment Variable
#region Command Line Defaults
if ( !includelocalvars )
{
pathext = ".com;.exe;.bat;.cmd".Split( ';' );
}
if ( addtimestamp )
{
AddTimeStamp( );
}
if ( log2html && string.IsNullOrWhiteSpace( htmlfile ) )
{
htmlfile = Path.Combine( Path.GetDirectoryName( source ), Path.GetFileNameWithoutExtension( source ) + timestamp + ".html" );
}
if ( logging && string.IsNullOrWhiteSpace( logfile ) )
{
logfile = Path.Combine( Path.GetDirectoryName( source ), Path.GetFileNameWithoutExtension( source ) + timestamp + ".log" );
}
#endregion Command Line Defaults
#region Command Line Dependencies
openalllogs = openalllogs && ( logging || log2html );
openhtml = openalllogs && log2html;
openlog = openalllogs && logging;
#endregion Command Line Dependencies
#region Interactive
if ( licensed && interactive )
{
try
{
if ( !ShowSettings( ) )
{
Console.Error.WriteLine( "Aborted by user request" );
return 1;
}
}
catch ( Exception )
{
return ShowHelp( "Interactive use of BatCodeCheck (/I switch) requires a licensed version" );
}
}
#endregion Interactive
#endregion Command Line Parsing
#if DEBUG
debugmode = true;
#endif
if ( File.Exists( source ) )
{
encoding = GetEncoding( source );
ReadSource( source );
CheckLinefeeds( );
CollectEnvironmentVariables( );
CollectLabels( );
CreateHTML( );
// Now parse the source line by line
rc = 0;
foreach ( string line in allsrc.Split( new string[] { "\r\n", "\n", "\r" }, StringSplitOptions.None ) )
{
linenum += 1;
// If /S switch was used, split the line on parenthesis and ampersands, except parentheses surrounding the list in FOR loops, and except comments
if ( splitoneliners && !new Regex( @"^\s*(::|REM)(\b|\s|$)", ignorecase ).IsMatch( line ) && line.IndexOfAny( "(&|)".ToCharArray( ) ) > -1 )
{
#region Split One-Liners
bool inelse = false;
bool inforloop = false;
string tmpline = string.Empty;
string pattern = @"(?<!\^)\((?!['\n\r])";
Regex regex = new Regex( pattern );
if ( regex.IsMatch( line ) )
{
MatchCollection matches = regex.Matches( line );
tmpline = line.Substring( 0, matches[0].Index );
if ( new Regex( @"\s%%[A-Z!]\s+IN\s+$", ignorecase ).IsMatch( tmpline ) )
{
inforloop = true;
}
else if ( new Regex( @"\)\s+ELSE\s+\(", ignorecase ).IsMatch( tmpline ) )
{
inelse = true;
}
}
if ( inforloop || inelse )
{
tmpline = regex.Replace( line, "(\n\t", 100, line.IndexOf( ')' ) + 1 );
}
else
{
tmpline = regex.Replace( line, "(\n\t" );
}
pattern = @"(?<!['\^\n\r])\)";
regex = new Regex( pattern );
if ( inforloop || inelse )
{
tmpline = regex.Replace( tmpline, "\n)", 100, line.IndexOf( ')' ) + 1 );
}
else
{
tmpline = regex.Replace( tmpline, "\n)" );
}
pattern = @"(?<!\^|2>|1>)&(?>!1|2)";
regex = new Regex( pattern );
tmpline = regex.Replace( tmpline, "\n\t" );
foreach ( string part in tmpline.Split( new string[] { "\n" }, StringSplitOptions.None ) )
{
if ( !string.IsNullOrWhiteSpace( part ) )
{
ParseLine( part );
}
}
#endregion Split One-Liners
}
else
{
ParseLine( line );
}
}
DisplayMessagesSummary( );
CloseHTML( );
RestoreConsoleColors( );
if ( OpenLogFile( ) + OpenHTMLFile( ) != 0 )
{
return 1;
}
return rc;
}
else
{
return ShowHelp( "File \"{0}\" not found", source );
}
}
#region Subroutines
public static void AddTimeStamp( )
{
if ( string.IsNullOrWhiteSpace( timestamp ) )
{
timestamp = DateTime.Now.ToString( ".yyyyMMddHHmmss" );
}
}
public static void CheckBadPractices( string line )
{
if ( checkall || checkbadpr )
{
string pattern;
Regex regex;
// SET dynamic variables
#region SET dynamic variables
pattern = string.Format( @"\bSET\s+({0})=", dynvarpattern );
regex = new Regex( pattern, ignorecase );
if ( regex.IsMatch( line ) )
{
MatchCollection matches = regex.Matches( line );
string dynvar = matches[0].Groups[1].Value;
message = string.Format( "Setting dynamic variable \"{0}\" to a static value may cause problems later on in the batch file; consider using an alternative variable name instead", dynvar.ToUpper( ) );
allbadpr += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
badprmsg += string.Format( "{0}{1}.", newline, message );
errinline = true;
}
#endregion SET dynamic variables
// SET system variables
#region SET system variables
pattern = string.Format( @"\bSET\s+({0})=", sysvarpattern );
regex = new Regex( pattern, ignorecase );
if ( regex.IsMatch( line ) )
{
MatchCollection matches = regex.Matches( line );
string dynvar = matches[0].Groups[1].Value;
message = string.Format( "Setting system variable \"{0}\" may cause problems later on in the batch file; consider using an alternative variable name instead", dynvar.ToUpper( ) );
allbadpr += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
badprmsg += string.Format( "{0}{1}.", newline, message );
errinline = true;
}
#endregion SET system variables
// Quoted paths
// Disabled for now, as it is extremely hard, if possible at all, to extract a path from a command line without a true interpreter
#region Quoted paths
/*
pattern = @"[^""=]((?:[A-Z]:|\\\\\w+\\\w+)(?:\\[^\\]+)*\\[^\s\\]+\s+[^\\]+(?:\\[^\\]+)*(?:\\|\b|$))";
regex = new Regex( pattern, ignorecase );
if ( regex.IsMatch( line ) )
{
MatchCollection matches = regex.Matches( line );
string path = matches[0].Groups[1].ToString( );
message = string.Format( "Whitespace found in unquoted path: \"{0}\"", path );
allbadpr += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
badprmsg += string.Format( "{0}{1}.", newline, message );
errinline = true;
}
pattern = @"[^""]((?:[A-Z]:|\\\\\w+\\\w+)(?:\\[^\\]+)*\\[^\\]*(\(|\))[^\\]*(?:\\[^\\]+)*\\?(?:\b|$))";
regex = new Regex( pattern, ignorecase );
if ( regex.IsMatch( line ) )
{
MatchCollection matches = regex.Matches( line );
string path = matches[0].Groups[1].ToString( );
string spec = matches[0].Groups[2].ToString( );
message = string.Format( "Parenthesis found in unquoted path: \"{1}\"; use doublequotes, especially inside code blocks", spec, path );
allbadpr += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
badprmsg += string.Format( "{0}{1}.", newline, message );
errinline = true;
}
pattern = @"[^""=]((?:[A-Z]:|\\\\\w+\\\w+)(?:\\[^\\]+)*\\[^\\]*(&|'|,|\^)[^\\]*(?:\\[^\\]+)*\\?(?:\b|$))";
regex = new Regex( pattern, ignorecase );
if ( regex.IsMatch( line ) )
{
MatchCollection matches = regex.Matches( line );
string path = matches[0].Groups[1].ToString( );
string spec = matches[0].Groups[2].ToString( );
message = string.Format( "Special character \"{0}\" found in unquoted path: \"{1}\"; use doublequotes", spec, path );
allbadpr += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
badprmsg += string.Format( "{0}{1}.", newline, message );
errinline = true;
}
*/
#endregion Quoted paths
}
}
public static void CheckBestPractices( string line )
{
if ( checkall || checkbestpr )
{
string pattern;
Regex regex;
// Doublequotes in SET statements
#region Doublequoted SET
pattern = @"(?:^|\b|\()(SET\s+""([\w_\.-]+)=([^""&\n\r]+)"")";
regex = new Regex( pattern, ignorecase );
if ( regex.IsMatch( line ) )
{
foreach ( Match match in regex.Matches( line ) )
{
string wrong = match.Groups[1].ToString( );
string correct = string.Format( "SET {0}={1}", match.Groups[2].ToString( ), match.Groups[3].ToString( ) );
message = string.Format( "Doublequoted SET statements are not recommended, except with /A (or /P) switch; use {0} instead of {1}", correct, wrong );
alltips += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
tipsmsg += string.Format( "{0}{1}.", newline, message );
errinline = true;
}
}
#endregion Doublequoted SET
// Devices with appended colons
#region Devices with appended colons
pattern = @"(?:>|\b)(AUX|CLOCK\$|COM[1-8]|CON|LPT[1-8]|NUL|PRN):(?:\b|$)";
regex = new Regex( pattern, ignorecase );
if ( regex.IsMatch( line ) )
{
MatchCollection matches = regex.Matches( line );
string device = matches[0].Groups[1].ToString( ).ToUpper( );
message = string.Format( "Redundant colon appended to device name \"{0}\"", device );
alltips += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
tipsmsg += string.Format( "{0}{1}.", newline, message );
errinline = true;
}
#endregion Devices with appended colons
// Obsolete commands BREAK, CTTY, KEYS, and 16-bit commands
#region Obsolete commands
pattern = string.Format( @"(?:^|&|\|\()\s*({0})\b", notx32pattern );
regex = new Regex( pattern, ignorecase );
if ( regex.IsMatch( line ) )
{
MatchCollection matches = regex.Matches( line );
foreach ( Match match in matches )
{
string cmd = match.Groups[1].ToString( ).ToUpper( );
message = string.Format( "Obsolete command \"{0}\" is accepted only for MS-DOS backwards compatibility", cmd );
alltips += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
tipsmsg += string.Format( "{0}{1}.", newline, message );
obsoletecmd += 1;
}
errinline = true;
}
pattern = string.Format( @"(?:^|&|\|\()\s*({0})\b", notx64pattern );
regex = new Regex( pattern, ignorecase );
if ( regex.IsMatch( line ) )
{
MatchCollection matches = regex.Matches( line );
foreach ( Match match in matches )
{
string cmd = match.Groups[1].ToString( ).ToUpper( );
if ( System.Environment.Is64BitOperatingSystem )
{
message = string.Format( "16-bit command \"{0}\" is not available in 64-bit Windows", cmd );
allerrors += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
errormsg += string.Format( "{0}{1}.", newline, message );
}
else
{
message = string.Format( "16-bit command \"{0}\" will not be available in 64-bit Windows", cmd );
alltips += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
tipsmsg += string.Format( "{0}{1}.", newline, message );
}
obsoletecmd += 1;
}
errinline = true;
}
#endregion Obsolete commands
// @ all over the place
#region Hiding command echo
if ( linenum > 1 )
{
pattern = @"^\s*@(?>!ECHO\s*(ON|OFF)\s)";
regex = new Regex( pattern, ignorecase );
if ( regex.IsMatch( line ) )
{
message = "Using multiple @s to hide command echoing is NOT recommended, as it makes debugging batch files harder. @ECHO OFF at the start of the batch file suffices to hide ALL command echoing.";
alltips += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
tipsmsg += string.Format( "{0}{1}.", newline, message );
errinline = true;
}
}
#endregion Hiding command echo
}
}
public static void CheckCommandLineArguments( string line )
{
if ( checkall || checkcmdargs )
{
string pattern;
Regex regex;
// Invalid arguments for ASSOC command
#region ASSOC command line
pattern = @"(^|\b|\()ASSOC(\b|$)";
regex = new Regex( pattern, ignorecase );
if ( regex.IsMatch( line ) )
{
pattern = @"(?:^|\b|\()ASSOC(\s+\.\w+(?:=[\w \.\-]{3,})?)?\s*$";
regex = new Regex( pattern, ignorecase );
if ( !regex.IsMatch( line ) )
{
pattern = @"(?:^|\b|\()ASSOC([^\n\r&|]+)";
regex = new Regex( pattern, ignorecase );
if ( regex.IsMatch( line ) )
{
string cla = regex.Match( line ).Groups[1].ToString( ).Trim( );
if ( !string.IsNullOrWhiteSpace( cla ) && cla[0] != '/' ) // No need to display a message for switches, the check for switches will handle that
{
message = string.Format( "Invalid command line argument(s) \"{0}\" for ASSOC command", cla );
allerrors += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
errormsg += string.Format( "{0}{1}.", newline, message );
errinline = true;
}
}
}
}
#endregion ASSOC command line
// Invalid arguments for BREAK command
#region BREAK command line
pattern = @"(^|\b|\()BREAK(\b|$)";
regex = new Regex( pattern, ignorecase );
if ( regex.IsMatch( line ) )
{
pattern = @"(?:^|\b|\()BREAK(\s+(ON|OFF))\s*$";
regex = new Regex( pattern, ignorecase );
if ( !regex.IsMatch( line ) )
{
pattern = @"(?:^|\b|\()BREAK([^\n\r&|]+)";
regex = new Regex( pattern, ignorecase );
if ( regex.IsMatch( line ) )
{
string cla = regex.Match( line ).Groups[1].ToString( ).Trim( );
if ( !String.IsNullOrWhiteSpace( cla ) && cla[0] != '/' ) // No need to display a message for switches, the check for switches will handle that
{
message = string.Format( "Invalid command line argument \"{0}\" for BREAK command, the only arguments accepted are ON and OFF", cla );
allerrors += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
errormsg += string.Format( "{0}{1}.", newline, message );
errinline = true;
}
}
}
message = "The BREAK command is available for backward compatibility only, it does not DO anything in Windows";
alltips += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
tipsmsg += string.Format( "{0}{1}.", newline, message );
errinline = true;
}
#endregion BREAK command line
// Invalid arguments for CALL command
#region CALL command line
pattern = @"(?:^|\b|\()CALL\s+(\""?)([^\n\r\s\""]+)(\1\b|$)";
regex = new Regex( pattern, ignorecase );
if ( regex.IsMatch( line ) )
{
bool callerr = false;
string cla = regex.Match( line ).Groups[2].ToString( );
if ( string.IsNullOrWhiteSpace( cla ) )
{
callerr = true;
}
else
{
if ( cla[0] != ':' ) // CALLs to subroutine labels will be handled later
{
pattern = string.Format( @"^({0})$", intcmdpattern );
regex = new Regex( pattern, ignorecase );
if ( !regex.IsMatch( cla ) )
{
if ( string.IsNullOrWhiteSpace( Which( cla ) ) )
{
callerr = true;
}
}
}
}
if ( callerr )
{
message = string.Format( "Invalid command line argument \"{0}\" for CALL command: no matching internal command, nor external command was found", cla );
allerrors += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
errormsg += string.Format( "{0}{1}.", newline, message );
errinline = true;
}
}
#endregion CALL command line
// Invalid switch for CD command
#region CD switches
pattern = @"(?:^|\b|\()(CD|CHDIR)\s+(/[^D][^\n\r\s/]*)";
regex = new Regex( pattern, ignorecase );
if ( regex.IsMatch( line ) )
{
string cmd = regex.Match( line ).Groups[1].ToString( );
string cls = regex.Match( line ).Groups[2].ToString( );
message = string.Format( "Invalid command line switch \"{0}\" for {1} command, the only accepted switch is /D", cls, cmd );
allerrors += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
errormsg += string.Format( "{0}{1}.", newline, message );
errinline = true;
}
#endregion CD switches
// Invalid argument for DATE command
#region DATE arguments
pattern = @"(^|\b|\()(?<![%!])DATE(?>![%!:])(\b|$)";
regex = new Regex( pattern, ignorecase );
if ( regex.IsMatch( line ) )
{
pattern = @"(?:^|\b|\()DATE\s+(/[^T][^\^\n\r\s&|\)']*|/T[^\^\n\r\s&|\)']+)";
regex = new Regex( pattern, ignorecase );
if ( regex.IsMatch( line ) )
{
string cls = regex.Match( line ).Groups[1].ToString( );
message = string.Format( "Invalid command line switch \"{0}\" for DATE command, the only valid switch is /T", cls );
allerrors += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
errormsg += string.Format( "{0}{1}.", newline, message );
errinline = true;
}
else
{
pattern = @"(?:^|\b|\()DATE\s+(/T|(%|!)[\w_-]{1,16}\2|[\d/-]{10})";
regex = new Regex( pattern, ignorecase );
if ( !regex.IsMatch( line ) )
{
string cls = regex.Match( line ).Groups[1].ToString( );
message = string.Format( "Invalid command line argument \"{0}\" for DATE command, this should be a date", cls );
allerrors += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
errormsg += string.Format( "{0}{1}.", newline, message );
errinline = true;
}
}
}
#endregion DATE arguments
// Invalid switch for EXIT command
#region EXIT switches
pattern = @"(?:^|\b|\()EXIT\s+(/[^B][^\n\r\s/]*)";
regex = new Regex( pattern, ignorecase );
if ( regex.IsMatch( line ) )
{
string cls = regex.Match( line ).Groups[1].ToString( );
message = string.Format( "Invalid command line switch \"{0}\" for EXIT command, the only accepted switch is /B", cls );
allerrors += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
errormsg += string.Format( "{0}{1}.", newline, message );
errinline = true;
}
#endregion EXIT switches
// Invalid switch for FOR command
#region FOR switches
pattern = @"(?:^|\b|\()FOR\s+(/[^DFLR][^\n\r\s=/]*)";
regex = new Regex( pattern, ignorecase );
if ( regex.IsMatch( line ) )
{
message = string.Format( "Invalid command line switch \"{0}\" for FOR command, accepted switches are /D, /F, /L and /R", regex.Match( line ).Groups[1].ToString( ) );
allerrors += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
errormsg += string.Format( "{0}{1}.", newline, message );
errinline = true;
}
#endregion FOR switches
// Invalid arguments for FTYPE command
#region FTYPE command line
pattern = @"(^|\b|\()FTYPE(\b|$)";
regex = new Regex( pattern, ignorecase );
if ( regex.IsMatch( line ) )
{
pattern = @"(?:^|\b|\()FTYPE(\s+[^\.\n\r&|][\w \.\-]{2,}(=[\w: \.\\\-\(\)]+)?)?\s*$";
regex = new Regex( pattern, ignorecase );
if ( !regex.IsMatch( line ) )
{
pattern = @"(?:^|\b|\()FTYPE\s+([^\n\r&|]+)";
regex = new Regex( pattern, ignorecase );
if ( regex.IsMatch( line ) )
{
string cla = regex.Match( line ).Groups[1].ToString( ).Trim( );
if ( !String.IsNullOrWhiteSpace( cla ) && cla[0] != '/' ) // No need to display a message for switches, the check for switches will handle that
{
message = string.Format( "Invalid command line argument(s) \"{0}\" for FTYPE command", cla );
allerrors += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
errormsg += string.Format( "{0}{1}.", newline, message );
errinline = true;
}
}
}
}
#endregion FTYPE command line
// Invalid switch for IF command
#region IF switches
pattern = @"(?:^|\b|\()(?<!/)IF\s+(/[^I][^\n\r\s/]*)";
regex = new Regex( pattern, ignorecase );
if ( regex.IsMatch( line ) )
{
string cls = regex.Match( line ).Groups[1].ToString( );
message = string.Format( "Invalid command line switch \"{0}\" for IF command, the only accepted switch is /I", cls );
allerrors += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
errormsg += string.Format( "{0}{1}.", newline, message );
errinline = true;
}
#endregion IF switches
// Invalid switch for SET command
#region SET switches
pattern = @"(?:^|\b|\()SET\s+(/[^AP][^\n\r\s=/]*)";
regex = new Regex( pattern, ignorecase );
if ( regex.IsMatch( line ) )
{
message = string.Format( "Invalid command line switch \"{0}\" for SET command, accepted switches are /A and /P", regex.Match( line ).Groups[1].ToString( ) );
allerrors += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
errormsg += string.Format( "{0}{1}.", newline, message );
errinline = true;
}
#endregion SET switches
// Invalid switch for SHIFT command
#region SHIFT switches
pattern = @"(?:^|\b|\()SHIFT\s+(/[^0-8][^\n\r\s/]*)";
regex = new Regex( pattern, ignorecase );
if ( regex.IsMatch( line ) )
{
string cls = regex.Match( line ).Groups[1].ToString( );
message = string.Format( "Invalid command line switch \"{0}\" for SHIFT command, the only accepted switch is a forward slash followed by a number from 0 to 8", cls );
allerrors += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
errormsg += string.Format( "{0}{1}.", newline, message );
errinline = true;
}
#endregion SHIFT switches
// Invalid argument for TIME command
#region TIME arguments
pattern = @"(^|\b|\()(?<![%!])TIME(?>![%!:])(\b|$)";
regex = new Regex( pattern, ignorecase );
if ( regex.IsMatch( line ) )
{
pattern = @"(?:^|\b|\()TIME\s+(/[^T][^\^\n\r\s&|\)']*|/T[^\^\n\r\s&|\)']+)";
regex = new Regex( pattern, ignorecase );
if ( regex.IsMatch( line ) )
{
string cls = regex.Match( line ).Groups[1].ToString( );
message = string.Format( "Invalid command line switch \"{0}\" for TIME command, the only valid switch is /T", cls );
allerrors += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
errormsg += string.Format( "{0}{1}.", newline, message );
errinline = true;
}
else
{
pattern = @"(?:^|\b|\()TIME\s+(/T|(%|!)[\w_-]{1,16}\2|[\d:\.,AMP\s]{4,11})";
regex = new Regex( pattern, ignorecase );
if ( !regex.IsMatch( line ) )
{
string cls = regex.Match( line ).Groups[1].ToString( );
message = string.Format( "Invalid command line argument \"{0}\" for TIME command, this should be a time value", cls );
allerrors += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
errormsg += string.Format( "{0}{1}.", newline, message );
errinline = true;
}
}
}
#endregion TIME arguments
// Switches not allowed for ASSOC, CALL, COLOR, FTYPE, GOTO, MD, MKDIR, PUSHD, SETLOCAL
#region Switches not allowed
pattern = @"(?:^|\b|\()(ASSOC|CALL|COLOR|FTYPE|GOTO|MD|MKDIR|PUSHD|SETLOCAL)\s+(/[^\n\r\s=/]*)";
regex = new Regex( pattern, ignorecase );
if ( regex.IsMatch( line ) )
{
string cmd = regex.Match( line ).Groups[1].ToString( ).ToUpper( );
string cls = regex.Match( line ).Groups[2].ToString( );
message = string.Format( "Switch \"{1}\" for {0} command is not valid, as {0} does not accept switches at all", cmd, cls );
allerrors += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
errormsg += string.Format( "{0}{1}.", newline, message );
errinline = true;
}
#endregion Switches not allowed
// Arguments not required for CLS, ENDLOCAL, PAUSE, POPD, VER
#region Arguments not required
pattern = @"(?:^|\b|\()(CLS|ENDLOCAL|PAUSE|POPD|VER)\s+([^\n\r&|>]+)";
regex = new Regex( pattern, ignorecase );
if ( regex.IsMatch( line ) )
{
string cmd = regex.Match( line ).Groups[1].ToString( ).ToUpper( );
string cla = regex.Match( line ).Groups[2].ToString( );
message = string.Format( "Argument \"{1}\" for {0} command is not valid, as {0} does not accept arguments at all", cmd, cla );
allerrors += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
errormsg += string.Format( "{0}{1}.", newline, message );
errinline = true;
}
#endregion Arguments not required
}
}
public static void CheckCommonMistakes( string line )
{
if ( checkall || checkcommerr )
{
string pattern;
Regex regex;
// IF [NOT] EXISTS or IF [NOT] EXIT or IF [NOT] EXITS instead of IF EXIST
#region IF EXIST
pattern = @"\bIF(\s+NOT)?\s+EXI(STS|T|TS)\s";
regex = new Regex( pattern, ignorecase );
if ( regex.IsMatch( line ) )
{
//message = "Found \"IF EXISTS ...\" (note the incorrect trailing \"S\"); use \"IF EXIST ...\" (without trailing \"S\") instead";
MatchCollection matches = regex.Matches( line );
string matchall = matches[0].ToString( ).ToUpper( );
string matchnot = matches[0].Groups[1].ToString( ).ToUpper( );
string matchend = matches[0].Groups[2].ToString( ).ToUpper( );
switch ( matchend )
{
case "STS":
message = string.Format( "Found \"{0} ...\" (note the incorrect trailing \"S\"); use \"IF{1} EXIST ...\" (without trailing \"S\") instead", matchall, matchnot );
break;
case "T":
message = string.Format( "Found \"{0} ...\" (note the missing \"S\"); use \"IF{1} EXIST ...\" (with \"S\") instead", matchall, matchnot );
break;
case "TS":
message = string.Format( "Found \"{0} ...\" (note the swapped \"S\" and \"T\"); use \"IF{1} EXIST ...\" (\"S\" first, followed by \"T\") instead", matchall, matchnot );
break;
}
allerrors += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
errormsg += string.Format( "{0}{1}.", newline, message );
errinline = true;
}
#endregion IF EXIST
// Common typos for %AllUsersProfile% variable
#region AllUsersProfile
pattern = @"(%|!)(A+L+USERPROFILES?)(\1)";
regex = new Regex( pattern, ignorecase );
if ( regex.IsMatch( line ) )
{
MatchCollection matches = regex.Matches( line );
foreach ( Match match in matches )
{
string wrong = match.Groups[2].ToString( ).ToUpper( );
string varchr = match.Groups[1].ToString( );
message = string.Format( "Possible typo: the system variable name is spelled \"{0}ALLUSERSPROFILE{0}\", not \"{0}{1}{0}\"", varchr, wrong );
allwarnings += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
warnmsg += string.Format( "{0}{1}.", newline, message );
errinline = true;
}
}
#endregion AllUsersProfile
// IF [NOT] ERRORLEVEL 0
#region IF ERRORLEVEL 0
pattern = @"(^|\b|\()IF\s+ERRORLEVEL\s+0\s";
regex = new Regex( pattern, ignorecase );
if ( regex.IsMatch( line ) )
{
message = "IF ERRORLEVEL 0 tests if the errorlevel is greater than or equal to 0, so for most native commands it ALWAYS returns true; use either IF NOT ERRORLEVEL 1 or IF %ErrorLevel% EQU 0 instead";
allbadpr += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
badprmsg += string.Format( "{0}{1}.", newline, message );
errinline = true;
rc = linenum;
}
pattern = @"(^|\b|\()IF\s+NOT\s+ERRORLEVEL\s+0\s";
regex = new Regex( pattern, ignorecase );
if ( regex.IsMatch( line ) )
{
message = "IF NOT ERRORLEVEL 0 tests if the errorlevel is less than 0, so for most native commands it always ALWAYS returns false; use either IF ERRORLEVEL 1 or IF %ErrorLevel% NEQ 0 instead";
allbadpr += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
badprmsg += string.Format( "{0}{1}.", newline, message );
errinline = true;
rc = linenum;
}
#endregion IF ERRORLEVEL 0
// :: inside code blocks
#region Double colons in code blocks
// check if :: is used inside a code block, but skip REM or ECHO followed by ::
bool codeblock = ( parenthesis > 0 );
bool dblcolcmnt = ( line.IndexOf( "::" ) > line.IndexOfAny( "(&|".ToCharArray( ) ) );
bool echoedcmnt = new Regex( @"^([^:]*|([^:]*:[^:]+)*)ECHO\b[^\n\r&\|\>]*::", ignorecase ).IsMatch( line ); // Skip if ECHO is found before :: and no redirection in between
bool remcomment = new Regex( @"^([^:]*|([^:]*:[^:]+)*)REM\b[^\n\r&]*::", ignorecase ).IsMatch( line ); // Skip if REM is found before :: and no ampersand in between
if ( dblcolcmnt && codeblock && !remcomment && !echoedcmnt )
{
message = "Found \"::\" inside a code block (i.e. in parenthesis); use \"REM\" instead";
allerrors += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
errormsg += string.Format( "{0}{1}.", newline, message );
errinline = true;
rc = linenum;
}
#endregion Double colons in code blocks
// CALLs to undefined subroutines
#region CALLs
if ( new Regex( @"(?:^|\s)CALL\s+:", ignorecase ).IsMatch( line ) )
// if ( new Regex( @"(?:^|\b)CALL\s+:", ignorecase ).IsMatch( line ) )
{
//MatchCollection matches = new Regex( @"(?:^|\b)CALL\s+:([\w\.\[\]\/+\-\(\)\{\}]{1,127})(?=$|\s|\)|&|\|)", ignorecase ).Matches( line );
MatchCollection matches = new Regex( @"(?:^|\s)CALL\s+:([\w\.\[\]\/+\-\(\)\{\}]{1,127})(?=$|\s|\)|&|\|)", ignorecase ).Matches( line );
foreach ( Match match in matches )
{
string label = match.Groups[1].ToString( );
if ( !labels.Keys.Contains( label ) )
{
callerrors.Add( label );
message = string.Format( "A CALL is made to a subroutine named \"{0}\" but the subroutine was not found", label );
allerrors += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
errormsg += string.Format( "{0}{1}.", newline, message );
errinline = true;
}
}
}
#endregion CALLs
// GOTOs to undefined labels
#region GOTOs
if ( new Regex( @"(?:^|\b|\s|\()GOTO\s", ignorecase ).IsMatch( line ) )
{
MatchCollection matches = new Regex( @"(?:^|\b|\s|\()GOTO\s+:?([\w\.\[\]/+\-]{1,127})[\)&|\s]*", ignorecase ).Matches( line );
foreach ( Match match in matches )
{
string label = match.Groups[1].ToString( );
if ( label.ToUpper( ) != "EOF" && !labels.Keys.Contains( label ) )
{
gotoerrors.Add( label );
message = string.Format( "A jump (GOTO) to a label named \"{0}\" was detected, but the label was not found", label );
allerrors += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
errormsg += string.Format( "{0}{1}.", newline, message );
errinline = true;
}
}
}
#endregion GOTOs
// Redirected Standard Error typos
#region Redirected Standard Error
pattern = @"\s(2(?:>>|<+|\s+)&\s*1)(?:\b|$)";
regex = new Regex( pattern, ignorecase );
if ( regex.IsMatch( line ) )
{
MatchCollection matches = regex.Matches( line );
string redirect = matches[0].Groups[1].ToString( ).ToUpper( );
message = string.Format( "Incorrect redirection of Standard Error to Standard Output: \"{0}\"", redirect );
allerrors += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
errormsg += string.Format( "{0}{1}.", newline, message );
errinline = true;
}
pattern = @"\s(1(?:>>|<+|\s+)&\s*2)(?:\b|$)";
regex = new Regex( pattern, ignorecase );
if ( regex.IsMatch( line ) )
{
MatchCollection matches = regex.Matches( line );
string redirect = matches[0].Groups[1].ToString( ).ToUpper( );
message = string.Format( "Incorrect redirection of Standard Output to Standard Error: \"{0}\"", redirect );
allerrors += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
errormsg += string.Format( "{0}{1}.", newline, message );
errinline = true;
}
pattern = @"2>&1[ \t""]*[^$&\|\)]";
regex = new Regex( pattern, ignorecase );
if ( regex.IsMatch( line ) )
{
message = "Redirection of Standard Error to Standard Output (2>&1) should always be placed at the end of the command";
allbadpr += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
badprmsg += string.Format( "{0}{1}.", newline, message );
errinline = true;
}
#endregion Redirected Standard Error
// Unescaped redirection in FOR /F loops
#region Unescaped redirection in FOR /F loops
pattern = @"FOR\s+/F\s+(""((delims|eol|skip|tokens)=[^ ""]*\s*|usebackq\s*)*"")?\s*%%[A-Z?]\s+IN\s+\('[^\^']*[\&\<\>\|]";
regex = new Regex( pattern, ignorecase );
if ( regex.IsMatch( line ) )
{
message = "Unescaped redirection symbol(s) in a FOR /F loop";
allerrors += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
errormsg += string.Format( "{0}{1}.", newline, message );
errinline = true;
}
#endregion Unescaped redirection in FOR /F loops
// Unescaped ECHOed parenthesis inside code block
#region Unescaped ECHOed parenthesis inside code block
if ( parenthesis > 0 ) // if we're inside a code block...
{
pattern = @"(^|\b)ECHO(?=\.|\s)[^&\|\n\r\(\)^]*[^^][\(\)]";
}
else
{
pattern = @"\(\s*ECHO(?=\.|\s)[^&\|\n\r\(\)^]*[^^][\(\)]";
}
regex = new Regex( pattern, ignorecase );
if ( regex.IsMatch( line ) )
{
message = "Unescaped ECHOed parenthesis inside a code block";
allerrors += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
errormsg += string.Format( "{0}{1}.", newline, message );
errinline = true;
}
#endregion Unescaped ECHOed parenthesis inside code block
// Delayed variables with delayed variable expansion disabled vv.
#region Delayed variables without delayed expansion
if ( delayedvarexp )
{
pattern = @"%\w{1,16}%";
regex = new Regex( pattern );
if ( regex.IsMatch( line ) )
{
string envvar = regex.Match( line ).Value.ToUpper( );
string varname = envvar.Substring( 1, envvar.Length - 2 );
// Skip warning if we are dealing with a (dynamic) system variable
pattern = string.Format( @"^({0}|{1})$", dynvarpattern, sysvarpattern );
if ( !new Regex( pattern, ignorecase ).IsMatch( varname ) )
{
message = string.Format( "Variable \"{0}\" found with delayed variable expansion enabled; shouldn't that be \"!{1}!\" instead?", envvar, varname );
// Skip warning if line is a comment
if ( !new Regex( @"(^|&)\s*(REM($|\s)|::)", ignorecase ).IsMatch( line ) )
{
allwarnings += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
warnmsg += string.Format( "{0}{1}.", newline, message );
errinline = true;
}
}
}
}
else
{
pattern = @"!\w{1,16}!";
regex = new Regex( pattern );
if ( regex.IsMatch( line ) )
{
// Skip warning if line is a comment
if ( !new Regex( @"^\s*(REM($|\s)|::)", ignorecase ).IsMatch( line ) )
{
if ( delayedvarglobal )
{
message = string.Format( "\"{0}\" looks like a delayed variable, check if delayed variable expansion is enabled", regex.Match( line ).Value.ToUpper( ) );
allbadpr += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
badprmsg += string.Format( "{0}{1}.", newline, message );
}
else
{
message = string.Format( "\"{0}\" looks like a delayed variable, but delayed variable expansion is not enabled", regex.Match( line ).Value.ToUpper( ) );
allerrors += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
errormsg += string.Format( "{0}{1}.", newline, message );
}
errinline = true;
}
}
}
#endregion Delayed variables without delayed expansion
// Excess number of delimiters in FOR /F loop
#region Too many delimiters
pattern = @"(?<!\w)FOR\s+/F\s+\""(?:[^""]+\s)?delims=([^\s\)\""]{5,})(?:\s[^""]+)?\""";
regex = new Regex( pattern, ignorecase );
if ( regex.IsMatch( line ) )
{
string delims = regex.Match( line ).Groups[1].ToString( );
List<char> nodups = new List<char>( );
foreach ( char del in delims )
{
if ( !nodups.Contains( del ) )
{
nodups.Add( del );
}
}
if ( nodups.Count == delims.Length )
{
message = string.Format( "Found {0} delimiters in a FOR /F loop: \"{1}\". Are you trying to split a string on a WORD? The value of \"delims\" is a CHARACTER array, each individual character is a delimiter", delims.Length, delims, delims.Length - nodups.Count );
alltips += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
tipsmsg += string.Format( "{0}{1}.", newline, message );
}
else
{
message = string.Format( "Found {0} delimiters in a FOR /F loop: \"{1}\" (including {2} duplicates). Are you trying to split a string on a WORD? The value of \"delims\" is a CHARACTER array, each individual character is a delimiter", delims.Length, delims, delims.Length - nodups.Count );
allerrors += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
errormsg += string.Format( "{0}{1}.", newline, message );
}
errinline = true;
}
else
{
pattern = @"(?<!\w)FOR\s+/F\s+\""(?:[^""]+\s)?delims=([^\s\)\""]+)(?:\s[^""]+)?\""";
regex = new Regex( pattern, ignorecase );
if ( regex.IsMatch( line ) )
{
string delims = regex.Match( line ).Groups[1].ToString( );
List<char> nodups = new List<char>( );
foreach ( char del in delims )
{
if ( !nodups.Contains( del ) )
{
nodups.Add( del );
}
}
if ( delims.Length > nodups.Count )
{
message = string.Format( "Found {0} duplicate delimiters (delims={1}) in a FOR /F loop", delims.Length - nodups.Count, delims );
allerrors += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
errormsg += string.Format( "{0}{1}.", newline, message );
errinline = true;
}
}
}
#endregion Too many delimiters
// Wildcards in IF comparison
#region Wildcards in IF comparison
pattern = @"\bIF\s+(?:/I\s+)?(?:NOT\s+)?\""%\w{1,16}%\""\s*==\s*\""[^""\*\?]*[\*\?]";
regex = new Regex( pattern, ignorecase );
if ( regex.IsMatch( line ) )
{
message = "Wildcard character(s) found in IF comparison; IF \"%var%\"==\"literal_string\" does not accept wildcards; though asterisks and exclamation marks CAN be valid literals, make sure you did not try to apply them as wildcards";
alltips += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
tipsmsg += string.Format( "{0}{1}.", newline, message );
errinline = true;
}
#endregion Wildcards in IF comparison
// ELSE on new line
#region ELSE on new line
pattern = @"(^|\n)\s*ELSE";
regex = new Regex( pattern, ignorecase );
if ( regex.IsMatch( line ) )
{
// Missing error message and incorrect results reported by Bob Berry
message = "ELSE should never be placed on a new line; placing ELSE on a new line will result in incorrect results without a warning or error message";
alltips += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
tipsmsg += string.Format( "{0}{1}.", newline, message );
errinline = true;
}
#endregion ELSE on new line
// count SETLOCAL and ENDLOCAL pairs
#region SETLOCAL ENDLOCAL pairs
pattern = @"^\s*SETLOCAL\b";
regex = new Regex( pattern, ignorecase );
if ( regex.IsMatch( line ) )
{
setlocal += 1;
}
if ( setlocal > 1 )
{
message = "Found a nested SETLOCAL";
if ( allwarnings.IndexOf( message ) > -1 )
{
allwarnings += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
warnmsg += string.Format( "{0}{1}.", newline, message );
}
errinline = true;
}
pattern = @"^\s*ENDLOCAL\b";
regex = new Regex( pattern, ignorecase );
if ( regex.IsMatch( line ) )
{
setlocal -= 1;
}
if ( setlocal < 0 )
{
message = "Number of ENDLOCALs exceeds number of SETLOCALs";
allwarnings += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
warnmsg += string.Format( "{0}{1}.", newline, message );
errinline = true;
}
#endregion SETLOCAL ENDLOCAL pairs
// Whitespace and equal signs in IF statements
#region Whitespace and equal signs in IF statements
pattern = @"(^|\b)IF\s+(/I\s+)?(?:\w+|\""[^""]+\"")\s*==?\s*(?:\w+|\""[^""]+\"")\s*[\(\w]";
regex = new Regex( pattern, ignorecase );
if ( regex.IsMatch( line ) )
{
// Single equal sign
pattern = @"(^|\b)IF\s+(/I\s+)?(?:\w+|\""[^""]+\"")\s*=\s*(?:\w+|\""[^""]+\"")\s*[\(\w]";
regex = new Regex( pattern, RegexOptions.IgnoreCase );
if ( regex.IsMatch( line ) )
{
message = "Single equal sign in IF statement, where a double equal sign is required";
allerrors += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
errormsg += string.Format( "{0}{1}.", newline, message );
errinline = true;
}
// Whitespace before code block
pattern = @"(?:^|\b)(IF\s+(/I\s)?\s*(\w+|\""[^""]+\"")\s*==?\s*(\w+|\""[^""]+\"")\()";
regex = new Regex( pattern, ignorecase );
if ( regex.IsMatch( line ) )
{
MatchCollection matches = regex.Matches( line );
string wrong = matches[0].ToString( );
string correct = string.Format( "IF {0}{1}=={2} (", matches[0].Groups[2].ToString( ).ToUpper( ), matches[0].Groups[3].ToString( ), matches[0].Groups[4].ToString( ) );
message = string.Format( "Insert whitespace after the IF comparison, before the opening parenthesis of the code block: {0}...) instead of {1}...)", correct, wrong );
allerrors += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
errormsg += string.Format( "{0}{1}.", newline, message );
errinline = true;
}
}
#endregion Whitespace and equal signs in IF statements
}
}
public static void CheckLinefeeds( )
{
string testlinefeeds = allsrc.Replace( "\r\n", "" );
if ( testlinefeeds.Contains( "\r" ) || testlinefeeds.Contains( "\n" ) )
{
int linecount = 0;
foreach ( string line in allsrc.Split( new string[] { "\r\n" }, StringSplitOptions.None ) )
{
linecount += 1;
if ( line.Contains( "\r" ) || line.Contains( "\n" ) )
{
incorrectlinefeeds.Add( linecount );
}
}
}
}
public static void CheckUndefinedVariables( string line )
{
// Ignore if test for undefined variables is excluded
if ( checkundef )
{
string pattern;
Regex regex;
// Skip this test if variable is inside comment
pattern = @"^\s*(::|REM\s)";
regex = new Regex( pattern, ignorecase );
if ( !regex.IsMatch( line ) )
{
// This regex pattern catches "straight" variables as well as "nested" ones
pattern = @"((?<!%)%{1,2}|(?:\^!)?!)([\w\.-]{1," + maxvarlen + @"})(\1|:)";
regex = new Regex( pattern, ignorecase );
if ( regex.IsMatch( line ) )
{
MatchCollection matches = regex.Matches( line );
foreach ( Match match in matches )
{
string envvar = match.Groups[2].ToString( );
if ( !upcaseenvvars.Contains( envvar.ToUpper( ) ) )
{
if ( !envvarerrors.Contains( envvar.ToUpper( ) ) )
{
envvarerrors.Add( envvar.ToUpper( ) );
}
if ( includelocalvars || !localenvvars.Contains( envvar.ToUpper( ) ) )
{
if ( mysteryvars > 0 )
{
if ( mysteryvars == 1 )
{
message = string.Format( "No SET statement found setting variable \"{0}\"; however, a line was found setting a nested variable, which could account for this program's inability to find the SET statement for this particular variable", envvar );
}
else
{
message = string.Format( "No SET statement found setting variable \"{0}\"; however, lines were found setting nested variables, which could account for this program's inability to find the SET statement for this particular variable", envvar );
}
allbadpr += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
badprmsg += string.Format( "{0}{1}.", newline, message );
}
else
{
message = string.Format( "Undefined variable \"{0}\"", envvar );
allerrors += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
errormsg += string.Format( "{0}{1}.", newline, message );
}
}
else
{
message = string.Format( "Variable \"{0}\" is never set in the batch file; however, the variable IS set in this computer's local environment. Use BatCodeCheck's \"/E\" switch to include the local environment variables", envvar );
allbadpr += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
badprmsg += string.Format( "{0}{1}.", newline, message );
}
errinline = true;
}
}
}
}
}
}
public static void CheckVulnerabilities( string line )
{
if ( checkall || checkvulnr )
{
string pattern;
Regex regex;
// Unquoted %CD% code insertion vulnerability
#region Code insertion
if ( !incomment )
{
pattern = @"[^""%!](([%!])(CD|__CD__)(:~\d+(,\d+)?|:[^\2\n\r]+=[^\2\n\r]*)?\2)";
regex = new Regex( pattern, ignorecase );
if ( regex.IsMatch( line ) )
{
string cd = regex.Match( line ).Groups[1].ToString( ).ToUpper( );
message = string.Format( "Unquoted {0} makes the code vulnerable to code insertion (see http://www.thesecurityfactory.be/command-injection-windows.html)", cd );
allbadpr += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
badprmsg += string.Format( "{0}{1}.", newline, message );
errinline = true;
}
else
{
pattern = @"[^""](([%!])([%!])(CD|__CD__)(:~\d+(,\d+)?|:[^\2\3\n\r]+=[^\2\3\n\r]*)?\3\2)";
regex = new Regex( pattern, ignorecase );
if ( regex.IsMatch( line ) )
{
string cd = regex.Match( line ).Groups[1].ToString( ).ToUpper( );
message = string.Format( "Unquoted {0} makes the code vulnerable to code insertion (see http://www.thesecurityfactory.be/command-injection-windows.html)", cd );
allbadpr += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
badprmsg += string.Format( "{0}{1}.", newline, message );
errinline = true;
}
}
}
#endregion Code insertion
// SET /P vulnerability
#region SET /P
pattern = @"(^|\b)SET\s+/P\s+""?\w{1,16}\s*=";
regex = new Regex( pattern, ignorecase );
if ( regex.IsMatch( line ) )
{
message = "SET /P could make your code vulnerable to exploits (see http://www.robvanderwoude.com/battech_inputvalidation.php#SetP)";
allbadpr += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
badprmsg += string.Format( "{0}{1}.", newline, message );
errinline = true;
}
#endregion SET /P
}
}
public static void CloseHTML( )
{
if ( log2html )
{
htmlstream.WriteLine( "</span>" );
htmlstream.WriteLine( );
htmlstream.WriteLine( "</pre>" );
htmlstream.WriteLine( "</body>" );
htmlstream.Write( "</html>" );
htmlstream.Close( );
htmlopen = false;
}
}
public static void CollectEnvironmentVariables( )
{
string pattern;
Regex regex;
// Static system environment variables
foreach ( string envvar in sysvarpattern.Split( '|' ) )
{
//envvars.Add( envvar, 0 );
envvars[envvar] = 0;
upcaseenvvars.Add( envvar.ToUpper( ) );
}
// Dynamic system environment variables
foreach ( string envvar in dynvarpattern.Split( '|' ) )
{
//envvars.Add( envvar, 0 );
envvars[envvar] = 0;
upcaseenvvars.Add( envvar.ToUpper( ) );
}
// Environment variables SET in the source code with "plain" SET statement
pattern = @"(?:^|\b)SET\s+(\w{1," + maxvarlen + @"})=";
regex = new Regex( pattern, ignorecase );
if ( regex.IsMatch( allsrc_nocomments ) )
{
foreach ( Match match in regex.Matches( allsrc_nocomments ) )
{
if ( !upcaseenvvars.Contains( match.Groups[1].ToString( ).ToUpper( ) ) )
{
//envvars.Add( match.Groups[1].ToString( ), 0 );
envvars[match.Groups[1].ToString( )] = 0;
upcaseenvvars.Add( match.Groups[1].ToString( ).ToUpper( ) );
}
}
}
// Environment variables SET in the source code with SET /A or SET /P statement
pattern = @"(?:^|\b)SET\s+(?:/[AP]\s+)?""?(\w{1," + maxvarlen + @"})\s*=";
regex = new Regex( pattern, ignorecase );
if ( regex.IsMatch( allsrc_nocomments ) )
{
foreach ( Match match in regex.Matches( allsrc_nocomments ) )
{
if ( !upcaseenvvars.Contains( match.Groups[1].ToString( ).ToUpper( ) ) )
{
//envvars.Add( match.Groups[1].ToString( ), 0 );
envvars[match.Groups[1].ToString( )] = 0;
upcaseenvvars.Add( match.Groups[1].ToString( ).ToUpper( ) );
}
}
}
// Take into account lines like SET %%A or SET %Var%=...
pattern = @"(^|\b)\s*SET\s+[%!]";
regex = new Regex( pattern, ignorecase );
if ( regex.IsMatch( allsrc_nocomments ) )
{
MatchCollection matches = regex.Matches( allsrc_nocomments );
mysteryvars = matches.Count;
}
// Include local environment variables if /E command line switch was used
if ( includelocalvars )
{
foreach ( string envvar in Environment.GetEnvironmentVariables( ).Keys )
{
if ( !upcaseenvvars.Contains( envvar ) )
{
//envvars.Add( envvar, 0 );
envvars[envvar] = 0;
upcaseenvvars.Add( envvar.ToUpper( ) );
}
}
}
// Check how often a variable is used
// Temporarily clone the keys into a new array because we are going to modify the dictionary while iterating through its keys
string[] envvarskeys = envvars.Keys.ToArray( );
foreach ( string envvar in envvarskeys )
{
string substringpattern = @"~\-?\d+(,\d+)?";
string replacepattern = @"[^\1\=]{1,127}=[^\1\=]";
pattern = string.Format( @"(%|!){0}(\:({1}|{2}))\1", envvar, substringpattern, replacepattern );
regex = new Regex( pattern, ignorecase );
int used = regex.Matches( allsrc_nocomments ).Count;
envvars[envvar] = used;
}
// All loacal environment variables
foreach ( string envvar in Environment.GetEnvironmentVariables( ).Keys )
{
if ( !localenvvars.Contains( envvar.ToUpper( ) ) )
{
localenvvars.Add( envvar.ToUpper( ) );
}
}
}
public static void CollectLabels( )
{
string pattern;
Regex regex;
pattern = @"(?:^|\n|\r)\s*:([\w\.\[\]/+\-\(\)\{\}]{1,127})";
regex = new Regex( pattern );
if ( regex.IsMatch( allsrc_nocomments ) )
{
foreach ( Match match in regex.Matches( allsrc_nocomments ) )
{
string label = match.Groups[1].ToString( );
// Find the number of GOTOs to this label
pattern = string.Format( @"(?<=^|\s|\b|\()GOTO[ \t]+:?{0}(?=\n|\r|[ \t]+[\n\r]|\)|$)", label );
regex = new Regex( pattern, ignorecase );
int jumps = regex.Matches( allsrc_nocomments ).Count;
//labels.Add( label, jumps );
labels[label] = jumps;
upcaselabels.Add( label.ToUpper( ) );
}
}
}
public static void CollectSubroutines( )
{
// For each label, check if it is a subroutine label
foreach ( string label in labels.Keys )
{
if ( subroutines.IndexOfKey( label ) == -1 )
{
string pattern = string.Format( @"(?=\n|\r|^)\s*:{0}\s*[\n\r]+(?:[ \t]*(?:[^\:\s]|\:\:)[^\n\r]*[\n\r]+)*[ \t]*($|GOTO[ \t]*:EOF[ \t]*(?=\n|\r|\)|&|$))", label );
Regex regex = new Regex( pattern, ignorecase );
if ( regex.IsMatch( allsrc_nocomments ) )
{
MatchCollection matches = regex.Matches( allsrc_nocomments );
foreach ( Match match in matches )
{
string pattern2 = @"(?=\n|\r|^)\s*:([\w\.\[\]/+\-\(\)\{\}]{1,127})(?=\n|\r|\s|$)";
Regex regex2 = new Regex( pattern2, ignorecase );
string match2 = match.Groups[1].ToString( );
if ( !regex2.IsMatch( match2 ) )
{
//subroutines.Add( label, 0 );
subroutines[label] = 0;
}
}
}
}
}
// Find the number of CALLs to each subroutine
foreach ( string subroutine in subroutines.Keys )
{
string pattern = string.Format( @"(?<=^|\n|\r|\s|\b|&\()CALL[ \t]+:{{0}}(?=\n|\r|\s|\b|&|\)|$)" );
Regex regex = new Regex( pattern, ignorecase );
if ( regex.IsMatch( allsrc_nocomments ) )
{
MatchCollection matches = regex.Matches( allsrc_nocomments );
subroutines[subroutine] = matches.Count;
}
}
}
public static string ConsoleColor2CSSColor( ConsoleColor color )
{
return ConsoleColor2CSSColor( color.ToString( ) );
}
public static string ConsoleColor2CSSColor( string color )
{
string csscolor = color.ToLower( );
switch ( csscolor )
{
case "darkyellow": // darkyellow is not a valid CSS value, reported and fixed by Alfons van Zwol
csscolor = "#808000";
break;
}
return csscolor;
}
public static void CreateHTML( )
{
if ( log2html )
{
htmlstream = new StreamWriter( htmlfile );
htmlopen = true;
htmlstream.WriteLine( "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">" );
htmlstream.WriteLine( "<html>" );
htmlstream.WriteLine( "<head>" );
htmlstream.WriteLine( "<title>BatCodeCheck Version {0} report for {1}</title>", progver, Path.GetFileName( source ) );
htmlstream.WriteLine( "<style type=\"text/css\">" );
htmlstream.WriteLine( "body" );
htmlstream.WriteLine( "{" );
htmlstream.WriteLine( "\tbackground-color: {0};", windowbgcolor.ToString( ).ToLower( ) );
htmlstream.WriteLine( "\tcolor: {0};", windowfgcolor.ToString( ).ToLower( ) );
htmlstream.WriteLine( "}" );
htmlstream.WriteLine( "</style>" );
htmlstream.WriteLine( "</head>" );
htmlstream.WriteLine( "<body>" );
htmlstream.WriteLine( "<pre>" );
htmlstream.WriteLine( "<span style=\"color: white;\">Time : {0}", DateTime.Now.ToString( "yyyy-MM-dd HH:mm:ss" ) );
htmlstream.WriteLine( "Program : BatCodeCheck, Version {0}", progver );
htmlstream.WriteLine( "Arguments : {0}", allargs );
htmlstream.WriteLine( "File name : \"{0}\"", Path.GetFullPath( source ) );
htmlstream.WriteLine( "File date : {0}", File.GetLastWriteTime( source ).ToString( "yyyy-MM-dd HH:mm:ss" ) );
htmlstream.WriteLine( "File encoding : {0}", encoding );
htmlstream.WriteLine( "Tests : {0}", checks );
foreach ( char check in checks )
{
string test = check.ToString( ).ToUpper( );
htmlstream.WriteLine( " {0} = {1}", test, tests[test] );
}
if ( envvarerrors.Count > 0 )
{
htmlstream.Write( "Local environment variables are {1}", ( includelocalvars ? "included" : "ignored" ) );
}
htmlstream.WriteLine( "</span>\n" );
}
}
public static void DisplayLineNumber( int num )
{
if ( dispnums )
{
Console.BackgroundColor = linenumbgcolor;
Console.ForegroundColor = linenumfgcolor;
if ( num == lastline )
{
Console.Write( "{0,4} ", string.Empty );
WriteLineHTML( string.Format( "{0,4} ", string.Empty ) );
}
else
{
Console.Write( "{0,4} ", num );
WriteLineHTML( string.Format( "{0,4} ", num ) );
}
Console.ResetColor( );
Console.BackgroundColor = windowbgcolor;
Console.ForegroundColor = windowfgcolor;
lastline = num;
}
}
public static void DisplayMessagesPerLine( string line )
{
bool padline = true;
string blankline = new String( ' ', Console.WindowWidth );
string msgline;
if ( errinline )
{
if ( lasterrline != linenum )
{
lineswitherrors += 1;
}
foreach ( string error in errormsg.Split( "\n\r".ToCharArray( ) ) )
{
if ( !String.IsNullOrWhiteSpace( error ) )
{
if ( wrap )
{
msgline = WordWrap( ( error ).Replace( "\t", " ERROR: " ), padline );
}
else
{
msgline = ( error + blankline ).Replace( "\t", " ERROR: " ).Substring( 0, Console.WindowWidth - 1 );
}
Console.BackgroundColor = errorbgcolor;
Console.ForegroundColor = errorfgcolor;
Console.Write( msgline );
WriteLineHTML( msgline );
Console.ResetColor( );
Console.WriteLine( );
}
}
foreach ( string warning in warnmsg.Split( "\n\r".ToCharArray( ) ) )
{
if ( !String.IsNullOrWhiteSpace( warning ) )
{
if ( wrap )
{
msgline = WordWrap( ( warning ).Replace( "\t", " WARNING: " ), padline );
}
else
{
msgline = ( warning + blankline ).Replace( "\t", " WARNING: " ).Substring( 0, Console.WindowWidth - 1 );
}
Console.BackgroundColor = warningbgcolor;
Console.ForegroundColor = warningfgcolor;
Console.Write( msgline );
WriteLineHTML( msgline );
Console.ResetColor( );
Console.WriteLine( );
}
}
foreach ( string badpractice in badprmsg.Split( "\n\r".ToCharArray( ) ) )
{
if ( !String.IsNullOrWhiteSpace( badpractice ) )
{
if ( wrap )
{
msgline = WordWrap( ( badpractice ).Replace( "\t", " WARNING: " ), padline );
}
else
{
msgline = ( badpractice + blankline ).Replace( "\t", " WARNING: " ).Substring( 0, Console.WindowWidth - 1 );
}
Console.BackgroundColor = badprbgcolor;
Console.ForegroundColor = badprfgcolor;
Console.Write( msgline );
WriteLineHTML( msgline );
Console.ResetColor( );
Console.WriteLine( );
}
}
foreach ( string tip in tipsmsg.Split( "\n\r".ToCharArray( ) ) )
{
if ( !String.IsNullOrWhiteSpace( tip ) )
{
if ( wrap )
{
msgline = WordWrap( ( tip ).Replace( "\t", " TIP: " ), padline );
}
else
{
msgline = ( tip + blankline ).Replace( "\t", " TIP: " ).Substring( 0, Console.WindowWidth - 1 );
}
Console.BackgroundColor = tipsbgcolor;
Console.ForegroundColor = tipsfgcolor;
Console.Write( msgline );
WriteLineHTML( msgline );
Console.ResetColor( );
Console.WriteLine( );
}
}
errinline = false;
lasterrline = linenum;
rc = linenum;
}
}
public static void DisplayMessagesSummary( )
{
// Encoding
if ( encoding != "ASCII" )
{
allerrsum += string.Format( "{0}The batch file \"{1}\" is stored in {2} encoding, whereas it should be in ASCII", newline, source, encoding );
}
// Summary
if ( lineswitherrors == 1 )
{
allerrsum += string.Format( "{0}1 line generated a warning and should be examined", newline );
}
else if ( lineswitherrors > 1 )
{
allerrsum += string.Format( "{0}{1,-3} lines generated warnings and should be examined", newline, lineswitherrors );
}
// Incorrect linefeeds
if ( incorrectlinefeeds.Count == 1 )
{
allerrsum += String.Format( "{0}1 line is terminated with improper linefeeds (line {1})", newline, incorrectlinefeeds[0] );
}
if ( incorrectlinefeeds.Count > 1 )
{
allerrsum += String.Format( "{0}{1,-3} lines are terminated with improper linefeeds (lines {2})", newline, incorrectlinefeeds.Count, string.Join( ", ", incorrectlinefeeds.ToArray( ) ) );
}
// Unterminated %
if ( unterminatedpercentsigns == 1 )
{
allerrsum += string.Format( "{0}1 unterminated percent sign was found", newline );
}
else if ( unterminatedpercentsigns > 1 )
{
allerrsum += string.Format( "{0}{1,-3} unterminated percent signs were found", newline, unterminatedpercentsigns );
}
// Unterminated !
if ( unterminatedexclamations == 1 )
{
allerrsum += string.Format( "{0}1 unterminated exclamation mark was found while delayed variable expansion was enabled", newline );
}
else if ( unterminatedexclamations > 1 )
{
allerrsum += string.Format( "{0}{1,-3} unterminated exclamation marks were found while delayed variable expansion was enabled", newline, unterminatedexclamations );
}
// Unterminated "
if ( unterminateddoublequotes == 1 )
{
allerrsum += string.Format( "{0}1 unterminated doublequote was found", newline );
}
else if ( unterminateddoublequotes > 1 )
{
allerrsum += string.Format( "{0}{1,-3} unterminated doublequotes were found", newline, unterminateddoublequotes );
}
// Unterminated '
if ( unterminatedsinglequotes == 1 )
{
allerrsum += string.Format( "{0}1 unterminated singlequote was found in a FOR /F loop", newline );
}
else if ( unterminatedsinglequotes > 1 )
{
allerrsum += string.Format( "{0}{1,-3} unterminated singlequotes were found in FOR /F loops", newline, unterminatedsinglequotes );
}
// Undefined environment variables
string ignoredmsg = ( includelocalvars ? string.Empty : " (ignoring local environment variables)" );
if ( envvarerrors.Count == 1 )
{
allerrsum += string.Format( "{0}1 undefined variable was detected{1}: {2}", newline, ignoredmsg, envvarerrors[0] );
}
else if ( envvarerrors.Count > 1 )
{
allerrsum += string.Format( "{0}{1,-3} undefined variables were detected{2}: {3}", newline, envvarerrors.Count, ignoredmsg, string.Join( ", ", envvarerrors ) );
}
// Undefined labels
if ( gotoerrors.Count == 1 )
{
allerrsum += string.Format( "{0}1 label was missing: {1}", newline, gotoerrors[0] );
}
else if ( gotoerrors.Count > 1 )
{
allerrsum += string.Format( "{0}{1,-3} labels were missing: {2}", newline, gotoerrors.Count, string.Join( ", ", gotoerrors ) );
}
// Undefined subroutines
if ( callerrors.Count == 1 )
{
allerrsum += string.Format( "{0}1 subroutine label was missing (note that subroutine labels are case sensitive): {1}", newline, callerrors[0] );
}
else if ( callerrors.Count > 1 )
{
allerrsum += string.Format( "{0}{1,-3} subroutine labels were missing (note that subroutine labels are case sensitive): {2}", newline, callerrors.Count, string.Join( ", ", callerrors ) );
}
// Obsolete commands
if ( obsoletecmd == 1 )
{
allerrsum += string.Format( "{0}1 obsolete or 16-bit command was found", newline );
}
else if ( obsoletecmd > 1 )
{
allerrsum += string.Format( "{0}{1,-3} obsolete and/or 16-bit commands were found", newline, obsoletecmd );
}
// Unterminated parentheses
if ( parenthesis > 0 )
{
allerrsum += string.Format( "{0}Opening parenthesis outnumber closing parenthesis by {1}", newline, parenthesis );
}
else if ( parenthesis < 0 )
{
allerrsum += string.Format( "{0}Closing parenthesis outnumber opening parenthesis by {1}", newline, Math.Abs( parenthesis ) );
}
// Unterminated SETLOCALs
if ( setlocal > 0 )
{
allerrsum += string.Format( "{0}SETLOCALs outnumber ENDLOCALs by {1}", newline, setlocal );
}
else if ( setlocal < 0 )
{
allerrsum += string.Format( "{0}ENDLOCALs outnumber SETLOCALs by {1}", newline, Math.Abs( setlocal ) );
}
// Display warnings if applicable
if ( !String.IsNullOrWhiteSpace( allerrors ) )
{
allerrors = string.Format( "{0}ERRORS / VULNERABILITIES:{1}{0}", newline, allerrors );
}
if ( !String.IsNullOrWhiteSpace( allwarnings ) )
{
allerrors = string.Format( "{0}{1}WARNINGS:{2}{1}", allerrors, newline, allwarnings );
}
if ( !String.IsNullOrWhiteSpace( allbadpr ) )
{
allerrors = string.Format( "{0}{1}RISKY CODE:{2}{1}", allerrors, newline, allbadpr );
}
if ( !String.IsNullOrWhiteSpace( alltips ) )
{
allerrors = string.Format( "{0}{1}TIPS / BEST PRACTICES:{2}{1}", allerrors, newline, alltips );
}
if ( !String.IsNullOrWhiteSpace( allerrsum ) )
{
allerrors = string.Format( "{0}{1}SUMMARY:{2}{1}", allerrors, newline, allerrsum );
}
Console.Write( VerboseSummary( ) );
if ( !String.IsNullOrEmpty( allerrors ) )
{
allerrors += string.Format( "{0}{0}Note that some warnings are only displayed once. Correct the errors and run this test again.", newline );
Console.ResetColor( );
Console.WriteLine( newline );
Console.WriteLine( "Time : {0}", DateTime.Now.ToString( "yyyy-MM-dd HH:mm:ss" ) );
Console.WriteLine( "Program : BatCodeCheck, Version {0}", progver );
Console.WriteLine( "Arguments : {0}", allargs );
Console.WriteLine( "File name : \"{0}\"", Path.GetFullPath( source ) );
Console.WriteLine( "File date : {0}", File.GetLastWriteTime( source ).ToString( "yyyy-MM-dd HH:mm:ss" ) );
Console.WriteLine( "File encoding : {0}", encoding );
Console.WriteLine( "Tests : {0}", checks );
foreach ( char check in checks )
{
string test = check.ToString( ).ToUpper( );
Console.WriteLine( " {0} = {1}", test, tests[test] );
}
if ( envvarerrors.Count > 0 )
{
Console.WriteLine( "Local environment variables are {0}", ( includelocalvars ? "included" : "ignored" ) );
}
Console.WriteLine( );
Console.BackgroundColor = reportbgcolor;
Console.ForegroundColor = reportfgcolor;
if ( wrap )
{
string[] all_errors = allerrors.Split( "\n\r".ToCharArray( ) );
foreach ( string all_err in all_errors )
{
Console.WriteLine( WordWrap( all_err, false ) );
}
}
else
{
Console.WriteLine( allerrors );
}
WriteLineHTML( allerrors );
Console.ResetColor( );
Console.WriteLine( );
rc = Math.Max( 1, rc );
if ( logging )
{
StreamWriter log = new StreamWriter( logfile, !replacelog );
log.WriteLine( "Time : {0}", DateTime.Now.ToString( "yyyy-MM-dd HH:mm:ss" ) );
log.WriteLine( "Program : BatCodeCheck, Version {0}", progver );
log.WriteLine( "Arguments : {0}", allargs );
log.WriteLine( "File name : \"{0}\"", Path.GetFullPath( source ) );
log.WriteLine( "File date : {0}", File.GetLastWriteTime( source ).ToString( "yyyy-MM-dd HH:mm:ss" ) );
log.WriteLine( "File encoding : {0}", encoding );
log.WriteLine( "Tests : {0}", checks );
foreach ( char check in checks )
{
string test = check.ToString( ).ToUpper( );
log.WriteLine( " {0} = {1}", test, tests[test] );
}
if ( envvarerrors.Count > 0 )
{
log.WriteLine( "Local environment variables are {0}", ( includelocalvars ? "included" : "ignored" ) );
}
log.WriteLine( "{0}{1}{1}", allerrors, newline );
log.Close( );
}
}
}
public static string EscapedHTML( char chr )
{
switch ( chr )
{
case '&':
return "&";
case '>':
return ">";
case '<':
return "<";
default:
return chr.ToString( );
}
}
public static string GetEncoding( string filename )
{
// Based on code by Jason Pierce (2Toad) on StackOverflow.com
// https://stackoverflow.com/a/19283954
// Determines a text file's encoding by analyzing its byte order mark (BOM).
// Defaults to ASCII when detection of the text file's endianness fails.
// Read the BOM
var bom = new byte[4];
using ( var file = new FileStream( filename, FileMode.Open, FileAccess.Read ) )
{
file.Read( bom, 0, 4 );
}
// Analyze the BOM
if ( bom[0] == 0x2b && bom[1] == 0x2f && bom[2] == 0x76 )
return "UTF-7";
if ( bom[0] == 0xef && bom[1] == 0xbb && bom[2] == 0xbf )
return "UTF-8";
if ( bom[0] == 0xff && bom[1] == 0xfe && bom[2] == 0 && bom[3] == 0 )
return "UTF-32LE";
if ( bom[0] == 0xff && bom[1] == 0xfe )
return "UTF-16LE";
if ( bom[0] == 0xfe && bom[1] == 0xff )
return "UTF-16BE";
if ( bom[0] == 0 && bom[1] == 0 && bom[2] == 0xfe && bom[3] == 0xff )
return "UTF-32BE";
// We actually have no idea what the encoding is if we reach this point, so
// you may wish to return null instead of defaulting to ASCII
return "ASCII";
}
public static List<string> ListInternalCommands( )
{
string comspec = Environment.GetEnvironmentVariable( "COMSPEC" );
StreamReader file = new StreamReader( comspec, Encoding.ASCII );
string content = file.ReadToEnd( );
file.Close( );
List<string> intcmds = new List<string>( );
string excludelist = "ABOVENORMAL,AFFINITY,ANSI,APPICON,ASCII,AZ,BAT,BELOWNORMAL,BOTH,CMD,CMDCMDLINE,CMDEXTVERSION,COM,COMSPEC,CONFIG,COPYCMD,COPYRIGHT,CRLF,CSVFS,CTRL,CURRENT,DEFINED,DIRCMD,DISABLEDELAYEDEXPANSION,DISABLEEXTENSIONS,DLL,DO,DOC,DOS,DWORD,ENABLEDELAYEDEXPANSION,ENABLEEXTENSIONS,ELSE,ENTER,EOF,EQU,ERROR,ERRORLEVEL,EXE,EXIST,EXISTS,EXPAND,FALSE,FAT,FH,GEQ,GTR,GUI,HIGH,HIGHESTNUMANODENUMBER,HH,HKEY,HSM,IDI,IDLE,IN,INFO,IS,JS,KERNEL,LEQ,LIST,LNK,LOCAL,LOW,LSS,MACHINE,MAX,MIN,MM,MSC,MUI,NEQ,NODE,NORMAL,NOT,NT,NTDLL,NTFS,NY,NYA,OFF,ON,OTHER,PATHEXT,PROCESSING,RANDOM,REALTIME,REFS,REG,REGEDT,SCRIPT,SEPARATE,SHARED,STACK,SYS,SZ,TEMP,TWO,UNC,UNCC,UNKNOWN,US,USER,VAR,VBS,VERSION,VS,WAIT,WA,WC,WD,WINDOWS,WKERNEL,WORD,WP,WS,WV,XCOPY,XP";
List<string> exclude = excludelist.Split( ",".ToCharArray( ) ).ToList<string>( );
string pattern = @"([A-Z]\0){2,}";
Regex regex = new Regex( pattern );
if ( regex.IsMatch( content ) )
{
foreach ( Match match in regex.Matches( content ) )
{
string intcmd = match.ToString( ).Replace( "\0", string.Empty );
if ( !exclude.Contains( intcmd ) && !intcmds.Contains( intcmd ) )
{
intcmds.Add( intcmd );
}
}
intcmds.Sort( );
}
if ( intcmds.Count == 0 )
{
// Return a default list if we could not find the internal commands in %COMSPEC%
string defaultinternalcommands = @"ASSOC|BREAK|CALL|CD|CHDIR|CLS|COLOR|COPY|DATE|DEL|DIR|DPATH|ECHO|ENDLOCAL|ERASE|EXIT|FOR|FTYPE|GOTO|IF|KEYS|MD|MKDIR|MKLINK|MOVE|PATH|PAUSE|POPD|PROMPT|PUSHD|RD|REM|REN|RENAME|RMDIR|SET|SETLOCAL|SHIFT|START|TIME|TITLE|TYPE|VER|VERIFY|VOL";
intcmds = defaultinternalcommands.Split( "|".ToCharArray( ), StringSplitOptions.RemoveEmptyEntries ).ToList<string>( );
}
return intcmds;
}
public static int OpenHTMLFile( )
{
if ( openhtml )
{
try
{
if ( File.Exists( htmlfile ) )
{
Process.Start( htmlfile );
}
else
{
return ShowHelp( string.Format( "Cannot find HTML log file \"{0}\"", htmlfile ) );
}
}
catch ( Exception e )
{
return ShowHelp( e.Message );
}
}
return 0;
}
public static int OpenLogFile( )
{
if ( openlog )
{
try
{
if ( File.Exists( logfile ) )
{
Process.Start( logfile );
}
else
{
return ShowHelp( string.Format( "Cannot find ASCII log file \"{0}\"", logfile ) );
}
}
catch ( Exception e )
{
return ShowHelp( e.Message );
}
}
return 0;
}
public static void ParseLine( string line )
{
string nocomments = StripComments( line );
ResetInlineVariables( );
DisplayLineNumber( linenum );
CheckCommonMistakes( nocomments );
CheckUndefinedVariables( nocomments );
CheckCommandLineArguments( nocomments );
CheckBadPractices( nocomments );
CheckBestPractices( nocomments );
ParseLineAndHighlight( line );
CheckVulnerabilities( nocomments );
DisplayMessagesPerLine( line );
}
public static void ParseLineAndHighlight( string line )
{
// Comments are highlighted and not interpreted at all.
// This may spell trouble in case of multiple commands on a single line.
if ( new Regex( @"^\s*(REM($|\s)|::)", ignorecase ).IsMatch( line ) )
{
string match = new Regex( @"^(\s*(REM($|\s)|::))", ignorecase ).Match( line ).ToString( );
Console.ForegroundColor = intcmdfgcolor;
Console.Write( match );
WriteSpanHTML( match );
Console.ForegroundColor = commentfgscolor;
line = line.Substring( match.Length );
Console.WriteLine( line );
WriteSpanHTML( line );
Console.ResetColor( );
}
// do not highlight ECHO ON or ECHO OFF as ECHOed text
else if ( new Regex( @"^\s*@?ECHO\s+(ON|OFF)\s*$", ignorecase ).IsMatch( line ) )
{
Console.ResetColor( );
Console.WriteLine( line );
WriteSpanHTML( line );
}
// check if delayed variable expansion is enabled (will choke on nested SETLOCALs)
else if ( new Regex( @"^\s*SETLOCAL\s+((EN|DIS)ABLEEXTENSIONS\s+)?ENABLEDELAYEDEXPANSION(\b|$)", ignorecase ).IsMatch( line ) )
{
delayedvarexp = true;
delayedvarglobal = true;
Console.ResetColor( );
Console.WriteLine( line );
WriteSpanHTML( line );
}
// check if delayed variable expansion is disabled (will choke on nested SETLOCALs)
else if ( new Regex( @"^\s*ENDLOCAL\b", ignorecase ).IsMatch( line ) )
{
delayedvarexp = false;
Console.ResetColor( );
Console.WriteLine( line );
WriteSpanHTML( line );
}
else
{
// ParseLinePerCharacter( line );
ParseLinePerCharacter2( line );
}
}
public static void ParseLinePerCharacter( string line )
{
#region Initialize Inline Variables
string part = string.Empty;
string buildword = string.Empty;
string lastword = string.Empty;
int doublequotes = 0;
int singlequotes = 0;
bool doublepercent = false;
bool doublequoted = false;
bool echoed = false;
bool escnext = false;
bool forfloop = false;
bool inexcl = false;
bool inperc = false;
bool isparenthesis = false;
bool redirection = false;
bool singlequoted = false;
bool tempdblquoted = false;
bool tempinexcl = false;
bool tempinperc = false;
bool tempsngquoted = false;
bool tildestr = false;
#endregion Initialize Inline Variables
// check if inside a FOR /F loop
forfloop = new Regex( @"(^\s*|\b|\(|\&)FOR\s+/F\s", ignorecase ).IsMatch( line );
char prevchr = ' ';
int index = -1;
foreach ( char chr in line )
{
index += 1;
#region Character Type
switch ( chr )
{
case '"':
doublequoted = ( doublequoted ^ !escnext ) && !echoed;
if ( !escnext && !echoed )
{
doublequotes += 1;
}
doublepercent = false;
escnext = false;
inexcl = false;
inperc = false;
redirection = false;
tildestr = false;
break;
case (char) 39:
singlequoted = ( singlequoted ^ !escnext ) && !echoed;
if ( !escnext && !echoed && !doublequoted )
{
singlequotes += 1;
}
doublepercent = false;
escnext = false;
inexcl = false;
inperc = false;
redirection = false;
tildestr = false;
break;
case '^':
doublepercent = false;
escnext = ( prevchr != chr );
inexcl = false;
inperc = false;
tildestr = false;
break;
case '%':
if ( prevchr == '%' )
{
doublepercent = true;
}
else
{
doublepercent = false;
inperc = !inperc;
}
escnext = false;
inexcl = false;
redirection = false;
tildestr = false;
break;
case '!':
if ( delayedvarexp )
{
inexcl = !inexcl;
}
doublepercent = false;
escnext = false;
inperc = false;
redirection = false;
tildestr = false;
break;
case '~':
if ( prevchr == '%' )
{
inperc = false;
tildestr = true;
}
else if ( prevchr == '!' )
{
inexcl = false;
tildestr = true;
}
escnext = false;
redirection = false;
break;
case '&':
if ( prevchr == chr )
{
doublequoted = false;
echoed = false;
inperc = false;
redirection = false;
}
else
{
redirection = !escnext;
}
if ( singlequoted && redirection )
{
message = "Unescaped ampersand(s) in singlequoted text (FOR /F loop" + ( forfloop ? string.Empty : "?" ) + ")";
if ( errormsg.IndexOf( message ) > -1 )
{
allerrors += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
errormsg += string.Format( "{0}{1}.", newline, message );
}
errinline = true;
}
doublepercent = false;
escnext = false;
inexcl = false;
inperc = false;
tildestr = false;
break;
case '|':
if ( prevchr == chr )
{
doublequoted = false;
echoed = false;
inperc = false;
redirection = false;
}
else
{
redirection = !escnext;
}
if ( singlequoted && redirection )
{
message = "Unescaped pipe symbol(s) in singlequoted text (FOR /F loop" + ( forfloop ? string.Empty : "?" ) + ")";
if ( errormsg.IndexOf( message ) > -1 )
{
allerrors += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
errormsg += string.Format( "{0}{1}.", newline, message );
}
errinline = true;
}
doublepercent = false;
escnext = false;
echoed = false;
incomment = false;
inexcl = false;
inperc = false;
tildestr = false;
break;
case '>':
redirection = !escnext;
if ( singlequoted && redirection )
{
message = "Unescaped \"greater than\" symbol(s) in singlequoted text (FOR /F loop" + ( forfloop ? string.Empty : "?" ) + ")";
if ( errormsg.IndexOf( message ) > -1 )
{
allerrors += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
errormsg += string.Format( "{0}{1}.", newline, message );
}
errinline = true;
}
doublepercent = false;
escnext = false;
echoed = false;
incomment = false;
inexcl = false;
inperc = false;
tildestr = false;
break;
case '<':
redirection = !escnext;
if ( singlequoted && redirection )
{
message = "Unescaped \"less than\" symbol(s) in singlequoted text (FOR /F loop" + ( forfloop ? string.Empty : "?" ) + ")";
if ( errormsg.IndexOf( message ) > -1 )
{
allerrors += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
errormsg += string.Format( "{0}{1}.", newline, message );
}
errinline = true;
}
doublepercent = false;
escnext = false;
inexcl = false;
inperc = false;
tildestr = false;
break;
case ' ':
case '\t':
if ( inexcl )
{
unterminatedexclamations += 1;
message = "Unterminated exclamation marks or whitespace in (delayed) variable name";
if ( errormsg.IndexOf( message ) > -1 )
{
allerrors += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
errormsg += string.Format( "{0}{1}.", newline, message );
}
errinline = true;
}
if ( inperc )
{
unterminatedexclamations += 1;
message = "Unterminated percent signs or whitespace in variable name";
if ( errormsg.IndexOf( message ) > -1 )
{
allerrors += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
errormsg += string.Format( "{0}{1}.", newline, message );
}
errinline = true;
}
doublepercent = false;
escnext = false;
inexcl = false;
inperc = false;
redirection = false;
tildestr = false;
break;
case '(':
if ( !echoed && !escnext )
{
parenthesis += 1;
}
doublepercent = false;
//inexcl = false;
//inperc = false;
isparenthesis = true;
redirection = false;
tildestr = false;
break;
case ')':
if ( !echoed && !escnext )
{
parenthesis -= 1;
}
if ( parenthesis < 0 )
{
message = "Too many closing parenthesis";
if ( allerrors.IndexOf( message ) == -1 )
{
allerrors += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
errormsg += string.Format( "{0}{1}.", newline, message );
errinline = true;
}
}
doublepercent = false;
//inexcl = false;
//inperc = false;
isparenthesis = true;
redirection = false;
tildestr = false;
break;
case 'a':
case 'd':
case 'f':
case 'n':
case 'p':
case 's':
case 't':
case 'x':
case 'z':
// "$PATH:" is ignored in the highlighting
if ( tildestr )
{
inexcl = false;
inperc = false;
tildestr = true;
}
escnext = false;
redirection = false;
break;
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
if ( ( prevchr == '%' ) || tildestr )
{
inexcl = false;
inperc = false;
tildestr = true;
}
escnext = false;
redirection = false;
break;
default:
doublepercent = false;
escnext = false;
redirection = false;
tildestr = false;
if ( tildestr )
{
if ( "ABCDEFGHIJKLMNOPQRSTUVWXYZ?".IndexOf( chr.ToString( ), StringComparison.InvariantCultureIgnoreCase ) > -1 )
{
if ( "adfnpstxz".IndexOf( prevchr.ToString( ) ) > -1 )
{
if ( line.IndexOf( "%%" + chr.ToString( ) ) > 3 && line.IndexOf( "%%" + chr.ToString( ) ) < index )
{
tildestr = true;
}
}
}
}
break;
}
#endregion Character Type
// check for unescaped redirection symbols in FOR /F loops
if ( redirection && singlequoted && ( parenthesis % 2 == 1 ) )
{
message = "Unescaped redirection symbol(s) in single quoted text (FOR /F loop?)";
if ( errormsg.IndexOf( message ) > -1 )
{
allerrors += string.Format( "{0}Line {1,5:0}:\t{2}", newline, linenum, message );
errormsg += string.Format( "{0}{1}.", newline, message );
}
errinline = true;
}
#region ECHOed text and comments
// check if we are dealing with ECHOed text or comments
if ( chr == ' ' || chr == '\t' )
{
lastword = buildword.ToUpper( ).Trim( );
buildword = string.Empty;
}
else
{
buildword += chr.ToString( );
}
if ( lastword == "ECHO" || lastword == "@ECHO" )
{
echoed = true;
}
else if ( ( lastword == "REM" || lastword == "::" ) && !echoed )
{
incomment = true;
commentlines += 1;
}
#endregion ECHOed text and comments
// as soon as an unescaped redirection symbol is encountered, we no longer have ECHOed text
if ( redirection && !doublequoted && !escnext )
{
echoed = false;
}
#region Highlighting
// correctly highlight the character we just parsed
if ( redirection )
{
// redirection symbols and parenthesis are highlighted in red
Console.ForegroundColor = redirectionfgcolor;
}
else if ( inexcl || inperc || tildestr || doublepercent || tempinexcl || tempinperc )
{
// variables and parameters are highlighted in green
Console.ForegroundColor = variablesfgcolor;
}
else if ( isparenthesis && !echoed )
{
// redirection symbols and parenthesis are highlighted in red
Console.ForegroundColor = parenthesisfgcolor;
}
else if ( echoed )
{
// ECHOed text is highlighted in cyan, with the exception of variables
Console.ForegroundColor =