0

I am using the {fmt} library.

Unfortunately, my program crashed after a few days, as I had an invalid format string. Easily fixed - but what if there are more?

It is possible to do compile time checking of string formats, which would have caught this error:

// Replace this:
fmt::print("{}",42)
// With this:
fmt::print(FMT_STRING("{}"),42)

I could do this manually with about 500 print statements across the entire code base.

But I'm wondering - is there a way to do this with a RegEx and a find/replace in Visual Studio?

I got as far as using .NET RegEx tester and a string match on this:

print[(]".*".*[)];

However, a robust search-and-replace is still eluding me after many hours of trying.


Update 2020-07-04.

Used my answer below to solve the problem. Luckily enough, the rest were perfect.

Contango
  • 76,540
  • 58
  • 260
  • 305
  • 7
    Just as an aside, I don't think Howard has much to do with the library. Per the readme: "The {fmt} library is maintained by Victor Zverovich (vitaut) and Jonathan Müller (foonathan) with contributions from many other people." – user975989 Jul 02 '20 at 12:20
  • @user975989 I absolutely stand corrected. Another user has thoughtfully edited the question to correct. – Contango Jul 03 '20 at 15:53
  • If you have to use a macro, you might as well make a macro that wraps `fmt::print` (to be used instead of it) and automatically calls `FMT_STRING` on the first argument. – HolyBlackCat Jul 04 '20 at 09:35

2 Answers2

2

In VSCode, this works:

Pattern: .*(fmt::print\()"\{\}"(,.*\)).*

Replacement: $1FMT_STRING("{}")$2


If you're a Python fan, this also works if you read the C++ script in as a string:

import re

pattern = '.*(fmt::print\()"{}"(.*\)).*'
fmt_snippet = 'fmt::print("{}",42)'
re.sub(pattern, r'\1FMT_STRING("{}")\2', fmt_snippet)
Contango
  • 76,540
  • 58
  • 260
  • 305
Mark Moretto
  • 2,344
  • 2
  • 15
  • 21
  • Thanks ever so much! The example works in VSCode, but required some editing to work in Visual Studio. I've marked your answer as the official one as you put all of the work in. – Contango Jul 04 '20 at 10:47
  • Thanks! What was the difference between the two? I used to use VS, but it became kind of sluggish with recent updates to how libraries are used. FWIW, I use VSC with C++ and it works great. Setting up different environments is a breeze. – Mark Moretto Jul 04 '20 at 14:35
  • I think VS Code uses `\(` for a literal, whereas Visual Studio uses .NET-style RegEx which uses `[(]` for a literal. I also had to tweak it slightly to make it handle the vast majority of instances across my codebase. About 500 replaces across 50k lines, and 5 which were fixed manually (see answer). – Contango Jul 04 '20 at 14:56
0

Expanding on the excellent answer from Mark Moretto:

In Visual Studio 2019, this worked beautifully across my entire C++ codebase:

Replace this:

(.*print[(])(".*?")(.*[)];)

With this:

$1FMT_STRING($2)$3

Then repeat with this to handle fmt::format:

(.*format[(])(".*?")(.*[)];)

For the global Search'n'Replace, ensure that RegEx icon which is .* is switched on.

I used .NET Regex Tester to form the expression.

It correctly fixed up 500 of 505 instances in about 5 seconds, but it it did trip up on this sort of thing which I fixed manually:

// Required manual fix (shift rogue bracket away from end). 
auto y = format("Test=\"{}\""), 42); 

Explanation (Optional)

I have always found RegEx expressions to be horribly complicated, but working through this example somehow made a lightbulb switch on in my head.

  1. (.*print[(]) matches anything from the start of the line to the opening print( and replaces with $1.
  2. (".*?") matches from the opening to closing quote, and replaces with $2.
  3. (.*[)];) matches everything to the closing );, and replaces with $3.
  4. $1FMT_STRING($2)$3 inserts FMT_STRING() in the right place.

