Ali
Ali

Reputation: 1621

Replacing a parameter in bash using sed

Trying to clean up several dozen redundant nagios config files, but sed isn't working for me (yes I'm fairly new to bash), here's the string I want to replace:

use                             store-service
host_name                       myhost
service_description             HTTP_JVM_SYM_DS
check_command                   check_http!'-p 8080 -N -u /SymmetricDS/app'
check_interval                  1

with this:

 use                            my-template-service    
 host_name                      myhost

just the host_name should stay unchanged since it'll be different for each file. Any help will be greatly appreciated. Tried escaping the ' and !, but get this error -bash: !'-p: event not found

Thanks

Upvotes: 1

Views: 137

Answers (3)

dawg
dawg

Reputation: 103714

Assuming you can glob the files to select on the log files of interest, I would first filter the files that you want to replace to be limited to five lines.

You can do that with Bash and awk:

for fn in *; do    # make that glob apply to your files...
    [[ -e "$fn"  &&  -f "$fn" &&  -s "$fn" ]] || continue
    line_cnt=$(awk 'FNR==NR{next}
                END {print NR}' "$fn")
    (( line_cnt == 5 )) || continue

    # at this point you only have files with 5 lines of text...
done

Once you have done that, you can add another awk to the loop to make the replacements:

for fn in *; do
    [[ -e "$fn"  &&  -f "$fn" &&  -s "$fn" ]] || continue
    line_cnt=$(awk -v l=5 'FNR==NR{next}
                END {print NR}' "$fn")
    (( line_cnt == 5 )) || continue

    awk 'BEGIN{tgt["use"]="my-template-service"
             tgt["host_name"]=""} 

         $1 in tgt {  if (tgt[$1]=="") s=$2
                else s=tgt[$1]
                printf "%-33s%s\n", $1, s
         }
         ' "$fn"
done

Upvotes: 1

MiniMax
MiniMax

Reputation: 1093

This is the GNU sed solution, check it. Backup your files before testing.

#!/bin/bash

# You should escape all special characters in this string (like $, ^, /, {, }, etc),
# which you need interpreted literally, not as regex - by the backslash.
# Your original string was contained only slashes from this list, but
# I decide don't escape them by backslashes, but change sed's s/pattern/replace/ 
# command to the s|patter|replace|. You can pick any more fittable character.
needle="use\s{1,}store-service\n\
host_name\s{1,}myhost\n\
service_description\s{1,}HTTP_JVM_SYM_DS\n\
check_command\s{1,}check_http!'-p 8080 -N -u /SymmetricDS/app'\n\
check_interval\s{1,}1"

replacement="use                             my-template-service\n\
host_name                       myhost"

# This echo command displays the generated substitute command,
# which will be used by sed 
# uncomment it for viewing
# echo "s/$needle/$replacement/" 

# for changing the file in place add the -i option.
sed -r "
/use\s{1,}store-service/ {
    N;N;N;N;    
    s|$needle|$replacement|
}" input.txt

Input

one
two
use                             store-service
host_name                       myhost
service_description             HTTP_JVM_SYM_DS
check_command                   check_http!'-p 8080 -N -u /SymmetricDS/app'
check_interval                  1
three
four

Output

one
two
use                             my-template-service
host_name                       myhost
three
four

Upvotes: 0

Zac B
Zac B

Reputation: 4232

Disclaimer: This question is somewhat light on info and rings a bit like "write my code for me". In good faith I'm assuming that it's not that, so I am answering in hopes that this can be used to learn more about text processing/regex substitutions in general, and not just to be copy-pasted somewhere and forgotten.

I suggest using perl instead of sed. While sed is often the right tool for the job, in this case I think Perl's better, for the following reasons:

  • Perl lets you easily do multi-line matches on a regex. This is possible with sed, but difficult (see this question for more info).
  • With multiple lines and complex delimiters and quote characters, sed starts to display different behavior depending on what platform you're using it on. For example, trying to do this with sed in "sorta multiline" mode gave me different results on OSX versus Linux (really GNU sed vs BSD sed). When using semi-advanced functionality like that, I'd stick with a tool that behaves consistently across platforms, which Perl does in this case.
  • Perl lets you deal with ASCII values and other special characters without a ton of "toothpick tower" escaping or subshelling. Since it's convenient to use ASCII values to match the single quotes in your pattern (we could use mixed double and single quotes instead, but that makes it harder to copy/paste this command into, say, a subshell or an eval'd part of a script), it's better to use a tool that supports this without extra hassle. It's possible with sed, but tricky; see this article for more info.
  • In sed/BRE, doing something as simple as a "one or more" match usually requires escaping special characters, aka [[:space:]]\{1,\}, which gets tedious. Since it's convenient to use a lot of repetition/grouping characters in this pattern, I prefer Perl for conciseness in this case, since it improves clarity of the matching code.
  • Perl lets you write comments in regex statements in one-liner mode via the x modifier. For big, multiline patterns like this one, having the pattern broken up and commented for readability really helps if you ever need to go back and change it. sed has comments too, but using them in single-pasteable-command mode (as opposed to a file of sed script code) can be tricky, and can result in less readable commands.

