From 0709c814e75707b567cb69549ccea90d869818ec Mon Sep 17 00:00:00 2001 From: Paul Ouellette Date: Mon, 27 May 2019 21:51:10 -0400 Subject: [PATCH 01/37] Implement add_complete and fish option completion --- src/argparse.lua | 66 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/src/argparse.lua b/src/argparse.lua index 21e0d0b..bbaf721 100644 --- a/src/argparse.lua +++ b/src/argparse.lua @@ -204,6 +204,9 @@ local add_help = {"add_help", function(self, value) if self._help_option_idx then table.remove(self._options, self._help_option_idx) + if self._complete_option_idx and self._complete_option_idx > self._help_option_idx then + self._complete_option_idx = self._complete_option_idx - 1 + end self._help_option_idx = nil end @@ -227,6 +230,39 @@ local add_help = {"add_help", function(self, value) end end} +local add_complete = {"add_complete", function(self, value) + typecheck("add_complete", {"boolean", "string", "table"}, value) + + if self._complete_option_idx then + table.remove(self._options, self._complete_option_idx) + if self._help_option_idx and self._help_option_idx > self._complete_option_idx then + self._help_option_idx = self._help_option_idx - 1 + end + self._complete_option_idx = nil + end + + if value then + local complete = self:option() + :description "Output a shell completion script for the specified shell." + :args(1) + :choices {"bash", "zsh", "fish"} + :action(function(_, _, shell) + self["get_" .. shell .. "_complete"](self) + os.exit(0) + end) + + if value ~= true then + complete = complete(value) + end + + if not complete._name then + complete "--completion" + end + + self._complete_option_idx = #self._options + end +end} + local Parser = class({ _arguments = {}, _options = {}, @@ -252,7 +288,8 @@ local Parser = class({ typechecked("help_usage_margin", "number"), typechecked("help_description_margin", "number"), typechecked("help_max_width", "number"), - add_help + add_help, + add_complete }) local Command = class({ @@ -1083,6 +1120,33 @@ function Parser:add_help_command(value) return self end +function Parser:get_bash_complete() print "not yet implemented" end + +function Parser:get_zsh_complete() print "not yet implemented" end + +function Parser:get_fish_complete() + local lines = {} + + for _, option in ipairs(self._options) do + local parts = {} + for _, alias in ipairs(option._aliases) do + if alias:match("^%-.$") then + table.insert(parts, "-s " .. alias:sub(2)) + elseif alias:match "^%-%-.+" then + table.insert(parts, "-l " .. alias:sub(3)) + end + end + if option._minargs > 0 then + table.insert(parts, "-r") + end + table.insert(parts, ("-d '%s'"):format(option._description:gsub("[\\']", "\\%0"))) + local line = ("complete -c %s %s"):format(self._name, table.concat(parts, " ")) + table.insert(lines, line) + end + + io.write(table.concat(lines, "\n"), "\n") +end + local function get_tip(context, wrong_name) local context_pool = {} local possible_name From b893c9bad1b8fef71727c3e4359fe3878c730837 Mon Sep 17 00:00:00 2001 From: Paul Ouellette Date: Tue, 28 May 2019 23:56:19 -0400 Subject: [PATCH 02/37] Finish fish completion generator --- src/argparse.lua | 63 ++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 55 insertions(+), 8 deletions(-) diff --git a/src/argparse.lua b/src/argparse.lua index bbaf721..726799e 100644 --- a/src/argparse.lua +++ b/src/argparse.lua @@ -1124,26 +1124,73 @@ function Parser:get_bash_complete() print "not yet implemented" end function Parser:get_zsh_complete() print "not yet implemented" end -function Parser:get_fish_complete() - local lines = {} +local function fish_escape(string) + return string:gsub("[\\']", "\\%0") +end + +function Parser:_fish_complete_help(lines, prefix) + for _, command in ipairs(self._commands) do + for _, alias in ipairs(command._aliases) do + local line = ("%s -n '__fish_use_subcommand' -xa '%s' -d '%s'") + :format(prefix, alias, fish_escape(command._description)) + table.insert(lines, line) + end + end for _, option in ipairs(self._options) do - local parts = {} + local parts = {prefix} + + if self._parent then + local aliases = table.concat(self._aliases, " ") + local condition = ("-n '__fish_seen_subcommand_from %s'"):format(aliases) + table.insert(parts, condition) + end + for _, alias in ipairs(option._aliases) do if alias:match("^%-.$") then table.insert(parts, "-s " .. alias:sub(2)) - elseif alias:match "^%-%-.+" then + elseif alias:match("^%-%-.+") then table.insert(parts, "-l " .. alias:sub(3)) end end - if option._minargs > 0 then + + if option._choices then + local choices = ("-xa '%s'"):format(table.concat(option._choices, " ")) + table.insert(parts, choices) + elseif option._minargs > 0 then table.insert(parts, "-r") end - table.insert(parts, ("-d '%s'"):format(option._description:gsub("[\\']", "\\%0"))) - local line = ("complete -c %s %s"):format(self._name, table.concat(parts, " ")) - table.insert(lines, line) + + if option._description then + local description = ("-d '%s'"):format(fish_escape(option._description)) + table.insert(parts, description) + end + + table.insert(lines, table.concat(parts, " ")) end + for _, command in ipairs(self._commands) do + command:_fish_complete_help(lines, prefix) + end +end + +function Parser:get_fish_complete() + local lines = {} + local prefix = ("complete -c %s"):format(self._name) + + if self._help_command_idx then + local help_cmd = self._commands[self._help_command_idx] + local help_aliases = table.concat(help_cmd._aliases, " ") + + for _, command in ipairs(self._commands) do + local line = ("%s -n '__fish_seen_subcommand_from %s' -xa '%s'") + :format(prefix, help_aliases, command._aliases[1]) + table.insert(lines, line) + end + end + + self:_fish_complete_help(lines, prefix) + io.write(table.concat(lines, "\n"), "\n") end From 8e04dc6eca83bcfd68b9e3fdd1d7140a25480383 Mon Sep 17 00:00:00 2001 From: Paul Ouellette Date: Thu, 30 May 2019 19:21:33 -0400 Subject: [PATCH 03/37] Show first sentence of description in option completion --- src/argparse.lua | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/argparse.lua b/src/argparse.lua index 726799e..2acf7db 100644 --- a/src/argparse.lua +++ b/src/argparse.lua @@ -1120,6 +1120,11 @@ function Parser:add_help_command(value) return self end +local function get_short_description(element) + local short = element._description:match("^(.-)%.%s") + return short or element._description:match("^(.-)%.?$") +end + function Parser:get_bash_complete() print "not yet implemented" end function Parser:get_zsh_complete() print "not yet implemented" end @@ -1131,8 +1136,13 @@ end function Parser:_fish_complete_help(lines, prefix) for _, command in ipairs(self._commands) do for _, alias in ipairs(command._aliases) do - local line = ("%s -n '__fish_use_subcommand' -xa '%s' -d '%s'") - :format(prefix, alias, fish_escape(command._description)) + local line = ("%s -n '__fish_use_subcommand' -xa '%s'"):format(prefix, alias) + + if command._description then + local description = fish_escape(get_short_description(command)) + line = ("%s -d '%s'"):format(line, description) + end + table.insert(lines, line) end end @@ -1162,7 +1172,7 @@ function Parser:_fish_complete_help(lines, prefix) end if option._description then - local description = ("-d '%s'"):format(fish_escape(option._description)) + local description = ("-d '%s'"):format(fish_escape(get_short_description(option))) table.insert(parts, description) end From c96637b1d775ccb91c2862464f8402695043d633 Mon Sep 17 00:00:00 2001 From: Paul Ouellette Date: Fri, 31 May 2019 12:10:59 -0400 Subject: [PATCH 04/37] Return completions as a string in get function --- src/argparse.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/argparse.lua b/src/argparse.lua index 2acf7db..a0e5360 100644 --- a/src/argparse.lua +++ b/src/argparse.lua @@ -247,7 +247,7 @@ local add_complete = {"add_complete", function(self, value) :args(1) :choices {"bash", "zsh", "fish"} :action(function(_, _, shell) - self["get_" .. shell .. "_complete"](self) + print(self["get_" .. shell .. "_complete"](self)) os.exit(0) end) @@ -1201,7 +1201,7 @@ function Parser:get_fish_complete() self:_fish_complete_help(lines, prefix) - io.write(table.concat(lines, "\n"), "\n") + return table.concat(lines, "\n") end local function get_tip(context, wrong_name) From d5e15583b8236a806b03f82515bd6efe89b0081d Mon Sep 17 00:00:00 2001 From: Paul Ouellette Date: Thu, 30 May 2019 23:13:18 -0400 Subject: [PATCH 05/37] Add tests for fish completions --- spec/completion_spec.lua | 81 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 spec/completion_spec.lua diff --git a/spec/completion_spec.lua b/spec/completion_spec.lua new file mode 100644 index 0000000..7cf2ab6 --- /dev/null +++ b/spec/completion_spec.lua @@ -0,0 +1,81 @@ +local Parser = require "argparse" +getmetatable(Parser()).error = function(_, msg) error(msg) end + +describe("tests related to generation of shell completion scripts", function() + describe("fish completion scripts", function() + it("generates correct completions for help flag", function() + local parser = Parser "foo" + assert.equal([[ +complete -c foo -s h -l help -d 'Show this help message and exit']], parser:get_fish_complete()) + end) + + it("generates correct completions for options with required argument", function() + local parser = Parser "foo" + :add_help(false) + parser:option "--bar" + assert.equal([[ +complete -c foo -l bar -r]], parser:get_fish_complete()) + end) + + it("generates correct completions for options with argument choices", function() + local parser = Parser "foo" + :add_help(false) + parser:option "--format" + :choices {"short", "medium", "full"} + assert.equal([[ +complete -c foo -l format -xa 'short medium full']], parser:get_fish_complete()) + end) + + it("generates correct completions for commands", function() + local parser = Parser "foo" + :add_help(false) + parser:command "install" + :add_help(false) + :description "Install a rock." + assert.equal([[ +complete -c foo -n '__fish_use_subcommand' -xa 'install' -d 'Install a rock']], parser:get_fish_complete()) + end) + + it("generates correct completions for command options", function() + local parser = Parser "foo" + :add_help(false) + local install = parser:command "install" + :add_help(false) + install:flag "-v --verbose" + assert.equal([[ +complete -c foo -n '__fish_use_subcommand' -xa 'install' +complete -c foo -n '__fish_seen_subcommand_from install' -s v -l verbose]], parser:get_fish_complete()) + end) + + it("generates completions for help command argument", function() + local parser = Parser "foo" + :add_help(false) + :add_help_command {add_help = false} + parser:command "install" + :add_help(false) + assert.equal([[ +complete -c foo -n '__fish_seen_subcommand_from help' -xa 'help' +complete -c foo -n '__fish_seen_subcommand_from help' -xa 'install' +complete -c foo -n '__fish_use_subcommand' -xa 'help' -d 'Show help for commands' +complete -c foo -n '__fish_use_subcommand' -xa 'install']], parser:get_fish_complete()) + end) + + it("uses fist sentence of descriptions", function() + local parser = Parser "foo" + :add_help(false) + parser:option "--bar" + :description "A description with a .period. Another sentence." + assert.equal([[ +complete -c foo -l bar -r -d 'A description with a .period']], parser:get_fish_complete()) + end) + + it("escapes backslashes and single quotes in descriptions", function() + local parser = Parser "foo" + :add_help(false) + parser:option "--bar" + :description "A description with illegal \\' characters." + assert.equal([[ +complete -c foo -l bar -r -d 'A description with illegal \\\' characters']], parser:get_fish_complete()) + end) + end) +end) From 46622e83de82719cdd1505e15ec4f2b64832cc61 Mon Sep 17 00:00:00 2001 From: Paul Ouellette Date: Fri, 31 May 2019 21:19:48 -0400 Subject: [PATCH 06/37] Remove support for disabling completion option It is disabled by default --- src/argparse.lua | 41 +++++++++++++---------------------------- 1 file changed, 13 insertions(+), 28 deletions(-) diff --git a/src/argparse.lua b/src/argparse.lua index a0e5360..10422f8 100644 --- a/src/argparse.lua +++ b/src/argparse.lua @@ -204,9 +204,6 @@ local add_help = {"add_help", function(self, value) if self._help_option_idx then table.remove(self._options, self._help_option_idx) - if self._complete_option_idx and self._complete_option_idx > self._help_option_idx then - self._complete_option_idx = self._complete_option_idx - 1 - end self._help_option_idx = nil end @@ -231,35 +228,23 @@ local add_help = {"add_help", function(self, value) end} local add_complete = {"add_complete", function(self, value) - typecheck("add_complete", {"boolean", "string", "table"}, value) + typecheck("add_complete", {"nil", "string", "table"}, value) - if self._complete_option_idx then - table.remove(self._options, self._complete_option_idx) - if self._help_option_idx and self._help_option_idx > self._complete_option_idx then - self._help_option_idx = self._help_option_idx - 1 - end - self._complete_option_idx = nil - end + local complete = self:option() + :description "Output a shell completion script for the specified shell." + :args(1) + :choices {"bash", "zsh", "fish"} + :action(function(_, _, shell) + print(self["get_" .. shell .. "_complete"](self)) + os.exit(0) + end) if value then - local complete = self:option() - :description "Output a shell completion script for the specified shell." - :args(1) - :choices {"bash", "zsh", "fish"} - :action(function(_, _, shell) - print(self["get_" .. shell .. "_complete"](self)) - os.exit(0) - end) + complete = complete(value) + end - if value ~= true then - complete = complete(value) - end - - if not complete._name then - complete "--completion" - end - - self._complete_option_idx = #self._options + if not complete._name then + complete "--completion" end end} From 9738045e18ddb7b63d0ef4a02b9f9581de7dccb0 Mon Sep 17 00:00:00 2001 From: Paul Ouellette Date: Fri, 7 Jun 2019 00:27:17 -0400 Subject: [PATCH 07/37] Implement Parser:get_bash_complete --- src/argparse.lua | 97 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 96 insertions(+), 1 deletion(-) diff --git a/src/argparse.lua b/src/argparse.lua index 10422f8..ce4e45c 100644 --- a/src/argparse.lua +++ b/src/argparse.lua @@ -1110,7 +1110,102 @@ local function get_short_description(element) return short or element._description:match("^(.-)%.?$") end -function Parser:get_bash_complete() print "not yet implemented" end +local function get_options(parser) + local options = {} + for _, option in ipairs(parser._options) do + for _, alias in ipairs(option._aliases) do + table.insert(options, alias) + end + end + return table.concat(options, " ") +end + +local function get_commands(parser) + local commands = {} + for _, command in ipairs(parser._commands) do + for _, alias in ipairs(command._aliases) do + table.insert(commands, alias) + end + end + return table.concat(commands, " ") +end + +function Parser:_bash_get_cmd(buf) + local cmds = {} + for _, command in ipairs(self._commands) do + local pattern = ("%s)"):format(table.concat(command._aliases, "|")) + local cmd_assign = ('cmd="%s"'):format(command._aliases[1]) + table.insert(cmds, (" "):rep(12) .. pattern) + table.insert(cmds, (" "):rep(16) .. cmd_assign) + table.insert(cmds, (" "):rep(16) .. "break") + table.insert(cmds, (" "):rep(16) .. ";;") + end + + local get_cmd = ([[ + for arg in ${COMP_WORDS[@]:1} + do + case "$arg" in +%s + esac + done +]]):format(table.concat(cmds, "\n")) + table.insert(buf, get_cmd) +end + +function Parser:_bash_cmd_completions(buf) + local subcmds = {} + for idx, command in ipairs(self._commands) do + if #command._options > 0 then + table.insert(subcmds, (" "):rep(8) .. command._aliases[1] .. ")") + + local opts + if idx == self._help_option_idx then + opts = ('opts="%s"'):format(get_commands(self)) + else + opts = ('opts="$opts %s"'):format(get_options(command)) + end + table.insert(subcmds, (" "):rep(12) .. opts) + table.insert(subcmds, (" "):rep(12) .. ";;") + end + end + + local cmd_completions = ([[ + case "$cmd" in + %s) + opts="$opts %s" + ;; +%s + esac +]]):format(self._name, get_commands(self), table.concat(subcmds, "\n")) + table.insert(buf, cmd_completions) +end + +function Parser:get_bash_complete() + local buf = {} + + local head = ([[ +_%s() { + local cur prev cmd opts arg + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + cmd="%s" + opts="%s" +]]):format(self._name, self._name, get_options(self)) + table.insert(buf, head) + + if #self._commands > 0 then + self:_bash_get_cmd(buf) + self:_bash_cmd_completions(buf) + end + + table.insert(buf, ' COMPREPLY=($(compgen -W "$opts" -- "$cur"))') + table.insert(buf, "}\n") + local complete = ("complete -F _%s -o bashdefault -o default %s") + :format(self._name, self._name) + table.insert(buf, complete) + + return table.concat(buf, "\n") +end function Parser:get_zsh_complete() print "not yet implemented" end From 03e7daf31f7002f9b826f6554af7eebbd43fcbc3 Mon Sep 17 00:00:00 2001 From: Paul Ouellette Date: Fri, 7 Jun 2019 23:52:50 -0400 Subject: [PATCH 08/37] Bash completions: complete option argument choices --- src/argparse.lua | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/argparse.lua b/src/argparse.lua index ce4e45c..e38aab8 100644 --- a/src/argparse.lua +++ b/src/argparse.lua @@ -1130,6 +1130,32 @@ local function get_commands(parser) return table.concat(commands, " ") end +function Parser:_bash_option_args(buf, indent) + local opts = {} + for _, option in ipairs(self._options) do + if option._choices or option._minargs > 0 then + local pattern = ("%s)"):format(table.concat(option._aliases, "|")) + local compreply + if option._choices then + compreply = ('COMPREPLY=($(compgen -W "%s" -- "$cur"))') + :format(table.concat(option._choices, " ")) + else + compreply = ('COMPREPLY=($(compgen -f "$cur"))') + end + table.insert(opts, (" "):rep(indent + 4) .. pattern) + table.insert(opts, (" "):rep(indent + 8) .. compreply) + table.insert(opts, (" "):rep(indent + 8) .. "return 0") + table.insert(opts, (" "):rep(indent + 8) .. ";;") + end + end + + if #opts > 0 then + table.insert(buf, (" "):rep(indent) .. 'case "$prev" in') + table.insert(buf, table.concat(opts, "\n")) + table.insert(buf, (" "):rep(indent) .. "esac\n") + end +end + function Parser:_bash_get_cmd(buf) local cmds = {} for _, command in ipairs(self._commands) do @@ -1157,6 +1183,7 @@ function Parser:_bash_cmd_completions(buf) for idx, command in ipairs(self._commands) do if #command._options > 0 then table.insert(subcmds, (" "):rep(8) .. command._aliases[1] .. ")") + command:_bash_option_args(subcmds, 12) local opts if idx == self._help_option_idx then @@ -1193,6 +1220,8 @@ _%s() { ]]):format(self._name, self._name, get_options(self)) table.insert(buf, head) + self:_bash_option_args(buf, 4) + if #self._commands > 0 then self:_bash_get_cmd(buf) self:_bash_cmd_completions(buf) From 5b733ba50a578dcd3e279a3e0ccf5bbe61b964aa Mon Sep 17 00:00:00 2001 From: Paul Ouellette Date: Mon, 10 Jun 2019 16:39:15 -0400 Subject: [PATCH 09/37] Make add_complete a Parser method --- src/argparse.lua | 50 ++++++++++++++++++++++++++---------------------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/src/argparse.lua b/src/argparse.lua index e38aab8..dfebda5 100644 --- a/src/argparse.lua +++ b/src/argparse.lua @@ -227,27 +227,6 @@ local add_help = {"add_help", function(self, value) end end} -local add_complete = {"add_complete", function(self, value) - typecheck("add_complete", {"nil", "string", "table"}, value) - - local complete = self:option() - :description "Output a shell completion script for the specified shell." - :args(1) - :choices {"bash", "zsh", "fish"} - :action(function(_, _, shell) - print(self["get_" .. shell .. "_complete"](self)) - os.exit(0) - end) - - if value then - complete = complete(value) - end - - if not complete._name then - complete "--completion" - end -end} - local Parser = class({ _arguments = {}, _options = {}, @@ -273,8 +252,7 @@ local Parser = class({ typechecked("help_usage_margin", "number"), typechecked("help_description_margin", "number"), typechecked("help_max_width", "number"), - add_help, - add_complete + add_help }) local Command = class({ @@ -1105,6 +1083,32 @@ function Parser:add_help_command(value) return self end +function Parser:add_complete(value) + if value then + assert(type(value) == "string" or type(value) == "table", + ("bad argument #1 to 'add_complete' (string or table expected, got %s)"):format(type(value))) + end + + local complete = self:option() + :description "Output a shell completion script for the specified shell." + :args(1) + :choices {"bash", "zsh", "fish"} + :action(function(_, _, shell) + print(self["get_" .. shell .. "_complete"](self)) + os.exit(0) + end) + + if value then + complete = complete(value) + end + + if not complete._name then + complete "--completion" + end + + return self +end + local function get_short_description(element) local short = element._description:match("^(.-)%.%s") return short or element._description:match("^(.-)%.?$") From f18902eb4cfd4599df5f4f0f0d4c28fbd61bed0b Mon Sep 17 00:00:00 2001 From: Paul Ouellette Date: Tue, 11 Jun 2019 00:23:40 -0400 Subject: [PATCH 10/37] Complete options if current word starts with dash --- src/argparse.lua | 36 +++++++++++++++--------------------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/src/argparse.lua b/src/argparse.lua index dfebda5..cdc55bf 100644 --- a/src/argparse.lua +++ b/src/argparse.lua @@ -1162,11 +1162,11 @@ end function Parser:_bash_get_cmd(buf) local cmds = {} - for _, command in ipairs(self._commands) do - local pattern = ("%s)"):format(table.concat(command._aliases, "|")) - local cmd_assign = ('cmd="%s"'):format(command._aliases[1]) - table.insert(cmds, (" "):rep(12) .. pattern) - table.insert(cmds, (" "):rep(16) .. cmd_assign) + for idx, command in ipairs(self._commands) do + table.insert(cmds, (" "):rep(12) .. ("%s)"):format(table.concat(command._aliases, "|"))) + if idx ~= self._help_command_idx then + table.insert(cmds, (" "):rep(16) .. ('cmd="%s"'):format(command._aliases[1])) + end table.insert(cmds, (" "):rep(16) .. "break") table.insert(cmds, (" "):rep(16) .. ";;") end @@ -1185,17 +1185,10 @@ end function Parser:_bash_cmd_completions(buf) local subcmds = {} for idx, command in ipairs(self._commands) do - if #command._options > 0 then + if #command._options > 0 and idx ~= self._help_command_idx then table.insert(subcmds, (" "):rep(8) .. command._aliases[1] .. ")") command:_bash_option_args(subcmds, 12) - - local opts - if idx == self._help_option_idx then - opts = ('opts="%s"'):format(get_commands(self)) - else - opts = ('opts="$opts %s"'):format(get_options(command)) - end - table.insert(subcmds, (" "):rep(12) .. opts) + table.insert(subcmds, (" "):rep(12) .. ('opts="$opts %s"'):format(get_options(command))) table.insert(subcmds, (" "):rep(12) .. ";;") end end @@ -1203,7 +1196,7 @@ function Parser:_bash_cmd_completions(buf) local cmd_completions = ([[ case "$cmd" in %s) - opts="$opts %s" + COMPREPLY=($(compgen -W "%s" -- "$cur")) ;; %s esac @@ -1212,17 +1205,14 @@ function Parser:_bash_cmd_completions(buf) end function Parser:get_bash_complete() - local buf = {} - - local head = ([[ + local buf = {([[ _%s() { local cur prev cmd opts arg cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" cmd="%s" opts="%s" -]]):format(self._name, self._name, get_options(self)) - table.insert(buf, head) +]]):format(self._name, self._name, get_options(self))} self:_bash_option_args(buf, 4) @@ -1231,7 +1221,11 @@ _%s() { self:_bash_cmd_completions(buf) end - table.insert(buf, ' COMPREPLY=($(compgen -W "$opts" -- "$cur"))') + table.insert(buf, [=[ + if [[ "$cur" = -* ]] + then + COMPREPLY=($(compgen -W "$opts" -- "$cur")) + fi]=]) table.insert(buf, "}\n") local complete = ("complete -F _%s -o bashdefault -o default %s") :format(self._name, self._name) From ab7717898c859d35e1f6d932c180be9e8dd0126f Mon Sep 17 00:00:00 2001 From: Paul Ouellette Date: Tue, 11 Jun 2019 01:35:00 -0400 Subject: [PATCH 11/37] Add tests for bash completions --- spec/completion_spec.lua | 202 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 202 insertions(+) diff --git a/spec/completion_spec.lua b/spec/completion_spec.lua index 7cf2ab6..15c8013 100644 --- a/spec/completion_spec.lua +++ b/spec/completion_spec.lua @@ -2,6 +2,208 @@ local Parser = require "argparse" getmetatable(Parser()).error = function(_, msg) error(msg) end describe("tests related to generation of shell completion scripts", function() + describe("bash completion scripts", function() + it("generates correct completions for help flag", function() + local parser = Parser "foo" + assert.equal([=[ +_foo() { + local cur prev cmd opts arg + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + cmd="foo" + opts="-h --help" + + if [[ "$cur" = -* ]] + then + COMPREPLY=($(compgen -W "$opts" -- "$cur")) + fi +} + +complete -F _foo -o bashdefault -o default foo]=], parser:get_bash_complete()) + end) + + it("generates correct completions for options with required argument", function() + local parser = Parser "foo" + :add_help(false) + parser:option "--bar" + assert.equal([=[ +_foo() { + local cur prev cmd opts arg + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + cmd="foo" + opts="--bar" + + case "$prev" in + --bar) + COMPREPLY=($(compgen -f "$cur")) + return 0 + ;; + esac + + if [[ "$cur" = -* ]] + then + COMPREPLY=($(compgen -W "$opts" -- "$cur")) + fi +} + +complete -F _foo -o bashdefault -o default foo]=], parser:get_bash_complete()) + end) + + it("generates correct completions for options with argument choices", function() + local parser = Parser "foo" + :add_help(false) + parser:option "--format" + :choices {"short", "medium", "full"} + assert.equal([=[ +_foo() { + local cur prev cmd opts arg + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + cmd="foo" + opts="--format" + + case "$prev" in + --format) + COMPREPLY=($(compgen -W "short medium full" -- "$cur")) + return 0 + ;; + esac + + if [[ "$cur" = -* ]] + then + COMPREPLY=($(compgen -W "$opts" -- "$cur")) + fi +} + +complete -F _foo -o bashdefault -o default foo]=], parser:get_bash_complete()) + end) + + it("generates correct completions for commands", function() + local parser = Parser "foo" + :add_help(false) + parser:command "install" + :add_help(false) + assert.equal([=[ +_foo() { + local cur prev cmd opts arg + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + cmd="foo" + opts="" + + for arg in ${COMP_WORDS[@]:1} + do + case "$arg" in + install) + cmd="install" + break + ;; + esac + done + + case "$cmd" in + foo) + COMPREPLY=($(compgen -W "install" -- "$cur")) + ;; + + esac + + if [[ "$cur" = -* ]] + then + COMPREPLY=($(compgen -W "$opts" -- "$cur")) + fi +} + +complete -F _foo -o bashdefault -o default foo]=], parser:get_bash_complete()) + end) + + it("generates correct completions for command options", function() + local parser = Parser "foo" + :add_help(false) + local install = parser:command "install" + :add_help(false) + install:flag "-v --verbose" + assert.equal([=[ +_foo() { + local cur prev cmd opts arg + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + cmd="foo" + opts="" + + for arg in ${COMP_WORDS[@]:1} + do + case "$arg" in + install) + cmd="install" + break + ;; + esac + done + + case "$cmd" in + foo) + COMPREPLY=($(compgen -W "install" -- "$cur")) + ;; + install) + opts="$opts -v --verbose" + ;; + esac + + if [[ "$cur" = -* ]] + then + COMPREPLY=($(compgen -W "$opts" -- "$cur")) + fi +} + +complete -F _foo -o bashdefault -o default foo]=], parser:get_bash_complete()) + end) + + it("generates completions for help command argument", function() + local parser = Parser "foo" + :add_help(false) + :add_help_command {add_help = false} + parser:command "install" + :add_help(false) + assert.equal([=[ +_foo() { + local cur prev cmd opts arg + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + cmd="foo" + opts="" + + for arg in ${COMP_WORDS[@]:1} + do + case "$arg" in + help) + break + ;; + install) + cmd="install" + break + ;; + esac + done + + case "$cmd" in + foo) + COMPREPLY=($(compgen -W "help install" -- "$cur")) + ;; + + esac + + if [[ "$cur" = -* ]] + then + COMPREPLY=($(compgen -W "$opts" -- "$cur")) + fi +} + +complete -F _foo -o bashdefault -o default foo]=], parser:get_bash_complete()) + end) + end) + describe("fish completion scripts", function() it("generates correct completions for help flag", function() local parser = Parser "foo" From bcce0fcdcf12768094a59e1c70dfb21de43b9d19 Mon Sep 17 00:00:00 2001 From: Paul Ouellette Date: Tue, 11 Jun 2019 17:22:27 -0400 Subject: [PATCH 12/37] Small improvements to bash completions --- spec/completion_spec.lua | 32 +++++++--------------- src/argparse.lua | 58 ++++++++++++++++++---------------------- 2 files changed, 35 insertions(+), 55 deletions(-) diff --git a/spec/completion_spec.lua b/spec/completion_spec.lua index 15c8013..5299fd3 100644 --- a/spec/completion_spec.lua +++ b/spec/completion_spec.lua @@ -13,8 +13,7 @@ _foo() { cmd="foo" opts="-h --help" - if [[ "$cur" = -* ]] - then + if [[ "$cur" = -* ]]; then COMPREPLY=($(compgen -W "$opts" -- "$cur")) fi } @@ -41,8 +40,7 @@ _foo() { ;; esac - if [[ "$cur" = -* ]] - then + if [[ "$cur" = -* ]]; then COMPREPLY=($(compgen -W "$opts" -- "$cur")) fi } @@ -70,8 +68,7 @@ _foo() { ;; esac - if [[ "$cur" = -* ]] - then + if [[ "$cur" = -* ]]; then COMPREPLY=($(compgen -W "$opts" -- "$cur")) fi } @@ -92,8 +89,7 @@ _foo() { cmd="foo" opts="" - for arg in ${COMP_WORDS[@]:1} - do + for arg in ${COMP_WORDS[@]:1}; do case "$arg" in install) cmd="install" @@ -106,11 +102,9 @@ _foo() { foo) COMPREPLY=($(compgen -W "install" -- "$cur")) ;; - esac - if [[ "$cur" = -* ]] - then + if [[ "$cur" = -* ]]; then COMPREPLY=($(compgen -W "$opts" -- "$cur")) fi } @@ -132,8 +126,7 @@ _foo() { cmd="foo" opts="" - for arg in ${COMP_WORDS[@]:1} - do + for arg in ${COMP_WORDS[@]:1}; do case "$arg" in install) cmd="install" @@ -151,8 +144,7 @@ _foo() { ;; esac - if [[ "$cur" = -* ]] - then + if [[ "$cur" = -* ]]; then COMPREPLY=($(compgen -W "$opts" -- "$cur")) fi } @@ -174,12 +166,8 @@ _foo() { cmd="foo" opts="" - for arg in ${COMP_WORDS[@]:1} - do + for arg in ${COMP_WORDS[@]:1}; do case "$arg" in - help) - break - ;; install) cmd="install" break @@ -191,11 +179,9 @@ _foo() { foo) COMPREPLY=($(compgen -W "help install" -- "$cur")) ;; - esac - if [[ "$cur" = -* ]] - then + if [[ "$cur" = -* ]]; then COMPREPLY=($(compgen -W "$opts" -- "$cur")) fi } diff --git a/src/argparse.lua b/src/argparse.lua index cdc55bf..6f92a07 100644 --- a/src/argparse.lua +++ b/src/argparse.lua @@ -1144,7 +1144,7 @@ function Parser:_bash_option_args(buf, indent) compreply = ('COMPREPLY=($(compgen -W "%s" -- "$cur"))') :format(table.concat(option._choices, " ")) else - compreply = ('COMPREPLY=($(compgen -f "$cur"))') + compreply = 'COMPREPLY=($(compgen -f "$cur"))' end table.insert(opts, (" "):rep(indent + 4) .. pattern) table.insert(opts, (" "):rep(indent + 8) .. compreply) @@ -1163,23 +1163,21 @@ end function Parser:_bash_get_cmd(buf) local cmds = {} for idx, command in ipairs(self._commands) do - table.insert(cmds, (" "):rep(12) .. ("%s)"):format(table.concat(command._aliases, "|"))) if idx ~= self._help_command_idx then + table.insert(cmds, (" "):rep(12) .. ("%s)"):format(table.concat(command._aliases, "|"))) table.insert(cmds, (" "):rep(16) .. ('cmd="%s"'):format(command._aliases[1])) + table.insert(cmds, (" "):rep(16) .. "break") + table.insert(cmds, (" "):rep(16) .. ";;") end - table.insert(cmds, (" "):rep(16) .. "break") - table.insert(cmds, (" "):rep(16) .. ";;") end - local get_cmd = ([[ - for arg in ${COMP_WORDS[@]:1} - do - case "$arg" in -%s - esac - done -]]):format(table.concat(cmds, "\n")) - table.insert(buf, get_cmd) + if #cmds > 0 then + table.insert(buf, (" "):rep(4) .. "for arg in ${COMP_WORDS[@]:1}; do") + table.insert(buf, (" "):rep(8) .. 'case "$arg" in') + table.insert(buf, table.concat(cmds, "\n")) + table.insert(buf, (" "):rep(8) .. "esac") + table.insert(buf, (" "):rep(4) .. "done\n") + end end function Parser:_bash_cmd_completions(buf) @@ -1193,15 +1191,14 @@ function Parser:_bash_cmd_completions(buf) end end - local cmd_completions = ([[ - case "$cmd" in - %s) - COMPREPLY=($(compgen -W "%s" -- "$cur")) - ;; -%s - esac -]]):format(self._name, get_commands(self), table.concat(subcmds, "\n")) - table.insert(buf, cmd_completions) + table.insert(buf, (" "):rep(4) .. 'case "$cmd" in') + table.insert(buf, (" "):rep(8) .. self._name .. ")") + table.insert(buf, (" "):rep(12) .. ('COMPREPLY=($(compgen -W "%s" -- "$cur"))'):format(get_commands(self))) + table.insert(buf, (" "):rep(12) .. ";;") + if #subcmds > 0 then + table.insert(buf, table.concat(subcmds, "\n")) + end + table.insert(buf, (" "):rep(4) .. "esac\n") end function Parser:get_bash_complete() @@ -1215,21 +1212,18 @@ _%s() { ]]):format(self._name, self._name, get_options(self))} self:_bash_option_args(buf, 4) - + self:_bash_get_cmd(buf) if #self._commands > 0 then - self:_bash_get_cmd(buf) self:_bash_cmd_completions(buf) end - table.insert(buf, [=[ - if [[ "$cur" = -* ]] - then + table.insert(buf, ([=[ + if [[ "$cur" = -* ]]; then COMPREPLY=($(compgen -W "$opts" -- "$cur")) - fi]=]) - table.insert(buf, "}\n") - local complete = ("complete -F _%s -o bashdefault -o default %s") - :format(self._name, self._name) - table.insert(buf, complete) + fi +} + +complete -F _%s -o bashdefault -o default %s]=]):format(self._name, self._name)) return table.concat(buf, "\n") end From 56364ff50f0fd7e6996841beb1a31c59c4d1c1b6 Mon Sep 17 00:00:00 2001 From: Paul Ouellette Date: Tue, 18 Jun 2019 12:02:24 -0400 Subject: [PATCH 13/37] Use saved help command instead of index --- src/argparse.lua | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/argparse.lua b/src/argparse.lua index 6f92a07..2daf34b 100644 --- a/src/argparse.lua +++ b/src/argparse.lua @@ -1080,6 +1080,7 @@ function Parser:add_help_command(value) help "help" end + self._help_command = help return self end @@ -1162,8 +1163,8 @@ end function Parser:_bash_get_cmd(buf) local cmds = {} - for idx, command in ipairs(self._commands) do - if idx ~= self._help_command_idx then + for _, command in ipairs(self._commands) do + if command ~= self._help_command then table.insert(cmds, (" "):rep(12) .. ("%s)"):format(table.concat(command._aliases, "|"))) table.insert(cmds, (" "):rep(16) .. ('cmd="%s"'):format(command._aliases[1])) table.insert(cmds, (" "):rep(16) .. "break") @@ -1182,8 +1183,8 @@ end function Parser:_bash_cmd_completions(buf) local subcmds = {} - for idx, command in ipairs(self._commands) do - if #command._options > 0 and idx ~= self._help_command_idx then + for _, command in ipairs(self._commands) do + if #command._options > 0 and command ~= self._help_command then table.insert(subcmds, (" "):rep(8) .. command._aliases[1] .. ")") command:_bash_option_args(subcmds, 12) table.insert(subcmds, (" "):rep(12) .. ('opts="$opts %s"'):format(get_options(command))) @@ -1289,9 +1290,8 @@ function Parser:get_fish_complete() local lines = {} local prefix = ("complete -c %s"):format(self._name) - if self._help_command_idx then - local help_cmd = self._commands[self._help_command_idx] - local help_aliases = table.concat(help_cmd._aliases, " ") + if self._help_command then + local help_aliases = table.concat(self._help_command._aliases, " ") for _, command in ipairs(self._commands) do local line = ("%s -n '__fish_seen_subcommand_from %s' -xa '%s'") From 188fb9d8ac20567a7809db33c1f1942b5b113978 Mon Sep 17 00:00:00 2001 From: Paul Ouellette Date: Tue, 18 Jun 2019 12:15:56 -0400 Subject: [PATCH 14/37] Use command summaries in completion descriptions --- src/argparse.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/argparse.lua b/src/argparse.lua index 2daf34b..f4b5bba 100644 --- a/src/argparse.lua +++ b/src/argparse.lua @@ -1111,8 +1111,8 @@ function Parser:add_complete(value) end local function get_short_description(element) - local short = element._description:match("^(.-)%.%s") - return short or element._description:match("^(.-)%.?$") + local short = element:_get_description():match("^(.-)%.%s") + return short or element:_get_description():match("^(.-)%.?$") end local function get_options(parser) From 4f99e3dce019dcea99dc9696800570280134253d Mon Sep 17 00:00:00 2001 From: Paul Ouellette Date: Tue, 18 Jun 2019 12:20:21 -0400 Subject: [PATCH 15/37] Add trailing newline to output of complete functions --- spec/completion_spec.lua | 42 ++++++++++++++++++++++++++-------------- src/argparse.lua | 7 ++++--- 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/spec/completion_spec.lua b/spec/completion_spec.lua index 5299fd3..5b7bf27 100644 --- a/spec/completion_spec.lua +++ b/spec/completion_spec.lua @@ -18,7 +18,8 @@ _foo() { fi } -complete -F _foo -o bashdefault -o default foo]=], parser:get_bash_complete()) +complete -F _foo -o bashdefault -o default foo +]=], parser:get_bash_complete()) end) it("generates correct completions for options with required argument", function() @@ -45,7 +46,8 @@ _foo() { fi } -complete -F _foo -o bashdefault -o default foo]=], parser:get_bash_complete()) +complete -F _foo -o bashdefault -o default foo +]=], parser:get_bash_complete()) end) it("generates correct completions for options with argument choices", function() @@ -73,7 +75,8 @@ _foo() { fi } -complete -F _foo -o bashdefault -o default foo]=], parser:get_bash_complete()) +complete -F _foo -o bashdefault -o default foo +]=], parser:get_bash_complete()) end) it("generates correct completions for commands", function() @@ -109,7 +112,8 @@ _foo() { fi } -complete -F _foo -o bashdefault -o default foo]=], parser:get_bash_complete()) +complete -F _foo -o bashdefault -o default foo +]=], parser:get_bash_complete()) end) it("generates correct completions for command options", function() @@ -149,7 +153,8 @@ _foo() { fi } -complete -F _foo -o bashdefault -o default foo]=], parser:get_bash_complete()) +complete -F _foo -o bashdefault -o default foo +]=], parser:get_bash_complete()) end) it("generates completions for help command argument", function() @@ -186,7 +191,8 @@ _foo() { fi } -complete -F _foo -o bashdefault -o default foo]=], parser:get_bash_complete()) +complete -F _foo -o bashdefault -o default foo +]=], parser:get_bash_complete()) end) end) @@ -194,7 +200,8 @@ complete -F _foo -o bashdefault -o default foo]=], parser:get_bash_complete()) it("generates correct completions for help flag", function() local parser = Parser "foo" assert.equal([[ -complete -c foo -s h -l help -d 'Show this help message and exit']], parser:get_fish_complete()) +complete -c foo -s h -l help -d 'Show this help message and exit' +]], parser:get_fish_complete()) end) it("generates correct completions for options with required argument", function() @@ -202,7 +209,8 @@ complete -c foo -s h -l help -d 'Show this help message and exit']], parser:get_ :add_help(false) parser:option "--bar" assert.equal([[ -complete -c foo -l bar -r]], parser:get_fish_complete()) +complete -c foo -l bar -r +]], parser:get_fish_complete()) end) it("generates correct completions for options with argument choices", function() @@ -211,7 +219,8 @@ complete -c foo -l bar -r]], parser:get_fish_complete()) parser:option "--format" :choices {"short", "medium", "full"} assert.equal([[ -complete -c foo -l format -xa 'short medium full']], parser:get_fish_complete()) +complete -c foo -l format -xa 'short medium full' +]], parser:get_fish_complete()) end) it("generates correct completions for commands", function() @@ -221,7 +230,8 @@ complete -c foo -l format -xa 'short medium full']], parser:get_fish_complete()) :add_help(false) :description "Install a rock." assert.equal([[ -complete -c foo -n '__fish_use_subcommand' -xa 'install' -d 'Install a rock']], parser:get_fish_complete()) +complete -c foo -n '__fish_use_subcommand' -xa 'install' -d 'Install a rock' +]], parser:get_fish_complete()) end) it("generates correct completions for command options", function() @@ -232,7 +242,8 @@ complete -c foo -n '__fish_use_subcommand' -xa 'install' -d 'Install a rock']], install:flag "-v --verbose" assert.equal([[ complete -c foo -n '__fish_use_subcommand' -xa 'install' -complete -c foo -n '__fish_seen_subcommand_from install' -s v -l verbose]], parser:get_fish_complete()) +complete -c foo -n '__fish_seen_subcommand_from install' -s v -l verbose +]], parser:get_fish_complete()) end) it("generates completions for help command argument", function() @@ -245,7 +256,8 @@ complete -c foo -n '__fish_seen_subcommand_from install' -s v -l verbose]], pars complete -c foo -n '__fish_seen_subcommand_from help' -xa 'help' complete -c foo -n '__fish_seen_subcommand_from help' -xa 'install' complete -c foo -n '__fish_use_subcommand' -xa 'help' -d 'Show help for commands' -complete -c foo -n '__fish_use_subcommand' -xa 'install']], parser:get_fish_complete()) +complete -c foo -n '__fish_use_subcommand' -xa 'install' +]], parser:get_fish_complete()) end) it("uses fist sentence of descriptions", function() @@ -254,7 +266,8 @@ complete -c foo -n '__fish_use_subcommand' -xa 'install']], parser:get_fish_comp parser:option "--bar" :description "A description with a .period. Another sentence." assert.equal([[ -complete -c foo -l bar -r -d 'A description with a .period']], parser:get_fish_complete()) +complete -c foo -l bar -r -d 'A description with a .period' +]], parser:get_fish_complete()) end) it("escapes backslashes and single quotes in descriptions", function() @@ -263,7 +276,8 @@ complete -c foo -l bar -r -d 'A description with a .period']], parser:get_fish_c parser:option "--bar" :description "A description with illegal \\' characters." assert.equal([[ -complete -c foo -l bar -r -d 'A description with illegal \\\' characters']], parser:get_fish_complete()) +complete -c foo -l bar -r -d 'A description with illegal \\\' characters' +]], parser:get_fish_complete()) end) end) end) diff --git a/src/argparse.lua b/src/argparse.lua index f4b5bba..d01b89f 100644 --- a/src/argparse.lua +++ b/src/argparse.lua @@ -1095,7 +1095,7 @@ function Parser:add_complete(value) :args(1) :choices {"bash", "zsh", "fish"} :action(function(_, _, shell) - print(self["get_" .. shell .. "_complete"](self)) + io.write(self["get_" .. shell .. "_complete"](self)) os.exit(0) end) @@ -1224,7 +1224,8 @@ _%s() { fi } -complete -F _%s -o bashdefault -o default %s]=]):format(self._name, self._name)) +complete -F _%s -o bashdefault -o default %s +]=]):format(self._name, self._name)) return table.concat(buf, "\n") end @@ -1302,7 +1303,7 @@ function Parser:get_fish_complete() self:_fish_complete_help(lines, prefix) - return table.concat(lines, "\n") + return table.concat(lines, "\n") .. "\n" end local function get_tip(context, wrong_name) From e9ee89eed5c37ad605d193d92863003c7d52471c Mon Sep 17 00:00:00 2001 From: Paul Ouellette Date: Mon, 24 Jun 2019 18:03:09 -0400 Subject: [PATCH 16/37] Initial get_zsh_complete with support for options --- src/argparse.lua | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/argparse.lua b/src/argparse.lua index d01b89f..f4afea2 100644 --- a/src/argparse.lua +++ b/src/argparse.lua @@ -1230,7 +1230,32 @@ complete -F _%s -o bashdefault -o default %s return table.concat(buf, "\n") end -function Parser:get_zsh_complete() print "not yet implemented" end +function Parser:get_zsh_complete() + local buf = {("compdef _%s %s\n"):format(self._name, self._name)} + table.insert(buf, ("_%s() {"):format(self._name)) + table.insert(buf, " _arguments -s -S -C \\") + + for _, option in ipairs(self._options) do + local line = {} + if #option._aliases > 1 then + table.insert(line, ("{%s}"):format(table.concat(option._aliases, ","))) + if option._description then + table.insert(line, ('"[%s]"'):format(get_short_description(option))) + end + else + table.insert(line, '"' .. option._aliases[1]) + if option._description then + table.insert(line, ("[%s]"):format(get_short_description(option))) + end + table.insert(line, '"') + end + table.insert(buf, (" "):rep(8) .. table.concat(line) .. " \\") + end + + table.insert(buf, "}") + + return table.concat(buf, "\n") .. "\n" +end local function fish_escape(string) return string:gsub("[\\']", "\\%0") From 3a0e7a270620f82be80b6ca5585eeaa9d2460abc Mon Sep 17 00:00:00 2001 From: Paul Ouellette Date: Mon, 1 Jul 2019 23:03:49 -0400 Subject: [PATCH 17/37] Zsh completion improvements --- src/argparse.lua | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/argparse.lua b/src/argparse.lua index f4afea2..5f58b34 100644 --- a/src/argparse.lua +++ b/src/argparse.lua @@ -1233,7 +1233,10 @@ end function Parser:get_zsh_complete() local buf = {("compdef _%s %s\n"):format(self._name, self._name)} table.insert(buf, ("_%s() {"):format(self._name)) - table.insert(buf, " _arguments -s -S -C \\") + table.insert(buf, " typeset -A opt_args") + table.insert(buf, " local context state line") + table.insert(buf, " local ret=1\n") + table.insert(buf, " _arguments -s -S -C \\") for _, option in ipairs(self._options) do local line = {} @@ -1249,9 +1252,11 @@ function Parser:get_zsh_complete() end table.insert(line, '"') end - table.insert(buf, (" "):rep(8) .. table.concat(line) .. " \\") + table.insert(buf, (" "):rep(4) .. table.concat(line) .. " \\") end + table.insert(buf, " && ret=0\n") + table.insert(buf, " return ret") table.insert(buf, "}") return table.concat(buf, "\n") .. "\n" From 0eac53893b6b981bc5c81b2b63570cbc103b891c Mon Sep 17 00:00:00 2001 From: Paul Ouellette Date: Tue, 2 Jul 2019 22:37:09 -0400 Subject: [PATCH 18/37] Normalize IFS --- spec/completion_spec.lua | 6 ++++++ src/argparse.lua | 1 + 2 files changed, 7 insertions(+) diff --git a/spec/completion_spec.lua b/spec/completion_spec.lua index 5b7bf27..2def596 100644 --- a/spec/completion_spec.lua +++ b/spec/completion_spec.lua @@ -7,6 +7,7 @@ describe("tests related to generation of shell completion scripts", function() local parser = Parser "foo" assert.equal([=[ _foo() { + local IFS=$' \t\n' local cur prev cmd opts arg cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" @@ -28,6 +29,7 @@ complete -F _foo -o bashdefault -o default foo parser:option "--bar" assert.equal([=[ _foo() { + local IFS=$' \t\n' local cur prev cmd opts arg cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" @@ -57,6 +59,7 @@ complete -F _foo -o bashdefault -o default foo :choices {"short", "medium", "full"} assert.equal([=[ _foo() { + local IFS=$' \t\n' local cur prev cmd opts arg cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" @@ -86,6 +89,7 @@ complete -F _foo -o bashdefault -o default foo :add_help(false) assert.equal([=[ _foo() { + local IFS=$' \t\n' local cur prev cmd opts arg cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" @@ -124,6 +128,7 @@ complete -F _foo -o bashdefault -o default foo install:flag "-v --verbose" assert.equal([=[ _foo() { + local IFS=$' \t\n' local cur prev cmd opts arg cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" @@ -165,6 +170,7 @@ complete -F _foo -o bashdefault -o default foo :add_help(false) assert.equal([=[ _foo() { + local IFS=$' \t\n' local cur prev cmd opts arg cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" diff --git a/src/argparse.lua b/src/argparse.lua index 5f58b34..db1a62d 100644 --- a/src/argparse.lua +++ b/src/argparse.lua @@ -1205,6 +1205,7 @@ end function Parser:get_bash_complete() local buf = {([[ _%s() { + local IFS=$' \t\n' local cur prev cmd opts arg cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" From 56716617a283de07ab7a74d7eea497a5cb8f476c Mon Sep 17 00:00:00 2001 From: Paul Ouellette Date: Wed, 3 Jul 2019 17:35:40 -0400 Subject: [PATCH 19/37] Add support for a completion subcommand --- src/argparse.lua | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/argparse.lua b/src/argparse.lua index db1a62d..5890066 100644 --- a/src/argparse.lua +++ b/src/argparse.lua @@ -1110,6 +1110,33 @@ function Parser:add_complete(value) return self end +function Parser:add_complete_command(value) + if value then + assert(type(value) == "string" or type(value) == "table", + ("bad argument #1 to 'add_complete_command' (string or table expected, got %s)"):format(type(value))) + end + + local complete = self:command() + :description "Output a shell completion script." + complete:argument "shell" + :description "The shell to output a completion script for." + :choices {"bash", "zsh", "fish"} + :action(function(_, _, shell) + io.write(self["get_" .. shell .. "_complete"](self)) + os.exit(0) + end) + + if value then + complete = complete(value) + end + + if not complete._name then + complete "completion" + end + + return self +end + local function get_short_description(element) local short = element:_get_description():match("^(.-)%.%s") return short or element:_get_description():match("^(.-)%.?$") From c545e49701c149ce59a5214737dcdc1c4836fbda Mon Sep 17 00:00:00 2001 From: Paul Ouellette Date: Thu, 4 Jul 2019 13:20:01 -0400 Subject: [PATCH 20/37] Prevent generation of broken completion scripts --- src/argparse.lua | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/argparse.lua b/src/argparse.lua index 5890066..f06b4f9 100644 --- a/src/argparse.lua +++ b/src/argparse.lua @@ -1084,11 +1084,40 @@ function Parser:add_help_command(value) return self end +local function is_shell_safe(parser) + if parser._name:find("[^%w_%-%+%.]") then + return false + end + for _, command in ipairs(parser._commands) do + for _, alias in ipairs(command._aliases) do + if alias:find("[^%w_%-%+%.]") then + return false + end + end + end + for _, option in ipairs(parser._options) do + for _, alias in ipairs(option._aliases) do + if alias:find("[^%w_%-%+%.]") then + return false + end + if option._choices then + for _, choice in ipairs(option._choices) do + if choice:find("[%s'\"]") then + return false + end + end + end + end + end + return true +end + function Parser:add_complete(value) if value then assert(type(value) == "string" or type(value) == "table", ("bad argument #1 to 'add_complete' (string or table expected, got %s)"):format(type(value))) end + assert(is_shell_safe(self)) local complete = self:option() :description "Output a shell completion script for the specified shell." @@ -1115,6 +1144,7 @@ function Parser:add_complete_command(value) assert(type(value) == "string" or type(value) == "table", ("bad argument #1 to 'add_complete_command' (string or table expected, got %s)"):format(type(value))) end + assert(is_shell_safe(self)) local complete = self:command() :description "Output a shell completion script." From c2b2a16abe6293fbef40a3331c53188beba2fe9a Mon Sep 17 00:00:00 2001 From: Paul Ouellette Date: Tue, 9 Jul 2019 00:24:44 -0400 Subject: [PATCH 21/37] Zsh completions: Improve option completion - support arguments with choices - allow invoking an option multiple times - complete files if an argument is required, but no choices are specified --- src/argparse.lua | 37 ++++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/src/argparse.lua b/src/argparse.lua index f06b4f9..afb5d2c 100644 --- a/src/argparse.lua +++ b/src/argparse.lua @@ -1290,27 +1290,38 @@ end function Parser:get_zsh_complete() local buf = {("compdef _%s %s\n"):format(self._name, self._name)} - table.insert(buf, ("_%s() {"):format(self._name)) - table.insert(buf, " typeset -A opt_args") - table.insert(buf, " local context state line") - table.insert(buf, " local ret=1\n") - table.insert(buf, " _arguments -s -S -C \\") + table.insert(buf, "_" .. self._name .. "() {") + table.insert(buf, [[ + local context state state_descr line ret=1 + typeset -A opt_args + + _arguments -s -S \]]) for _, option in ipairs(self._options) do local line = {} if #option._aliases > 1 then - table.insert(line, ("{%s}"):format(table.concat(option._aliases, ","))) - if option._description then - table.insert(line, ('"[%s]"'):format(get_short_description(option))) + if option._maxcount > 1 then + table.insert(line, '"*"') end + table.insert(line, "{" .. table.concat(option._aliases, ",") .. '}"') else - table.insert(line, '"' .. option._aliases[1]) - if option._description then - table.insert(line, ("[%s]"):format(get_short_description(option))) - end table.insert(line, '"') + if option._maxcount > 1 then + table.insert(line, "*") + end + table.insert(line, option._aliases[1]) end - table.insert(buf, (" "):rep(4) .. table.concat(line) .. " \\") + if option._description then + table.insert(line, "[" .. get_short_description(option) .. "]") + end + if option._choices then + table.insert(line, ": :(" .. table.concat(option._choices, " ") .. ")") + elseif option._minargs > 0 then + table.insert(line, ": :_files") + end + table.insert(line, '"') + + table.insert(buf, " " .. table.concat(line) .. " \\") end table.insert(buf, " && ret=0\n") From b7be8cd99a0b96920198e0e37a1e26637deb82b1 Mon Sep 17 00:00:00 2001 From: Paul Ouellette Date: Sat, 13 Jul 2019 16:00:22 -0400 Subject: [PATCH 22/37] Zsh completions: complete commands and command options Supports an arbitrary number of nested subcommands. --- src/argparse.lua | 76 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 64 insertions(+), 12 deletions(-) diff --git a/src/argparse.lua b/src/argparse.lua index afb5d2c..2632731 100644 --- a/src/argparse.lua +++ b/src/argparse.lua @@ -1288,14 +1288,8 @@ complete -F _%s -o bashdefault -o default %s return table.concat(buf, "\n") end -function Parser:get_zsh_complete() - local buf = {("compdef _%s %s\n"):format(self._name, self._name)} - table.insert(buf, "_" .. self._name .. "() {") - table.insert(buf, [[ - local context state state_descr line ret=1 - typeset -A opt_args - - _arguments -s -S \]]) +function Parser:_zsh_arguments(buf, cmd_name, indent) + table.insert(buf, (" "):rep(indent) .. "_arguments -s -S \\") for _, option in ipairs(self._options) do local line = {} @@ -1321,14 +1315,72 @@ function Parser:get_zsh_complete() end table.insert(line, '"') - table.insert(buf, " " .. table.concat(line) .. " \\") + table.insert(buf, (" "):rep(indent + 2) .. table.concat(line) .. " \\") end - table.insert(buf, " && ret=0\n") - table.insert(buf, " return ret") + if #self._commands > 0 then + table.insert(buf, (" "):rep(indent + 2) .. '": :_' .. cmd_name .. '_cmds" \\') + table.insert(buf, (" "):rep(indent + 2) .. '"*:: :->args" \\') + end + table.insert(buf, (" "):rep(indent + 2) .. "&& return 0") +end + +function Parser:_zsh_cmds(buf, cmd_name) + table.insert(buf, "\n_" .. cmd_name .. "_cmds() {") + table.insert(buf, " local -a commands=(") + + for _, command in ipairs(self._commands) do + local line = {} + if #command._aliases > 1 then + table.insert(line, "{" .. table.concat(command._aliases, ",") .. '}"') + else + table.insert(line, '"' .. command._aliases[1]) + end + if command._description then + table.insert(line, ":" .. get_short_description(command)) + end + table.insert(buf, " " .. table.concat(line) .. '"') + end + + table.insert(buf, ' )\n _describe "command" commands\n}') +end + +function Parser:_zsh_complete_help(buf, cmds_buf, cmd_name, indent) + if #self._commands == 0 then + return + end + + self:_zsh_cmds(cmds_buf, cmd_name) + table.insert(buf, "\n" .. (" "):rep(indent) .. "case $words[1] in") + + for _, command in ipairs(self._commands) do + local name = cmd_name .. "_" .. command._aliases[1] + table.insert(buf, (" "):rep(indent + 2) .. table.concat(command._aliases, "|") .. ")") + command:_zsh_arguments(buf, name, indent + 4) + command:_zsh_complete_help(buf, cmds_buf, name, indent + 4) + table.insert(buf, (" "):rep(indent + 4) .. ";;\n") + end + + table.insert(buf, (" "):rep(indent) .. "esac") +end + +function Parser:get_zsh_complete() + local buf = {("compdef _%s %s\n"):format(self._name, self._name)} + local cmds_buf = {} + table.insert(buf, "_" .. self._name .. "() {") + if #self._commands > 0 then + table.insert(buf, " local context state state_descr line") + table.insert(buf, " typeset -A opt_args\n") + end + self:_zsh_arguments(buf, self._name, 2) + self:_zsh_complete_help(buf, cmds_buf, self._name, 2) table.insert(buf, "}") - return table.concat(buf, "\n") .. "\n" + local result = table.concat(buf, "\n") + if #cmds_buf > 0 then + result = result .. "\n" .. table.concat(cmds_buf, "\n") + end + return result .. "\n" end local function fish_escape(string) From d747e69a552b55dbbe379b32d1d4bdc63a7f1265 Mon Sep 17 00:00:00 2001 From: Paul Ouellette Date: Sat, 13 Jul 2019 18:42:37 -0400 Subject: [PATCH 23/37] Zsh completions: complete positional arguments - Completes choices is they exist, otherwise calls _files - Supports repeated arguments --- src/argparse.lua | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/src/argparse.lua b/src/argparse.lua index 2632731..a93b2e1 100644 --- a/src/argparse.lua +++ b/src/argparse.lua @@ -1080,6 +1080,7 @@ function Parser:add_help_command(value) help "help" end + help._is_help_command = true self._help_command = help return self end @@ -1318,10 +1319,31 @@ function Parser:_zsh_arguments(buf, cmd_name, indent) table.insert(buf, (" "):rep(indent + 2) .. table.concat(line) .. " \\") end - if #self._commands > 0 then - table.insert(buf, (" "):rep(indent + 2) .. '": :_' .. cmd_name .. '_cmds" \\') - table.insert(buf, (" "):rep(indent + 2) .. '"*:: :->args" \\') + if self._is_help_command then + table.insert(buf, (" "):rep(indent + 2) .. '": :(' .. get_commands(self._parent) .. ')" \\') + else + for _, argument in ipairs(self._arguments) do + local spec + if argument._choices then + spec = ": :(" .. table.concat(argument._choices, " ") .. ")" + else + spec = ": :_files" + end + if argument._maxargs == math.huge then + table.insert(buf, (" "):rep(indent + 2) .. '"*' .. spec .. '" \\') + break + end + for _ = 1, argument._maxargs do + table.insert(buf, (" "):rep(indent + 2) .. '"' .. spec .. '" \\') + end + end + + if #self._commands > 0 then + table.insert(buf, (" "):rep(indent + 2) .. '": :_' .. cmd_name .. '_cmds" \\') + table.insert(buf, (" "):rep(indent + 2) .. '"*:: :->args" \\') + end end + table.insert(buf, (" "):rep(indent + 2) .. "&& return 0") end From 11ae9b8bdffa13fe52fd8ce064779a0d741e7199 Mon Sep 17 00:00:00 2001 From: Paul Ouellette Date: Wed, 17 Jul 2019 23:40:47 -0400 Subject: [PATCH 24/37] Remove Parser._help_command --- spec/completion_spec.lua | 2 +- src/argparse.lua | 29 ++++++++++++----------------- 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/spec/completion_spec.lua b/spec/completion_spec.lua index 2def596..fe78537 100644 --- a/spec/completion_spec.lua +++ b/spec/completion_spec.lua @@ -259,9 +259,9 @@ complete -c foo -n '__fish_seen_subcommand_from install' -s v -l verbose parser:command "install" :add_help(false) assert.equal([[ +complete -c foo -n '__fish_use_subcommand' -xa 'help' -d 'Show help for commands' complete -c foo -n '__fish_seen_subcommand_from help' -xa 'help' complete -c foo -n '__fish_seen_subcommand_from help' -xa 'install' -complete -c foo -n '__fish_use_subcommand' -xa 'help' -d 'Show help for commands' complete -c foo -n '__fish_use_subcommand' -xa 'install' ]], parser:get_fish_complete()) end) diff --git a/src/argparse.lua b/src/argparse.lua index a93b2e1..d7c069d 100644 --- a/src/argparse.lua +++ b/src/argparse.lua @@ -1081,7 +1081,6 @@ function Parser:add_help_command(value) end help._is_help_command = true - self._help_command = help return self end @@ -1222,7 +1221,7 @@ end function Parser:_bash_get_cmd(buf) local cmds = {} for _, command in ipairs(self._commands) do - if command ~= self._help_command then + if not command._is_help_command then table.insert(cmds, (" "):rep(12) .. ("%s)"):format(table.concat(command._aliases, "|"))) table.insert(cmds, (" "):rep(16) .. ('cmd="%s"'):format(command._aliases[1])) table.insert(cmds, (" "):rep(16) .. "break") @@ -1242,7 +1241,7 @@ end function Parser:_bash_cmd_completions(buf) local subcmds = {} for _, command in ipairs(self._commands) do - if #command._options > 0 and command ~= self._help_command then + if #command._options > 0 and not command._is_help_command then table.insert(subcmds, (" "):rep(8) .. command._aliases[1] .. ")") command:_bash_option_args(subcmds, 12) table.insert(subcmds, (" "):rep(12) .. ('opts="$opts %s"'):format(get_options(command))) @@ -1413,14 +1412,22 @@ function Parser:_fish_complete_help(lines, prefix) for _, command in ipairs(self._commands) do for _, alias in ipairs(command._aliases) do local line = ("%s -n '__fish_use_subcommand' -xa '%s'"):format(prefix, alias) - if command._description then local description = fish_escape(get_short_description(command)) line = ("%s -d '%s'"):format(line, description) end - table.insert(lines, line) end + + if command._is_help_command then + local help_aliases = table.concat(command._aliases, " ") + + for _, cmd in ipairs(self._commands) do + local line = ("%s -n '__fish_seen_subcommand_from %s' -xa '%s'") + :format(prefix, help_aliases, table.concat(cmd._aliases, " ")) + table.insert(lines, line) + end + end end for _, option in ipairs(self._options) do @@ -1463,19 +1470,7 @@ end function Parser:get_fish_complete() local lines = {} local prefix = ("complete -c %s"):format(self._name) - - if self._help_command then - local help_aliases = table.concat(self._help_command._aliases, " ") - - for _, command in ipairs(self._commands) do - local line = ("%s -n '__fish_seen_subcommand_from %s' -xa '%s'") - :format(prefix, help_aliases, command._aliases[1]) - table.insert(lines, line) - end - end - self:_fish_complete_help(lines, prefix) - return table.concat(lines, "\n") .. "\n" end From 397c1fd739bb63eec505ff71188226a3f2b2fc9c Mon Sep 17 00:00:00 2001 From: Paul Ouellette Date: Thu, 18 Jul 2019 16:16:58 -0400 Subject: [PATCH 25/37] Zsh completions: complete multiple option arguments --- src/argparse.lua | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/argparse.lua b/src/argparse.lua index d7c069d..e78b6fb 100644 --- a/src/argparse.lua +++ b/src/argparse.lua @@ -1308,9 +1308,12 @@ function Parser:_zsh_arguments(buf, cmd_name, indent) if option._description then table.insert(line, "[" .. get_short_description(option) .. "]") end + if option._maxargs == math.huge then + table.insert(line, ":*") + end if option._choices then table.insert(line, ": :(" .. table.concat(option._choices, " ") .. ")") - elseif option._minargs > 0 then + elseif option._maxargs > 0 then table.insert(line, ": :_files") end table.insert(line, '"') From b265bf58d19007f0f0e0475a8841cb4979cada62 Mon Sep 17 00:00:00 2001 From: Paul Ouellette Date: Thu, 18 Jul 2019 16:19:01 -0400 Subject: [PATCH 26/37] Zsh completions: return 1 if no completions --- src/argparse.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/src/argparse.lua b/src/argparse.lua index e78b6fb..912840f 100644 --- a/src/argparse.lua +++ b/src/argparse.lua @@ -1398,6 +1398,7 @@ function Parser:get_zsh_complete() end self:_zsh_arguments(buf, self._name, 2) self:_zsh_complete_help(buf, cmds_buf, self._name, 2) + table.insert(buf, "\n return 1") table.insert(buf, "}") local result = table.concat(buf, "\n") From 461f2bd2e511d93cb1de86e8c90d07c241e60de5 Mon Sep 17 00:00:00 2001 From: Paul Ouellette Date: Thu, 18 Jul 2019 22:17:09 -0400 Subject: [PATCH 27/37] Completion clean ups --- spec/completion_spec.lua | 3 +- src/argparse.lua | 77 +++++++++++++++++----------------------- 2 files changed, 34 insertions(+), 46 deletions(-) diff --git a/spec/completion_spec.lua b/spec/completion_spec.lua index fe78537..105d11f 100644 --- a/spec/completion_spec.lua +++ b/spec/completion_spec.lua @@ -260,8 +260,7 @@ complete -c foo -n '__fish_seen_subcommand_from install' -s v -l verbose :add_help(false) assert.equal([[ complete -c foo -n '__fish_use_subcommand' -xa 'help' -d 'Show help for commands' -complete -c foo -n '__fish_seen_subcommand_from help' -xa 'help' -complete -c foo -n '__fish_seen_subcommand_from help' -xa 'install' +complete -c foo -n '__fish_seen_subcommand_from help' -xa 'help install' complete -c foo -n '__fish_use_subcommand' -xa 'install' ]], parser:get_fish_complete()) end) diff --git a/src/argparse.lua b/src/argparse.lua index 912840f..dc0ba4b 100644 --- a/src/argparse.lua +++ b/src/argparse.lua @@ -1084,18 +1084,18 @@ function Parser:add_help_command(value) return self end -local function is_shell_safe(parser) - if parser._name:find("[^%w_%-%+%.]") then +function Parser:_is_shell_safe() + if self._name:find("[^%w_%-%+%.]") then return false end - for _, command in ipairs(parser._commands) do + for _, command in ipairs(self._commands) do for _, alias in ipairs(command._aliases) do if alias:find("[^%w_%-%+%.]") then return false end end end - for _, option in ipairs(parser._options) do + for _, option in ipairs(self._options) do for _, alias in ipairs(option._aliases) do if alias:find("[^%w_%-%+%.]") then return false @@ -1117,7 +1117,7 @@ function Parser:add_complete(value) assert(type(value) == "string" or type(value) == "table", ("bad argument #1 to 'add_complete' (string or table expected, got %s)"):format(type(value))) end - assert(is_shell_safe(self)) + assert(self:_is_shell_safe()) local complete = self:option() :description "Output a shell completion script for the specified shell." @@ -1144,7 +1144,7 @@ function Parser:add_complete_command(value) assert(type(value) == "string" or type(value) == "table", ("bad argument #1 to 'add_complete_command' (string or table expected, got %s)"):format(type(value))) end - assert(is_shell_safe(self)) + assert(self:_is_shell_safe()) local complete = self:command() :description "Output a shell completion script." @@ -1172,9 +1172,9 @@ local function get_short_description(element) return short or element:_get_description():match("^(.-)%.?$") end -local function get_options(parser) +function Parser:_get_options() local options = {} - for _, option in ipairs(parser._options) do + for _, option in ipairs(self._options) do for _, alias in ipairs(option._aliases) do table.insert(options, alias) end @@ -1182,9 +1182,9 @@ local function get_options(parser) return table.concat(options, " ") end -local function get_commands(parser) +function Parser:_get_commands() local commands = {} - for _, command in ipairs(parser._commands) do + for _, command in ipairs(self._commands) do for _, alias in ipairs(command._aliases) do table.insert(commands, alias) end @@ -1196,15 +1196,13 @@ function Parser:_bash_option_args(buf, indent) local opts = {} for _, option in ipairs(self._options) do if option._choices or option._minargs > 0 then - local pattern = ("%s)"):format(table.concat(option._aliases, "|")) local compreply if option._choices then - compreply = ('COMPREPLY=($(compgen -W "%s" -- "$cur"))') - :format(table.concat(option._choices, " ")) + compreply = 'COMPREPLY=($(compgen -W "' .. table.concat(option._choices, " ") .. '" -- "$cur"))' else compreply = 'COMPREPLY=($(compgen -f "$cur"))' end - table.insert(opts, (" "):rep(indent + 4) .. pattern) + table.insert(opts, (" "):rep(indent + 4) .. table.concat(option._aliases, "|") .. ")") table.insert(opts, (" "):rep(indent + 8) .. compreply) table.insert(opts, (" "):rep(indent + 8) .. "return 0") table.insert(opts, (" "):rep(indent + 8) .. ";;") @@ -1222,8 +1220,8 @@ function Parser:_bash_get_cmd(buf) local cmds = {} for _, command in ipairs(self._commands) do if not command._is_help_command then - table.insert(cmds, (" "):rep(12) .. ("%s)"):format(table.concat(command._aliases, "|"))) - table.insert(cmds, (" "):rep(16) .. ('cmd="%s"'):format(command._aliases[1])) + table.insert(cmds, (" "):rep(12) .. table.concat(command._aliases, "|") .. ")") + table.insert(cmds, (" "):rep(16) .. 'cmd="' .. command._aliases[1] .. '"') table.insert(cmds, (" "):rep(16) .. "break") table.insert(cmds, (" "):rep(16) .. ";;") end @@ -1244,14 +1242,14 @@ function Parser:_bash_cmd_completions(buf) if #command._options > 0 and not command._is_help_command then table.insert(subcmds, (" "):rep(8) .. command._aliases[1] .. ")") command:_bash_option_args(subcmds, 12) - table.insert(subcmds, (" "):rep(12) .. ('opts="$opts %s"'):format(get_options(command))) + table.insert(subcmds, (" "):rep(12) .. 'opts="$opts ' .. command:_get_options() .. '"') table.insert(subcmds, (" "):rep(12) .. ";;") end end table.insert(buf, (" "):rep(4) .. 'case "$cmd" in') table.insert(buf, (" "):rep(8) .. self._name .. ")") - table.insert(buf, (" "):rep(12) .. ('COMPREPLY=($(compgen -W "%s" -- "$cur"))'):format(get_commands(self))) + table.insert(buf, (" "):rep(12) .. 'COMPREPLY=($(compgen -W "' .. self:_get_commands() .. '" -- "$cur"))') table.insert(buf, (" "):rep(12) .. ";;") if #subcmds > 0 then table.insert(buf, table.concat(subcmds, "\n")) @@ -1268,7 +1266,7 @@ _%s() { prev="${COMP_WORDS[COMP_CWORD-1]}" cmd="%s" opts="%s" -]]):format(self._name, self._name, get_options(self))} +]]):format(self._name, self._name, self:_get_options())} self:_bash_option_args(buf, 4) self:_bash_get_cmd(buf) @@ -1322,7 +1320,7 @@ function Parser:_zsh_arguments(buf, cmd_name, indent) end if self._is_help_command then - table.insert(buf, (" "):rep(indent + 2) .. '": :(' .. get_commands(self._parent) .. ')" \\') + table.insert(buf, (" "):rep(indent + 2) .. '": :(' .. self._parent:_get_commands() .. ')" \\') else for _, argument in ipairs(self._arguments) do local spec @@ -1412,25 +1410,20 @@ local function fish_escape(string) return string:gsub("[\\']", "\\%0") end -function Parser:_fish_complete_help(lines, prefix) +function Parser:_fish_complete_help(buf, prefix) for _, command in ipairs(self._commands) do for _, alias in ipairs(command._aliases) do local line = ("%s -n '__fish_use_subcommand' -xa '%s'"):format(prefix, alias) if command._description then - local description = fish_escape(get_short_description(command)) - line = ("%s -d '%s'"):format(line, description) + line = ("%s -d '%s'"):format(line, fish_escape(get_short_description(command))) end - table.insert(lines, line) + table.insert(buf, line) end if command._is_help_command then - local help_aliases = table.concat(command._aliases, " ") - - for _, cmd in ipairs(self._commands) do - local line = ("%s -n '__fish_seen_subcommand_from %s' -xa '%s'") - :format(prefix, help_aliases, table.concat(cmd._aliases, " ")) - table.insert(lines, line) - end + local line = ("%s -n '__fish_seen_subcommand_from %s' -xa '%s'") + :format(prefix, table.concat(command._aliases, " "), self:_get_commands()) + table.insert(buf, line) end end @@ -1438,9 +1431,7 @@ function Parser:_fish_complete_help(lines, prefix) local parts = {prefix} if self._parent then - local aliases = table.concat(self._aliases, " ") - local condition = ("-n '__fish_seen_subcommand_from %s'"):format(aliases) - table.insert(parts, condition) + table.insert(parts, "-n '__fish_seen_subcommand_from " .. table.concat(self._aliases, " ") .. "'") end for _, alias in ipairs(option._aliases) do @@ -1452,30 +1443,28 @@ function Parser:_fish_complete_help(lines, prefix) end if option._choices then - local choices = ("-xa '%s'"):format(table.concat(option._choices, " ")) - table.insert(parts, choices) + table.insert(parts, "-xa '" .. table.concat(option._choices, " ") .. "'") elseif option._minargs > 0 then table.insert(parts, "-r") end if option._description then - local description = ("-d '%s'"):format(fish_escape(get_short_description(option))) - table.insert(parts, description) + table.insert(parts, "-d '" .. fish_escape(get_short_description(option)) .. "'") end - table.insert(lines, table.concat(parts, " ")) + table.insert(buf, table.concat(parts, " ")) end for _, command in ipairs(self._commands) do - command:_fish_complete_help(lines, prefix) + command:_fish_complete_help(buf, prefix) end end function Parser:get_fish_complete() - local lines = {} - local prefix = ("complete -c %s"):format(self._name) - self:_fish_complete_help(lines, prefix) - return table.concat(lines, "\n") .. "\n" + local buf = {} + local prefix = "complete -c " .. self._name + self:_fish_complete_help(buf, prefix) + return table.concat(buf, "\n") .. "\n" end local function get_tip(context, wrong_name) From 8d258186c6df87ca5bf56f69c0a0387f50b3d66a Mon Sep 17 00:00:00 2001 From: Paul Ouellette Date: Sat, 20 Jul 2019 09:19:58 -0400 Subject: [PATCH 28/37] Fish completions improvement --- src/argparse.lua | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/argparse.lua b/src/argparse.lua index dc0ba4b..9347ca2 100644 --- a/src/argparse.lua +++ b/src/argparse.lua @@ -1411,6 +1411,8 @@ local function fish_escape(string) end function Parser:_fish_complete_help(buf, prefix) + table.insert(buf, "") + for _, command in ipairs(self._commands) do for _, alias in ipairs(command._aliases) do local line = ("%s -n '__fish_use_subcommand' -xa '%s'"):format(prefix, alias) @@ -1420,11 +1422,12 @@ function Parser:_fish_complete_help(buf, prefix) table.insert(buf, line) end - if command._is_help_command then - local line = ("%s -n '__fish_seen_subcommand_from %s' -xa '%s'") - :format(prefix, table.concat(command._aliases, " "), self:_get_commands()) - table.insert(buf, line) - end + end + + if self._is_help_command then + local line = ("%s -n '__fish_seen_subcommand_from %s' -xa '%s'") + :format(prefix, table.concat(self._aliases, " "), self._parent:_get_commands()) + table.insert(buf, line) end for _, option in ipairs(self._options) do From e6506bd408f386afde0ab0f97c247c5434c2d4ab Mon Sep 17 00:00:00 2001 From: Paul Ouellette Date: Sat, 20 Jul 2019 09:20:32 -0400 Subject: [PATCH 29/37] Update completion tests --- spec/completion_spec.lua | 420 +++++++++++++++++---------------------- spec/comptest | 42 ++++ 2 files changed, 220 insertions(+), 242 deletions(-) create mode 100755 spec/comptest diff --git a/spec/completion_spec.lua b/spec/completion_spec.lua index 105d11f..5cf314d 100644 --- a/spec/completion_spec.lua +++ b/spec/completion_spec.lua @@ -1,155 +1,81 @@ -local Parser = require "argparse" -getmetatable(Parser()).error = function(_, msg) error(msg) end +local script = "./spec/comptest" +local script_cmd = "lua" + +if package.loaded["luacov.runner"] then + script_cmd = script_cmd .. " -lluacov" +end + +script_cmd = script_cmd .. " " .. script + +local function get_output(args) + local handler = io.popen(script_cmd .. " " .. args .. " 2>&1", "r") + local output = handler:read("*a") + handler:close() + return output +end describe("tests related to generation of shell completion scripts", function() - describe("bash completion scripts", function() - it("generates correct completions for help flag", function() - local parser = Parser "foo" - assert.equal([=[ -_foo() { + it("generates correct bash completion script", function() + assert.equal([=[ +_comptest() { local IFS=$' \t\n' local cur prev cmd opts arg cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" - cmd="foo" - opts="-h --help" - - if [[ "$cur" = -* ]]; then - COMPREPLY=($(compgen -W "$opts" -- "$cur")) - fi -} - -complete -F _foo -o bashdefault -o default foo -]=], parser:get_bash_complete()) - end) - - it("generates correct completions for options with required argument", function() - local parser = Parser "foo" - :add_help(false) - parser:option "--bar" - assert.equal([=[ -_foo() { - local IFS=$' \t\n' - local cur prev cmd opts arg - cur="${COMP_WORDS[COMP_CWORD]}" - prev="${COMP_WORDS[COMP_CWORD-1]}" - cmd="foo" - opts="--bar" + cmd="comptest" + opts="-h --help -f --files --direction" case "$prev" in - --bar) + -f|--files) COMPREPLY=($(compgen -f "$cur")) return 0 ;; - esac - - if [[ "$cur" = -* ]]; then - COMPREPLY=($(compgen -W "$opts" -- "$cur")) - fi -} - -complete -F _foo -o bashdefault -o default foo -]=], parser:get_bash_complete()) - end) - - it("generates correct completions for options with argument choices", function() - local parser = Parser "foo" - :add_help(false) - parser:option "--format" - :choices {"short", "medium", "full"} - assert.equal([=[ -_foo() { - local IFS=$' \t\n' - local cur prev cmd opts arg - cur="${COMP_WORDS[COMP_CWORD]}" - prev="${COMP_WORDS[COMP_CWORD-1]}" - cmd="foo" - opts="--format" - - case "$prev" in - --format) - COMPREPLY=($(compgen -W "short medium full" -- "$cur")) + --direction) + COMPREPLY=($(compgen -W "north south east west" -- "$cur")) return 0 ;; esac - if [[ "$cur" = -* ]]; then - COMPREPLY=($(compgen -W "$opts" -- "$cur")) - fi -} - -complete -F _foo -o bashdefault -o default foo -]=], parser:get_bash_complete()) - end) - - it("generates correct completions for commands", function() - local parser = Parser "foo" - :add_help(false) - parser:command "install" - :add_help(false) - assert.equal([=[ -_foo() { - local IFS=$' \t\n' - local cur prev cmd opts arg - cur="${COMP_WORDS[COMP_CWORD]}" - prev="${COMP_WORDS[COMP_CWORD-1]}" - cmd="foo" - opts="" - for arg in ${COMP_WORDS[@]:1}; do case "$arg" in - install) + completion) + cmd="completion" + break + ;; + install|i) cmd="install" break ;; + admin) + cmd="admin" + break + ;; esac done case "$cmd" in - foo) - COMPREPLY=($(compgen -W "install" -- "$cur")) + comptest) + COMPREPLY=($(compgen -W "help completion install i admin" -- "$cur")) ;; - esac - - if [[ "$cur" = -* ]]; then - COMPREPLY=($(compgen -W "$opts" -- "$cur")) - fi -} - -complete -F _foo -o bashdefault -o default foo -]=], parser:get_bash_complete()) - end) - - it("generates correct completions for command options", function() - local parser = Parser "foo" - :add_help(false) - local install = parser:command "install" - :add_help(false) - install:flag "-v --verbose" - assert.equal([=[ -_foo() { - local IFS=$' \t\n' - local cur prev cmd opts arg - cur="${COMP_WORDS[COMP_CWORD]}" - prev="${COMP_WORDS[COMP_CWORD-1]}" - cmd="foo" - opts="" - - for arg in ${COMP_WORDS[@]:1}; do - case "$arg" in - install) - cmd="install" - break - ;; - esac - done - - case "$cmd" in - foo) - COMPREPLY=($(compgen -W "install" -- "$cur")) + completion) + opts="$opts -h --help" ;; install) - opts="$opts -v --verbose" + case "$prev" in + --deps-mode) + COMPREPLY=($(compgen -W "all one order none" -- "$cur")) + return 0 + ;; + --pair) + COMPREPLY=($(compgen -f "$cur")) + return 0 + ;; + esac + + opts="$opts -h --help --deps-mode --no-doc --pair" + ;; + admin) + opts="$opts -h --help" ;; esac @@ -158,131 +84,141 @@ _foo() { fi } -complete -F _foo -o bashdefault -o default foo -]=], parser:get_bash_complete()) - end) - - it("generates completions for help command argument", function() - local parser = Parser "foo" - :add_help(false) - :add_help_command {add_help = false} - parser:command "install" - :add_help(false) - assert.equal([=[ -_foo() { - local IFS=$' \t\n' - local cur prev cmd opts arg - cur="${COMP_WORDS[COMP_CWORD]}" - prev="${COMP_WORDS[COMP_CWORD-1]}" - cmd="foo" - opts="" - - for arg in ${COMP_WORDS[@]:1}; do - case "$arg" in - install) - cmd="install" - break - ;; - esac - done - - case "$cmd" in - foo) - COMPREPLY=($(compgen -W "help install" -- "$cur")) - ;; - esac - - if [[ "$cur" = -* ]]; then - COMPREPLY=($(compgen -W "$opts" -- "$cur")) - fi -} - -complete -F _foo -o bashdefault -o default foo -]=], parser:get_bash_complete()) - end) +complete -F _comptest -o bashdefault -o default comptest +]=], get_output("completion bash")) end) - describe("fish completion scripts", function() - it("generates correct completions for help flag", function() - local parser = Parser "foo" - assert.equal([[ -complete -c foo -s h -l help -d 'Show this help message and exit' -]], parser:get_fish_complete()) - end) + it("generates correct zsh completion script", function() + assert.equal([=[ +compdef _comptest comptest - it("generates correct completions for options with required argument", function() - local parser = Parser "foo" - :add_help(false) - parser:option "--bar" - assert.equal([[ -complete -c foo -l bar -r -]], parser:get_fish_complete()) - end) +_comptest() { + local context state state_descr line + typeset -A opt_args - it("generates correct completions for options with argument choices", function() - local parser = Parser "foo" - :add_help(false) - parser:option "--format" - :choices {"short", "medium", "full"} - assert.equal([[ -complete -c foo -l format -xa 'short medium full' -]], parser:get_fish_complete()) - end) + _arguments -s -S \ + {-h,--help}"[Show this help message and exit]" \ + {-f,--files}"[A description with illegal \' characters]:*: :_files" \ + "--direction[The direction to go in]: :(north south east west)" \ + ": :_comptest_cmds" \ + "*:: :->args" \ + && return 0 - it("generates correct completions for commands", function() - local parser = Parser "foo" - :add_help(false) - parser:command "install" - :add_help(false) - :description "Install a rock." - assert.equal([[ -complete -c foo -n '__fish_use_subcommand' -xa 'install' -d 'Install a rock' -]], parser:get_fish_complete()) - end) + case $words[1] in + help) + _arguments -s -S \ + {-h,--help}"[Show this help message and exit]" \ + ": :(help completion install i admin)" \ + && return 0 + ;; - it("generates correct completions for command options", function() - local parser = Parser "foo" - :add_help(false) - local install = parser:command "install" - :add_help(false) - install:flag "-v --verbose" - assert.equal([[ -complete -c foo -n '__fish_use_subcommand' -xa 'install' -complete -c foo -n '__fish_seen_subcommand_from install' -s v -l verbose -]], parser:get_fish_complete()) - end) + completion) + _arguments -s -S \ + {-h,--help}"[Show this help message and exit]" \ + ": :(bash zsh fish)" \ + && return 0 + ;; - it("generates completions for help command argument", function() - local parser = Parser "foo" - :add_help(false) - :add_help_command {add_help = false} - parser:command "install" - :add_help(false) - assert.equal([[ -complete -c foo -n '__fish_use_subcommand' -xa 'help' -d 'Show help for commands' -complete -c foo -n '__fish_seen_subcommand_from help' -xa 'help install' -complete -c foo -n '__fish_use_subcommand' -xa 'install' -]], parser:get_fish_complete()) - end) + install|i) + _arguments -s -S \ + {-h,--help}"[Show this help message and exit]" \ + "--deps-mode: :(all one order none)" \ + "--no-doc[Install without documentation]" \ + "*--pair[A pair of files]: :_files" \ + && return 0 + ;; - it("uses fist sentence of descriptions", function() - local parser = Parser "foo" - :add_help(false) - parser:option "--bar" - :description "A description with a .period. Another sentence." - assert.equal([[ -complete -c foo -l bar -r -d 'A description with a .period' -]], parser:get_fish_complete()) - end) + admin) + _arguments -s -S \ + {-h,--help}"[Show this help message and exit]" \ + ": :_comptest_admin_cmds" \ + "*:: :->args" \ + && return 0 - it("escapes backslashes and single quotes in descriptions", function() - local parser = Parser "foo" - :add_help(false) - parser:option "--bar" - :description "A description with illegal \\' characters." - assert.equal([[ -complete -c foo -l bar -r -d 'A description with illegal \\\' characters' -]], parser:get_fish_complete()) - end) + case $words[1] in + help) + _arguments -s -S \ + {-h,--help}"[Show this help message and exit]" \ + ": :(help add remove)" \ + && return 0 + ;; + + add) + _arguments -s -S \ + {-h,--help}"[Show this help message and exit]" \ + ": :_files" \ + && return 0 + ;; + + remove) + _arguments -s -S \ + {-h,--help}"[Show this help message and exit]" \ + ": :_files" \ + && return 0 + ;; + + esac + ;; + + esac + + return 1 +} + +_comptest_cmds() { + local -a commands=( + "help:Show help for commands" + "completion:Output a shell completion script" + {install,i}":Install a rock" + "admin" + ) + _describe "command" commands +} + +_comptest_admin_cmds() { + local -a commands=( + "help:Show help for commands" + "add:Add a rock to a server" + "remove:Remove a rock from a server" + ) + _describe "command" commands +} +]=], get_output("completion zsh")) + end) + + it("generates correct fish completion script", function() + assert.equal([=[ + +complete -c comptest -n '__fish_use_subcommand' -xa 'help' -d 'Show help for commands' +complete -c comptest -n '__fish_use_subcommand' -xa 'completion' -d 'Output a shell completion script' +complete -c comptest -n '__fish_use_subcommand' -xa 'install' -d 'Install a rock' +complete -c comptest -n '__fish_use_subcommand' -xa 'i' -d 'Install a rock' +complete -c comptest -n '__fish_use_subcommand' -xa 'admin' +complete -c comptest -s h -l help -d 'Show this help message and exit' +complete -c comptest -s f -l files -r -d 'A description with illegal \\\' characters' +complete -c comptest -l direction -xa 'north south east west' -d 'The direction to go in' + +complete -c comptest -n '__fish_seen_subcommand_from help' -xa 'help completion install i admin' +complete -c comptest -n '__fish_seen_subcommand_from help' -s h -l help -d 'Show this help message and exit' + +complete -c comptest -n '__fish_seen_subcommand_from completion' -s h -l help -d 'Show this help message and exit' + +complete -c comptest -n '__fish_seen_subcommand_from install i' -s h -l help -d 'Show this help message and exit' +complete -c comptest -n '__fish_seen_subcommand_from install i' -l deps-mode -xa 'all one order none' +complete -c comptest -n '__fish_seen_subcommand_from install i' -l no-doc -d 'Install without documentation' +complete -c comptest -n '__fish_seen_subcommand_from install i' -l pair -r -d 'A pair of files' + +complete -c comptest -n '__fish_use_subcommand' -xa 'help' -d 'Show help for commands' +complete -c comptest -n '__fish_use_subcommand' -xa 'add' -d 'Add a rock to a server' +complete -c comptest -n '__fish_use_subcommand' -xa 'remove' -d 'Remove a rock from a server' +complete -c comptest -n '__fish_seen_subcommand_from admin' -s h -l help -d 'Show this help message and exit' + +complete -c comptest -n '__fish_seen_subcommand_from help' -xa 'help add remove' +complete -c comptest -n '__fish_seen_subcommand_from help' -s h -l help -d 'Show this help message and exit' + +complete -c comptest -n '__fish_seen_subcommand_from add' -s h -l help -d 'Show this help message and exit' + +complete -c comptest -n '__fish_seen_subcommand_from remove' -s h -l help -d 'Show this help message and exit' +]=], get_output("completion fish")) end) end) diff --git a/spec/comptest b/spec/comptest new file mode 100755 index 0000000..32da5a0 --- /dev/null +++ b/spec/comptest @@ -0,0 +1,42 @@ +#!/usr/bin/env lua + +local argparse = require "argparse" + +local parser = argparse "comptest" + :add_help_command() + :add_complete_command() + +parser:option "-f --files" + :description "A description with illegal \\' characters." + :args "+" + +parser:option "--direction" + :description "The direction to go in." + :choices {"north", "south", "east", "west"} + +local install = parser:command "install i" + :description "Install a rock." + +install:option "--deps-mode" + :choices {"all", "one", "order", "none"} + +install:flag "--no-doc" + :description "Install without documentation." + +install:option "--pair" + :description "A pair of files." + :args "2" + :count "*" + +local admin = parser:command "admin" + :add_help_command() + +local admin_add = admin:command "add" + :description "Add a rock to a server." +admin_add:argument "rock" + +local admin_remove = admin:command "remove" + :description "Remove a rock from a server." +admin_remove:argument "rock" + +parser:parse() From 146dcd4ecdc9d844740bb5b0a478a241337bb1fe Mon Sep 17 00:00:00 2001 From: Paul Ouellette Date: Mon, 22 Jul 2019 17:37:25 -0400 Subject: [PATCH 30/37] Fix Parser:_is_shell_safe --- src/argparse.lua | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/argparse.lua b/src/argparse.lua index 9347ca2..75e0ead 100644 --- a/src/argparse.lua +++ b/src/argparse.lua @@ -1088,8 +1088,8 @@ function Parser:_is_shell_safe() if self._name:find("[^%w_%-%+%.]") then return false end - for _, command in ipairs(self._commands) do - for _, alias in ipairs(command._aliases) do + if self._aliases then + for _, alias in ipairs(self._aliases) do if alias:find("[^%w_%-%+%.]") then return false end @@ -1100,15 +1100,20 @@ function Parser:_is_shell_safe() if alias:find("[^%w_%-%+%.]") then return false end - if option._choices then - for _, choice in ipairs(option._choices) do - if choice:find("[%s'\"]") then - return false - end + end + if option._choices then + for _, choice in ipairs(option._choices) do + if choice:find("[%s'\"]") then + return false end end end end + for _, command in ipairs(self._commands) do + if not command:_is_shell_safe() then + return false + end + end return true end @@ -1117,7 +1122,6 @@ function Parser:add_complete(value) assert(type(value) == "string" or type(value) == "table", ("bad argument #1 to 'add_complete' (string or table expected, got %s)"):format(type(value))) end - assert(self:_is_shell_safe()) local complete = self:option() :description "Output a shell completion script for the specified shell." @@ -1144,7 +1148,6 @@ function Parser:add_complete_command(value) assert(type(value) == "string" or type(value) == "table", ("bad argument #1 to 'add_complete_command' (string or table expected, got %s)"):format(type(value))) end - assert(self:_is_shell_safe()) local complete = self:command() :description "Output a shell completion script." @@ -1258,6 +1261,7 @@ function Parser:_bash_cmd_completions(buf) end function Parser:get_bash_complete() + assert(self:_is_shell_safe()) local buf = {([[ _%s() { local IFS=$' \t\n' @@ -1387,6 +1391,7 @@ function Parser:_zsh_complete_help(buf, cmds_buf, cmd_name, indent) end function Parser:get_zsh_complete() + assert(self:_is_shell_safe()) local buf = {("compdef _%s %s\n"):format(self._name, self._name)} local cmds_buf = {} table.insert(buf, "_" .. self._name .. "() {") @@ -1464,6 +1469,7 @@ function Parser:_fish_complete_help(buf, prefix) end function Parser:get_fish_complete() + assert(self:_is_shell_safe()) local buf = {} local prefix = "complete -c " .. self._name self:_fish_complete_help(buf, prefix) From d61b5e4f3d7dee57d1cd123edf0fb0ab01bf654e Mon Sep 17 00:00:00 2001 From: Paul Ouellette Date: Tue, 23 Jul 2019 01:45:49 -0400 Subject: [PATCH 31/37] Replace _aliases[1] with equivalent _name --- src/argparse.lua | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/argparse.lua b/src/argparse.lua index 75e0ead..9f8f4a7 100644 --- a/src/argparse.lua +++ b/src/argparse.lua @@ -1224,7 +1224,7 @@ function Parser:_bash_get_cmd(buf) for _, command in ipairs(self._commands) do if not command._is_help_command then table.insert(cmds, (" "):rep(12) .. table.concat(command._aliases, "|") .. ")") - table.insert(cmds, (" "):rep(16) .. 'cmd="' .. command._aliases[1] .. '"') + table.insert(cmds, (" "):rep(16) .. 'cmd="' .. command._name .. '"') table.insert(cmds, (" "):rep(16) .. "break") table.insert(cmds, (" "):rep(16) .. ";;") end @@ -1243,7 +1243,7 @@ function Parser:_bash_cmd_completions(buf) local subcmds = {} for _, command in ipairs(self._commands) do if #command._options > 0 and not command._is_help_command then - table.insert(subcmds, (" "):rep(8) .. command._aliases[1] .. ")") + table.insert(subcmds, (" "):rep(8) .. command._name .. ")") command:_bash_option_args(subcmds, 12) table.insert(subcmds, (" "):rep(12) .. 'opts="$opts ' .. command:_get_options() .. '"') table.insert(subcmds, (" "):rep(12) .. ";;") @@ -1305,7 +1305,7 @@ function Parser:_zsh_arguments(buf, cmd_name, indent) if option._maxcount > 1 then table.insert(line, "*") end - table.insert(line, option._aliases[1]) + table.insert(line, option._name) end if option._description then table.insert(line, "[" .. get_short_description(option) .. "]") @@ -1360,7 +1360,7 @@ function Parser:_zsh_cmds(buf, cmd_name) if #command._aliases > 1 then table.insert(line, "{" .. table.concat(command._aliases, ",") .. '}"') else - table.insert(line, '"' .. command._aliases[1]) + table.insert(line, '"' .. command._name) end if command._description then table.insert(line, ":" .. get_short_description(command)) @@ -1380,7 +1380,7 @@ function Parser:_zsh_complete_help(buf, cmds_buf, cmd_name, indent) table.insert(buf, "\n" .. (" "):rep(indent) .. "case $words[1] in") for _, command in ipairs(self._commands) do - local name = cmd_name .. "_" .. command._aliases[1] + local name = cmd_name .. "_" .. command._name table.insert(buf, (" "):rep(indent + 2) .. table.concat(command._aliases, "|") .. ")") command:_zsh_arguments(buf, name, indent + 4) command:_zsh_complete_help(buf, cmds_buf, name, indent + 4) From 56e81ad786bd85209833aaa39e82618675f2b016 Mon Sep 17 00:00:00 2001 From: Paul Ouellette Date: Tue, 23 Jul 2019 15:03:35 -0400 Subject: [PATCH 32/37] Use the base name for completions --- spec/comptest | 2 +- src/argparse.lua | 32 ++++++++++++++++++++------------ 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/spec/comptest b/spec/comptest index 32da5a0..c47d5b6 100755 --- a/spec/comptest +++ b/spec/comptest @@ -2,7 +2,7 @@ local argparse = require "argparse" -local parser = argparse "comptest" +local parser = argparse() :add_help_command() :add_complete_command() diff --git a/src/argparse.lua b/src/argparse.lua index 9f8f4a7..e45a6c8 100644 --- a/src/argparse.lua +++ b/src/argparse.lua @@ -1085,10 +1085,11 @@ function Parser:add_help_command(value) end function Parser:_is_shell_safe() - if self._name:find("[^%w_%-%+%.]") then - return false - end - if self._aliases then + if self._basename then + if self._basename:find("[^%w_%-%+%.]") then + return false + end + else for _, alias in ipairs(self._aliases) do if alias:find("[^%w_%-%+%.]") then return false @@ -1170,6 +1171,10 @@ function Parser:add_complete_command(value) return self end +local function base_name(pathname) + return pathname:gsub("[/\\]*$", ""):match(".*[/\\]([^/\\]*)") or pathname +end + local function get_short_description(element) local short = element:_get_description():match("^(.-)%.%s") return short or element:_get_description():match("^(.-)%.?$") @@ -1251,7 +1256,7 @@ function Parser:_bash_cmd_completions(buf) end table.insert(buf, (" "):rep(4) .. 'case "$cmd" in') - table.insert(buf, (" "):rep(8) .. self._name .. ")") + table.insert(buf, (" "):rep(8) .. self._basename .. ")") table.insert(buf, (" "):rep(12) .. 'COMPREPLY=($(compgen -W "' .. self:_get_commands() .. '" -- "$cur"))') table.insert(buf, (" "):rep(12) .. ";;") if #subcmds > 0 then @@ -1261,6 +1266,7 @@ function Parser:_bash_cmd_completions(buf) end function Parser:get_bash_complete() + self._basename = base_name(self._name) assert(self:_is_shell_safe()) local buf = {([[ _%s() { @@ -1270,7 +1276,7 @@ _%s() { prev="${COMP_WORDS[COMP_CWORD-1]}" cmd="%s" opts="%s" -]]):format(self._name, self._name, self:_get_options())} +]]):format(self._basename, self._basename, self:_get_options())} self:_bash_option_args(buf, 4) self:_bash_get_cmd(buf) @@ -1285,7 +1291,7 @@ _%s() { } complete -F _%s -o bashdefault -o default %s -]=]):format(self._name, self._name)) +]=]):format(self._basename, self._basename)) return table.concat(buf, "\n") end @@ -1391,16 +1397,17 @@ function Parser:_zsh_complete_help(buf, cmds_buf, cmd_name, indent) end function Parser:get_zsh_complete() + self._basename = base_name(self._name) assert(self:_is_shell_safe()) - local buf = {("compdef _%s %s\n"):format(self._name, self._name)} + local buf = {("compdef _%s %s\n"):format(self._basename, self._basename)} local cmds_buf = {} - table.insert(buf, "_" .. self._name .. "() {") + table.insert(buf, "_" .. self._basename .. "() {") if #self._commands > 0 then table.insert(buf, " local context state state_descr line") table.insert(buf, " typeset -A opt_args\n") end - self:_zsh_arguments(buf, self._name, 2) - self:_zsh_complete_help(buf, cmds_buf, self._name, 2) + self:_zsh_arguments(buf, self._basename, 2) + self:_zsh_complete_help(buf, cmds_buf, self._basename, 2) table.insert(buf, "\n return 1") table.insert(buf, "}") @@ -1469,9 +1476,10 @@ function Parser:_fish_complete_help(buf, prefix) end function Parser:get_fish_complete() + self._basename = base_name(self._name) assert(self:_is_shell_safe()) local buf = {} - local prefix = "complete -c " .. self._name + local prefix = "complete -c " .. self._basename self:_fish_complete_help(buf, prefix) return table.concat(buf, "\n") .. "\n" end From a0c5ddf1026fd589d7ec61e89c93a3d5be119f93 Mon Sep 17 00:00:00 2001 From: Paul Ouellette Date: Tue, 23 Jul 2019 15:06:16 -0400 Subject: [PATCH 33/37] Update completion tests --- spec/completion_spec.lua | 28 ++++++++++++---------------- spec/comptest | 15 ++++++--------- 2 files changed, 18 insertions(+), 25 deletions(-) diff --git a/spec/completion_spec.lua b/spec/completion_spec.lua index 5cf314d..89c60f2 100644 --- a/spec/completion_spec.lua +++ b/spec/completion_spec.lua @@ -23,15 +23,15 @@ _comptest() { cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" cmd="comptest" - opts="-h --help -f --files --direction" + opts="-h --help --completion -v --verbose -f --files" case "$prev" in - -f|--files) - COMPREPLY=($(compgen -f "$cur")) + --completion) + COMPREPLY=($(compgen -W "bash zsh fish" -- "$cur")) return 0 ;; - --direction) - COMPREPLY=($(compgen -W "north south east west" -- "$cur")) + -f|--files) + COMPREPLY=($(compgen -f "$cur")) return 0 ;; esac @@ -66,13 +66,9 @@ _comptest() { COMPREPLY=($(compgen -W "all one order none" -- "$cur")) return 0 ;; - --pair) - COMPREPLY=($(compgen -f "$cur")) - return 0 - ;; esac - opts="$opts -h --help --deps-mode --no-doc --pair" + opts="$opts -h --help --deps-mode --no-doc" ;; admin) opts="$opts -h --help" @@ -98,8 +94,9 @@ _comptest() { _arguments -s -S \ {-h,--help}"[Show this help message and exit]" \ + "--completion[Output a shell completion script for the specified shell]: :(bash zsh fish)" \ + "*"{-v,--verbose}"[Set the verbosity level]" \ {-f,--files}"[A description with illegal \' characters]:*: :_files" \ - "--direction[The direction to go in]: :(north south east west)" \ ": :_comptest_cmds" \ "*:: :->args" \ && return 0 @@ -124,7 +121,6 @@ _comptest() { {-h,--help}"[Show this help message and exit]" \ "--deps-mode: :(all one order none)" \ "--no-doc[Install without documentation]" \ - "*--pair[A pair of files]: :_files" \ && return 0 ;; @@ -170,7 +166,7 @@ _comptest_cmds() { "help:Show help for commands" "completion:Output a shell completion script" {install,i}":Install a rock" - "admin" + "admin:Rock server administration interface" ) _describe "command" commands } @@ -193,10 +189,11 @@ complete -c comptest -n '__fish_use_subcommand' -xa 'help' -d 'Show help for com complete -c comptest -n '__fish_use_subcommand' -xa 'completion' -d 'Output a shell completion script' complete -c comptest -n '__fish_use_subcommand' -xa 'install' -d 'Install a rock' complete -c comptest -n '__fish_use_subcommand' -xa 'i' -d 'Install a rock' -complete -c comptest -n '__fish_use_subcommand' -xa 'admin' +complete -c comptest -n '__fish_use_subcommand' -xa 'admin' -d 'Rock server administration interface' complete -c comptest -s h -l help -d 'Show this help message and exit' +complete -c comptest -l completion -xa 'bash zsh fish' -d 'Output a shell completion script for the specified shell' +complete -c comptest -s v -l verbose -d 'Set the verbosity level' complete -c comptest -s f -l files -r -d 'A description with illegal \\\' characters' -complete -c comptest -l direction -xa 'north south east west' -d 'The direction to go in' complete -c comptest -n '__fish_seen_subcommand_from help' -xa 'help completion install i admin' complete -c comptest -n '__fish_seen_subcommand_from help' -s h -l help -d 'Show this help message and exit' @@ -206,7 +203,6 @@ complete -c comptest -n '__fish_seen_subcommand_from completion' -s h -l help -d complete -c comptest -n '__fish_seen_subcommand_from install i' -s h -l help -d 'Show this help message and exit' complete -c comptest -n '__fish_seen_subcommand_from install i' -l deps-mode -xa 'all one order none' complete -c comptest -n '__fish_seen_subcommand_from install i' -l no-doc -d 'Install without documentation' -complete -c comptest -n '__fish_seen_subcommand_from install i' -l pair -r -d 'A pair of files' complete -c comptest -n '__fish_use_subcommand' -xa 'help' -d 'Show help for commands' complete -c comptest -n '__fish_use_subcommand' -xa 'add' -d 'Add a rock to a server' diff --git a/spec/comptest b/spec/comptest index c47d5b6..e597de9 100755 --- a/spec/comptest +++ b/spec/comptest @@ -5,15 +5,16 @@ local argparse = require "argparse" local parser = argparse() :add_help_command() :add_complete_command() + :add_complete() + +parser:flag "-v --verbose" + :description "Set the verbosity level." + :count "*" parser:option "-f --files" :description "A description with illegal \\' characters." :args "+" -parser:option "--direction" - :description "The direction to go in." - :choices {"north", "south", "east", "west"} - local install = parser:command "install i" :description "Install a rock." @@ -23,12 +24,8 @@ install:option "--deps-mode" install:flag "--no-doc" :description "Install without documentation." -install:option "--pair" - :description "A pair of files." - :args "2" - :count "*" - local admin = parser:command "admin" + :description "Rock server administration interface." :add_help_command() local admin_add = admin:command "add" From 5f0cf3721e4ec0e5e11191e7c36e418e342ffe98 Mon Sep 17 00:00:00 2001 From: Paul Ouellette Date: Tue, 23 Jul 2019 17:46:16 -0400 Subject: [PATCH 34/37] Completions: validate/escape some missed things --- spec/completion_spec.lua | 4 ++-- spec/comptest | 2 +- src/argparse.lua | 14 ++++++++++++-- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/spec/completion_spec.lua b/spec/completion_spec.lua index 89c60f2..42f5103 100644 --- a/spec/completion_spec.lua +++ b/spec/completion_spec.lua @@ -96,7 +96,7 @@ _comptest() { {-h,--help}"[Show this help message and exit]" \ "--completion[Output a shell completion script for the specified shell]: :(bash zsh fish)" \ "*"{-v,--verbose}"[Set the verbosity level]" \ - {-f,--files}"[A description with illegal \' characters]:*: :_files" \ + {-f,--files}"[A description with illegal \"' characters]:*: :_files" \ ": :_comptest_cmds" \ "*:: :->args" \ && return 0 @@ -193,7 +193,7 @@ complete -c comptest -n '__fish_use_subcommand' -xa 'admin' -d 'Rock server admi complete -c comptest -s h -l help -d 'Show this help message and exit' complete -c comptest -l completion -xa 'bash zsh fish' -d 'Output a shell completion script for the specified shell' complete -c comptest -s v -l verbose -d 'Set the verbosity level' -complete -c comptest -s f -l files -r -d 'A description with illegal \\\' characters' +complete -c comptest -s f -l files -r -d 'A description with illegal "\' characters' complete -c comptest -n '__fish_seen_subcommand_from help' -xa 'help completion install i admin' complete -c comptest -n '__fish_seen_subcommand_from help' -s h -l help -d 'Show this help message and exit' diff --git a/spec/comptest b/spec/comptest index e597de9..948bf33 100755 --- a/spec/comptest +++ b/spec/comptest @@ -12,7 +12,7 @@ parser:flag "-v --verbose" :count "*" parser:option "-f --files" - :description "A description with illegal \\' characters." + :description "A description with illegal \"' characters." :args "+" local install = parser:command "install i" diff --git a/src/argparse.lua b/src/argparse.lua index e45a6c8..de333dc 100644 --- a/src/argparse.lua +++ b/src/argparse.lua @@ -1110,6 +1110,15 @@ function Parser:_is_shell_safe() end end end + for _, argument in ipairs(self._arguments) do + if argument._choices then + for _, choice in ipairs(argument._choices) do + if choice:find("[%s'\"]") then + return false + end + end + end + end for _, command in ipairs(self._commands) do if not command:_is_shell_safe() then return false @@ -1314,7 +1323,8 @@ function Parser:_zsh_arguments(buf, cmd_name, indent) table.insert(line, option._name) end if option._description then - table.insert(line, "[" .. get_short_description(option) .. "]") + local description = get_short_description(option):gsub('["%]:]', "\\%0") + table.insert(line, "[" .. description .. "]") end if option._maxargs == math.huge then table.insert(line, ":*") @@ -1369,7 +1379,7 @@ function Parser:_zsh_cmds(buf, cmd_name) table.insert(line, '"' .. command._name) end if command._description then - table.insert(line, ":" .. get_short_description(command)) + table.insert(line, ":" .. get_short_description(command):gsub('["]', "\\%0")) end table.insert(buf, " " .. table.concat(line) .. '"') end From 9055452e413601cdc50e90bb3513f0e91ecc4655 Mon Sep 17 00:00:00 2001 From: Paul Ouellette Date: Wed, 24 Jul 2019 11:06:58 -0400 Subject: [PATCH 35/37] Zsh completions: use #compdef This means zsh scripts can't be sourced, but they can be automatically loaded from files which is the standard way of doing it. --- spec/completion_spec.lua | 4 +++- src/argparse.lua | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/spec/completion_spec.lua b/spec/completion_spec.lua index 42f5103..6812cd2 100644 --- a/spec/completion_spec.lua +++ b/spec/completion_spec.lua @@ -86,7 +86,7 @@ complete -F _comptest -o bashdefault -o default comptest it("generates correct zsh completion script", function() assert.equal([=[ -compdef _comptest comptest +#compdef comptest _comptest() { local context state state_descr line @@ -179,6 +179,8 @@ _comptest_admin_cmds() { ) _describe "command" commands } + +_comptest ]=], get_output("completion zsh")) end) diff --git a/src/argparse.lua b/src/argparse.lua index de333dc..d105d43 100644 --- a/src/argparse.lua +++ b/src/argparse.lua @@ -1409,7 +1409,7 @@ end function Parser:get_zsh_complete() self._basename = base_name(self._name) assert(self:_is_shell_safe()) - local buf = {("compdef _%s %s\n"):format(self._basename, self._basename)} + local buf = {("#compdef %s\n"):format(self._basename)} local cmds_buf = {} table.insert(buf, "_" .. self._basename .. "() {") if #self._commands > 0 then @@ -1425,7 +1425,7 @@ function Parser:get_zsh_complete() if #cmds_buf > 0 then result = result .. "\n" .. table.concat(cmds_buf, "\n") end - return result .. "\n" + return result .. "\n\n_" .. self._basename .. "\n" end local function fish_escape(string) From f598e21376654458c79141f987494996c0027f5e Mon Sep 17 00:00:00 2001 From: Paul Ouellette Date: Wed, 24 Jul 2019 11:29:30 -0400 Subject: [PATCH 36/37] Add documentation for completions --- docsrc/completions.rst | 74 ++++++++++++++++++++++++++++++++++++++++++ docsrc/index.rst | 1 + 2 files changed, 75 insertions(+) create mode 100644 docsrc/completions.rst diff --git a/docsrc/completions.rst b/docsrc/completions.rst new file mode 100644 index 0000000..9208844 --- /dev/null +++ b/docsrc/completions.rst @@ -0,0 +1,74 @@ +Shell completions +================= + +Argparse supports generating shell completion scripts for Bash, Zsh, and Fish. +The completion scripts support completing options, commands, and argument +choices. + +The Parser methods ``:get_bash_complete()``, ``get_zsh_complete()``, and +``:get_fish_complete()`` return completion scripts as a string. + +Adding a completion option or command +------------------------------------- + +A ``--completion`` option can be added to the parser using the +``:add_complete([value])`` method. The optional ``value`` argument is a string +or table used to configure the option. + +.. code-block:: lua + :linenos: + + local parser = argparse() + :add_complete() + +.. code-block:: none + + $ lua script.lua -h + +.. code-block:: none + + Usage: script.lua [-h] [--completion {bash,zsh,fish}] + + Options: + -h, --help Show this help message and exit. + --completion {bash,zsh,fish} + Output a shell completion script for the specified shell. + +A similar ``completion`` command can be added to the parser using the +``:add_complete_command([value])`` method. + +Activating completions +---------------------- + +Bash +^^^^ + +Save the generated completion script at +``~/.local/share/bash-completion/completions/script.lua`` or add the following +line to the ``~/.bashrc``: + +.. code-block:: bash + + source <(script.lua --completion bash) + +Zsh +^^^ + +The completion script should be placed in a directory in the ``$fpath``. A new +directory can be added to to the ``$fpath`` by adding e.g. +``fpath=(~/.zfunc $fpath)`` in the ``~/.zshrc`` before ``compinit``. Save the +completion script with: + +.. code-block:: none + + $ script.lua --completion zsh > ~/.zfunc/_script.lua + +Fish +^^^^ + +Save the completion script at ``~/.config/fish/completions/script.lua.fish`` or +add the following line to the file ``~/.config/fish/config.fish``: + +.. code-block:: fish + + script.lua --completion fish | source diff --git a/docsrc/index.rst b/docsrc/index.rst index 0e8c6c0..e4d0de1 100644 --- a/docsrc/index.rst +++ b/docsrc/index.rst @@ -13,6 +13,7 @@ Contents: defaults callbacks messages + completions misc This is a tutorial for `argparse `_, a feature-rich command line parser for Lua. From c9f8f90a601e6119739f736c4b8a0c201aae1e90 Mon Sep 17 00:00:00 2001 From: Paul Ouellette Date: Thu, 25 Jul 2019 11:07:39 -0400 Subject: [PATCH 37/37] Completions: improve documentation --- docsrc/completions.rst | 40 ++++++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/docsrc/completions.rst b/docsrc/completions.rst index 9208844..18574b7 100644 --- a/docsrc/completions.rst +++ b/docsrc/completions.rst @@ -1,7 +1,9 @@ Shell completions ================= -Argparse supports generating shell completion scripts for Bash, Zsh, and Fish. +Argparse can generate shell completion scripts for +`Bash `_, `Zsh `_, and +`Fish `_. The completion scripts support completing options, commands, and argument choices. @@ -11,9 +13,9 @@ The Parser methods ``:get_bash_complete()``, ``get_zsh_complete()``, and Adding a completion option or command ------------------------------------- -A ``--completion`` option can be added to the parser using the +A ``--completion`` option can be added to a parser using the ``:add_complete([value])`` method. The optional ``value`` argument is a string -or table used to configure the option. +or table used to configure the option (by calling the option with ``value``). .. code-block:: lua :linenos: @@ -34,18 +36,20 @@ or table used to configure the option. --completion {bash,zsh,fish} Output a shell completion script for the specified shell. -A similar ``completion`` command can be added to the parser using the +A similar ``completion`` command can be added to a parser using the ``:add_complete_command([value])`` method. -Activating completions ----------------------- +Using completions +----------------- Bash ^^^^ Save the generated completion script at -``~/.local/share/bash-completion/completions/script.lua`` or add the following -line to the ``~/.bashrc``: +``/usr/share/bash-completion/completions/script.lua`` or +``~/.local/share/bash-completion/completions/script.lua``. + +Alternatively, add the following line to the ``~/.bashrc``: .. code-block:: bash @@ -54,20 +58,20 @@ line to the ``~/.bashrc``: Zsh ^^^ -The completion script should be placed in a directory in the ``$fpath``. A new -directory can be added to to the ``$fpath`` by adding e.g. -``fpath=(~/.zfunc $fpath)`` in the ``~/.zshrc`` before ``compinit``. Save the -completion script with: - -.. code-block:: none - - $ script.lua --completion zsh > ~/.zfunc/_script.lua +Save the completion script in the ``/usr/share/zsh/site-functions/`` directory +or any directory in the ``$fpath``. The file name should be an underscore +followed by the program name. A new directory can be added to to the ``$fpath`` +by adding e.g. ``fpath=(~/.zfunc $fpath)`` in the ``~/.zshrc`` before +``compinit``. Fish ^^^^ -Save the completion script at ``~/.config/fish/completions/script.lua.fish`` or -add the following line to the file ``~/.config/fish/config.fish``: +Save the completion script at +``/usr/share/fish/vendor_completions.d/script.lua.fish`` or +``~/.config/fish/completions/script.lua.fish``. + +Alternatively, add the following line to the file ``~/.config/fish/config.fish``: .. code-block:: fish