Skip to content

Instantly share code, notes, and snippets.

@stecman
Created June 21, 2016 01:42
Show Gist options
  • Save stecman/fc8c44e3dd657cd845aebc9666e8877d to your computer and use it in GitHub Desktop.
Save stecman/fc8c44e3dd657cd845aebc9666e8877d to your computer and use it in GitHub Desktop.
symfony-console-completion string and escaping parsing
diff --git a/src/CompletionContext.php b/src/CompletionContext.php
index 6029253..4316801 100644
--- a/src/CompletionContext.php
+++ b/src/CompletionContext.php
@@ -198,20 +198,13 @@ class CompletionContext
$this->wordIndex = null;
$cursor = 0;
- $breaks = preg_quote($this->wordBreaks);
+ $matches = $this->splitOnBreaks($this->commandLine, $this->wordBreaks);
- if (!preg_match_all("/([^$breaks]*)([$breaks]*)/", $this->commandLine, $matches)) {
- return;
- }
-
- // Groups:
- // 1: Word
- // 2: Break characters
- foreach ($matches[0] as $index => $wholeMatch) {
+ foreach ($matches as $index => $match) {
// Determine which word the cursor is in
- $cursor += strlen($wholeMatch);
- $word = $matches[1][$index];
- $breaks = $matches[2][$index];
+ $word = $match['word'];
+ $breaks = $match['break'];
+ $cursor += strlen($word) + strlen($breaks);
if ($this->wordIndex === null && $cursor >= $this->charIndex) {
$this->wordIndex = $index;
@@ -246,6 +239,96 @@ class CompletionContext
}
/**
+ * Split a string into words on break characters, accounting for strings and escaped spaces
+ *
+ * Returns a collection of ['word' => string, 'break' => string] groups for each word,
+ * where "break" is the non-word characters between this word and then following one.
+ *
+ * @param string $string
+ * @param string $breaks
+ * @param string[] $wordGroupers
+ * @return array
+ */
+ protected function splitOnBreaks($string, $breaks, $wordGroupers = ['\'', '"'])
+ {
+ $matches = [];
+
+ $word = '';
+ $break = '';
+
+ $currentGroup = null;
+ $escaping = false;
+ $inBreakSpace = false;
+
+ for ($pos = 0, $length = strlen($string); $pos < $length; $pos++) {
+ $char = $string[$pos];
+ $treatAsWord = false;
+
+ // If we're escaping, we don't care what this character is - add it to the word
+ if ($escaping) {
+ $escaping = false;
+ $treatAsWord = true;
+ }
+
+ // If a backslash is encountered, ignore any special function of the following character
+ else if ($char === '\\' && !$escaping) {
+ $escaping = true;
+ $treatAsWord = true;
+ }
+
+ // Check for the start or end of a group
+ // We can only be inside one group at a time
+ else {
+ foreach ($wordGroupers as $groupChar) {
+ if ($char === $groupChar) {
+ // If we encouter the character that opened the current group, close the group
+ if ($currentGroup === $groupChar) {
+ $currentGroup = null;
+ break;
+ }
+
+ // If we're not in a group yet, use this character as the group
+ if (!$currentGroup) {
+ $currentGroup = $groupChar;
+ break;
+ }
+ }
+ }
+ }
+
+ // If this is not a break character, add it to the word, otherwise add to the break space
+ // Being inside a group counts break characters as word characters
+ if ($treatAsWord || $currentGroup !== null || strpos($breaks, $char) === false) {
+
+ // If the previous character was a break, this is a new word
+ if ($inBreakSpace) {
+ $matches[] = [
+ 'word' => $word,
+ 'break' => $break
+ ];
+
+ $word = '';
+ $break = '';
+ $inBreakSpace = false;
+ }
+
+ $word .= $char;
+ } else {
+ $inBreakSpace = true;
+ $break .= $char;
+ }
+ }
+
+ // Append the last word
+ $matches[] = [
+ 'word' => $word,
+ 'break' => $break
+ ];
+
+ return $matches;
+ }
+
+ /**
* Reset the computed words so that $this->splitWords is forced to run again
*/
protected function reset()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment