{"action":"created","comment":{"url":"https://api.github.com/repos/thoughtbot/gitsh/pulls/comments/71979307","id":71979307,"diff_hunk":"@@ -0,0 +1,83 @@\n+require 'gitsh/parser'\n+\n+module Gitsh\n+  class CompletionEscaper\n+    UNQUOTED_STRING_ESCAPABLES = Gitsh::Parser::UNQUOTED_STRING_ESCAPABLES\n+    QUOTED_STRING_ESCAPABLES = {\n+      '\"' => Gitsh::Parser::SOFT_STRING_ESCAPABLES,\n+      \"'\" => Gitsh::Parser::HARD_STRING_ESCAPABLES,\n+    }.freeze\n+\n+    def initialize(completer, options)\n+      @completer = completer\n+      @line_editor = options[:line_editor]\n+    end\n+\n+    def call(input)\n+      OptionEscaper.new(\n+        completer,\n+        input,\n+        line_editor.line_buffer,\n+      ).escaped_options\n+    end\n+\n+    private\n+\n+    attr_reader :completer, :line_editor\n+\n+    class OptionEscaper\n+      def initialize(completer, input, full_input)\n+        @completer = completer\n+        @input = input\n+        @full_input = full_input\n+      end\n+\n+      def escaped_options\n+        unescaped_options.map { |option| escape(option) }\n+      end\n+\n+      private\n+\n+      attr_reader :completer, :input, :full_input\n+\n+      def unescaped_options\n+        completer.call(unescape(input))\n+      end\n+\n+      def escape(option)\n+        if completing_quoted_argument?\n+          quote_char = input_before_current_argument[-1]\n+          escape_chars(option, QUOTED_STRING_ESCAPABLES[quote_char]).strip\n+        else\n+          escape_chars(option, UNQUOTED_STRING_ESCAPABLES)\n+        end\n+      end\n+\n+      def escape_chars(text, chars)\n+        quotable_characters = Regexp.escape(chars)\n+        text.gsub(/([#{quotable_characters}])(?!$)/) { |char| \"\\\\#{char}\" }\n+      end\n+\n+      def unescape(input)","path":"lib/gitsh/completion_escaper.rb","position":61,"original_position":61,"commit_id":"d87ef7fd31faf3290b0ae8e92e4d904b5aa01c68","original_commit_id":"d87ef7fd31faf3290b0ae8e92e4d904b5aa01c68","user":{"login":"georgebrock","id":35861,"avatar_url":"https://avatars.githubusercontent.com/u/35861?v=3","gravatar_id":"","url":"https://api.github.com/users/georgebrock","html_url":"https://github.com/georgebrock","followers_url":"https://api.github.com/users/georgebrock/followers","following_url":"https://api.github.com/users/georgebrock/following{/other_user}","gists_url":"https://api.github.com/users/georgebrock/gists{/gist_id}","starred_url":"https://api.github.com/users/georgebrock/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/georgebrock/subscriptions","organizations_url":"https://api.github.com/users/georgebrock/orgs","repos_url":"https://api.github.com/users/georgebrock/repos","events_url":"https://api.github.com/users/georgebrock/events{/privacy}","received_events_url":"https://api.github.com/users/georgebrock/received_events","type":"User","site_admin":false},"body":"This doesn't interpret escape sequences in the same way as the parser: we should only allow escape sequences for the characters listed in the relevant `*__STRING_ESCAPABLES` constant, any other `\\` should by taken as a literal.\r\n\r\nBasis of a test could be:\r\n\r\n<pre><code>!touch file.txt\r\nadd f\\i<kbd>tab</kbd>  # will complete to `add file.txt`, but shouldn't</code></pre>","created_at":"2016-07-23T22:53:01Z","updated_at":"2016-07-23T22:53:01Z","html_url":"https://github.com/thoughtbot/gitsh/pull/259#discussion_r71979307","pull_request_url":"https://api.github.com/repos/thoughtbot/gitsh/pulls/259","_links":{"self":{"href":"https://api.github.com/repos/thoughtbot/gitsh/pulls/comments/71979307"},"html":{"href":"https://github.com/thoughtbot/gitsh/pull/259#discussion_r71979307"},"pull_request":{"href":"https://api.github.com/repos/thoughtbot/gitsh/pulls/259"}}},"pull_request":{"url":"https://api.github.com/repos/thoughtbot/gitsh/pulls/259","id":78584493,"html_url":"https://github.com/thoughtbot/gitsh/pull/259","diff_url":"https://github.com/thoughtbot/gitsh/pull/259.diff","patch_url":"https://github.com/thoughtbot/gitsh/pull/259.patch","issue_url":"https://api.github.com/repos/thoughtbot/gitsh/issues/259","number":259,"state":"open","locked":false,"title":"Tab complete spaces (and other special characters)","user":{"login":"georgebrock","id":35861,"avatar_url":"https://avatars.githubusercontent.com/u/35861?v=3","gravatar_id":"","url":"https://api.github.com/users/georgebrock","html_url":"https://github.com/georgebrock","followers_url":"https://api.github.com/users/georgebrock/followers","following_url":"https://api.github.com/users/georgebrock/following{/other_user}","gists_url":"https://api.github.com/users/georgebrock/gists{/gist_id}","starred_url":"https://api.github.com/users/georgebrock/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/georgebrock/subscriptions","organizations_url":"https://api.github.com/users/georgebrock/orgs","repos_url":"https://api.github.com/users/georgebrock/repos","events_url":"https://api.github.com/users/georgebrock/events{/privacy}","received_events_url":"https://api.github.com/users/georgebrock/received_events","type":"User","site_admin":false},"body":"Allows users to tab-complete options that contain exotic characters like spaces, by:\r\n\r\n* Explicitly telling GNU Readline which characters constitute word boundaries in the gitsh language.\r\n* Using GNU Readline's `char_is_quoted_p` setting to inform Readline when a word boundary is escaped.\r\n* Expanding the parser to support more escape sequences.\r\n* Decorating the completer with a new `Gitsh::CompletionEscaper` which unescapes user input on the way in (so we can match it against the possibly completions) and then escapes the completions on the way out (so they can be correctly parsed by the shell)","created_at":"2016-07-23T22:45:16Z","updated_at":"2016-07-23T22:53:01Z","closed_at":null,"merged_at":null,"merge_commit_sha":"b2697535ef485707520d274014cb7ba3aac11c22","assignee":null,"assignees":[],"milestone":null,"commits_url":"https://api.github.com/repos/thoughtbot/gitsh/pulls/259/commits","review_comments_url":"https://api.github.com/repos/thoughtbot/gitsh/pulls/259/comments","review_comment_url":"https://api.github.com/repos/thoughtbot/gitsh/pulls/comments{/number}","comments_url":"https://api.github.com/repos/thoughtbot/gitsh/issues/259/comments","statuses_url":"https://api.github.com/repos/thoughtbot/gitsh/statuses/d87ef7fd31faf3290b0ae8e92e4d904b5aa01c68","head":{"label":"thoughtbot:gb-tab-complete-spaces","ref":"gb-tab-complete-spaces","sha":"d87ef7fd31faf3290b0ae8e92e4d904b5aa01c68","user":{"login":"thoughtbot","id":6183,"avatar_url":"https://avatars.githubusercontent.com/u/6183?v=3","gravatar_id":"","url":"https://api.github.com/users/thoughtbot","html_url":"https://github.com/thoughtbot","followers_url":"https://api.github.com/users/thoughtbot/followers","following_url":"https://api.github.com/users/thoughtbot/following{/other_user}","gists_url":"https://api.github.com/users/thoughtbot/gists{/gist_id}","starred_url":"https://api.github.com/users/thoughtbot/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/thoughtbot/subscriptions","organizations_url":"https://api.github.com/users/thoughtbot/orgs","repos_url":"https://api.github.com/users/thoughtbot/repos","events_url":"https://api.github.com/users/thoughtbot/events{/privacy}","received_events_url":"https://api.github.com/users/thoughtbot/received_events","type":"Organization","site_admin":false},"repo":{"id":14063942,"name":"gitsh","full_name":"thoughtbot/gitsh","owner":{"login":"thoughtbot","id":6183,"avatar_url":"https://avatars.githubusercontent.com/u/6183?v=3","gravatar_id":"","url":"https://api.github.com/users/thoughtbot","html_url":"https://github.com/thoughtbot","followers_url":"https://api.github.com/users/thoughtbot/followers","following_url":"https://api.github.com/users/thoughtbot/following{/other_user}","gists_url":"https://api.github.com/users/thoughtbot/gists{/gist_id}","starred_url":"https://api.github.com/users/thoughtbot/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/thoughtbot/subscriptions","organizations_url":"https://api.github.com/users/thoughtbot/orgs","repos_url":"https://api.github.com/users/thoughtbot/repos","events_url":"https://api.github.com/users/thoughtbot/events{/privacy}","received_events_url":"https://api.github.com/users/thoughtbot/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/thoughtbot/gitsh","description":"An interactive shell for git","fork":false,"url":"https://api.github.com/repos/thoughtbot/gitsh","forks_url":"https://api.github.com/repos/thoughtbot/gitsh/forks","keys_url":"https://api.github.com/repos/thoughtbot/gitsh/keys{/key_id}","collaborators_url":"https://api.github.com/repos/thoughtbot/gitsh/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/thoughtbot/gitsh/teams","hooks_url":"https://api.github.com/repos/thoughtbot/gitsh/hooks","issue_events_url":"https://api.github.com/repos/thoughtbot/gitsh/issues/events{/number}","events_url":"https://api.github.com/repos/thoughtbot/gitsh/events","assignees_url":"https://api.github.com/repos/thoughtbot/gitsh/assignees{/user}","branches_url":"https://api.github.com/repos/thoughtbot/gitsh/branches{/branch}","tags_url":"https://api.github.com/repos/thoughtbot/gitsh/tags","blobs_url":"https://api.github.com/repos/thoughtbot/gitsh/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/thoughtbot/gitsh/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/thoughtbot/gitsh/git/refs{/sha}","trees_url":"https://api.github.com/repos/thoughtbot/gitsh/git/trees{/sha}","statuses_url":"https://api.github.com/repos/thoughtbot/gitsh/statuses/{sha}","languages_url":"https://api.github.com/repos/thoughtbot/gitsh/languages","stargazers_url":"https://api.github.com/repos/thoughtbot/gitsh/stargazers","contributors_url":"https://api.github.com/repos/thoughtbot/gitsh/contributors","subscribers_url":"https://api.github.com/repos/thoughtbot/gitsh/subscribers","subscription_url":"https://api.github.com/repos/thoughtbot/gitsh/subscription","commits_url":"https://api.github.com/repos/thoughtbot/gitsh/commits{/sha}","git_commits_url":"https://api.github.com/repos/thoughtbot/gitsh/git/commits{/sha}","comments_url":"https://api.github.com/repos/thoughtbot/gitsh/comments{/number}","issue_comment_url":"https://api.github.com/repos/thoughtbot/gitsh/issues/comments{/number}","contents_url":"https://api.github.com/repos/thoughtbot/gitsh/contents/{+path}","compare_url":"https://api.github.com/repos/thoughtbot/gitsh/compare/{base}...{head}","merges_url":"https://api.github.com/repos/thoughtbot/gitsh/merges","archive_url":"https://api.github.com/repos/thoughtbot/gitsh/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/thoughtbot/gitsh/downloads","issues_url":"https://api.github.com/repos/thoughtbot/gitsh/issues{/number}","pulls_url":"https://api.github.com/repos/thoughtbot/gitsh/pulls{/number}","milestones_url":"https://api.github.com/repos/thoughtbot/gitsh/milestones{/number}","notifications_url":"https://api.github.com/repos/thoughtbot/gitsh/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/thoughtbot/gitsh/labels{/name}","releases_url":"https://api.github.com/repos/thoughtbot/gitsh/releases{/id}","deployments_url":"https://api.github.com/repos/thoughtbot/gitsh/deployments","created_at":"2013-11-02T09:33:38Z","updated_at":"2016-07-21T20:18:33Z","pushed_at":"2016-07-23T22:45:16Z","git_url":"git://github.com/thoughtbot/gitsh.git","ssh_url":"git@github.com:thoughtbot/gitsh.git","clone_url":"https://github.com/thoughtbot/gitsh.git","svn_url":"https://github.com/thoughtbot/gitsh","homepage":"https://thoughtbot.com/tools","size":2021,"stargazers_count":1580,"watchers_count":1580,"language":"Ruby","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":true,"forks_count":72,"mirror_url":null,"open_issues_count":24,"forks":72,"open_issues":24,"watchers":1580,"default_branch":"master"}},"base":{"label":"thoughtbot:master","ref":"master","sha":"ea9bb5c9b703bcb2dd1f351a2ce8a0be10a65de8","user":{"login":"thoughtbot","id":6183,"avatar_url":"https://avatars.githubusercontent.com/u/6183?v=3","gravatar_id":"","url":"https://api.github.com/users/thoughtbot","html_url":"https://github.com/thoughtbot","followers_url":"https://api.github.com/users/thoughtbot/followers","following_url":"https://api.github.com/users/thoughtbot/following{/other_user}","gists_url":"https://api.github.com/users/thoughtbot/gists{/gist_id}","starred_url":"https://api.github.com/users/thoughtbot/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/thoughtbot/subscriptions","organizations_url":"https://api.github.com/users/thoughtbot/orgs","repos_url":"https://api.github.com/users/thoughtbot/repos","events_url":"https://api.github.com/users/thoughtbot/events{/privacy}","received_events_url":"https://api.github.com/users/thoughtbot/received_events","type":"Organization","site_admin":false},"repo":{"id":14063942,"name":"gitsh","full_name":"thoughtbot/gitsh","owner":{"login":"thoughtbot","id":6183,"avatar_url":"https://avatars.githubusercontent.com/u/6183?v=3","gravatar_id":"","url":"https://api.github.com/users/thoughtbot","html_url":"https://github.com/thoughtbot","followers_url":"https://api.github.com/users/thoughtbot/followers","following_url":"https://api.github.com/users/thoughtbot/following{/other_user}","gists_url":"https://api.github.com/users/thoughtbot/gists{/gist_id}","starred_url":"https://api.github.com/users/thoughtbot/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/thoughtbot/subscriptions","organizations_url":"https://api.github.com/users/thoughtbot/orgs","repos_url":"https://api.github.com/users/thoughtbot/repos","events_url":"https://api.github.com/users/thoughtbot/events{/privacy}","received_events_url":"https://api.github.com/users/thoughtbot/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/thoughtbot/gitsh","description":"An interactive shell for git","fork":false,"url":"https://api.github.com/repos/thoughtbot/gitsh","forks_url":"https://api.github.com/repos/thoughtbot/gitsh/forks","keys_url":"https://api.github.com/repos/thoughtbot/gitsh/keys{/key_id}","collaborators_url":"https://api.github.com/repos/thoughtbot/gitsh/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/thoughtbot/gitsh/teams","hooks_url":"https://api.github.com/repos/thoughtbot/gitsh/hooks","issue_events_url":"https://api.github.com/repos/thoughtbot/gitsh/issues/events{/number}","events_url":"https://api.github.com/repos/thoughtbot/gitsh/events","assignees_url":"https://api.github.com/repos/thoughtbot/gitsh/assignees{/user}","branches_url":"https://api.github.com/repos/thoughtbot/gitsh/branches{/branch}","tags_url":"https://api.github.com/repos/thoughtbot/gitsh/tags","blobs_url":"https://api.github.com/repos/thoughtbot/gitsh/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/thoughtbot/gitsh/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/thoughtbot/gitsh/git/refs{/sha}","trees_url":"https://api.github.com/repos/thoughtbot/gitsh/git/trees{/sha}","statuses_url":"https://api.github.com/repos/thoughtbot/gitsh/statuses/{sha}","languages_url":"https://api.github.com/repos/thoughtbot/gitsh/languages","stargazers_url":"https://api.github.com/repos/thoughtbot/gitsh/stargazers","contributors_url":"https://api.github.com/repos/thoughtbot/gitsh/contributors","subscribers_url":"https://api.github.com/repos/thoughtbot/gitsh/subscribers","subscription_url":"https://api.github.com/repos/thoughtbot/gitsh/subscription","commits_url":"https://api.github.com/repos/thoughtbot/gitsh/commits{/sha}","git_commits_url":"https://api.github.com/repos/thoughtbot/gitsh/git/commits{/sha}","comments_url":"https://api.github.com/repos/thoughtbot/gitsh/comments{/number}","issue_comment_url":"https://api.github.com/repos/thoughtbot/gitsh/issues/comments{/number}","contents_url":"https://api.github.com/repos/thoughtbot/gitsh/contents/{+path}","compare_url":"https://api.github.com/repos/thoughtbot/gitsh/compare/{base}...{head}","merges_url":"https://api.github.com/repos/thoughtbot/gitsh/merges","archive_url":"https://api.github.com/repos/thoughtbot/gitsh/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/thoughtbot/gitsh/downloads","issues_url":"https://api.github.com/repos/thoughtbot/gitsh/issues{/number}","pulls_url":"https://api.github.com/repos/thoughtbot/gitsh/pulls{/number}","milestones_url":"https://api.github.com/repos/thoughtbot/gitsh/milestones{/number}","notifications_url":"https://api.github.com/repos/thoughtbot/gitsh/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/thoughtbot/gitsh/labels{/name}","releases_url":"https://api.github.com/repos/thoughtbot/gitsh/releases{/id}","deployments_url":"https://api.github.com/repos/thoughtbot/gitsh/deployments","created_at":"2013-11-02T09:33:38Z","updated_at":"2016-07-21T20:18:33Z","pushed_at":"2016-07-23T22:45:16Z","git_url":"git://github.com/thoughtbot/gitsh.git","ssh_url":"git@github.com:thoughtbot/gitsh.git","clone_url":"https://github.com/thoughtbot/gitsh.git","svn_url":"https://github.com/thoughtbot/gitsh","homepage":"https://thoughtbot.com/tools","size":2021,"stargazers_count":1580,"watchers_count":1580,"language":"Ruby","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":true,"forks_count":72,"mirror_url":null,"open_issues_count":24,"forks":72,"open_issues":24,"watchers":1580,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/thoughtbot/gitsh/pulls/259"},"html":{"href":"https://github.com/thoughtbot/gitsh/pull/259"},"issue":{"href":"https://api.github.com/repos/thoughtbot/gitsh/issues/259"},"comments":{"href":"https://api.github.com/repos/thoughtbot/gitsh/issues/259/comments"},"review_comments":{"href":"https://api.github.com/repos/thoughtbot/gitsh/pulls/259/comments"},"review_comment":{"href":"https://api.github.com/repos/thoughtbot/gitsh/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/thoughtbot/gitsh/pulls/259/commits"},"statuses":{"href":"https://api.github.com/repos/thoughtbot/gitsh/statuses/d87ef7fd31faf3290b0ae8e92e4d904b5aa01c68"}}}}
{"actor":{"display_login":"georgebrock"}}