0

I have a string value stored in a variable:

PTYPE="Other Farm|Raised Ranch|Farm house|Other|A-Frame|Log Home"

I want to find & replace Other with some value like NOTHING. All values are stored in variables.

WhatToChange=Other
NewValue=NOTHING

echo $PTYPE|sed -e "s@${WhatToChange}@${NewValue}@g"

This is replacing all the occurances of Other and getting output like:

NOTHING Farm|Raised Ranch|Farm house|NOTHING|A-Frame|Log Home

Is there any way I can exactly change only the exact one? The place for ${WhatToChange} is variable.

Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
  • Please take a look at [How do I format my posts using Markdown or HTML?](https://stackoverflow.com/help/formatting). – Cyrus Jan 24 '22 at 19:56
  • 2
    While it's not your immediate problem, note that `echo $PTYPE` is itself buggy. _Always_ quote your expansions, as in, `echo "$PTYPE"` – Charles Duffy Jan 24 '22 at 20:04
  • @CharlesDuffy - yes, I am quoting it in my script. – Tahir Khalil Jan 25 '22 at 22:42
  • @TahirKhalil, ...but you're _not_ quoting it in `echo $PTYPE | sed` in the question. If your real code doesn't have that bug, I'd suggest editing the question appropriately. (For context, see [I just assigned a variable, but `echo $variable` shows something else](https://stackoverflow.com/questions/29378566/i-just-assigned-a-variable-but-echo-variable-shows-something-else)) – Charles Duffy Jan 26 '22 at 01:59

5 Answers5

2

To match either the exact character | or the beginning of the line, use ([|]|^).

To match either the exact character | or the end of the line, use ([|]|$).

To put a | back in place only when appropriate, store these in match groups, and refer to those groups with \1 or \2:

PTYPE="Other Farm|Raised Ranch|Farm house|Other|A-Frame|Log Home"
WhatToChange=Other
NewValue=NOTHING

sed -re "s@(^|[|])${WhatToChange}($|[|])@\1${NewValue}\2@g" <<<"$PTYPE"

...emits as output:

Other Farm|Raised Ranch|Farm house|NOTHING|A-Frame|Log Home

...and still works even if WhatToChange is matched at the beginning or end of the list.

Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
  • Thanks for your solution - its working well in almost all the conditions except in case where PTYPE contain only one value without field delimiter like if: PTYPE="Other" Same is the case with other solutions. – Tahir Khalil Jan 25 '22 at 22:31
  • @TahirKhalil, I cannot reproduce that problem. See https://ideone.com/PeJkhk -- it correctly changes `PTYPE2="Other"` to output of `NOTHING`. Could you create your own sandbox instance (at ideone.com, or https://repl.it/, or similar) that demonstrates the issue? – Charles Duffy Jan 26 '22 at 02:03
  • @TahirKhalil, ... Thinking about it, one possible way to get the behavior you're reporting would be if the input has DOS newlines, making the real value `PTYPE2="Other"$'\r'`. Just a theory, of course. – Charles Duffy Jan 26 '22 at 03:46
2

As you have well defined fields and want an exact match, awk could be easier to use than sed; at the very least, you won't have to worry about escaping the strings for using it in the sed expression:

echo "Other Farm|Raised Ranch|Farm house|Other|A-Frame|Log Home" |
awk -v old="Other" -v new="NOTHING" \
    'BEGIN {FS = OFS = "|"} {for(i=1;i<=NF;i++) if($i == old) $i = new} 1'

output:

Other Farm|Raised Ranch|Farm house|NOTHING|A-Frame|Log Home
Fravadona
  • 13,917
  • 1
  • 23
  • 35
  • Thanks - I am but confused with using existing variable in within if -- cant I use my already declared variable within script to be used in awk compare? – Tahir Khalil Jan 25 '22 at 22:34
  • It's not that straight-forward to expand bash variables **inside** other languages; you need to **escape** them correctly or else they will break the script (you'll probably experiment it with the accepted answer). In my code I already implemented a way to pass your values to the awk script through the `old` and `new` parameters; you just have to call `awk -v old="$WhatToChange" -v new="$NewValue" 'BEGIN{...etc...'` – Fravadona Jan 25 '22 at 23:37
  • Thanks - I opted this after trying various others -- it suites all my conditions. Thank you once again. – Tahir Khalil Feb 01 '22 at 10:44
2

For fun, some perl:

This is like @Charles's sed solution: Note the \Q...\E so that the "to change" value is treated as literal text.

echo "$PTYPE" | perl -spe '
    s{ (?:^|\|)\K \Q$WhatToChange\E (?=\||$) }{$NewValue}gx
' -- -WhatToChange=Other -NewValue=NOTHING

This is like @Fravadona's awk solution:

echo "$PTYPE" | perl -F'[|]' -sane '
    print join "|", map {$_ eq $WhatToChange ? $NewValue : $_} @F
' -- -WhatToChange=Other -NewValue=NOTHING
glenn jackman
  • 238,783
  • 38
  • 220
  • 352
2

How about

