HookFactory.php 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. <?php
  2. namespace Stecman\Component\Symfony\Console\BashCompletion;
  3. final class HookFactory
  4. {
  5. /**
  6. * Hook scripts
  7. *
  8. * These are shell-specific scripts that pass required information from that shell's
  9. * completion system to the interface of the completion command in this module.
  10. *
  11. * The following placeholders are replaced with their value at runtime:
  12. *
  13. * %%function_name%% - name of the generated shell function run for completion
  14. * %%program_name%% - command name completion will be enabled for
  15. * %%program_path%% - path to program the completion is for/generated by
  16. * %%completion_command%% - command to be run to compute completions
  17. *
  18. * NOTE: Comments are stripped out by HookFactory::stripComments as eval reads
  19. * input as a single line, causing it to break if comments are included.
  20. * While comments work using `... | source /dev/stdin`, existing installations
  21. * are likely using eval as it's been part of the instructions for a while.
  22. *
  23. * @var array
  24. */
  25. protected static $hooks = array(
  26. // BASH Hook
  27. 'bash' => <<<'END'
  28. # BASH completion for %%program_path%%
  29. function %%function_name%% {
  30. # Copy BASH's completion variables to the ones the completion command expects
  31. # These line up exactly as the library was originally designed for BASH
  32. local CMDLINE_CONTENTS="$COMP_LINE"
  33. local CMDLINE_CURSOR_INDEX="$COMP_POINT"
  34. local CMDLINE_WORDBREAKS="$COMP_WORDBREAKS";
  35. export CMDLINE_CONTENTS CMDLINE_CURSOR_INDEX CMDLINE_WORDBREAKS
  36. local RESULT STATUS;
  37. RESULT="$(%%completion_command%% </dev/null)";
  38. STATUS=$?;
  39. local cur mail_check_backup;
  40. mail_check_backup=$MAILCHECK;
  41. MAILCHECK=-1;
  42. _get_comp_words_by_ref -n : cur;
  43. # Check if shell provided path completion is requested
  44. # @see Completion\ShellPathCompletion
  45. if [ $STATUS -eq 200 ]; then
  46. _filedir;
  47. return 0;
  48. # Bail out if PHP didn't exit cleanly
  49. elif [ $STATUS -ne 0 ]; then
  50. echo -e "$RESULT";
  51. return $?;
  52. fi;
  53. COMPREPLY=(`compgen -W "$RESULT" -- $cur`);
  54. __ltrim_colon_completions "$cur";
  55. MAILCHECK=mail_check_backup;
  56. };
  57. if [ "$(type -t _get_comp_words_by_ref)" == "function" ]; then
  58. complete -F %%function_name%% "%%program_name%%";
  59. else
  60. >&2 echo "Completion was not registered for %%program_name%%:";
  61. >&2 echo "The 'bash-completion' package is required but doesn't appear to be installed.";
  62. fi
  63. END
  64. // ZSH Hook
  65. , 'zsh' => <<<'END'
  66. # ZSH completion for %%program_path%%
  67. function %%function_name%% {
  68. local -x CMDLINE_CONTENTS="$words"
  69. local -x CMDLINE_CURSOR_INDEX
  70. (( CMDLINE_CURSOR_INDEX = ${#${(j. .)words[1,CURRENT]}} ))
  71. local RESULT STATUS
  72. RESULT=("${(@f)$( %%completion_command%% )}")
  73. STATUS=$?;
  74. # Check if shell provided path completion is requested
  75. # @see Completion\ShellPathCompletion
  76. if [ $STATUS -eq 200 ]; then
  77. _path_files;
  78. return 0;
  79. # Bail out if PHP didn't exit cleanly
  80. elif [ $STATUS -ne 0 ]; then
  81. echo -e "$RESULT";
  82. return $?;
  83. fi;
  84. compadd -- $RESULT
  85. };
  86. compdef %%function_name%% "%%program_name%%";
  87. END
  88. );
  89. /**
  90. * Return the names of shells that have hooks
  91. *
  92. * @return string[]
  93. */
  94. public static function getShellTypes()
  95. {
  96. return array_keys(self::$hooks);
  97. }
  98. /**
  99. * Return a completion hook for the specified shell type
  100. *
  101. * @param string $type - a key from self::$hooks
  102. * @param string $programPath
  103. * @param string $programName
  104. * @param bool $multiple
  105. *
  106. * @return string
  107. */
  108. public function generateHook($type, $programPath, $programName = null, $multiple = false)
  109. {
  110. if (!isset(self::$hooks[$type])) {
  111. throw new \RuntimeException(sprintf(
  112. "Cannot generate hook for unknown shell type '%s'. Available hooks are: %s",
  113. $type,
  114. implode(', ', self::getShellTypes())
  115. ));
  116. }
  117. // Use the program path if an alias/name is not given
  118. $programName = $programName ?: $programPath;
  119. if ($multiple) {
  120. $completionCommand = '$1 _completion';
  121. } else {
  122. $completionCommand = $programPath . ' _completion';
  123. }
  124. return str_replace(
  125. array(
  126. '%%function_name%%',
  127. '%%program_name%%',
  128. '%%program_path%%',
  129. '%%completion_command%%',
  130. ),
  131. array(
  132. $this->generateFunctionName($programPath, $programName),
  133. $programName,
  134. $programPath,
  135. $completionCommand
  136. ),
  137. $this->stripComments(self::$hooks[$type])
  138. );
  139. }
  140. /**
  141. * Generate a function name that is unlikely to conflict with other generated function names in the same shell
  142. */
  143. protected function generateFunctionName($programPath, $programName)
  144. {
  145. return sprintf(
  146. '_%s_%s_complete',
  147. $this->sanitiseForFunctionName(basename($programName)),
  148. substr(md5($programPath), 0, 16)
  149. );
  150. }
  151. /**
  152. * Make a string safe for use as a shell function name
  153. *
  154. * @param string $name
  155. * @return string
  156. */
  157. protected function sanitiseForFunctionName($name)
  158. {
  159. $name = str_replace('-', '_', $name);
  160. return preg_replace('/[^A-Za-z0-9_]+/', '', $name);
  161. }
  162. /**
  163. * Strip '#' style comments from a string
  164. *
  165. * BASH's eval doesn't work with comments as it removes line breaks, so comments have to be stripped out
  166. * for this method of sourcing the hook to work. Eval seems to be the most reliable method of getting a
  167. * hook into a shell, so while it would be nice to render comments, this stripping is required for now.
  168. *
  169. * @param string $script
  170. * @return string
  171. */
  172. protected function stripComments($script)
  173. {
  174. return preg_replace('/(^\s*\#.*$)/m', '', $script);
  175. }
  176. }