Anyway, following is the matcher I came up with. It's commented inline as much as I can make it, but the non-commented parts are explained here:

  • The -0777 switch tells perl to consume input files whole before processing them, rather than operating line-by-line. See perlrun for more info on this and the other flags. Thanks to @glennjackman for pointing this out in the comments on the original question!
  • The -p switch tells Perl to read STDIN until it sees a delimiter (which is end-of-input as set by -0777), run the program supplied, and print that program's return value before shutting down. Since our "program" is just a string substitution statement, its return value is the substituted string.
  • The -e switch tells perl to evaluate the next string argument for a program to run, rather than finding a script file or similar.
  • Input is piped from mytext.txt, which could be a file containing your pattern. You could also pipe input to Perl e.g. via cat mytext.txt | perl ... and it would work exactly the same way.
  • The regex modifiers work as follows: I use the multiline m modifier to match more than one \n-delimited statement, and the extended x modifier so we can have comments and turn off matching of literal whitespace, for clarity. You could get rid of comments and literal whitespace and splat it all into one line if you wanted, but good luck making any changes after you've forgotten what it does. See perlre for more info on these modifiers.

This command will replace the literal string you supplied, in a file that contains it (it can have more than just that string before/after it; only that block of text will be manipulated). It is less than literal in one minor way: it allows any number (one or more) of space characters between the first and second words in each line. If I remember Nagios configs, the number of spaces doesn't particularly matter anyway.

This command will not change the contents of a file it is supplied. If a file does not match the pattern, its contents will be printed out unchanged by this command. If it contains that pattern, the replaced contents will be printed out. You can write those contents to a new file, or do anything you like with them.

perl -0777pe '
# Use the pipe "|" character as an expression delimiter, since 
# the pattern contains slashes.
s|
    # 'use', one or more space-equivalent characters, and then 'store-service',
    # on one line.
    use \s+ store-service \n 
    # Open a capturing group.
    (
        # Capture the host name line in its entirety, then close the group.
        host_name \s+ \S+ 
    # Close the group and end the line.
    ) \n
    service_description \s+ HTTP_JVM_SYM_DS \n
    # Look for check_command, spaces, and check_http!, but keep matching on the
    # same line.
    check_command \s+ check_http!
        # Look for a single quote character by ASCII value, since shell
        # escaping these can be ugly/tricky, and makes your code less copy-
        # pasteable in/out of scripts/subcommands.
        \047
        # Look for the arguments to check_http, delimited by explicit \s
        # spaces, since we are in "extended" mode in order to be able to write
        # these comments and the expression on multiple lines.
        -p \s 8080 \s -N \s -u \s /SymmetricDS/app
        # Look for another single quote and the end of the line.
        \047 \n
    check_interval \s+ 1\n
# Replace all of the matched text with the "use my-template-service" line,
# followed by the contents of the first matching group (the host_name line).
# You could capture the "use" statement in another group, or use e.g.
# sprintf() to align fields here instead of a big literal space line, but
# this is the simplest, most obvious way to get the replacement done.
|use                             my-template-service\n$1|mx
' < mytext.txt

Upvotes: 2

Related Questions