Regex matching across single and multiple lines - regex

Lets say I am using Visual Studio's Find and Replace tool and I want to find and comment out every instance of Console.WriteLine(...). However, I can wind up with situations where Console.WriteLine(...) goes across multiple lines like so:
Console.WriteLine("Adding drive to VM with ID: {0}. Drive HostVMID is {1}",
vm.ID, drive.HostVmId);
These can go on for 2, 3, 4, etc lines and finally end with ); to close the statement. Then I can have other lines that are immediately followed by important blocks of code:
Console.WriteLine("Creating snapshot for VM: {0} {1}", dbVm.ID, dbVm.VmName);
dbContext.Add(new RTVirtualMachineSnapshot(dbVm));
So what I want to do is come up with a regex statement that will find both the first type of instances of Console.WriteLine as well as simple single-line instances of it.
The Regex that I got from another user was:
Console\.writeline(?>.+)(?<!;)
Which will match any line that contains Console.WriteLine but does not end with a semicolon. However I need it to continue on until it finally does reach a closing parenthesis followed by a semicolon.
Ive tried the following regex:
(Console\.writeline(?>.+)(?<!\);)
However I think thats incorrect because it still only matches the first line and doesnt capture the following lines when the writeline spans multiple lines.
At the end of the day I want to be able to capture a full Console.writeline statement regardless of how many lines it spans using Visual Studio's find and replace feature and I am a little confused on the regex I would need to use to do this.

I guess you could try this which just looks for a pseudo termination,
but does not take into account string quotes.
(?s)\b(Console\s*\.\s*WriteLine\s*\((?:(?!\)\s*;).)*\)\s*;)
Formatted:
(?s)
\b
( # (1 start)
Console \s* \. \s* WriteLine \s* \(
(?:
(?! \) \s* ; )
.
)*
\) \s* ;
) # (1 end)
add:
If you cannot use dot-all modifier (?s) and Dot-all options are not available (should be),
substitute [\S\s] for the dot.
Then just substitute "/** $1 **/" to comment it out.

Related

How to find and replace a particular character but only if it is in quotes?

