12

Would like to perform a search and replace in a file with 12000 lines.

Specifically, if an occurrence of ^ SetFontSize 28 exists after a ^Hide block and before the next ^Hide or ^Show, change 28 to 18.

Here's a snippet from the original file.

Hide # Gear - Endgame
    ItemLevel >= 77
    Rarity = Magic
    LinkedSockets >= 3
    BaseType "Runic Hatchet"
    SetTextColor 140 190 255 # Magic Item Highlight
    SetFontSize 28

Hide # Gear - Endgame
    ItemLevel >= 77
    Rarity = Magic
    Sockets >= 3
    BaseType "Runic Hatchet"
    SetTextColor 140 190 255 # Magic Item Highlight
    SetFontSize 28

Show # Gear - Endgame
    ItemLevel >= 83
    Rarity = Normal
    Sockets < 3
    BaseType "Tiger Hook"
    SetTextColor 240 240 240 # Normal Item Highlight
    SetBackgroundColor 70 70 70
    SetFontSize 28

The end result for one of the Hide blocks should look like this:

Hide # Gear - Endgame
    ItemLevel >= 77
    Rarity = Magic
    LinkedSockets >= 3
    BaseType "Runic Hatchet"
    SetTextColor 140 190 255 # Magic Item Highlight
    SetFontSize 18

Replacing SetFontSize 28 to SetFontSize 18, but only if it appears in a ^Hide block.

The nasty regex I tried was: :%s/^Hide\(.*\)SetFontSize 28$/Hide\1SetFontSize 18/g

But was told pattern not found. Please let me know if any additional information is required or if my request is unclear.

2
  • 5
    Does every Hide block have a SetFontSize line (whatever be the value)? If so, you could use :%s/Hide\_.\{-\}SetFontSize \zs28/18/
    – muru
    Feb 20, 2016 at 18:32
  • 2
    @muru whatever be the value would cause trouble, your solution only works if every Hide block have a SetFontSize line and it's value is exactly 28, otherwise, it matches until 28 of another block.
    – dedowsdi
    Jun 8, 2019 at 6:08

2 Answers 2

5

It looks like your nasty regex was not nasty enough... :-)

Search Section

The search would have to be changed to this:

^Hide\(\(\(Show\|Hide\)\@!\_.\)*\)SetFontSize 28

This includes quite a few uncommon things and so many parenthesis... Let's see what we have in there:

The Caret (^)

The caret is used to mean beginning of the line. I think we're already familiar with this one.

An important point, the ^ does not work except as the very first character in your pattern. After the it is taken verbatim. To include a beginning of line within your expression you need to use \_^. However, in our situation we did not need that.

(There is a similar phenomena with $ and \_$)

The first and last parenthesis (\( ... \))

The first and last parenthesis are used on their own which means it will grab whatever appears inside and set it in parameter \1. You already used that in your own regex, so I'll assume you also are familiar with this one.

The second set of parenthesis

As you may notice, there is a second set of parenthesis followed by an asterisk \( ... \)*. This means we are looking for whatever matches any number of times. This is the usual way of using the asterisk so you should be familiar with it.

The third set of parenthesis, OR, and \_.

Yes, there are actually three parenthesis before the word Show. This last set is necessary for two reasons: the \| and the following @!.

In regard to the OR operation, you should already be familiar with it.

Show\|Hide    or    Hide\|Show

The order does not matter here. The \ is necessary in front of the | to work in vim.

The parenthesis around this expression allows us to follow the expression by something. Here the @!.

\(Show\|Hide\)@!

This one is much less familiar. It means if not matched. The use of this is not very easy, though, but you need to follow that expression with what you want to extract which should not match said expression. This is why we have \_. behind that pattern.

The \_. means match anything whatsoever. Contrary to the . on its own, which doesn't match the \n character. In other words, we match any character on any number of lines unless it matches Show or Hide.

Note that the parenthesis around that expression are also important as is the asterisk, so this whole thing is really what makes it work:

\(\(Show\|Hide\)@!\_.\)*

a.k.a. match whatever up to the next Show or Hide characters (note that it would also match Showing, Shower, HideMe, etc. you should be able to use \< and \> if it is necessary to match the word exactly.)

Side Note: to search on multiple lines, it is also possible to use the \n character in the pattern. However, it's not as versatile than the \_. pattern.

SetFontSize 28

Now the section has to include SetFontSize 28 as well. Just like you had in your regex. If no SetFontSize 28 appears in that section, try the search again on the next section.

Because of the negation above (the match except Show or Hide) the search doesn't leak to the next section, taking the risk of messing it up.

Replacement Section

The replacement is just the same as you had it:

.../Hide\1SetFontSize 18/

We use the parenthesis in the Search so the \1 works as expected.

Complete Search and Replace

The resulting patterns goes like this:

:%s/^Hide\(\(\(Show\|Hide\)@!\_.\)*\)SetFontSize 28/Hide\1SetFontSize 18/

The \(Show\|Hide\) should include all the possible headers.

Sources

Regex to match any character including newline (\_.\{-})

Search for lines not containing pattern and other helpful searches (@!)

Vim documentation: pattern (\_^)

1
  • 1
    I like the ()*, my version of your answer : %s/\v^Hide.*\n(\s+.*\n)*\s*SetFontSize\s+\zs28/16.
    – dedowsdi
    Jun 8, 2019 at 6:34
3

One way to solve this would be to use :global that generates a range output.

Typical use of the global command would be

:[range]g/{pattern}/[cmd]

It also has the option of making this pattern generate a range rather than a single line match by using , in the form of

:[range]g/{first pattern}/,/{second pattern}/[cmd]

This generates a range that is applied to the command.

For your example the first pattern would be matching the first Hide entry and the second pattern is either Hide, Show, or the end of the file (assuming you want that last Hide case).

:g/Hide/,/\(Hide\|Show\|\%$\)/s/SetFontSize 28/SetFontSize 18/

The first regex is simple, /Hide/. The second regex contains a few interesting parts.

  • \( and \) creates a grouping of atoms to match
  • \| is the OR operation
  • \%$ represents the end of the file

Once we've defined our ranges, we apply substitute with a pattern and a string just like you normally would.

Please note that the regex used in this example is very basic. You'll want to make sure that your identifiers for the beginning and end of the of the range captures the right areas.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.