I have the following file:

<?xml version="1.0" encoding="utf-8"?>
<!--Generated by crowdin.net-->
  <string name="test" >- test</string>
  <string name="test" >test-test</string>
  <string name="test" >test - test</string>

and I would like to replace the en dash with its unicode value, but not all of them, just the one in the string tag

I run several sed with different regex, but I couldn't figured it out. One of those was

sed -i.bak "s/-[^-\<\>0-9]/\&#8211\;/g" strings.xml

the output was:

<?xml version="1.0" encoding="utf-8"?>
<!-&#8211;enerated by-->
  <string name="test" >&#8211;test</string>
  <string name="test2" >test&#8211;est</string>
  <string name="test3" >test &#8211;test</string>

my problem is that is also replacing empty spaces and the first char of the second word. I have not that big experience with regex and sed. Could you please explain me what I am doing wrong?

Note: I'm using OSX.


With a recent (for \K and s///r) perl and assuming your <string> tags don't nest:

perl -0777 -pi.bak -e's{<string.*?>\K.*?(?=</string>)}{$&=~s/-/&#8211;/rg}ges' file.xml
  • -0777: slurp mode: handle the whole file at once (to allow <string> tags to span several lines).
  • -p: sed mode
  • -i.bak: in-place editing with .bak extension (BTW, that's where some sed implementations got that idea from)
  • s{...}{...}ges: substitute globally (g), where . matches newline characters as well (s), and treat the replacement as perl code to execute (e).
  • <string.*?>\K.*?</string>: match from <string...> to </string> but don't include the tags themselves in the part that is matched (\K defines where the matched portion starts, and (?=...) is a look-ahead operator that only checks if </string> is there, but doesn't include it in the match).
  • $&=~s/.../.../rg. Do the substitution on the matched part ($&). The r flag is to actually not modify $& but return the substituted string.
  • Nice! Why do you say this won't work on nested tags? It does on my system. – terdon Jul 1 '14 at 12:58
  • @terdon, it won't replace the - in <string><string></string>-</string> – Stéphane Chazelas Jul 1 '14 at 13:06
  • Ah, yes. I saw it would work on <string>-<string>-</string></string> and assumed. – terdon Jul 1 '14 at 13:12

Phew, after some time I got it. This is a naive solution. terdon's answer is more correct and you should use his though :).

sed -Ei.bak "s/(.*<string[^>]*\")(.*)-(.*)/\1\2\&#8211;\3/g" strings.xml

I am using Backreferences to refer back to a previously matched string. These are \1 \2 etc.

In this case sed should match following groups:

  • (.*<string[^>]*\") - any characters followed by a string tag opening until a quote ". Group 1
  • (.*) - anything after the " (including right now >) until group 3. Group 2
  • - the matching dash
  • (.*) - anything after the matching dash Group 3

Then I replace it with the previously matched groups and the dash HTML value &#8211;, by using \n with n as the reference to group n.


I currently try to fix some problems, so please cope with me:

  1. Group 1 matches also dsfjpasj<string
  2. Group 1 should include the string tag ending character >
  3. As terdon points out: "this won't work for cases where you have >1 - or nested tags or tags spanning multiple lines"

Read more:


  • 1
    Note that the format for -i is different for non-GNU sed (the OP is on OSX). Also, this won't work for cases where you have >1 - or nested tags or tags spanning multiple lines. – terdon Jul 1 '14 at 11:55
  • 2
    @terdon, that -Ei.bak syntax will work with FreeBSD/OSX sed. The difference is that the backup extension is optional in GNU sed (and has to be -i.bak, not -i .back) while it's required in FreeBSD/OSX (where both -i.back and -i .back are allowed). But here, since it's provided and is not in the form -i .back, it will work with both GNU and FreeBSD/OSX sed. – Stéphane Chazelas Jul 1 '14 at 12:08
  • two more questions: It would be much effort make it works for the case with more than 1 "-"? If I would like to replace em dash, can I simply change from (.*)-(.*) to (.*)--(.*) ? – Blackbelt Jul 1 '14 at 12:56

If I understand correctly, you want to replace all cases (three in your example) of - within <strng></string> tags and only those cases. If so, these approaches should work assuming your XML is sane:

  1. Use a regular expression and a simple tool like sed

    sed 's/\(<string[^>]*>[^-]*\)-\([^-]*<\/string\)/\1\&#8211;\2/' file.xml 
  2. If your file is always like the example above and you can be sure that your tags will always be <string name="test" ></string>, you can use lookbehinds:

    perl -pe 's/(?<=<string name="test" >)([^<]*?)-([^<]*)/$1&#8211;$2/g' file.xml
  3. None of the above will work if you have more than a single - within the tags. To deal with such cases, you can write a simple little script that checks whether we're within <string></string> tags. This should also deal with nested tags.

    perl -F'<' -lane 'for($i=0;$i<=$#F;$i++){
        $a++ if $F[$i]=~/^string/; 
        $F[$i]=~s/-/&#8211;/g if $a>0; 
        $a-- if $F[$i]=~/^\/string/
    } print join "<",@F' file.xml
  • Hi thanks for your answer. I tried the lookbehind solution but it looks like is not supported/implemented in sed osx – Blackbelt Jul 1 '14 at 12:51
  • @blackbelt the lookbehind example is using perl, not sed. As far as I know, sed does not support lookarounds. – terdon Jul 1 '14 at 12:54
  • @blackbelt you're welcome :). I suggest you accept Stephane's answer below, it is simpler than mine yet still works for multiline, (some) nested tags and multiple - cases. – terdon Jul 1 '14 at 12:55

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.