// Caller has provided an empty string for aScriptFilename if this is a compiled script.
// Otherwise, aScriptFilename can be NULL if caller hasn't determined the filename of the script yet.
{
mIsRestart = aIsRestart;
char buf[2048]; // Just to make sure we have plenty of room to do things with.
#ifdef AUTOHOTKEYSC
// Fix for v1.0.29: Override the caller's use of __argv[0] by using GetModuleFileName(),
// so that when the script is started from the command line but the user didn't type the
// extension, the extension will be included. This necessary because otherwise
// #SingleInstance wouldn't be able to detect duplicate versions in every case.
// It also provides more consistency.
GetModuleFileName(NULL, buf, sizeof(buf));
#else
if (!aScriptFilename) // v1.0.46.08: Change in policy: store the default script in the My Documents directory rather than in Program Files. It's more correct and solves issues that occur due to Vista's file-protection scheme.
{
// Since no script-file was specified on the command line, use the default name.
// For backward compatibility, FIRST check if there's an AutoHotkey.ini file in the current
// directory. If there is, that needs to be used to retain compatibility.
aScriptFilename = NAME_P ".ini";
if (GetFileAttributes(aScriptFilename) == 0xFFFFFFFF) // File doesn't exist, so fall back to new method.
{
aScriptFilename = buf;
VarSizeType filespec_length = BIV_MyDocuments(aScriptFilename, ""); // e.g. C:\Documents and Settings\Home\My Documents
if (filespec_length > sizeof(buf)-16) // Need room for 16 characters ('\\' + "AutoHotkey.ahk" + terminator).
return FAIL; // Very rare, so for simplicity just abort.
strcpy(aScriptFilename + filespec_length, "\\AutoHotkey.ahk"); // Append the filename: .ahk vs. .ini seems slightly better in terms of clarity and usefulness (e.g. the ability to double click the default script to launch it).
// Now everything is set up right because even if aScriptFilename is a nonexistent file, the
// user will be prompted to create it by a stage further below.
}
//else since the legacy .ini file exists, everything is now set up right. (The file might be a directory, but that isn't checked due to rarity.)
}
// In case the script is a relative filespec (relative to current working dir):
char *unused;
if (!GetFullPathName(aScriptFilename, sizeof(buf), buf, &unused)) // This is also relied upon by mIncludeLibraryFunctionsThenExit. Succeeds even on nonexistent files.
return FAIL; // Due to rarity, no error msg, just abort.
#endif
// Using the correct case not only makes it look better in title bar & tray tool tip,
// it also helps with the detection of "this script already running" since otherwise
// it might not find the dupe if the same script name is launched with different
// lowercase/uppercase letters:
ConvertFilespecToCorrectCase(buf); // This might change the length, e.g. due to expansion of 8.3 filename.
char *filename_marker;
if ( !(filename_marker = strrchr(buf, '\\')) )
filename_marker = buf;
else
++filename_marker;
if ( !(mFileSpec = SimpleHeap::Malloc(buf)) ) // The full spec is stored for convenience, and it's relied upon by mIncludeLibraryFunctionsThenExit.
return FAIL; // It already displayed the error for us.
filename_marker[-1] = '\0'; // Terminate buf in this position to divide the string.
if ( !(mMainWindowTitle = SimpleHeap::Malloc(buf)) )
return FAIL; // It already displayed the error for us.
// It may be better to get the module name this way rather than reading it from the registry
// (though it might be more proper to parse it out of the command line args or something),
// in case the user has moved it to a folder other than the install folder, hasn't installed it,
// or has renamed the EXE file itself. Also, enclose the full filespec of the module in double
// quotes since that's how callers usually want it because ActionExec() currently needs it that way:
*buf = '"';
if (GetModuleFileName(NULL, buf + 1, sizeof(buf) - 2)) // -2 to leave room for the enclosing double quotes.
{
size_t buf_length = strlen(buf);
buf[buf_length++] = '"';
buf[buf_length] = '\0';
if ( !(mOurEXE = SimpleHeap::Malloc(buf)) )
return FAIL; // It already displayed the error for us.
else
{
char *last_backslash = strrchr(buf, '\\');
if (!last_backslash) // probably can't happen due to the nature of GetModuleFileName().
mOurEXEDir = "";
last_backslash[1] = '\0'; // i.e. keep the trailing backslash for convenience.
if ( !(mOurEXEDir = SimpleHeap::Malloc(buf + 1)) ) // +1 to omit the leading double-quote.
return FAIL; // It already displayed the error for us.
}
}
return OK;
}
ResultType Script::CreateWindows()
// Returns OK or FAIL.
{
if (!mMainWindowTitle || !*mMainWindowTitle) return FAIL; // Init() must be called before this function.
// Register a window class for the main window:
WNDCLASSEX wc = {0};
wc.cbSize = sizeof(wc);
wc.lpszClassName = WINDOW_CLASS_MAIN;
wc.hInstance = g_hInstance;
wc.lpfnWndProc = MainWindowProc;
// The following are left at the default of NULL/0 set higher above:
//wc.style = 0; // CS_HREDRAW | CS_VREDRAW
//wc.cbClsExtra = 0;
//wc.cbWndExtra = 0;
wc.hIcon = wc.hIconSm = (HICON)LoadImage(g_hInstance, MAKEINTRESOURCE(IDI_MAIN), IMAGE_ICON, 0, 0, LR_SHARED); // Use LR_SHARED to conserve memory (since the main icon is loaded for so many purposes).
mNIC.hIcon = mCustomIcon ? mCustomIcon : (HICON)LoadImage(g_hInstance, MAKEINTRESOURCE(g_IconTray), IMAGE_ICON, 0, 0, LR_SHARED); // Use LR_SHARED to conserve memory (since the main icon is loaded for so many purposes).
#endif
UPDATE_TIP_FIELD
// If we were called due to an Explorer crash, I don't think it's necessary to call
// Shell_NotifyIcon() to remove the old tray icon because it was likely destroyed
// along with Explorer. So just add it unconditionally:
if (!Shell_NotifyIcon(NIM_ADD, &mNIC))
mNIC.hWnd = NULL; // Set this as an indicator that tray icon is not installed.
: (HICON)LoadImage(g_hInstance, MAKEINTRESOURCE(icon), IMAGE_ICON, 0, 0, LR_SHARED); // Use LR_SHARED for simplicity and performance more than to conserve memory in this case.
if (Shell_NotifyIcon(NIM_MODIFY, &mNIC))
{
icon_shows_paused = g.IsPaused;
icon_shows_suspended = g_IsSuspended;
}
// else do nothing, just leave it in the same state.
}
ResultType Script::AutoExecSection()
{
if (!mIsReadyToExecute)
return FAIL;
if (mFirstLine != NULL)
{
// Choose a timeout that's a reasonable compromise between the following competing priorities:
// 1) That we want hotkeys to be responsive as soon as possible after the program launches
// in case the user launches by pressing ENTER on a script, for example, and then immediately
// tries to use a hotkey. In addition, we want any timed subroutines to start running ASAP
// because in rare cases the user might rely upon that happening.
// 2) To support the case when the auto-execute section never finishes (such as when it contains
// an infinite loop to do background processing), yet we still want to allow the script
// to put custom defaults into effect globally (for things such as KeyDelay).
// Obviously, the above approach has its flaws; there are ways to construct a script that would
// result in unexpected behavior. However, the combination of this approach with the fact that
// the global defaults are updated *again* when/if the auto-execute section finally completes
// raises the expectation of proper behavior to a very high level. In any case, I'm not sure there
// is any better approach that wouldn't break existing scripts or require a redesign of some kind.
// If this method proves unreliable due to disk activity slowing the program down to a crawl during
// the critical milliseconds after launch, one thing that might fix that is to have ExecUntil()
// be forced to run a minimum of, say, 100 lines (if there are that many) before allowing the
// timer expiration to have its effect. But that's getting complicated and I'd rather not do it
// unless someone actually reports that such a thing ever happens. Still, to reduce the chance
// of such a thing ever happening, it seems best to boost the timeout from 50 up to 100:
SET_AUTOEXEC_TIMER(100);
AutoExecSectionIsRunning = true;
// v1.0.25: This is now done here, closer to the actual execution of the first line in the script,
// to avoid an unnecessary Sleep(10) that would otherwise occur in ExecUntil:
mLastScriptRest = mLastPeekTime = GetTickCount();
++g_nThreads;
ResultType result = mFirstLine->ExecUntil(UNTIL_RETURN);
--g_nThreads;
KILL_AUTOEXEC_TIMER // This also does "g.AllowThreadToBeInterrupted = true"
AutoExecSectionIsRunning = false;
return result;
}
return OK;
}
ResultType Script::Edit()
{
#ifdef AUTOHOTKEYSC
return OK; // Do nothing.
#else
// This is here in case a compiled script ever uses the Edit command. Since the "Edit This
// Script" menu item is not available for compiled scripts, it can't be called from there.
TitleMatchModes old_mode = g.TitleMatchMode;
g.TitleMatchMode = FIND_ANYWHERE;
HWND hwnd = WinExist(g, mFileName, "", mMainWindowTitle, ""); // Exclude our own main window.
InitNewThread(0, true, true, ACT_INVALID); // Since this special thread should always run, no checking of g_MaxThreadsTotal is done before calling this.
if (g_nFileDialogs) // See MsgSleep() for comments on this.
SetCurrentDirectory(g_WorkingDir);
// Use g.AllowThreadToBeInterrupted to forbid any hotkeys, timers, or user defined menu items
// to interrupt. This is mainly done for peace-of-mind (since possible interactions due to
// interruptions have not been studied) and the fact that this most users would not want this
// subroutine to be interruptible (it usually runs quickly anyway). Another reason to make
// it non-interruptible is that some OnExit subroutines might destruct things used by the
// script's hotkeys/timers/menu items, and activating these items during the deconstruction
// would not be safe. Finally, if a logoff or shutdown is occurring, it seems best to prevent
// timed subroutines from running -- which might take too much time and prevent the exit from
// occurring in a timely fashion. An option can be added via the FutureUse param to make it
// interruptible if there is ever a demand for that. UPDATE: g_AllowInterruption is now used
// instead of g.AllowThreadToBeInterrupted for two reasons:
// 1) It avoids the need to do "int mUninterruptedLineCountMax_prev = g_script.mUninterruptedLineCountMax;"
// (Disable this item so that ExecUntil() won't automatically make our new thread uninterruptible
// after it has executed a certain number of lines).
// 2) If the thread we're interrupting is uninterruptible, the uinterruptible timer might be
// currently pending. When it fires, it would make the OnExit subroutine interruptible
// rather than the underlying subroutine. The above fixes the first part of that problem.
// The 2nd part is fixed by reinstating the timer when the uninterruptible thread is resumed.
// This special handling is only necessary here -- not in other places where new threads are
// created -- because OnExit is the only type of thread that can interrupt an uninterruptible
// thread.
bool g_AllowInterruption_prev = g_AllowInterruption; // Save current setting.
g_AllowInterruption = false; // Mark the thread just created above as permanently uninterruptible (i.e. until it finishes and is destroyed).
// This addresses the 2nd part of the problem described in the above large comment:
if (!mFileSpec || !*mFileSpec) return LOADING_FAILED;
#ifndef AUTOHOTKEYSC // When not in stand-alone mode, read an external script file.
DWORD attr = GetFileAttributes(mFileSpec);
if (attr == MAXDWORD) // File does not exist or lacking the authorization to get its attributes.
{
char buf[MAX_PATH + 256];
if (aScriptWasNotspecified) // v1.0.46.09: Give a more descriptive prompt to help users get started.
{
snprintf(buf, sizeof(buf),
"To help you get started, would you like to create a sample script in the My Documents folder?\n"
"\n"
"Press YES to create and display the sample script.\n"
"Press NO to exit.\n");
}
else // Mostly for backward compatibility, also prompt to create if an explicitly specified script doesn't exist.
snprintf(buf, sizeof(buf), "The script file \"%s\" does not exist. Create it now?", mFileSpec);
int response = MsgBox(buf, MB_YESNO);
if (response != IDYES)
return 0;
FILE *fp2 = fopen(mFileSpec, "a");
if (!fp2)
{
MsgBox("Could not create file, perhaps because the current directory is read-only"
" or has insufficient permissions.");
return LOADING_FAILED;
}
fputs(
"; IMPORTANT INFO ABOUT GETTING STARTED: Lines that start with a\n"
"; semicolon, such as this one, are comments. They are not executed.\n"
"\n"
"; This script has a special filename and path because it is automatically\n"
"; launched when you run the program directly. Also, any text file whose\n"
"; name ends in .ahk is associated with the program, which means that it\n"
"; can be launched simply by double-clicking it. You can have as many .ahk\n"
"; files as you want, located in any folder. You can also run more than\n"
"; one ahk file simultaneously and each will get its own tray icon.\n"
"\n"
"; SAMPLE HOTKEYS: Below are two sample hotkeys. The first is Win+Z and it\n"
"; launches a web site in the default browser. The second is Control+Alt+N\n"
"; and it launches a new Notepad window (or activates an existing one). To\n"
"; try out these hotkeys, run AutoHotkey again, which will load this file.\n"
"\n"
"#z::Run www.autohotkey.com\n"
"\n"
"^!n::\n"
"IfWinExist Untitled - Notepad\n"
"\tWinActivate\n"
"else\n"
"\tRun Notepad\n"
"return\n"
"\n"
"\n"
"; Note: From now on whenever you run AutoHotkey directly, this script\n"
"; will be loaded. So feel free to customize it to suit your needs.\n"
"\n"
"; Please read the QUICK-START TUTORIAL near the top of the help file.\n"
"; It explains how to perform common automation tasks such as sending\n"
"; keystrokes and mouse clicks. It also explains more about hotkeys.\n"
"\n"
, fp2);
fclose(fp2);
// One or both of the below would probably fail -- at least on Win95 -- if mFileSpec ever
// has spaces in it (since it's passed as the entire param string). So enclose the filename
// in double quotes. I don't believe the directory needs to be in double quotes since it's
// a separate field within the CreateProcess() and ShellExecute() structures:
snprintf(buf, sizeof(buf), "\"%s\"", mFileSpec);
if (!ActionExec("edit", buf, mFileDir, false))
if (!ActionExec("Notepad.exe", buf, mFileDir, false))
{
MsgBox("Can't open script."); // Short msg since so rare.
return LOADING_FAILED;
}
// future: have it wait for the process to close, then try to open the script again:
return 0;
}
#endif
// v1.0.42: Placeholder to use in place of a NULL label to simplify code in some places.
// This must be created before loading the script because it's relied upon when creating
// hotkeys to provide an alternative to having a NULL label. It will be given a non-NULL
// mJumpToLine further down.
if ( !(mPlaceholderLabel = new Label("")) ) // Not added to linked list since it's never looked up.
return LOADING_FAILED;
// Load the main script file. This will also load any files it includes with #Include.
if ( LoadIncludedFile(mFileSpec, false, false) != OK
|| !AddLine(ACT_EXIT) // Fix for v1.0.47.04: Add an Exit because otherwise, a script that ends in an IF-statement will crash in PreparseBlocks() because PreparseBlocks() expects every IF-statements mNextLine to be non-NULL (helps loading performance too).
|| !PreparseBlocks(mFirstLine) ) // Must preparse the blocks before preparsing the If/Else's further below because If/Else may rely on blocks.
return LOADING_FAILED; // Error was already displayed by the above calls.
// ABOVE: In v1.0.47, the above may have auto-included additional files from the userlib/stdlib.
// That's why the above is done prior to adding the EXIT lines and other things below.
#ifndef AUTOHOTKEYSC
if (mIncludeLibraryFunctionsThenExit)
{
fclose(mIncludeLibraryFunctionsThenExit);
return 0; // Tell our caller to do a normal exit.
}
#endif
// v1.0.35.11: Restore original working directory so that changes made to it by the above (via
// "#Include C:\Scripts" or "#Include %A_ScriptDir%" or even stdlib/userlib) do not affect the
// script's runtime working directory. This preserves the flexibility of having a startup-determined
// working directory for the script's runtime (i.e. it seems best that the mere presence of
// "#Include NewDir" should not entirely eliminate this flexibility).
SetCurrentDirectory(g_WorkingDirOrig); // g_WorkingDirOrig previously set by WinMain().
// Rather than do this, which seems kinda nasty if ever someday support same-line
// else actions such as "else return", just add two EXITs to the end of every script.
// That way, if the first EXIT added accidentally "corrects" an actionless ELSE
// or IF, the second one will serve as the anchoring end-point (mRelatedLine) for that
// IF or ELSE. In other words, since we never want mRelatedLine to be NULL, this should
// make absolutely sure of that:
//if (mLastLine->mActionType == ACT_ELSE ||
// ACT_IS_IF(mLastLine->mActionType)
// ...
// Second ACT_EXIT: even if the last line of the script is already "exit", always add
// another one in case the script ends in a label. That way, every label will have
// a non-NULL target, which simplifies other aspects of script execution.
// Making sure that all scripts end with an EXIT ensures that if the script
// file ends with ELSEless IF or an ELSE, that IF's or ELSE's mRelatedLine
// will be non-NULL, which further simplifies script execution.
// Not done since it's number doesn't much matter: ++mCombinedLineNumber;
++mCombinedLineNumber; // So that the EXITs will both show up in ListLines as the line # after the last physical one in the script.
if (!(AddLine(ACT_EXIT) && AddLine(ACT_EXIT))) // Second exit guaranties non-NULL mRelatedLine(s).
return LOADING_FAILED;
mPlaceholderLabel->mJumpToLine = mLastLine; // To follow the rule "all labels should have a non-NULL line before the script starts running".
if (!PreparseIfElse(mFirstLine))
return LOADING_FAILED; // Error was already displayed by the above calls.
// Use FindOrAdd, not Add, because the user may already have added it simply by
// referring to it in the script:
if ( !(g_ErrorLevel = FindOrAddVar("ErrorLevel")) )
return LOADING_FAILED; // Error. Above already displayed it for us.
// Initialize the var state to zero right before running anything in the script:
g_ErrorLevel->Assign(ERRORLEVEL_NONE);
// Initialize the random number generator:
// Note: On 32-bit hardware, the generator module uses only 2506 bytes of static
// data, so it doesn't seem worthwhile to put it in a class (so that the mem is
// only allocated on first use of the generator). For v1.0.24, _ftime() is not
// used since it could be as large as 0.5 KB of non-compressed code. A simple call to
// GetSystemTimeAsFileTime() seems just as good or better, since it produces
// a FILETIME, which is "the number of 100-nanosecond intervals since January 1, 1601."
// Use the low-order DWORD since the high-order one rarely changes. If my calculations are correct,
// the low-order 32-bits traverses its full 32-bit range every 7.2 minutes, which seems to make
// using it as a seed superior to GetTickCount for most purposes.
RESEED_RANDOM_GENERATOR;
return mLineCount; // The count of runnable lines that were loaded, which might be zero.
|| action_end[1] == ':' ) // v1.0.44.07: This prevents "$(::fn_call()" from being seen as a function-call vs. hotkey-with-call. For simplicity and due to rarity, omit_leading_whitespace() isn't called; i.e. assumes that the colon immediate follows the '('.
return false;
char *aBuf_last_char = action_end + strlen(action_end) - 1; // Above has already ensured that action_end is "(...".
if (aPendingFunctionHasBrace) // Caller specified that an optional open-brace may be present at the end of aBuf.
{
if (*aPendingFunctionHasBrace = (*aBuf_last_char == '{')) // Caller has ensured that aBuf is rtrim'd.
{
*aBuf_last_char = '\0'; // For the caller, remove it from further consideration.
// Below: Use double-colon as delimiter to set these apart from normal labels.
// The main reason for this is that otherwise the user would have to worry
// about a normal label being unintentionally valid as a hotkey, e.g.
// "Shift:" might be a legitimate label that the user forgot is also
// a valid hotkey:
#define HOTKEY_FLAG "::"
#define HOTKEY_FLAG_LENGTH 2
{
if (!aFileSpec || !*aFileSpec) return FAIL;
#ifndef AUTOHOTKEYSC
if (Line::sSourceFileCount >= Line::sMaxSourceFiles)
{
if (Line::sSourceFileCount >= ABSOLUTE_MAX_SOURCE_FILES)
return ScriptError("Too many includes."); // Short msg since so rare.
int new_max;
if (Line::sMaxSourceFiles)
{
new_max = 2*Line::sMaxSourceFiles;
if (new_max > ABSOLUTE_MAX_SOURCE_FILES)
new_max = ABSOLUTE_MAX_SOURCE_FILES;
}
else
new_max = 100;
// For simplicity and due to rarity of every needing to, expand by reallocating the array.
// Use a temp var. because realloc() returns NULL on failure but leaves original block allocated.
char **realloc_temp = (char **)realloc(Line::sSourceFile, new_max*sizeof(char *)); // If passed NULL, realloc() will do a malloc().
if (!realloc_temp)
return ScriptError(ERR_OUTOFMEM); // Short msg since so rare.
Line::sSourceFile = realloc_temp;
Line::sMaxSourceFiles = new_max;
}
char full_path[MAX_PATH];
#endif
// Keep this var on the stack due to recursion, which allows newly created lines to be given the
// correct file number even when some #include's have been encountered in the middle of the script:
int source_file_index = Line::sSourceFileCount;
if (!source_file_index)
// Since this is the first source file, it must be the main script file. Just point it to the
// location of the filespec already dynamically allocated:
Line::sSourceFile[source_file_index] = mFileSpec;
#ifndef AUTOHOTKEYSC // The "else" part below should never execute for compiled scripts since they never include anything (other than the main/combined script).
else
{
// Get the full path in case aFileSpec has a relative path. This is done so that duplicates
// can be reliably detected (we only want to avoid including a given file more than once):
// Check if this file was already included. If so, it's not an error because we want
// to support automatic "include once" behavior. So just ignore repeats:
if (!aAllowDuplicateInclude)
for (int f = 0; f < source_file_index; ++f) // Here, source_file_index==Line::sSourceFileCount
if (!lstrcmpi(Line::sSourceFile[f], full_path)) // Case insensitive like the file system (testing shows that "โ" == "ฮฃ" in the NTFS, which is hopefully how lstrcmpi works regardless of locale).
return OK;
// The file is added to the list further below, after the file has been opened, in case the
// opening fails and aIgnoreLoadFailure==true.
}
#endif
UCHAR *script_buf = NULL; // Init for the case when the buffer isn't used (non-standalone mode).
ULONG nDataSize = 0;
// <buf> should be no larger than LINE_SIZE because some later functions rely upon that:
vk_type remap_source_vk, remap_dest_vk = 0; // Only dest is initialized to enforce the fact that it is the flag/signal to indicate whether remapping is in progress.
char remap_source[32], remap_dest[32], remap_dest_modifiers[8]; // Must fit the longest key name (currently Browser_Favorites [17]), but buffer overflow is checked just in case.
if (next_buf_length && next_buf_length != -1) // Prevents infinite loop when file ends with an unclosed "/*" section. Compare directly to -1 since length is unsigned.
{
if (in_comment_section) // Look for the uncomment-flag.
{
if (!strncmp(next_buf, "*/", 2))
{
in_comment_section = false;
next_buf_length -= 2; // Adjust for removal of /* from the beginning of the string.
memmove(next_buf, next_buf + 2, next_buf_length + 1); // +1 to include the string terminator.
next_buf_length = ltrim(next_buf, next_buf_length); // Get rid of any whitespace that was between the comment-end and remaining text.
if (!*next_buf) // The rest of the line is empty, so it was just a naked comment-end.
continue;
}
else
continue;
}
else if (!in_continuation_section && !strncmp(next_buf, "/*", 2))
{
in_comment_section = true;
continue; // It's now commented out, so the rest of this line is ignored.
}
}
if (in_comment_section) // Above has incremented and read the next line, which is everything needed while inside /* .. */
{
if (next_buf_length == -1) // Compare directly to -1 since length is unsigned.
break; // By design, it's not an error. This allows "/*" to be used to comment out the bottommost portion of the script without needing a matching "*/".
// Otherwise, continue reading lines so that they can be merged with the line above them
// if they qualify as continuation lines.
continue;
}
if (!in_continuation_section) // This is either the first iteration or the line after the end of a previous continuation section.
{
// v1.0.38.06: The following has been fixed to exclude "(:" and "(::". These should be
// labels/hotkeys, not the start of a contination section. In addition, a line that starts
// with '(' but that ends with ':' should be treated as a label because labels such as
// "(label):" are far more common than something obscure like a continuation section whose
// join character is colon, namely "(Join:".
if ( !(in_continuation_section = (next_buf_length != -1 && *next_buf == '(' // Compare directly to -1 since length is unsigned.
// And also the following remaining unaries (i.e. those that aren't also binaries): !, ~
// The first line below checks for ::, ++, and --. Those can't be continuation lines because:
// "::" isn't a valid operator (this also helps performance if there are many hotstrings).
// ++ and -- are ambiguous with an isolated line containing ++Var or --Var (and besides,
// wanting to use ++ to continue an expression seems extremely rare, though if there's ever
// demand for it, might be able to look at what lies to the right of the operator's operand
// -- though that would produce inconsisent continuation behavior since ++Var itself still
// could never be a continuation line due to ambiguity).
//
// The logic here isn't smart enough to differentiate between a leading ! or - that's
// meant as a continuation character and one that isn't. Even if it were, it would
// still be ambiguous in some cases because the author's intent isn't known; for example,
// the leading minus sign on the second line below is ambiguous, so will probably remain
// a continuation character in both v1 and v2:
// x := y
// -z ? a:=1 : func()
if ((*next_buf == ':' || *next_buf == '+' || *next_buf == '-') && next_buf[1] == *next_buf // See above.
|| (*next_buf == '.' || *next_buf == '?') && !IS_SPACE_OR_TAB_OR_NBSP(next_buf[1]) // The "." and "?" operators require a space or tab after them to be legitimate. For ".", this is done in case period is ever a legal character in var names, such as struct support. For "?", it's done for backward compatibility since variable names can contain question marks (though "?" by itself is not considered a variable in v1.0.46).
&& next_buf[1] != '=' // But allow ".=" (and "?=" too for code simplicity), since ".=" is the concat-assign operator.
|| !strchr(CONTINUATION_LINE_SYMBOLS, *next_buf)) // Line doesn't start with a continuation char.
break; // Leave is_continuation_line set to its default of false.
// Some of the above checks must be done before the next ones.
if ( !(hotkey_flag = strstr(next_buf, HOTKEY_FLAG)) ) // Without any "::", it can't be a hotkey or hotstring.
{
is_continuation_line = true; // Override the default set earlier.
break;
}
if (*next_buf == ':') // First char is ':', so it's more likely a hotstring than a hotkey.
{
// Remember that hotstrings can contain what *appear* to be quoted literal strings,
// so detecting whether a "::" is in a quoted/literal string in this case would
// be more complicated. That's one reason this other method is used.
if (!IS_HOTSTRING_OPTION(*cp)) // Not a perfect test, but eliminates most of what little remaining ambiguity exists between ':' as a continuation character vs. ':' as the start of a hotstring. It especially eliminates the ":=" operator.
{
hotstring_options_all_valid = false;
break;
}
if (hotstring_options_all_valid && *cp == ':') // It's almost certainly a hotstring.
break; // So don't treat it as a continuation line.
//else it's not a hotstring but it might still be a hotkey such as ": & x::".
// So continue checking below.
}
// Since above didn't "break", this line isn't a hotstring but it is probably a hotkey
// because above already discovered that it contains "::" somewhere. So try to find out
// if there's anything that disqualifies this from being a hotkey, such as some
// expression line that contains a quoted/literal "::" (or a line starting with
// a comma that contains an unquoted-but-literal "::" such as for FileAppend).
if (*next_buf == ',')
{
cp = omit_leading_whitespace(next_buf + 1);
// The above has set cp to the position of the non-whitespace item to the right of
// this comma. Normal (single-colon) labels can't contain commas, so only hotkey
// labels are sources of ambiguity. In addition, normal labels and hotstrings have
// already been checked for, higher above.
if ( strncmp(cp, HOTKEY_FLAG, HOTKEY_FLAG_LENGTH) // It's not a hotkey such as ",::action".
&& strncmp(cp - 1, COMPOSITE_DELIMITER, COMPOSITE_DELIMITER_LENGTH) ) // ...and it's not a hotkey such as ", & y::action".
is_continuation_line = true; // Override the default set earlier.
}
else // First symbol in line isn't a comma but some other operator symbol.
{
// Check if the "::" found earlier appears to be inside a quoted/literal string.
// This check is NOT done for a line beginning with a comma since such lines
// can contain an unquoted-but-literal "::". In addition, this check is done this
// way to detect hotkeys such as the following:
// +keyname:: (and other hotkey modifier symbols such as ! and ^)
// +keyname1 & keyname2::
// +^:: (i.e. a modifier symbol followed by something that is a hotkey modifer and/or a hotkey suffix and/or an expression operator).
// <:: and &:: (i.e. hotkeys that are also expression-continuation symbols)
// By contrast, expressions that qualify as continuation lines can look like:
// . "xxx::yyy"
// + x . "xxx::yyy"
// In addition, hotkeys like the following should continue to be supported regardless
// of how things are done here:
// ^"::
// . & "::
// Finally, keep in mind that an expression-continuation line can start with two
// consecutive unary operators like !! or !*. It can also start with a double-symbol
if (*next_buf != ',') // Insert space before expression operators so that built/combined expression works correctly (some operators like 'and', 'or', '.', and '?' currently require spaces on either side) and also for readability of ListLines.
buf[buf_length++] = ' ';
memcpy(buf + buf_length, next_buf, next_buf_length + 1); // Append this line to prev. and include the zero terminator.
buf_length += next_buf_length;
continue; // Check for yet more continuation lines after this one.
}
// Since above didn't continue, there is no continuation line or section. In addition,
// since this line isn't blank, no further searching is needed.
break;
} // if (!in_continuation_section)
// OTHERWISE in_continuation_section != 0, so the above has found the first line of a new
// continuation section.
// "has_continuation_section" indicates whether the line we're about to construct is partially
// composed of continuation lines beneath it. It's separate from continuation_line_count
// in case there is another continuation section immediately after/adjacent to the first one,
// but the second one doesn't have any lines in it:
has_continuation_section = true;
continuation_line_count = 0; // Reset for this new section.
// Otherwise, parse options. First set the defaults, which can be individually overridden
// by any options actually present. RTrim defaults to ON for two reasons:
// 1) Whitespace often winds up at the end of a lines in a text editor by accident. In addition,
// whitespace at the end of any consolidated/merged line will be rtrim'd anyway, since that's
// how command parsing works.
// 2) Copy & paste from the forum and perhaps other web sites leaves a space at the end of each
// line. Although this behavior is probably site/browser-specific, it's a consideration.
do_ltrim = g_ContinuationLTrim; // Start off at global default.
do_rtrim = true; // Seems best to rtrim even if this line is a hotstring, since it is very rare that trailing spaces and tabs would ever be desirable.
// For hotstrings (which could be detected via *buf==':'), it seems best not to default the
// escape character (`) to be literal because the ability to have `t `r and `n inside the
// hotstring continuation section seems more useful/common than the ability to use the
// accent character by itself literally (which seems quite rare in most languages).
literal_escapes = false;
literal_derefs = false;
literal_delimiters = true; // This is the default even for hotstrings because although using (*buf != ':') would improve loading performance, it's not a 100% reliable way to detect hotstrings.
// The default is linefeed because:
// 1) It's the best choice for hotstrings, for which the line continuation mechanism is well suited.
// 2) It's good for FileAppend.
// 3) Minor: Saves memory in large sections by being only one character instead of two.
suffix[0] = '\n';
suffix[1] = '\0';
suffix_length = 1;
for (next_option = omit_leading_whitespace(next_buf + 1); *next_option; next_option = omit_leading_whitespace(option_end))
{
// Find the end of this option item:
if ( !(option_end = StrChrAny(next_option, " \t")) ) // Space or tab.
option_end = next_option + strlen(next_option); // Set to position of zero terminator instead.
// Temporarily terminate to help eliminate ambiguity for words contained inside other words,
// such as hypothetical "Checked" inside of "CheckedGray":
orig_char = *option_end;
*option_end = '\0';
if (!strnicmp(next_option, "Join", 4))
{
next_option += 4;
strlcpy(suffix, next_option, sizeof(suffix)); // The word "Join" by itself will product an empty string, as documented.
// Passing true for the last parameter supports `s as the special escape character,
// which allows space to be used by itself and also at the beginning or end of a string
do_ltrim = (next_option[5] != '0'); // i.e. Only an explicit zero will turn it off.
else if (!strnicmp(next_option, "RTrim", 5))
do_rtrim = (next_option[5] != '0');
else
{
// Fix for v1.0.36.01: Missing "else" above, because otherwise, the option Join`r`n
// would be processed above but also be processed again below, this time seeing the
// accent and thinking it's the signal to treat accents literally for the entire
// continuation section rather than as escape characters.
// Within this terminated option substring, allow the characters to be adjacent to
// improve usability:
for (; *next_option; ++next_option)
{
switch (*next_option)
{
case '`': // Although not using g_EscapeChar (reduces code size/complexity), #EscapeChar is still supported by continuation sections; it's just that enabling the option uses '`' rather than the custom escape-char (one reason is that that custom escape-char might be ambiguous with future/past options if it's somehing weird like an alphabetic character).
literal_escapes = true;
break;
case '%': // Same comment as above.
literal_derefs = true;
break;
case ',': // Same comment as above.
literal_delimiters = false;
break;
case 'C': // v1.0.45.03: For simplicity, anything that begins with "C" is enough to
case 'c': // identify it as the option to allow comments in the section.
in_continuation_section = CONTINUATION_SECTION_WITH_COMMENTS; // Override the default, which is boolean true (i.e. 1).
break;
}
}
}
// If the item was not handled by the above, ignore it because it is unknown.
*option_end = orig_char; // Undo the temporary termination.
} // for() each item in option list
continue; // Now that the open-parenthesis of this continuation section has been processed, proceed to the next line.
} // if (!in_continuation_section)
// Since above didn't "continue", we're in the continuation section and thus next_buf contains
// either a line to be appended onto buf or the closing parenthesis of this continuation section.
if (next_buf_length == -1) // Compare directly to -1 since length is unsigned.
{
ScriptError(ERR_MISSING_CLOSE_PAREN, buf);
return CloseAndReturn(fp, script_buf, FAIL);
}
if (next_buf_length == -2) // v1.0.45.03: Special flag that means "this is a commented-out line to be
continue; // entirely omitted from the continuation section." Compare directly to -2 since length is unsigned.
if (*next_buf == ')')
{
in_continuation_section = 0; // Facilitates back-to-back continuation sections and proper incrementing of phys_line_number.
next_buf_length = rtrim(next_buf); // Done because GetLine() wouldn't have done it due to have told it we're in a continuation section.
// Anything that lies to the right of the close-parenthesis gets appended verbatim, with
// no trimming (for flexibility) and no options-driven translation:
cp = next_buf + 1; // Use temp var cp to avoid altering next_buf (for maintainability).
--next_buf_length; // This is now the length of cp, not next_buf.
}
else
{
cp = next_buf;
// The following are done in this block only because anything that comes after the closing
// parenthesis (i.e. the block above) is exempt from translations and custom trimming.
// This means that commas are always delimiters and percent signs are always deref symbols
// Escape each comma and percent sign in the body of the continuation section so that
// the later parsing stages will see them as literals. Although, it's not always
// necessary to do this (e.g. commas in the last parameter of a command don't need to
// be escaped, nor do percent signs in hotstrings' auto-replace text), the settings
// are applied unconditionally because:
// 1) Determining when its safe to omit the translation would add a lot of code size and complexity.
// 2) The translation doesn't affect the functionality of the script since escaped literals
// are always de-escaped at a later stage, at least for everything that's likely to matter
// or that's reasonable to put into a continuation section (e.g. a hotstring's replacement text).
// UPDATE for v1.0.44.11: #EscapeChar, #DerefChar, #Delimiter are now supported by continuation
// sections because there were some requests for that in forum.
int replacement_count = 0;
if (literal_escapes) // literal_escapes must be done FIRST because otherwise it would also replace any accents added for literal_delimiters or literal_derefs.
{
one_char_string[0] = g_EscapeChar; // These strings were terminated earlier, so no need to
two_char_string[0] = g_EscapeChar; // do it here. In addition, these strings must be set by
two_char_string[1] = g_EscapeChar; // each iteration because the #EscapeChar (and similar directives) can occur multiple times, anywhere in the script.
// Append this continuation line onto the primary line.
// The suffix for the previous line gets written immediately prior writing this next line,
// which allows the suffix to be omitted for the final line. But if this is the first line,
// No suffix is written because there is no previous line in the continuation section.
// In addition, cp!=next_buf, this is the special line whose text occurs to the right of the
// continuation section's closing parenthesis. In this case too, the previous line doesn't
// get a suffix.
if (continuation_line_count > 1 && suffix_length && cp == next_buf)
{
memcpy(buf + buf_length, suffix, suffix_length + 1); // Append and include the zero terminator.
buf_length += suffix_length; // Must be done only after the old value of buf_length was used above.
}
if (next_buf_length)
{
memcpy(buf + buf_length, cp, next_buf_length + 1); // Append this line to prev. and include the zero terminator.
buf_length += next_buf_length; // Must be done only after the old value of buf_length was used above.
}
} // for() each sub-line (continued line) that composes this line.
// buf_length can't be -1 (though next_buf_length can) because outer loop's condition prevents it:
if (!buf_length) // Done only after the line number increments above so that the physical line number is properly tracked.
goto continue_main_loop; // In lieu of "continue", for performance.
// Since neither of the above executed, or they did but didn't "continue",
// buf now contains a non-commented line, either by itself or built from
// any continuation sections/lines that might have been present. Also note that
// by design, phys_line_number will be greater than mCombinedLineNumber whenever
// a continuation section/lines were used to build this combined line.
// If there's a previous line waiting to be processed, its fate can now be determined based on the
// nature of *this* line:
if (*pending_function)
{
// Somewhat messy to decrement then increment later, but it's probably easier than the
// alternatives due to the use of "continue" in some places above. NOTE: phys_line_number
// would not need to be decremented+incremented even if the below resulted in a recursive
// call to us (though it doesn't currently) because line_number's only purpose is to
// remember where this layer left off when the recursion collapses back to us.
// Fix for v1.0.31.05: It's not enough just to decrement mCombinedLineNumber because there
// might be some blank lines or commented-out lines between this function call/definition
// and the line that follows it, each of which will have previously incremented mCombinedLineNumber.
saved_line_number = mCombinedLineNumber;
mCombinedLineNumber = pending_function_line_number; // Done so that any syntax errors that occur during the calls below will report the correct line number.
// Open brace means this is a function definition. NOTE: buf was already ltrimmed by GetLine().
// Could use *g_act[ACT_BLOCK_BEGIN].Name instead of '{', but it seems too elaborate to be worth it.
if (*buf == '{' || pending_function_has_brace) // v1.0.41: Support one-true-brace, e.g. fn(...) {
{
// Note that two consecutive function definitions aren't possible:
// fn1()
// fn2()
// {
// ...
// }
// In the above, the first would automatically be deemed a function call by means of
// the check higher above (by virtue of the fact that the line after it isn't an open-brace).
if (g.CurrentFunc)
{
// Though it might be allowed in the future -- perhaps to have nested functions have
// access to their parent functions' local variables, or perhaps just to improve
// script readability and maintainability -- it's currently not allowed because of
// the practice of maintaining the func_exception_var list on our stack:
goto continue_main_loop; // In lieu of "continue", for performance.
}
// The following "examine_line" label skips the following parts above:
// 1) IsFunction() because that's only for a function call or definition alone on a line
// e.g. not "if fn()" or x := fn(). Those who goto this label don't need that processing.
// 2) The "if (*pending_function)" block: Doesn't seem applicable for the callers of this label.
// 3) The inner loop that handles continuation sections: Not needed by the callers of this label.
// 4) Things like the following should be skipped because callers of this label don't want the
// physical line number changed (which would throw off the count of lines that lie beneath a remap):
// mCombinedLineNumber = phys_line_number + 1;
// ++phys_line_number;
// 5) "mCurrLine = NULL": Probably not necessary since it's only for error reporting. Worst thing
// that could happen is that syntax errors would be thrown off, which testing shows isn't the case.
examine_line:
// "::" alone isn't a hotstring, it's a label whose name is colon.
// Below relies on the fact that no valid hotkey can start with a colon, since
// ": & somekey" is not valid (since colon is a shifted key) and colon itself
// should instead be defined as "+;::". It also relies on short-circuit boolean:
hotstring_start = NULL;
hotstring_options = NULL; // Set default as "no options were specified for this hotstring".
hotkey_flag = NULL;
if (buf[0] == ':' && buf[1])
{
if (buf[1] != ':')
{
hotstring_options = buf + 1; // Point it to the hotstring's option letters.
// The following relies on the fact that options should never contain a literal colon.
// ALSO, the following doesn't use IS_HOTSTRING_OPTION() for backward compatibility,
// performance, and because it seems seldom if ever necessary at this late a stage.
if ( !(hotstring_start = strchr(hotstring_options, ':')) )
hotstring_start = NULL; // Indicate that this isn't a hotstring after all.
else
++hotstring_start; // Points to the hotstring itself.
}
else // Double-colon, so it's a hotstring if there's more after this (but this means no options are present).
if (buf[2])
hotstring_start = buf + 2; // And leave hotstring_options at its default of NULL to indicate no options.
//else it's just a naked "::", which is considered to be an ordinary label whose name is colon.
}
if (hotstring_start)
{
// Find the hotstring's final double-colon by considering escape sequences from left to right.
// This is necessary for to handles cases such as the following:
// ::abc```::::Replacement String
// The above hotstring translates literally into "abc`::".
char *escaped_double_colon = NULL;
for (cp = hotstring_start; ; ++cp) // Increment to skip over the symbol just found by the inner for().
{
for (; *cp && *cp != g_EscapeChar && *cp != ':'; ++cp); // Find the next escape char or colon.
if (!*cp) // end of string.
break;
cp1 = cp + 1;
if (*cp == ':')
{
if (*cp1 == ':') // Found a non-escaped double-colon, so this is the right one.
{
hotkey_flag = cp++; // Increment to have loop skip over both colons.
// and the continue with the loop so that escape sequences in the replacement
// text (if there is replacement text) are also translated.
}
// else just a single colon, or the second colon of an escaped pair (`::), so continue.
continue;
}
switch (*cp1)
{
// Only lowercase is recognized for these:
case 'a': *cp1 = '\a'; break; // alert (bell) character
case 'b': *cp1 = '\b'; break; // backspace
case 'f': *cp1 = '\f'; break; // formfeed
case 'n': *cp1 = '\n'; break; // newline
case 'r': *cp1 = '\r'; break; // carriage return
case 't': *cp1 = '\t'; break; // horizontal tab
case 'v': *cp1 = '\v'; break; // vertical tab
// Otherwise, if it's not one of the above, the escape-char is considered to
// mark the next character as literal, regardless of what it is. Examples:
// `` -> `
// `:: -> :: (effectively)
// `; -> ;
// `c -> c (i.e. unknown escape sequences resolve to the char after the `)
}
// Below has a final +1 to include the terminator:
MoveMemory(cp, cp1, strlen(cp1) + 1);
// Since single colons normally do not need to be escaped, this increments one extra
// for double-colons to skip over the entire pair so that its second colon
// is not seen as part of the hotstring's final double-colon. Example:
// ::ahc```::::Replacement String
if (*cp == ':' && *cp1 == ':')
++cp;
} // for()
if (!hotkey_flag)
hotstring_start = NULL; // Indicate that this isn't a hotstring after all.
}
if (!hotstring_start) // Not a hotstring (hotstring_start is checked *again* in case above block changed it; otherwise hotkeys like ": & x" aren't recognized).
{
// Note that there may be an action following the HOTKEY_FLAG (on the same line).
if (hotkey_flag = strstr(buf, HOTKEY_FLAG)) // Find the first one from the left, in case there's more than 1.
{
if (hotkey_flag == buf && hotkey_flag[2] == ':') // v1.0.46: Support ":::" to mean "colon is a hotkey".
++hotkey_flag;
// v1.0.40: It appears to be a hotkey, but validate it as such before committing to processing
// it as a hotkey. If it fails validation as a hotkey, treat it as a command that just happens
// to contain a double-colon somewhere. This avoids the need to escape double colons in scripts.
// Note: Hotstrings can't suffer from this type of ambiguity because a leading colon or pair of
// colons makes them easier to detect.
cp = omit_trailing_whitespace(buf, hotkey_flag); // For maintainability.
orig_char = *cp;
*cp = '\0'; // Temporarily terminate.
if (!Hotkey::TextInterpret(omit_leading_whitespace(buf), NULL, false)) // Passing NULL calls it in validate-only mode.
hotkey_flag = NULL; // It's not a valid hotkey, so indicate that it's a command (i.e. one that contains a literal double-colon, which avoids the need to escape the double-colon).
*cp = orig_char; // Undo the temp. termination above.
}
}
// Treat a naked "::" as a normal label whose label name is colon:
if (is_label = (hotkey_flag && hotkey_flag > buf)) // It's a hotkey/hotstring label.
{
if (g.CurrentFunc)
{
// Even if it weren't for the reasons below, the first hotkey/hotstring label in a script
// will end the auto-execute section with a "return". Therefore, if this restriction here
// is ever removed, be sure that that extra return doesn't get put inside the function.
//
// The reason for not allowing hotkeys and hotstrings inside a function's body is that
// when the subroutine is launched, the hotstring/hotkey would be using the function's
// local variables. But that is not appropriate and it's likely to cause problems even
// if it were. It doesn't seem useful in any case. By contrast, normal labels can
// safely exist inside a function body and since the body is a block, other validation
// ensures that a Gosub or Goto can't jump to it from outside the function.
ScriptError("Hotkeys/hotstrings are not allowed inside functions.", buf);
return CloseAndReturn(fp, script_buf, FAIL);
}
if (mLastLine && mLastLine->mActionType == ACT_IFWINACTIVE)
{
mCurrLine = mLastLine; // To show vicinity lines.
ScriptError("IfWin should be #IfWin.", buf);
return CloseAndReturn(fp, script_buf, FAIL);
}
*hotkey_flag = '\0'; // Terminate so that buf is now the label itself.
hotkey_flag += HOTKEY_FLAG_LENGTH; // Now hotkey_flag is the hotkey's action, if any.
if (!hotstring_start)
{
ltrim(hotkey_flag); // Has already been rtrimmed by GetLine().
rtrim(buf); // Trim the new substring inside of buf (due to temp termination). It has already been ltrimmed.
cp = hotkey_flag; // Set default, conditionally overridden below (v1.0.44.07).
// v1.0.40: Check if this is a remap rather than hotkey:
if ( *hotkey_flag // This hotkey's action is on the same line as its label.
&& (remap_dest_vk = hotkey_flag[1] ? TextToVK(cp = Hotkey::TextToModifiers(hotkey_flag, NULL)) : 0xFF) ) // And the action appears to be a remap destination rather than a command.
// For above:
// Fix for v1.0.44.07: Set remap_dest_vk to 0xFF if hotkey_flag's length is only 1 because:
// 1) It allows a destination key that doesn't exist in the keyboard layout (such as 6::โก in
// English).
// 2) It improves performance a little by not calling TextToVK except when the destination key
// might be a mouse button or some longer key name whose actual/correct VK value is relied
// upon by other places below.
// Fix for v1.0.40.01: Since remap_dest_vk is also used as the flag to indicate whether
// this line qualifies as a remap, must do it last in the statement above. Otherwise,
// the statement might short-circuit and leave remap_dest_vk as non-zero even though
// the line shouldn't be a remap. For example, I think a hotkey such as "x & y::return"
// would trigger such a bug.
{
// These will be ignored in other stages if it turns out not to be a remap later below:
if (cp - hotkey_flag < sizeof(remap_dest_modifiers)) // Avoid reading beyond the end.
remap_dest_modifiers[cp - hotkey_flag] = '\0'; // Terminate at the proper end of the modifier string.
remap_stage = 0; // Init for use in the next stage.
// In the unlikely event that the dest key has the same name as a command, disqualify it
// from being a remap (as documented). v1.0.40.05: If the destination key has any modifiers,
// it is unambiguously a key name rather than a command, so the switch() isn't necessary.
if (*remap_dest_modifiers)
goto continue_main_loop; // It will see that remap_dest_vk is non-zero and act accordingly.
switch (remap_dest_vk)
{
case VK_CONTROL: // Checked in case it was specified as "Control" rather than "Ctrl".
case VK_SLEEP:
if (StrChrAny(hotkey_flag, " \t,")) // Not using g_delimiter (reduces code size/complexity).
break; // Any space, tab, or enter means this is a command rather than a remap destination.
goto continue_main_loop; // It will see that remap_dest_vk is non-zero and act accordingly.
// "Return" and "Pause" as destination keys are always considered commands instead.
// This is documented and is done to preserve backward compatibility.
case VK_RETURN:
// v1.0.40.05: Although "Return" can't be a destination, "Enter" can be. Must compare
// to "Return" not "Enter" so that things like "vk0d" (the VK of "Enter") can also be a
// destination key:
if (!stricmp(remap_dest, "Return"))
break;
goto continue_main_loop; // It will see that remap_dest_vk is non-zero and act accordingly.
case VK_PAUSE: // Used for both "Pause" and "Break"
break;
default: // All other VKs are valid destinations and thus the remap is valid.
goto continue_main_loop; // It will see that remap_dest_vk is non-zero and act accordingly.
}
// Since above didn't goto, indicate that this is not a remap after all:
remap_dest_vk = 0;
}
}
// else don't trim hotstrings since literal spaces in both substrings are significant.
// If this is the first hotkey label encountered, Add a return before
// adding the label, so that the auto-exectute section is terminated.
// Only do this if the label is a hotkey because, for example,
// the user may want to fully execute a normal script that contains
// no hotkeys but does contain normal labels to which the execution
// should fall through, if specified, rather than returning.
// But this might result in weirdness? Example:
//testlabel:
// Sleep, 1
// return
// ^a::
// return
// It would put the hard return in between, which is wrong. But in the case above,
// the first sub shouldn't have a return unless there's a part up top that ends in Exit.
// So if Exit is encountered before the first hotkey, don't add the return?
// Even though wrong, the return is harmless because it's never executed? Except when
// falling through from above into a hotkey (which probably isn't very valid anyway)?
// Update: Below must check if there are any true hotkey labels, not just regular labels.
// Otherwise, a normal (non-hotkey) label in the autoexecute section would count and
// thus the RETURN would never be added here, even though it should be:
// Notes about the below macro:
// Fix for v1.0.34: Don't point labels to this particular RETURN so that labels
// can point to the very first hotkey or hotstring in a script. For example:
// Goto Test
// Test:
// ^!z::ToolTip Without the fix`, this is never displayed by "Goto Test".
// UCHAR_MAX signals it not to point any pending labels to this RETURN.
// mCurrLine = NULL -> signifies that we're in transition, trying to load a new one.
#define CHECK_mNoHotkeyLabels \
if (mNoHotkeyLabels)\
{\
mNoHotkeyLabels = false;\
if (!AddLine(ACT_RETURN, NULL, UCHAR_MAX))\
return CloseAndReturn(fp, script_buf, FAIL);\
mCurrLine = NULL;\
}
CHECK_mNoHotkeyLabels
// For hotstrings, the below makes the label include leading colon(s) and the full option
// string (if any) so that the uniqueness of labels is preserved. For example, we want
// the following two hotstring labels to be unique rather than considered duplicates:
// ::abc::
// :c:abc::
if (!AddLabel(buf, true)) // Always add a label before adding the first line of its section.
return CloseAndReturn(fp, script_buf, FAIL);
hook_action = 0; // Set default.
if (*hotkey_flag) // This hotkey's action is on the same line as its label.
{
if (!hotstring_start)
// Don't add the alt-tabs as a line, since it has no meaning as a script command.
// But do put in the Return regardless, in case this label is ever jumped to
// via Goto/Gosub:
if ( !(hook_action = Hotkey::ConvertAltTab(hotkey_flag, false)) )
if (!ParseAndAddLine(hotkey_flag, IsFunction(hotkey_flag) ? ACT_EXPRESSION : ACT_INVALID)) // It can't be a function definition vs. call since it's a single-line hotkey.
return CloseAndReturn(fp, script_buf, FAIL);
// Also add a Return that's implicit for a single-line hotkey. This is also
// done for auto-replace hotstrings in case gosub/goto is ever used to jump
// to their labels:
if (!AddLine(ACT_RETURN))
return CloseAndReturn(fp, script_buf, FAIL);
}
if (hotstring_start)
{
if (!*hotstring_start)
{
// The following error message won't indicate the correct line number because
// the hotstring (as a label) does not actually exist as a line. But it seems
// best to report it this way in case the hotstring is inside a #Include file,
// so that the correct file name and approximate line number are shown:
ScriptError("This hotstring is missing its abbreviation.", buf); // Display buf vs. hotkey_flag in case the line is simply "::::".
return CloseAndReturn(fp, script_buf, FAIL);
}
// In the case of hotstrings, hotstring_start is the beginning of the hotstring itself,
// i.e. the character after the second colon. hotstring_options is NULL if no options,
// otherwise it's the first character in the options list (option string is not terminated,
// but instead ends in a colon). hotkey_flag is blank if it's not an auto-replace
// hotstring, otherwise it contains the auto-replace text.
// v1.0.42: Unlike hotkeys, duplicate hotstrings are not detected. This is because
// hotstrings are less commonly used and also because it requires more code to find
// hotstring duplicates (and performs a lot worse if a script has thousands of
// hotstrings) because of all the hotstring options.
if (!Hotstring::AddHotstring(mLastLabel, hotstring_options ? hotstring_options : ""
if (hk = Hotkey::FindHotkeyByTrueNature(buf, suffix_has_tilde)) // Parent hotkey found. Add a child/variant hotkey for it.
{
if (hook_action) // suffix_has_tilde has always been ignored for these types (alt-tab hotkeys).
{
// Hotkey::Dynamic() contains logic and comments similar to this, so maintain them together.
// An attempt to add an alt-tab variant to an existing hotkey. This might have
// merit if the intention is to make it alt-tab now but to later disable that alt-tab
// aspect via the Hotkey cmd to let the context-sensitive variants shine through
// (take effect).
hk->mHookAction = hook_action;
}
else
{
// Detect duplicate hotkey variants to help spot bugs in scripts.
if (hk->FindVariant()) // See if there's already a variant matching the current criteria (suffix_has_tilde does not make variants distinct form each other because it would require firing two hotkey IDs in response to pressing one hotkey, which currently isn't in the design).
if (!hk->AddVariant(mLastLabel, suffix_has_tilde))
{
ScriptError(ERR_OUTOFMEM, buf);
return CloseAndReturn(fp, script_buf, FAIL);
}
}
}
else // No parent hotkey yet, so create it.
if ( !(hk = Hotkey::AddHotkey(mLastLabel, hook_action, NULL, suffix_has_tilde, false)) )
return CloseAndReturn(fp, script_buf, FAIL); // It already displayed the error.
}
goto continue_main_loop; // In lieu of "continue", for performance.
} // if (is_label = ...)
// Otherwise, not a hotkey or hotstring. Check if it's a generic, non-hotkey label:
if (buf[buf_length - 1] == ':') // Labels must end in a colon (buf was previously rtrimmed).
{
if (buf_length == 1) // v1.0.41.01: Properly handle the fact that this line consists of only a colon.
{
ScriptError(ERR_UNRECOGNIZED_ACTION, buf);
return CloseAndReturn(fp, script_buf, FAIL);
}
// Labels (except hotkeys) must contain no whitespace, delimiters, or escape-chars.
// This is to avoid problems where a legitimate action-line ends in a colon,
// such as "WinActivate SomeTitle" and "#Include c:".
// We allow hotkeys to violate this since they may contain commas, and since a normal
// script line (i.e. just a plain command) is unlikely to ever end in a double-colon:
for (cp = buf, is_label = true; *cp; ++cp)
if (IS_SPACE_OR_TAB(*cp) || *cp == g_delimiter || *cp == g_EscapeChar)
{
is_label = false;
break;
}
if (is_label) // It's a generic, non-hotkey/non-hotstring label.
{
// v1.0.44.04: Fixed this check by moving it after the above loop.
// Above has ensured buf_length>1, so it's safe to check for double-colon:
// v1.0.44.03: Don't allow anything that ends in "::" (other than a line consisting only
// of "::") to be a normal label. Assume it's a command instead (if it actually isn't, a
// later stage will report it as "invalid hotkey"). This change avoids the situation in
// which a hotkey like ^!ฮฃ:: is seen as invalid because the current keyboard layout doesn't
// have a "ฮฃ" key. Without this change, if such a hotkey appears at the top of the script,
// its subroutine would execute immediately as a normal label, which would be especially
// bad if the hotkey were something like the "Shutdown" command.
if (buf[buf_length - 2] == ':' && buf_length > 2) // i.e. allow "::" as a normal label, but consider anything else with double-colon to be a failed-hotkey label that terminates the auto-exec section.
{
CHECK_mNoHotkeyLabels // Terminate the auto-execute section since this is a failed hotkey vs. a mere normal label.
snprintf(msg_text, sizeof(msg_text), "Note: The hotkey %s will not be active because it does not exist in the current keyboard layout.", buf);
MsgBox(msg_text);
}
buf[--buf_length] = '\0'; // Remove the trailing colon.
rtrim(buf, buf_length); // Has already been ltrimmed.
if (!AddLabel(buf, false))
return CloseAndReturn(fp, script_buf, FAIL);
goto continue_main_loop; // In lieu of "continue", for performance.
}
}
// Since above didn't "goto", it's not a label.
if (*buf == '#')
{
saved_line_number = mCombinedLineNumber; // Backup in case IsDirective() processes an include file, which would change mCombinedLineNumber's value.
switch(IsDirective(buf)) // Note that it may alter the contents of buf, at least in the case of #IfWin.
{
case CONDITION_TRUE:
// Since the directive may have been a #include which called us recursively,
// restore the class's values for these two, which are maintained separately
// like this to avoid having to specify them in various calls, especially the
// hundreds of calls to ScriptError() and LineError():
mCurrFileIndex = source_file_index;
mCombinedLineNumber = saved_line_number;
goto continue_main_loop; // In lieu of "continue", for performance.
case FAIL: // IsDirective() already displayed the error.
return CloseAndReturn(fp, script_buf, FAIL);
//case CONDITION_FALSE: Do nothing; let processing below handle it.
}
}
// Otherwise, treat it as a normal script line.
// v1.0.41: Support the "} else {" style in one-true-brace (OTB). As a side-effect,
// any command, not just an else, is probably supported to the right of '}', not just "else".
// This is undocumented because it would make for less readable scripts, and doesn't seem
// to have much value.
if (*buf == '}')
{
if (!AddLine(ACT_BLOCK_END))
return CloseAndReturn(fp, script_buf, FAIL);
// The following allows the next stage to see "else" or "else {" if it's present:
if ( !*(buf = omit_leading_whitespace(buf + 1)) )
goto continue_main_loop; // It's just a naked "}", so no more processing needed for this line.
buf_length = strlen(buf); // Update for possible use below.
}
// First do a little special handling to support actions on the same line as their
// ELSE, e.g.:
// else if x = 1
// This is done here rather than in ParseAndAddLine() because it's fairly
// complicated to do there (already tried it) mostly due to the fact that
// literal_map has to be properly passed in a recursive call to itself, as well
// as properly detecting special commands that don't have keywords such as
// IF comparisons, ACT_ASSIGN, +=, -=, etc.
// v1.0.41: '{' was added to the line below to support no spaces inside "}else{".
if (!(action_end = StrChrAny(buf, "\t ,{"))) // Position of first tab/space/comma/open-brace. For simplicitly, a non-standard g_delimiter is not supported.
action_end = buf + buf_length; // It's done this way so that ELSE can be fully handled here; i.e. that ELSE does not have to be in the list of commands recognizable by ParseAndAddLine().
// The following method ensures that words or variables that start with "Else", e.g. ElseAction, are not
// incorrectly detected as an Else command:
if (strlicmp(buf, "Else", (UINT)(action_end - buf))) // It's not an ELSE. ("Else" is used vs. g_act[ACT_ELSE].Name for performance).
{
// It's not an ELSE. Also, at this stage it can't be ACT_EXPRESSION (such as an isolated function call)
// because it would have been already handled higher above.
// v1.0.41.01: Check if there is a command/action on the same line as the '{'. This is apparently
// a style that some people use, and it also supports "{}" as a shorthand way of writing an empty block.
if (*buf == '{')
{
if (!AddLine(ACT_BLOCK_BEGIN))
return CloseAndReturn(fp, script_buf, FAIL);
if ( *(action_end = omit_leading_whitespace(buf + 1)) ) // There is an action to the right of the '{'.
{
mCurrLine = NULL; // To signify that we're in transition, trying to load a new one.
if (!ParseAndAddLine(action_end, IsFunction(action_end) ? ACT_EXPRESSION : ACT_INVALID)) // If it's a function, it must be a call vs. a definition because a function can't be defined on the same line as an open-brace.
return CloseAndReturn(fp, script_buf, FAIL);
}
// Otherwise, there was either no same-line action or the same-line action was successfully added,
// so do nothing.
}
else
if (!ParseAndAddLine(buf))
return CloseAndReturn(fp, script_buf, FAIL);
}
else // This line is an ELSE, possibly with another command immediately after it (on the same line).
{
// Add the ELSE directly rather than calling ParseAndAddLine() because that function
// would resolve escape sequences throughout the entire length of <buf>, which we
// don't want because we wouldn't have access to the corresponding literal-map to
// figure out the proper use of escaped characters:
if (!AddLine(ACT_ELSE))
return CloseAndReturn(fp, script_buf, FAIL);
mCurrLine = NULL; // To signify that we're in transition, trying to load a new one.
action_end = omit_leading_whitespace(action_end); // Now action_end is the word after the ELSE.
if (*action_end == g_delimiter) // Allow "else, action"
if (*action_end && !ParseAndAddLine(action_end, IsFunction(action_end) ? ACT_EXPRESSION : ACT_INVALID)) // If it's a function, it must be a call vs. a definition because a function can't be defined on the same line as an Else.
return CloseAndReturn(fp, script_buf, FAIL);
// Otherwise, there was either no same-line action or the same-line action was successfully added,
// so do nothing.
}
continue_main_loop: // This method is used in lieu of "continue" for performance and code size reduction.
if (remap_dest_vk)
{
// For remapping, decided to use a "macro expansion" approach because I think it's considerably
// smaller in code size and complexity than other approaches would be. I originally wanted to
// do it with the hook by changing the incoming event prior to passing it back out again (for
// example, a::b would transform an incoming 'a' keystroke into 'b' directly without having
// to suppress the original keystroke and simulate a new one). Unfortunately, the low-level
// hooks apparently do not allow this. Here is the test that confirmed it:
// if (event.vkCode == 'A')
// {
// event.vkCode = 'B';
// event.scanCode = 0x30; // Or use vk_to_sc(event.vkCode).
case 1: // Stage 1: Add key-down hotkey label, e.g. *LButton::
buf_length = sprintf(buf, "*%s::", remap_source); // Should be no risk of buffer overflow due to prior validation.
goto examine_line; // Have the main loop process the contents of "buf" as though it came in from the script.
case 2: // Stage 2.
// Copied into a writable buffer for maintainability: AddLine() might rely on this.
// Also, it seems unnecessary to set press-duration to -1 even though the auto-exec section might
// have set it to something higher than -1 because:
// 1) Press-duration doesn't apply to normal remappings since they use down-only and up-only events.
// 2) Although it does apply to remappings such as a::B and a::^b (due to press-duration being
// applied after a change to modifier state), those remappings are fairly rare and supporting
// a non-negative-one press-duration (almost always 0) probably adds a degree of flexibility
// that may be desirable to keep.
// 3) SendInput may become the predominant SendMode, so press-duration won't often be in effect anyway.
// 4) It has been documented that remappings use the auto-execute section's press-duration.
strcpy(buf, "-1"); // Does NOT need to be "-1, -1" for SetKeyDelay (see above).
// The primary reason for adding Key/MouseDelay -1 is to minimize the chance that a one of
// these hotkey threads will get buried under some other thread such as a timer, which
// would disrupt the remapping if #MaxThreadsPerHotkey is at its default of 1.
AddLine(remap_dest_is_mouse ? ACT_SETMOUSEDELAY : ACT_SETKEYDELAY, &buf, 1, NULL); // PressDuration doesn't need to be specified because it doesn't affect down-only and up-only events.
if (remap_keybd_to_mouse)
{
// Since source is keybd and dest is mouse, prevent keyboard auto-repeat from auto-repeating
// the mouse button (since that would be undesirable 90% of the time). This is done
// by inserting a single extra IF-statement above the Send that produces the down-event:
buf_length = sprintf(buf, "if not GetKeyState(\"%s\")", remap_dest); // Should be no risk of buffer overflow due to prior validation.
remap_stage = 9; // Have it hit special stage 9+1 next time for code reduction purposes.
goto examine_line; // Have the main loop process the contents of "buf" as though it came in from the script.
}
// Otherwise, remap_keybd_to_mouse==false, so fall through to next case.
case 10:
extra_event = ""; // Set default.
switch (remap_dest_vk)
{
case VK_LMENU:
case VK_RMENU:
case VK_MENU:
switch (remap_source_vk)
{
case VK_LCONTROL:
case VK_CONTROL:
extra_event = "{LCtrl up}"; // Somewhat surprisingly, this is enough to make "Ctrl::Alt" properly remap both right and left control.
break;
case VK_RCONTROL:
extra_event = "{RCtrl up}";
break;
// Below is commented out because its only purpose was to allow a shift key remapped to alt
// to be able to alt-tab. But that wouldn't work correctly due to the need for the following
// hotkey, which does more harm than good by impacting the normal Alt key's ability to alt-tab
// (if the normal Alt key isn't remapped): *Tab::Send {Blind}{Tab}
//case VK_LSHIFT:
//case VK_SHIFT:
// extra_event = "{LShift up}";
// break;
//case VK_RSHIFT:
// extra_event = "{RShift up}";
// break;
}
break;
}
mCurrLine = NULL; // v1.0.40.04: Prevents showing misleading vicinity lines for a syntax-error such as %::%
sprintf(buf, "{Blind}%s%s{%s DownTemp}", extra_event, remap_dest_modifiers, remap_dest); // v1.0.44.05: DownTemp vs. Down. See Send's DownTemp handler for details.
if (!AddLine(ACT_SEND, &buf, 1, NULL)) // v1.0.40.04: Check for failure due to bad remaps such as %::%.
return CloseAndReturn(fp, script_buf, FAIL);
AddLine(ACT_RETURN);
// Add key-up hotkey label, e.g. *LButton up::
buf_length = sprintf(buf, "*%s up::", remap_source); // Should be no risk of buffer overflow due to prior validation.
remap_stage = 2; // Adjust to hit stage 3 next time (in case this is stage 10).
goto examine_line; // Have the main loop process the contents of "buf" as though it came in from the script.
sprintf(buf, "{Blind}{%s Up}", remap_dest); // Unlike the down-event above, remap_dest_modifiers is not included for the up-event; e.g. ^{b up} is inappropriate.
AddLine(ACT_SEND, &buf, 1, NULL);
AddLine(ACT_RETURN);
remap_dest_vk = 0; // Reset to signal that the remapping expansion is now complete.
break; // Fall through to the next section so that script loading can resume at the next line.
}
} // if (remap_dest_vk)
// Since above didn't "continue", resume loading script line by line:
buf = next_buf;
buf_length = next_buf_length;
next_buf = (buf == buf1) ? buf2 : buf1;
// The line above alternates buffers (toggles next_buf to be the unused buffer), which helps
// performance because it avoids memcpy from buf2 to buf1.
} // for each whole/constructed line.
if (*pending_function) // Since this is the last non-comment line, the pending function must be a function call, not a function definition.
{
// Somewhat messy to decrement then increment later, but it's probably easier than the
// alternatives due to the use of "continue" in some places above.
saved_line_number = mCombinedLineNumber;
mCombinedLineNumber = pending_function_line_number; // Done so that any syntax errors that occur during the calls below will report the correct line number.
if (!ParseAndAddLine(pending_function, ACT_EXPRESSION)) // Must be function call vs. definition since otherwise the above would have detected the opening brace beneath it and already cleared pending_function.
return CloseAndReturn(fp, script_buf, FAIL);
mCombinedLineNumber = saved_line_number;
}
#ifdef AUTOHOTKEYSC
free(script_buf); // AutoIt3: Close the archive and free the file in memory.
oRead.Close(); //
#else
fclose(fp);
#endif
return OK;
}
// Small inline to make LoadIncludedFile() code cleaner.
size_t Script::GetLine(char *aBuf, int aMaxCharsToRead, int aInContinuationSection, UCHAR *&aMemFile) // last param = reference to pointer
#else
size_t Script::GetLine(char *aBuf, int aMaxCharsToRead, int aInContinuationSection, FILE *fp)
#endif
{
size_t aBuf_length = 0;
#ifdef AUTOHOTKEYSC
if (!aBuf || !aMemFile) return -1;
if (aMaxCharsToRead < 1) return -1; // We're signaling to caller that the end of the memory file has been reached.
// Otherwise, continue reading characters from the memory file until either a newline is
// reached or aMaxCharsToRead have been read:
// Track "i" separately from aBuf_length because we want to read beyond the bounds of the memory file.
int i;
for (i = 0; i < aMaxCharsToRead; ++i)
{
if (aMemFile[i] == '\n')
{
// The end of this line has been reached. Don't copy this char into the target buffer.
// In addition, if the previous char was '\r', remove it from the target buffer:
if (aBuf_length > 0 && aBuf[aBuf_length - 1] == '\r')
aBuf[--aBuf_length] = '\0';
++i; // i.e. so that aMemFile will be adjusted to omit this newline char.
break;
}
else
aBuf[aBuf_length++] = aMemFile[i];
}
// We either read aMaxCharsToRead or reached the end of the line (as indicated by the newline char).
// In the former case, aMemFile might now be changed to be a position outside the bounds of the
// memory area, which the caller will reflect back to us during the next call as a 0 value for
// aMaxCharsToRead, which we then signal to the caller (above) as the end of the file):
aMemFile += i; // Update this value for use by the caller.
// Terminate the buffer (the caller has already ensured that there's room for the terminator
// via its value of aMaxCharsToRead):
aBuf[aBuf_length] = '\0';
#else
if (!aBuf || !fp) return -1;
if (aMaxCharsToRead < 1) return 0;
if (feof(fp)) return -1; // Previous call to this function probably already read the last line.
if (fgets(aBuf, aMaxCharsToRead, fp) == NULL) // end-of-file or error
{
*aBuf = '\0'; // Reset since on error, contents added by fgets() are indeterminate.
return -1;
}
aBuf_length = strlen(aBuf);
if (!aBuf_length)
return 0;
if (aBuf[aBuf_length-1] == '\n')
aBuf[--aBuf_length] = '\0';
if (aBuf[aBuf_length-1] == '\r') // In case there are any, e.g. a Macintosh or Unix file?
aBuf[--aBuf_length] = '\0';
#endif
if (aInContinuationSection)
{
char *cp = omit_leading_whitespace(aBuf);
if (aInContinuationSection == CONTINUATION_SECTION_WITHOUT_COMMENTS) // By default, continuation sections don't allow comments (lines beginning with a semicolon are treated as literal text).
{
// Caller relies on us to detect the end of the continuation section so that trimming
// will be done on the final line of the section and so that a comment can immediately
// follow the closing parenthesis (on the same line). Example:
// (
// Text
// ) ; Same line comment.
if (*cp != ')') // This isn't the last line of the continuation section, so leave the line untrimmed (caller will apply the ltrim setting on its own).
return aBuf_length; // Earlier sections are responsible for keeping aBufLength up-to-date with any changes to aBuf.
//else this line starts with ')', so continue on to later section that checks for a same-line comment on its right side.
}
else // aInContinuationSection == CONTINUATION_SECTION_WITH_COMMENTS (i.e. comments are allowed in this continuation section).
{
// Fix for v1.0.46.09+: The "com" option shouldn't put "ltrim" into effect.
if (!strncmp(cp, g_CommentFlag, g_CommentFlagLength)) // Case sensitive.
{
*aBuf = '\0'; // Since this line is a comment, have the caller ignore it.
return -2; // Callers tolerate -2 only when in a continuation section. -2 indicates, "don't include this line at all, not even as a blank line to which the JOIN string (default "\n") will apply.
}
if (*cp == ')') // This isn't the last line of the continuation section, so leave the line untrimmed (caller will apply the ltrim setting on its own).
{
ltrim(aBuf); // Ltrim this line unconditionally so that caller will see that it starts with ')' without having to do extra steps.
aBuf_length = strlen(aBuf); // ltrim() doesn't always return an accurate length, so do it this way.
}
}
}
// Since above didn't return, either:
// 1) We're not in a continuation section at all, so apply ltrim() to support semicolons after tabs or
// other whitespace. Seems best to rtrim also.
// 2) CONTINUATION_SECTION_WITHOUT_COMMENTS but this line is the final line of the section. Apply
// trim() and other logic further below because caller might rely on it.
// 3) CONTINUATION_SECTION_WITH_COMMENTS (i.e. comments allowed), but this line isn't a comment (though
// it may start with ')' and thus be the final line of this section). In either case, need to check
// for same-line comments further below.
if (aInContinuationSection != CONTINUATION_SECTION_WITH_COMMENTS) // Case #1 & #2 above.
{
aBuf_length = trim(aBuf);
if (!strncmp(aBuf, g_CommentFlag, g_CommentFlagLength)) // Case sensitive.
{
// Due to other checks, aInContinuationSection==false whenever the above condition is true.
*aBuf = '\0';
return 0;
}
}
//else CONTINUATION_SECTION_WITH_COMMENTS (case #3 above), which due to other checking also means that
// this line isn't a comment (though it might have a comment on its right side, which is checked below).
// CONTINUATION_SECTION_WITHOUT_COMMENTS would already have returned higher above if this line isn't
// the last line of the continuation section.
if (g_AllowSameLineComments)
{
// Handle comment-flags that appear to the right of a valid line. But don't
// allow these types of comments if the script is considers to be the AutoIt2
// style, to improve compatibility with old scripts that may use non-escaped
// comment-flags as literal characters rather than comments:
StrReplace(parameter, "%A_ScriptDir%", mFileDir, SCS_INSENSITIVE, 1, space_remaining); // v1.0.35.11. Caller has ensured string is writable.
if (strcasestr(parameter, "%A_AppData%")) // v1.0.45.04: This and the next were requested by Tekl to make it easier to customize scripts on a per-user basis.
item_end = omit_leading_whitespace(item_end); // Move up to the next comma, assignment-op, or '\0'.
bool convert_the_operator;
switch(*item_end)
{
case ',': // No initializer is present for this variable, so move on to the next one.
item = omit_leading_whitespace(item_end + 1); // Set "item" for use by the next iteration.
continue; // No further processing needed below.
case '\0': // No initializer is present for this variable, so move on to the next one.
item = item_end; // Set "item" for use by the next iteration.
continue;
case ':':
if (item_end[1] != '=') // Colon with no following '='.
return ScriptError(ERR_UNRECOGNIZED_ACTION, item); // Vague error since so rare.
item_end += 2; // Point to the character after the ":=".
convert_the_operator = false;
break;
case '=': // Here '=' is clearly an assignment not a comparison, so further below it will be converted to :=
++item_end; // Point to the character after the "=".
convert_the_operator = true;
break;
}
char *right_side_of_operator = item_end; // Save for use by VAR_DECLARE_STATIC below.
// Since above didn't "continue", this declared variable also has an initializer.
// Add that initializer as a separate line to be executed at runtime. Separate lines
// might actually perform better at runtime because most initializers tend to be simple
// literals or variables that are simplified into non-expressions at runtime. In addition,
// items without an initializer are omitted, further improving runtime performance.
// However, the following must be done ONLY after having done the FindOrAddVar()
// above, since that may have changed this variable to a non-default type (local or global).
// But what about something like "global x, y=x"? Even that should work as long as x
// appears in the list prior to initializers that use it.
// Now, find the comma (or terminator) that marks the end of this sub-statement.
// The search must exclude commas that are inside quoted/literal strings and those that
// are inside parentheses (chiefly those of function-calls, but possibly others).
for (in_quotes = false, open_parens = 0; *item_end; ++item_end) // FIND THE NEXT "REAL" COMMA.
{
if (*item_end == ',') // This is outside the switch() further below so that its "break" can get out of the loop.
{
if (!in_quotes && open_parens < 1) // A delimiting comma other than one in a sub-statement or function. Shouldn't need to worry about unquoted escaped commas since they don't make sense in a declaration list.
break;
// Otherwise, its a quoted/literal comma or one in parentheses (such as function-call).
continue; // Continue past it to look for the correct comma.
}
switch (*item_end)
{
case '"': // There are sections similar this one later below; so see them for comments.
in_quotes = !in_quotes;
break;
case '(':
if (!in_quotes) // Literal parentheses inside a quoted string should not be counted for this purpose.
++open_parens;
break;
case ')':
if (!in_quotes)
{
if (!open_parens)
return ScriptError(ERR_MISSING_OPEN_PAREN, item);
--open_parens;
}
break;
//default: some other character; just have the loop skip over it.
}
} // for() to look for the ending comma or terminator of this sub-statement.
if (open_parens) // At least one '(' is never closed.
return ScriptError(ERR_MISSING_CLOSE_PAREN, item); // Use "item" because the problem is probably somewhere after that point in the declaration list.
// Above has now found the final comma of this sub-statement (or the terminator if there is no comma).
char *terminate_here = omit_trailing_whitespace(item, item_end-1) + 1; // v1.0.47.02: Fix the fact that "x=5 , y=6" would preserve the whitespace at the end of "5". It also fixes wrongly showing a syntax error for things like: static d="xyz" , e = 5
char orig_char = *terminate_here;
*terminate_here = '\0'; // Temporarily terminate (it might already be the terminator, but that's harmless).
if (declare_type == VAR_DECLARE_STATIC) // v1.0.46: Support simple initializers for static variables.
{
// The following is similar to the code used to support default values for function parameters.
//else it's an int or float, so just assign the numeric string itself (there
// doesn't seem to be any need to convert it to float/int first, though that would
// make things more consistent such as storing .1 as 0.1).
}
if (*right_side_of_operator) // It can be "" in cases such as "" being specified literally in the script, in which case nothing needs to be done because all variables start off as "".
var->Assign(right_side_of_operator);
}
}
else // A non-static initializer, so a line of code must be produced that will executed at runtime every time the function is called.
{
char *line_to_add;
if (convert_the_operator) // Convert first '=' in item to be ":=".
{
// Prevent any chance of overflow by using new_buf (overflow might otherwise occur in cases
// such as this sub-statement being the very last one in the declaration list, and being
// at the limit of the buffer's capacity).
char new_buf[LINE_SIZE]; // Using so much stack space here and in caller seems unlikely to affect performance, so _alloca seems unlikely to help.
StrReplace(strcpy(new_buf, item), "=", ":=", SCS_SENSITIVE, 1); // Can't overflow because there's only one replacement and we know item's length can't be that close to the capacity limit.
line_to_add = new_buf;
}
else
line_to_add = item;
if (belongs_to_if_or_else_or_loop && !open_brace_was_added) // v1.0.46.01: Put braces to allow initializers to work even directly under an IF/ELSE/LOOP. Note that the braces aren't added or needed for static initializers.
{
if (!AddLine(ACT_BLOCK_BEGIN))
return FAIL;
open_brace_was_added = true;
}
// Call Parse() vs. AddLine() because it detects and optimizes simple assignments into
// non-exprssions for faster runtime execution.
if (!ParseAndAddLine(line_to_add)) // For simplicity and maintainability, call self rather than trying to set things up properly to stay in self.
return FAIL; // Above already displayed the error.
}
*terminate_here = orig_char; // Undo the temporary termination.
// Set "item" for use by the next iteration:
item = (*item_end == ',') // i.e. it's not the terminator and thus not the final item in the list.
? omit_leading_whitespace(item_end + 1)
: item_end; // It's the terminator, so let the loop detect that to finish.
} // for() each item in the declaration list.
if (open_brace_was_added)
if (!AddLine(ACT_BLOCK_END))
return FAIL;
return OK;
} // single-iteration for-loop
// Since above didn't return, it's not a declaration such as "global MyVar".
if ( !(end_marker = ParseActionType(action_name, aLineText, true)) )
return FAIL; // It already displayed the error.
}
// Above has ensured that end_marker is the address of the last character of the action name,
// or NULL if there is no action name.
// Find the arguments (not to be confused with exec_params) of this action, if it has any:
default: // To minimize the times where expressions must have an outer set of parentheses, assume all unknown operators are expressions.
aActionType = ACT_IFEXPR;
} // switch()
} // Detection of type of IF-statement.
if (aActionType == ACT_IFEXPR) // There are various ways above for aActionType to become ACT_IFEXPR.
{
// Since this is ACT_IFEXPR, action_args is known not to be an empty string, which is relied on below.
char *action_args_last_char = action_args + strlen(action_args) - 1; // Shouldn't be a whitespace char since those should already have been removed at an earlier stage.
if (*action_args_last_char == '{') // This is an if-expression statement with an open-brace on the same line.
{
*action_args_last_char = '\0';
rtrim(action_args, action_args_last_char - action_args); // Remove the '{' and all its whitespace from further consideration.
add_openbrace_afterward = true;
}
}
else // It's a IF-statement, but a traditional/non-expression one.
{
// Set things up to be parsed as args later on.
*operation = g_delimiter;
if (aActionType == ACT_IFBETWEEN || aActionType == ACT_IFNOTBETWEEN)
{
// I decided against the syntax "if var between 3,8" because the gain in simplicity
// and the small avoidance of ambiguity didn't seem worth the cost in terms of readability.
for (next_word = operation;;)
{
if ( !(next_word = strcasestr(next_word, "and")) )
return ScriptError("BETWEEN requires the word AND.", aLineText); // Seems too rare a thing to warrant falling back to ACT_IFEXPR for this.
// This section is done before the section that checks whether action_name is a valid command
// because it avoids ambiguity in a line such as the following:
// Input = test ; Would otherwise be confused with the Input command.
// But there may be times when a line like this is used:
// MsgBox = ; i.e. the equals is intended to be the first parameter, not an operator.
// In the above case, the user can provide the optional comma to avoid the ambiguity:
// MsgBox, =
char action_args_2nd_char = action_args[1];
bool convert_pre_inc_or_dec = false; // Set default.
switch(*action_args)
{
case '=': // i.e. var=value (old-style assignment)
aActionType = ACT_ASSIGN;
break;
case ':':
// v1.0.40: Allow things like "MsgBox :: test" to be valid by insisting that '=' follows ':'.
if (action_args_2nd_char == '=') // i.e. :=
aActionType = ACT_ASSIGNEXPR;
break;
case '+':
// Support for ++i (and in the next case, --i). In these cases, action_name must be either
// "+" or "-", and the first character of action_args must match it.
if ((convert_pre_inc_or_dec = action_name[0] == '+' && !action_name[1]) // i.e. the pre-increment operator; e.g. ++index.
|| action_args_2nd_char == '=') // i.e. x+=y (by contrast, post-increment is recognized only after we check for a command name to cut down on ambiguity).
aActionType = ACT_ADD;
break;
case '-':
// Do a complete validation/recognition of the operator to allow a line such as the following,
// which omits the first optional comma, to still be recognized as a command rather than a
// variable-with-operator:
// SetBatchLines -1
if ((convert_pre_inc_or_dec = action_name[0] == '-' && !action_name[1]) // i.e. the pre-decrement operator; e.g. --index.
|| action_args_2nd_char == '=') // i.e. x-=y (by contrast, post-decrement is recognized only after we check for a command name to cut down on ambiguity).
aActionType = ACT_SUB;
break;
case '*':
if (action_args_2nd_char == '=') // i.e. *=
aActionType = ACT_MULT;
break;
case '/':
if (action_args_2nd_char == '=') // i.e. /=
aActionType = ACT_DIV;
// ACT_DIV is different than //= and // because ACT_DIV supports floating point inputs by yielding
// a floating point result (i.e. it doesn't Floor() the result when the inputs are floats).
else if (action_args_2nd_char == '/' && action_args[2] == '=') // i.e. //=
aActionType = ACT_EXPRESSION; // Mark this line as a stand-alone expression.
break;
case '.':
case '|':
case '&':
case '^':
if (action_args_2nd_char == '=') // i.e. .= and |= and &= and ^=
aActionType = ACT_EXPRESSION; // Mark this line as a stand-alone expression.
break;
//case '?': Stand-alone ternary such as true ? fn1() : fn2(). These are rare so are
// checked later, only after action_name has been checked to see if it's a valid command.
case '>':
case '<':
if (action_args_2nd_char == *action_args && action_args[2] == '=') // i.e. >>= and <<=
aActionType = ACT_EXPRESSION; // Mark this line as a stand-alone expression.
break;
//default: Leave aActionType set to ACT_INVALID. This also covers case '\0' in case that's possible.
} // switch()
if (aActionType) // An assignment or other type of action was discovered above.
{
if (convert_pre_inc_or_dec) // Set up pre-ops like ++index and --index to be parsed properly later.
{
// The following converts:
// ++x -> EnvAdd x,1 (not really "EnvAdd" per se; but ACT_ADD).
// Set action_args to be the word that occurs after the ++ or --:
action_args = omit_leading_whitespace(++action_args); // Though there generally isn't any.
if (StrChrAny(action_args, EXPR_ALL_SYMBOLS ".")) // Support things like ++Var ? f1() : f2() and ++Var /= 5. Don't need strstr(action_args, " ?") because the search already looks for ':'.
aActionType = ACT_EXPRESSION; // Mark this line as a stand-alone expression.
else
{
// Set up aLineText and action_args to be parsed later on as a list of two parameters:
// The variable name followed by the amount to be added or subtracted (e.g. "ScriptVar, 1").
// We're not changing the length of aLineText by doing this, so it should be large enough:
size_t new_length = strlen(action_args);
// Since action_args is just a pointer into the aLineText buffer (which caller has ensured
// is modifiable), use memmove() so that overlapping source & dest are properly handled:
memmove(aLineText, action_args, new_length + 1); // +1 to include the zero terminator.
// Append the second param, which is just "1" since the ++ and -- only inc/dec by 1:
aLineText[new_length++] = g_delimiter;
aLineText[new_length++] = '1';
aLineText[new_length] = '\0';
}
}
else if (aActionType != ACT_EXPRESSION) // i.e. it's ACT_ASSIGN/ASSIGNEXPR/ADD/SUB/MULT/DIV
{
if (aActionType != ACT_ASSIGN) // i.e. it's ACT_ASSIGNEXPR/ADD/SUB/MULT/DIV
{
// Find the first non-function comma, which in the case of ACT_ADD/SUB can be
// either a statement-separator comma (expression) or the time units arg.
// Reasons for this:
// 1) ACT_ADD/SUB: Need to distinguish compound statements from date/time math;
// e.g. "x+=1, y+=2" should be marked as a stand-alone expression, not date math.
// 2) ACT_ASSIGNEXPR/MULT/DIV (and ACT_ADD/SUB for that matter): Need to make
// comma-separated sub-expressions into one big ACT_EXPRESSION so that the
// leftmost sub-expression will get evaluated prior to the others (for consistency
// and as documented). However, this has some side-effects, such as making
// the leftmost /= operator into true division rather than ENV_DIV behavior,
// and treating blanks as errors in math expressions when otherwise ENV_MULT
// would treat them as zero.
// ALSO: ACT_ASSIGNEXPR/ADD/SUB/MULT/DIV are made into ACT_EXPRESSION *only* when multi-
// statement commas are present because the following legacy behaviors must be retained:
// 1) Math treatment of blanks as zero in ACT_ADD/SUB/etc.
// 2) EnvDiv's special behavior, which is different than both true divide and floor divide.
// 3) Possibly add/sub's date/time math.
// 4) For performance, don't want trivial assignments to become ACT_EXPRESSION.
case '"': // This is whole section similar to another one later below, so see it for comments.
in_quotes = !in_quotes;
break;
case '(':
if (!in_quotes) // Literal parentheses inside a quoted string should not be counted for this purpose.
++open_parens;
break;
case ')':
if (!in_quotes)
--open_parens;
break;
}
if (*cp == g_delimiter && !in_quotes && open_parens < 1) // A delimiting comma other than one in a sub-statement or function. Shouldn't need to worry about unquoted escaped commas since they don't make sense with += and -=.
{
if (aActionType == ACT_ADD || aActionType == ACT_SUB)
{
cp = omit_leading_whitespace(cp + 1);
if (StrChrAny(cp, EXPR_ALL_SYMBOLS ".")) // Don't need strstr(cp, " ?") because the search already looks for ':'.
aActionType = ACT_EXPRESSION; // It's clearly an expression not a word like Days or %VarContainingTheWordDays%.
//else it's probably date/time math, so leave it as-is.
}
else // ACT_ASSIGNEXPR/MULT/DIV, for which any non-function comma qualifies it as multi-statement.
aActionType = ACT_EXPRESSION;
break;
}
}
}
if (aActionType != ACT_EXPRESSION) // The above didn't make it a stand-alone expression.
{
// The following converts:
// x+=2 -> ACT_ADD x, 2.
// x:=2 -> ACT_ASSIGNEXPR, x, 2
// etc.
// But post-inc/dec are recognized only after we check for a command name to cut down on ambiguity
*action_args = g_delimiter; // Replace the =,+,-,:,*,/ with a delimiter for later parsing.
if (aActionType != ACT_ASSIGN) // i.e. it's not just a plain equal-sign (which has no 2nd char).
action_args[1] = ' '; // Remove the "=" from consideration.
}
}
//else it's already an isolated expression, so no changes are desired.
action_args = aLineText; // Since this is an assignment and/or expression, use the line's full text for later parsing.
} // if (aActionType)
} // Handling of assignments and other operators.
}
//else aActionType was already determined by the caller.
// Now the above has ensured that action_args is the first parameter itself, or empty-string if none.
// If action_args now starts with a delimiter, it means that the first param is blank/empty.
if (!aActionType && !aOldActionType) // Caller nor logic above has yet determined the action.
if ( !(aActionType = ConvertActionType(action_name)) ) // Is this line a command?
aOldActionType = ConvertOldActionType(action_name); // If not, is it an old-command?
if (!aActionType && !aOldActionType) // Didn't find any action or command in this line.
{
// v1.0.41: Support one-true brace style even if there's no space, but make it strict so that
// things like "Loop{ string" are reported as errors (in case user intended a file-pattern loop).
if (!stricmp(action_name, "Loop{") && !*action_args)
{
aActionType = ACT_LOOP;
add_openbrace_afterward = true;
}
else if (*action_args == '?' && IS_SPACE_OR_TAB(action_args[1]) // '?' currently requires a trailing space or tab because variable names can contain '?' (except '?' by itself). For simplicty, no NBSP check.
|| strchr(EXPR_ALL_SYMBOLS ".", *action_args))
{
char *question_mark;
if ((*action_args == '+' || *action_args == '-') && action_args[1] == *action_args) // Post-inc/dec. See comments further below.
{
if (action_args[2]) // i.e. if the ++ and -- isn't the last thing; e.g. x++ ? fn1() : fn2() ... Var++ //= 2
aActionType = ACT_EXPRESSION; // Mark this line as a stand-alone expression.
else
{
// The logic here allows things like IfWinActive-- to be seen as commands even without
// a space before the -- or ++. For backward compatibility and code simplicity, it seems
// best to keep that behavior rather than distinguishing between Command-- and Command --.
// In any case, "Command --" should continue to be seen as a command regardless of what
// changes are ever made. That's why this section occurs below the command-name lookup.
// To avoid hindering load-time error detection such as misspelled command names, allow stand-alone
// expressions only for things that can produce a side-effect (currently only ternaries like
// the ones mentioned later below need to be checked since the following other things were
// previously recognized as ACT_EXPRESSION if appropriate: function-calls, post- and
// pre-inc/dec (++/--), and assignment operators like := += *= (though these don't necessarily
// need to be ACT_EXPRESSION to support multi-statement; they can be ACT_ASSIGNEXPR, ACT_ADD, etc.
// and still support comma-separated statements.
// Stand-alone ternaries are checked for here rather than earlier to allow a command name
// (of present) to take precedence (since stand-alone ternaries seem much rarer than
// "Command ? something" such as "MsgBox ? something". Could also check for a colon somewhere
// to the right if further ambiguity-resolution is ever needed. Also, a stand-alone ternary
// should have at least one function-call and/or assignment; otherwise it would serve no purpose.
// A line may contain a stand-alone ternary operator to call functions that have side-effects
// or perform assignments. For example:
// IsDone ? fn1() : fn2()
// 3 > 2 ? x:=1 : y:=1
// (3 > 2) ... not supported due to overlap with continuation sections.
aActionType = ACT_EXPRESSION; // Mark this line as a stand-alone expression.
action_args = aLineText; // Since this is an assignment and/or expression, use the line's full text for later parsing.
}
//else leave it as an unknown action to avoid hindering load-time error detection.
// In other words, don't be too permissive about what gets marked as a stand-alone expression.
}
if (!aActionType) // Above still didn't find a valid action (i.e. check aActionType again in case the above changed it).
{
if (*action_args == '(') // v1.0.46.11: Recognize as multi-statements that start with a function, like "fn(), x:=4". v1.0.47.03: Removed the following check to allow a close-brace to be followed by a comma-less function-call: strchr(action_args, g_delimiter).
{
aActionType = ACT_EXPRESSION; // Mark this line as a stand-alone expression.
action_args = aLineText; // Since this is a function-call followed by a comma and some other expression, use the line's full text for later parsing.
}
else
// v1.0.40: Give a more specific error message now now that hotkeys can make it here due to
// the change that avoids the need to escape double-colons:
int max_params = max_params_override ? max_params_override
: (mIsAutoIt2 ? (this_action.MaxParamsAu2WithHighBit & 0x7F) // 0x7F removes the high-bit from consideration; that bit is used for an unrelated purpose.
: this_action.MaxParams);
int max_params_minus_one = max_params - 1;
bool is_expression;
ActionTypeType *np;
for (nArgs = mark = 0; action_args[mark] && nArgs < max_params; ++nArgs)
{
if (nArgs == 2) // i.e. the 3rd arg is about to be added.
{
switch (aActionType) // will be ACT_INVALID if this_action is an old-style command.
{
case ACT_IFWINEXIST:
case ACT_IFWINNOTEXIST:
case ACT_IFWINACTIVE:
case ACT_IFWINNOTACTIVE:
subaction_start = action_args + mark;
if (subaction_end_marker = ParseActionType(subaction_name, subaction_start, false))
if ( !(subaction_type = ConvertActionType(subaction_name)) )
&& IS_SPACE_OR_TAB(arg[nArgs][1]); // Followed by a space or tab.
// Find the end of the above arg:
for (in_quotes = false, open_parens = 0; action_args[mark]; ++mark)
{
switch (action_args[mark])
{
case '"':
// The simple method below is sufficient for our purpose even if a quoted string contains
// pairs of double-quotes to represent a single literal quote, e.g. "quoted ""word""".
// In other words, it relies on the fact that there must be an even number of quotes
// inside any mandatory-numeric arg that is an expression such as x=="red,blue"
in_quotes = !in_quotes;
break;
case '(':
if (!in_quotes) // Literal parentheses inside a quoted string should not be counted for this purpose.
++open_parens;
break;
case ')':
if (!in_quotes)
--open_parens;
break;
}
if (action_args[mark] == g_delimiter && !literal_map[mark]) // A non-literal delimiter (unless its within double-quotes of a mandatory-numeric arg) is a match.
{
// If we're inside a pair of quotes or parentheses and this arg is known to be an expression, this
// delimiter is part this arg and thus not to be used as a delimiter between command args:
if (in_quotes || open_parens > 0)
{
if (is_expression)
continue;
if (aActionType == ACT_TRANSFORM && (nArgs == 2 || nArgs == 3)) // i.e. the 3rd or 4th arg is about to be added.
{
// Somewhat inefficient in the case where it has to be called for both Arg#2 and Arg#3,
// but that is pretty rare. Overall, expressions and quoted strings in these args
// is rare too, so the inefficiency of redundant calls to ConvertTransformCmd() is
// very small on average, and seems worth the benefit in terms of code simplification.
// Note that the following might return TRANS_CMD_INVALID just because the sub-command
// is containined in a variable reference. That is why TRANS_CMD_INVALID does not
// produce an error at this stage, but only later when the line has been constructed
// far enough to call ArgHasDeref():
// i.e. Not the first param, only the third and fourth, which currently are either both numeric or both non-numeric for all cases.
switch(Line::ConvertTransformCmd(arg[1])) // arg[1] is the second arg.
{
// See comment above for why TRANS_CMD_INVALID isn't yet reported as an error:
#define TRANSFORM_NON_EXPRESSION_CASES \
case TRANS_CMD_INVALID:\
case TRANS_CMD_ASC:\
case TRANS_CMD_UNICODE:\
case TRANS_CMD_DEREF:\
case TRANS_CMD_HTML:\
break; // Do nothing. Leave this_new_arg.is_expression set to its default of false.
TRANSFORM_NON_EXPRESSION_CASES
default:
// For all other sub-commands, Arg #3 and #4 are expression-capable. It doesn't
// seem necessary to call LegacyArgIsExpression() because the mere fact that
// we're inside a pair of quotes or parentheses seems enough to indicate that this
// really is an expression.
continue;
}
}
// v1.0.43.07: Fixed below to use this_action instead of g_act[aActionType] so that the
// numeric params of legacy commands like EnvAdd/Sub/LeftClick can be detected. Without
// this fix, the last comma in a line like "EnvSub, var, Add(2, 3)" is seen as a parameter
// delimiter, which causes a loadtime syntax error.
if (np = this_action.NumericParams) // This command has at least one numeric parameter.
{
// As of v1.0.25, pure numeric parameters can optionally be numeric expressions, so check for that:
nArgs_plus_one = nArgs + 1;
for (; *np; ++np)
if (*np == nArgs_plus_one) // This arg is enforced to be purely numeric.
break;
if (*np) // Match found, so this is a purely numeric arg.
continue; // This delimiter is disqualified, so look for the next one.
}
} // if in quotes or parentheses
// Since above didn't "continue", this is a real delimiter.
action_args[mark] = '\0'; // Terminate the previous arg.
// Trim any whitespace from the previous arg. This operation
// will not alter the contents of anything beyond action_args[i],
// so it should be safe. In addition, even though it changes
// the contents of the arg[nArgs] substring, we don't have to
// update literal_map because the map is still accurate due
// to the nature of rtrim). UPDATE: Note that this version
// of rtrim() specifically avoids trimming newline characters,
// since the user may have included literal newlines at the end
// of the string by using an escape sequence:
rtrim(arg[nArgs]);
// Omit the leading whitespace from the next arg:
for (++mark; IS_SPACE_OR_TAB(action_args[mark]); ++mark);
// Now <mark> marks the end of the string, the start of the next arg,
// or a delimiter-char (if the next arg is blank).
break; // Arg was found, so let the outer loop handle it.
// The section below is here in light of rare legacy cases such as the below:
// -%y% ; i.e. make it negative.
// +%y% ; might happen with up/down adjustments on SoundSet, GuiControl progress/slider, etc?
// Although the above are detected as non-expressions and thus non-double-derefs,
// the following are not because they're too rare or would sacrifice too much flexibility:
// 1%y%.0 ; i.e. at a tens/hundreds place and make it a floating point. In addition,
// 1%y% could be an array, so best not to tag that as non-expression.
// For that matter, %y%.0 could be an obscure kind of reverse-notation array itself.
// However, as of v1.0.29, things like %y%000 are allowed, e.g. Sleep %Seconds%000
// 0x%y% ; i.e. make it hex (too rare to check for, plus it could be an array).
// %y%%z% ; i.e. concatenate two numbers to make a larger number (too rare to check for)
char *cp = aArgText + (*aArgText == '-' || *aArgText == '+'); // i.e. +1 if second term evaluates to true.
return *cp != g_DerefChar // If no deref, for simplicity assume it's an expression since any such non-numeric item would be extremely rare in pre-expression era.
|| !aArgMap || *(aArgMap + (cp != aArgText)) // There's no literal-map or this deref char is not really a deref char because it's marked as a literal.
|| !(cp = strchr(cp + 1, g_DerefChar)) // There is no next deref char.
|| (cp[1] && !IsPureNumeric(cp + 1, false, true, true)); // But that next deref char is not the last char, which means this is not a single isolated deref. v1.0.29: Allow things like Sleep %Var%000.
// Above does not need to check whether last deref char is marked literal in the
// arg map because if it is, it would mean the first deref char lacks a matching
// close-symbol, which will be caught as a syntax error below regardless of whether
if (*op_end == '"') // If not followed immediately by another, this is the end of it.
{
++op_end;
if (*op_end != '"') // String terminator or some non-quote character.
break; // The previous char is the ending quote.
//else a pair of quotes, which resolves to a single literal quote.
// This pair is skipped over and the loop continues until the real end-quote is found.
}
}
// op_end is now set correctly to allow the outer loop to continue.
continue; // Ignore this literal string, letting the runtime expression parser recognize it.
}
// Find the end of this operand (if *op_end is '\0', strchr() will find that too):
for (op_end = op_begin + 1; !strchr(EXPR_OPERAND_TERMINATORS, *op_end); ++op_end); // Find first whitespace, operator, or paren.
if (*op_end == '=' && op_end[-1] == '.') // v1.0.46.01: Support .=, but not any use of '.' because that is reserved as a struct/member operator.
--op_end;
// Now op_end marks the end of this operand. The end might be the zero terminator, an operator, etc.
// Must be done only after op_end has been set above (since loop uses op_end):
if (*op_begin == '.' && strchr(" \t=", op_begin[1])) // If true, it can't be something like "5." because the dot inside would never be parsed separately in that case. Also allows ".=" operator.
continue;
//else any '.' not followed by a space, tab, or '=' is likely a number without a leading zero,
// so continue on below to process it.
operand_length = op_end - op_begin;
// Check if it's AND/OR/NOT:
if (operand_length < 4 && operand_length > 1) // Ordered for short-circuit performance.
if (deref[deref_count].is_function = is_function) // It's a function not a variable.
// Set to NULL to catch bugs. It must and will be filled in at a later stage
// because the setting of each function's mJumpToLine relies upon the fact that
// functions are added to the linked list only upon being formally defined
// so that the most recently defined function is always last in the linked
// list, awaiting its mJumpToLine that will appear beneath it.
deref[deref_count].func = NULL;
else // It's a variable (or a scientific-notation literal) rather than a function.
{
if (toupper(op_end[-1]) == 'E' && (orig_char == '+' || orig_char == '-') // Listed first for short-circuit performance with the below.
&& strchr(op_begin, '.')) // v1.0.46.11: This item appears to be a scientific-notation literal with the OPTIONAL +/- sign PRESENT on the exponent (e.g. 1.0e+001), so check that before checking if it's a variable name.
{
*op_end = orig_char; // Undo the temporary termination.
do // Skip over the sign and its exponent; e.g. the "+1" in "1.0e+1". There must be a sign in this particular sci-notation number or we would never have arrived here.
++op_end;
while (*op_end >= '0' && *op_end <= '9'); // Avoid isdigit() because it sometimes causes a debug assertion failure at: (unsigned)(c + 1) <= 256 (probably only in debug mode), and maybe only when bad data got in it due to some other bug.
// No need to do the following because a number can't validly be followed by the ".=" operator:
//if (*op_end == '=' && op_end[-1] == '.') // v1.0.46.01: Support .=, but not any use of '.' because that is reserved as a struct/member operator.
// --op_end;
continue; // Pure number, which doesn't need any processing at this stage.
}
// Since above didn't "continue", treat this item as a variable name:
if ( !(deref[deref_count].var = FindOrAddVar(op_begin, operand_length)) )
return FAIL; // The called function already displayed the error.
}
++deref_count; // Since above didn't "continue" or "return".
}
}
//else purely numeric or '?'. Do nothing since pure numbers and '?' don't need any
// processing at this stage.
*op_end = orig_char; // Undo the temporary termination.
} // expression pre-parsing loop.
// Now that the derefs have all been recognized above, simplify any special cases --
// such as single isolated derefs -- to enhance runtime performance.
// Make args that consist only of a quoted string-literal into non-expressions also:
if (!deref_count && *this_new_arg.text == '"')
{
// It has no derefs (e.g. x:="string" or x:=1024*1024), but since it's a single
// string literal, convert into a non-expression. This is mainly for use by
// ACT_ASSIGNEXPR, but it seems slightly beneficial for other things in case
// they ever use quoted numeric ARGS such as "13", etc. It's also simpler
// to do it unconditionally.
// Find the end of this string literal, noting that a pair of double quotes is
// a literal double quote inside the string:
for (cp = this_new_arg.text + 1;; ++cp)
{
if (!*cp) // No matching end-quote. Probably impossible due to validation further above.
return FAIL; // Force a silent failure so that the below can continue with confidence.
if (*cp == '"') // If not followed immediately by another, this is the end of it.
{
++cp;
if (*cp != '"') // String terminator or some non-quote character.
break; // The previous char is the ending quote.
//else a pair of quotes, which resolves to a single literal quote.
// This pair is skipped over and the loop continues until the real end-quote is found.
}
}
// cp is now the character after the first literal string's ending quote.
// If that char is the terminator, that first string is the only string and this
// is a simple assignment of a string literal to be converted here.
// v1.0.40.05: Leave Send/PostMessage args (all of them, but specifically
// wParam and lParam) as expressions so that at runtime, the leading '"' in a
// quoted numeric string such as "123" can be used to differentiate that string
// from a numeric value/expression such as 123 or 122+1.
if (!*cp && aActionType != ACT_SENDMESSAGE && aActionType != ACT_POSTMESSAGE)
{
this_new_arg.is_expression = false;
// Bugfix for 1.0.25.06: The below has been disabled because:
// 1) It yields inconsistent results due to AutoTrim. For example, the assignment
// x := " string" should retain the leading spaces unconditionally, since any
// more complex expression would. But if := were converted to = in this case,
// AutoTrim would be in effect for it, which is undesirable.
// 2) It's not necessary in since ASSIGNEXPR handles both expressions and non-expressions.
//if (aActionType == ACT_ASSIGNEXPR)
// aActionType = ACT_ASSIGN; // Convert to simple assignment.
if (control_type == GUI_CONTROL_TREEVIEW && aArgc > 3) // Reserve it for future use such as a tab-indented continuation section that lists the tree hierarchy.
if (!strcasestr(new_raw_arg1, "ms") && !IsPureNumeric(new_raw_arg1, true, false)) // For simplicity and due to rarity, new_arg[0].is_expression isn't checked, so a line with no variables or function-calls like "SetBatchLines % 1+1" will be wrongly seen as a syntax error.
if (aArgc > 3) // EVEN THOUGH IT'S NUMERIC, due to MsgBox's smart-comma handling, this cannot be an expression because it would never have been detected as the fourth parameter to begin with.
if (!line.ArgHasDeref(4)) // i.e. if it's a deref, we won't try to validate it now.
if (!IsPureNumeric(new_raw_arg4, false, true, true))
if ( !(this_param.var = AddVar(param_start, param_length, insert_pos, 2)) ) // Pass 2 as last parameter to mean "it's a local but more specifically a function's parameter".
return FAIL; // It already displayed the error, including attempts to have reserved names as parameter names.
// v1.0.35: Check if a default value is specified for this parameter and set up for the next iteration.
// The following section is similar to that used to support initializers for static variables.
// So maybe maintain them together.
this_param.default_type = PARAM_DEFAULT_NONE; // Set default.
param_start = omit_leading_whitespace(param_end);
if (*param_start == '=') // This is the default value of the param just added.
{
param_start = omit_leading_whitespace(param_start + 1); // Start of the default value.
if (*param_start == '"') // Quoted literal string, or the empty string.
{
// v1.0.46.13: Adde support for quoted/literal strings beyond simply "".
// The following section is nearly identical to one in ExpandExpression().
// Find the end of this string literal, noting that a pair of double quotes is
// a literal double quote inside the string.
for (target = buf, param_end = param_start + 1;;) // Omit the starting-quote from consideration, and from the resulting/built string.
{
if (!*param_end) // No matching end-quote. Probably impossible due to load-time validation.
return ScriptError(ERR_MISSING_CLOSE_QUOTE, param_start); // Reporting param_start vs. aBuf seems more informative in the case of quoted/literal strings.
if (*param_end == '"') // And if it's not followed immediately by another, this is the end of it.
{
++param_end;
if (*param_end != '"') // String terminator or some non-quote character.
break; // The previous char is the ending quote.
//else a pair of quotes, which resolves to a single literal quote. So fall through
// to the below, which will copy of quote character to the buffer. Then this pair
// is skipped over and the loop continues until the real end-quote is found.
}
//else some character other than '\0' or '"'.
*target++ = *param_end++;
}
*target = '\0'; // Terminate it in the buffer.
// The above has also set param_end for use near the bottom of the loop.
ConvertEscapeSequences(buf, g_EscapeChar, false); // Raw escape sequences like `n haven't been converted yet, so do it now.
// Caller must ensure that aFuncName doesn't already exist as a defined function.
// If aFuncNameLength is 0, the entire length of aFuncName is used.
{
aErrorWasShown = false; // Set default for this output parameter.
int i;
char *char_after_last_backslash, *terminate_here;
DWORD attr;
#define FUNC_LIB_EXT ".ahk"
#define FUNC_LIB_EXT_LENGTH 4
#define FUNC_USER_LIB "\\AutoHotkey\\Lib\\" // Needs leading and trailing backslash.
#define FUNC_USER_LIB_LENGTH 16
#define FUNC_STD_LIB "Lib\\" // Needs trailing but not leading backslash.
#define FUNC_STD_LIB_LENGTH 4
#define FUNC_LIB_COUNT 2
static FuncLibrary sLib[FUNC_LIB_COUNT] = {0};
if (!sLib[0].path) // Allocate & discover paths only upon first use because many scripts won't use anything from the library. This saves a bit of memory and performance.
{
for (i = 0; i < FUNC_LIB_COUNT; ++i)
if ( !(sLib[i].path = SimpleHeap::Malloc(MAX_PATH)) ) // Need MAX_PATH for to allow room for appending each candidate file/function name.
return NULL; // Due to rarity, simply pass the failure back to caller.
// DETERMINE PATH TO "USER" LIBRARY:
FuncLibrary *this_lib = sLib; // For convenience and maintainability.
*this_lib->path = '\0'; // Mark this library as disabled.
this_lib->length = 0; //
}
for (i = 0; i < FUNC_LIB_COUNT; ++i)
{
attr = GetFileAttributes(sLib[i].path); // Seems to accept directories that have a trailing backslash, which is good because it simplifies the code.
if (attr == 0xFFFFFFFF || !(attr & FILE_ATTRIBUTE_DIRECTORY)) // Directory doesn't exist or it's a file vs. directory. Relies on short-circuit boolean order.
{
*sLib[i].path = '\0'; // Mark this library as disabled.
sLib[i].length = 0; //
}
}
}
// Above must ensure that all sLib[].path elements are non-NULL (but they can be "" to indicate "no library").
if (!aFuncNameLength) // Caller didn't specify, so use the entire string.
for (int second_iteration = 0; second_iteration < 2; ++second_iteration)
{
for (i = 0; i < FUNC_LIB_COUNT; ++i)
{
if (!*sLib[i].path) // Library is marked disabled, so skip it.
continue;
if (sLib[i].length + naked_filename_length >= MAX_PATH-FUNC_LIB_EXT_LENGTH)
continue; // Path too long to match in this library, but try others.
dest = (char *)memcpy(sLib[i].path + sLib[i].length, naked_filename, naked_filename_length); // Append the filename to the library path.
strcpy(dest + naked_filename_length, FUNC_LIB_EXT); // Append the file extension.
attr = GetFileAttributes(sLib[i].path); // Testing confirms that GetFileAttributes() doesn't support wildcards; which is good because we want filenames containing question marks to be "not found" rather than being treated as a match-pattern.
if (attr == 0xFFFFFFFF || (attr & FILE_ATTRIBUTE_DIRECTORY)) // File doesn't exist or it's a directory. Relies on short-circuit boolean order.
continue;
// Since above didn't "continue", a file exists whose name matches that of the requested function.
// Before loading/including that file, set the working directory to its folder so that if it uses
// #Include, it will be able to use more convenient/intuitive relative paths. This is similar to
// the "#Include DirName" feature.
// Call SetWorkingDir() vs. SetCurrentDirectory() so that it succeeds even for a root drive like
// C: that lacks a backslash (see SetWorkingDir() for details).
terminate_here = sLib[i].path + sLib[i].length - 1; // The trailing backslash in the full-path-name to this library.
*terminate_here = '\0'; // Temporarily terminate it for use with SetWorkingDir().
SetWorkingDir(sLib[i].path); // See similar section in the #Include directive.
*terminate_here = '\\'; // Undo the termination.
if (!LoadIncludedFile(sLib[i].path, false, false)) // Fix for v1.0.47.05: Pass false for allow-dupe because otherwise, it's possible for a stdlib file to attempt to include itself (especially via the LibNamePrefix_ method) and thus give a misleading "duplicate function" vs. "func does not exist" error message. Obsolete: For performance, pass true for allow-dupe so that it doesn't have to check for a duplicate file (seems too rare to worry about duplicates since by definition, the function doesn't yet exist so it's file shouldn't yet be included).
{
aErrorWasShown = true; // Above has just displayed its error (e.g. syntax error in a line, failed to open the include file, etc). So override the default set earlier.
return NULL;
}
if (mIncludeLibraryFunctionsThenExit)
{
// For each included library-file, write out two #Include lines:
// 1) Use #Include in its "change working directory" mode so that any explicit #include directives
// or FileInstalls inside the library file itself will work consistently and properly.
// 2) Use #IncludeAgain (to improve performance since no dupe-checking is needed) to include
// the library file itself.
// We don't directly append library files onto the main script here because:
// 1) ahk2exe needs to be able to see and act upon FileInstall and #Include lines (i.e. library files
// might contain #Include even though it's rare).
// 2) #IncludeAgain and #Include directives that bring in fragments rather than entire functions or
// subroutines wouldn't work properly if we resolved such includes in AutoHotkey.exe because they
// wouldn't be properly interleaved/asynchronous, but instead brought out of their library file
// and deposited separately/synchronously into the temp-include file by some new logic at the
// AutoHotkey.exe's code for the #Include directive.
// 3) ahk2exe prefers to omit comments from included files to minimize size of compiled scripts.
// It already displayed the error for us. These mem errors are so unusual that we're not going
// to bother varying the error message to include ERR_ABORT if this occurs during runtime.
return NULL;
Func *the_new_func = new Func(new_name, aIsBuiltIn);
if (!the_new_func)
{
ScriptError(ERR_OUTOFMEM);
return NULL;
}
// v1.0.47: The following ISN'T done because it would slow down commonly used functions. This is because
// commonly-called functions like InStr() tend to be added first (since they appear so often throughout
// the script); thus subsequent lookups are fast if they are kept at the beginning of the list rather
// than being displaced to the end by all other functions).
// NOT DONE for the reason above:
// Unlike most of the other link lists, attach new items at the beginning of the list because
// that allows the standard/user library feature to perform much better for scripts that have hundreds
// of functions. This is because functions brought in dynamically from a library will then be at the
// beginning of the list, which allows the function lookup that immediately follows library-loading to
// find a match almost immediately.
if (!mFirstFunc) // The list is empty, so this will be the first and last item.
mFirstFunc = the_new_func;
else
mLastFunc->mNextFunc = the_new_func;
// This must be done after the above:
mLastFunc = the_new_func; // There's at least one spot in the code that relies on mLastFunc being the most recently added function.
return the_new_func;
}
size_t Line::ArgLength(int aArgNum)
// "ArgLength" is the arg's fully resolved, dereferenced length during runtime.
// Callers must call this only at times when sArgDeref and sArgVar are defined/meaningful.
// Caller must ensure that aArgNum should be 1 or greater.
// ArgLength() was added in v1.0.44.14 to help its callers improve performance by avoiding
// costly calls to strlen() (which is especially beneficial for huge strings).
{
#ifdef _DEBUG
if (aArgNum < 1)
{
LineError("DEBUG: BAD", WARN);
aArgNum = 1; // But let it continue.
}
#endif
if (aArgNum > mArgc) // Arg doesn't exist, so don't try accessing sArgVar (unlike sArgDeref, it wouldn't be valid to do so).
return 0; // i.e. treat it as the empty string.
// The length is not known and must be calculcated in the following situations:
// - The arg consists of more than just a single isolated variable name (not possible if the arg is
// ARG_TYPE_INPUT_VAR).
// - The arg is a built-in variable, in which case the length isn't known, so it must be derived from
// the string copied into sArgDeref[] by an earlier stage.
// - The arg is a normal variable but it's VAR_ATTRIB_BINARY_CLIP. In such cases, our callers do not
// recognize/support binary-clipboard as binary and want the apparent length of the string returned
// (i.e. strlen(), which takes into account the position of the first binary zero wherever it may be).
--aArgNum; // Convert to zero-based index (squeeze a little more performance out of it by avoiding a new variable).
if (sArgVar[aArgNum])
{
Var &var = *sArgVar[aArgNum]; // For performance and convenience.
if (var.Type() == VAR_NORMAL && (g_NoEnv || var.Length())) // v1.0.46.02: Recognize environment variables (when g_NoEnv==false) by falling through to strlen() for them.
return var.LengthIgnoreBinaryClip(); // Do it the fast way (unless it's binary clipboard, in which case this call will internally call strlen()).
}
// Otherwise, length isn't known due to no variable, a built-in variable, or an environment variable.
// So do it the slow way.
return strlen(sArgDeref[aArgNum]);
}
Var *Line::ResolveVarOfArg(int aArgIndex, bool aCreateIfNecessary)
// Returns NULL on failure. Caller has ensured that none of this arg's derefs are function-calls.
// Args that are input or output variables are normally resolved at load-time, so that
// they contain a pointer to their Var object. This is done for performance. However,
// in order to support dynamically resolved variables names like AutoIt2 (e.g. arrays),
// we have to do some extra work here at runtime.
// Callers specify false for aCreateIfNecessary whenever the contents of the variable
// they're trying to find is unimportant. For example, dynamically built input variables,
// such as "StringLen, length, array%i%", do not need to be created if they weren't
// previously assigned to (i.e. they weren't previously used as an output variable).
// In the above example, the array element would never be created here. But if the output
// variable were dynamic, our call would have told us to create it.
{
// The requested ARG isn't even present, so it can't have a variable. Currently, this should
// never happen because the loading procedure ensures that input/output args are not marked
// as variables if they are blank (and our caller should check for this and not call in that case):
if (aArgIndex >= mArgc)
return NULL;
ArgStruct &this_arg = mArg[aArgIndex]; // For performance and convenience.
// Since this function isn't inline (since it's called so frequently), there isn't that much more
// overhead to doing this check, even though it shouldn't be needed since it's the caller's
// responsibility:
if (this_arg.type == ARG_TYPE_NORMAL) // Arg isn't an input or output variable.
return NULL;
if (!*this_arg.text) // The arg's variable is not one that needs to be dynamically resolved.
return VAR(this_arg); // Return the var's address that was already determined at load-time.
// The above might return NULL in the case where the arg is optional (i.e. the command allows
// the var name to be omitted). But in that case, the caller should either never have called this
// function or should check for NULL upon return. UPDATE: This actually never happens, see
// comment above the "if (aArgIndex >= mArgc)" line.
// Static to correspond to the static empty_var further below. It needs the memory area
// to support resolving dynamic environment variables. In the following example,
// the result will be blank unless the first line is present (without this fix here):
//null = %SystemRoot% ; bogus line as a required workaround in versions prior to v1.0.16
//thing = SystemRoot
//StringTrimLeft, output, %thing%, 0
//msgbox %output%
static char sVarName[MAX_VAR_NAME_LENGTH + 1]; // Will hold the dynamically built name.
// At this point, we know the requested arg is a variable that must be dynamically resolved.
// This section is similar to that in ExpandArg(), so they should be maintained together:
char *pText = this_arg.text; // Start at the begining of this arg's text.
int var_name_length = 0;
if (this_arg.deref) // There's at least one deref.
{
// Caller has ensured that none of these derefs are function calls (i.e. deref->is_function is alway false).
for (DerefType *deref = this_arg.deref // Start off by looking for the first deref.
; deref->marker; ++deref) // A deref with a NULL marker terminates the list.
{
// FOR EACH DEREF IN AN ARG (if we're here, there's at least one):
// Copy the chars that occur prior to deref->marker into the buffer:
Var *Script::FindVar(char *aVarName, size_t aVarNameLength, int *apInsertPos, int aAlwaysUse
, bool *apIsException, bool *apIsLocal)
// Caller has ensured that aVarName isn't NULL. It must also ignore the contents of apInsertPos when
// a match (non-NULL value) is returned.
// Returns the Var whose name matches aVarName. If it doesn't exist, NULL is returned.
// If caller provided a non-NULL apInsertPos, it will be given a the array index that a newly
// inserted item should have to keep the list in sorted order (which also allows the ListVars command
// to display the variables in alphabetical order).
{
if (!*aVarName)
return NULL;
if (!aVarNameLength) // Caller didn't specify, so use the entire string.
aVarNameLength = strlen(aVarName);
// For the below, no error is reported because callers don't want that. Instead, simply return
// NULL to indicate that names that are illegal or too long are not found. When the caller later
// tries to add the variable, it will get an error then:
if (aVarNameLength > MAX_VAR_NAME_LENGTH)
return NULL;
// The following copy is made because it allows the various searches below to use stricmp() instead of
// strlicmp(), which close to doubles their performance. The copy includes only the first aVarNameLength
// characters from aVarName:
char var_name[MAX_VAR_NAME_LENGTH + 1];
strlcpy(var_name, aVarName, aVarNameLength + 1); // +1 to convert length to size.
Var *found_var = NULL; // Set default.
bool is_local;
if (aAlwaysUse == ALWAYS_USE_GLOBAL)
is_local = false;
else if (aAlwaysUse == ALWAYS_USE_LOCAL)
// v1.0.44.10: The following was changed from it's former value of "true" so that places further below
// (including passing is_local is call to AddVar()) don't have to ensure that g.CurrentFunc!=NULL.
// This fixes a crash that occured when a caller specified ALWAYS_USE_LOCAL even though the current
// thread isn't actually inside a *called* function (perhaps meaning things like a timed subroutine
// that lies inside a "container function").
// Some callers like SYSGET_CMD_MONITORAREA might try to find/add a local array if they see that their
// base variable is classified as local (such classification occurs at loadtime, but only for non-dynamic
// variable references). But the current thread entered a "container function" by means other than a
// function-call (such as SetTimer), not only is g.CurrentFunc NULL, but there's no easy way to discover
// which function owns the currently executing line (a means could be added to the class "Var" or "Line"
// but doesn't seem worth it yet due to performance and memory reduction).
is_local = (g.CurrentFunc != NULL);
else if (aAlwaysUse == ALWAYS_PREFER_LOCAL)
{
if (g.CurrentFunc) // Caller relies on us to do this final check.
is_local = true;
else
{
is_local = false;
aAlwaysUse = ALWAYS_USE_GLOBAL; // Override aAlwaysUse for maintainability, in case there are more references to it below.
}
}
else // aAlwaysUse == ALWAYS_USE_DEFAULT
{
is_local = g.CurrentFunc && g.CurrentFunc->mDefaultVarType != VAR_ASSUME_GLOBAL; // i.e. ASSUME_LOCAL or ASSUME_NONE
if (mFuncExceptionVar) // Caller has ensured that this non-NULL if and only if g.CurrentFunc is non-NULL.
{
int i;
for (i = 0; i < mFuncExceptionVarCount; ++i)
{
if (!stricmp(var_name, mFuncExceptionVar[i]->mName)) // lstrcmpi() is not used: 1) avoids breaking exisitng scripts; 2) provides consistent behavior across multiple locales; 3) performance.
{
is_local = !is_local; // Since it's an exception, it's always the opposite of what it would have been.
found_var = mFuncExceptionVar[i];
break;
}
}
// The following section is necessary because a function's parameters are not put into the
// exception list during load-time. Thus, for an VAR_ASSUME_GLOBAL function, these are basically
// treated as exceptions too.
// If this function is one that assumes variables are global, the function's parameters are
// implicitly declared local because parameters are always local:
// Since the following is inside this block, it is checked only at loadtime. It doesn't need
// to be checked at runtime because most things that resolve input variables or variables whose
// contents will be read (as compared to something that tries to create a dynamic variable, such
// as ResolveVarOfArg() for an output variable) at runtime use the ALWAYS_PREFER_LOCAL flag to
// indicate that a local of the same name as a global should take precedence. This adds more
// flexibility/benefit than its costs in terms of confusion because otherwise there would be
// no way to dynamically reference the local variables of an assume-global function.
if (g.CurrentFunc->mDefaultVarType == VAR_ASSUME_GLOBAL && !is_local) // g.CurrentFunc is also known to be non-NULL in this case.
{
for (i = 0; i < g.CurrentFunc->mParamCount; ++i)
if (!stricmp(var_name, g.CurrentFunc->mParam[i].var->mName)) // lstrcmpi() is not used: 1) avoids breaking exisitng scripts; 2) provides consistent behavior across multiple locales; 3) performance.
{
is_local = true;
found_var = g.CurrentFunc->mParam[i].var;
break;
}
}
} // if (there is an exception list)
} // aAlwaysUse == ALWAYS_USE_DEFAULT
// Above has ensured that g.CurrentFunc!=NULL whenever is_local==true.
if (apIsLocal) // Its purpose is to inform caller of type it would have been in case we don't find a match.
*apIsLocal = is_local; // And it stays this way even if globals will be searched because caller wants that. In other words, a local var is created by default when there is not existing global or local.
if (apInsertPos) // Set default. Caller should ignore the value when match is found.
*apInsertPos = -1;
if (apIsException)
*apIsException = (found_var != NULL);
if (found_var) // Match found (as an exception or load-time "is parameter" exception).
return found_var; // apInsertPos does not need to be set because caller doesn't need it when match is found.
// Init for binary search loop:
int left, right, mid, result; // left/right must be ints to allow them to go negative and detect underflow.
Var **var; // An array of pointers-to-var.
if (is_local)
{
var = g.CurrentFunc->mVar;
right = g.CurrentFunc->mVarCount - 1;
}
else
{
var = mVar;
right = mVarCount - 1;
}
// Binary search:
for (left = 0; left <= right;) // "right" was already initialized above.
{
mid = (left + right) / 2;
result = stricmp(var_name, var[mid]->mName); // lstrcmpi() is not used: 1) avoids breaking exisitng scripts; 2) provides consistent behavior across multiple locales; 3) performance.
if (result > 0)
left = mid + 1;
else if (result < 0)
right = mid - 1;
else // Match found.
return var[mid];
}
// Since above didn't return, no match was found in the main list, so search the lazy list if there
// is one. If there's no lazy list, the value of "left" established above will be used as the
// insertion point further below:
if (is_local)
{
var = g.CurrentFunc->mLazyVar;
right = g.CurrentFunc->mLazyVarCount - 1;
}
else
{
var = mLazyVar;
right = mLazyVarCount - 1;
}
if (var) // There is a lazy list to search (and even if the list is empty, left must be reset to 0 below).
{
// Binary search:
for (left = 0; left <= right;) // "right" was already initialized above.
{
mid = (left + right) / 2;
result = stricmp(var_name, var[mid]->mName); // lstrcmpi() is not used: 1) avoids breaking exisitng scripts; 2) provides consistent behavior across multiple locales; 3) performance.
if (result > 0)
left = mid + 1;
else if (result < 0)
right = mid - 1;
else // Match found.
return var[mid];
}
}
// Since above didn't return, no match was found and "left" always contains the position where aVarName
// should be inserted to keep the list sorted. The item is always inserted into the lazy list unless
// there is no lazy list.
// Set the output parameter, if present:
if (apInsertPos) // Caller wants this value even if we'll be resorting to searching the global list below.
*apInsertPos = left; // This is the index a newly inserted item should have to keep alphabetical order.
// Since no match was found, if this is a local fall back to searching the list of globals at runtime
// if the caller didn't insist on a particular type:
if (is_local)
{
if (aAlwaysUse == ALWAYS_PREFER_LOCAL)
{
// In this case, callers want to fall back to globals when a local wasn't found. However,
// they want the insertion (if our caller will be doing one) to insert according to the
// current assume-mode. Therefore, if the mode is VAR_ASSUME_GLOBAL, pass the apIsLocal
// and apInsertPos variables to FindVar() so that it will update them to be global.
// Otherwise, do not pass them since they were already set correctly by us above.
if (g.CurrentFunc->mDefaultVarType == VAR_ASSUME_GLOBAL)
// Increase by orders of magnitude each time because realloc() is probably an expensive operation
// in terms of hurting performance. So here, a little bit of memory is sacrificed to improve
// the expected level of performance for scripts that use hundreds of thousands of variables.
if (!var_count_max)
alloc_count = aIsLocal ? 100 : 1000; // 100 conserves memory since every function needs such a block, and most functions have much fewer than 100 local variables.
else if (var_count_max < 1000)
alloc_count = 1000;
else if (var_count_max < 9999) // Making this 9999 vs. 10000 allows an exact/whole number of lazy_var blocks to fit into main indices between 10000 and 99999
alloc_count = 9999;
else if (var_count_max < 100000)
{
alloc_count = 100000;
// This is also the threshold beyond which the lazy list is used to accelerate performance.
// Create the permanently lazy list:
Var **&lazy_var = aIsLocal ? g.CurrentFunc->mLazyVar : mLazyVar;
if ( !(lazy_var = (Var **)malloc(MAX_LAZY_VARS * sizeof(Var *))) )
{
ScriptError(ERR_OUTOFMEM);
return NULL;
}
}
else if (var_count_max < 1000000)
alloc_count = 1000000;
else
alloc_count = var_count_max + 1000000; // i.e. continue to increase by 4MB (1M*4) each time.
Var **temp = (Var **)realloc(var, alloc_count * sizeof(Var *)); // If passed NULL, realloc() will do a malloc().
if (!temp)
{
ScriptError(ERR_OUTOFMEM);
return NULL;
}
var = temp;
var_count_max = alloc_count;
}
if (!lazy_var)
{
if (aInsertPos != var_count) // Need to make room at the indicated position for this variable.
//else both are zero or the item is being inserted at the end of the list, so it's easy.
var[aInsertPos] = the_new_var;
++var_count;
return the_new_var;
}
//else the variable was already inserted into the lazy list, so the above is not done.
// Since above didn't return, the lazy list is not only present, but full because otherwise it
// would have returned higher above.
// Since the lazy list is now at its max capacity, merge it into the main list (if the
// main list was at capacity, this section relies upon the fact that the above already
// increased its capacity by an amount far larger than the number of items containined
// in the lazy list).
// LAZY LIST: Although it's not nearly as good as hashing (which might be implemented in the future,
// though it would be no small undertaking since it affects so many design aspects, both load-time
// and runtime for scripts), this method of accelerating inserts into a binary search array is
// enormously beneficial because it improves the scalability of binary-search by two orders
// of magnitude (from about 100,000 variables to at least 5M). Credit for the idea goes to Lazlo.
// DETAILS:
// The fact that this merge operation is so much faster than total work required
// to insert each one into the main list is the whole reason for having the lazy
// list. In other words, the large memmove() that would otherwise be required
// to insert each new variable into the main list is completely avoided. Large memmove()s
// are far more costly than small ones because apparently they can't fit into the CPU
// cache, so the operation would take hundreds or even thousands of times longer
// depending on the speed difference between main memory and CPU cache. But above and
// beyond the CPU cache issue, the lazy sorting method results in vastly less memory
// being moved than would have been required without it, so even if the CPU doesn't have
// a cache, the lazy list method vastly increases performance for scripts that have more
// than 100,000 variables, allowing at least 5 million variables to be created without a
// dramatic reduction in performance.
char *target_name;
Var **insert_pos, **insert_pos_prev;
int i, left, right, mid;
// Append any items from the lazy list to the main list that are alphabetically greater than
// the last item in the main list. Above has already ensured that the main list is large enough
// to accept all items in the lazy list.
for (i = lazy_var_count - 1, target_name = var[var_count - 1]->mName
; i > -1 && stricmp(target_name, lazy_var[i]->mName) < 0
; --i);
// Above is a self-contained loop.
// Now do a separate loop to append (in the *correct* order) anything found above.
for (int j = i + 1; j < lazy_var_count; ++j) // Might have zero iterations.
var[var_count++] = lazy_var[j];
lazy_var_count = i + 1; // The number of items that remain after moving out those that qualified.
// This will have zero iterations if the above already moved them all:
for (insert_pos = var + var_count, i = lazy_var_count - 1; i > -1; --i)
{
// Modified binary search that relies on the fact that caller has ensured a match will never
// be found in the main list for each item in the lazy list:
for (target_name = lazy_var[i]->mName, left = 0, right = (int)(insert_pos - var - 1); left <= right;)
{
mid = (left + right) / 2;
if (stricmp(target_name, var[mid]->mName) > 0) // lstrcmpi() is not used: 1) avoids breaking exisitng scripts; 2) provides consistent behavior across multiple locales; 3) performance.
left = mid + 1;
else // it must be < 0 because caller has ensured it can't be equal (i.e. that there will be no match)
right = mid - 1;
}
// Now "left" contains the insertion point is is known to be less than var_count due to a previous
// set of loops above. Make a gap there large enough to hold all items because that allows a
// smaller total amount of memory to be moved by shifting the gap to the left in the main list,
// gradually filling it as we go:
insert_pos_prev = insert_pos; // "prev" is the now the position of the beginning of the gap, but the gap is about to be shifted left by moving memory right.
insert_pos = var + left; // This is where it *would* be inserted if we weren't doing the accelerated merge.
if (!strcmp(lowercase, "clipboard")) return (void *)VAR_CLIPBOARD;
if (!strcmp(lowercase, "clipboardall")) return (void *)VAR_CLIPBOARDALL;
if (!strcmp(lowercase, "comspec")) return BIV_ComSpec; // Lacks an "A_" prefix for backward compatibility with pre-NoEnv scripts and also it's easier to type & remember.
if (!strcmp(lowercase, "programfiles")) return BIV_ProgramFiles; // v1.0.43.08: Added to ease the transition to #NoEnv.
// Otherwise:
return (void *)VAR_NORMAL;
}
// Otherwise, lowercase begins with "a_", so it's probably one of the built-in variables.
char *lower = lowercase + 2;
// Keeping the most common ones near the top helps performance a little.
if (!strcmp(lower, "index")) return BIV_LoopIndex; // A short name since it's typed so often.
if ( !strcmp(lower, "mmmm") // Long name of month.
if (!strcmp(lower, "temp")) return BIV_Temp; // Debatably should be A_TempDir, but brevity seemed more popular with users, perhaps for heavy uses of the temp folder.
if (!strcmp(lower, "programfiles")) return BIV_ProgramFiles;
if (!strcmp(lower, "mydocuments")) return BIV_MyDocuments;
// Otherwise, it can't be a match for any built-in variable:
return (void *)VAR_NORMAL;
}
if (!strncmp(lower, "reg", 3))
{
lower += 3;
if (!strcmp(lower, "type")) return BIV_LoopRegType;
if (!strcmp(lower, "key")) return BIV_LoopRegKey;
if (!strcmp(lower, "subkey")) return BIV_LoopRegSubKey;
if (!strcmp(lower, "name")) return BIV_LoopRegName;
if (!strcmp(lower, "timemodified")) return BIV_LoopRegTimeModified;
// Otherwise, it can't be a match for any built-in variable:
return (void *)VAR_NORMAL;
}
}
if (!strcmp(lower, "thisfunc")) return BIV_ThisFunc;
if (!strcmp(lower, "thislabel")) return BIV_ThisLabel;
if (!strcmp(lower, "thismenuitem")) return BIV_ThisMenuItem;
if (!strcmp(lower, "thismenuitempos")) return BIV_ThisMenuItemPos;
if (!strcmp(lower, "thismenu")) return BIV_ThisMenu;
if (!strcmp(lower, "thishotkey")) return BIV_ThisHotkey;
if (!strcmp(lower, "priorhotkey")) return BIV_PriorHotkey;
if (!strcmp(lower, "timesincethishotkey")) return BIV_TimeSinceThisHotkey;
if (!strcmp(lower, "timesincepriorhotkey")) return BIV_TimeSincePriorHotkey;
if (!strcmp(lower, "endchar")) return BIV_EndChar;
if (!strcmp(lower, "lasterror")) return BIV_LastError;
if (!strcmp(lower, "eventinfo")) return BIV_EventInfo; // It's called "EventInfo" vs. "GuiEventInfo" because it applies to non-Gui events such as OnClipboardChange.
if (!strcmp(lower, "guicontrol")) return BIV_GuiControl;
if ( !strcmp(lower, "guicontrolevent") // v1.0.36: A_GuiEvent was added as a synonym for A_GuiControlEvent because it seems unlikely that A_GuiEvent will ever be needed for anything:
// Don't check aStartingLine here at top: only do it at the bottom
// for its differing return values.
for (Line *line = aStartingLine; line;)
{
// Check if any of each arg's derefs are function calls. If so, do some validation and
// preprocessing to set things up for better runtime performance:
for (i = 0; i < line->mArgc; ++i) // For each arg.
{
ArgStruct &this_arg = line->mArg[i]; // For performance and convenience.
// Exclude the derefs of output and input vars from consideration, since they can't
// be function calls:
if (!this_arg.is_expression // For now, only expressions are capable of calling functions. If ever change this, might want to add a check here for this_arg.type != ARG_TYPE_NORMAL (for performance).
|| !this_arg.deref) // No function-calls present.
continue;
for (deref = this_arg.deref; deref->marker; ++deref) // For each deref.
{
if (!deref->is_function)
continue;
if ( !(deref->func = FindFunc(deref->marker, deref->length)) )
{
#ifndef AUTOHOTKEYSC
bool error_was_shown;
if ( !(deref->func = FindFuncInLibrary(deref->marker, deref->length, error_was_shown)) )
{
abort = true; // So that the caller doesn't also report an error.
// When above already displayed the proximate cause of the error, it's usually
// undesirable to show the cascade effects of that error in a second dialog:
//else it's some other, non-special character, so ignore it.
} // for() that finds the end of this param of this function.
// Above would have returned unless *param_end is either a comma or close-paren (namely the
// one that terminates this parameter of this function).
if (deref->param_count >= func.mParamCount) // Check this every iteration to avoid going beyond MAX_FUNCTION_PARAMS.
{
abort = true; // So that the caller doesn't also report an error.
return line->PreparseError("Too many parameters passed to function.", deref->marker);
}
// Below relies on the above check having been done first to avoid reading beyond the
// end of the mParam array.
// If this parameter is formally declared as ByRef, report a load-time error if
// the actual-parameter is obviously not a variable (can't catch everything, such
// as invalid double derefs, e.g. Array%VarContainingSpaces%):
if (!func.mIsBuiltIn && func.mParam[deref->param_count].is_byref)
{
// First check if there are any EXPR_TELLTALES characters in this param, since the
// presence of an expression for this parameter means it can't resolve to a variable
// as required by ByRef:
for (cp = param_start, param_last_char = omit_trailing_whitespace(param_start, param_end - 1)
; cp <= param_last_char; ++cp)
{
if (*cp == ':' && cp[1] == '=')
// v1.0.46.05: This section fixes the inability to pass ByRef certain non-trivial
// assignments like X := " ". Although this doesn't give 100% detection, something
// more elaborate seems unjustified (in both code size and performance) given that
// this is only a syntax check.
break;
if (strchr(EXPR_FORBIDDEN_BYREF, *cp)) // This character isn't allowed in something passed ByRef unless it's an assignment (which is checked below).
{
if (Line::StartsWithAssignmentOp(cp) || strstr(cp, " ? ")) // v1.0.46.09: Also allow a ternary unconditionally, because it can be an arbitrarily complex expression followed by two branches that yield variables.
{
// Skip over :=, +=, -=, *=, /=, ++, -- ... because they can be passed ByRef.
// In fact, don't even continue the loop because any assignment can be followed
// by an arbitrarily complex sub-expression that shouldn't disqualify ByRef.
break;
}
abort = true; // So that the caller doesn't also report an error.
return line->PreparseError(ERR_BYREF, param_start); // param_start seems more informative than func.mParam[deref->param_count].var->mName
}
}
// Below relies on the above having been done because the above should prevent
// any is_function derefs from being possible since their parentheses would have been caught
// as an error:
// For each deref after the function name itself, ensure that there is at least
// one deref in between this param's param_start and param_end. This finds many
// common syntax errors such as passing a literal number or string to a ByRef
// parameter. Note that there can be more than one for something like Array%i%_%j%
abort = true; // So that the caller doesn't also report an error.
return line->PreparseError(ERR_BLANK_PARAM, param_start); // Report param_start vs. aBuf to give an idea of where the blank parameter is in a possibly long list of params.
}
}
//else it might be ')', in which case the next iteration will handle it.
// Above has ensured that param_start now points to the next parameter, or ')' if none.
} // for each parameter of this function call.
if (deref->param_count < func.mMinParams)
{
abort = true; // So that the caller doesn't also report an error.
return line->PreparseError("Too few parameters passed to function.", deref->marker);
}
} // for each deref of this arg
} // for each arg of this line
// All lines in our recursion layer are assigned to the block that the caller specified:
if (line->mParentLine == NULL) // i.e. don't do it if it's already "owned" by an IF or ELSE.
line->mParentLine = aParentLine; // Can be NULL.
if (ACT_IS_IF_OR_ELSE_OR_LOOP(line->mActionType) || line->mActionType == ACT_REPEAT)
{
// Make the line immediately following each ELSE, IF or LOOP be enclosed by that stmt.
// This is done to make it illegal for a Goto or Gosub to jump into a deeper layer,
// such as in this example:
// #y::
// ifwinexist, pad
// {
// goto, label1
// ifwinexist, pad
// label1:
// ; With or without the enclosing block, the goto would still go to an illegal place
// ; in the below, resulting in an "unexpected else" error:
// {
// msgbox, ifaction
// } ; not necessary to make this line enclosed by the if because labels can't point to it?
// else
// msgbox, elseaction
// }
// return
// In this case, the loader should have already ensured that line->mNextLine is not NULL:
line->mNextLine->mParentLine = line;
// Go onto the IF's or ELSE's action in case it too is an IF, rather than skipping over it:
line = line->mNextLine;
continue;
}
switch (line->mActionType)
{
case ACT_BLOCK_BEGIN:
// Some insane limit too large to ever likely be exceeded, yet small enough not
// to be a risk of stack overflow when recursing in ExecUntil(). Mostly, this is
// here to reduce the chance of a program crash if a binary file, a corrupted file,
// or something unexpected has been loaded as a script when it shouldn't have been.
// Update: Increased the limit from 100 to 1000 so that large "else if" ladders
// can be constructed. Going much larger than 1000 seems unwise since ExecUntil()
// will have to recurse for each nest-level, possibly resulting in stack overflow
// if things get too deep:
if (nest_level > 1000)
{
abort = true; // So that the caller doesn't also report an error.
return line->PreparseError("Nesting too deep."); // Short msg since so rare.
}
// Since the current convention is to store the line *after* the
// BLOCK_END as the BLOCK_BEGIN's related line, that line can
// be legitimately NULL if this block's BLOCK_END is the last
// line in the script. So it's up to the called function
// to report an error if it never finds a BLOCK_END for us.
// UPDATE: The design requires that we do it here instead:
++nest_level;
if (NULL == (line->mRelatedLine = PreparseBlocks(line->mNextLine, 1, line)))
if (abort) // the above call already reported the error.
return NULL;
else
{
abort = true; // So that the caller doesn't also report an error.
// If not end-of-script or error, line_temp is now either:
// 1) If this if's/loop's action was a BEGIN_BLOCK: The line after the end of the block.
// 2) If this if's/loop's action was another IF or LOOP:
// a) the line after that if's else's action; or (if it doesn't have one):
// b) the line after that if's/loop's action
// 3) If this if's/loop's action was some single-line action: the line after that action.
// In all of the above cases, line_temp is now the line where we
// would expect to find an ELSE for this IF, if it has one.
// Now the above has ensured that line_temp is this line's else, if it has one.
// Note: line_temp will be NULL if the end of the script has been reached.
// UPDATE: That can't happen now because all scripts end in ACT_EXIT:
if (line_temp == NULL) // Error or end-of-script was reached.
return NULL;
// Seems best to keep this check for mainability because changes to other checks can impact
// whether this check will ever be "true":
if (line->mRelatedLine != NULL)
return line->PreparseError("Q"); // Placeholder since it shouldn't happen. Formerly "This if-statement or LOOP unexpectedly already had an ELSE or end-point."
// Set it to the else's action, rather than the else itself, since the else itself
// is never needed during execution. UPDATE: No, instead set it to the ELSE itself
// (if it has one) since we jump here at runtime when the IF is finished (whether
// it's condition was true or false), thus skipping over any nested IF's that
// aren't in blocks beneath it. If there's no ELSE, the below value serves as
// the jumppoint we go to when the if-statement is finished. Example:
// if x
// if y
// if z
// action1
// else
// action2
// action3
// x's jumppoint should be action3 so that all the nested if's
// under the first one can be skipped after the "if x" line is recursively
// evaluated. Because of this behavior, all IFs will have a related line
// with the possibly exception of the very last if-statement in the script
// (which is possible only if the script doesn't end in a Return or Exit).
line->mRelatedLine = line_temp; // Even if <line> is a LOOP and line_temp and else?
// Even if aMode == ONLY_ONE_LINE, an IF and its ELSE count as a single
// statement (one line) due to its very nature (at least for this purpose),
// so always continue on to evaluate the IF's ELSE, if present:
if (line_temp->mActionType == ACT_ELSE)
{
if (line->mActionType == ACT_LOOP || line->mActionType == ACT_REPEAT)
{
// this can't be our else, so let the caller handle it.
if (aMode != ONLY_ONE_LINE)
// This ELSE was encountered while sequentially scanning the contents
// of a block or at the otuermost nesting layer. More thought is required
// to verify this is correct. UPDATE: This check is very old and I haven't
// found a case that can produce it yet, but until proven otherwise its safer
if ( !(line->mAttribute = FindLabel(line_raw_arg2)) )
if (!Hotkey::ConvertAltTab(line_raw_arg2, true))
return line->PreparseError(ERR_NO_LABEL);
break;
case ACT_SETTIMER:
if (!line->ArgHasDeref(1))
if ( !(line->mAttribute = FindLabel(line_raw_arg1)) )
return line->PreparseError(ERR_NO_LABEL);
if (*line_raw_arg2 && !line->ArgHasDeref(2))
if (!Line::ConvertOnOff(line_raw_arg2) && !IsPureNumeric(line_raw_arg2, true) // v1.0.46.16: Allow negatives to support the new run-only-once mode.
&& !line->mArg[1].is_expression) // v1.0.46.10: Don't consider expressions THAT CONTAIN NO VARIABLES OR FUNCTION-CALLS like "% 2*500" to be a syntax error.
return line->PreparseError(ERR_PARAM2_INVALID);
break;
case ACT_GROUPADD: // This must be done here because it relies on all other lines already having been added.
if (*LINE_RAW_ARG4 && !line->ArgHasDeref(4))
{
// If the label name was contained in a variable, that label is now resolved and cannot
// be changed. This is in contrast to something like "Gosub, %MyLabel%" where a change in
// the value of MyLabel will change the behavior of the Gosub at runtime:
Label *label = FindLabel(LINE_RAW_ARG4);
if (!label)
return line->PreparseError(ERR_NO_LABEL);
line->mRelatedLine = (Line *)label; // The script loader has ensured that this can't be NULL.
// Can't do this because the current line won't be the launching point for the
// Gosub. Instead, the launching point will be the GroupActivate rather than the
// GroupAdd, so it will be checked by the GroupActivate or not at all (since it's
// not that important in the case of a Gosub -- it's mostly for Goto's):
//return IsJumpValid(label->mJumpToLine);
}
break;
case ACT_ELSE:
// Should never happen because the part that handles the if's, above, should find
// all the elses and handle them. UPDATE: This happens if there's
// an extra ELSE in this scope level that has no IF:
return line->PreparseError(ERR_ELSE_WITH_NO_IF);
} // switch()
line = line->mNextLine; // If NULL due to physical end-of-script, the for-loop's condition will catch it.
if (aMode == ONLY_ONE_LINE) // Return the next unprocessed line to the caller.
// In this case, line shouldn't be (and probably can't be?) NULL because the line after
// a single-line action shouldn't be the physical end of the script. That's because
// the loader has ensured that all scripts now end in ACT_EXIT. And that final
// ACT_EXIT should never be parsed here in ONLY_ONE_LINE mode because the only time
// that mode is used is for the action of an IF, an ELSE, or possibly a LOOP.
// In all of those cases, the final ACT_EXIT line in the script (which is explicitly
// insertted by the loader) cannot be the line that was just processed by the
// switch(). Therefore, the above assignment should not have set line to NULL
// (which is good because NULL would probably be construed as "failure" by our
// caller in this case):
return line;
// else just continue the for-loop at the new value of line.
} // for()
// End of script has been reached. line is now NULL so don't dereference it.
// If we were still looking for an EndBlock to match up with a begin, that's an error.
// This indicates that the at least one BLOCK_BEGIN is missing a BLOCK_END.
// However, since the blocks were already balanced by the block pre-parsing function,
// this should be impossible unless the design of this function is flawed.
if (aMode == UNTIL_BLOCK_END)
#ifdef _DEBUG
return mLastLine->PreparseError("DEBUG: The script ended while a block was still open."); // This is a bug because the preparser already verified all blocks are balanced.
#else
return NULL; // Shouldn't happen, so just return failure.
#endif
// If we were told to process a single line, we were recursed and it should have returned above,
// so it's an error here (can happen if we were called with aStartingLine == NULL?):
if (aMode == ONLY_ONE_LINE)
return mLastLine->PreparseError("Q"); // Placeholder since probably impossible. Formerly "The script ended while an action was still expected."
// Otherwise, return something non-NULL to indicate success to the top-level caller:
Line *Line::sLog[] = {NULL}; // Initialize all the array elements.
DWORD Line::sLogTick[]; // No initialization needed.
int Line::sLogNext = 0; // Start at the first element.
#ifdef AUTOHOTKEYSC // Reduces code size to omit things that are unused, and helps catch bugs at compile-time.
char *Line::sSourceFile[1]; // No init needed.
#else
char **Line::sSourceFile = NULL; // Init to NULL for use with realloc() and for maintainability.
int Line::sMaxSourceFiles = 0;
#endif
int Line::sSourceFileCount = 0; // Zero source files initially. The main script will be the first.
char *Line::sDerefBuf = NULL; // Buffer to hold the values of any args that need to be dereferenced.
size_t Line::sDerefBufSize = 0;
int Line::sLargeDerefBufs = 0; // Keeps track of how many large bufs exist on the call-stack, for the purpose of determining when to stop the buffer-freeing timer.
char *Line::sArgDeref[MAX_ARGS]; // No init needed.
Var *Line::sArgVar[MAX_ARGS]; // Same.
void Line::FreeDerefBufIfLarge()
{
if (sDerefBufSize > LARGE_DEREF_BUF_SIZE)
{
// Freeing the buffer should be safe even if the script's current quasi-thread is in the middle
// of executing a command, since commands are all designed to make only temporary use of the
// deref buffer (they make copies of anything they need prior to calling MsgSleep() or anything
// else that might pump messages and thus result in a call to us here).
free(sDerefBuf); // The above size-check has ensured this is non-NULL.
SET_S_DEREF_BUF(NULL, 0);
--sLargeDerefBufs;
if (!sLargeDerefBufs)
KILL_DEREF_TIMER
}
//else leave the timer running because some other deref buffer in a recursed ExpandArgs() layer
// is still waiting to be freed (even if it isn't, it should be harmless to keep the timer running
// just in case, since each call to ExpandArgs() will reset/postpone the timer due to the script
// having demonstrated that it isn't idle).
}
ResultType Line::ExecUntil(ExecUntilMode aMode, char **apReturnValue, Line **apJumpToLine)
// Start executing at "this" line, stop when aMode indicates.
// RECURSIVE: Handles all lines that involve flow-control.
// aMode can be UNTIL_RETURN, UNTIL_BLOCK_END, ONLY_ONE_LINE.
// Returns FAIL, OK, EARLY_RETURN, or EARLY_EXIT.
// apJumpToLine is a pointer to Line-ptr (handle), which is an output parameter. If NULL,
// the caller is indicating it doesn't need this value, so it won't (and can't) be set by
// the called recursion layer.
{
Line *unused_jump_to_line;
Line *&caller_jump_to_line = apJumpToLine ? *apJumpToLine : unused_jump_to_line; // Simplifies code in other places.
// Important to init, since most of the time it will keep this value.
// Tells caller that no jump is required (default):
caller_jump_to_line = NULL;
// The benchmark improvement of having the following variables declared outside the loop rather than inside
// is about 0.25%. Since that is probably not even statistically significant, the only reason for declaring
// them here is in case compilers other than MSVC++ 7.1 benefit more -- and because it's an old silly habit.
__int64 loop_iteration;
WIN32_FIND_DATA *loop_file;
RegItemStruct *loop_reg_item;
LoopReadFileStruct *loop_read_file;
char *loop_field;
Line *jump_to_line; // Don't use *apJumpToLine because it might not exist.
Label *jump_to_label; // For use with Gosub & Goto & GroupActivate.
ResultType if_condition, result;
LONG_OPERATION_INIT
for (Line *line = this; line != NULL;)
{
// If a previous command (line) had the clipboard open, perhaps because it directly accessed
// the clipboard via Var::Contents(), we close it here for performance reasons (see notes
// in Clipboard::Open() for details):
CLOSE_CLIPBOARD_IF_OPEN;
// The below must be done at least when the keybd or mouse hook is active, but is currently
// always done since it's a very low overhead call, and has the side-benefit of making
// the app maximally responsive when the script is busy during high BatchLines.
// This low-overhead call achieves at least two purposes optimally:
// 1) Keyboard and mouse lag is minimized when the hook(s) are installed, since this single
// Peek() is apparently enough to route all pending input to the hooks (though it's inexplicable
// why calling MsgSleep(-1) does not achieve this goal, since it too does a Peek().
// Nevertheless, that is the testing result that was obtained: the mouse cursor lagged
// in tight script loops even when MsgSleep(-1) or (0) was called every 10ms or so.
// 2) The app is maximally responsive while executing with a high or infinite BatchLines.
// 3) Hotkeys are maximally responsive. For example, if a user has game hotkeys, using
// a GetTickCount() method (which very slightly improves performance by cutting back on
// the number of Peek() calls) would introduce up to 10ms of delay before the hotkey
// finally takes effect. 10ms can be significant in games, where ping (latency) itself
// can sometimes be only 10 or 20ms. UPDATE: It looks like PeekMessage() yields CPU time
// automatically, similar to a Sleep(0), when our queue has no messages. Since this would
// make scripts slow to a crawl, only do the Peek() every 5ms or so (though the timer
// granularity is 10ms on mosts OSes, so that's the true interval).
// 4) Timed subroutines are run as consistently as possible (to help with this, a check
// similar to the below is also done for single commmands that take a long time, such
// as URLDownloadToFile, FileSetAttrib, etc.
LONG_OPERATION_UPDATE
// If interruptions are currently forbidden, it's our responsibility to check if the number
// of lines that have been run since this quasi-thread started now indicate that
// interruptibility should be reenabled. But if UninterruptedLineCountMax is negative, don't
// bother checking because this quasi-thread will stay non-interruptible until it finishes.
// v1.0.38.04: If g.ThreadIsCritical==true, no need to check or accumulate g.UninterruptedLineCount
// because the script is now in charge of this thread's interruptibility.
if (!g.AllowThreadToBeInterrupted && !g.ThreadIsCritical && g_script.mUninterruptedLineCountMax > -1) // Ordered for short-circuit performance.
{
// Note that there is a timer that handles the UninterruptibleTime setting, so we don't
// have handle that setting here. But that timer is killed by the DISABLE_UNINTERRUPTIBLE
// macro we call below. This is because we don't want the timer to "fire" after we've
// already met the conditions which allow interruptibility to be restored, because if
// it did, it might interfere with the fact that some other code might already be using
// g.AllowThreadToBeInterrupted again for its own purpose:
if (g.UninterruptedLineCount > g_script.mUninterruptedLineCountMax)
MAKE_THREAD_INTERRUPTIBLE
else
// Incrementing this unconditionally makes it a cruder measure than g.LinesPerCycle,
// but it seems okay to be less accurate for this purpose:
++g.UninterruptedLineCount;
}
// The below handles the message-loop checking regardless of whether
// aMode is ONLY_ONE_LINE (i.e. recursed) or not (i.e. we're using
// the for-loop to execute the script linearly):
if ((g.LinesPerCycle > -1 && g_script.mLinesExecutedThisCycle >= g.LinesPerCycle)
// Sleep in between batches of lines, like AutoIt, to reduce the chance that
// a maxed CPU will interfere with time-critical apps such as games,
// video capture, or video playback. Note: MsgSleep() will reset
// mLinesExecutedThisCycle for us:
MsgSleep(10); // Don't use INTERVAL_UNSPECIFIED, which wouldn't sleep at all if there's a msg waiting.
// At this point, a pause may have been triggered either by the above MsgSleep()
// or due to the action of a command (e.g. Pause, or perhaps tray menu "pause" was selected during Sleep):
while (g.IsPaused) // Benches slightly faster than while() for some reason. Also, an initial "if (g.IsPaused)" prior to the loop doesn't make it any faster.
MsgSleep(INTERVAL_UNSPECIFIED); // Must check often to periodically run timed subroutines.
// Do these only after the above has had its opportunity to spend a significant amount
// of time doing what it needed to do. i.e. do these immediately before the line will actually
// be run so that the time it takes to run will be reflected in the ListLines log.
g_script.mCurrLine = line; // Simplifies error reporting when we get deep into function calls.
// Maintain a circular queue of the lines most recently executed:
sLog[sLogNext] = line; // The code actually runs faster this way than if this were combined with the above.
// Get a fresh tick in case tick_now is out of date. Strangely, it takes benchmarks 3% faster
// on my system with this line than without it, but that's probably just a quirk of the build
// or the CPU's caching. It was already shown previously that the released version of 1.0.09
// was almost 2% faster than an early version of this version (yet even now, that prior version
// benchmarks slower than this one, which I can't explain).
sLogTick[sLogNext++] = GetTickCount(); // Incrementing here vs. separately benches a little faster.
if (sLogNext >= LINE_LOG_SIZE)
sLogNext = 0;
// Do this only after the opportunity to Sleep (above) has passed, because during
// that sleep, a new subroutine might be launched which would likely overwrite the
// deref buffer used for arg expansion, below:
// Expand any dereferences contained in this line's args.
// Note: Only one line at a time be expanded via the above function. So be sure
// to store any parts of a line that are needed prior to moving on to the next
// line (e.g. control stmts such as IF and LOOP). Also, don't expand
// ACT_ASSIGN because a more efficient way of dereferencing may be possible
// in that case:
if (line->mActionType != ACT_ASSIGN)
{
result = line->ExpandArgs();
// As of v1.0.31, ExpandArgs() will also return EARLY_EXIT if a function call inside one of this
// line's expressions did an EXIT.
if (result != OK)
return result; // In the case of FAIL: Abort the current subroutine, but don't terminate the app.
}
if (ACT_IS_IF(line->mActionType))
{
++g_script.mLinesExecutedThisCycle; // If and its else count as one line for this purpose.
if_condition = line->EvaluateCondition();
if (if_condition == FAIL)
return FAIL;
if (if_condition == CONDITION_TRUE)
{
// line->mNextLine has already been verified non-NULL by the pre-parser, so
// this dereference is safe:
result = line->mNextLine->ExecUntil(ONLY_ONE_LINE, apReturnValue, &jump_to_line);
if (jump_to_line == line)
// Since this IF's ExecUntil() encountered a Goto whose target is the IF
// itself, continue with the for-loop without moving to a different
// line. Also: stay in this recursion layer even if aMode == ONLY_ONE_LINE
// because we don't want the caller handling it because then it's cleanup
// to jump to its end-point (beyond its own and any unowned elses) won't work.
// Example:
// if x <-- If this layer were to do it, its own else would be unexpectedly encountered.
// label1:
// if y <-- We want this statement's layer to handle the goto.
// goto, label1
// else
// ...
// else
// ...
continue;
if (aMode == ONLY_ONE_LINE)
{
// When jump_to_line!=NULL, the above call to ExecUntil() told us to jump somewhere.
// But since we're in ONLY_ONE_LINE mode, our caller must handle it because only it knows how
// to extricate itself from whatever it's doing:
caller_jump_to_line = jump_to_line; // Tell the caller to handle this jump (if applicable). jump_to_line==NULL is ok.
return result;
}
if (result == FAIL || result == EARLY_RETURN || result == EARLY_EXIT
|| result == LOOP_BREAK || result == LOOP_CONTINUE)
// EARLY_RETURN can occur if this if's action was a block, and that block
// contained a RETURN, or if this if's only action is RETURN. It can't
// occur if we just executed a Gosub, because that Gosub would have been
// done from a deeper recursion layer (and executing a Gosub in
// ONLY_ONE_LINE mode can never return EARLY_RETURN).
return result;
// Now this if-statement, including any nested if's and their else's,
// has been fully evaluated by the recusion above. We must jump to
// the end of this if-statement to get to the right place for
// execution to resume. UPDATE: Or jump to the goto target if the
// call to ExecUntil told us to do that instead:
if (jump_to_line != NULL && jump_to_line->mParentLine != line->mParentLine)
{
caller_jump_to_line = jump_to_line; // Tell the caller to handle this jump.
return OK;
}
if (jump_to_line != NULL) // jump to where the caller told us to go, rather than the end of IF.
line = jump_to_line;
else // Do the normal clean-up for an IF statement:
{
// Set line to be either the IF's else or the end of the if-stmt:
if ( !(line = line->mRelatedLine) )
// The preparser has ensured that the only time this can happen is when
// the end of the script has been reached (i.e. this if-statement
// has no else and it's the last statement in the script):
return OK;
if (line->mActionType == ACT_ELSE)
line = line->mRelatedLine;
// Now line is the ELSE's "I'm finished" jump-point, which is where
// we want to be. If line is now NULL, it will be caught when this
// loop iteration is ended by the "continue" stmt below. UPDATE:
// it can't be NULL since all scripts now end in ACT_EXIT.
// else the IF had NO else, so we're already at the IF's "I'm finished" jump-point.
}
}
else // if_condition == CONDITION_FALSE
{
if ( !(line = line->mRelatedLine) ) // Set to IF's related line.
// The preparser has ensured that this can only happen if the end of the script
// has been reached. UPDATE: Probably can't happen anymore since all scripts
// are now provided with a terminating ACT_EXIT:
return OK;
if (line->mActionType != ACT_ELSE && aMode == ONLY_ONE_LINE)
// Since this IF statement has no ELSE, and since it was executed
// in ONLY_ONE_LINE mode, the IF-ELSE statement, which counts as
// one line for the purpose of ONLY_ONE_LINE mode, has finished:
return OK;
if (line->mActionType == ACT_ELSE) // This IF has an else.
{
// Preparser has ensured that every ELSE has a non-NULL next line:
result = line->mNextLine->ExecUntil(ONLY_ONE_LINE, apReturnValue, &jump_to_line);
if (aMode == ONLY_ONE_LINE)
{
// When jump_to_line!=NULL, the above call to ExecUntil() told us to jump somewhere.
// But since we're in ONLY_ONE_LINE mode, our caller must handle it because only it knows how
// to extricate itself from whatever it's doing:
caller_jump_to_line = jump_to_line; // Tell the caller to handle this jump (if applicable). jump_to_line==NULL is ok.
return result;
}
if (result == FAIL || result == EARLY_RETURN || result == EARLY_EXIT
|| result == LOOP_BREAK || result == LOOP_CONTINUE)
return result;
if (jump_to_line != NULL && jump_to_line->mParentLine != line->mParentLine)
{
caller_jump_to_line = jump_to_line; // Tell the caller to handle this jump.
return OK;
}
if (jump_to_line != NULL)
// jump to where the called function told us to go, rather than the end of our ELSE.
line = jump_to_line;
else // Do the normal clean-up for an ELSE statement.
line = line->mRelatedLine;
// Now line is the ELSE's "I'm finished" jump-point, which is where
// we want to be. If line is now NULL, it will be caught when this
// loop iteration is ended by the "continue" stmt below. UPDATE:
// it can't be NULL since all scripts now end in ACT_EXIT.
// else the IF had NO else, so we're already at the IF's "I'm finished" jump-point.
}
// else the IF had NO else, so we're already at the IF's "I'm finished" jump-point.
} // if_condition == CONDITION_FALSE
continue; // Let the for-loop process the new location specified by <line>.
} // if (ACT_IS_IF)
// If above didn't continue, it's not an IF, so handle the other
// flow-control types:
switch (line->mActionType)
{
case ACT_GOSUB:
// A single gosub can cause an infinite loop if misused (i.e. recusive gosubs),
// so be sure to do this to prevent the program from hanging:
++g_script.mLinesExecutedThisCycle;
if ( !(jump_to_label = (Label *)line->mRelatedLine) )
// The label is a dereference, otherwise it would have been resolved at load-time.
// So send true because we don't want to update its mRelatedLine. This is because
// we want to resolve the label every time through the loop in case the variable
// that contains the label changes, e.g. Gosub, %MyLabel%
if ( !(jump_to_label = line->GetJumpTarget(true)) )
return FAIL; // Error was already displayed by called function.
// I'm pretty sure it's not valid for this call to ExecUntil() to tell us to jump
// somewhere, because the called function, or a layer even deeper, should handle
// the goto prior to returning to us? So the last parameter is omitted:
result = jump_to_label->Execute();
// Must do these return conditions in this specific order:
if (result == FAIL || result == EARLY_EXIT)
return result;
if (aMode == ONLY_ONE_LINE)
// This Gosub doesn't want its caller to know that the gosub's
// subroutine returned early:
return (result == EARLY_RETURN) ? OK : result;
// If the above didn't return, the subroutine finished successfully and
// we should now continue on with the line after the Gosub:
line = line->mNextLine;
continue; // Resume looping starting at the above line. "continue" is actually slight faster than "break" in these cases.
case ACT_GOTO:
// A single goto can cause an infinite loop if misused, so be sure to do this to
// prevent the program from hanging:
++g_script.mLinesExecutedThisCycle;
if ( !(jump_to_label = (Label *)line->mRelatedLine) )
// The label is a dereference, otherwise it would have been resolved at load-time.
// So send true because we don't want to update its mRelatedLine. This is because
// we want to resolve the label every time through the loop in case the variable
// that contains the label changes, e.g. Gosub, %MyLabel%
if ( !(jump_to_label = line->GetJumpTarget(true)) )
return FAIL; // Error was already displayed by called function.
// Now that the Goto is certain to occur:
g.CurrentLabel = jump_to_label; // v1.0.46.16: Support A_ThisLabel.
// One or both of these lines can be NULL. But the preparser should have
// ensured that all we need to do is a simple compare to determine
// whether this Goto should be handled by this layer or its caller
// (i.e. if this Goto's target is not in our nesting level, it MUST be the
// caller's responsibility to either handle it or pass it on to its
// caller).
if (aMode == ONLY_ONE_LINE || line->mParentLine != jump_to_label->mJumpToLine->mParentLine)
{
caller_jump_to_line = jump_to_label->mJumpToLine; // Tell the caller to handle this jump.
return OK;
}
// Otherwise, we will handle this Goto since it's in our nesting layer:
line = jump_to_label->mJumpToLine;
continue; // Resume looping starting at the above line. "continue" is actually slight faster than "break" in these cases.
case ACT_GROUPACTIVATE: // Similar to ACT_GOSUB, which is why this section is here rather than in Perform().
{
++g_script.mLinesExecutedThisCycle; // Always increment for GroupActivate.
WinGroup *group;
if ( !(group = (WinGroup *)mAttribute) )
group = g_script.FindGroup(ARG1);
result = OK; // Set default.
if (group)
{
// Note: This will take care of DoWinDelay if needed:
// This check probably isn't necessary since IsJumpValid() is mostly
// for Goto's. But just in case the gosub's target label is some
// crazy place:
return FAIL;
// This section is just like the Gosub code above, so maintain them together.
result = jump_to_label->Execute();
if (result == FAIL || result == EARLY_EXIT)
return result;
}
}
//else no such group, so just proceed.
if (aMode == ONLY_ONE_LINE) // v1.0.45: These two lines were moved here from above to provide proper handling for GroupActivate that lacks a jump/gosub and that lies directly beneath an IF or ELSE.
return (result == EARLY_RETURN) ? OK : result;
line = line->mNextLine;
continue; // Resume looping starting at the above line. "continue" is actually slight faster than "break" in these cases.
}
case ACT_RETURN:
// Although a return is really just a kind of block-end, keep it separate
// because when a return is encountered inside a block, it has a double function:
// to first break out of all enclosing blocks and then return from the gosub.
// NOTE: The return's ARG1 expression has been evaluated by ExpandArgs() above,
// which is desirable *even* if apReturnValue is NULL (i.e. the caller will be
// ignoring the return value) in case the return's expression calls a function
// which has side-effects. For example, "return LogThisEvent()".
if (apReturnValue) // Caller wants the return value.
*apReturnValue = ARG1; // This sets it to blank if this return lacks an arg.
//else the return value, if any, is discarded.
// Don't count returns against the total since they should be nearly instantaneous. UPDATE: even if
// the return called a function (e.g. return fn()), that function's lines would have been added
// to the total, so there doesn't seem much problem with not doing it here.
//++g_script.mLinesExecutedThisCycle;
if (aMode != UNTIL_RETURN)
// Tells the caller to return early if it's not the Gosub that directly
// brought us into this subroutine. i.e. it allows us to escape from
// any number of nested blocks in order to get back out of their
// recursive layers and back to the place this RETURN has meaning
// to someone (at the right recursion layer):
return EARLY_RETURN;
return OK;
case ACT_BREAK:
return LOOP_BREAK;
case ACT_CONTINUE:
return LOOP_CONTINUE;
case ACT_LOOP:
case ACT_REPEAT:
{
HKEY root_key_type; // For registry loops, this holds the type of root key, independent of whether it is local or remote.
AttributeType attr = line->mAttribute;
if (attr == ATTR_LOOP_REG)
root_key_type = RegConvertRootKey(ARG1);
else if (ATTR_LOOP_IS_UNKNOWN_OR_NONE(attr))
{
// Since it couldn't be determined at load-time (probably due to derefs),
// determine whether it's a file-loop, registry-loop or a normal/counter loop.
// But don't change the value of line->mAttribute because that's our
// indicator of whether this needs to be evaluated every time for
// this particular loop (since the nature of the loop can change if the
// contents of the variables dereferenced for this line change during runtime):
switch (line->mArgc)
{
case 0:
attr = ATTR_LOOP_NORMAL;
break;
case 1:
// Unlike at loadtime, allow it to be negative at runtime in case it was a variable
// reference that resolved to a negative number, to indicate that 0 iterations
// should be performed. UPDATE: Also allow floating point numbers at runtime
// but not at load-time (since it doesn't make sense to have a literal floating
// point number as the iteration count, but a variable containing a pure float
// ONLY AFTER THE ABOVE IS IT CERTAIN THE LOOP WILL LAUNCH (i.e. there was no error or early return).
// So only now is it safe to make changes to global things like g.mLoopIteration.
bool continue_main_loop = false; // Init these output parameters prior to starting each type of loop.
jump_to_line = NULL; //
// IN CASE THERE'S AN OUTER LOOP ENCLOSING THIS ONE, BACK UP THE A_LOOPXXX VARIABLES:
loop_iteration = g.mLoopIteration;
loop_file = g.mLoopFile;
loop_reg_item = g.mLoopRegItem;
loop_read_file = g.mLoopReadFile;
loop_field = g.mLoopField;
// INIT "A_INDEX" (one-based not zero-based). This is done here rather than in each PerformLoop()
// function because it reduces code size and also because registry loops and file-pattern loops
// can be intrinsically recursive (this is also related to the loop-recursion bugfix documented
// for v1.0.20: fixes A_Index so that it doesn't wrongly reset to 0 inside recursive file-loops
// and registry loops).
g.mLoopIteration = 1;
// PERFORM THE LOOP:
switch ((size_t)attr)
{
case ATTR_LOOP_NORMAL: // Listed first for performance.
bool is_infinite; // "is_infinite" is more maintainable and future-proof than using LLONG_MAX to simulate an infinite loop. Plus it gives peace-of-mind and the LLONG_MAX method doesn't measurably improve benchmarks (nor does BOOL vs. bool).
__int64 iteration_limit;
if (line->mArgc > 0) // At least one parameter is present.
{
// Note that a 0 means infinite in AutoIt2 for the REPEAT command; so the following handles
// that too.
iteration_limit = ATOI64(ARG1); // If it's negative, zero iterations will be performed automatically.
else // It's either ACT_REPEAT or an ACT_LOOP without parameters.
{
iteration_limit = 0; // Avoids debug-mode's "used without having been defined" (though it's merely passed as a parameter, not ever used in this case).
is_infinite = true; // Override the default set earlier.
}
result = line->PerformLoop(apReturnValue, continue_main_loop, jump_to_line
, iteration_limit, is_infinite);
break;
case ATTR_LOOP_PARSE:
// The phrase "csv" is unique enough since user can always rearrange the letters
// to do a literal parse using C, S, and V as delimiters:
if (stricmp(ARG3, "CSV"))
result = line->PerformLoopParse(apReturnValue, continue_main_loop, jump_to_line);
else
result = line->PerformLoopParseCSV(apReturnValue, continue_main_loop, jump_to_line);
break;
case ATTR_LOOP_READ_FILE:
FILE *read_file;
if (*ARG2 && (read_file = fopen(ARG2, "r"))) // v1.0.47: Added check for "" to avoid debug-assertion failure while in debug mode (maybe it's bad to to open file "" in release mode too).
{
result = line->PerformLoopReadFile(apReturnValue, continue_main_loop, jump_to_line, read_file, ARG3);
fclose(read_file);
}
else
// The open of a the input file failed. So just set result to OK since setting the
// ErrorLevel isn't supported with loops (since that seems like it would be an overuse
// of ErrorLevel, perhaps changing its value too often when the user would want
// it saved -- in any case, changing that now might break existing scripts).
result = OK;
break;
case ATTR_LOOP_FILEPATTERN:
result = line->PerformLoopFilePattern(apReturnValue, continue_main_loop, jump_to_line, file_loop_mode
, recurse_subfolders, ARG1);
break;
case ATTR_LOOP_REG:
// This isn't the most efficient way to do things (e.g. the repeated calls to
// RegConvertRootKey()), but it the simplest way for now. Optimization can
// be done at a later time:
bool is_remote_registry;
HKEY root_key;
if (root_key = RegConvertRootKey(ARG1, &is_remote_registry)) // This will open the key if it's remote.
{
// root_key_type needs to be passed in order to support GetLoopRegKey():
result = line->PerformLoopReg(apReturnValue, continue_main_loop, jump_to_line, file_loop_mode
if (line->mAttribute) // This is the ACT_BLOCK_BEGIN that starts a function's body.
{
// Any time this happens at runtime it means a function has been defined inside the
// auto-execute section, a block, or other place the flow of execution can reach
// on its own. This is not considered a call to the function. Instead, the entire
// body is just skipped over using this high performance method. However, the function's
// opening brace will show up in ListLines, but that seems preferable to the performance
// overhead of explicitly removing it here.
line = line->mRelatedLine; // Resume execution at the line following this functions end-block.
continue; // Resume looping starting at the above line. "continue" is actually slight faster than "break" in these cases.
}
// Don't count block-begin/end against the total since they should be nearly instantaneous:
//++g_script.mLinesExecutedThisCycle;
// In this case, line->mNextLine is already verified non-NULL by the pre-parser:
result = line->mNextLine->ExecUntil(UNTIL_BLOCK_END, apReturnValue, &jump_to_line);
if (jump_to_line == line)
// Since this Block-begin's ExecUntil() encountered a Goto whose target is the
// block-begin itself, continue with the for-loop without moving to a different
// line. Also: stay in this recursion layer even if aMode == ONLY_ONE_LINE
// because we don't want the caller handling it because then it's cleanup
// to jump to its end-point (beyond its own and any unowned elses) won't work.
// Example:
// if x <-- If this layer were to do it, its own else would be unexpectedly encountered.
// label1:
// { <-- We want this statement's layer to handle the goto.
// if y
// goto, label1
// else
// ...
// }
// else
// ...
continue;
if (aMode == ONLY_ONE_LINE)
{
// When jump_to_line!=NULL, the above call to ExecUntil() told us to jump somewhere.
// But since we're in ONLY_ONE_LINE mode, our caller must handle it because only it knows how
// to extricate itself from whatever it's doing:
caller_jump_to_line = jump_to_line; // Tell the caller to handle this jump (if applicable). jump_to_line==NULL is ok.
return result;
}
if (result == FAIL || result == EARLY_RETURN || result == EARLY_EXIT
|| result == LOOP_BREAK || result == LOOP_CONTINUE)
return result;
// Currently, all blocks are normally executed in ONLY_ONE_LINE mode because
// they are the direct actions of an IF, an ELSE, or a LOOP. So the
// above will already have returned except when the user has created a
// generic, standalone block with no assciated control statement.
// Check to see if we need to jump somewhere:
if (jump_to_line != NULL && line->mParentLine != jump_to_line->mParentLine)
{
caller_jump_to_line = jump_to_line; // Tell the caller to handle this jump (if applicable).
return OK;
}
if (jump_to_line != NULL) // jump to where the caller told us to go, rather than the end of our block.
line = jump_to_line;
else // Just go to the end of our block and continue from there.
line = line->mRelatedLine;
// Now line is the line after the end of this block. Can be NULL (end of script).
// UPDATE: It can't be NULL (not that it matters in this case) since the loader
// has ensured that all scripts now end in an ACT_EXIT.
continue; // Resume looping starting at the above line. "continue" is actually slight faster than "break" in these cases.
case ACT_BLOCK_END:
// Don't count block-begin/end against the total since they should be nearly instantaneous:
//++g_script.mLinesExecutedThisCycle;
if (aMode != UNTIL_BLOCK_END)
// Rajat found a way for this to happen that basically amounts to this:
// If within a loop you gosub a label that is also inside of the block, and
// that label sometimes doesn't return (i.e. due to a missing "return" somewhere
// in its flow of control), the loop(s)'s block-end symbols will be encountered
// by the subroutine, and these symbols don't have meaning to it. In other words,
// the subroutine has put us into a waiting-for-return state rather than a
// waiting-for-block-end state, so when block-end's are encountered, that is
// considered a runtime error:
return line->LineError("A \"return\" must be encountered prior to this \"}\"." ERR_ABORT); // Former error msg was "Unexpected end-of-block (Gosub without Return?)."
return OK; // It's the caller's responsibility to resume execution at the next line, if appropriate.
// ACT_ELSE can happen when one of the cases in this switch failed to properly handle
// aMode == ONLY_ONE_LINE. But even if ever happens, it will just drop into the default
// case, which will result in a FAIL (silent exit of thread) as an indicator of the problem.
// So it's commented out:
//case ACT_ELSE:
// // Shouldn't happen if the pre-parser and this function are designed properly?
// Thus, Perform() should be designed to only return FAIL if it's an error that would make
// it unsafe to proceed in the subroutine we're executing now:
return result; // Can be either OK or FAIL.
line = line->mNextLine;
} // switch()
} // for each line
// Above loop ended because the end of the script was reached.
// At this point, it should be impossible for aMode to be
// UNTIL_BLOCK_END because that would mean that the blocks
// aren't all balanced (or there is a design flaw in this
// function), but they are balanced because the preparser
// verified that. It should also be impossible for the
// aMode to be ONLY_ONE_LINE because the function is only
// called in that mode to execute the first action-line
// beneath an IF or an ELSE, and the preparser has already
// verified that every such IF and ELSE has a non-NULL
// line after it. Finally, aMode can be UNTIL_RETURN, but
// that is normal mode of operation at the top level,
// so probably shouldn't be considered an error. For example,
// if the script has no hotkeys, it is executed from its
// first line all the way to the end. For it not to have
// a RETURN or EXIT is not an error. UPDATE: The loader
// now ensures that all scripts end in ACT_EXIT, so
// this line should never be reached:
return OK;
}
ResultType Line::EvaluateCondition() // __forceinline on this reduces benchmarks, probably because it reduces caching effectiveness by having code in the case that doesn't execute much in the benchmarks.
// Returns FAIL, CONDITION_TRUE, or CONDITION_FALSE.
{
#ifdef _DEBUG
if (!ACT_IS_IF(mActionType))
return LineError("DEBUG: EvaluateCondition() was called with a line that isn't a condition."
// Use ATOF to support hex, float, and integer formats. Also, explicitly compare to 0.0
// to avoid truncation of double, which would result in a value such as 0.1 being seen
// as false rather than true. Fixed in v1.0.25.12 so that only the following are false:
// 0
// 0.0
// 0x0
// (variants of the above)
// blank string
// ... in other words, "if var" should be true if it contains a non-numeric string.
cp = ARG1; // It should help performance to resolve the ARG1 macro only once.
if (!*cp)
if_condition = false;
else if (!IsPureNumeric(cp, true, false, true)) // i.e. a var containing all whitespace would be considered "true", since it's a non-blank string that isn't equal to 0.0.
if_condition = true;
else // It's purely numeric, not blank, and not all whitespace.
if_condition = (ATOF(cp) != 0.0);
break;
// For ACT_IFWINEXIST and ACT_IFWINNOTEXIST, although we validate that at least one
// of their window params is non-blank during load, it's okay at runtime for them
// all to resolve to be blank (due to derefs), without an error being reported.
// It's probably more flexible that way, and in any event WinExist() is equipped to
// Note: Even if aFilePattern is just a directory (i.e. with not wildcard pattern), it seems best
// not to append "\\*.*" to it because the pattern might be a script variable that the user wants
// to conditionally resolve to various things at runtime. In other words, it's valid to have
// only a single directory be the target of the loop.
{
// Make a local copy of the path given in aFilePattern because as the lines of
// the loop are executed, the deref buffer (which is what aFilePattern might
// point to if we were called from ExecUntil()) may be overwritten --
// and we will need the path string for every loop iteration. We also need
// to determine naked_filename_or_pattern:
char file_path[MAX_PATH], naked_filename_or_pattern[MAX_PATH]; // Giving +3 extra for "*.*" seems fairly pointless because any files that actually need that extra room would fail to be retrieved by FindFirst/Next due to their inability to support paths much over 256.
for ( file_found = (file_search != INVALID_HANDLE_VALUE) // Convert FindFirst's return value into a boolean so that it's compatible with with FindNext's.
// Above is a self-contained loop that keeps fetching files until there's no more files, or a file
// is found that isn't filtered out. It also sets file_found and new_current_file for use by the
// outer loop.
} // for()
// The script's loop is now over.
if (file_search != INVALID_HANDLE_VALUE)
FindClose(file_search);
// If aRecurseSubfolders is true, we now need to perform the loop's body for every subfolder to
// search for more files and folders inside that match aFilePattern. We can't do this in the
// first loop, above, because it may have a restricted file-pattern such as *.txt and we want to
// find and recurse into ALL folders:
if (!aRecurseSubfolders) // No need to continue into the "recurse" section.
return OK;
// Since above didn't return, this is a file-loop and recursion into sub-folders has been requested.
// Append *.* to file_path so that we can retrieve all files and folders in the aFilePattern
// main folder. We're only interested in the folders, but we have to use *.* to ensure
// that the search will find all folder names:
if (file_path_length > sizeof(file_path) - 4) // v1.0.45.03: No room to append "*.*", so for simplicity, skip this folder (don't recurse into it).
return OK; // This situation might be impossible except for 32000-capable paths because the OS seems to reserve room inside every directory for at least the maximum length of a short filename.
char *append_pos = file_path + file_path_length;
strcpy(append_pos, "*.*"); // Above has already verified that no overflow is possible.
// v1.0.45.03: Skip over folders whose full-path-names are too long to be supported by the ANSI
// versions of FindFirst/FindNext. Without this fix, the section below formerly called PerformLoop()
// with a truncated full-path-name, which caused the last_backslash-finding logic to find the wrong
// backslash, which in turn caused infinite recursion and a stack overflow (i.e. caused by the
// full-path-name getting truncated in the same spot every time, endlessly).
|| path_and_pattern_length + strlen(new_current_file.cFileName) > sizeof(file_path) - 2) // -2 to reflect: 1) the backslash to be added between cFileName and naked_filename_or_pattern; 2) the zero terminator.
continue;
// Build the new search pattern, which consists of the original file_path + the subfolder name
// we just discovered + the original pattern:
sprintf(append_pos, "%s\\%s", new_current_file.cFileName, naked_filename_or_pattern); // Indirectly set file_path to the new search pattern. This won't overflow due to the check above.
// Pass NULL for the 2nd param because it will determine its own current-file when it does
// its first loop iteration. This is because this directory is being recursed into, not
// processed itself as a file-loop item (since this was already done in the first loop,
// above, if its name matches the original search pattern):
result = PerformLoopFilePattern(apReturnValue, aContinueMainLoop, aJumpToLine, aFileLoopMode, aRecurseSubfolders, file_path);
// result should never be LOOP_CONTINUE because the above call to PerformLoop() should have
// handled that case. However, it can be LOOP_BREAK if it encoutered the break command.
if (result == LOOP_BREAK || result == EARLY_RETURN || result == EARLY_EXIT || result == FAIL)
{
FindClose(file_search);
return result; // Return even LOOP_BREAK, since our caller can be either ExecUntil() or ourself.
}
if (aContinueMainLoop // The call to PerformLoop() above signaled us to break & return.
|| aJumpToLine)
// Above: There's no need to check "aJumpToLine == this" because PerformLoop() would already have
// handled it. But if it set aJumpToLine to be non-NULL, it means we have to return and let our caller
// handle the jump.
break;
} while (FindNextFile(file_search, &new_current_file));
FindClose(file_search);
return OK;
}
ResultType Line::PerformLoopReg(char **apReturnValue, bool &aContinueMainLoop, Line *&aJumpToLine, FileLoopModeType aFileLoopMode
// See comments in PerformLoop() for details about this section.
// Note that ®_item is passed to ExecUntil() rather than... (comment was never finished).
#define MAKE_SCRIPT_LOOP_PROCESS_THIS_ITEM \
{\
g.mLoopRegItem = ®_item;\
result = mNextLine->ExecUntil(ONLY_ONE_LINE, apReturnValue, &jump_to_line);\
++g.mLoopIteration;\
if (result == LOOP_BREAK || result == EARLY_RETURN || result == EARLY_EXIT || result == FAIL)\
{\
RegCloseKey(hRegKey);\
return result;\
}\
if (jump_to_line)\
{\
if (jump_to_line == this)\
aContinueMainLoop = true;\
else\
aJumpToLine = jump_to_line;\
break;\
}\
}
DWORD name_size;
// First enumerate the values, which are analogous to files in the file system.
// Later, the subkeys ("subfolders") will be done:
if (count_values > 0 && aFileLoopMode != FILE_LOOP_FOLDERS_ONLY) // The caller doesn't want "files" (values) excluded.
{
reg_item.InitForValues();
// Going in reverse order allows values to be deleted without disrupting the enumeration,
// at least in some cases:
for (i = count_values - 1;; --i)
{
// Don't use CONTINUE in loops such as this due to the loop-ending condition being explicitly
// checked at the bottom.
name_size = sizeof(reg_item.name); // Must reset this every time through the loop.
*reg_item.name = '\0';
if (RegEnumValue(hRegKey, i, reg_item.name, &name_size, NULL, ®_item.type, NULL, NULL) == ERROR_SUCCESS)
MAKE_SCRIPT_LOOP_PROCESS_THIS_ITEM
// else continue the loop in case some of the lower indexes can still be retrieved successfully.
if (i == 0) // Check this here due to it being an unsigned value that we don't want to go negative.
break;
}
}
// If the loop is neither processing subfolders nor recursing into them, don't waste the performance
// doing the next loop:
if (!count_subkeys || (aFileLoopMode == FILE_LOOP_FILES_ONLY && !aRecurseSubfolders))
{
RegCloseKey(hRegKey);
return OK;
}
// Enumerate the subkeys, which are analogous to subfolders in the files system:
// Going in reverse order allows keys to be deleted without disrupting the enumeration,
// at least in some cases:
reg_item.InitForSubkeys();
char subkey_full_path[MAX_REG_ITEM_LENGTH + 1]; // But doesn't include the root key name, which is not only by design but testing shows that if it did, the length could go over 260.
for (i = count_subkeys - 1;; --i) // Will have zero iterations if there are no subkeys.
{
// Don't use CONTINUE in loops such as this due to the loop-ending condition being explicitly
// checked at the bottom.
name_size = sizeof(reg_item.name); // Must be reset for every iteration.
if (RegEnumKeyEx(hRegKey, i, reg_item.name, &name_size, NULL, NULL, NULL, ®_item.ftLastWriteTime) == ERROR_SUCCESS)
{
if (aFileLoopMode != FILE_LOOP_FILES_ONLY) // have the script's loop process this subkey.
MAKE_SCRIPT_LOOP_PROCESS_THIS_ITEM
if (aRecurseSubfolders) // Now recurse into the subkey, regardless of whether it was processed above.
{
// Build the new subkey name using the an area of memory on the stack that we won't need
// after the recusive call returns to us. Omit the leading backslash if subkey is blank,
// which supports recursively searching the contents of keys contained within a root key
for (; fgets(loop_info.mCurrentLine, sizeof(loop_info.mCurrentLine), loop_info.mReadFile);)
{
line_length = strlen(loop_info.mCurrentLine);
if (line_length && loop_info.mCurrentLine[line_length - 1] == '\n') // Remove newlines like FileReadLine does.
loop_info.mCurrentLine[--line_length] = '\0';
// See comments in PerformLoop() for details about this section.
g.mLoopReadFile = &loop_info;
result = mNextLine->ExecUntil(ONLY_ONE_LINE, apReturnValue, &jump_to_line);
++g.mLoopIteration;
if (result == LOOP_BREAK || result == EARLY_RETURN || result == EARLY_EXIT || result == FAIL)
{
if (loop_info.mWriteFile)
fclose(loop_info.mWriteFile);
return result;
}
if (jump_to_line) // See comments in PerformLoop() about this section.
{
if (jump_to_line == this)
aContinueMainLoop = true;
else
aJumpToLine = jump_to_line; // Signal our caller to handle this jump.
break;
}
}
if (loop_info.mWriteFile)
fclose(loop_info.mWriteFile);
// Don't return result because we want to always return OK unless it was one of the values
// already explicitly checked and returned above. In other words, there might be values other
// than OK that aren't explicitly checked for, above.
return OK;
}
__forceinline ResultType Line::Perform() // __forceinline() currently boosts performance a bit, though it's probably more due to the butterly effect and cache hits/misses.
// Performs only this line's action.
// Returns OK or FAIL.
// The function should not be called to perform any flow-control actions such as
// Goto, Gosub, Return, Block-Begin, Block-End, If, Else, etc.
{
char buf_temp[MAX_REG_ITEM_LENGTH + 1], *contents; // For registry and other things.
WinGroup *group; // For the group commands.
Var *output_var = OUTPUT_VAR; // Okay if NULL. Users of it should only consider it valid if their first arg is actually an output_variable.
ToggleValueType toggle; // For commands that use on/off/neutral.
// Use signed values for these in case they're really given an explicit negative value:
int start_char_num, chars_to_extract; // For String commands.
size_t source_length; // For String commands.
SymbolType var_is_pure_numeric, value_is_pure_numeric; // For math operations.
vk_type vk; // For GetKeyState.
Label *target_label; // For ACT_SETTIMER and ACT_HOTKEY
int instance_number; // For sound commands.
DWORD component_type; // For sound commands.
__int64 device_id; // For sound commands. __int64 helps avoid compiler warning for some conversions.
bool is_remote_registry; // For Registry commands.
HKEY root_key; // For Registry commands.
ResultType result; // General purpose.
// Even though the loading-parser already checked, check again, for now,
// at least until testing raises confidence. UPDATE: Don't this because
// sometimes (e.g. ACT_ASSIGN/ADD/SUB/MULT/DIV) the number of parameters
// required at load-time is different from that at runtime, because params
// are taken out or added to the param list:
//if (nArgs < g_act[mActionType].MinParams) ...
switch (mActionType)
{
case ACT_ASSIGN:
// Note: This line's args have not yet been dereferenced in this case (i.e. ExpandArgs() hasn't been
// called). The below function will handle that if it is needed.
return PerformAssign(); // It will report any errors for us.
case ACT_ASSIGNEXPR:
// Currently, ACT_ASSIGNEXPR can occur even when mArg[1].is_expression==false, such as things like var:=5
// and var:=Array%i%. Search on "is_expression = " to find such cases in the script-loading/parsing
// routines.
if (mArgc > 1)
{
if (mArg[1].is_expression) // v1.0.45: ExpandExpression() already took care of it for us (for performance reasons).
return OK;
// sArgVar is used to enhance performance, which would otherwise be poor for dynamic variables
// such as Var:=Array%i% (which is an expression and handled by ACT_ASSIGNEXPR rather than
// ACT_ASSIGN) because Array%i% would have to be resolved twice (once here and once
// previously by ExpandArgs()) just to find out if it's IsBinaryClip()).
if (ARGVARRAW2) // RAW is safe due to the above check of mArgc > 1.
{
if (ARGVARRAW2->IsBinaryClip()) // This can be reached via things like: x := binary_clip
// Performance should be good in this case since IsBinaryClip() implies a single isolated deref,
// which would never have been copied into the deref buffer.
return output_var->AssignBinaryClip(*ARGVARRAW2); // ARG2 must be VAR_NORMAL due to IsBinaryClip() check above (it can't even be VAR_CLIPBOARDALL).
// v1.0.46.01: The following can be reached because loadtime no longer translates such statements
// into ACT_ASSIGN vs. ACT_ASSIGNEXPR. Even without that change, it can also be reached by
// something like:
// DynClipboardAll = ClipboardAll
// ClipSaved := %DynClipboardAll%
if (ARGVARRAW2->Type() == VAR_CLIPBOARDALL)
return output_var->AssignClipboardAll();
}
}
// Note that simple assignments such as Var:="xyz" or Var:=Var2 are resolved to be
// non-expressions at load-time. In these cases, ARG2 would have been expanded
// normally rather than evaluated as an expression.
return output_var->Assign(ARG2); // ARG2 now contains the evaluated result of the expression.
case ACT_EXPRESSION:
// Nothing needs to be done because the expression in ARG1 (which is the only arg) has already
// been evaluated and its functions and subfunctions called. Examples:
// fn(123, "string", var, fn2(y))
// x&=3
// var ? func() : x:=y
return OK;
// Like AutoIt2, if either output_var or ARG1 aren't purely numeric, they
// will be considered to be zero for all of the below math functions:
// v1.0.43.10: Allow chars-to-extract to be blank, which means "get all characters".
// However, for backward compatibility, examine the raw arg, not ARG4. That way, any existing
// scripts that use a variable reference or expression that resolves to an empty string will
// have the parameter treated as zero (as in previous versions) rather than "all characters".
if (mArgc < 4 || !*mArg[3].text)
chars_to_extract = INT_MAX;
else
{
chars_to_extract = ATOI(ARG4); // Use 32-bit signed to detect negatives and fit it VarSizeType.
if (chars_to_extract < 1)
return output_var->Assign(); // Set it to be blank in this case.
}
start_char_num = ATOI(ARG3);
if (toupper(*ARG5) == 'L') // Chars to the left of start_char_num will be extracted.
{
// TRANSLATE "L" MODE INTO THE EQUIVALENT NORMAL MODE:
if (start_char_num < 1) // Starting at a character number that is invalid for L mode.
return output_var->Assign(); // Blank seems most appropriate for the L option in this case.
start_char_num -= (chars_to_extract - 1);
if (start_char_num < 1)
// Reduce chars_to_extract to reflect the fact that there aren't enough chars
// to the left of start_char_num, so we'll extract only them:
chars_to_extract -= (1 - start_char_num);
}
// ABOVE HAS CONVERTED "L" MODE INTO NORMAL MODE, so "L" no longer needs to be considered below.
// UPDATE: The below is also needed for the L option to work correctly. Older:
// It's somewhat debatable, but it seems best not to report an error in this and
// other cases. The result here is probably enough to speak for itself, for script
// debugging purposes:
if (start_char_num < 1)
start_char_num = 1; // 1 is the position of the first char, unlike StringGetPos.
source_length = ArgLength(2); // This call seems unavoidable in both "L" mode and normal mode.
if (source_length < (UINT)start_char_num) // Source is empty or start_char_num lies to the right of the entire string.
return output_var->Assign(); // No chars exist there, so set it to be blank.
source_length -= (start_char_num - 1); // Fix for v1.0.44.14: Adjust source_length to be the length starting at start_char_num. Otherwise, the length passed to Assign() could be too long, and it now expects an accurate length.
// Otherwise, the credentials are being set or updated:
if (!g_script.mRunAsUser) // allocate memory (only needed the first time this is done).
{
// It's more memory efficient to allocate a single block and divide it up.
// This memory is freed automatically by the OS upon program termination.
if ( !(g_script.mRunAsUser = (WCHAR *)malloc(3 * RUNAS_SIZE_IN_BYTES)) )
return LineError(ERR_OUTOFMEM ERR_ABORT);
g_script.mRunAsPass = g_script.mRunAsUser + RUNAS_SIZE_IN_WCHARS; // Fixed for v1.0.47.01 to use RUNAS_SIZE_IN_WCHARS vs. RUNAS_SIZE_IN_BYTES (since pointer math adds 2 bytes not 1 due to the type of pointer).
// For code size reduction, no runtime validation is done (only load-time). Thus, anything other
// than "Off" (especially NEUTRAL) is considered to be "On":
toggle = ConvertOnOff(ARG1, NEUTRAL);
if (g.ThreadIsCritical = (toggle != TOGGLED_OFF)) // Assign.
{
// v1.0.46: When the current thread is critical, have the script check messages less often to
// reduce situations where an OnMesage or GUI message must be discarded due to "thread already
// running". Using 16 rather than the default of 5 solves reliability problems in a custom-menu-draw
// script and probably many similar scripts -- even when the system is under load (though 16 might not
// be enough during an extreme load depending on the exact preemption/timeslice dynamics involved).
// DON'T GO TOO HIGH because this setting reduces response time for ALL messages, even those that
// don't launch script threads (especially painting/drawing and other screen-update events).
// Future enhancement: Could allow the value of 16 to be customized via something like "Critical 25".
// However, it seems best not to allow it to go too high (say, no more than 2000) because that would
// cause the script to completely hang if the critical thread never finishes, or takes a long time
// to finish. A configurable limit might also allow things to work better on Win9x because it has
// a bigger tickcount granularity.
if (!*ARG1 || toggle == TOGGLED_ON) // i.e. an omitted first arg is the same as "ON".
g.PeekFrequency = 16; // Some hardware has a tickcount granularity of 15 instead of 10, so this covers more variations.
else // ARG1 is present but it's not "On" or "Off"; so treat it as a number.
g.PeekFrequency = ATOU(ARG1); // For flexibility (and due to rarity), don't bother checking if too large/small (even if it is it's probably inconsequential).
g.AllowThreadToBeInterrupted = false;
g.LinesPerCycle = -1; // v1.0.47: It seems best to ensure SetBatchLines -1 is in effect because
g.IntervalBeforeRest = -1; // otherwise it may check messages during the interval that it isn't supposed to.
}
else
{
g.PeekFrequency = DEFAULT_PEEK_FREQUENCY;
g.AllowThreadToBeInterrupted = true;
}
// If it's being turned off, allow thread to be immediately interrupted regardless of any
// "Thread Interrupt" settings.
// Now that the thread's interruptibility has been explicitly set, the script is in charge
// of managing this thread's interruptibility, thus kill the timer unconditionally:
KILL_UNINTERRUPTIBLE_TIMER // Done here for maintainability and performance, even though UninterruptibleTimeout() will also kill it.
// Although the above kills the timer, it does not remove any WM_TIMER message that it might already
// have placed into the queue. And since we have other types of timers, purging the queue of all
// WM_TIMERS would be too great a loss of maintainability and reliability. To solve this,
// UninterruptibleTimeout() checks the value of g.ThreadIsCritical.
return OK;
case ACT_THREAD:
switch (ConvertThreadCommand(ARG1))
{
case THREAD_CMD_PRIORITY:
g.Priority = ATOI(ARG2);
break;
case THREAD_CMD_INTERRUPT:
// If either one is blank, leave that setting as it was before.
if (*ARG1)
g_script.mUninterruptibleTime = ATOI(ARG2); // 32-bit (for compatibility with DWORDs returned by GetTickCount).
if (*ARG2)
g_script.mUninterruptedLineCountMax = ATOI(ARG3); // 32-bit also, to help performance (since huge values seem unnecessary).
break;
case THREAD_CMD_NOTIMERS:
g.AllowTimers = (*ARG2 && ATOI64(ARG2) == 0);
break;
// If invalid command, do nothing since that is always caught at load-time unless the command
// is in a variable reference (very rare in this case).
}
return OK;
case ACT_GROUPADD: // Adding a WindowSpec *to* a group, not adding a group.
{
if ( !(group = (WinGroup *)mAttribute) )
if ( !(group = g_script.FindGroup(ARG1, true)) ) // Last parameter -> create-if-not-found.
return FAIL; // It already displayed the error for us.
target_label = NULL;
if (*ARG4)
{
if ( !(target_label = (Label *)mRelatedLine) ) // Jump target hasn't been resolved yet, probably due to it being a deref.
KeyHistoryToFile(); // Signal it to close the file, if it's open.
break;
case TOGGLED_ON:
g_KeyHistoryToFile = true;
break;
case TOGGLED_OFF:
g_KeyHistoryToFile = false;
KeyHistoryToFile(); // Signal it to close the file, if it's open.
break;
// We know it's a variable because otherwise the loading validation would have caught it earlier:
case TOGGLE_INVALID:
return LineError(ERR_PARAM1_INVALID, FAIL, ARG1);
}
if (*ARG2) // The user also specified a filename, so update the target filename.
KeyHistoryToFile(ARG2);
return OK;
}
#endif
// Otherwise:
return ShowMainWindow(MAIN_MODE_KEYHISTORY, false); // Pass "unrestricted" when the command is explicitly used in the script.
case ACT_LISTLINES:
return ShowMainWindow(MAIN_MODE_LINES, false); // Pass "unrestricted" when the command is explicitly used in the script.
case ACT_LISTVARS:
return ShowMainWindow(MAIN_MODE_VARS, false); // Pass "unrestricted" when the command is explicitly used in the script.
case ACT_LISTHOTKEYS:
return ShowMainWindow(MAIN_MODE_HOTKEYS, false); // Pass "unrestricted" when the command is explicitly used in the script.
case ACT_MSGBOX:
{
int result;
HWND dialog_owner = THREAD_DIALOG_OWNER; // Resolve macro only once to reduce code size.
// If the MsgBox window can't be displayed for any reason, always return FAIL to
// the caller because it would be unsafe to proceed with the execution of the
// current script subroutine. For example, if the script contains an IfMsgBox after,
// this line, it's result would be unpredictable and might cause the subroutine to perform
// the opposite action from what was intended (e.g. Delete vs. don't delete a file).
if (!mArgc) // When called explicitly with zero params, it displays this default msg.
result = MsgBox("Press OK to continue.", MSGBOX_NORMAL, NULL, 0, dialog_owner);
else if (mArgc == 1) // In the special 1-parameter mode, the first param is the prompt.
result = MsgBox(ARG1, MSGBOX_NORMAL, NULL, 0, dialog_owner);
else
result = MsgBox(ARG3, ATOI(ARG1), ARG2, ATOF(ARG4), dialog_owner); // dialog_owner passed via parameter to avoid internally-displayed MsgBoxes from being affected by script-thread's owner setting.
// Above allows backward compatibility with AutoIt2's param ordering while still
// permitting the new method of allowing only a single param.
// v1.0.40.01: Rather than displaying another MsgBox in response to a failed attempt to display
// a MsgBox, it seems better (less likely to cause trouble) just to abort the thread. This also
// solves a double-msgbox issue when the maximum number of MsgBoxes is reached. In addition, the
// max-msgbox limit is the most common reason for failure, in which case a warning dialog has
// already been displayed, so there is no need to display another:
//if (!result)
// // It will fail if the text is too large (say, over 150K or so on XP), but that
// // has since been fixed by limiting how much it tries to display.
// // If there were too many message boxes displayed, it will already have notified
// // the user of this via a final MessageBox dialog, so our call here will
// // not have any effect. The below only takes effect if MsgBox()'s call to
// // MessageBox() failed in some unexpected way:
// LineError("The MsgBox could not be displayed." ERR_ABORT);
return result ? OK : FAIL;
}
case ACT_INPUTBOX:
return InputBox(output_var, ARG2, ARG3, toupper(*ARG4) == 'H' // 4th is whether to hide input.
, *ARG5 ? ATOI(ARG5) : INPUTBOX_DEFAULT // Width
, *ARG6 ? ATOI(ARG6) : INPUTBOX_DEFAULT // Height
, *ARG7 ? ATOI(ARG7) : INPUTBOX_DEFAULT // Xpos
, *ARG8 ? ATOI(ARG8) : INPUTBOX_DEFAULT // Ypos
// ARG9: future use for Font name & size, e.g. "Courier:8"
, ATOF(ARG10) // Timeout
, ARG11 // Initial default string for the edit field.
if (width + precision + 2 > MAX_FORMATTED_NUMBER_LENGTH) // +2 to allow room for decimal point itself and leading minus sign.
return OK; // Don't change it.
// Create as "%ARG2f". Note that %f can handle doubles in MSVC++:
sprintf(g.FormatFloat, "%%%s%s%s", ARG2
, dot_pos ? "" : "." // Add a dot if none was specified so that "0" is the same as "0.", which seems like the most user-friendly approach; it's also easier to document in the help file.
, IsPureNumeric(ARG2, true, true, true) ? "f" : ""); // If it's not pure numeric, assume the user already included the desired letter (e.g. SetFormat, Float, 0.6e).
}
else if (!stricmp(ARG1, "Integer"))
{
switch(*ARG2)
{
case 'd':
case 'D':
g.FormatIntAsHex = false;
break;
case 'h':
case 'H':
g.FormatIntAsHex = true;
break;
// Otherwise, since the first letter isn't recongized, do nothing since 99% of the time such a
// probably would be caught at load-time.
}
}
// Otherwise, ignore invalid type at runtime since 99% of the time it would be caught at load-time:
if ( (toggle = ConvertOnOff(ARG1, NEUTRAL)) != NEUTRAL )
g.AutoTrim = (toggle == TOGGLED_ON);
return OK;
case ACT_STRINGCASESENSE:
if ((g.StringCaseSense = ConvertStringCaseSense(ARG1)) == SCS_INVALID)
g.StringCaseSense = SCS_INSENSITIVE; // For simplicity, just fall back to default if value is invalid (normally its caught at load-time; only rarely here).
return OK;
case ACT_DETECTHIDDENWINDOWS:
if ( (toggle = ConvertOnOff(ARG1, NEUTRAL)) != NEUTRAL )
g.DetectHiddenWindows = (toggle == TOGGLED_ON);
return OK;
case ACT_DETECTHIDDENTEXT:
if ( (toggle = ConvertOnOff(ARG1, NEUTRAL)) != NEUTRAL )
g.DetectHiddenText = (toggle == TOGGLED_ON);
return OK;
case ACT_BLOCKINPUT:
switch (toggle = ConvertBlockInput(ARG1))
{
case TOGGLED_ON:
ScriptBlockInput(true);
break;
case TOGGLED_OFF:
ScriptBlockInput(false);
break;
case TOGGLE_SEND:
case TOGGLE_MOUSE:
case TOGGLE_SENDANDMOUSE:
case TOGGLE_DEFAULT:
g_BlockInputMode = toggle;
break;
case TOGGLE_MOUSEMOVE:
g_BlockMouseMove = true;
Hotkey::InstallMouseHook();
break;
case TOGGLE_MOUSEMOVEOFF:
g_BlockMouseMove = false; // But the mouse hook is left installed because it might be needed by other things. This approach is similar to that used by the Input command.
break;
// default (NEUTRAL or TOGGLE_INVALID): do nothing.
// Even if the reload failed, it seems best to return OK anyway. That way,
// the script can take some follow-on action, e.g. it can sleep for 1000
// after issuing the reload command and then take action under the assumption
// that the reload didn't work (since obviously if the process and thread
// in which the Sleep is running still exist, it didn't work):
return OK;
case ACT_SLEEP:
{
// Only support 32-bit values for this command, since it seems unlikely anyone would to have
// it sleep more than 24.8 days or so. It also helps performance on 32-bit hardware because
// MsgSleep() is so heavily called and checks the value of the first parameter frequently:
int sleep_time = ATOI(ARG1); // Keep it signed vs. unsigned for backward compatibility (e.g. scripts that do Sleep -1).
// Do a true sleep on Win9x because the MsgSleep() method is very inaccurate on Win9x
// for some reason (a MsgSleep(1) causes a sleep between 10 and 55ms, for example).
// But only do so for short sleeps, for which the user has a greater expectation of
// accuracy. UPDATE: Do not change the 25 below without also changing it in Critical's
// documentation.
if (sleep_time < 25 && sleep_time > 0 && g_os.IsWin9x()) // Ordered for short-circuit performance. v1.0.38.05: Added "sleep_time > 0" so that Sleep -1/0 will work the same on Win9x as it does on other OSes.
Sleep(sleep_time);
else
MsgSleep(sleep_time);
return OK;
}
case ACT_INIREAD:
return IniRead(ARG2, ARG3, ARG4, ARG5);
case ACT_INIWRITE:
return IniWrite(FOUR_ARGS);
case ACT_INIDELETE:
// To preserve maximum compatibility with existing scripts, only send NULL if ARG3
// was explicitly omitted. This is because some older scripts might rely on the
// fact that a blank ARG3 does not delete the entire section, but rather does
char *Line::ToText(char *aBuf, int aBufSize, bool aCRLF, DWORD aElapsed, bool aLineWasResumed) // aBufSize should be an int to preserve negatives from caller (caller relies on this).
// aBufSize is an int so that any negative values passed in from caller are not lost.
// Caller has ensured that aBuf isn't NULL.
// Translates this line into its text equivalent, putting the result into aBuf and
// returning the position in aBuf of its new string terminator.
{
if (aBufSize < 3)
return aBuf;
else
aBufSize -= (1 + aCRLF); // Reserve one char for LF/CRLF after each line (so that it always get added).
// MY: Full filename is required, even if it's the main file, because some editors (EditPlus)
// seem to rely on that to determine which file and line number to jump to when the user double-clicks
// the error message in the output window.
// v1.0.47: Added a space before the colon as originally intended. Toralf said, "With this minor
// change the error lexer of Scite recognizes this line as a Microsoft error message and it can be
// used to jump to that line."
#define STD_ERROR_FORMAT "%s (%d) : ==> %s\n"
printf(STD_ERROR_FORMAT, sSourceFile[mFileIndex], mLineNumber, aErrorText); // printf() does not signifantly increase the size of the EXE, probably because it shares most of the same code with sprintf(), etc.
if (*aExtraInfo)
printf(" Specifically: %s\n", aExtraInfo);
}
else
{
char source_file[MAX_PATH * 2];
if (mFileIndex)
snprintf(source_file, sizeof(source_file), " in #include file \"%s\"", sSourceFile[mFileIndex]);
else
*source_file = '\0'; // Don't bother cluttering the display if it's the main script file.
char buf[MSGBOX_TEXT_SIZE];
char *buf_marker = buf + snprintf(buf, sizeof(buf), "%s%s: %-1.500s\n\n" // Keep it to a sane size in case it's huge.
cp += snprintf(cp, buf_space_remaining, "Line Text: %-1.100s%s\nError: " // i.e. the word "Error" is omitted as being too noisy when there's no ExtraInfo to put into the dialog.
, aExtraInfo // aExtraInfo defaults to "" so this is safe.
// Caller should specify NULL for aParams if it wants us to attempt to parse out params from
// within aAction. Caller may specify empty string ("") instead to specify no params at all.
// Remember that aAction and aParams can both be NULL, so don't dereference without checking first.
// Note: For the Run & RunWait commands, aParams should always be NULL. Params are parsed out of
// the aActionString at runtime, here, rather than at load-time because Run & RunWait might contain
// deferenced variable(s), which can only be resolved at runtime.
{
HANDLE hprocess_local;
HANDLE &hprocess = aProcess ? *aProcess : hprocess_local; // To simplify other things.
hprocess = NULL; // Init output param if the caller gave us memory to store it. Even if caller didn't, other things below may rely on this being initialized.
if (aOutputVar) // Same
aOutputVar->Assign();
// Launching nothing is always a success:
if (!aAction || !*aAction) return OK;
size_t aAction_length = strlen(aAction);
if (aAction_length >= LINE_SIZE) // Max length supported by CreateProcess() is 32 KB. But there hasn't been any demand to go above 16 KB, so seems little need to support it (plus it reduces risk of stack overflow).
{
if (aDisplayErrors)
ScriptError("String too long." ERR_ABORT); // Short msg since so rare.
return FAIL;
}
// Declare this buf here to ensure it's in scope for the entire function, since its
// Since CreateProcess() requires that the 2nd param be modifiable, ensure that it is
// (even if this is ANSI and not Unicode; it's just safer):
char *command_line; // Need a new buffer other than parse_buf because parse_buf's contents may still be pointed to directly or indirectly for use further below.
if (aParams && *aParams)
{
command_line = (char *)_alloca(aAction_length + strlen(aParams) + 10); // +10 to allow room for space, terminator, and any extra chars that might get added in the future.
sprintf(command_line, "%s %s", aAction, aParams);
}
else // We're running the original action from caller.
strcpy(command_line, aAction); // CreateProcessW() requires modifiable string. Although non-W version is used now, it feels safer to make it modifiable anyway.
}
if (use_runas)
{
if (!DoRunAs(command_line, aWorkingDir, aDisplayErrors, aUpdateLastError, si.wShowWindow // wShowWindow (min/max/hide).
, aOutputVar, pi, success, hprocess, system_error_text)) // These are output parameters it will set for us.
return FAIL; // It already displayed the error, if appropriate.
}
else
{
// MSDN: "If [lpCurrentDirectory] is NULL, the new process is created with the same
// current drive and directory as the calling process." (i.e. since caller may have
// specified a NULL aWorkingDir). Also, we pass NULL in for the first param so that
// it will behave the following way (hopefully under all OSes): "the first white-space รป delimited
// token of the command line specifies the module name. If you are using a long file name that
// contains a space, use quoted strings to indicate where the file name ends and the arguments
// begin (see the explanation for the lpApplicationName parameter). If the file name does not
// contain an extension, .exe is appended. Therefore, if the file name extension is .com,
// this parameter must include the .com extension. If the file name ends in a period (.) with
// no extension, or if the file name contains a path, .exe is not appended. If the file name does
// not contain a directory path, the system searches for the executable file in the following
// sequence...".
// Provide the app name (first param) if possible, for greater expected reliability.
// UPDATE: Don't provide the module name because if it's enclosed in double quotes,