FOSS Unleashed

How to get sed to pull out a block

For a good long while, sed has been a great mystery to me. After-all, it is stylized after the infamous ed editor. Another thing that has eluded me fairly often, is how I can, from a shell prompt, print out a block of code, and only that block of code?

1
2
3
4
5
6
7
8
9
10
11
; sed '/cfg_services/,/)/p;d' /etc/rc.conf
cfg_services+=(
eudev
'mount' 'sysctl'
'@lo.iface' '@ntp' '@scron'
alsa
@agetty-tty{2..6}
sshd
@rc.local runit
nginx
)

Great. We’re looking for ‘cfg_services’, then we’re looking for the next ‘)’, printing those, then deleting all other output. But what if we had nested blocks of code? Assuming one has sane indentation, one can rely on that:

1
2
3
4
5
6
7
8
9
int main(void) {
if (foo) {
printf("Hello, World!\n");
}

if (bar) {
printf("Goodbye, World!\n");
}
}

Given that code, we want to print the foo check:

1
2
3
4
; sed '/if .foo/,/\t}/p;d' test.c
if (foo) {
printf("Hello, World!\n");
}

If we want both blocks, we can simplify the first regex:

1
2
3
4
5
6
7
; sed '/if (/,/\t}/p;d' test.c
if (foo) {
printf("Hello, World!\n");
}
if (bar) {
printf("Goodbye, World!\n");
}

Though this might not be as useful, since we can’t quite operate on each block individually.

Now, what if we wanted to add something to the block with sed? How would we do that? sed has an -i flag that allows it to modify the file it was given, however, we have to operate on the file differently.

1
2
3
4
5
6
7
8
9
10
11
12
; sed -i '/if (foo/,/\t}/ { /}/ i \\t\tfoo();'\n'}' test.c
; cat test.c
int main(void) {
if (foo) {
printf("Hello, World!\n");
foo();
}

if (bar) {
printf("Goodbye, World!\n");
}
}

A few things to note here. First, if you’re wanting to test your arguments to sed like this, then omit the -i and let it print out the entire file, or pipe that into another sed that is only printing the block you’re wanting to modify. Second thing to note is that -i supresses sed’s behavior to output to stdout, it is printing back into the file it read from instead. Thus to see the results, we have to print the file out again with cat.

But how does it work? We’re using the same pattern matching as before, but instead of having the address affect the p command, we’re having affect a command block in sed, which in this example is: '{ /}/ i \\t\tfoo();'\n'}'. Here, we’re giving a second address to work on, but this address is only within the context of the block we found, we’re looking for the closing brace, and running the i command on it. The i and a commands tell sed to insert and append respectively. If we did not give these commands a second address, they would run on all of the lines found with the first address range. Both commands are terminated with a newline, which my shell natively supports, but if you are using bash you will need to use $'\n' instead of the raw \n. We also have to provide an extra backslash to escape as both a and i will consume one.

Nice and simple. Hope everyone has a good week!