1

I have "|" delimited text data, and want to transform a column values

$ cat infile
Mark|father
Jason|SOn
Jose|son
Steffy|daugHter

I want to search for (father|son|daughter) case insensitively and substitute any case of father to Father, any case of son to Son, any case of daughter to Daughter

So outfile should look like this

$ cat outfile
Mark Father
Jason Son
Jose Son
Steffy Daughter

I'm trying different combination of IGNORECASE with sub or gsub, but it prints all entries as is in infile

4
  • 2
    while you edited your question adding that field separator, you likely invalidate most of the answers here you received, you could just tell it at first and that was not the things you could skip it from saying in your question. Commented Aug 13, 2021 at 5:41
  • 2
    Next time, please try to give accurate representations of your data. The field separator is an important detail that can completely change the available solutions. Should we now understand that you want to i) fix the case of the words and ii) change the output field separator from a | to a space?
    – terdon
    Commented Aug 13, 2021 at 8:11
  • 1
    @αғsнιη apology for makin up my minds at such a critical stage, initially I thought it would be any awk function that would respect global FS|OFS in the begin section of awk script. But in this case, split function did not work with space, and I got clue why, so I asked. But I got your point amigo. I'll keep standards in mind for next time. Hence, I appreciate contribution from each of you community members ❤
    – Sollosa
    Commented Aug 13, 2021 at 20:09
  • @terdon apologies amigo, I understand your point, FS was really important, especially for split function, which I chose over others. I appreciate everyone's contributions here.
    – Sollosa
    Commented Aug 13, 2021 at 20:25

5 Answers 5

5

This is an attempt at answering the original version of the question. The requirements have changed since.

That's one thing the GNU implementation of sed is good at:

$ sed -E 's/(^|\s)(son|daughter|father)(\s|$)/\1\L\u\2\3/i' < file
Mark Father
Jason Son
Jose Son
Steffy Daughter

The regexp matches on either of those 3 words but only if they're neither preceded nor followed by a non-whitespace.

