HookFactory   A
last analyzed

Complexity

Total Complexity 8

Size/Duplication

Total Lines 209
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 0

Importance

Changes 0
Metric Value
wmc 8
lcom 1
cbo 0
dl 0
loc 209
rs 10
c 0
b 0
f 0

5 Methods

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