Notes:

  • On point 1:
    • Note the use of [(] to indicate a literal (.
    • Note the use of .*. This is a wildcard where . means any character and * means any number of repetitions.
    • It will also match on fmt::print(. Needs a different RegEx to handle format( and fmt::format( (see above).
  • On point 2:
    • Note the use of ? to indicate that it should stop on the very first closing quote it sees (i.e. a "non-greedy match").

Appendix A: Test Cases (Optional)

Paste the Input Tests Cases below into .NET Regex Tester for syntax highlighting and easier editing of the expression to modify it slightly.

Input test cases:

// Test non-namespace match.
print("Hello, world!");
print("Test: {}", 42);
print("Test: {}: {}", 42, "A");
print("Test: {:0}: {} : {}", 42, "A", myVariable);
print("{}, {}, {}", 42, "A", "B");
    print("Hello, world!");
    print("Test: {}", 42);
    print("Test: {}: {}", 42, "A");
    print("Test: {:0}: {} : {}", 42, "A", myVariable);
    print("{}, {}, {}", 42, "A", "B");

// Test namespace match.
fmt::print("Hello, world!");
fmt::print("Test: {}", 42);
fmt::print("Test: {}: {}", 42, "A");
fmt::print("Test: {:0}: {} : {}", 42, "A", myVariable);
fmt::print("{}, {}, {}", 42, "A", "B");
    fmt::print("Hello, world!");
    fmt::print("Test: {}", 42);
    fmt::print("Test: {}: {}", 42, "A");
    fmt::print("Test: {:0}: {} : {}", 42, "A", myVariable);
    fmt::print("{}, {}, {}", 42, "A", "B");

// Test compatibility with existing (should be no change).
​fmt::print(FMT_STRING("Hello, world!"));
​fmt::print(FMT_STRING("Test: {}"), 42);
​fmt::print(FMT_STRING("Test: {}: {}"), 42, "A");
​fmt::print(FMT_STRING("Test: {:0}: {} : {}"), 42, "A", myVariable);
​fmt::print(FMT_STRING("{}, {}, {}"), 42, "A", "B");
​    fmt::print("Hello, world!");
​    fmt::print(FMT_STRING("Test: {}"), 42);
​    fmt::print(FMT_STRING("Test: {}: {}"), 42, "A");
​    fmt::print(FMT_STRING("Test: {:0}: {} : {}"), 42, "A", myVariable);
​    fmt::print(FMT_STRING("{}, {}, {}"), 42, "A", "B");

Output of test cases (all correct):

// Test non-namespace match.
​print(FMT_STRING("Hello, world!"));
​print(FMT_STRING("Test: {}"), 42);
​print(FMT_STRING("Test: {}: {}"), 42, "A");
​print(FMT_STRING("Test: {:0}: {} : {}"), 42, "A", myVariable);
​print(FMT_STRING("{}, {}, {}"), 42, "A", "B");
​    print(FMT_STRING("Hello, world!"));
​    print(FMT_STRING("Test: {}"), 42);
​    print(FMT_STRING("Test: {}: {}"), 42, "A");
​    print(FMT_STRING("Test: {:0}: {} : {}"), 42, "A", myVariable);
​    print(FMT_STRING("{}, {}, {}"), 42, "A", "B");
​
​// Test namespace match.
​fmt::print(FMT_STRING("Hello, world!"));
​fmt::print(FMT_STRING("Test: {}"), 42);
​fmt::print(FMT_STRING("Test: {}: {}"), 42, "A");
​fmt::print(FMT_STRING("Test: {:0}: {} : {}"), 42, "A", myVariable);
​fmt::print(FMT_STRING("{}, {}, {}"), 42, "A", "B");
​    fmt::print(FMT_STRING("Hello, world!"));
​    fmt::print(FMT_STRING("Test: {}"), 42);
​    fmt::print(FMT_STRING("Test: {}: {}"), 42, "A");
​    fmt::print(FMT_STRING("Test: {:0}: {} : {}"), 42, "A", myVariable);
​    fmt::print(FMT_STRING("{}, {}, {}"), 42, "A", "B");
​
​// Test compatibility with existing (should be no change).
​​fmt::print(FMT_STRING("Hello, world!"));
​​fmt::print(FMT_STRING("Test: {}"), 42);
​​fmt::print(FMT_STRING("Test: {}: {}"), 42, "A");
​​fmt::print(FMT_STRING("Test: {:0}: {} : {}"), 42, "A", myVariable);
​​fmt::print(FMT_STRING("{}, {}, {}"), 42, "A", "B");
​​    fmt::print(FMT_STRING("Hello, world!"));
​​    fmt::print(FMT_STRING("Test: {}"), 42);
​​    fmt::print(FMT_STRING("Test: {}: {}"), 42, "A");
​​    fmt::print(FMT_STRING("Test: {:0}: {} : {}"), 42, "A", myVariable);
​​    fmt::print(FMT_STRING("{}, {}, {}"), 42, "A", "B");

Contango
  • 76,540
  • 58
  • 260
  • 305