Problem:
I have thousands of documents which contains a specific character I don't want. E.g. the character a. These documents contain a variety of characters, but the a's I want to replace are inside double quotes or single quotes.
I would like to find and replace them, and I thought using Regex would be needed. I am using VSCode, but I'm open to any suggestions.
My attempt:
I was able to find the following regex to match for a specific string containing the values inside the ().
".*?(r).*?"
However, this only highlights the entire quote. I want to highlight the character only.
Any solution, perhaps outside of regex, is welcome.
Example outcomes:
Given, the character is a, find replace to b
Somebody once told me "apples" are good for you => Somebody once told me "bpples" are good for you
"Aardvarks" make good kebabs => "Abrdvbrks" make good kebabs
The boy said "aaah!" when his mom told him he was eating aardvark => The boy said "bbbh!" when his mom told him he was eating aardvark
Visual Studio Code
VS Code uses JavaScript RegEx engine for its find / replace functionality. This means you are very limited in working with regex in comparison to other flavors like .NET or PCRE.
Lucky enough that this flavor supports lookaheads and with lookaheads you are able to look for but not consume character. So one way to ensure that we are within a quoted string is to look for number of quotes down to bottom of file / subject string to be odd after matching an a:
a(?=[^"]*"[^"]*(?:"[^"]*"[^"]*)*$)
Live demo
This looks for as in a double quoted string, to have it for single quoted strings substitute all "s with '. You can't have both at a time.
There is a problem with regex above however, that it conflicts with escaped double quotes within double quoted strings. To match them too if it matters you have a long way to go:
a(?=[^"\\]*(?:\\.[^"\\]*)*"[^"\\]*(?:\\.[^"\\]*)*(?:"[^"\\]*(?:\\.[^"\\]*)*"[^"\\]*(?:\\.[^"\\]*)*)*$)
Applying these approaches on large files probably will result in an stack overflow so let's see a better approach.
I am using VSCode, but I'm open to any suggestions.
That's great. Then I'd suggest to use awk or sed or something more programmatic in order to achieve what you are after or if you are able to use Sublime Text a chance exists to work around this problem in a more elegant way.
Sublime Text
This is supposed to work on large files with hundred of thousands of lines but care that it works for a single character (here a) that with some modifications may work for a word or substring too:
Search for:
(?:"|\G(?<!")(?!\A))(?<r>[^a"\\]*+(?>\\.[^a"\\]*)*+)\K(a|"(*SKIP)(*F))(?(?=((?&r)"))\3)
^ ^ ^
Replace it with: WHATEVER\3
Live demo
RegEx Breakdown:
(?: # Beginning of non-capturing group #1
" # Match a `"`
| # Or
\G(?<!")(?!\A) # Continue matching from last successful match
# It shouldn't start right after a `"`
) # End of NCG #1
(?<r> # Start of capturing group `r`
[^a"\\]*+ # Match anything except `a`, `"` or a backslash (possessively)
(?>\\.[^a"\\]*)*+ # Match an escaped character or
# repeat last pattern as much as possible
)\K # End of CG `r`, reset all consumed characters
( # Start of CG #2
a # Match literal `a`
| # Or
"(*SKIP)(*F) # Match a `"` and skip over current match
)
(?(?= # Start a conditional cluster, assuming a positive lookahead
((?&r)") # Start of CG #3, recurs CG `r` and match `"`
) # End of condition
\3 # If conditional passed match CG #3
) # End of conditional
Three-step approach
Last but not least...
Matching a character inside quotation marks is tricky since delimiters are exactly the same so opening and closing marks can not be distinguished from each other without taking a look at adjacent strings. What you can do is change a delimiter to something else so that you can look for it later.
Step 1:
Search for: "[^"\\]*(?:\\.[^"\\]*)*"
Replace with: $0Я
Step 2:
Search for: a(?=[^"\\]*(?:\\.[^"\\]*)*"Я)
Replace with whatever you expect.
Step 3:
Search for: "Я
Replace with nothing to revert every thing.
/(["'])(.*?)(a)(.*?\1)/g
With the replace pattern:
$1$2$4
As far as I'm aware, VS Code uses the same regex engine as JavaScript, which is why I've written my example in JS.
The problem with this is that if you have multiple a's in 1 set of quotes, then it will struggle to pull out the right values, so there needs to be some sort of code behind it, or you, hammering the replace button until no more matches are found, to recurse the pattern and get rid of all the a's in between quotes
let regex = /(["'])(.*?)(a)(.*?\1)/g,
subst = `$1$2$4`,
str = `"a"
"helapke"
Not matched - aaaaaaa
"This is the way the world ends"
"Not with fire"
"ABBA"
"abba",
'I can haz cheezburger'
"This is not a match'
`;
// Loop to get rid of multiple a's in quotes
while(str.match(regex)){
str = str.replace(regex, subst);
}
const result = str;
console.log(result);
Firstly a few of considerations:
There could be multiple a characters within a single quote.
Each quote (using single or double quotation marks) consists of an opening quote character, some text and the same closing quote character. A simple approach is to assume that when the quote characters are counted sequentially, the odd ones are opening quotes and the even ones are closing quotes.
Following point 2, it could be worth some further thought on whether single-quoted strings should be allowed. See the following example: It's a shame 'this quoted text' isn't quoted. Here, the simple approach would think there were two quoted strings: s a shame and isn. Another: This isn't a quote ...'this is' and 'it's unclear where this quote ends'. I've avoided attempting to tackle these complexities and gone with the simple approach below.
The bad news is that point 1 presents a bit of a problem, as a capturing group with a wildcard repeat character after it (e.g. (.*)*) will only capture the last captured "thing". But the good news is there's a way of getting around this within certain limits. Many regex engines will allow up to 99 capturing groups (*). So if we can make the assumption that there will be no more than 99 as in each quote (UPDATE ...or even if we can't - see step 3), we can do the following...
(*) Unfortunately my first port of call, Notepad++ doesn't - it only allows up to 9. Not sure about VS Code. But regex101 (used for the online demos below) does.
TL;DR - What to do?
Search for: "([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*([^a"]*)a*"
Replace with: "\1\2\3\4\5\6\7\8\9\10\11\12\13\14\15\16\17\18\19\20\21\22\23\24\25\26\27\28\29\30\31\32\33\34\35\36\37\38\39\40\41\42\43\44\45\46\47\48\49\50\51\52\53\54\55\56\57\58\59\60\61\62\63\64\65\66\67\68\69\70\71\72\73\74\75\76\77\78\79\80\81\82\83\84\85\86\87\88\89\90\91\92\93\94\95\96\97\98\99"
(Optionally keep repeating steps the previous two steps if there's a possibility of > 99 such characters in a single quote until they've all been replaced).
Repeat step 1 but replacing all " with ' in the regular expression, i.e: '([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*([^a']*)a*'
Repeat steps 2-3.
Online demos
Please see the following regex101 demos, which could actually be used to perform the replacements if you're able to copy the whole text into the contents of "TEST STRING":
Demo for double quotes
Demo for single quotes.
If you can use Visual Studio (instead of Visual Studio Code), it is written in C++ and C# and uses the .NET Framework regular expressions, which means you can use variable length lookbehinds to accomplish this.
(?<="[^"\n]*)a(?=[^"\n]*")
Adding some more logic to the above regular expression, we can tell it to ignore any locations where there are an even amount of " preceding it. This prevents matches for a outside of quotes. Take, for example, the string "a" a "a". Only the first and last a in this string will be matched, but the one in the middle will be ignored.
(?<!^[^"\n]*(?:(?:"[^"\n]*){2})+)(?<="[^"\n]*)a(?=[^"\n]*")
Now the only problem is this will break if we have escaped " within two double quotes such as "a\"" a "a". We need to add more logic to prevent this behaviour. Luckily, this beautiful answer exists for properly matching escaped ". Adding this logic to the regex above, we get the following:
(?<!^[^"\n]*(?:(?:"(?:[^"\\\n]|\\.)*){2})+)(?<="[^"\n]*)a(?=[^"\n]*")
I'm not sure which method works best with your strings, but I'll explain this last regex in detail as it also explains the two previous ones.
(?<!^[^"\n]*(?:(?:"(?:[^"\\\n]|\\.)*){2})+) Negative lookbehind ensuring what precedes doesn't match the following
^ Assert position at the start of the line
[^"\n]* Match anything except " or \n any number of times
(?:(?:"(?:[^"\\\n]|\\.)*){2})+ Match the following one or more times. This ensures if there are any " preceding the match that they are balanced in the sense that there is an opening and closing double quote.
(?:"(?:[^"\\\n]|\\.)*){2} Match the following exactly twice
" Match this literally
(?:[^"\\\n]|\\.)* Match either of the following any number of times
[^"\\\n] Match anything except ", \ and \n
\\. Matches \ followed by any character
(?<="[^"\n]*) Positive lookbehind ensuring what precedes matches the following
" Match this literally
[^"\n]* Match anything except " or \n any number of times
a Match this literally
(?=[^"\n]*") Positive lookahead ensuring what follows matches the following
[^"\n]* Match anything except " or \n any number of times
" Match this literally
You can drop the \n from the above pattern as the following suggests. I added it just in case there's some sort of special cases I'm not considering (i.e. comments) that could break this regex within your text. The \A also forces the regex to match from the start of the string (or file) instead of the start of the line.
(?<!\A[^"]*(?:(?:"(?:[^"\\]|\\.)*){2})+)(?<="[^"]*)a(?=[^"]*")
You can test this regex here
This is what it looks like in Visual Studio:
I am using VSCode, but I'm open to any suggestions.
If you want to stay in an Editor environment, you could use
Visual Studio (>= 2012) or even notepad++ for quick fixup.
This avoids having to use a spurious script environment.
Both of these engines (Dot-Net and boost, respectively) use the \G construct.
Which is start the next match at the position where the last one left off.
Again, this is just a suggestion.
This regex doesn't check the validity of balanced quotes within the entire
string ahead of time (but it could with the addition of a single line).
It is all about knowing where the inside and outside of quotes are.
I've commented the regex, but if you need more info let me know.
Again this is just a suggestion (I know your editor uses ECMAScript).
Find (?s)(?:^([^"]*(?:"[^"a]*(?=")"[^"]*(?="))*"[^"a]*)|(?!^)\G)a([^"a]*(?:(?=a.*?")|(?:"[^"]*$|"[^"]*(?=")(?:"[^"a]*(?=")"[^"]*(?="))*"[^"a]*)))
Replace $1b$2
That's all there is to it.
https://regex101.com/r/loLFYH/1
Comments
(?s) # Dot-all inine modifier
(?:
^ # BOS
( # (1 start), Find first quote from BOS (written back)
[^"]*
(?: # --- Cluster
" [^"a]* # Inside quotes with no 'a'
(?= " )
" [^"]* # Between quotes, get up to next quote
(?= " )
)* # --- End cluster, 0 to many times
" [^"a]* # Inside quotes, will be an 'a' ahead of here
# to be sucked up by this match
) # (1 end)
| # OR,
(?! ^ ) # Not-BOS
\G # Continue where left off from last match.
# Must be an 'a' at this point
)
a # The 'a' to be replaced
( # (2 start), Up to the next 'a' (to be written back)
[^"a]*
(?: # --------------------
(?= a .*? " ) # If stopped before 'a', must be a quote ahead
| # or,
(?: # --------------------
" [^"]* $ # If stopped at a quote, check for EOS
| # or,
" [^"]* # Between quotes, get up to next quote
(?= " )
(?: # --- Cluster
" [^"a]* # Inside quotes with no 'a'
(?= " )
" [^"]* # Between quotes
(?= " )
)* # --- End cluster, 0 to many times
" [^"a]* # Inside quotes, will be an 'a' ahead of here
# to be sucked up on the next match
) # --------------------
) # --------------------
) # (2 end)
"Inside double quotes" is rather tricky, because there are may complicating scenarios to consider to fully automate this.
What are your precise rules for "enclosed by quotes"? Do you need to consider multi-line quotes? Do you have quoted strings containing escaped quotes or quotes used other than starting/ending string quotation?
However there may be a fairly simple expression to do much of what you want.
Search expression: ("[^a"]*)a
Replacement expression: $1b
This doesn't consider inside or outside of quotes - you have do that visually. But it highlights text from the quote to the matching character, so you can quickly decide if this is inside or not.
If you can live with the visual inspection, then we can build up this pattern to include different quote types and upper and lower case.

Reg Exp For javascript

hiii every body
i need help for this reg expression pattern
i need to search on text for this
( anything ) -
check this example to every statement
i need to detect if this pattern exist on the statement that i will feed to my function and get the matched string
be careful for space and braces and dash and anything mean any content Arabic or English no matter what is it , just pattern start with ( and end to - and if this pattern exist on the first statement so it say exist
thanks for every one .....
The task can be easier if it is described in a way "guiding to"
the proper solution. Let's rewrite your task the following way:
The text to match:
Should start with ( and a space.
Then there is a non-empty sequence of chars other than )
(the text between parentheses, which you wrote as anything).
The last part is a space, ), another space and -.
Having such a description, it is obvious, that the pattern should be:
\( [^)]+ \) -
where each fragment: \(, [^)]+ and \) - expresses each of the
above conditions.
Note: If spaces after ( and before ) are optional, then you can express it
with ? after each such space, and then the whole regex will change to
\( ?[^)]+ ?\) -.

Find strings not matching pattern in Notepad++ regex

I want to use Notepad++ regex to find all strings that do not match a pattern.
Sample Input Text:
{~Newline~}{~Indent,4~}{~Colour,Blue~}To be or not to be,{~Newline~}{~Indent,6~}
{~Colour,Green~}that {~StartItalic~}is{~EndItalic~} the question.{~EndDocument~}
The parts between {~ and ~} are markdown codes. Everything else is plaintext. I want to find all strings which do not have the structure of the markdown, and insert the code {~Plain~} in front of them. The result would look like this:
{~Newline~}{~Indent,4~}{~Colour,Blue~}{~Plain~}To be or not to be,{~Newline~}{~Indent,6~}{~Colour,Green~}{~Plain~}that {~StartItalic~}{~Plain~}is{~EndItalic~}{~Plain~} the question.{~EndDocument~}
The markdown syntax is open-ended, so I can't just use a list of possible codes to not process.
I could insert {~Plain~} after every ~}, then delete every {~Plain~} that's followed by {~, but that seems incredibly clunky.
I hope this works with the current version of Notepad++ (don't have it right now).
Matching with:
~}((?:[^{]|(?:{[^~]))+){~
and then replacing by
~}{~Plain~}$1{~
might work. The first group should capture everything between closing ~} and the next {~. It will also match { and } in the text, as long as they are not part of an opening tag {~.
EDIT Additional explanation, so you can modify it better:
~} end of previous tag
( start of the "interesting" group that contains text
(?: non-capturing group for +
[^{] everything except opening braces
| OR
(?:
{ opening brace followed by ...
[^~] ... some character which is not `~`
)
)+ end of non-capturing group for +, repeated 1 or more times
) end of the "interesting" group
{~ start of the next tag
Here is an interactive example: regex101 example
You need to use Negative Lookahead. This regex will match all ~} occurrences, so you can just replace them with ~}{~Plain~}:
~}(?!{~|$)
If you don't want to match the space in {~Indent,6~} {~Colour,Green~}, just use this:
~}(?!{~|$| )

How to replace a parentheses at the start of a line in textpad

I know that the character ^ finds a character at the start of a line, but I need to delete a left parentheses at the start of many lines. When I try to do a replace, I receive the following error:
Unmatched marking parenthesis ( or \(. The error occurred while parsing the regular expression: '^(>>>HERE>>>'.
What is the correct syntax when searching for the character ( at the start of a line?
You need to escape the parenthesis with a slash, \(
Textpad regex is pretty frustrating when you need to match parentheses, as both (-) and \(-\) make matching groups. (Very old versions of Textpad had only \(-\) as capturing markers, as unslashed parentheses were not "special".)
You can get around this by using a character class with one item, the relevant parenthesis. For example, when there's an error in java, a stack trace appears that looks in part like this:
at java.util.Formatter.format(Formatter.java:2455)
at java.lang.String.format(String.java:2940)
at day11$boardWrapper.toString(day11.java:85)
When running code through Textpad, you need a regex to catch the filename and line number. The pattern below captures those in \1 and \2, respectively, allowing me to configure the tool to let me jump directly to the offending line.
at.+[(]([^:]+):([0-9]+)[)]
at.+ offending function name
[(] (
( ) capturing group 1
[^:]+ anything not a colon (i.e., the filename)
: colon
( ) capturing group 2
[0-9]+ digits (line number)
[)] )

Multiline selection of blocks with ID at the end of each block with regular expression

I have regular expression:
BEGIN\s+\[([\s\S]*?)END\s+ID=(.*)\]
which select multiline text and ID from text below. I would like to select only IDs with prefix X_, but if I change ID=(.*) to ID=(X_.*) begin is selected from second pair not from third as I need. Could someone help me to get correct expression please?
text example:
BEGIN [
text a
END ID=X_1]
BEGIN [
text b
text c
END ID=Y_1]
text aaa
text bbb
BEGIN [
text d
text e
END ID=X_2]
text xxx
BEGIN [
text bbb
END ID=X_3]
It isn't the .* that's gobbling everything up as people keep saying, it's the [\s\S]*?. .* can't do it because (as the OP said) the dot doesn't match newlines.
When the END\s+ID=(X_.*)\] part of your regex fails to match the last line of the second block, you're expecting it to abandon that block and start over with the third one. That's what it have to do to make the shortest match.
In reality, it backtracks to the beginning of the line and lets [\s\S]*? consume it instead. And it keeps on consuming until it finds a place where END\s+ID=(X_.*)\] can match, which happens to be the last line of the third block.
The following regex avoids that problem by matching line by line, checking each one to see if it starts with END. This effectively confines the match to one block at a time.
(?m)^BEGIN\s+\[[\r\n]+((?:(?!END).*[\r\n]+)*)END\s+ID=(X_.*)\]
Note that I used ^ to anchor each match to the beginning of a line, so I used (?m) to turn on multiline mode. But I did not--and you should not--turn on single-line/DOTALL mode.
Assuming there aren't any newlines inside a block and the BEGIN/END statements are the first non-space of their line, I'd write the regex like this (Perl notation; change the delimiters and remove comments, whitespaces and the /x modifier if you use a different engine)
m{
\n \s* BEGIN \s+ \[ # match the beginning
( (?!\n\s*\n) .)*? # match anything that isn't an empty line
# checking with a negative look-ahead (?!PATTERN)
\n \s* END \s+ ID=X_[^\]]* \] # the ID may not contain "]"
}sx # /x: use extended syntax, /s: "." matches newlines
If the content may be anything, it might be best to create a list of all blocks, and then grep through them. This regex matches any block:
m{ (
BEGIN \s+ \[
.*? # non-greedy matching is important here
END \s+ ID=[^\]]* \] # greedy matching is safe here
) }xs
(add newlines if wanted)
Then only keep those matches that match this regex:
/ID = X_[^\]]* \] $/x # anchor at end of line
If we don't do this, backtracking may prevent a correct match ([\s\S]*? can contain END ID=X_). Your regex would put anything inside the blocks until it sees a X_.*.
So using BEGIN\s+\[([/s/S]*?)END\s+ID=(.*?)\] — note the extra question mark — one match would be:
BEGIN [
text b
text c
END ID=Y_1]
text aaa
text bbb
BEGIN [
text d
text e
END ID=X_2]
…instead of failing at the Y_. A greedy match (your unchanged regex) should result in the whole file being matched: Your (.*) eats up all characters (until the end of file) and then goes back until it finds a ].
EDIT:
Should you be using perls regex engine, we can use the (*FAIL) verb:
/BEGIN\s+\[(.*?)END\s+ID=(X_[^\]]*|(*FAIL))\]/s
"Either have an ID starting with X_ or the match fails". However, this does not solve the problem with END ID=X_1]-like statements inside your data.
Change your .* to a [^\]]* (i.e. match non-]s), so that your matches can't spill over past an END block, giving you something like BEGIN\s+\[([^\]]*?)END\s+ID=(X_[^\]]*)\]