mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-12 08:27:27 +02:00
fix: handle comma-separated values with spaces between tokens
When users type commands like "-k 1, 1111" (with space after comma), shell-quote tokenizes this as separate tokens ["-k", "1,", "1111"]. Previously, the "1111" would be dropped. Added a collectCommaSeparatedTokens helper that looks ahead when a value ends with a trailing comma to consume additional tokens that appear to be continuations of the comma-separated list. Updated all comma-separated flag parsers (-k, -a, -e, -p, -P, -t, -d, --tag) to use this helper for consistent behavior. Added comprehensive tests for multi-token comma-separated values.
This commit is contained in:
@@ -1513,6 +1513,161 @@ describe("parseReqCommand", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("comma-separated values spanning multiple tokens", () => {
|
||||
describe("kind flag (-k)", () => {
|
||||
it("should parse kinds when space after comma creates separate tokens", () => {
|
||||
// Simulates input: -k 1, 1111 (where shell tokenizes as ["-k", "1,", "1111"])
|
||||
const result = parseReqCommand(["-k", "1,", "1111"]);
|
||||
expect(result.filter.kinds).toEqual([1, 1111]);
|
||||
});
|
||||
|
||||
it("should parse multiple tokens with trailing commas", () => {
|
||||
// Simulates input: -k 1, 3, 7 (tokenized as ["-k", "1,", "3,", "7"])
|
||||
const result = parseReqCommand(["-k", "1,", "3,", "7"]);
|
||||
expect(result.filter.kinds).toEqual([1, 3, 7]);
|
||||
});
|
||||
|
||||
it("should stop consuming tokens when hitting a flag", () => {
|
||||
// Simulates input: -k 1, -l 10 (tokenized as ["-k", "1,", "-l", "10"])
|
||||
const result = parseReqCommand(["-k", "1,", "-l", "10"]);
|
||||
expect(result.filter.kinds).toEqual([1]);
|
||||
expect(result.filter.limit).toBe(10);
|
||||
});
|
||||
|
||||
it("should stop consuming tokens when hitting a relay URL", () => {
|
||||
// Simulates input: -k 1, relay.com (tokenized as ["-k", "1,", "relay.com"])
|
||||
const result = parseReqCommand(["-k", "1,", "relay.com"]);
|
||||
expect(result.filter.kinds).toEqual([1]);
|
||||
expect(result.relays).toEqual(["wss://relay.com/"]);
|
||||
});
|
||||
|
||||
it("should handle mixed single token and multi-token values", () => {
|
||||
// Simulates input: -k 1,3, 7 (tokenized as ["-k", "1,3,", "7"])
|
||||
const result = parseReqCommand(["-k", "1,3,", "7"]);
|
||||
expect(result.filter.kinds).toEqual([1, 3, 7]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("author flag (-a)", () => {
|
||||
it("should parse authors when space after comma creates separate tokens", () => {
|
||||
const hex1 = "a".repeat(64);
|
||||
const hex2 = "b".repeat(64);
|
||||
// Simulates input: -a hex1, hex2 (tokenized as ["-a", "hex1,", "hex2"])
|
||||
const result = parseReqCommand(["-a", `${hex1},`, hex2]);
|
||||
expect(result.filter.authors).toEqual([hex1, hex2]);
|
||||
});
|
||||
|
||||
it("should stop consuming tokens when hitting a flag", () => {
|
||||
const hex = "a".repeat(64);
|
||||
const result = parseReqCommand(["-a", `${hex},`, "-k", "1"]);
|
||||
expect(result.filter.authors).toEqual([hex]);
|
||||
expect(result.filter.kinds).toEqual([1]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("event ID flag (-e)", () => {
|
||||
it("should parse event IDs when space after comma creates separate tokens", () => {
|
||||
const hex1 = "a".repeat(64);
|
||||
const hex2 = "b".repeat(64);
|
||||
const result = parseReqCommand(["-e", `${hex1},`, hex2]);
|
||||
expect(result.filter["#e"]).toEqual([hex1, hex2]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("pubkey tag flag (-p)", () => {
|
||||
it("should parse pubkeys when space after comma creates separate tokens", () => {
|
||||
const hex1 = "a".repeat(64);
|
||||
const hex2 = "b".repeat(64);
|
||||
const result = parseReqCommand(["-p", `${hex1},`, hex2]);
|
||||
expect(result.filter["#p"]).toEqual([hex1, hex2]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("uppercase P tag flag (-P)", () => {
|
||||
it("should parse pubkeys when space after comma creates separate tokens", () => {
|
||||
const hex1 = "a".repeat(64);
|
||||
const hex2 = "b".repeat(64);
|
||||
const result = parseReqCommand(["-P", `${hex1},`, hex2]);
|
||||
expect(result.filter["#P"]).toEqual([hex1, hex2]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("hashtag flag (-t)", () => {
|
||||
it("should parse hashtags when space after comma creates separate tokens", () => {
|
||||
// Simulates input: -t nostr, bitcoin (tokenized as ["-t", "nostr,", "bitcoin"])
|
||||
const result = parseReqCommand(["-t", "nostr,", "bitcoin"]);
|
||||
expect(result.filter["#t"]).toEqual(["nostr", "bitcoin"]);
|
||||
});
|
||||
|
||||
it("should parse multiple tokens with trailing commas", () => {
|
||||
// Simulates input: -t nostr, bitcoin, lightning
|
||||
const result = parseReqCommand([
|
||||
"-t",
|
||||
"nostr,",
|
||||
"bitcoin,",
|
||||
"lightning",
|
||||
]);
|
||||
expect(result.filter["#t"]).toEqual(["nostr", "bitcoin", "lightning"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("d-tag flag (-d)", () => {
|
||||
it("should parse d-tags when space after comma creates separate tokens", () => {
|
||||
const result = parseReqCommand(["-d", "article1,", "article2"]);
|
||||
expect(result.filter["#d"]).toEqual(["article1", "article2"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("generic tag flag (--tag, -T)", () => {
|
||||
it("should parse generic tag values when space after comma creates separate tokens", () => {
|
||||
// Simulates input: --tag a val1, val2 (tokenized as ["--tag", "a", "val1,", "val2"])
|
||||
const result = parseReqCommand(["--tag", "a", "val1,", "val2"]);
|
||||
expect(result.filter["#a"]).toEqual(["val1", "val2"]);
|
||||
});
|
||||
|
||||
it("should stop at flags", () => {
|
||||
const result = parseReqCommand(["--tag", "a", "val1,", "-k", "1"]);
|
||||
expect(result.filter["#a"]).toEqual(["val1"]);
|
||||
expect(result.filter.kinds).toEqual([1]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("complex scenarios with multi-token values", () => {
|
||||
it("should handle multiple flags with trailing commas", () => {
|
||||
const hex = "a".repeat(64);
|
||||
const result = parseReqCommand([
|
||||
"-k",
|
||||
"1,",
|
||||
"3",
|
||||
"-a",
|
||||
`${hex}`,
|
||||
"-t",
|
||||
"nostr,",
|
||||
"bitcoin",
|
||||
]);
|
||||
expect(result.filter.kinds).toEqual([1, 3]);
|
||||
expect(result.filter.authors).toEqual([hex]);
|
||||
expect(result.filter["#t"]).toEqual(["nostr", "bitcoin"]);
|
||||
});
|
||||
|
||||
it("should handle trailing comma at end of input", () => {
|
||||
// No more tokens to consume, should just parse what we have
|
||||
const result = parseReqCommand(["-k", "1,"]);
|
||||
expect(result.filter.kinds).toEqual([1]);
|
||||
});
|
||||
|
||||
it("should not consume relay URLs as value continuations", () => {
|
||||
const result = parseReqCommand([
|
||||
"-t",
|
||||
"nostr,",
|
||||
"wss://relay.damus.io",
|
||||
]);
|
||||
expect(result.filter["#t"]).toEqual(["nostr"]);
|
||||
expect(result.relays).toEqual(["wss://relay.damus.io/"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("edge cases", () => {
|
||||
it("should handle empty args", () => {
|
||||
const result = parseReqCommand([]);
|
||||
|
||||
@@ -48,6 +48,56 @@ function parseCommaSeparated<T>(
|
||||
return addedAny;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a value ends with a trailing comma, indicating more values may follow in the next token
|
||||
*/
|
||||
function hasTrailingComma(value: string): boolean {
|
||||
return value.trimEnd().endsWith(",");
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a token looks like a flag (starts with -)
|
||||
*/
|
||||
function isFlag(token: string): boolean {
|
||||
return token.startsWith("-");
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect all comma-separated values that may span multiple tokens.
|
||||
* Handles cases like "-k 1, 1111" where "1," and "1111" are separate tokens.
|
||||
* Returns the merged value string and the number of additional tokens consumed.
|
||||
*/
|
||||
function collectCommaSeparatedTokens(
|
||||
currentValue: string,
|
||||
args: string[],
|
||||
startIndex: number,
|
||||
): { mergedValue: string; tokensConsumed: number } {
|
||||
let mergedValue = currentValue;
|
||||
let tokensConsumed = 0;
|
||||
let lookIndex = startIndex;
|
||||
|
||||
// Keep consuming tokens while the current value ends with a trailing comma
|
||||
// and the next token is not a flag
|
||||
while (hasTrailingComma(mergedValue) && lookIndex < args.length) {
|
||||
const nextToken = args[lookIndex];
|
||||
// Stop if the next token is a flag or looks like a relay URL
|
||||
if (
|
||||
isFlag(nextToken) ||
|
||||
isRelayDomain(nextToken) ||
|
||||
nextToken.startsWith("wss://") ||
|
||||
nextToken.startsWith("ws://")
|
||||
) {
|
||||
break;
|
||||
}
|
||||
// Append the next token to our value
|
||||
mergedValue = mergedValue + nextToken;
|
||||
tokensConsumed++;
|
||||
lookIndex++;
|
||||
}
|
||||
|
||||
return { mergedValue, tokensConsumed };
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse REQ command arguments into a Nostr filter
|
||||
* Supports:
|
||||
@@ -111,20 +161,26 @@ export function parseReqCommand(args: string[]): ParsedReqCommand {
|
||||
switch (flag) {
|
||||
case "-k":
|
||||
case "--kind": {
|
||||
// Support comma-separated kinds: -k 1,3,7
|
||||
// Support comma-separated kinds: -k 1,3,7 or -k 1, 3, 7 (with spaces)
|
||||
if (!nextArg) {
|
||||
i++;
|
||||
break;
|
||||
}
|
||||
const addedAny = parseCommaSeparated(
|
||||
// Collect values that may span multiple tokens (e.g., "-k 1, 1111")
|
||||
const { mergedValue, tokensConsumed } = collectCommaSeparatedTokens(
|
||||
nextArg,
|
||||
args,
|
||||
i + 2,
|
||||
);
|
||||
const addedAny = parseCommaSeparated(
|
||||
mergedValue,
|
||||
(v) => {
|
||||
const kind = parseInt(v, 10);
|
||||
return isNaN(kind) ? null : kind;
|
||||
},
|
||||
kinds,
|
||||
);
|
||||
i += addedAny ? 2 : 1;
|
||||
i += addedAny ? 2 + tokensConsumed : 1;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -135,8 +191,14 @@ export function parseReqCommand(args: string[]): ParsedReqCommand {
|
||||
i++;
|
||||
break;
|
||||
}
|
||||
// Collect values that may span multiple tokens (e.g., "-a npub1, npub2")
|
||||
const { mergedValue, tokensConsumed } = collectCommaSeparatedTokens(
|
||||
nextArg,
|
||||
args,
|
||||
i + 2,
|
||||
);
|
||||
let addedAny = false;
|
||||
const values = nextArg.split(",").map((a) => a.trim());
|
||||
const values = mergedValue.split(",").map((a) => a.trim());
|
||||
for (const authorStr of values) {
|
||||
if (!authorStr) continue;
|
||||
// Check for $me and $contacts aliases (case-insensitive)
|
||||
@@ -167,7 +229,7 @@ export function parseReqCommand(args: string[]): ParsedReqCommand {
|
||||
}
|
||||
}
|
||||
}
|
||||
i += addedAny ? 2 : 1;
|
||||
i += addedAny ? 2 + tokensConsumed : 1;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -190,8 +252,15 @@ export function parseReqCommand(args: string[]): ParsedReqCommand {
|
||||
break;
|
||||
}
|
||||
|
||||
// Collect values that may span multiple tokens (e.g., "-e note1, nevent1")
|
||||
const { mergedValue, tokensConsumed } = collectCommaSeparatedTokens(
|
||||
nextArg,
|
||||
args,
|
||||
i + 2,
|
||||
);
|
||||
|
||||
let addedAny = false;
|
||||
const values = nextArg.split(",").map((v) => v.trim());
|
||||
const values = mergedValue.split(",").map((v) => v.trim());
|
||||
|
||||
for (const val of values) {
|
||||
if (!val) continue;
|
||||
@@ -216,7 +285,7 @@ export function parseReqCommand(args: string[]): ParsedReqCommand {
|
||||
}
|
||||
}
|
||||
|
||||
i += addedAny ? 2 : 1;
|
||||
i += addedAny ? 2 + tokensConsumed : 1;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -226,8 +295,11 @@ export function parseReqCommand(args: string[]): ParsedReqCommand {
|
||||
i++;
|
||||
break;
|
||||
}
|
||||
// Collect values that may span multiple tokens (e.g., "-p npub1, npub2")
|
||||
const { mergedValue: mergedValueP, tokensConsumed: tokensConsumedP } =
|
||||
collectCommaSeparatedTokens(nextArg, args, i + 2);
|
||||
let addedAny = false;
|
||||
const values = nextArg.split(",").map((p) => p.trim());
|
||||
const values = mergedValueP.split(",").map((p) => p.trim());
|
||||
for (const pubkeyStr of values) {
|
||||
if (!pubkeyStr) continue;
|
||||
// Check for $me and $contacts aliases (case-insensitive)
|
||||
@@ -258,7 +330,7 @@ export function parseReqCommand(args: string[]): ParsedReqCommand {
|
||||
}
|
||||
}
|
||||
}
|
||||
i += addedAny ? 2 : 1;
|
||||
i += addedAny ? 2 + tokensConsumedP : 1;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -269,8 +341,13 @@ export function parseReqCommand(args: string[]): ParsedReqCommand {
|
||||
i++;
|
||||
break;
|
||||
}
|
||||
// Collect values that may span multiple tokens (e.g., "-P npub1, npub2")
|
||||
const {
|
||||
mergedValue: mergedValuePU,
|
||||
tokensConsumed: tokensConsumedPU,
|
||||
} = collectCommaSeparatedTokens(nextArg, args, i + 2);
|
||||
let addedAny = false;
|
||||
const values = nextArg.split(",").map((p) => p.trim());
|
||||
const values = mergedValuePU.split(",").map((p) => p.trim());
|
||||
for (const pubkeyStr of values) {
|
||||
if (!pubkeyStr) continue;
|
||||
// Check for $me and $contacts aliases (case-insensitive)
|
||||
@@ -301,19 +378,24 @@ export function parseReqCommand(args: string[]): ParsedReqCommand {
|
||||
}
|
||||
}
|
||||
}
|
||||
i += addedAny ? 2 : 1;
|
||||
i += addedAny ? 2 + tokensConsumedPU : 1;
|
||||
break;
|
||||
}
|
||||
|
||||
case "-t": {
|
||||
// Support comma-separated hashtags: -t nostr,bitcoin,lightning
|
||||
if (nextArg) {
|
||||
// Collect values that may span multiple tokens (e.g., "-t nostr, bitcoin")
|
||||
const {
|
||||
mergedValue: mergedValueT,
|
||||
tokensConsumed: tokensConsumedT,
|
||||
} = collectCommaSeparatedTokens(nextArg, args, i + 2);
|
||||
const addedAny = parseCommaSeparated(
|
||||
nextArg,
|
||||
mergedValueT,
|
||||
(v) => v, // hashtags are already strings
|
||||
tTags,
|
||||
);
|
||||
i += addedAny ? 2 : 1;
|
||||
i += addedAny ? 2 + tokensConsumedT : 1;
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
@@ -323,12 +405,17 @@ export function parseReqCommand(args: string[]): ParsedReqCommand {
|
||||
case "-d": {
|
||||
// Support comma-separated d-tags: -d article1,article2,article3
|
||||
if (nextArg) {
|
||||
// Collect values that may span multiple tokens (e.g., "-d art1, art2")
|
||||
const {
|
||||
mergedValue: mergedValueD,
|
||||
tokensConsumed: tokensConsumedD,
|
||||
} = collectCommaSeparatedTokens(nextArg, args, i + 2);
|
||||
const addedAny = parseCommaSeparated(
|
||||
nextArg,
|
||||
mergedValueD,
|
||||
(v) => v, // d-tags are already strings
|
||||
dTags,
|
||||
);
|
||||
i += addedAny ? 2 : 1;
|
||||
i += addedAny ? 2 + tokensConsumedD : 1;
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
@@ -410,14 +497,20 @@ export function parseReqCommand(args: string[]): ParsedReqCommand {
|
||||
genericTags.set(letter, tagSet);
|
||||
}
|
||||
|
||||
// Collect values that may span multiple tokens (e.g., "--tag a val1, val2")
|
||||
const {
|
||||
mergedValue: mergedValueTag,
|
||||
tokensConsumed: tokensConsumedTag,
|
||||
} = collectCommaSeparatedTokens(valueArg, args, i + 3);
|
||||
|
||||
// Parse comma-separated values
|
||||
const addedAny = parseCommaSeparated(
|
||||
valueArg,
|
||||
mergedValueTag,
|
||||
(v) => v, // tag values are already strings
|
||||
tagSet,
|
||||
);
|
||||
|
||||
i += addedAny ? 3 : 1;
|
||||
i += addedAny ? 3 + tokensConsumedTag : 1;
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user