javascript - Hur man undviker sista tomma raden i Deno TextLineStream


Jag kör följande kod,

import { TextLineStream } from "https://deno.land/[email /streams/mod.ts";
const cmd1 = new Deno.Command("echo", {args:["foo\n\nbar"], stdout: "piped"}).spawn();
for await (const chunk of cmd1.stdout.pipeThrough(new TextDecoderStream).pipeThrough(new TextLineStream)) {
    console.log("result: ", chunk);
}

fick

result:  foo
result:
result:  bar
result:

vilja

result:  foo
result:
result:  bar

Hur kan jag upptäcka slutet på en ström och undvika en tom rad i slutet?
Tack för din hjälp.

Svar #1

Uppdatering: En fix för detta skapades i pull-förfrågan på denoland/deno_std#3103. Den blev sammanslagen och bör vara tillgänglig med början från std-biblioteksutgåvan version 0.173.0.


Typen av cmd1.stdout i din fråga är ReadableStream<Uint8Array>. Efter att ha kopplat detta genom de andra transformströmklasserna, blir det resulterande värdet ReadableStream<string>. När du använder en for await...of-loop är varje värde som returneras helt enkelt en rad (string), så du har ingen inneboende referens för att avgöra slutet av strömmen inom loopen för att kunna fatta beslut. (Detta skulle fortfarande vara sant även om du manuellt skulle iterera strömmens AsyncIterableIterator<string>.) När strömmen har slutat och loopen är klar, är det redan för sent att gå tillbaka och hantera den sista (tomma) raden.

Du kan dock hantera detta genom att lagra varje tidigare rad i en variabel utanför loopen och använda den tidigare raden inuti loopen (istället för den aktuella raden). Sedan, efter att loopen slutar, kontrollera om den sista tidigare raden är tom — om den inte är det, använd den precis som du gjorde med de andra raderna — om den är tom kan du helt enkelt ignorera den.

Nedan har jag inkluderat ett självständigt exempel som du kan köra på vilken plattform som helst utan behörigheter och se samma resultat — det är baserat på data i din fråga och jämför den exakta input du visade med ett par liknande inputs med olika typer av slut för att göra en jämförelse. Det inkluderar också radnummer som en bonus — de är användbara som en referensmekanism när man jämför mot standardbeteendet i TextLineStream. Den intressanta koden finns i klassen RefinedTextLineStream, och du kan helt enkelt använda den i din egen kod på detta sätt för att se det önskade resultatet som du beskrev:

const stream = cmd1.stdout
  .pipeThrough(new TextDecoderStream())
  .pipeThrough(new TextLineStream())
  .pipeThrough(new RefinedTextLineStream());

for await (const [line] of stream) {
  console.log("result: ", line);
}

Här är det reproducerbara exemplet:

so-74905946.ts:

import { TextLineStream } from "https://deno.land/[email /streams/text_line_stream.ts";

type LineWithNumber = [line: string, lineNumber: number];

/** For use with Deno's std library TextLineStream */
class RefinedTextLineStream extends TransformStream<string, LineWithNumber> {
  #lineNumber = 0;
  #previous = "";

  constructor() {
    super({
      transform: (textLine, controller) => {
        if (this.#lineNumber > 0) {
          controller.enqueue([this.#previous, this.#lineNumber]);
        }
        this.#lineNumber += 1;
        this.#previous = textLine;
      },
      flush: (controller) => {
        if (this.#previous.length > 0) {
          controller.enqueue([this.#previous, this.#lineNumber]);
        }
      },
    });
  }
}

const exampleInputs: [name: string, input: string][] = [
  ["input from question", "foo\n\nbar\n"],
  ["input with extra final line feed", "foo\n\nbar\n\n"],
  ["input without final line feed", "foo\n\nbar"],
];

for (const [name, input] of exampleInputs) {
  console.log(`\n${name}: ${JSON.stringify(input)}`);

  const [textLineStream1, textLineStream2] = new File([input], "untitled")
    .stream()
    .pipeThrough(new TextDecoderStream())
    .pipeThrough(new TextLineStream())
    .tee();

  console.log("\ndefault:");
  let lineNumber = 0;
  for await (const line of textLineStream1) {
    lineNumber += 1;
    console.log(lineNumber, line);
  }

  console.log("\nskipping empty final line:");
  const stream = textLineStream2.pipeThrough(new RefinedTextLineStream());
  for await (const [line, lineNumber] of stream) console.log(lineNumber, line);
}

Körning i terminalen:

% deno --version
deno 1.29.1 (release, x86_64-apple-darwin)
v8 10.9.194.5
typescript 4.9.4

% deno run so-74905946.ts  

input from question: "foo\n\nbar\n"

default:
1 foo
2 
3 bar
4 

skipping empty final line:
1 foo
2 
3 bar

input with extra final line feed: "foo\n\nbar\n\n"

default:
1 foo
2 
3 bar
4 
5 

skipping empty final line:
1 foo
2 
3 bar
4 

input without final line feed: "foo\n\nbar"

default:
1 foo
2 
3 bar

skipping empty final line:
1 foo
2 
3 bar