Dead hotstring locator

Post your working scripts, libraries and tools for AHK v1.1 and older
User avatar
kunkel321
Posts: 1136
Joined: 30 Nov 2015, 21:19

Dead hotstring locator

04 Apr 2023, 14:10

I have a personal version of AutoCorrect.ahk that I've customized over the years. I wanted a way to locate duplicate hotstring triggers and also locate no-end-char sub-trigger-strings that would prevent longer trigger strings from being usable. For example, given these two,

Code: Select all

::ccc::Biz ;unreachable
:*:cc::Wap; makes above unreachable
you can never expand the Biz one, because as soon as you press the second "c" then Wap expands. And, of course, when there are duplicates, only the first one is seen.

Running the below script as it is written, yields:
Spoiler

Code: Select all

#SingleInstance, Force
; A script to find dead hotstrings. Ver 4-5-2023-c.
; for AHK v1

varMain =
(
:T:aaa::blah
::bbb::blah
::ccc::Biz ;unreachable
::aaa::blua
:*:cc::Wap; makes above unreachable
::ddd::bluab
::eee::Boo
::ccc::Biz
::ccc::Biz
::fff::blah
::ggg::Foo ;unreachable
::eee::Bang
:*B0:gg::Bar; makes above unreachable
)
;~ MainList := "Autocorrect.ahk" ; Must be in same folder as this script.
;~ FileRead, varMain, %MainList% ; load entire file contents into variable.

regex := "(:(?:\*|\?|\w)*:)(..*)::(.*?)(?=\s;|$)\s*(;.*)?" ; swagfag-based
	;~ OutArrays
	;~ 1 = :options:
	;~ 2 = trigger
	;~ 3 = expansion when present
	;~ 4 = comment when present
duplicates := "" ; Checking for trigger string, not expansion text.
dupeCount := 0
unreachable := "" ; This means that an ':*:immediate trigger' string  is blocking it.
unreachCount := 0
StartTime := A_TickCount

