I have a file that has this :
cat textfile.csv
Name,Age,City
Jen,40,San Diego
Jen,,NYC
Bob,29,Miami,
,30,Boston
Jen,,NYC
Bob,21,Seattle
instead of using vim to manually do it .. is there a way I can use awk/sed to edit the 2nd Jen's age from ,, to whatever I want?
Also notice how there are 2 Jen,,NYC ... I want to be able to choose the 2nd Jen,,NYC (above the seattle one) and edit that one
Eventually this file will grow and there maybe more than 1 Bob/Jen so I want to be able to target the specific Jen I want and edit the field (whether that be name,age,city) if needed.
CodePudding user response:
Here an attempt at a solution:
modify.bash
#!/bin/bash
# Arguments
# $1: name to find
# $2: previous line
# $3: new age
if [[ $# -eq 3 ]]
then
name="$1"
previous="$2"
newage="$3"
else
echo "Usage: $0 <name> <previous> <newage>"
exit 1
fi
# CSV input file
csvfile="textfile.csv"
if [[ ! -f "$csvfile" ]]
then
echo "ERROR: cannot find file $csvfile"
exit 1
fi
# awk script
if [[ ! -f modify.awk ]]
then
echo "ERROR: missing awk script modify.awk"
exit 1
fi
awk -f modify.awk -v previous="$previous" name="$name" newage="$newage" textfile.csv
- this bash script it just a wrapper to the awk script, to process arguments and do some validations.
-v
ofawk
allows you to create variables thatawk
can use in its code.- the variable values must be double quoted to handle spaces properly.
- this script prints the result to the screen without modifying the original file. That output can be redirected to a file using
>newfile
.
modify.awk
BEGIN { FS="," }
$0~previous {
f = 1
print
next
}
f && $0~name {
f = 0
print $1 "," newage "," $3
next
}
{ f = 0; print }
- since awk needs to match on a pattern which is stored in a variable, use syntax
$0~VARIABLE
instead of/pattern/
. - if the
previous
pattern is found, setf = 1
. - when
f == 1
AND (here &&) the name is found, print the name (first field), the new age (stored in a variable), and the city. And do not forget to set f back to 0. - lastly reset f, print the line and keep going.
Using these scripts
Modifies the age of Jen in the line that follows Boston city.
$ modify.bash Jen Boston 99
same as before, but modifies the Jen after the "San Diego" line. This illustrates how to set
$previous
when there is a space involved.$ modify.bash Jen "San Diego" 99
same again, but this time specify the entire previous line. Since the pattern can be anything, as long as it matches, it is ok.
$ modify.bash Jen ",30,Boston" 99
CodePudding user response:
Using nested sed
expressions, the exact line of the match can be targetted. To target the second line starting with Jen
, the first must be excluded with 0,/^Jen/!
followed by a nested expression to prevent the command from affecting further matches.
$ sed '0,/^Jen/!{0,/^Jen/s/,/&31/}' input_file
Name,Age,City
Jen,40,San Diego
Jen,31,NYC
Bob,29,Miami,
,30,Boston
Jen,,NYC
Bob,21,Seattle
$ sed '0,/^Jen/!{0,/^Jen/!{0,/^Jen/s/,/&31/}}'
Name,Age,City
Jen,40,San Diego
Jen,,NYC
Bob,3129,Miami,
,3130,Boston
Jen,31,NYC
Bob,21,Seattle
CodePudding user response:
I would use GNU AWK
for this task following way, say you want set age of 2nd Jen,,NYC
to 35
and let textfile.csv
content be
Name,Age,City
Jen,40,San Diego
Jen,,NYC
Bob,29,Miami,
,30,Boston
Jen,,NYC
Bob,21,Seattle
then
awk -i inplace 'BEGIN{FS=OFS=","}$0=="Jen,,NYC"{cnt =1;if(cnt==2){$2=35}}{print}' textfile.csv
will alter textfile.csv
content to
Name,Age,City
Jen,40,San Diego
Jen,,NYC
Bob,29,Miami,
,30,Boston
Jen,35,NYC
Bob,21,Seattle
Explanation: I inform GNU AWK
that both field separator (FS
) and output field separator (OFS
) is ,
. Then if currently processed line ($0
) is Jen,,NYC
I increase cnt
value by 1
. if
value of cnt
is equal to 2
I set 2nd field value to be 35
. For each line, change or not, I print
it. You might elect to first run without -i inplace
so GNU AWK
will output to standard output, check if it did give what you desire, then apply -i inplace
.
(tested in gawk 4.2.1)