echo ${PTYPE//$WhatToChange/$NewValue}

UPDATE:

I just realized that the replacement should happen only if WhatToChange is the whole content between two separators (|). In this case, we can do it in bash as well (without the need to revert to a child process):

if [[ $PTYPE =~ (.*[|]|^)$WhatToChange([|].*|$) ]]
  echo "${BASH_REMATCH[1]}${NewValue}${BASH_REMATCH[2]}"
fi

UPDATE (based on the comment by Fravadona):

Used in this way, WhatToChange is interpreted as a regular expression. This can be useful, if you want to catch for instance variations of the string, for instance

WhatToChange='[Oo]ther' # to catch Other and other

If you always want to have a literal match, you have to quote the variable: [[ $PTYPE =~ (.*[|]|^)"$WhatToChange"([|].*|$) ]]

user1934428
  • 19,864
  • 7
  • 42
  • 87
  • This doesn't solve the problem of substrings being replaced when the OP only wants complete words to be replaced (that is, the problem of "Other Farm" being changed to "NOTHING Farm" in the OP's example input, when they only want "Other" to be replaced when it's present as a complete item). – Charles Duffy Jan 25 '22 at 15:38
  • no - its replacing all "Other" – Tahir Khalil Jan 25 '22 at 22:37
  • @TahirKhalil: This is what it's supposed to do. In your original question (before you edited it), you had all 'Other' strings replaced. Please don't edit a question in a way that it invalidates a correct answer, but ask in such cases a new question instead. Furthermore, if you want to replace only one occurance of the pattern, you would need to specify the rule according to which the correct occurance can be recognized. – user1934428 Jan 26 '22 at 13:03
  • @CharlesDuffy : My answer relates to the original question, before it was edited by the OP. – user1934428 Jan 26 '22 at 13:03
  • @user1934428, even revision 1 of the question at https://stackoverflow.com/revisions/70839711/1 explicitly states that `Other Farm` should not change to `NOTHING Farm`. – Charles Duffy Jan 26 '22 at 18:00
  • @CharlesDuffy: Right. My misreading. And now I finally understand, what the OP meant with "the exact one". He is looking for an exact match. – user1934428 Jan 27 '22 at 07:34
  • @CharlesDuffy : I updated my answer. Would you mind to check it for correctness? – user1934428 Jan 27 '22 at 07:52
  • I would double quote `$WhatToChange` in the regex (thus escaping regex characters), and add a capture group for it (so you can test if there was a match before doing the replacement) – Fravadona Jan 27 '22 at 10:18
  • From the question, I thought that there will be only words, so I did not consider this. Actually, allowing `WhatToChange` to contain a regexp, could be an advantage. You are right that it makes sense to check that there was a match. – user1934428 Jan 27 '22 at 10:22
  • @Fravadona: Actually, the problem with regexp characters inside 'WhatToChange' is also present in the `sed` attempt shown in the question, so I guess the OP either knows that there won't be such characters, or wants to allow for regexp in the interpolation. Still, it is a good idea to point this out. – user1934428 Jan 27 '22 at 10:28
  • Looks good to me as-updated. – Charles Duffy Jan 27 '22 at 14:59
  • BTW, if you want `$WhatToChange` to be interpreted literally instead of as a regex, put it in double quotes (while keeping the parts that _should_ be treated as regexes unquoted). Thus: `[[ $PTYPE =~ (.*[|]|^)"$WhatToChange"([|].*|$) ]]` – Charles Duffy Jan 27 '22 at 15:00
0

This might work for you (GNU sed & bash):

<<<"$PTYPE" sed 'y/|/\n/;s/^'"$WhatToChange"'$/'"$NewValue"'/mg;y/\n/|/'

Input $PTYPE as a here-string into sed.

Translate | separators to newlines.

Replace $WhatToChange to $NewValue for each matched line.

Translate newlines back to |'s.

N.B. The use of the m flag in the substitution command allows sed to work in multiline mode and this presents each value between separators on its own line.

An alternative:

sed -z 'y/|/\x00/;s/^'"$WhatToChange"'$/'"$NewValue"'/mg;y/\x00/|/;' file
potong
  • 55,640
  • 6
  • 51
  • 83
  • This works in the OP's specific case when the input can't already contain newlines, but it wouldn't work well if one wanted to run the same processing against a multi-line file. – Charles Duffy Jan 26 '22 at 02:00
  • @CharlesDuffy I'm not sure I understand you, sed **normally** uses newlines as separators and strips them before populating the pattern space so this solution would still work. When I say **normally** I mean when not using the `-z` option or introducing newlines via then `N`, `H` or `G` command a work around for the first exception is to translate newlines to nulls, substitute and then reverse the translation. – potong Jan 26 '22 at 13:10
  • @CharlesDuffy if you mean the values within the shell variables - then the alternative solution should be suffice. Now if those values contain nulls.... – potong Jan 26 '22 at 13:17
  • Don't forget to escape `WhatToChange` and `NewValue` for `sed` (pattern and replacement respectively) – Fravadona Jan 26 '22 at 14:30
  • Can't have a NUL in a shell variable (since they're C strings), so safe there either way. – Charles Duffy Jan 26 '22 at 23:00