;MsgBox, varMain is: |%varMain%|
Loop, parse, varMain, `n, `r ; Check contents line-by-line.
{ ; 88888888888888888 OUTER LOOP 88888888888888888888888888
	If (SubStr(A_LoopField, "1", "1")!=":") ; If line does not start with colon, then skip rest of loop.
		continue
	Else
	{
		;MsgBox, A_LoopField is: |%A_LoopField%|
		RegExMatch(A_LoopField, regex, Hotty)
		LineNum := A_Index ; Save index of outer loop before A_Index gets reused below.
		LineText := A_LoopField ; Save text before A_LoopField gets reused below.
		varMain := StrReplace(varMain,A_LoopField,"",,1) ; Remove Hotty from list so we don't compare it with itself.
		Loop, parse, varMain, `n, `r ; Check contents line-by-line.
		{ ; ########## INNER LOOP #####################
			If (SubStr(A_LoopField, "1", "1")!=":") ; If line does not start with colon, then skip rest of loop.
				continue
			else
				RegExMatch(A_LoopField, regex, subHotty)
				if (subHotty2 = Hotty2)
				{
					duplicates := duplicates . "lines " . LineNum . "`t" . Hotty1 . "`t" . Hotty2 . "::" . Hotty3 . Hotty4 . "`n
					-and " . A_Index . "`t" . subHotty1 . "`t" . subHotty2 . "::" . subHotty3 . subHotty4 . "`n"
					dupeCount++
				}
				else if (instr(subHotty1,"*") && RegExMatch(Hotty2, "^" . subHotty2 . ".*"))
				or (instr(Hotty1,"*") && RegExMatch(subHotty2, "^" . Hotty2 . ".*"))
				{
					unreachable := unreachable . "lines " . LineNum . "`t" . Hotty1 . "`t" . Hotty2 . "::" . Hotty3 . Hotty4 . "`n
					-and " . A_Index . "`t" . subHotty1 . "`t" . subHotty2 . "::" . subHotty3 . subHotty4 . "`n"
					unreachCount++
				}
				;soundbeep, , 250
				continue

		} ; #######################################
	}
} ; 8888888888888888888888888888888888

ElapsedTime := (A_TickCount - StartTime) / 60000
ElapsedTime := Round(ElapsedTime, 2)
dupeSum := % " ---- " dupeCount " Duplicates: ---- `n" duplicates
unreachSum := % " ---- " unreachCount " Unreachable items: --- `n" unreachable
MsgBox, Search of %MainList% took %ElapsedTime% minutes.`n---------------------------`n%dupeSum%`n%unreachSum%

Esc::ExitApp
It's noteworthy that AHK can have "context specific" hotkeys. As in
https://www.autohotkey.com/docs/v1/Hotstrings.htm#variant

The above script can't differentiate those, and will flag them as dupes. It does ignore ;commented-out lines, but only per line. It can't ignore hotstrings in
/*
block comments
*/

I processed AutoCorrect.ahk (the original one). If anyone is curious:
fyi before processing it, I ;by-line-commented-out the ambiguous items.
Spoiler

Edit:
-It doesn't work well with triplicates.... Those get flagged as three different duplicate pairs. Might try to fix that in the future.

Edit 4-5-2023: I see an error in the autocorrect "unreachable" list...

Code: Select all

lines 264	:*:	lsit::list
 -and 3818	::	realsitic::realistic
The top one doesn't make the bottom one unreachable. If it was a "middle-of-word" replacement (:*?:) it would make it redundant though. I will fix this in the next version. Fixed. (It was two of the regexes in the inner loop.)
Maybe will add a third check for redundant items...

Edit 2 on 4-5-2023: I rearranged the logic parts in above ver 4-5-2023 b, circumventing the need for a couple of the regexes. By doing this I was able to shave a minute off of the time needed to process Autocorrect.ahk. (from 3.15 minutes, to 2.14). Also I simplified the regex pattern. It occurred to me that I don't need to ensure that the hotstring options are valid... I just need to differentiate them from the rest of the hotstring.
-Side note: With the new logic, I'm assuming that ANY line starting with a semicolon is a hotstring (or at least a hotstring trigger). Hopefully this is a valid assumption(?)

Edit 3 on 4-5-2023: Used the regex from here viewtopic.php?f=76&t=115786&p=516098#p516098 in the above
ver 4-5-2023-c. It speeded things up a bit.

Mildly interesting notes: My timings for processing Autocorrect.ahk:
The original 2007 Autocorrect.ahk has 5295 lines (though not all are hotstrings).
-The first version of this script took 3.15 minutes.
-Changing the logic so If InStr circumvented non-hotstring lines: 2.14 mins.
-Updated regex so that unneeded groups were no longer captured: 1:81 mins.
-Added "S)" regex option (Studies the pattern to try improve its performance). It wasn't able to improve this particular regex though: 1.82 mins.
-Ran it again the same way: 1.82 mins again.
-Took the "S)" back off: 1.75 this time.
Last edited by kunkel321 on 14 Apr 2023, 20:40, edited 13 times in total.
ste(phen|ve) kunkel
Jasonosaj
Posts: 53
Joined: 02 Feb 2022, 15:02
Location: California

Re: Dead hotstring locator

04 Apr 2023, 15:15

I've also done some playing around and achieved something similar with the TF.ahk library, as well as added some features like search for correction, . It's not particularly pretty but it seems to work okay.

Code: Select all

; 
;------------------------------------------------------------------------------
; Settings
;------------------------------------------------------------------------------
#NoEnv ; For security
#Persistent
#MenuMaskKey vkFF
#SingleInstance force
#Requires AutoHotkey v1.1.33
Process, Priority,,High
SetTitleMatchMode,2 
SetWorkingDir %A_ScriptDir%
SendMode, Input		; avoid later characters being put in expanded hotstrings (09:47 AM 4/08/2022)
menu, tray, icon, % a_scriptDir "\Media\Autocorrect2.ico"

CoordMode, Menu, Window
Coordmode, Caret, Window

#Include %A_ScriptDir%/Lib-Autocorrect
#Include TF-master/tf.ahk

FileEncoding, UTF-8

Global FirstCorrection, LastCorrection
Global F := A_ScriptFullPath
Global Source := A_ScriptDir
Global Destination := A_ScriptDir "\AutocorrectBackups\" A_Now " - Autosort- Autocorrect.ahk" ; " - *.*"

;; Establish the boundaries within which new lines will be written.  Preserve non-hotstring portions.
Gosub, CalculateBoundaries
Return

;; End of Autocomplete

; Sort hotstrings and remove exact dupes
^#!F12::
	Destination := A_ScriptDir "\AutocorrectBackups\" A_Now " - Manual Sort - 1 - JJK Autocorrect.ahk" ; " - *.*"
	Source := a_scriptDir
	FileCopy, %Source%, %Destination%
	Gosub, CalculateBoundaries
	TF_Sort("!"F, U, FirstCorrection, LastCorrection) ; uses the TF.ahk library to sort the entries into alpha order
	TF_RemoveDuplicateLines("!"F, FirstCorrection, LastCorrection, 1, False) 
		Reload
		Msgbox, error reloading
		Return

; Add new entry		
^#!h::
	KeyWait LControl
	Keywait LWin
	Keywait LAlt
	Keywait H
	If ErrorLevel {
		Send, {LControl Up}
		Send, {LWin Up}
		Send, {LAlt Up}
		Send, {H Up}
	}
	SendInput ^{right}
	sleep, 100
	SendInput +^{left}
	AutoTrim On 				; Delete any leading and trailing whitespace on the clipboard. Why would you want this?
	ClipboardOld := ClipboardAll
	Clipboard := "" 				; Must start off blank for detection to work.
	Send ^c
	ClipWait 3
	if ErrorLevel { 				; ClipWait timed out.
		Tooltip, error with copy process
		Settimer, Tooltip, -2000
		return
	}
	; Replace CRLF and/or LF with `n for use in a "send-raw" hotstring
	Hotstring := StrReplace(Clipboard, "''","''''") 		; Do this replacement first to avoid interfering with the others below.
	Hotstring := StrReplace(Hotstring,"`r`n","`r")
	Hotstring := StrReplace(Hotstring,"`n","`r")
	Hotstring := StrReplace(Hotstring,A_Tab,"q")
	Hotstring := StrReplace(Hotstring,";","`;")
	OldStr := Hotstring 						; RTrim(Hotstring, OmitChars := " ")
	TitleCase := IsTitleCase(Hotstring)
	StringLower, Hotstring, Hotstring
	Hotstring := Trim(Hotstring)
	PossibleHotstring := SearchFunction(Hotstring)
	Sleep, 1000
	Clipboard := ClipboardOld
	SetTimer, MoveCaret, 10					; This will move the InputBox's caret to a more friendly position:
	If (PossibleHotstring != "") 					; Show the InputBox, providing the default hotstring:
		InputBox, Hotstring, New Hotstring, Provide the corrected word on the right side. You can also edit the left side if you wish.`n`nExample entry:`n::teh::the,,,,,,,, ::%Hotstring%::%PossibleHotstring% ; ::%Hotstring%::%Hotstring%
	else
		InputBox, Hotstring, New Hotstring, Provide the corrected word on the right side. You can also edit the left side if you wish.`n`nExample entry:`n::teh::the,,,,,,,, ::%Hotstring%:: ; ::%Hotstring%::%Hotstring%
	if ErrorLevel <> 0 						; The user pressed Cancel.
		return
	ReplPos := InStr(Hotstring, ":", , 0)
	ReplStr := SubStr(Hotstring, ReplPos+1)
	SrcPos := InStr(Hotstring, ":", , , 2)
	SrcStr := SubStr(Hotstring, SrcPos+1, ReplPos-SrcPos-2)
	NewStr := StrReplace(OldStr, SrcStr, ReplStr)
	If (TitleCase) {
	  StringLower, NewStr, NewStr, T
	}
	SendInput %NewStr% ;%Ending%
	FileCopy, %Source%, %Destination%				; Otherwise, add the hotstring and reload the script:
	Sleep, 200
	TF_InsertLine("!"F, LastCorrection, LastCorrection, Hotstring)	; Uses TF.ahk library to insert the new line before the bottom section of the script, which we want to remain undisturbed
	Reload
	Sleep 3000 ; If successful, the reload will close this instance during the Sleep, so the line below will never be reached.
	MsgBox, 4,, The hotstring just added appears to be improperly formatted. Would you like to open the script for editing? Note that the bad hotstring is at the bottom of the script.
	IfMsgBox, Yes, Edit
		return

; Determines if word is title cased
IsTitleCase(str) {
	FirstCharCode:=asc(str)
	if(FirstCharCode > 64 and FirstCharCode < 91) { 
		return true 
	} else {
		return false
	}
}

SearchFunction(StringTemp) {
	UrlDownloadToFile % "https://www.google.com/search?q=" . StringTemp, temp
	If ErrorLevel {
		msgbox, No access to internet
		StringTemp := ""
		Return StringTemp
	}
	FileRead, StringTemp, temp
	FileDelete temp
	Clipboard := StringTemp
	ClipWait 0.5
	;Msgbox, % Clipboard StringTemp
	if (RegExMatch(StringTemp, "(Including results for|Showing results for|Did you mean:)(.*?)</a>", match)) {
		match2 := RegExReplace(match2,"<.+?>")
		;StringReplace, StringTemp, match2, &#39;,', All
		StringTemp := StrReplace(Match2,"&#39;","'")
		StringTemp := Trim(StringTemp)
		;Msgbox, % StringTemp "afterregex "
	}
	else {
		StringTemp := ""
	}
	Temp := Match := ""
	Return StringTemp
}

MoveCaret:
	IfWinNotActive, New Hotstring
		return
	; Otherwise, move the InputBox's insertion point to where the user will type the abbreviation.
	Send {HOME}
	Loop % StrLen(Hotstring) + 4
		SendInput {Right}
	SetTimer, MoveCaret, Off
	return

Tooltip:
	Tooltip
	Return
	
ReloadScript:
	Reload
	
CalculateBoundaries:
	LineCount := TF_CountLines(F)
	FirstLineTemp := TF_Find(F,1,LineCount,"Full Words",0,0)
	FirstLineTempArray := StrSplit(FirstLineTemp,",")
	FirstCorrection := FirstLineTempArray[2]+3
	LastLineTemp := TF_Find(F,1,LineCount,"DO NOT EDIT BELOW THIS LINE",0,0)
	LastLineTempArray := StrSplit(LastLineTemp,",")
	LastCorrection := LastLineTempArray[2]-3
	FirstlineTemp := FirstLineTempArray := LastLineTemp := LastLineTempArray := ""
	return
User avatar
kunkel321
Posts: 1136
Joined: 30 Nov 2015, 21:19

Re: Dead hotstring locator

05 Apr 2023, 09:12

Jasonosaj wrote:
04 Apr 2023, 15:15
I've also done some playing around and achieved something similar with the TF.ahk library, as well as added some features like search for correction, . It's not particularly pretty but it seems to work okay.
It looks pretty cool. I can't get it to work though. I have tf.ahk in the correct location, but it looks like you have autocorrect.ahk in some unique location. Anyway, I see that you combined the tool with a version of the Hotsting Helper tool and make them external to your main autocorrect file. I was thinking of doing something similar.
ste(phen|ve) kunkel
User avatar
Relayer
Posts: 160
Joined: 30 Sep 2013, 13:09
Location: Delaware, USA

Re: Dead hotstring locator

05 Apr 2023, 10:12

Hi,

When I started using AutoHotkey 10+ years ago I decided that all of my hotstrings would be "triggered" with a semicolon, ';'. This cures some issues and also keeps a slip of the finger from replacing text I didn't want to replace. It gives full control and become second-nature once you get used to it. I haven't thought about it enough to know if it solves the issues you point out but I thought I'd offer it as an alternative to explore.

Relayer
User avatar
kunkel321
Posts: 1136
Joined: 30 Nov 2015, 21:19

Re: Dead hotstring locator

05 Apr 2023, 11:59

@Relayer
Excellent. I also use semicolon as my standard prefix. I even have it as a default setting in my version of Hotstring Helper viewtopic.php?f=6&t=114688&p At first I used an equals sign, but it occurred to me that the semicolon is one of the eight home keys on a qwerty keyboard, so it was the most sensible choice. Of course that doesn't apply to auto-correct-as-you-type hotstrings. I use it for my other custom expansions though--most of which are acronym-based.

EDIT: @Relayer Actually, You said that you "trigger" your expansions with a semicolon. With the mentioned Hotstring Helper - Multi Line tool, my setup is

Code: Select all

 ;=======Change=options=for=multi=word=entry=options=and=trigger=strings=as=desired==============
 myDefaultOpts := ""    ; PreEnter these hotstring options; "*" = end char not needed, etc.
 myPrefix := ";"        ; Optional character that you want suggested at the beginning of each hotstring.
 addFirstLetters := 5   ; Add first letter of this many words. (5 recommended; 0 = don't use feature.)
  tooSmallLen := 2      ; Only first letters from words longer than this. (Moot if addFirstLetters = 0)
 mySuffix := ""         ; An empty string "" means don't use feature.
;===========================================================one=more=below=======================
But yours would probably be:

Code: Select all

 ;=======Change=options=for=multi=word=entry=options=and=trigger=strings=as=desired==============
 myDefaultOpts := "*"    ; PreEnter these hotstring options; "*" = end char not needed, etc.
 myPrefix := ""         ; Optional character that you want suggested at the beginning of each hotstring.
 addFirstLetters := 5   ; Add first letter of this many words. (5 recommended; 0 = don't use feature.)
  tooSmallLen := 2      ; Only first letters from words longer than this. (Moot if addFirstLetters = 0)
 mySuffix := ";"          ; An empty string "" means don't use feature.
;===========================================================one=more=below=======================
ste(phen|ve) kunkel
User avatar
Relayer
Posts: 160
Joined: 30 Sep 2013, 13:09
Location: Delaware, USA

Re: Dead hotstring locator

06 Apr 2023, 07:33

Steve,

Yes, I use a suffix. And semicolon as a home position is one of the reasons I chose it. I was never comfortable with auto-correct as I touch type and am not always looking at the screen and I found that even an occasional substitution I did not intend can lead to issues/embarrassment. Maybe I am just a control freak!

Relayer
User avatar
kunkel321
Posts: 1136
Joined: 30 Nov 2015, 21:19

Re: Dead hotstring locator

06 Apr 2023, 09:12

I find that the autocorrections are helpful. I'm not a great typer, though, so I always monitor (look at) the screen while I'm typing. So if the wrong word is inserted, I catch it. Doesn't happen too often though.

As I think about using semicolon as a prefix vs a suffix, I think an argument can be made for using it as a prefix... When using a semicolon in normal typing, it is always after a word. This much I know; to thine own self be true. If you happen to have a trigger like ::know;::, then it would get accidentally triggered. In normal typing though, a semicolon is never attached to the beginning of a word.

Though I have once or twice accidentally activated a hotstring when typing an AHK in-line comment!
ste(phen|ve) kunkel
Kotte
Posts: 46
Joined: 03 May 2021, 08:33

Re: Dead hotstring locator

13 Apr 2023, 04:33

@kunkel321
Cool! This script was way faster than I expected! Great job! I had a small issue though. It flagged my case sensitive hotstrings as duplicates. Example:

Code: Select all

varMain =
(
:c:_a_::Send {U+0x03B1}
:c:_A_::Send {U+0x0392}
)
Could this be remedied?
User avatar
kunkel321
Posts: 1136
Joined: 30 Nov 2015, 21:19

Re: Dead hotstring locator

13 Apr 2023, 08:41

Kotte wrote:
13 Apr 2023, 04:33
@kunkel321
Cool! This script was way faster than I expected! Great job! I had a small issue though. It flagged my case sensitive hotstrings as duplicates. Example:

Code: Select all

varMain =
(
:c:_a_::Send {U+0x03B1}
:c:_A_::Send {U+0x0392}
)
Could this be remedied?
Glad it's useful for you! Hmmm.. Surely this is possible. I guess that a simple variable comparison IF (var1 = var2) must not be case-sensitive(?) Maybe we can set it up so that if the options contain "C", then it uses a regExMatch comparison. We can definitely make those case-sensitive...
Last edited by kunkel321 on 15 Apr 2023, 10:52, edited 1 time in total.
ste(phen|ve) kunkel
User avatar
kunkel321
Posts: 1136
Joined: 30 Nov 2015, 21:19

Re: Dead hotstring locator

14 Apr 2023, 13:50

This comparison technique seems to give the desired effect. It's noteworthy that AHK v2 appears to have a StrCompare function that allows case-sensitivity. Might be a good reason to make a v2 version of this in the future...

Code: Select all

#SingleInstance, Force
;===================

HtSt1 := "?C*"
var1 := "_abc_"
var2 := "_aBc_"

If (var1 !== var2) and (var1 = var2) and (InStr(HtSt1,"C"))
	MsgBox Only difference is case.
else if (var1 = var2)
	MsgBox Duplicates.
else
	MsgBox Not duplicates.

ExitApp
Esc:: ExitApp
Okay here is a preliminary version.
Notes: This doesn't replace the one in the top post. It's more like a 'fork.' It no longer checks for beginning-of-word items like :*:trig::expansion, but it does skip items if the only difference is case AND if c or C is in the hotstring options. This version also adds a third loop. The loop that used to be the inner loop, is now the middle loop. The third loop allows the script to handle triplicates/quads/etc better. I did timing checks with AutoCorrect.ahk again. The "case sensitivity" part adds about 0.2 minutes.

Also... Previous versions were skipping hotstrings that were indented. This one does not.

EDIT on 4-14-2023. Rearranged the logic so that, only if matching triggers are found, is case-sensitive matching done.
EDIT on 4-15-2023. This version observes block comments /* ... */. Also the elapsed time is now presented as M:SS.
Second EDIT on 4-15-2023. It occurred to me that the regex was capturing four subpatterns, but only using the first two. The others will get used in another fork, but they are not needed here. Without capturing those, the AutoCorrect.ahk file is processed in under a minute...
Spoiler
Edit again one minute later... I just realized that the full string is no longer getting reported in the summary at the end :wtf: . I did sorta' like having it that way... I also like the script to be fast though. I dunno' maybe I'll leave it like this.

Code: Select all

#SingleInstance, Force
; A script to find duplicate hotstrings. Ver 4-15-2023
; for AHK v1
StartTime := A_TickCount

varMain =
(
::ddd::bluab
::ccc::yatta
	:c:_a_::little a
	:c:_A_::big A
	msgbox no hotstring here
	!#^+x::also not a hotstring
	::aaa::blah
	:c:B::big bee
	:c:b::little bee1
/*
:c:b::little bee2
::aaa::blah
::ddd::doesn't get culled.
::fff::
::ccc::yaddaya
::aaa::blah
*/
::ddd::bluab
::aaa::blah
)
;~ MainList := "Autocorrect.ahk" ; Must be in same folder as this script.
;~ FileRead, varMain, %MainList% ; load entire file contents into variable.

;~ regex := "(:(?:\*|\?|\w)*:)(..*)::(.*?)(?=\s;|$)\s*(;.*)?" ; swagfag-based
regex := "(:(?:\*|\?|\w)*:)(..*)::.*?"
	;~ Outputs -- 1 = :options: , 2 = trigger
duplicates := "" ; Checking for trigger string, not expansion text.
dupeCount := 0
isBlockCom := 0

; Then trim any left-spaces, cull commented lines.
Loop, parse, varMain, `n, `r ; Check contents line-by-line.
{ ; 000000 PreLoop 000000
	pLoopFld := LTrim(A_LoopField, OmitChars := "`t ")
	if (pLoopFld != A_LoopField)
		varMain := StrReplace(varMain, A_LoopField, pLoopFld,, 1) ; Remove whitespace from list
	; Cull commented-out hotstrings.  Necessary for below StrReplaces to replace correct instance of hS.
	If (SubStr(pLoopFld, "1", "2")=="/*")
		isBlockCom := 1
	If (SubStr(pLoopFld, "1", "2")=="*/")
		isBlockCom := 0
	If (SubStr(pLoopFld, "1", "1")!=":") or (isBlockCom = 1)
		varMain := StrReplace(varMain, pLoopFld, "comment",, 1)
	else
		continue
} ; 00000000000
;MsgBox, varMain is: |%varMain%|

Loop, parse, varMain, `n, `r ; Check contents line-by-line.
{ ; 1111111111 OUTER LOOP 1111111111

	oLoopFld := A_LoopField
	If (SubStr(oLoopFld, "1", "1")!=":") ; If line does not start with colon, then skip rest of loop.
	{
		varMain := StrReplace(varMain, oLoopFld, "o1---XYZ",, 1)
		continue
	}
	Else
	{
		RegExMatch(oLoopFld, regex, oHtSt)
		oLineNum := A_Index ; Save index of outer loop before A_Index gets reused below.
		varMain := StrReplace(varMain, oLoopFld, "o2---XYZ",, 1) ; Remove oHtSt from list so we don't compare it with itself.

		Loop, parse, varMain, `n, `r ; Check contents line-by-line.
		{ ; 2222222222 MIDDLE LOOP 2222222222
			mLoopFld := A_LoopField ; This assignement is unneeded, but I'm keeping it for consistency.
			If (SubStr(mLoopFld, "1", "1")!=":") ; If line does not start with colon, then skip rest of loop.
				continue
			else
			{
				RegExMatch(mLoopFld, regex, mHtSt)
				if (mHtSt2 = oHtSt2) ; If they match ...
					If (mHtSt2 !== oHtSt2) and (mHtSt2 = oHtSt2) and (InStr(mHtSt1,"C")) ; Check for cAsE.
						continue
				Else
				{
					duplicates := duplicates "------`nlines " oLineNum "`t" oHtSt1 "`t" oHtSt2 "::" oHtSt3 oHtSt4 "`n
					and "A_Index "`t" mHtSt1 "`t" mHtSt2 "::" mHtSt3 mHtSt4 "`n"
					dupeCount++
					varMain := StrReplace(varMain, mLoopFld, "m---XYZ",,1) ; Remove mHtSt from list
					Loop, parse, varMain, `n, `r ; Check contents line-by-line.
					{ ; 3333333333 INNER LOOP 3333333333
	;MsgBox, At top of INNER loop:`nvarMain is:`n %varMain%`n`noHtSt2 is: |%oHtSt2%| `nmHtSt2 is: |%mHtSt2%|`nA_LoopField is: |%A_LoopField%|`n`nduplicates is:`n%duplicates%
						iLoopFld := A_LoopField
						If (SubStr(iLoopFld, "1", "1")!=":") ; If line does not start with colon, then skip rest of loop.
							continue
						else
							RegExMatch(iLoopFld, regex, iHtSt)
							if (iHtSt2 = mHtSt2)
							{
								duplicates := duplicates " and " A_Index "`t" iHtSt1 "`t" iHtSt2 "::" iHtSt3 iHtSt4 "`n"
								varMain := StrReplace(varMain, iLoopFld, "i---XYZ",,1) ; Remove iHtSt from list
							}
											} ; 33333333333333333333
					oHtSt2 := "oX"
				}
			}
		} ; 22222222222222222222
	}
} ; 11111111111111111111

ElapsedTime := (A_TickCount - StartTime) / 1000
min := StrReplace((round(ElapsedTime // 60) . " min, "), "0 min, ","")
sec := round(Mod(ElapsedTime, 60), 3) . " seconds"

dupeSum := % dupeCount " Duplicates:`n" duplicates
MsgBox, Search of %MainList% took %min%%sec%.`n---------------------------`n%dupeSum%
ExitApp

Esc::ExitApp
Another EDIT on 4-15-2023.
I'll post this version separately. It's not really a "fork" of the script that is directly above this one, but the above one is technically faster, so folks might prefer it. This version has the advantage of showing the entire hotstring in the summary report. This could make it easier for you to decide which one of the duplicates you want to remove or change. The disadvantage is that it's a tiny bit slower (but still is <1min for AutoCorrrect.ahk). The summary report is not as sleek now either. Previously we were displaying line number -> options -> trigger -> expansion. Now we are displaying line number -> options -> trigger -> the entire line I did want to keep the options and the trigger strings each in their own columns to make them easier to differentiate. With the below version of the script, the summary report looks like this:
Spoiler
It's worth noting that the columns sometimes get messed up in the popup messagebox. They are fine when pasted into a text editor though (may depend on your Windows tab settings). (Note also that I changed the names of a couple vars.)

Code: Select all

#SingleInstance, Force
; A script to find duplicate hotstrings. Ver 4-15-2023
; for AHK v1
StartTime := A_TickCount

HotStrList =
(
::ddd::bluab
::ccc::yatta
	:c:_a_::little a
	:c:_A_::big A
	msgbox no hotstring here
	!#^+x::also not a hotstring
	::aaa::blah
	:c:B::big bee
	:c:b::little bee1
:c:b::little bee2
::aaa::blah
:?:ddd::doesn't get culled.
::fff::
:*:ccc::yaddaya
::aaa::blah
::ddd::bluab
::aaa::blah
)
;~ TargetScript := "Autocorrect.ahk" ; Must be in same folder as this script.
;~ FileRead, HotStrList, %TargetScript% ; load entire file contents into variable.

regex := "(:(?:\*|\?|\w)*:)(..*)::.*?"
	;~ Outputs -- 1 = :options: , 2 = trigger
duplicates := "" ; Checking for trigger string, not expansion text.
dupeCount := 0
isBlockCom := 0

; Then trim any left-spaces, cull commented lines.
Loop, parse, HotStrList, `n, `r ; Check contents line-by-line.
{ ; 000000 PreLoop 000000
	pLoopFld := LTrim(A_LoopField, OmitChars := "`t ")
	if (pLoopFld != A_LoopField)
		HotStrList := StrReplace(HotStrList, A_LoopField, pLoopFld,, 1) ; Remove whitespace from list
	; Cull commented-out hotstrings.  Necessary for below StrReplaces to replace correct instance of hS.
	If (SubStr(pLoopFld, "1", "2")=="/*")
		isBlockCom := 1
	If (SubStr(pLoopFld, "1", "2")=="*/")
		isBlockCom := 0
	If (SubStr(pLoopFld, "1", "1")!=":") or (isBlockCom = 1)
		HotStrList := StrReplace(HotStrList, pLoopFld, "comment",, 1)
	else
		continue
} ; 00000000000
;MsgBox, HotStrList is: |%HotStrList%|

Loop, parse, HotStrList, `n, `r ; Check contents line-by-line.
{ ; 1111111111 OUTER LOOP 1111111111

	oLoopFld := A_LoopField
	If (SubStr(oLoopFld, "1", "1")!=":") ; If line does not start with colon, then skip rest of loop.
	{
		HotStrList := StrReplace(HotStrList, oLoopFld, "o1---XYZ",, 1)
		continue
	}
	Else
	{
		RegExMatch(oLoopFld, regex, oHtSt)
		oLineNum := A_Index ; Save index of outer loop before A_Index gets reused below.
		HotStrList := StrReplace(HotStrList, oLoopFld, "o2---XYZ",, 1) ; Remove oHtSt from list so we don't compare it with itself.

		Loop, parse, HotStrList, `n, `r ; Check contents line-by-line.
		{ ; 2222222222 MIDDLE LOOP 2222222222
			mLoopFld := A_LoopField ; This assignement is unneeded, but I'm keeping it for consistency.
			If (SubStr(mLoopFld, "1", "1")!=":") ; If line does not start with colon, then skip rest of loop.
				continue
			else
			{
				RegExMatch(mLoopFld, regex, mHtSt)
				if (mHtSt2 = oHtSt2) ; If they match ...
					If (mHtSt2 !== oHtSt2) and (mHtSt2 = oHtSt2) and (InStr(mHtSt1,"C")) ; Check for cAsE.
						continue
				Else
				{
					duplicates .= "------------------------------------`nlines " oLineNum "`t" StrReplace(oHtSt1,":","") "`t" oHtSt2 "`t" oLoopFld "`n
					and "A_Index "`t" StrReplace(mHtSt1,":","") "`t" mHtSt2  "`t" mLoopFld "`n"
					dupeCount++
					HotStrList := StrReplace(HotStrList, mLoopFld, "m---XYZ",,1) ; Remove mHtSt from list
					Loop, parse, HotStrList, `n, `r ; Check contents line-by-line.
					{ ; 3333333333 INNER LOOP 3333333333
	;MsgBox, At top of INNER loop:`nHotStrList is:`n %HotStrList%`n`noHtSt2 is: |%oHtSt2%| `nmHtSt2 is: |%mHtSt2%|`nA_LoopField is: |%A_LoopField%|`n`nduplicates is:`n%duplicates%
						iLoopFld := A_LoopField
						If (SubStr(iLoopFld, "1", "1")!=":") ; If line does not start with colon, then skip rest of loop.
							continue
						else
							RegExMatch(iLoopFld, regex, iHtSt)
							if (iHtSt2 = mHtSt2)
							{
								duplicates .=  " and " A_Index "`t" StrReplace(iHtSt1,":","") "`t" iHtSt2  "`t" iLoopFld "`n"
								HotStrList := StrReplace(HotStrList, iLoopFld, "i---XYZ",,1) ; Remove iHtSt from list
							}
											} ; 33333333333333333333
					oHtSt2 := "oX"
				}
			}
		} ; 22222222222222222222
	}
} ; 11111111111111111111

ElapsedTime := (A_TickCount - StartTime) / 1000
min := StrReplace((round(ElapsedTime // 60) . " min, "), "0 min, ","")
sec := round(Mod(ElapsedTime, 60), 3) . " seconds"

dupeSum := % dupeCount " Duplicates:`n" duplicates
MsgBox, Search of %MainList% took %min%%sec%.`n---------------------------`n%dupeSum%
ExitApp


Esc::ExitApp

ste(phen|ve) kunkel
User avatar
kunkel321
Posts: 1136
Joined: 30 Nov 2015, 21:19

Re: Dead hotstring locator

17 Apr 2023, 15:21

Current version of this takes about 1 min, 23 secs to process the original AutoCorrect.ahk script. Below is a brief (13 sec) screencast. The screencast processes only a small subsection of AutoCorrect.ahk.

Edit 4-18-2023: Cleaned up code a little.

Code: Select all

#SingleInstance, Force
; A script to find duplicate hotstrings. Ver 4-18-2023
; Don't flag different-cAsE-only triggers, if :C: present.
; Send results to text file.
; for AHK v1

;===== User Options: Change as desired ============
FaveEditor := "C:\Program Files (x86)\TED Notepad\TedNPad.exe" ; Default used if this is not found.
myDivider := "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
;======== End of User Options =====================

FileSelectFile, TargetScript, 3, %A_ScriptDir%, Open a script file to search for dublicate hotstrings, AutoHotkey scripts (*.ahk)
if (TargetScript = "")
{
    MsgBox, Nothing selected, exiting script.
	ExitApp
}

StartTime := A_TickCount
FileRead, HotStrList, %TargetScript% ; load entire file contents into variable.
StringSplit, HotStrList, HotStrList, `n ; Determines number of lines for Prog Bar range.

Gui, +Owner
Gui, pg:Destroy
Gui, pg:-MinimizeBox +alwaysOnTop
Gui, pg:Add, Progress, w400 h30 cGreen vMyProgress Range0-%HotStrList0%, 0
Gui, pg:Show,, % "Processing file [" StrReplace(TargetScript, A_ScriptDir "\", "") "]..."
Gui, pg:Submit, NoHide
GuiControl, pg:, MyProgress, 0

StartTime := A_TickCount
regex := "(:(?:\*|\?|\w)*:)(..*)::.*?"
duplicates := "" ; Checking for trigger string, not expansion text.
dupeCount := 0
isBlockCom := 0

; Then trim any left-spaces, cull commented lines.
Loop, parse, HotStrList, `n, `r ; Check contents line-by-line.
{ ; 000000 PreLoop 000000
	pLoopFld := LTrim(A_LoopField, OmitChars := "`t ")
	if (pLoopFld != A_LoopField)
		HotStrList := StrReplace(HotStrList, A_LoopField, pLoopFld,, 1) ; Remove whitespace from list
	; Cull commented-out hotstrings.  Necessary for below StrReplaces to replace correct instance of hS.
	If (SubStr(pLoopFld, "1", "2")=="/*")
		isBlockCom := 1
	If (SubStr(pLoopFld, "1", "2")=="*/")
		isBlockCom := 0
	If (SubStr(pLoopFld, "1", "1")!=":") or (isBlockCom = 1)
		HotStrList := StrReplace(HotStrList, pLoopFld, "comment",, 1)
	else
		continue
} ; 00000000000


Loop, parse, HotStrList, `n, `r ; Check contents line-by-line.
{ ; 1111111111 OUTER LOOP 1111111111
	GuiControl, pg:, MyProgress, +1 ; For above progress bar.
	oLoopFld := A_LoopField
	If (SubStr(oLoopFld, "1", "1")!=":") ; If line does not start with colon, then skip rest of loop.
	{
		HotStrList := StrReplace(HotStrList, oLoopFld, "o1---XYZ",, 1)
		continue
	}
	Else
	{
		RegExMatch(oLoopFld, regex, oHtSt)
		oLineNum := A_Index ; Save index of outer loop before A_Index gets reused below.
		HotStrList := StrReplace(HotStrList, oLoopFld, "o2---XYZ",, 1) ; Remove oHtSt from list so we don't compare it with itself.

		Loop, parse, HotStrList, `n, `r ; Check contents line-by-line.
		{ ; 2222222222 MIDDLE LOOP 2222222222
			mLoopFld := A_LoopField ; This assignement is unneeded, but I'm keeping it for consistency.
			If (SubStr(mLoopFld, "1", "1")!=":") ; If line does not start with colon, then skip rest of loop.
				continue
			else
			{
				RegExMatch(mLoopFld, regex, mHtSt)
				if (mHtSt2 = oHtSt2) ; If they match ...
					If (mHtSt2 !== oHtSt2) and (mHtSt2 = oHtSt2) and (InStr(mHtSt1,"C")) ; Check for cAsE.
						continue
				Else
				{
					vTab := StrLen(oHtSt2)<8? "`t" : "" ; Add extra tab after trig str if it's short.
					duplicates .= myDivider "`nlines " oLineNum "`t" StrReplace(oHtSt1,":","") "`t" oHtSt2 "`t" vTab oLoopFld "`n
					and "A_Index "`t" StrReplace(mHtSt1,":","") "`t" mHtSt2  "`t" vTab mLoopFld "`n"
					dupeCount++
					HotStrList := StrReplace(HotStrList, mLoopFld, "m---XYZ",,1) ; Remove mHtSt from list
					Loop, parse, HotStrList, `n, `r ; Check contents line-by-line.
					{ ; 3333333333 INNER LOOP 3333333333
						iLoopFld := A_LoopField
						If (SubStr(iLoopFld, "1", "1")!=":") ; If line does not start with colon, then skip rest of loop.
							continue
						else
							RegExMatch(iLoopFld, regex, iHtSt)
							if (iHtSt2 = mHtSt2)
							{
								duplicates .=  " and " A_Index "`t" StrReplace(iHtSt1,":","") "`t" iHtSt2  "`t" vTab iLoopFld "`n"
								HotStrList := StrReplace(HotStrList, iLoopFld, "i---XYZ",,1) ; Remove iHtSt from list
							}
											} ; 33333333333333333333
					oHtSt2 := "oX"
				}
			}
		} ; 22222222222222222222
	}
} ; 11111111111111111111

Gui, pg:Hide ; Remove progress bar.

ElapsedSecs := (A_TickCount - StartTime) / 1000
min := StrReplace((round(ElapsedSecs // 60) . " min, "), "0 min, ","")
sec := round(Mod(ElapsedSecs, 60), 3) . " seconds"
dupeSum := % "Search of [" StrReplace(TargetScript, A_ScriptDir "\", "") "] took " min sec " and found " dupeCount " duplicate groups.`n`n Location`tOpts`tTrigger`t`tFull line`n" duplicates
DupeFile :=  A_ScriptDir "\Duplicates Found-" A_YYYY "-" A_MM "-" A_DD "_" A_Hour "-" A_Min "-" A_Sec
; Example output: S:\AutoHotkey\MasterScript\AutocorrectProject\Duplicates Found-2023-04-17_11-28-55.txt
FileAppend, %dupeSum%, %DupeFile%.txt
MsgBox , 4132, Message, % "We found " dupeCount " duplicate groups, in " min sec "; and the file [" StrReplace(DupeFile, A_ScriptDir "\", "") ".txt] was created.`n`n  Open it?"
IfMsgBox Yes ; User clicked Yes.
   {
   run, %FaveEditor% "%DupeFile%.txt",, UseErrorLevel
		If ErrorLevel
			Run, %DupeFile%.txt
	}

ExitApp
Esc::ExitApp
https://app.screencast.com/ld8A3RTlimvd8
ste(phen|ve) kunkel
User avatar
kunkel321
Posts: 1136
Joined: 30 Nov 2015, 21:19

Re: Dead hotstring locator

20 Apr 2023, 14:37

This version is a fork of the original duplicate-finder script. First some information for noobs—like me! With these three hotstrings

Code: Select all

::snippit::snippet1
:?:ippit::ippet2 
:?:ppit::ppet3
Any of these might be executed if you mistype snippet as “snippit”. The one that will get executed is whichever appears first. I put the digits at the end simply in case anyone would like to test it… Run the code, then type snippit. Then rearrange the lines and rerun the script. You get either snippit1, snippit2 or snippit3, whichever is at the top of the list.

It could be argued that the ones with the shorter triggers are better. The first one can only autocorrect one word, “snippet.” The second one would correct, sippet, snippet, tippet, trippet, OR whippet. And the shortest one would correct, lappet, moppet, muppet, poppet, puppet, sippet, snippet, tappet, tippet, trippet, OR whippet. A really important point is that there aren’t any English words that end with “ppit” otherwise our autocorrect entry would misspell them.

A related tangent… I’m left-hand dominant and apparently have a lazy right pinky finger. I noticed that I occasionally type words ending in ly as l;y (such as occasional;y). So I added a hotstring to my master script :?:l;y::ly This can potentially fix more than nine thousand possible words.

Anyhow… Are these types of matches “duplicate” hotstring triggers? Whatever the case, this version of the script locates them. They are reported with the full trigger, then the partial trigger – only if/when ? appears in the options – is beneath it. The script is not yet smart enough to group triplicates… The above examples would be reported as

Code: Select all

 Location	Opts	Trigger		Full line
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
lines 8		snippit		::snippit::snippet1
 and 9	?	ippit		:?:ippit::ippet2
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
lines 8		snippit		::snippit::snippet1
 and 10	?	ppit		:?:ppit::ppet3
The extra regexes do make it take longer… AutoCorrect.ahk takes about 2:25. You'll note in the below AutoCorrect.ahk report that ign is a submatch for (the correctly spelled) align. This is a special usage that Jim Biancolo added to the original 2007 version of the script. His entry :?:ign::ing can potentially fix about 9405 words, but it would misspell several words that (correctly) end in ign. He fixes this in the script by having trigger only items such as ::align:: appar above the ign one.

Code: Select all

Search of [AutoCorrect.ahk] took 2 min, 24.891 seconds and found 1 duplicate groups.  

 Location	Opts	Trigger		Full line
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
lines 141		align		::align::
 and 181	?	ign		:?:ign::ing

Code: Select all

#SingleInstance, Force
; A script to find hotstrings with duplicate trigger endings. Ver 4-20-2023
; Send results to text file.
; for AHK v1

;===== User Options: Change as desired ============
FaveEditor := "C:\Program Files (x86)\TED Notepad\TedNPad.exe" ; Default used if this is not found.
myDivider := "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
;======== End of User Options =====================

FileSelectFile, TargetScript, 3, %A_ScriptDir%, Open a script file to search for dublicate hotstrings, AutoHotkey scripts (*.ahk)
if (TargetScript = "")
{
    MsgBox, Nothing selected, exiting script.
	ExitApp
}

StartTime := A_TickCount
FileRead, HotStrList, %TargetScript% ; load entire file contents into variable.
StringSplit, HotStrList, HotStrList, `n ; Determines number of lines for Prog Bar range.

Gui, +Owner
Gui, pg:Destroy
Gui, pg:-MinimizeBox +alwaysOnTop
Gui, pg:Add, Progress, w400 h30 cGreen vMyProgress Range0-%HotStrList0%, 0
Gui, pg:Show,, % "Processing file [" StrReplace(TargetScript, A_ScriptDir "\", "") "]..."
Gui, pg:Submit, NoHide
GuiControl, pg:, MyProgress, 0

StartTime := A_TickCount
regex := "(:(?:\*|\?|\w)*:)(..*)::.*?" ; For extracting subgroups.
duplicates := "" ; Checking for trigger string, not expansion text.
dupeCount := 0
isBlockCom := 0

; Then trim any left-spaces, cull commented lines.
Loop, parse, HotStrList, `n, `r ; Check contents line-by-line.
{ ; 000000 PreLoop 000000
	pLoopFld := LTrim(A_LoopField, OmitChars := "`t ")
	if (pLoopFld != A_LoopField)
		HotStrList := StrReplace(HotStrList, A_LoopField, pLoopFld,, 1) ; Remove whitespace from list
	; Cull commented-out hotstrings.  Necessary for below StrReplaces to replace correct instance of hS.
	If (SubStr(pLoopFld, "1", "2")=="/*")
		isBlockCom := 1
	If (SubStr(pLoopFld, "1", "2")=="*/")
		isBlockCom := 0
	If (SubStr(pLoopFld, "1", "1")!=":") or (isBlockCom = 1)
		HotStrList := StrReplace(HotStrList, pLoopFld, "comment",, 1)
	else
		continue
} ; 00000000000


Loop, parse, HotStrList, `n, `r ; Check contents line-by-line.
{ ; 1111111111 OUTER LOOP 1111111111
	GuiControl, pg:, MyProgress, +1 ; For above progress bar.
	oLoopFld := A_LoopField
	If (SubStr(oLoopFld, "1", "1")!=":")
		continue
	Else
	{
		RegExMatch(oLoopFld, regex, oHtSt)
		oLineNum := A_Index ; Save index of outer loop before A_Index gets reused below.
		Loop, parse, HotStrList, `n, `r ; Check contents line-by-line.
		{ ; 2222222222 MIDDLE LOOP 2222222222 ; "inner loop" was removed.
			mLoopFld := A_LoopField ; This assignement is unneeded, but I'm keeping it for consistency.
			If (SubStr(mLoopFld, "1", "1")!=":")
				continue
			Else if !RegExMatch(A_LoopField, ":.*?\?.*?:..*::.*?")
				continue ; If NO '?' in options, don't cull line, just skip it.
			else
			{
				RegExMatch(mLoopFld, regex, mHtSt)
				if RegExMatch(oHtSt2, "..*" . mHtSt2 . "$")
				{
					ovTab := StrLen(oHtSt2)<8? "`t" : "" ; Add extra tab after trig str if it's short.
					mvTab := StrLen(mHtSt2)<8? "`t" : ""
					duplicates .= myDivider "`nlines " oLineNum "`t" StrReplace(oHtSt1,":","") "`t" oHtSt2 "`t" ovTab oLoopFld "`n
					and "A_Index "`t" StrReplace(mHtSt1,":","") "`t" mHtSt2  "`t" mvTab mLoopFld "`n"
					dupeCount++
					HotStrList := StrReplace(HotStrList, mLoopFld, "m---XYZ",,1) ; Remove mHtSt from list
				}
			}
		} ; 22222222222222222222
	}
} ; 11111111111111111111

Gui, pg:Hide ; Remove progress bar.

ElapsedSecs := (A_TickCount - StartTime) / 1000
min := StrReplace((round(ElapsedSecs // 60) . " min, "), "0 min, ","")
sec := round(Mod(ElapsedSecs, 60), 3) . " seconds"
dupeSum := % "Search of [" StrReplace(TargetScript, A_ScriptDir "\", "") "] took " min sec " and found " dupeCount " duplicate groups.`n`n Location`tOpts`tTrigger`t`tFull line`n" duplicates

DupeFile :=  A_ScriptDir "\Duplicates Found-" A_YYYY "-" A_MM "-" A_DD "_" A_Hour "-" A_Min "-" A_Sec
; Example output: S:\AutoHotkey\MasterScript\AutocorrectProject\Duplicates Found-2023-04-17_11-28-55.txt
FileAppend, %dupeSum%, %DupeFile%.txt
MsgBox , 4132, Message, % "We found " dupeCount " duplicate groups, in " min sec "; and the file [" StrReplace(DupeFile, A_ScriptDir "\", "") ".txt] was created.`n`n  Open it?"
IfMsgBox Yes ; User clicked Yes.
   {
   run, %FaveEditor% "%DupeFile%.txt",, UseErrorLevel
		If ErrorLevel
			Run, %DupeFile%.txt
	}

ExitApp
Esc::ExitApp
ste(phen|ve) kunkel
User avatar
kunkel321
Posts: 1136
Joined: 30 Nov 2015, 21:19

Re: Dead hotstring locator

29 Apr 2023, 19:42

This is a branch of the original at the top. In my AutoCorrect file, I've been going through and finding when there are multiple hotstrings that correct the same misspelling in the similar words, and replacing them with hotstrings that work via partial words, i.e. word endings, beginnings, and middles. This had resulted in dozens of word end/beginning/middle hotstrings mixed in with the main list. I created this script to remove those from the main list, and group them, then save everything together in one .ahk file.

Notes:
autocorrects for word endings have hotstring options like this :?:trig::replacement
autocorrects for word beginnings have hotstring options like this :*:trig::replacement
autocorrects for word middles have hotstring options like this :*?:trig::replacement

The below script will skip hotstrings inside #If directives, commented-out ones, and hotstring triggers that don't have the replacement on the same line.

Note the delimiter variable defined near the top. The three groups of hotstrings which are extracted, will be placed right above this delimiter. If the delimiter is not found in your AutoCorrect script, the groups will be at the bottom.

Code: Select all

#SingleInstance, Force
#Requires AutoHotkey v1.1.33
; https://www.autohotkey.com/boards/viewtopic.php?f=6&t=115742&p=519724#p519724
; A script to separate hotstrings that have *, ?, or *? in the trigger options.
; Group them and cull them from the list.  Save to an .ahk file.
; Put the groups in the middle of the file, after the text defined in 'delimiter.'
; by Kunkel321, v4-29-2023

;===== User Options: Change as desired ============
myDivider := "; ==========================================================="
CommentOutNotRemove := 0 ; Set to 0 for line to be removed, 1 for line to be commented-out.
delimiter = ; This is right from the original AutoCorrect.ahk script.
(join`r`n
;------------------------------------------------------------------------------
; Word endings
;------------------------------------------------------------------------------
)
;======== End of User Options =====================

FileSelectFile, TargetScript, 3, %A_ScriptDir%, Open a script file to search for dublicate hotstrings, AutoHotkey scripts (*.ahk)
if (TargetScript = "")
	ExitApp

StartTime := A_TickCount
FileRead, HotStrList, %TargetScript% ; load entire file contents into variable.
StringSplit, HotStrList, HotStrList, `n ; Determines number of lines for Prog Bar range.
FileName := RegExReplace(TargetScript, ".+\\(.+)\.*.{4}", "$1")

Gui, +Owner
Gui, pg:Destroy
Gui, pg:-MinimizeBox +alwaysOnTop
Gui, pg:Add, Progress, w400 h30 cGreen vMyProgress Range0-%HotStrList0%, 0
Gui, pg:Show,, Processing
Gui, pg:Submit, NoHide
GuiControl, pg:, MyProgress, 0
CurrLine := 0

StartTime := A_TickCount
regex := ".*?(:(?:\*|\?|\w)*:)..*::(.*)?"
wdMiddles := "", wdEndings := "", wdBegginings := ""
isBlockCom := 0 ; 1 means script is looking inside a block comment.
isContextSp := 0 ; 1 means inside a Context-Specific #If directive.

Loop, parse, HotStrList, `n, `r ; Check script line-by-line.
{
	GuiControl, pg:, MyProgress, +1 ; For above progress bar.
		CurrLine++
	WinSetTitle, Processing,, Processing line %Currline% of %HotStrList0% in file %FileName%

	If (SubStr(LTrim(A_LoopField, OmitChars := "`t "), "1", "3")=="#If")
	{ ; A Teadrinker regex.
      RegExMatch(A_LoopField . "@", "i)#I(?=f)\w+(\s*?(\s;|@))?", m)
      if (m1 = "")
         isContextSp := 1
      else
         isContextSp := 0
    }
	; The LTrim is in case the code is indented.
	If (SubStr(LTrim(A_LoopField, OmitChars := "`t "), "1", "2")=="/*")
		isBlockCom := 1
	If (SubStr(LTrim(A_LoopField, OmitChars := "`t "), "1", "2")=="*/")
		isBlockCom := 0

	If (SubStr(LTrim(A_LoopField, OmitChars := "`t "), "1", "1")==":") and (isContextSp = 0) and (isBlockCom = 0)
	{
		RegExMatch(A_LoopField, regex, myStr)
		If (myStr2 = "") ; No expansion text in HS, so leave it there.
			continue
		If instr(myStr1, "?") and instr(myStr1, "*")
		{
			wdMiddles .= A_LoopField . "`n"
			If (CommentOutNotRemove = 1)
				HotStrList := StrReplace(HotStrList, A_LoopField, "`; " A_LoopField "`; separated~~~~~~")
			else
				HotStrList := StrReplace(HotStrList,  A_LoopField . "`r`n", "" )
		}
		If instr(myStr1, "?") and !instr(myStr1, "*")
		{
			wdEndings .= A_LoopField . "`n"
			If (CommentOutNotRemove = 1)
				HotStrList := StrReplace(HotStrList, A_LoopField, "`; " A_LoopField "`; separated~~~~~~")
					else
				HotStrList := StrReplace(HotStrList, A_LoopField . "`r`n", "" )
		}
		If instr(myStr1, "*") and !instr(myStr1, "?")
		{
			wdBegginings .= A_LoopField . "`n"
			If (CommentOutNotRemove = 1)
				HotStrList := StrReplace(HotStrList, A_LoopField, "`; " A_LoopField "`; separated~~~~~~")
					else
				HotStrList := StrReplace(HotStrList, A_LoopField . "`r`n", "" )
		}
	}
}

Gui, pg:Hide ; Remove progress bar.

HotSarr := strsplit(HotStrList, delimiter)

ElapsedSecs := (A_TickCount - StartTime) / 1000
min := StrReplace((round(ElapsedSecs // 60) . " min, "), "0 min, ","")
sec := round(Mod(ElapsedSecs, 60), 3) . " seconds"

endSum := myDivider "`n; `t Extracted Word Ending Triggers`n" myDivider "`n" wdEndings "`n`n"
begSum := myDivider "`n; `t Extracted Word Beggining Triggers`n" myDivider "`n" wdBegginings "`n`n"
midSum := myDivider "`n; `t Extracted Word Middle Triggers`n" myDivider "`n" wdMiddles "`n`n"

Summary := % HotSarr[1] "`n`n" endSum begSum midSum myDivider "`n`n" delimiter "`n" HotSarr[2]

SepFile :=  A_ScriptDir "\" FileName "-Separated-" A_MM "-" A_DD "_" A_Hour "-" A_Min "-" A_Sec ".ahk"
FileAppend, %Summary%, %SepFile%
MsgBox , 4132, Message, % "Processing took: " min sec ".`n`nOpen File?"
	IfMsgBox Yes ; User clicked Yes.
		Run, %SepFile%

ExitApp
Esc::ExitApp
ste(phen|ve) kunkel

Return to “Scripts and Functions (v1)”

Who is online

Users browsing this forum: No registered users and 66 guests