2

(EDIT: In the original question, I had accidentally used \w instead of \<. Comments and answers with \w should be interpreted as using \< as well.)

In my LaTeX source files, section headings have an initial uppercase on each first word, but all lowercase on subsequent words. I want to convert all words in each section heading to initial uppercase. Section headings have the form "*section{Here is my section name}". That is, I want to replace this:

Some text.
\section{New information}
Some other text.
\subsection{More cool stuff}

with

Some text.
\section{New Information}
Some other text.
\subsection{More Cool Stuff}

I am currently using this to perform the task:

:g/section{/s/\<\(.\)/\u&/gc

This globally searches for lines containing "section{" and then searches within the line for characters that begin words, replacing the found character with its uppercase variant, with c allowing me to reject the change if I want.

However, this means that I always first find the "s" at the beginning of the sectioning command--\section, or \subsection, etc., and I have to hit "n" to reject the substitution and move on to the next match.

In some contexts it's possible to search and move to the end of the match, using e:

/section{/e

places the cursor on the "{".

However, I don't see a way to mix the e suffix functionality with the combination of a global line search and a within-line search and replace.

:g/section{/es/\<\(.\)/\u&/gc

generates the error "Not an editor command" on "es".

:g/section{/e s/\<\(.\)/\u&/gc

results in a new buffer with the message '"s/\w(.)/\u&/gc" [NEW DIRECTORY]'. I don't know what Vim thinks I'm trying to do, but it's not what I want.

Is there a way to find a line and then search/replace only within part of the line?

  • Why are you using :g/{pattern}/{cmd}? Just use :h :s. You just want blabla section{This is a section} to become blabla section{ This Is A Section}, right? Look at :h /\zs and :h /\ze. If I'm wrong, add a 3-4 lines of test code, it would help. – klaus Mar 19 at 17:18
  • Thanks @klaus. I added some example text. I don't yet see how to use \zs or \ze to do what I want (see my comment in response to Ben Noble's answer. – Mars Mar 20 at 2:57
  • I figured it out. See comment on Ben Noble's answer. Thanks @klaus. \zs was helpful. – Mars Mar 20 at 3:13
3

If I understand the question correctly, you want to convert lines of the form:

\section{Here is my section name}

to

\section{Here Is My Section Name}

One way to do this is using a look-behind:

s/\%(\\section{[^}]*\)\@<=\<\w/\u&/gc

Breaking this down,

\%(                             start grouping
      \\section{[^}]*           match \section{ followed by anything except }
\)\@<=                          ensure matching at this position (not part of the match) 
\<\w                            match start of words (first character)

\u&                             upper case the match

The way this works is vim stores the group preceding the \@<=, then for each match of \<\w, looks back character by character to see if it can match what is in the look behind. This means it is often slow, as the help mentions. It is also distinct from using \zs, which would first match \section{Here Is My Section, restart the match, then match \<\w.

  • Why would vim have :h /\zs if it has such powerful look around regex atoms. Just made me confused. – klaus Mar 20 at 14:49
  • \zs is usually much faster and you should use it where possible – Mass Mar 20 at 18:49
  • 2
    note that it is difficult or impossible to use \zs in this case – Mass Mar 20 at 18:58
  • Thanks Mass. This works perfectly. (I also want to match "\subsection{" and "\subsubsection", but that's fixed simply by removing \\ in your suggestion.) After reading the docs for \@<= I am puzzled by [^}]*. The way I interpret \@<=, this should mean that I match the entire rest of the line up until the first closing brace, and then start looking for initial characters with \<\w--i.e. I am then searching for word-beginnings within the string "}", which obviously would fail. That is clearly a misinterpretation, since your search expression works and it doesn't without `[^}]*. – Mars Mar 20 at 19:18
  • @Mars, I added slightly more explanation. That's a misinterpretation because vim actually first matches the \<\w, then starts checking what is in the look-behind (opposite of what you described). If you don't include [^}]* you'd never be able to match the T in \section{Some Thing} because the look behind match needs to abut the start of the word. – Mass Mar 20 at 19:38
2

Limit the match using \zs and \ze as Klaus suggested and adjusting per Mars:

:g/section{/s/[^{\\]\zs\<\(.\)/\u&/gc

The g/section/ is needed because we want to do this only on the section{ lines, but section{ should not be involved in the replacement match.

Then [^{\\]\zs means that we don't try to replace after the initial \ on the line, nor after the { (since we already have an uppercase character after the brace), but then start searching for word-beginnings after that.

The \zs is needed because otherwise the [^{\\] causes the interword space to be what's matched and then "uppercased", which would cause no change.

  • Thanks to you and @Klaus for the suggestion. I didn't know about \zs and \ze. However, %s/section{\zs\w\(.\)/\u&/gc doesn't do what I was trying to do. The \zs causes the uppercase replacement to take place on the first character after section{, but that is the only thing that matches. The g on the end of the line does not cause subsequent words on the line to be uppercased. I assume this is because as far as Vim is concerned, there is only one match on the line: The one that begins with "section{". I will continue experimenting, though. – Mars Mar 20 at 2:43
  • \w in the preceding comment should be \<, but the same comment applies. – Mars Mar 20 at 2:51
  • OK, I have figured out a solution: :g/section{/s/[^{\\]\zs\<\(.\)/\u&/gc . The g/section/ is needed because I want to do this only on the "section{" lines, but "section{" should not be involved in the replacement match. Then [^{\\]\zs means that I don't try to replace after the initial "\" on the line, nor after the "{" (since I already have an uppercase character after the brace), but then start searching for word-beginnings after that. The \zs is needed because otherwise the [^{\\] causes the interword space to be what's matched and then "uppercased", which would cause no change. – Mars Mar 20 at 3:09
  • @Mars yes i do see how that’s a problem. Im going to edit my answer to use your solution, but I would consider Mass’s: it’s quite good. – D. Ben Knoble Mar 20 at 3:16
1

You can use a sub-replace-expression to do the title casing:

:%s/.*{\zs.*\ze}/\=substitute(submatch(0), '\<\w', '\u&', 'g')/

Overview:

Use \zs & \ze to set the match to be inside the curly brackets ({\zs.*\ze}). Then do a substitute on the match (submatch(0)) where every start of a word (\<\w)) is capitalized (\u&)

For more help see:

:h /\zs
:h sub-replace-expression
:h /\<
:h submatch()
:h substitute 
  • Thanks Peter--this is the logic that I was looking for. There's a typo, "\" before "}", and I need to replace the initial .* with section--otherwise I uppercase other LaTeX commands. I can't figure out how to get the effect of 'c', though, i.e. to get Vim to ask whether to replace each initial letter. I didn't mention it in the original question, because I wanted to keep things simple, but I actually don't want to capitalize every word inside the braces; this is title case, so some words such as "the" must remain all lower-case. (I won't program a title-case grammar into a search!) – Mars Mar 20 at 18:57
  • 1
    Using the confirm flag per word is going to be tricky. I think you might be better off using a look-behind and using gU + gn. You can repeat with . and skip with n. Vimcasts: Operating on search matches using gn – Peter Rincker Mar 20 at 19:01
1

Use a Macro!

You already have three excellent answers explaining how to achieve this using regular expressions. However, I thought I'd offer a solution that uses a completely different Vim feature. viz. a recording, or macro.

In many circumstances, a macro is a terrific alternative to a complex regular expression, and, to my mind, if you don't already have a good grasp of intermediate/advanced regular expression constructs, then macros are easier to pick up, because they just use all the editing commands you already know.

Getting your head around how to ensure that the macro you record will work in all the places you intend to play it back does take a little bit of practice, but, personally, I found this easier than learning how all Vim's various regexp atoms work, and remembering the arcane syntax required to invoke them.

The Solution

Here's how to uppercase all your headings with a recursive macro.

First, place your cursor on the beginning of the word New, and then type the following:

qqqqqgUlhellbw@qq

This will record a recursive macro that updates all the words in the current section header to begin with an uppercase character. To play this back, just run the command:

:g/section/norm!f{l@q

Amazing! So how does this work?

Explanation

Let's break down the macro into its constituent parts:

qqqqq {some stuff} @qq

The beginning and end of our recording is just boilerplate for recording a recursive macro. You don't need to know how it works to use it, but how it does work is straightforward:

qqq

The first three q characters starts recording a macro into register q and then immediately ends the recording, so the macro ends up empty. This will become important when we get to the end of our recording

qq

Now we start recording our real macro into register q

{some stuff}

Then we type our macro steps. We'll come back to this.

@qq

Finally, we type @q to make our macro recursive, and then type q to end the recording.

When running the final macro, the @q will cause our macro to run again and again until it hits an "error", but when recording the macro, the q register is empty (because we specifically emptied it as the first thing we did), so typing it does nothing.

Now lets take a look at the commands in the middle that actually do the work. We need to write a series of commands that will a). perform one change, and b). move the cursor to the next location where we want the change to be made, throwing some kind of error if we've reached the last location. So:

gUhellbw

First we need to uppercase the first character:

gU

Done!

Next, we need to check if we're finished and jump to the next location. The easiest way to check if we're finished is to jump to the end of the current word and then attempt to move the cursor right twice. If we're on the final word, we'll hit the end of the line and the macro will stop.

he

This is an example of the sort of thing you need to watch out for when recording a macro. We want to jump to the end of the word. Normally, e will do this. But if we're on a single letter word like A, it will instead jump to the end of the next word. So first we move left once with h and then we jump to the end of the word with e.

llb

If we at the end of the final word on the line, attempting to move right twice will first move onto the } character and then hit the end of the line, causing an "error" which means the macro will stop*. If there was no error, we are good to continue so we jump back to the start of the word with b.

w

Now we just need to move to the next word that needs to be uppercased.

So our macro is complete!

Now we just need to play it back once per section heading:

:g/section/norm!f{l@q

We use the same :global search that you already supplied: :g/section/. Then, for our command, we just use some more normal mode commands, run via the :normal! command:

f{l

This jumps to the first { on the line and then moves right onto the first character of the section heading.

@q

This plays back the macro.

* N.B. I'm presuming you're using the default setting for 'whichwrap'. If you have edited this setting to include l, then replace the l commands in the macro with either Right Arrow, or Space, depending on which of these is not present in your 'whichwrap' setting. (There's not really any good reason to have all three in the setting, so if they are all present, consider removing one!)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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