D_________
D_________

Reputation: 583

Replace each match with an incremented number

I need to add an ID property to a list of objects in a JSON file. I found this useful in-line Perl script that replaces each match with a random int.

:%! perl -pne 's/XYZ/int(rand 1000)/ge'

Assuming I don't know the list's length in advance, how would I replace something like this

{
    "id" : XYZ
},
{
    "id" : XYZ
},
{ ... }

with this:

{
    "id" : 1
},
{
    "id" : 2
},
{ ... }

I've tried this:

:%! perl -pne 's/XYZ/(0..100)/ge'

but that assumes I know the length of the list in advance and also does not work as expected. I know I can get a number of matches using something like this:

:%s/"id" : XYZ//gn

Upvotes: 0

Views: 1320

Answers (4)

choroba
choroba

Reputation: 241768

-p already processes the input line by line, so -n is redundant.

perl -pe 's/XYZ/$i++/ge'

$i++ returns the value of $i in numeric context and adds 1 to it.

Upvotes: 3

Ingo Karkat
Ingo Karkat

Reputation: 172520

For those looking for a pure Vim solution, here's a more canonical and general solution than in @sidyll's answer that doesn't require :perldo nor :global, and would also work with multiple matches in a line (adding the g flag to :substitute):

With built-in substitute

In Vim, you can use :help sub-replace-expression to replace text with arbitrary expressions. Unfortunately, incrementing a variable is a statement in Vim, not an expression. Either one has to resort to tricks like using the length of a List as the counter (add() can be used in an expression):

:let c = [] | %substitute/XYZ/\=len(add(c, 0))/

Or you have to define a separate function (once):

function! Increment()
    let g:i += 1
    return g:i
endfunction
let g:i = 0 | %substitute/XYZ/\=Increment()/

With plugin

The expression syntax is a bit weird (but if you're into Perl, you're used to much worse :-). Also, the explicit separate initialization of the counter (and function) is cumbersome. My PatternsOnText plugin improves on the built-in :substitute command with several variants. Among them :SubstituteExecute, which benefits from a predefined context object:

:%SubstituteExecute /XYZ/ let v:val.n += 1 | return v:val.n

Renumbering is such a frequent task that the plugin even has a special command for it:

:Renumber /XYZ/

Caveats

  • A structured but flexible syntax like HTML or JSON is ill suited for parsing with regular expressions, unless you confidently know that the used subset of the allowed syntax is very regular (e.g. because you know the input source and/or have passed the input through a pretty-printer). Prefer specialized tools over generic ones (i.e. use xmlstarlet for XML and jq for JSON instead of grep and sed).

Upvotes: 2

sidyll
sidyll

Reputation: 59277

If you are doing this in Vim and you have the +perl feature then:

:%perldo s/XYZ/++$i/e

If you want to do it with plain Vim commands, you can follow the same logic and use a variable. However, it needs to be created beforehand and incremented after each substitution. Thus you may need a help from :g to execute two commands:

:let i=0
:g/XYZ/let i+=1|s//\=i

The empty s// reuses the pattern from :g.

Alternatively, just filter it through Perl as you did originally:

:%!perl -pe 's/XYZ/++$i/e'

Upvotes: 4

Grinnz
Grinnz

Reputation: 9231

Using Perl's built in JSON parser (in 5.14+) you don't need to deal with strings that may show up elsewhere, whether strings and numbers should be quoted or not, or other complications, you can just modify the JSON as a data structure (code will probably need to be altered to modify the correct part of your full JSON structure):

perl -0777 -MJSON::PP -E'my $data = decode_json readline; $_->{id} = ++$i foreach @$data; print encode_json $data'

Even better would be to use JSON::MaybeXS as that will install and use a much faster parser by default.

The -0777 switch is a convention for one-liners to cause readline or the <> operator (also used by the -p switch) to return the whole input at once.

Upvotes: 3

Related Questions