oops-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,336 @@
1
+ # oops: bash hooks (bash 3.2 or later). Put this line in ~/.bashrc (or
2
+ # ~/.bash_profile, if that is where your settings live), after whatever puts
3
+ # oops's Ruby on PATH and as early as possible after that:
4
+ #
5
+ # eval "$(oops init bash)"
6
+ #
7
+ # On a terminal that is not recorded yet, the shell is started again under
8
+ # `oops record` (a pty relay). Inside the recorded shell the hooks write
9
+ # invisible APC markers around every command line:
10
+ #
11
+ # ESC _ oops;start;<seq>;<epoch>;<base64 cwd>;<base64 cmd> ESC \
12
+ # ESC _ oops;end;<seq>;<exit> ESC \
13
+ #
14
+ # "Recorded" means $OOPS_TTY (set by the relay) names this very terminal, so
15
+ # shells started from a recorded one on another tty (tmux, GUI terminals)
16
+ # get recorded on their own, and `exec bash` on the same tty is not recorded
17
+ # twice. Markers go straight to the terminal device: inside `{ ...; } >file`
18
+ # the DEBUG trap runs with stdout redirected.
19
+ #
20
+ # The preexec hook uses the bash-preexec technique (DEBUG trap armed by the
21
+ # last PROMPT_COMMAND entry), inlined and reduced to what oops needs.
22
+ if [[ $- == *i* ]] && [ -t 0 ] && [ -t 1 ] && [ -z "${OOPS_DISABLE:-}" ] &&
23
+ [ -z "${BASH_EXECUTION_STRING:-}" ]; then
24
+ __oops_bin=__OOPS_BIN__
25
+ __oops_tty=$(tty 2>/dev/null || :)
26
+
27
+ # True when bash was started to run a script (`bash -i script`). bash 4+
28
+ # already has the script in $0 while the rc file runs; bash 3.2 still has
29
+ # "bash" there, so look at the shell's own argv.
30
+ __oops_runs_script() {
31
+ local args word skip= after_dashes= found= noglob=
32
+ case ${0##*/} in
33
+ bash | -bash | sh | -sh) ;;
34
+ *) if [ -f "$0" ] && ! [ "$0" -ef "$BASH" ]; then return 0; fi ;;
35
+ esac
36
+ args=$(ps -ww -o args= -p $$ 2>/dev/null) || return 1
37
+ case $args in
38
+ "$0 "*) args=${args#"$0 "} ;;
39
+ *) return 1 ;;
40
+ esac
41
+ case $- in *f*) noglob=1 ;; esac
42
+ set -f
43
+ for word in $args; do
44
+ if [ -n "$after_dashes" ]; then
45
+ found=1
46
+ break
47
+ elif [ -n "$skip" ]; then
48
+ skip=
49
+ continue
50
+ fi
51
+ case $word in
52
+ --rcfile | --init-file | -o | +o | -O | +O) skip=1 ;;
53
+ --) after_dashes=1 ;;
54
+ -* | +*) ;;
55
+ *)
56
+ found=1
57
+ break
58
+ ;;
59
+ esac
60
+ done
61
+ [ -n "$noglob" ] || set +f
62
+ [ -n "$found" ]
63
+ }
64
+
65
+ # Without a tty name (tty(1) failed) this terminal cannot be told apart
66
+ # from a recorded one, so the shell is left alone: no restart, no hooks.
67
+ if [ -n "$__oops_tty" ] && [ "${OOPS_TTY:-}" != "$__oops_tty" ]; then
68
+ if __oops_runs_script; then
69
+ : # not a prompt to record; run the script as asked
70
+ elif command -v "$__oops_bin" >/dev/null 2>&1; then
71
+ # execfail: if the exec fails anyway, keep this shell running.
72
+ __oops_execfail=$(shopt -p execfail || :)
73
+ shopt -s execfail
74
+ if shopt -q login_shell; then
75
+ exec "$__oops_bin" record -- "$BASH" -l
76
+ else
77
+ exec "$__oops_bin" record -- "$BASH"
78
+ fi
79
+ eval "$__oops_execfail"
80
+ unset __oops_execfail
81
+ else
82
+ echo "oops: cannot run $__oops_bin; recording disabled" >&2
83
+ fi
84
+ elif [ -n "$__oops_tty" ]; then
85
+ # Our PROMPT_COMMAND entries. Each keeps $? and $_ for the entries after
86
+ # it: a hook returns the status, and `&& __oops_ok "$_"` (run only when
87
+ # it is 0) puts $_ back. A non-zero status inside an && list does not
88
+ # trigger `set -e`.
89
+ __oops_e_pre='__oops_precmd "$_" && __oops_ok "$_"'
90
+ __oops_e_pass='__oops_pass "$_" && __oops_ok "$_"'
91
+ __oops_e_arm='__oops_arm'
92
+ # The DEBUG trap is read and set here, not inside __oops_install: in a
93
+ # function the DEBUG trap reads as unset, and bash puts back the one the
94
+ # function started with when it returns.
95
+ __oops_e_install='__oops_install "$(trap -p DEBUG)" && __oops_ok; [ -z "${__oops_trap:-}" ] || trap -- "$__oops_trap" DEBUG'
96
+
97
+ __oops_ok() { return 0; }
98
+ __oops_b64() { printf '%s' "$1" | base64 | tr -d '\n'; }
99
+
100
+ # $1: $BASH_COMMAND, the first simple command of the line. The full line
101
+ # is the newest history entry, but only if the history grew since the
102
+ # prompt; otherwise the line was not saved (HISTCONTROL, HISTIGNORE,
103
+ # history off) and the entry belongs to an older command.
104
+ __oops_preexec() {
105
+ local line num
106
+ line=$(HISTTIMEFORMAT='' builtin history 1)
107
+ num=${line#"${line%%[0-9]*}"}
108
+ num=${num%%[!0-9]*}
109
+ if [ -n "$num" ] && [ "$num" != "$__oops_hist" ]; then
110
+ line=${line#*[0-9][* ] }
111
+ else
112
+ line=$1
113
+ fi
114
+ __OOPS_SEQ=$((__OOPS_SEQ + 1))
115
+ __oops_open=$__OOPS_SEQ
116
+ printf '\033_oops;start;%s;%s;%s;%s\033\\' "$__OOPS_SEQ" "$(date +%s)" \
117
+ "$(__oops_b64 "$PWD")" "$(__oops_b64 "${line:0:8192}")" 2>/dev/null >"$__oops_tty" || :
118
+ }
119
+
120
+ # The DEBUG trap runs before every simple command; only the first one
121
+ # after a prompt starts a record. $1 is handed back as the status (for a
122
+ # chained trap) and $2 restores $_, which calling a function overwrites.
123
+ # READLINE_LINE is set while a `bind -x` widget (fzf, atuin) runs; such
124
+ # commands are not the user's line. bash 3.2 does not set it, so there a
125
+ # widget's command takes the record of the line typed after it.
126
+ __oops_debug() {
127
+ if [ -n "$__oops_armed" ] && [ -z "${COMP_LINE:-}" ] && [ -z "${READLINE_LINE+x}" ] &&
128
+ [ "${BASH_SUBSHELL:-0}" = 0 ]; then
129
+ case $BASH_COMMAND in
130
+ __oops_precmd*) ;; # empty line: the prompt is drawn again
131
+ *)
132
+ case $__oops_tail in
133
+ *"$BASH_COMMAND"*) ;; # entries after a rewrap, see __oops_wrap
134
+ *)
135
+ __oops_armed=
136
+ __oops_tail=
137
+ __oops_preexec "$BASH_COMMAND"
138
+ ;;
139
+ esac
140
+ ;;
141
+ esac
142
+ fi
143
+ return "$1"
144
+ }
145
+
146
+ # First PROMPT_COMMAND entry: $? must be read before anything else runs.
147
+ __oops_precmd() {
148
+ local exit_status=$?
149
+ __oops_armed=
150
+ if [ -n "$__oops_open" ]; then
151
+ printf '\033_oops;end;%s;%s\033\\' "$__oops_open" "$exit_status" 2>/dev/null >"$__oops_tty" || :
152
+ __oops_open=
153
+ fi
154
+ __oops_status=$exit_status
155
+ return "$exit_status"
156
+ }
157
+
158
+ # Stands in for an entry of ours that is no longer in its place.
159
+ __oops_pass() { return "${__oops_status:-0}"; }
160
+
161
+ __oops_pc_is_array() {
162
+ case $(declare -p PROMPT_COMMAND 2>/dev/null) in
163
+ 'declare -a'*) return 0 ;;
164
+ esac
165
+ return 1
166
+ }
167
+
168
+ # True when __oops_precmd is the first entry and __oops_arm the last.
169
+ __oops_wrapped() {
170
+ local nl=$'\n' i last=
171
+ case ${PROMPT_COMMAND:-} in
172
+ "$__oops_e_pre$nl"*"$nl$__oops_e_arm") return 0 ;;
173
+ esac
174
+ __oops_pc_is_array || return 1
175
+ case ${PROMPT_COMMAND:-} in "$__oops_e_pre$nl"*) ;; *) return 1 ;; esac
176
+ for i in "${!PROMPT_COMMAND[@]}"; do last=$i; done
177
+ [ "${PROMPT_COMMAND[$last]}" = "$__oops_e_arm" ]
178
+ }
179
+
180
+ # Puts __oops_precmd first and __oops_arm last in PROMPT_COMMAND (an
181
+ # array in bash 5.1+); our entries found elsewhere become __oops_pass, so
182
+ # text around them stays valid. $1 is the entry running now: the entries
183
+ # after it still run in this prompt, before __oops_arm, so their text is
184
+ # kept in __oops_tail and skipped by the DEBUG trap.
185
+ __oops_wrap() {
186
+ local cur=$1 nl=$'\n' sep=$' \t\n;' i elem first= seen= tail=
187
+ if __oops_pc_is_array; then
188
+ for i in "${!PROMPT_COMMAND[@]}"; do
189
+ [ -n "$first" ] || first=$i
190
+ elem=${PROMPT_COMMAND[$i]}
191
+ if [ -n "$seen" ]; then
192
+ tail="$tail$nl$elem"
193
+ else
194
+ case $elem in *"$cur"*) seen=1 tail=${elem#*"$cur"} ;; esac
195
+ fi
196
+ elem=${elem//"$__oops_e_install"/"$__oops_e_pass"}
197
+ elem=${elem//"$__oops_e_pre"/"$__oops_e_pass"}
198
+ PROMPT_COMMAND[$i]=${elem//"$__oops_e_arm"/"$__oops_e_pass"}
199
+ done
200
+ PROMPT_COMMAND[$first]="$__oops_e_pre$nl${PROMPT_COMMAND[$first]}"
201
+ PROMPT_COMMAND+=("$__oops_e_arm")
202
+ else
203
+ elem=${PROMPT_COMMAND:-}
204
+ case $elem in *"$cur"*) tail=${elem#*"$cur"} ;; esac
205
+ elem=${elem//"$__oops_e_install"/"$__oops_e_pass"}
206
+ elem=${elem//"$__oops_e_pre"/"$__oops_e_pass"}
207
+ elem=${elem//"$__oops_e_arm"/"$__oops_e_pass"}
208
+ PROMPT_COMMAND="$__oops_e_pre$nl$elem$nl$__oops_e_arm"
209
+ fi
210
+ case $tail in *[!"$sep"]*) __oops_tail=$tail ;; *) __oops_tail= ;; esac
211
+ }
212
+
213
+ __oops_history_number() {
214
+ local line
215
+ line=$(HISTTIMEFORMAT='' builtin history 1)
216
+ line=${line#"${line%%[0-9]*}"}
217
+ __oops_hist=${line%%[!0-9]*}
218
+ }
219
+
220
+ # Last PROMPT_COMMAND entry: the next simple command is the user's. A
221
+ # sourced rc file may have put entries before or after ours; wrap again.
222
+ __oops_arm() {
223
+ __oops_tail=
224
+ __oops_wrapped || __oops_wrap "$__oops_e_arm"
225
+ __oops_history_number
226
+ __oops_armed=1
227
+ return 0
228
+ }
229
+
230
+ # Runs at the first prompt, after the rest of the rc file, so hooks and
231
+ # DEBUG traps set up later in it are kept. $1: `trap -p DEBUG`.
232
+ __oops_install() {
233
+ local prev=$1
234
+ if __oops_bp_present; then
235
+ __oops_use_bp
236
+ return 0
237
+ fi
238
+ __oops_wrap "$__oops_e_install"
239
+ case $prev in
240
+ *__oops_debug*) __oops_trap= ;;
241
+ '') __oops_trap='__oops_debug 0 "$_"' ;;
242
+ *)
243
+ eval "set -- $prev"
244
+ __oops_trap="__oops_debug \"\$?\" \"\$_\" && __oops_ok \"\$_\"; $3"
245
+ ;;
246
+ esac
247
+ __oops_history_number
248
+ __oops_armed=1
249
+ return 0
250
+ }
251
+
252
+ # --- bash-preexec ---
253
+ #
254
+ # bash-preexec (behind atuin, Ghostty's bash integration and others) needs
255
+ # the same two things as the hooks above: the DEBUG trap, and its
256
+ # `__bp_interactive_mode` entry as the last PROMPT_COMMAND entry. Both of
257
+ # us wrapping PROMPT_COMMAND on every prompt broke recording (our arm
258
+ # entry used up its flag, and our entries piled up, which made its string
259
+ # clean-up crawl in bash 3.2). So when it is loaded, oops stops using its own
260
+ # DEBUG trap and PROMPT_COMMAND entries and registers with its arrays.
261
+
262
+ __oops_bp_present() { declare -F __bp_install >/dev/null 2>&1; }
263
+
264
+ # bash-preexec runs this with the line from history. When the line was
265
+ # not saved (HISTCONTROL, HISTIGNORE) that is an older line, and oops
266
+ # has nothing better than a placeholder: the DEBUG trap's $BASH_COMMAND
267
+ # is out of reach here.
268
+ __oops_bp_preexec() { __oops_preexec "(not saved in history)"; }
269
+
270
+ # bash-preexec restores $? before it calls each precmd function.
271
+ __oops_bp_precmd() {
272
+ __oops_precmd
273
+ local exit_status=$?
274
+ __oops_history_number
275
+ return "$exit_status"
276
+ }
277
+
278
+ # Drops our first-prompt entry from PROMPT_COMMAND; it has done its job.
279
+ # ":" is what bash-preexec cleans away itself.
280
+ __oops_forget_install_entry() {
281
+ local i
282
+ if __oops_pc_is_array; then
283
+ for i in "${!PROMPT_COMMAND[@]}"; do
284
+ PROMPT_COMMAND[$i]=${PROMPT_COMMAND[$i]//"$__oops_e_install"/:}
285
+ done
286
+ else
287
+ PROMPT_COMMAND=${PROMPT_COMMAND:-}
288
+ PROMPT_COMMAND=${PROMPT_COMMAND//"$__oops_e_install"/:}
289
+ fi
290
+ }
291
+
292
+ __oops_use_bp() {
293
+ __oops_bp=1
294
+ __oops_armed=
295
+ case " ${precmd_functions[*]:-} " in
296
+ *" __oops_bp_precmd "*) ;;
297
+ *) precmd_functions=(__oops_bp_precmd ${precmd_functions[@]+"${precmd_functions[@]}"}) ;;
298
+ esac
299
+ case " ${preexec_functions[*]:-} " in
300
+ *" __oops_bp_preexec "*) ;;
301
+ *) preexec_functions+=(__oops_bp_preexec) ;;
302
+ esac
303
+ __oops_forget_install_entry
304
+ __oops_history_number
305
+ }
306
+
307
+ # Once per shell process; sourcing ~/.bashrc again must not redo this.
308
+ if [ -z "${__oops_hooked:-}" ]; then
309
+ __oops_hooked=1
310
+ __oops_open=
311
+ __oops_armed=
312
+ __oops_tail=
313
+ __oops_hist=
314
+ __oops_trap=
315
+ case ${__OOPS_SEQ:-} in '' | *[!0-9]*) __OOPS_SEQ=0 ;; esac
316
+ # Same pid as the shell that set __OOPS_PID: this shell replaced it
317
+ # with `exec`, so that command (still open in the relay) is done.
318
+ if [ "${__OOPS_PID:-}" = "$$" ] && [ "$__OOPS_SEQ" != 0 ]; then
319
+ printf '\033_oops;end;%s;%s\033\\' "$__OOPS_SEQ" 0 2>/dev/null >"$__oops_tty" || :
320
+ fi
321
+ # Exported so a nested shell on this tty continues the numbering.
322
+ export __OOPS_SEQ
323
+ export __OOPS_PID=$$
324
+ fi
325
+ if __oops_bp_present; then
326
+ __oops_use_bp
327
+ else
328
+ # Again after a sourced rc file replaced PROMPT_COMMAND.
329
+ case ${PROMPT_COMMAND:-} in
330
+ *__oops_precmd* | *__oops_install*) ;;
331
+ *) PROMPT_COMMAND="${PROMPT_COMMAND:-}"$'\n'"$__oops_e_install" ;;
332
+ esac
333
+ fi
334
+ fi
335
+ unset -f __oops_runs_script
336
+ fi
@@ -0,0 +1,102 @@
1
+ # oops: zsh hooks. Put this line in ~/.zshrc, after whatever puts oops's Ruby
2
+ # on PATH and as early as possible after that:
3
+ #
4
+ # eval "$(oops init zsh)"
5
+ #
6
+ # On a terminal that is not recorded yet, the shell is started again under
7
+ # `oops record` (a pty relay). Inside the recorded shell the hooks write
8
+ # invisible APC markers around every command:
9
+ #
10
+ # ESC _ oops;start;<seq>;<epoch>;<base64 cwd>;<base64 cmd> ESC \
11
+ # ESC _ oops;end;<seq>;<exit> ESC \
12
+ #
13
+ # "Recorded" means $OOPS_TTY (set by the relay) names this very terminal, so
14
+ # shells started from a recorded one on another tty (tmux, GUI terminals)
15
+ # get recorded on their own, and `exec zsh` on the same tty is not recorded
16
+ # twice. Markers go straight to the terminal device, so a redirected stdout
17
+ # never receives them.
18
+ # `zsh -ic 'cmd'` and `zsh -i script` are left alone so the command runs.
19
+ if [[ -o interactive && -t 0 && -t 1 && -n "${TTY:-}" && -z "${OOPS_DISABLE:-}" &&
20
+ -z "${ZSH_EXECUTION_STRING:-}" && -z "${ZSH_SCRIPT:-}" ]]; then
21
+ __oops_bin=__OOPS_BIN__
22
+ if [[ "${OOPS_TTY:-}" != "$TTY" ]]; then
23
+ # A failed exec ends zsh, so check first; without the binary the shell
24
+ # simply runs unrecorded.
25
+ if command -v "$__oops_bin" >/dev/null 2>&1; then
26
+ # Restart this same zsh. A bare name (a login shell's "-zsh") could
27
+ # find a different zsh on PATH, so a login shell uses $SHELL, the
28
+ # login shell it was started as, and other bare names go through
29
+ # `command -v`.
30
+ __oops_zsh=${${ZSH_ARGZERO#-}:-zsh}
31
+ if [[ "$__oops_zsh" != */* ]]; then
32
+ if [[ "$ZSH_ARGZERO" == -* && "${SHELL:-}" == */"$__oops_zsh" && -x "$SHELL" ]]; then
33
+ __oops_zsh=$SHELL
34
+ else
35
+ __oops_zsh=${$(command -v -- "$__oops_zsh" 2>/dev/null):-$__oops_zsh}
36
+ fi
37
+ fi
38
+ # Ghostty injects its shell integration through ZDOTDIR and removes the
39
+ # injection again, so the restarted shell would not get it. Note
40
+ # whether this one has it (the integration sets _ghostty_state), so
41
+ # the restarted shell can load it again; see the end of this block.
42
+ if (( $+_ghostty_state )); then export __OOPS_GHOSTTY=1; else unset __OOPS_GHOSTTY; fi
43
+ exec "$__oops_bin" record -- "$__oops_zsh"
44
+ else
45
+ print -ru2 -- "oops: cannot run $__oops_bin; recording disabled"
46
+ fi
47
+ else
48
+ zmodload zsh/datetime 2>/dev/null
49
+ autoload -Uz add-zsh-hook
50
+
51
+ # The ";" before "}" keeps this valid with IGNORE_CLOSE_BRACES.
52
+ __oops_b64() { printf '%s' "$1" | base64 | tr -d '\n'; }
53
+
54
+ # $1 is the line as typed (empty if history is off; $3 is the full text
55
+ # that runs). Very long lines are cut so the marker stays small.
56
+ __oops_preexec() {
57
+ emulate -L zsh
58
+ local cmd=${1:-$3}
59
+ __OOPS_SEQ=$(( __OOPS_SEQ + 1 ))
60
+ __oops_open=$__OOPS_SEQ
61
+ printf '\033_oops;start;%s;%s;%s;%s\033\\' "$__OOPS_SEQ" "${EPOCHSECONDS:-$(date +%s)}" \
62
+ "$(__oops_b64 "$PWD")" "$(__oops_b64 "${cmd[1,8192]}")" 2>/dev/null >"$TTY"
63
+ }
64
+
65
+ # $? must be read before anything else runs; it is handed back unchanged.
66
+ __oops_precmd() {
67
+ local exit_status=$?
68
+ emulate -L zsh
69
+ if [[ -n "$__oops_open" ]]; then
70
+ printf '\033_oops;end;%s;%s\033\\' "$__oops_open" "$exit_status" 2>/dev/null >"$TTY"
71
+ __oops_open=
72
+ fi
73
+ return $exit_status
74
+ }
75
+
76
+ # Once per shell process; sourcing ~/.zshrc again must not redo this.
77
+ if [[ -z "${__oops_hooked:-}" ]]; then
78
+ __oops_hooked=1
79
+ __oops_open=
80
+ case "${__OOPS_SEQ:-}" in ('' | *[!0-9]*) __OOPS_SEQ=0 ;; esac
81
+ # Same pid as the shell that set __OOPS_PID: this shell replaced it
82
+ # with `exec`, so that command (still open in the relay) is done.
83
+ if [[ "${__OOPS_PID:-}" == "$$" && "$__OOPS_SEQ" != 0 ]]; then
84
+ printf '\033_oops;end;%s;%s\033\\' "$__OOPS_SEQ" 0 2>/dev/null >"$TTY"
85
+ fi
86
+ # Exported so a nested shell on this tty continues the numbering.
87
+ export __OOPS_SEQ
88
+ export __OOPS_PID=$$
89
+ fi
90
+ add-zsh-hook preexec __oops_preexec
91
+ add-zsh-hook precmd __oops_precmd
92
+
93
+ # The shell this one replaced had Ghostty's integration (see above); load
94
+ # it here too, as Ghostty documents for a manual setup. It does nothing
95
+ # when it is already loaded. This comes after the hooks above, so
96
+ # __oops_precmd stays the first precmd hook and sees $?.
97
+ if [[ -n "${__OOPS_GHOSTTY:-}" && -n "${GHOSTTY_RESOURCES_DIR:-}" &&
98
+ -r "$GHOSTTY_RESOURCES_DIR/shell-integration/zsh/ghostty-integration" ]]; then
99
+ builtin source "$GHOSTTY_RESOURCES_DIR/shell-integration/zsh/ghostty-integration"
100
+ fi
101
+ fi
102
+ fi
@@ -0,0 +1,26 @@
1
+ module Oops
2
+ # The snippets printed by `oops init zsh|bash`, meant for
3
+ # `eval "$(oops init zsh)"` in the rc file, after whatever puts oops's Ruby
4
+ # on PATH and as early as possible after that. The templates live in
5
+ # lib/oops/shell/; the absolute path of the oops executable is written into
6
+ # them, so a later change to PATH cannot break the hooks.
7
+ module ShellInit
8
+ SHELLS = %w[zsh bash].freeze
9
+ TEMPLATE_DIR = File.expand_path("shell", __dir__)
10
+ BIN_PLACEHOLDER = "__OOPS_BIN__".freeze
11
+
12
+ def self.snippet(shell, bin: "oops")
13
+ raise ArgumentError, "unsupported shell: #{shell}" unless SHELLS.include?(shell)
14
+
15
+ template = File.read(File.join(TEMPLATE_DIR, "oops.#{shell}"), encoding: Encoding::UTF_8)
16
+ template.gsub(BIN_PLACEHOLDER) { shell_quote(bin) }
17
+ end
18
+
19
+ # One single-quoted word for both shells: ' becomes '\''.
20
+ def self.shell_quote(text)
21
+ "'#{text.to_s.gsub("'") { "'\\''" }}'"
22
+ end
23
+
24
+ private_class_method :shell_quote
25
+ end
26
+ end