sed behaving strangely when -n and the delete command are combined

Rob Landley rob at landley.net
Fri Jul 10 07:23:25 UTC 2009


On Thursday 09 July 2009 08:50:31 Denys Vlasenko wrote:
> > That said, this change isn't nearly complex enough to justify 250 lines
> > worth of changes:
> >
> >  editors/sed.c       |  462
> > ++++++++++++++++++++++++++--------------------------
> >  testsuite/sed.tests |   13 +
> >  2 files changed, 252 insertions(+), 223 deletions(-)
> >
> > I only looked through the first two pages of that patch, which seemed to
> > be full of random unrelated stuff, and then started doing range
> > comparisons that don't seem right.  But the way this patch is structured,
> > it's really hard to see what you actually changed and what's noise.
>
> Please find attached patch which has indentation change filtered out.

Analyzing sed behavior always puts me in a bad mood.  It's so fiddly...

Ok, let's see:

>--- busybox.4/editors/sed.c 2009-07-09 15:47:57.000000000 +0200
>+++ busybox.5/editors/sed.c 2009-06-30 19:19:15.000000000 +0200
>@@ -865,21 +865,23 @@ static void process_files(void)
>    /* Prime the pump */
>    next_line = get_next_line(&next_gets_char);
>
>-   /* go through every line in each file */
>+   /* Go through every line in each file */
>  again:
>    substituted = 0;
>
>    /* Advance to next line.  Stop if out of lines. */
>    pattern_space = next_line;
>-   if (!pattern_space) return;
>+   if (!pattern_space)
>+       return;
>    last_gets_char = next_gets_char;
>
>    /* Read one line in advance so we can act on the last line,
>     * the '$' address */
>    next_line = get_next_line(&next_gets_char);
>    linenum++;
>+
>+   /* For every line, go through all the commands */
>  restart:
>-   /* for every line, go through all the commands */
>    for (sed_cmd = G.sed_cmd_head.next; sed_cmd; sed_cmd = sed_cmd->next) {
>        int old_matched, matched;

So the first page of the patch is still nothing but white noise...

>@@ -893,35 +894,51 @@ static void process_files(void)
>
>            || (!sed_cmd->beg_line && !sed_cmd->end_line
>
>                && !sed_cmd->beg_match && !sed_cmd->end_match)
>            /* Or did we match the start of a numerical range? */
>-           || (sed_cmd->beg_line > 0 && (sed_cmd->beg_line == linenum))
>+           || (sed_cmd->beg_line > 0 && (sed_cmd->beg_line == linenum
>+                           /* "shadowed beginning" case: "1d;1,ENDp" - p
> still matches at line 2 +                            * even though 1d
> skipped line 1 which is a start line for p */

It's somewhat disturbing to have a comment inside a nested parenthetical.  The 
indentation gives no hints so it's a bit hard to track the structure here.

I note the comment could just be one line:

/* "1d;1,ENDp" should start matching at line 2. */

> +                          
> || (sed_cmd->end_line && sed_cmd->beg_line < linenum && sed_cmd->end_line
> >= linenum)

This is more complicated than it needs to be.

If you're going to slavishly copy what gnu does, you might want to run a few 
more tests to see what that actually is.  Specifically:

  echo -e "one\ntwo\nthree\nfour" | sed -ne '2d;2,1p'
  three

So the logic can be simplified: when your start is a number it triggers on the 
next line >= to that number.   Just change the match start to >=, and then 
disable numerical starting matches are disabled after the first hit.  Beyond 
that, you want to fall through like it was already doing to the normal match 
ending logic, which has had <= linenum since my days.

So you can get closer to the gnu behavior by changing the code _less_.  Which 
means you don't have to avoid snapshotting the line and continuing on to the 
"should we end the match now" logic.

Also, why do you discard the comment about snapshotting the line?  The 
ordering is funky, it has to happen _between_ the match start and match end 
tests, which is why I added a comment to point it out (after it got moved to 
the wrong place twice, anyway).

> >+                           || (sed_cmd->end_match &&
> sed_cmd->beg_line < linenum) +                       )
>+           )
>            /* Or does this line match our begin address regex? */
>
>            || (beg_match(sed_cmd, pattern_space))
>
>            /* Or did we match last line of input? */
>
>            || (sed_cmd->beg_line == -1 && next_line == NULL);
>
>-       /* Snapshot the value */
>-
>        matched = sed_cmd->in_match;
>
>-       /* Is this line the end of the current match? */
>+       //bb_error_msg("cmd:'%c' matched:%d beg_line:%d end_line:%d
> linenum:%d", +       //sed_cmd->cmd, matched, sed_cmd->beg_line,
> sed_cmd->end_line, linenum);