\L turns the whole word to lower case, \u only the first character to uppercase (those come from ex/vi from the 70s, but unfortunately didn't make it to standard sed).

The same would work with perl -pe instead of sed -E (making it probably more portable as more systems have perl than GNU sed), though in perl you can simplify it to:

perl -pe 's/(?<!\S)(son|daughter|father)(?!\S)/\L\u$&/i'

That is, using negative look-around operators to make sure those strings are not part of longer whitespace-delimited words (like Jason in your input). See also the \b in perl and \<, \> in sed, word boundary operators, but those would be more like (?!\w) so would turn grand-son to grand-Son for instance as - is not a word constituent character.

Those replace only up to one occurrence per line. To replace all occurrences, you can add the g flag to the perl one above. Adding it to the sed one could miss some as on a Mark son SON sOn, the first match will replace " son " with " Son ", and then sed will resume searching at "SON sOn", so won't find a match for \s before SON. That could be solved by doubling all the whitespace characters beforehand, and reverting afterwards:

sed -E 's/\s/&&/g
        s/(^|\s)(son|daughter|father)(\s|$)/\1\L\u\2\3/ig
        s/(\s)\1/\1/g'

Though that's starting to be a bit too convoluted.

0
4

I'd use a hash lookup instead of a regexp comparison and *sub() for efficiency and robustness (in case you decide to use a string that contains regexp metachars or backreferences or can be a substring of some other string):

$ cat tst.awk
BEGIN {
    FS = "|"
    split("Father|Son|Daughter",tmp)
    for (i in tmp) {
        map[tolower(tmp[i])] = tmp[i]
    }
}
{ lc = tolower($2) }
lc in map {
    $2 = map[lc]
}
{ print }

$ awk -f tst.awk file
Mark Father
Jason Son
Jose Son
Steffy Daughter
6
  • Thanks Ed, 1 Q tho, if FS is other than space in infile, will split line be written with space or that FS instead?
    – Sollosa
    Commented Aug 12, 2021 at 18:29
  • You're welcome. I'm not sure I understand the question but: FS is used to split $0 into fields, OFS is used to combine fields into $0. So if you want input and output fields to both be separated by, say , instead of space, then add FS=OFS="," at the end of the BEGIN section (or put it at the top of it and change split("Father Son Daughter",tmp) to split("Father,Son,Daughter",tmp) or split("Father Son Daughter",tmp," ")).
    – Ed Morton
    Commented Aug 12, 2021 at 18:34
  • yea my file is "|" separated, so I guess I have to write as split("Father Son Daughter",tmp,"|")
    – Sollosa
    Commented Aug 12, 2021 at 19:11
  • No, that's the opposite of what I said. split("Father Son Daughter",tmp,"|") says split the string "Father Son Daughter" at every "|" but there are no |s in "Father Son Daughter". It's the input (FS) and output (OFS) you want split at every |, not the string in that function call. Please edit your question to show the real separators in your input and output and then I'll update my script to show how to handle that.
    – Ed Morton
    Commented Aug 12, 2021 at 19:12
  • 1
    OK, I updated my answer. I wasn't sure if you REALLY wanted Father|Son|Daughter to be |-separated or not but I went ahead and changed them to be so anyway as re-reading your comments it sounded like you might want that for some reason.
    – Ed Morton
    Commented Aug 13, 2021 at 3:48
4

One way (will work in all the awk implementations) would be to lowercase the second column but uppercase the only first character; then check if the contents of those were matched, then update the second column's value with its converted content saved in tmp.

$ awk -F'|' '{ tmp=toupper(substr($2,1,1)) tolower(substr($2,2)) }
  tmp ~ /^(Father|Son|Daughter)$/  { $2=tmp }1' infile
Mark Father
Jason Son
Jose Son
Steffy Daughter

note that when using IGNORECASE (GNU awk specific), that would only applies to all the matching processing (string/regex) you want to perform not at the replacement.

4
  • Ok, it works in 1 liner awk, but not in awk script. I copied { tmp=toupper(substr($2,1,1)) tolower(substr($2,2)) } tmp ~ /^Father|Son|Daughter$/ { sub($2, tmp, $2) }1 into awk file, and it's showing syntax error. Sayin error at source line.. context is >>> { <<< sub...
    – Sollosa
    Commented Aug 12, 2021 at 9:32
  • yes you're right, so how'd this 1 liner be translated into awkscript?
    – Sollosa
    Commented Aug 12, 2021 at 12:55
  • I just changed tmp ~ /../ { sub($2,tmp,$2}1 ==> tmp ~ /../ && sub($2,tmp,$2) +1. In fact I too wanted $2=tmp. Maybe the author afshin doesn't want any field calculations to happen .
    – guest_7
    Commented Aug 12, 2021 at 13:25
  • @guest_7, and αғsнιη as long as you modify individual elements whether that's via $2 = ... or sub(...,...,$2), $0 is rebuilt and the spacing is mangled. Commented Aug 12, 2021 at 14:12
1
 awk -F "|" 'BEGIN{IGNORECASE=1}{gsub(/daughter/,"Daughter",$2);gsub(/son/,"Son",$2);gsub(/fatheR/,"Father",$2);print }' filename

output

Mark Father
Jason Son
Jose Son
Steffy Daughter
1

Using Raku (formerly known as Perl_6)

raku -pe 's:i:g/ «father» | «daughter» | «son» /{$/.tclc}/;' 

OR

raku -pe 's:i:g/ «father» | «daughter» | «son» /{$/.wordcase}/;'

The :ignorecase regex adverb does case-insensitive matching in Raku (abbreviated :i). Left « and right » word-boundaries ensure that only whole words are matched (e.g. no spurious matches which may result in output like JaSon). Note you can use << instead of « for the left word-boundary and >> instead of » for the right word-boundary.

To change case, Raku has a nifty wordcase routine which (you guessed it), takes words and capitalizes the first letter while converting all non-initial letters to lowercase. [The Raku function tclc (literally 'titlecase-lowercase') does the same thing in the default, with fewer options].

Sample Input:

Mark|father
Jason|SOn
Jose|son
Steffy|daugHter
Agnes|moTHer

Sample Output:

Mark|Father
Jason|Son
Jose|Son
Steffy|Daughter
Agnes|moTHer

If the OP wants to split on a separating character such as |, simply call the following Raku one-liner before or after the code above:

raku -ne '.split("|").put;' 

Sample Output:

Mark Father
Jason Son
Jose Son
Steffy Daughter
Agnes moTHer

ADDENDUM:

@Stéphane Chazelas notes in the comments that for the code above (for example) hyphenated words will receive an internal capitalization (e.g. god-son to god-Son). The below code uses three literal matches which avoids this issue:

raku -ne '.wordcase(:where({ $_.fc eq "father" | "daughter" | "son"})).put;'

OR

raku -pe '.=wordcase(:where({ $_.fc eq "father" | "daughter" | "son"}));'

https://docs.raku.org/language/regexes
https://raku.org/

2
  • 1
    Might be worth noting the word here is sequence of alnums or underscores, not whitespace delimited words. For instance, it would turn god-son to god-Son. It's still better than with the word boundary operators in some other tools in that it would be OK with echo $'m\u00e9son' or echo $'me\u0301son' Commented Sep 11, 2021 at 9:13
  • Thank you, @ Stéphane Chazelas! You are absolutely correct. I updated the answer to address your concerns. Commented Sep 11, 2021 at 20:02

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .