diff --git a/barg.js b/barg.js index f7f1abf..de31449 100644 --- a/barg.js +++ b/barg.js @@ -1,222 +1,47 @@ -const ELEMENTS_SEP = ";"; -const ALIAS_SEP = ","; -const ATTR_SEP = ":"; - -let var_values = {}; -let required_values = []; - -let schema = {}; -let alias_map = {}; - -let position = 0; -let level = 1; -let commands = ["root"]; - -function register_command(raw_entry) { - const header = raw_entry.split(ELEMENTS_SEP).slice(1); - const aliases = header.shift().split(ALIAS_SEP); - const name = aliases[0]; - const help = header.shift(); - - // Schema - schema[ref(name, "type")] = "command"; - schema[ref(name, "help")] = help; - - // Alias - for (const alias of aliases) { - alias_map[ref(`unk::${alias}`)] = ref(name); - } - - // Control - commands.push(name); - level += 1; - position = 0; +function log(text) { + console.log(JSON.stringify(text, null, 2)); } -function end_command() { - level -= 1; - commands.pop(); +function join(...rest) { + return rest.filter((v) => !!v).join("."); } -function register_argument(raw_entry) { - const elements = raw_entry.split(ELEMENTS_SEP).slice(1); - let aliases = elements.shift().split(ALIAS_SEP); - let name = aliases[0]; - const entry = { - required: false, - kind: "positional", - }; - for (const element of elements) { - const [attribute, attr_value] = element.split(ATTR_SEP); - - if (attribute === "required") entry.required = true; - else if (attribute === "repeatable") entry.repeatable = true; - else if (attribute === "help") entry.help = attr_value; - else if (attribute === "default") entry.default_value = attr_value; - else if (attribute === "dest") entry.dest = attr_value; - else if (attribute === "type") entry.kind = attr_value; - - // Defaults - if (entry.dest === undefined) entry.dest = name; - if (entry.value === undefined) entry.value = entry.default_value; - } - - // Alias - if (entry.kind === "positional") { - name = String(position++); - aliases = [`pos::${name}`]; - } else if (entry.kind === "rest") { - aliases = ["__rest"]; - } - - for (const alias of aliases) { - if (entry.kind === "option" || entry.kind === "flag") - alias_map[ref(`narg::${alias}`)] = ref(name); - else alias_map[ref(alias)] = ref(name); - } - - // Schema - schema[ref(name, "type")] = "argument"; - for (const [key, value] of Object.entries(entry)) { - schema[ref(name, key)] = value; - } +function take_one(list) { + return list.shift(); } -function generate_schema(spec) { - for (const entry of spec) { - const elements = entry.split(ELEMENTS_SEP); - const type = elements.shift(); - - switch (type) { - case "command": - register_command(entry); - break; - case "end": - end_command(); - break; - case "argument": - register_argument(entry); - break; - default: - throw new Error(`Invalid entry type: '${type}'`); - } - } - - position = 0; - level = 1; - commands = ["root"]; -} - -function parse_input(spec, input) { - // rest - var_values = {}; - required_values = []; - position = 0; - level = 1; - commands = ["root"]; - schema = {}; - alias_map = {}; - - // code - generate_schema(spec); - const tokens = tokenize(input); - - // log({ schema, alias_map }); - - function next_token() { - const tagged_token = tokens.shift(); - const [type, token] = tagged_token.split("::"); - return { original: tagged_token, type, token }; - } - - function next_value() { - const tagged_token = next_token(); - if (tagged_token.type !== "unk") { - throw new Error(`Expected option value, but got: ${tagged_token.token}`); - } - return tagged_token.token; - } - - while (tokens.length > 0) { - const { - original: token, - type: token_type, - token: token_value, - } = next_token(); - if (token_type === "rest") { - var_values["__rest"] = token_value; - continue; - } - - let found = false; - for (let i = commands.length; i >= 1; i--) { - level = i; - let entry_ref = alias_map[ref(token)]; - - if (!entry_ref && level === commands.length && token_type === "unk") { - entry_ref = alias_map[ref(`pos::${position++}`)]; - } - - if (!entry_ref) { - continue; - } - - const [_token_type, token_value] = token.split("::"); - const type = schema[join(entry_ref, "type")]; - if (type === "command") { - commands.push(token_value); - level += commands.length; - } else if (type === "argument") { - const kind = schema[join(entry_ref, "kind")]; - const dest = schema[join(entry_ref, "dest")]; - const value = schema[join(entry_ref, "value")]; - - var_values[dest] = value; // default one? - - if (kind === "flag") var_values[dest] = true; - else if (kind === "option") var_values[dest] = next_value(); - else if (kind === "positional") var_values[dest] = token_value; - else if (kind === "rest") var_values[dest] = token_value; - } - - found = true; - break; - } - - if (!found) { - console.log(`(Skipping invalid argument: '${token_value}'`); - } - } - - log({ commands, var_values }); - return var_values; +function take_all(list) { + const all = [...list]; + list.length = 0; + return all; } function tokenize(input) { - const raw_tokens = input.split(/\s+/); - raw_tokens.shift(); // drop command - + const raw_tokens = input.trim().split(/\s+/).slice(1); // skip command const tokens = []; - while (raw_tokens.length > 0) { - let token = take_token(raw_tokens); + + while (raw_tokens.length) { + const raw_token = take_one(raw_tokens); + + if (raw_token === "--") { + tokens.push(`rest::${take_all(raw_tokens).join(" ")}`); + break; + } + + let key; let value; - if (token === "--") { - tokens.push(`rest::${take_all(raw_tokens).join(" ")}`); - continue; - } - - if (token.startsWith("--")) { - [token, value] = token.slice(2).split("="); - tokens.push(`narg::${token}`); - } else if (token.startsWith("-")) { - [token, value] = token.slice(1).split("="); - tokens.push(...token.split("").map((t) => `narg::${t}`)); + if (raw_token.startsWith("--")) { + [key, value] = raw_token.slice(2).split("="); + tokens.push(`narg::${key}`); + } else if (raw_token.startsWith("-")) { + [key, value] = raw_token.slice(1).split("="); + tokens.push(...key.split("").map((k) => `narg::${k}`)); } else { - tokens.push(`unk::${token}`); + tokens.push(`unk::${raw_token}`); } - if (value) { + if (value !== undefined) { tokens.push(`value::${value}`); } } @@ -224,27 +49,180 @@ function tokenize(input) { return tokens; } -function take_token(tokens) { - return tokens.shift(); +function parse_input(spec, input) { + const tokens = tokenize(input); + const values = {}; + + const schema = {}; + const alias_map = {}; + + let position = 0; + let level = 1; + let commands = ["root"]; + + function ref(...rest) { + return commands.slice(0, level).concat(rest).join("."); + } + + function get_entry_ref(key) { + return alias_map[ref(key)]; + } + + function get_entry_item(...keys) { + return schema[join(...keys)]; + } + + function generate_schema() { + for (const entry of spec) { + const elements = entry.split(";"); + const type = elements.shift(); + + switch (type) { + case "command": { + const aliases = elements.shift().split(","); + const name = aliases[0]; + const help = elements.shift(); + + // Schema + schema[ref(name, "type")] = "command"; + schema[ref(name, "help")] = help; + + // Alias + for (const alias of aliases) { + alias_map[ref(`unk::${alias}`)] = ref(name); + } + + // Control + commands.push(name); + level += 1; + position = 0; + break; + } + case "end": { + level -= 1; + commands.pop(); + break; + } + case "argument": { + let aliases = elements.shift().split(","); + let name = aliases[0]; + + const entry = { required: false, kind: "positional" }; + for (const element of elements) { + const [attribute, attr_value] = element.split(":"); + + if (attribute === "required") entry.required = true; + else if (attribute === "repeatable") entry.repeatable = true; + else if (attribute === "help") entry.help = attr_value; + else if (attribute === "default") entry.default_value = attr_value; + else if (attribute === "dest") entry.dest = attr_value; + else if (attribute === "type") entry.kind = attr_value; + + // Defaults + if (entry.dest === undefined) entry.dest = name; + if (entry.value === undefined) entry.value = entry.default_value; + } + + // Alias + if (entry.kind === "positional") { + name = String(position++); + aliases = [`pos::${name}`]; + } else if (entry.kind === "rest") { + aliases = ["__rest"]; + } + + for (const alias of aliases) { + if (entry.kind === "option" || entry.kind === "flag") { + alias_map[ref(`narg::${alias}`)] = ref(name); + } else { + alias_map[ref(alias)] = ref(name); + } + } + + // Schema + schema[ref(name, "type")] = "argument"; + for (const [key, value] of Object.entries(entry)) { + schema[ref(name, key)] = value; + } + break; + } + default: + throw new Error(`Invalid entry type: '${type}'`); + } + } + + commands = ["root"]; + level = 1; + position = 0; + } + + function next_token_value() { + const token = take_one(tokens); + const [token_type, token_key] = token.split("::"); + if (token_type !== "unk") { + throw new Error(`Expected option value, but got: ${token_type}. Token: '${token_key}'`); + } + return token_key; + } + + generate_schema(); + + while (tokens.length > 0) { + const token = take_one(tokens); + const [token_type, token_key] = token.split("::"); + if (token_type === "rest") { + values["__rest"] = token_key; + continue; + } + + // Bubble up arguments -> Loop over all command levels starting by the latest + let found = false; + for (level = commands.length; level >= 1; level--) { + let entry_ref = get_entry_ref(token); + + // Positional arguments are valid only for the latest command + if (!entry_ref && level === commands.length && token_type === "unk") { + entry_ref = get_entry_ref(`pos::${position}`); + if (entry_ref) { + position += 1; + } + } + + // Try with parent command + if (!entry_ref) { + continue; + } + + const entry_type = get_entry_item(entry_ref, "type"); + if (entry_type === "command") { + commands.push(token_key); + } else if (entry_type === "argument") { + const kind = get_entry_item(entry_ref, "kind"); + const dest = get_entry_item(entry_ref, "dest"); + const value = get_entry_item(entry_ref, "value"); + + values[dest] = value; // default one + + if (kind === "flag") values[dest] = true; + else if (kind === "option") values[dest] = next_token_value(); + else if (kind === "positional") values[dest] = token_key; + else if (kind === "rest") values[dest] = token_key; + else throw new Error(`Invalid attribute type: '${kind}'`); + } + + found = true; + break; + } + + if (!found) { + console.log(`(Skipping invalid argument: '${token_key}'`); + } + } + + log({ commands, values }); + return values; } -function take_all(tokens) { - const all = [...tokens]; - tokens.length = 0; - return all; -} - -function ref(...rest) { - return commands.slice(0, level).concat(rest).join("."); -} - -function join(...rest) { - return rest.filter((v) => !!v).join("."); -} - -function log(text) { - console.log(JSON.stringify(text, null, 2)); -} const SPEC = [ "command;build;Build a dev container", "argument;from;type:option;map:from_name;help:Name of the base container", diff --git a/package.json b/package.json index 3dbc1ca..e9b8833 100644 --- a/package.json +++ b/package.json @@ -1,3 +1,6 @@ { - "type": "module" + "type": "module", + "prettier": { + "printWidth": 100 + } }