From 2488ee1f5e0b7038677f448268f7531727f6061b Mon Sep 17 00:00:00 2001 From: Stephen Holdaway Date: Tue, 13 Feb 2018 01:55:12 +1300 Subject: [PATCH 1/5] Rewrite command-line splitting to tokenize quoted strings This replaces the previous simple "split on breaks" regex with a function that uses a more complete regex and returns tokens shapes. This vastly improves the readability of CompletionContext::splitCommand() Partially resolves #67 (this doesn't support output of quoted strings yet) --- src/CompletionContext.php | 158 ++++++++++++++---- .../BashCompletion/CompletionContextTest.php | 38 +++++ .../BashCompletion/CompletionHandlerTest.php | 15 ++ 3 files changed, 174 insertions(+), 37 deletions(-) diff --git a/src/CompletionContext.php b/src/CompletionContext.php index 6029253..edcace5 100644 --- a/src/CompletionContext.php +++ b/src/CompletionContext.php @@ -61,7 +61,7 @@ class CompletionContext * * @var string */ - protected $wordBreaks = "'\"()= \t\n"; + protected $wordBreaks = "= \t\n"; /** * Set the whole contents of the command line as a string @@ -178,12 +178,15 @@ public function setCharIndex($index) * This defaults to a sane value based on BASH's word break characters and shouldn't * need to be changed unless your completions contain the default word break characters. * + * @deprecated This is becoming an internal setting that doesn't make sense to expose publicly. + * * @see wordBreaks * @param string $charList - a single string containing all of the characters to break words on */ public function setWordBreaks($charList) { - $this->wordBreaks = $charList; + // Drop quotes from break characters - strings are handled separately to word breaks now + $this->wordBreaks = str_replace(array('"', '\''), '', $charList);; $this->reset(); } @@ -194,55 +197,136 @@ public function setWordBreaks($charList) */ protected function splitCommand() { - $this->words = array(); - $this->wordIndex = null; - $cursor = 0; - - $breaks = preg_quote($this->wordBreaks); - - if (!preg_match_all("/([^$breaks]*)([$breaks]*)/", $this->commandLine, $matches)) { - return; - } - - // Groups: - // 1: Word - // 2: Break characters - foreach ($matches[0] as $index => $wholeMatch) { - // Determine which word the cursor is in - $cursor += strlen($wholeMatch); - $word = $matches[1][$index]; - $breaks = $matches[2][$index]; - - if ($this->wordIndex === null && $cursor >= $this->charIndex) { - $this->wordIndex = $index; + $tokens = $this->tokenizeString($this->commandLine); - // Find the user's cursor position relative to the end of this word - // The end of the word is the internal cursor minus any break characters that were captured - $cursorWordOffset = $this->charIndex - ($cursor - strlen($breaks)); + foreach ($tokens as $token) { + if ($token['type'] != 'break') { + $this->words[] = $this->getTokenValue($token); + } - if ($cursorWordOffset < 0) { - // Cursor is inside the word - truncate the word at the cursor - // (This emulates normal BASH completion behaviour I've observed, though I'm not entirely sure if it's useful) - $word = substr($word, 0, strlen($word) + $cursorWordOffset); + // Determine which word index the cursor is inside once we reach it's offset + if ($this->wordIndex === null && $this->charIndex <= $token['offsetEnd']) { + $this->wordIndex = count($this->words) - 1; - } elseif ($cursorWordOffset > 0) { + if ($token['type'] == 'break') { // Cursor is in the break-space after a word // Push an empty word at the cursor to allow completion of new terms at the cursor, ignoring words ahead $this->wordIndex++; - $this->words[] = $word; $this->words[] = ''; continue; } - } - if ($word !== '') { - $this->words[] = $word; + if ($this->charIndex < $token['offsetEnd']) { + // Cursor is inside the current word - truncate the word at the cursor + // (This emulates normal BASH completion behaviour I've observed, though I'm not entirely sure if it's useful) + $relativeOffset = $this->charIndex - $token['offset']; + $truncated = substr($token['value'], 0, $relativeOffset); + + $this->words[$this->wordIndex] = $truncated; + } } } - if ($this->wordIndex > count($this->words) - 1) { - $this->wordIndex = count($this->words) - 1; + // Cursor position is past the end of the command line string - consider it a new word + if ($this->wordIndex === null) { + $this->wordIndex = count($this->words); + $this->words[] = ''; + } + } + + /** + * Return a token's value with escaping and quotes removed + * + * @see self::tokenizeString() + * @param array $token + * @return string + */ + protected function getTokenValue($token) + { + $value = $token['value']; + + // Remove outer quote characters (or first quote if unclosed) + if ($token['type'] == 'quoted') { + $value = preg_replace('/^(?:[\'"])(.*?)(?:[\'"])?$/', '$1', $value); } + + // Remove escape characters + $value = preg_replace('/\\\\(.)/', '$1', $value); + + return $value; + } + + /** + * Break a string into words, quoted strings and non-words (breaks) + * + * Returns an array of unmodified segments of $string with offset and type information. + * + * @param string $string + * @return array as [ [type => string, value => string, offset => int], ... ] + */ + protected function tokenizeString($string) + { + // Map capture groups to returned token type + $typeMap = array( + 'double_quote_string' => 'quoted', + 'single_quote_string' => 'quoted', + 'word' => 'word', + 'break' => 'break', + ); + + // Escape every word break character including whitespace + // preg_quote won't work here as it doesn't understand the ignore whitespace flag ("x") + $breaks = preg_replace('/(.)/', '\\\$1', $this->wordBreaks); + + $pattern = <<<"REGEX" + /(?: + (?P + "(\\\\.|[^\"\\\\])*(?:"|$) + ) | + (?P + '(\\\\.|[^'\\\\])*(?:'|$) + ) | + (?P + (?:\\\\.|[^$breaks])+ + ) | + (?P + [$breaks]+ + ) + )/x +REGEX; + + $tokens = array(); + + if (!preg_match_all($pattern, $string, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) { + return $tokens; + } + + foreach ($matches as $set) { + foreach ($set as $groupName => $match) { + + // Ignore integer indices preg_match outputs (duplicates of named groups) + if (is_integer($groupName)) { + continue; + } + + // Skip if the offset indicates this group didn't match + if ($match[1] === -1) { + continue; + } + + $tokens[] = array( + 'type' => $typeMap[$groupName], + 'value' => $match[0], + 'offset' => $match[1], + 'offsetEnd' => $match[1] + strlen($match[0]) + ); + + // Move to the next set (only one group should match per set) + continue; + } + } + + return $tokens; } /** diff --git a/tests/Stecman/Component/Symfony/Console/BashCompletion/CompletionContextTest.php b/tests/Stecman/Component/Symfony/Console/BashCompletion/CompletionContextTest.php index 9374d13..816b26c 100644 --- a/tests/Stecman/Component/Symfony/Console/BashCompletion/CompletionContextTest.php +++ b/tests/Stecman/Component/Symfony/Console/BashCompletion/CompletionContextTest.php @@ -92,6 +92,44 @@ public function testWordBreakingWithSmallInputs() $this->assertEquals('', $context->getCurrentWord()); } + public function testQuotedStringWordBreaking() + { + $context = new CompletionContext(); + $context->setCharIndex(1000); + $context->setCommandLine('make horse --legs=3 --name="Jeff the horse" --colour Extreme\ Blanc \'foo " bar\''); + + // Ensure spaces and quotes + $this->assertEquals( + array( + 'make', + 'horse', + '--legs', + '3', + '--name', + 'Jeff the horse', + '--colour', + 'Extreme Blanc', + 'foo " bar', + '', + ), + $context->getWords() + ); + + $context = new CompletionContext(); + $context->setCommandLine('console --tag='); + + // Cursor after equals symbol on option argument + $context->setCharIndex(14); + $this->assertEquals( + array( + 'console', + '--tag', + '' + ), + $context->getWords() + ); + } + public function testConfigureFromEnvironment() { putenv("CMDLINE_CONTENTS=beam up li"); diff --git a/tests/Stecman/Component/Symfony/Console/BashCompletion/CompletionHandlerTest.php b/tests/Stecman/Component/Symfony/Console/BashCompletion/CompletionHandlerTest.php index f93c1aa..9ae1599 100644 --- a/tests/Stecman/Component/Symfony/Console/BashCompletion/CompletionHandlerTest.php +++ b/tests/Stecman/Component/Symfony/Console/BashCompletion/CompletionHandlerTest.php @@ -80,6 +80,21 @@ public function testCompleteOptionFull() $this->assertArraySubset(array('--jazz-hands'), $this->getTerms($handler->runCompletion())); } + public function testCompleteOptionEqualsValue() + { + // Cursor at the "=" sign + $handler = $this->createHandler('app completion-aware --option-with-suggestions='); + $this->assertEquals(array('one-opt', 'two-opt'), $this->getTerms($handler->runCompletion())); + + // Cursor at an opening quote + $handler = $this->createHandler('app completion-aware --option-with-suggestions="'); + $this->assertEquals(array('one-opt', 'two-opt'), $this->getTerms($handler->runCompletion())); + + // Cursor inside a quote with value + $handler = $this->createHandler('app completion-aware --option-with-suggestions="two'); + $this->assertEquals(array('two-opt'), $this->getTerms($handler->runCompletion())); + } + public function testCompleteOptionOrder() { // Completion of options should be able to happen anywhere after the command name From ab585cf6b7352945501a417d91cc163c9e163168 Mon Sep 17 00:00:00 2001 From: Stephen Holdaway Date: Mon, 7 Jan 2019 16:02:28 +1300 Subject: [PATCH 2/5] Make BASH hook compatible with spaces in completion results ZSH handles multi-word completion results correctly out of the box, but BASH needed some prodding to stop it interpreting all space-delimited words as separate suggestions. There are two parts to this fix: - The addition of a local `IFS` variable was required to prevent splitting from happening inside multi-word completions. - Spaces needed to be manually escaped when quotes were missing as completing as completion was otherwise dumping the raw result without escaping. I haven't looked into how filename completion mode handles this without showing backslashes in the completion result, but the behaviour here mirrors ZSH so this isn't a huge problem. Custom multi-word completions in BASH aren't particularly intuitive - these threads were helpful while figuring this solution out: https://stackoverflow.com/questions/26509260/bash-tab-completion-with-spaces https://stackoverflow.com/questions/1146098/properly-handling-spaces-and-quotes-in-bash-completion https://stackoverflow.com/questions/10652492/bash-autocompletion-how-to-pass-this-array-to-compgen-without-significant-whit --- src/HookFactory.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/HookFactory.php b/src/HookFactory.php index 19601e8..ce04032 100644 --- a/src/HookFactory.php +++ b/src/HookFactory.php @@ -41,6 +41,9 @@ function %%function_name%% { local RESULT STATUS; + # Force splitting by newline instead of default delimiters + local IFS=$'\n' + RESULT="$(%%completion_command%% Date: Sun, 20 Jan 2019 14:18:44 +1300 Subject: [PATCH 3/5] Escape completion results for BASH before writing to stdout In the previous commit I used `printf '%q'` in the BASH hook to handle escaping unquoted strings, however this didn't handle escaping quote characters inside strings where needed. This commit moves the responsibility of escaping completion results into PHP to make the escaping logic testable and easier to maintain. BASH supports automatic escaping/quoting in completion results only when the `-o filenames` option is used to register a completion function. The `filenames` option has behaviour that is incompatible with a generic completion handler, such as appending a `/` on results that happen to match directory name in the CWD. Unfortuntately this means we cannot use this functionality. There is no API to change this behaviour as far as I've been able to see from the BASH docs and source code. ZSH handles escaping of completion functions perfectly fine out of the box. --- src/CompletionCommand.php | 49 ++++++++++++++- src/CompletionContext.php | 62 +++++++++++++++++-- src/HookFactory.php | 8 +-- .../BashCompletion/CompletionContextTest.php | 33 +++++++++- 4 files changed, 138 insertions(+), 14 deletions(-) diff --git a/src/CompletionCommand.php b/src/CompletionCommand.php index 0a4a809..a462820 100644 --- a/src/CompletionCommand.php +++ b/src/CompletionCommand.php @@ -76,7 +76,54 @@ protected function execute(InputInterface $input, OutputInterface $output) $output->write($hook, true); } else { $handler->setContext(new EnvironmentCompletionContext()); - $output->write($this->runCompletion(), true); + + // Get completion results + $results = $this->runCompletion(); + + // Escape results for the current shell + $shellType = $input->getOption('shell-type') ?: $this->getShellType(); + + foreach ($results as &$result) { + $result = $this->escapeForShell($result, $shellType); + } + + $output->write($results, true); + } + } + + /** + * Escape each completion result for the specified shell + * + * @param string $result - Completion results that should appear in the shell + * @param string $shellType - Valid shell type from HookFactory + * @return string + */ + protected function escapeForShell($result, $shellType) + { + switch ($shellType) { + // BASH requires special escaping for multi-word and special character results + // This emulates registering completion with`-o filenames`, without side-effects like dir name slashes + case 'bash': + $context = $this->handler->getContext(); + $wordStart = substr($context->getRawCurrentWord(), 0, 1); + + if ($wordStart == "'") { + // If the current word is single-quoted, escape any single quotes in the result + $result = str_replace("'", "\\'", $result); + } else if ($wordStart == '"') { + // If the current word is double-quoted, escape any double quotes in the result + $result = str_replace('"', '\\"', $result); + } else { + // Otherwise assume the string is unquoted and word breaks should be escaped + $result = preg_replace('/([\s\'"\\\\])/', '\\\\$1', $result); + } + + // Escape output to prevent special characters being lost when passing results to compgen + return escapeshellarg($result); + + // No transformation by default + default: + return $result; } } diff --git a/src/CompletionContext.php b/src/CompletionContext.php index edcace5..f09ab9b 100644 --- a/src/CompletionContext.php +++ b/src/CompletionContext.php @@ -32,17 +32,27 @@ class CompletionContext protected $charIndex = 0; /** - * An array containing the individual words in the current command line. + * An array of the individual words in the current command line. * * This is not set until $this->splitCommand() is called, when it is populated by * $commandLine exploded by $wordBreaks * * Bash equivalent: COMP_WORDS * - * @var array|null + * @var string[]|null */ protected $words = null; + /** + * Words from the currently command-line before quotes and escaping is processed + * + * This is indexed the same as $this->words, but in their raw input terms are in their input form, including + * quotes and escaping. + * + * @var string[]|null + */ + protected $rawWords = null; + /** * The index in $this->words containing the word at the current cursor position. * @@ -101,6 +111,22 @@ public function getCurrentWord() return ''; } + /** + * Return the unprocessed string for the word under the cursor + * + * This preserves any quotes and escaping that are present in the input command line. + * + * @return string + */ + public function getRawCurrentWord() + { + if (isset($this->rawWords[$this->wordIndex])) { + return $this->rawWords[$this->wordIndex]; + } + + return ''; + } + /** * Return a word by index from the command line * @@ -132,6 +158,22 @@ public function getWords() return $this->words; } + /** + * Get the unprocessed/literal words from the command line + * + * This is indexed the same as getWords(), but preserves any quoting and escaping from the command line + * + * @return string[] + */ + public function getRawWords() + { + if ($this->rawWords === null) { + $this->splitCommand(); + } + + return $this->rawWords; + } + /** * Get the index of the word the cursor is currently in * @@ -202,6 +244,7 @@ protected function splitCommand() foreach ($tokens as $token) { if ($token['type'] != 'break') { $this->words[] = $this->getTokenValue($token); + $this->rawWords[] = $token['value']; } // Determine which word index the cursor is inside once we reach it's offset @@ -213,16 +256,22 @@ protected function splitCommand() // Push an empty word at the cursor to allow completion of new terms at the cursor, ignoring words ahead $this->wordIndex++; $this->words[] = ''; + $this->rawWords[] = ''; continue; } if ($this->charIndex < $token['offsetEnd']) { - // Cursor is inside the current word - truncate the word at the cursor - // (This emulates normal BASH completion behaviour I've observed, though I'm not entirely sure if it's useful) + // Cursor is inside the current word - truncate the word at the cursor to complete on + // This emulates BASH completion's behaviour with COMP_CWORD + + // Create a copy of the token with its value truncated + $truncatedToken = $token; $relativeOffset = $this->charIndex - $token['offset']; - $truncated = substr($token['value'], 0, $relativeOffset); + $truncatedToken['value'] = substr($token['value'], 0, $relativeOffset); - $this->words[$this->wordIndex] = $truncated; + // Replace the current word with the truncated value + $this->words[$this->wordIndex] = $this->getTokenValue($truncatedToken); + $this->rawWords[$this->wordIndex] = $truncatedToken['value']; } } } @@ -231,6 +280,7 @@ protected function splitCommand() if ($this->wordIndex === null) { $this->wordIndex = count($this->words); $this->words[] = ''; + $this->rawWords[] = ''; } } diff --git a/src/HookFactory.php b/src/HookFactory.php index ce04032..277a68d 100644 --- a/src/HookFactory.php +++ b/src/HookFactory.php @@ -68,11 +68,6 @@ function %%function_name%% { COMPREPLY=(`compgen -W "$RESULT" -- $cur`); - # Escape any spaces in results if the current word doesn't begin with a quote - if [[ ! -z $COMPREPLY ]] && [[ ! $cur =~ ^[\'\"] ]]; then - COMPREPLY=($(printf '%q\n' "${COMPREPLY[@]}")); - fi; - __ltrim_colon_completions "$cur"; MAILCHECK=mail_check_backup; @@ -156,6 +151,9 @@ public function generateHook($type, $programPath, $programName = null, $multiple $completionCommand = $programPath . ' _completion'; } + // Pass shell type during completion so output can be encoded if the shell requires it + $completionCommand .= " --shell-type $type"; + return str_replace( array( '%%function_name%%', diff --git a/tests/Stecman/Component/Symfony/Console/BashCompletion/CompletionContextTest.php b/tests/Stecman/Component/Symfony/Console/BashCompletion/CompletionContextTest.php index 816b26c..30c0667 100644 --- a/tests/Stecman/Component/Symfony/Console/BashCompletion/CompletionContextTest.php +++ b/tests/Stecman/Component/Symfony/Console/BashCompletion/CompletionContextTest.php @@ -96,9 +96,9 @@ public function testQuotedStringWordBreaking() { $context = new CompletionContext(); $context->setCharIndex(1000); - $context->setCommandLine('make horse --legs=3 --name="Jeff the horse" --colour Extreme\ Blanc \'foo " bar\''); + $context->setCommandLine('make horse --legs=3 --name="Jeff the horse" --colour Extreme\\ Blanc \'foo " bar\''); - // Ensure spaces and quotes + // Ensure spaces and quotes are processed correctly $this->assertEquals( array( 'make', @@ -115,6 +115,23 @@ public function testQuotedStringWordBreaking() $context->getWords() ); + // Confirm the raw versions of the words are indexed correctly + $this->assertEquals( + array( + 'make', + 'horse', + '--legs', + '3', + '--name', + '"Jeff the horse"', + '--colour', + 'Extreme\\ Blanc', + "'foo \" bar'", + '', + ), + $context->getRawWords() + ); + $context = new CompletionContext(); $context->setCommandLine('console --tag='); @@ -130,6 +147,18 @@ public function testQuotedStringWordBreaking() ); } + public function testGetRawCurrentWord() + { + $context = new CompletionContext(); + + $context->setCommandLine('cmd "double quoted" --option \'value\''); + $context->setCharIndex(13); + $this->assertEquals(1, $context->getWordIndex()); + + $this->assertEquals(array('cmd', '"double q', '--option', "'value'"), $context->getRawWords()); + $this->assertEquals('"double q', $context->getRawCurrentWord()); + } + public function testConfigureFromEnvironment() { putenv("CMDLINE_CONTENTS=beam up li"); From 0240a08b74c96338eb71849b66c897f870d3cf8d Mon Sep 17 00:00:00 2001 From: Stephen Holdaway Date: Sun, 20 Jan 2019 15:38:00 +1300 Subject: [PATCH 4/5] Fix and test for missing semicolons in shell hooks A missing semicolon on the line `local IFS=` was breaking the hook after it was collapsed to one line for eval. This fixes all missing semicolons and adds a test to catch this in future. --- src/HookFactory.php | 22 ++++---- .../BashCompletion/HookFactoryTest.php | 53 +++++++++++++++++++ 2 files changed, 64 insertions(+), 11 deletions(-) diff --git a/src/HookFactory.php b/src/HookFactory.php index 277a68d..8262629 100644 --- a/src/HookFactory.php +++ b/src/HookFactory.php @@ -33,16 +33,16 @@ function %%function_name%% { # Copy BASH's completion variables to the ones the completion command expects # These line up exactly as the library was originally designed for BASH - local CMDLINE_CONTENTS="$COMP_LINE" - local CMDLINE_CURSOR_INDEX="$COMP_POINT" + local CMDLINE_CONTENTS="$COMP_LINE"; + local CMDLINE_CURSOR_INDEX="$COMP_POINT"; local CMDLINE_WORDBREAKS="$COMP_WORDBREAKS"; - export CMDLINE_CONTENTS CMDLINE_CURSOR_INDEX CMDLINE_WORDBREAKS + export CMDLINE_CONTENTS CMDLINE_CURSOR_INDEX CMDLINE_WORDBREAKS; local RESULT STATUS; # Force splitting by newline instead of default delimiters - local IFS=$'\n' + local IFS=$'\n'; RESULT="$(%%completion_command%% &2 echo "Completion was not registered for %%program_name%%:"; >&2 echo "The 'bash-completion' package is required but doesn't appear to be installed."; -fi +fi; END // ZSH Hook , 'zsh' => <<<'END' # ZSH completion for %%program_path%% function %%function_name%% { - local -x CMDLINE_CONTENTS="$words" - local -x CMDLINE_CURSOR_INDEX - (( CMDLINE_CURSOR_INDEX = ${#${(j. .)words[1,CURRENT]}} )) + local -x CMDLINE_CONTENTS="$words"; + local -x CMDLINE_CURSOR_INDEX; + (( CMDLINE_CURSOR_INDEX = ${#${(j. .)words[1,CURRENT]}} )); - local RESULT STATUS - RESULT=("${(@f)$( %%completion_command%% )}") + local RESULT STATUS; + RESULT=("${(@f)$( %%completion_command%% )}"); STATUS=$?; # Check if shell provided path completion is requested @@ -105,7 +105,7 @@ function %%function_name%% { return $?; fi; - compadd -- $RESULT + compadd -- $RESULT; }; compdef %%function_name%% "%%program_name%%"; diff --git a/tests/Stecman/Component/Symfony/Console/BashCompletion/HookFactoryTest.php b/tests/Stecman/Component/Symfony/Console/BashCompletion/HookFactoryTest.php index c253a62..13559ec 100644 --- a/tests/Stecman/Component/Symfony/Console/BashCompletion/HookFactoryTest.php +++ b/tests/Stecman/Component/Symfony/Console/BashCompletion/HookFactoryTest.php @@ -54,6 +54,59 @@ public function generateHookDataProvider() ); } + public function testForMissingSemiColons() + { + $class = new \ReflectionClass('Stecman\Component\Symfony\Console\BashCompletion\HookFactory'); + $properties = $class->getStaticProperties(); + $hooks = $properties['hooks']; + + // Check each line is commented or closed correctly to be collapsed for eval + foreach ($hooks as $shellType => $hook) { + $line = strtok($hook, "\n"); + $lineNumber = 0; + + while ($line !== false) { + $lineNumber++; + + if (!$this->isScriptLineValid($line)) { + $this->fail("$shellType hook appears to be missing a semicolon on line $lineNumber:\n> $line"); + } + + $line = strtok("\n"); + } + } + } + + /** + * Check if a line of shell script is safe to be collapsed to one line for eval + */ + protected function isScriptLineValid($line) + { + if (preg_match('/^\s*#/', $line)) { + // Line is commented out + return true; + } + + if (preg_match('/[;\{\}]\s*$/', $line)) { + // Line correctly ends with a semicolon or syntax + return true; + } + + if (preg_match(' + /( + ;\s*then | + \s*else + ) + \s*$ + /x', $line) + ) { + // Line ends with another permitted sequence + return true; + } + + return false; + } + protected function hasProgram($programName) { exec(sprintf( From cbec6e8d6d922e3d8e049de84d7f1d55bc624eea Mon Sep 17 00:00:00 2001 From: Stephen Holdaway Date: Sun, 20 Jan 2019 15:46:25 +1300 Subject: [PATCH 5/5] Dynamically enable filename completion in BASH instead of calling _filedir `_filedir` is a function provided by the bash-completion project, not by BASH itself. I was seeing inconsistent completion results with multi-word/escaped results as the completer was returning filenames, but wasn't in `filenames` mode. I went to enable filename completion mode to get better completion behaviour for paths from `_filedir`, but it turns out the completion hook function can just be switched to BASH's default file/dir completion behaviour instead. --- src/HookFactory.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/HookFactory.php b/src/HookFactory.php index 8262629..d86f9b2 100644 --- a/src/HookFactory.php +++ b/src/HookFactory.php @@ -57,7 +57,8 @@ function %%function_name%% { # Check if shell provided path completion is requested # @see Completion\ShellPathCompletion if [ $STATUS -eq 200 ]; then - _filedir; + # Turn file/dir completion on temporarily and give control back to BASH + compopt -o default; return 0; # Bail out if PHP didn't exit cleanly