Take the 2-minute tour ×
Unix & Linux Stack Exchange is a question and answer site for users of Linux, FreeBSD and other Un*x-like operating systems.. It's 100% free, no registration required.

Can sed replace text with a string formatted like printf's formatted printing?

The following sed command replaces a line starting with the current value of "$domain" with several values specified in variables.

/bin/sed  "s/\(^${domain} *${limittype} * ${limititem}.*\)/$EXPL#\1\n${domain} ${limittype} ${limititem} ${value}/" /etc/security/limits.conf

However the output will not be properly aligned because the length of the values of domain etc. are not the same.

So the output would be something like

#oracle   hard   nproc    131072
oracle hard nproc 666

While valid, it is difficult to read. I would prefer to get something like

#oracle   hard   nproc    131072
oracle   hard   nproc    666

The best I can come with to get the desired output is:

/bin/sed  "s/\(^${domain}\)\( *\)\(${limittype}\)\( *\)\(${limititem}\)\( *\)\(.*\)/$EXPL#\1\2\3\4\5\6\7\n${domain}\2${limittype}\4${limititem}\6${value}/" /etc/security/limits.conf

But I believe there must a more elegant way to do this.

The sed one liners document contains some examples that use a specified number of characters, e.g.

sed -e :a -e 's/^.\{1,78\}$/ &/;ta'  # set at 78 plus 1 space

But this is in the regexp section not in the replacement section.

share|improve this question
    
use tabs instead of spaces in the replacement? –  Kevin May 31 '12 at 13:27
    
Can you please provide an example input section and its corresponding preferred output? Your $EXPL#\1\n bit seems wrong. –  Mikel Jun 1 '12 at 3:16
    
No. sed can't do this in any way that is practical. If you can provide a clearer description of the problem, many people here can provide an alternative solution using bash, awk, or any number of other tools. –  Mikel Jun 1 '12 at 3:18
add comment

3 Answers

up vote 4 down vote accepted

This uses the extended regex syntax -r, which clears up a lot of the clutter. Also, because you already know some of the field values, you don't actually need to back-reference them, again reducing clutter (and overhead).

& is a special replacement value: it hold the entire matched pattern. Using the &, again reduces clutter. As it is not a back-reference, it has significantly less overhead.

I've used ( +) vs. ( *). The + assumes that there is at least one space between input fields. Just change it to the * it that is not the case.

EXPL=
dom=oracle
typ=hard
itm=nproc
val=666

echo "oracle   hard   nproc    131072" |
  sed -r "s/^$dom( +)$typ( +)$itm( +).*/$EXPL#&\n$dom\1$typ\2$itm\3$val/" 

output

#oracle   hard   nproc    131072
oracle   hard   nproc    666
share|improve this answer
    
Great! Does exactly what I want and thanks for the explanation on &. –  Bram Jun 1 '12 at 7:20
add comment

While you can theoretically do this entirely in sed (since it's Turing-complete), this isn't the right tool for the job.

An easy way is to insert tabs in sed, then post-process them to spaces. If you can determine the position of all columns, pipe the sed output through expand.

</etc/security/limits.conf \
sed  "s/\(^${domain} *${limittype} * ${limititem}.*\)/$EXPL#\1\n${domain}\t${limittype}\t${limititem}\t${value}/" |
expand -t 10,17,26

(Use a literal tab character instead of \t if your sed doesn't support \t.)

If you don't know the column widths in advance, try the BSD column utility. It looks at the whole input file to determine column widths that accommodate the length of all rows.

</etc/security/limits.conf \
sed  "s/\(^${domain} *${limittype} * ${limititem}.*\)/$EXPL#\1 ${domain} ${limittype} ${limititem} ${value}/" |
column -t

If your sed script rewrites both commented-out lines and non-commented-out lines, or if you use column, you'll need a bit of post-processing to skew the commented-out lines by the width of the comment marker.

… | sed '/^#/ s/ //'

You can use awk instead. It has a printf function. As an added bonus, there is an easy way to protect special characters such as . or * in a searched-for column content.

</etc/security/limits.conf awk -v domain="$domain" -v limittype="$limittype" -v limititem="$limititem" -v value="$value" '
$1 == domain && $2 == limittype && $3 == limititem  {
    printf "#%-9s %-8s %-9s %s\n%-9s %-8s %-9s %s\n", $1, $2, $3, $4, $1, $2, $3, value; next
}
1 {print}
'
share|improve this answer
    
Or possibly column -t. –  Mikel Jun 1 '12 at 3:16
    
I'll play with the awk version a bit, just as an excercise but right now I can't get it to work. Thanks for the elaborate answer! –  Bram Jun 1 '12 at 7:42
    
@Mikel Oh, thanks for remininding me. I meant to say “expand if you know the width, column if you don't” and forgot to write a paragraph. –  Gilles Jun 1 '12 at 7:50
    
@Bram I missed out a bit in the awk snippet to print out the input if it doesn't match, try now. –  Gilles Jun 1 '12 at 7:50
    
@Gilles: I edited the answer, removing the check on value because $value contains the new value and the proposed version would never match a line. I also added an extra line of input so a comment would be written showing the old value. Thanks again. –  Bram Jun 1 '12 at 8:20
add comment

Just use printf to format sed output:

printf "%5s %12s %4s\n" $(sed 's/.../.../')
share|improve this answer
1  
I've seen people embed printf into sed, like sed 's/xxx/$(printf "%5s %12s %4s\n")yyy/' but is it doing a sub-process for each line processed? If so then doing the OPs problem in awk, using it's printf feature probably makes more sense. Good luck to all –  shellter May 31 '12 at 18:14
add comment

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.