Find and replace substrings within short or long MText entities. This little utility, put together quickly to meet a customer request, seems to be able to work around the way that an MText entity breaks up a long character string into 250-character blocks.
The user interface is bare-bones… basic command line prompts. But a cool thing is that it does “remember” the last entered find and replace substrings by saving them on the internal AutoLisp “black-board”. This means that these values survive moving from one drawing to another in MDI (multidocument) mode.
(Does such a utility already exist? Donno… but it’s fun writing one, even if it’s an exercise in reinventing a wheel.)
Before presenting this little tool, here is what drove the need for it.
Let’s say you’ve inserted a large MText entity on many dozens of drawings (example shown below). Buried within this text string is a specification number. You discover that the spec number is wrong!
At a minimum, you’d like to be able to just open each drawing, “hit a button”, and have the MText update to include the correct spec number (i.e. do a substring “Find/Replace”).
MText entity structure
The MText entity’s character string is stored in blocks of characters, about 250 characters each. If the MText string is short, there is one block. Longer strings… multiple blocks.
To illustrate using the above example… type this at the AutoCAD command prompt:
(entget (car (entsel))) [Enter] and then pick on the MText entity.
This display of the picked MText entity’s subrecords is displayed in the command window. The character parts are highlighted. Since the MText extends beyond 250 characters, there are multiple blocks defined (first one highlighted in yellow, the second in green).
This complicates writing a little find/replace utility for MText substring replacement. We need to concatenate these internal blocks of characters, then do our find/replace, and then break them back up into 250-character chunks and push them back out to the entity.
An MText Find/Replace Utility
Here is the attempt at doing the above. The utility’s source code is reproduced below. The first half or so includes some internal funtions to parse a character string and insert replacement substrings. Starting about half-way down is the main program. It does an “ssget” to find all MText entities on the active drawing and begins to process each, one at a time. For each one it opens up the MText entity and creates one long string of text from the 250-char blocks held in the entity (there may only be one). Then it applies the find/replace string parsing function. Now, if any change was made by the find/replace function, it rebuilds the entity with the new text string… breaking it up into 250-character blocks and stuffing these back into the original MText entity.
One other thing of interest… the “find” and “replace” substrings that you type in to the command are “remembered” on the AutoLISP black-board. This means that these values survive going from one drawing to another in MDI mode. You still have to re-appload the function but the default substrings from the previous run will still be there.
; --- M T E X T _ F R . L S P ------- (defun fr_txt_replace ( s what replace firsthitonly / cnt j chk hitit) ; replace "what" with "replace" in s. Case insensitive. ; firsthitonly = nil if replace all instances, /= nil if replace first instance only ; -- internal (defun do_strchr2 (str chx startfrom case_sensitive / rpos pos xflag len ignore_case) ; Return char pos of first occurance of char "ch" in string "str" ; Supports chx strlen > 1 ; and user gives starting place to look for chx, usually 1 ; "startfrom" = 1 to start search from first character ; "case_sensitive" = 1=case sensitive, nil=case in-sensitive (if (not case_sensitive)(setq ignore_case 1)(setq ignore_case nil)) (setq pos 0) (if (not startfrom) (setq startfrom 0) ; ELSE (if (> startfrom 0)(setq startfrom (1- startfrom))) ; needs to be 0 index based ) (if (AND str chx (/= chx "") (> (setq len (strlen str)) 0) (<= startfrom len)) (progn (setq xflag nil) (while (AND (= xflag nil) (< pos len )) (setq xflag 1) ; default to exit loop (if (setq ixy (vl-string-search chx str startfrom)) (progn (setq pos (1+ ixy)) ; return index is 0-based, increment by 1 ) ) ) ) ) pos ; return char position or 0 if not found ) ; -- main routine -- (setq cnt 1) (if (not what) (setq what "")) (if (not replace) (setq replace "")) (if (= what "") (if (OR (not s) (= s "")) (setq s replace)) ; ELSE (progn (setq hitit nil) (while (AND (not hitit) (> (setq pos (do_strchr2 s what cnt nil)) 0)) ; Hit a match, replace it (setq s (strcat (substr s 1 (- pos 1)) replace (substr s (+ pos (strlen what))))) (setq cnt (+ pos (strlen replace))) (if firsthitonly (setq hitit 1)) ; exit on first hit if option is "ON" ) ) ) s ) ; -- MAIN PROGRAM STARTS BELOW -- (defun c:mtext_fr ( / ss find replace slen ix mtext_en oldstr ed newed code default_find default_replace cnt chars_to_output ixx newstr x chg_cnt) ; Generic Find/Replace of MTEXT substring ; Look for previous find/replace substrings stored on the ; "black board". If found, present these as defaults. (setq default_find (vl-bb-ref 'save_find)) (setq default_replace (vl-bb-ref 'save_replace)) (if (AND default_find (/= default_find "")) (progn ; default "find" string found from previous run (setq find default_find) ; default from previous run (setq find (getstring (strcat "\nFind [" default_find "]=") T)) (if (= find "")(setq find default_find)) ; use previous ) ; ELSE (setq find (getstring "\nFind=" T)) ) (if (AND find (/= find "")) ; Save the "find" string on the "black-board" for ; reference next time command is invoked. (vl-bb-set 'save_find find) ) (if default_replace (progn (setq replace default_replace) ; default from previous (setq replace (getstring (strcat "\nReplace with [" default_replace "]=") T)) (if (= replace "")(setq replace default_replace)) ; use previous ) ; ELSE (setq replace (getstring "\nReplace with=" T)) ) (if (AND replace (/= replace "")) ; Save the "replace" string on the "black-board" for ; reference next time command is invoked. (vl-bb-set 'save_replace replace) ) (setq chg_cnt nil) ; track how many MTEXT ents changed (setq ss (ssget "_X" '((0 . "MTEXT")))) (if (/= ss nil) (progn (setq slen (sslength ss)) (setq ix 0) (while (< ix slen) (setq mtext_en (ssname ss ix)) (setq ed (entget mtext_en)) ; open the MTEXT entity ; Mtext string is stored in "1" and "3" subrecords. ; Extract all of the pieces of the Mtext (there may only be one) ; and build one long character string. (setq oldstr "") (foreach x ed (if (OR (= (car x) 1)(= (car x) 3)) (progn (setq oldstr (strcat oldstr (cdr x))) ) ) ) (setq newstr (fr_txt_replace oldstr find replace nil)) (if (/= newstr oldstr) (progn ; update MTEXT entity. This may be tricky. Write ; out blocks of 250 characters at a time. Start ; with "3" subrecords if there are going to be ; two or more required. Always end with a "1" ; subrecord. (setq cnt (1+ (fix (/ (strlen newstr) 251.0)))) ; cnt=1 if just one "1" subrecord takes care of the MTEXT ; character string. cnt=2 means a "3" and a "1". cnt=3 ; means two "3's" and a "1"... and so on. (princ "\nOLD=") (princ oldstr) (princ "\nNEW=") (princ newstr) (if (not chg_cnt)(setq chg_cnt 0)) (setq chg_cnt (1+ chg_cnt)) ; Now rebuild the MTEXT character string records (setq newed nil) (foreach x ed (if (OR (= (car x) 1)(= (car x) 3)) (progn ; hit first part of the MTEXT text string. Output ; all of the revised character string. (if (> cnt 0) (progn ; output it now (setq ixx 0) (repeat cnt (setq chars_to_output (substr newstr 1 250)) (setq newstr (substr newstr 251)) ; strip off what is being output (setq code 3) (setq ixx (1+ ixx)) (if (= ixx cnt) (progn ; this is the last block of characters. (setq code 1) ) ) (setq newed (cons (cons code chars_to_output) newed)) ) (setq cnt 0) ; flag to not repeat ) ) ) ; ELSE (progn ; anything other than char subrecord (setq newed (cons x newed)) ) ) ) ; Now update the MTEXT entity with the new collection ; of subrecord data (setq newed (reverse newed)) ; put back in original order (entmod newed) (entupd mtext_en) ) ) (setq ix (1+ ix)) ) (setq ss nil) ) ) ; Give some indication of change count (if (not chg_cnt) (princ "\nNo changes") ; ELSE (progn (princ "\nMTEXT modified: ") (princ chg_cnt) ) ) ) (Princ "\nType MTEXT_FR [Enter] to run.") (princ)
If you’d like to test this utility, cut and paste from above into an ascii text file called mtext_fr.lsp. Back up your drawing!!! Then, APPLOAD the “.lsp” file. Type MTEXT_FR [Enter] at the command prompt.
UPDATE: customer reports success. He modified the above utility. He hard-coded the find/replace text substrings instead of having the (getstring…) calls to prompt for the values [in other words, he used (setq find "somestring") and (setq replace "newstring")]. Then he created a two-line script file and referenced this in AutoCAD Electrical’s “Project-Wide Utilities” command. Ran it against his project… issue resolved.