Why are you adding commented out code?  If somebody wants to add printf() 
lines to dump values, do you think they'll really have a hard time doing so in 
future?  Does cluttering up the place with dead code make it more readable?

>+       /* Is this line the end of the current match? */
>        if (matched) {
>-           sed_cmd->in_match = !(
>+           int n = (
>                /* has the ending line come, or is this a single address
> command? */
> -               (sed_cmd->end_line ?
>+               sed_cmd->end_line ?
>                    sed_cmd->end_line == -1 ?
>                        !next_line
>
>                        : (sed_cmd->end_line <= linenum)
>                    :
>                    : !sed_cmd->end_match
>
>-               )
>+               );

I think this is more random cosmetic changes mixed in with the functional 
ones?  Except for not setting in_match immediately, which I think is actually 
a bad thing.

>+           if (!n) {
>                /* or does this line matches our last address regex */
>-               || (sed_cmd->end_match && old_matched
>+               n = (sed_cmd->end_match
>+                    && old_matched
>                     && (regexec(sed_cmd->end_match,
>-                                pattern_space, 0, NULL, 0) == 0))
>+                                pattern_space, 0, NULL, 0) == 0)
>            );
>+               if (n && sed_cmd->beg_line > 0) {
>+                   /* Once matched, "n,regex" range is dead, disabling it
> */ +                   regfree(sed_cmd->end_match);
>+                   free(sed_cmd->end_match);
>+                   sed_cmd->end_match = NULL;
>+               }
>+           }
>+           sed_cmd->in_match = !n;
>        }

Ok, buried in the random wordwrapping changes, you're now calling regfree in 
the middle of a sed, which seems way overkill.

If we're in a match, and beg_line > 0, set beg_line = -1.  That's all you need 
to do to prevent the match from triggering again after we end it.  You can 
even do that test every time, it's small and simple and really cheap, happens 
totally in L1 cache, and more or less parallelizes away on a modern processor 
with multiple execution cores.

>-       /* Skip blocks of commands we didn't match. */
>+       /* Skip blocks of commands we didn't match */
>        if (sed_cmd->cmd == '{') {
>            if (sed_cmd->invert ? matched : !matched) {
>                while (sed_cmd->cmd != '}') {
>@@ -934,7 +951,9 @@ static void process_files(void)
>        }
>

Noise...

>        /* Okay, so did this line match? */
>-       if (sed_cmd->invert ? !matched : matched) {
>+       if (sed_cmd->invert ? matched : !matched)
>+           continue; /* no */
>+

Um, not noise.  This is, in fact, a functional change totally unrelated to the 
rest of the patch of the kind that tends to cause subtle bugs, which I used to 
be very sensitive to in sed because it's so complicated and fiddly.

You left this change in, but didn't change the indentation of everything that 
used to be a block, so the indentation is now misleading.  (I realize this was 
probably fallout from trying to simplify the patch into something other people 
could read, but wouldn't it have been better roll back the above hunk as well 
when doing that?  I'm all for this kind of cleanup, but if something breaks in 
sed and I bisect it to this commit, it's headache time...)

On an unrelated note, instead of adding a comment "no" perhaps it would be 
better to change the comment before to "continue if this line didn't match"?

I believe the rest of the changes to this file are more white noise unrelated 
to the bug at hand.

Rob
-- 
Latency is more important than throughput. It's that simple. - Linus Torvalds


More information about the busybox mailing list