about summary refs log tree commit diff
path: root/Functions/MIME/zsh-mime-handler
blob: abaf0b6e34bcbdfe9c946be1a5733ac01e0bb03c (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
# Handler for MIME types using associative arrays
# zsh_mime_handlers and zsh_mime_flags set up by zsh-mime-setup.
#
# The only flags it handles are copiousoutput and needsterminal.
# copiousoutput is assumed to imply needsterminal.  Apart from
# those, it tries to be a bit cunning about quoting, which
# can be a nightmare in MIME handling.  If it sees something like
#   netscape %s
# and it only has one file to handle (the usual case) then it will handle it
# internally just by appending a file.
#
# Anything else is handled by passing to sh -c, which is the only think
# with a high probability of working.  If it sees something with
# quotes, e.g.
#   /usr/bin/links "%s"
# it will assume someone else has tried to fix the quoting problem and not
# do that.  If it sees something with no quotes but other metacharacters,
# e.g.
#   cat %s | handler
# then it will do any quoting and pass the result to sh -c.
# So for example if the argument is "My File", the command executed
# is supposedly
#   sh -c 'cat My\ File | handler'
#
# This note is mostly here so you can work out what I tried to do when
# it goes horribly wrong.

local autocd
[[ -o autocd ]] && autocd=autocd

emulate -L zsh
setopt extendedglob cbases nullglob $autocd

# We need zformat from zsh/zutil for %s replacement.
zmodload -i zsh/zutil

autoload -Uz zsh-mime-contexts

# Look for options.  Because of the way this is usually invoked,
# (there is always a command to be handled), only handle options
# up to second last argument.
local opt
integer list
while (( $# - $OPTIND > 0 )); do
  if getopts "l" opt; then
    case $opt in
      (l)
      list=1
      ;;

      (*)
      return 1
      ;;
    esac
  else
    break
  fi
done
shift $(( OPTIND - 1 ))

# Always called with a filename argument first.
# There might be other arguments; don't really know what to do
# with these, but if they came from e.g. `*.ps' then we might
# just as well pass them all down.  However, we just take the
# suffix from the first since that's what invoked us via suffix -s.

local suffix s
local -a match mbegin mend

suffix=${1:t}
if [[ $suffix != *.* ]]; then
  "No suffix in command: $1" >&2
  return 1
fi
suffix=${suffix#*.}

local handler flags no_sh no_bg arg
integer i
local -a exec_asis hand_nonex

# Set to a list of patterns which are ignored and executed as they are,
# despite being called for interpretation by the mime handler.
# Defaults to executable files, which ensures that they are executed as
# they are, even if they have a suffix.
zsh-mime-contexts -a $suffix execute-as-is exec_asis || exec_asis=('*(*)' '*(/)')

# Set to a list of patterns for which the handler will be used even
# if the file doesn't exist on the disk.
zsh-mime-contexts -a $suffix handle-nonexistent hand_nonex ||
  hand_nonex=('[[:alpha:]]#:/*')

local pattern
local -a files

# Search some path for the file, if required.
# We do this before any other tests that need to find the
# actual file or its directory.
local dir
local -a filepath
if zsh-mime-contexts -t $suffix find-file-in-path && [[ $1 != /* ]] &&
  [[ $1 != */* || -o pathdirs ]]; then
  zsh-mime-contexts -a $suffix file-path filepath || filepath=($path)
  for dir in $filepath; do
    if [[ -e $dir/$1 ]]; then
      1=$dir/$1
      break
    fi
  done
fi

# In case the pattern contains glob qualifiers, as it does by default,
# we need to do real globbing, not just pattern matching.
# The strategy is to glob the files in the directory using the
# pattern and see if the one we've been passed is in the list.
local dirpref=${1%/*}
if [[ $dirpref = $1 ]]; then
  dirpref=
else
  dirpref+=/
fi

for pattern in $exec_asis; do
  files=(${dirpref}${~pattern})
  if [[ -n ${files[(r)$1]} ]]; then
    if (( list )); then
      for (( i = 1; i <= $#; i++ )); do
	(( i == 1 )) || print -n " "
	arg=${argv[i]}
	if [[ -n $arg ]]; then
	  print -rn -- ${(q)arg}
	else
	  print "''"
	fi
      done
      print
    else
      "$@"
    fi
    return
  fi
done

if [[ ! -e $1 ]]; then
  local nonex_ok
  for pattern in $hand_nonex; do
    if [[ $1 = ${~pattern} ]]; then
      nonex_ok=1
      break
    fi
  done
  if [[ -z $nonex_ok ]]; then
    if (( list )); then
      print -r -- "${(q)@}"
    else
      "$@"
    fi
    return
  fi
fi

if ! zsh-mime-contexts -s $suffix handler handler; then
  # Look for handler starting with longest suffix match.
  # Typically we'd only get a match for the shortest, but don't assume so.
  s=$suffix
  while true; do
    handler="${zsh_mime_handlers[$s]}"
    if [[ -n $handler ]]; then
      break
    fi
    if [[ $s = *.* ]]; then
      s=${s#*.}
    else
      break
    fi
  done
  if [[ -z $handler ]]; then
    if [[ $suffix = *.* ]]; then
      print "No handler specified for suffix .$suffix or any final part" >&2
    else
      print "No handler specified for suffix .$suffix" >&2
    fi
    return 1
  fi
fi
if ! zsh-mime-contexts -s $suffix flags flags; then
  # Same again for flags.
  s=$suffix
  while true; do
    flags="${zsh_mime_flags[$suffix]}"
    if [[ -n $flags ]]; then
      break
    fi
    if [[ $s = *.* ]]; then
      s=${s#*.}
    else
      break
    fi
  done
fi

# Set to yes if we use eval instead of sh -c for complicated mailcap lines
# Can possibly break some mailcap entries which expect sh compatibility,
# but is faster, as a new process is not spawned.
zsh-mime-contexts -t $suffix current-shell && no_sh=yes

# Set to yes if the process shouldn't be backgrounded even if it doesn't need a
# terminal and display is set.
zsh-mime-contexts -t $suffix never-background && no_bg=yes

local hasmeta stdin

# See if the handler has shell metacharacters in.
# Don't count whitespace since we can split that when it's unquoted.
if [[ $handler = *[\\\;\*\?\|\"\'\`\$]* ]]; then
    hasmeta=1
fi

local -a execargs files

if [[ $handler = *%s* ]]; then
  # We need to replace %s with the file(s).
  local command
  if [[ -n $hasmeta || $# -gt 1 ]]; then
    # The handler is complicated, either due to special
    # characters or multiple files.  We are going to pass it
    # down to sh, since it's probably written for sh syntax.
    #
    # See if it's a good idea to quote the filename(s).
    # It isn't if there are already quotes in the handler, since
    # that means somebody already tried to take account of that.
    if [[ $handler = *[\'\"]* ]]; then
      # Probably we ought not even to handle multiple
      # arguments, but at least the error message ought
      # to make it obvious what's going on.
      zformat -f command $handler s:"$argv[1]"
    else
      zformat -f command $handler s:"${(q)argv[1]}"
    fi
    if (( list )); then
      execargs=(${(Q)${(z)command}} ${argv[1,-1]})
    elif [[ $no_sh = yes ]]; then
      execargs=(eval $command)
    else
      execargs=(sh -c $command)
    fi
  else
    # Simple command, one filename.
    # Split and add the file without extra quoting,
    # since later we will just execute the array as is.
    for command in ${=handler}; do
	zformat -f command $command s:"$1"
	execargs+=($command)
    done
  fi
else
  # If there's no %s, the input is supposed to come from stdin.
  stdin=1
  if [[ -n $hasmeta && $no_sh != yes && list -eq 0 ]]; then
    execargs=(sh -c "$handler")
  else
    execargs=(${=handler})
  fi
fi

if (( list )); then
  for (( i = 1; i <= ${#execargs}; i++ )); do
    (( i == 1 )) || print -n " "
    arg=${execargs[i]}
    if [[ -n $arg ]]; then
      print -rn -- ${(q)arg}
    else
      print -n "''"
    fi
  done
  print
  return 0
fi

# Now execute the command in the appropriate fashion.
if [[ $flags = *copiousoutput* ]]; then
  # We need to page the output.
  # Careful in case PAGER is a set of commands and arguments.
  local -a pager
  zsh-mime-contexts -a $suffix pager pager || pager=(${=PAGER:-more})
  if [[ -n $stdin ]]; then
    cat $argv | $execargs | $pager
  else
    $execargs | eval ${PAGER:-more}
  fi
elif [[ $no_bg = yes || $flags = *needsterminal* || -z $DISPLAY ]]; then
  # Needs a terminal, so run synchronously.
  # Obviously, if $DISPLAY is empty but the handler needs a
  # GUI we are in trouble anyway.  However, it's possible for
  # the handler to be smart about this, like pick-web-browser,
  # and even if it just produces an error message it's better to
  # have it run synchronously.
  if [[ -n $stdin ]]; then
    cat $argv | $execargs
  else
    $execargs
  fi
else
  # Doesn't need a terminal and we have a $DISPLAY, so run
  # it in the background.  sh probably isn't smart enough to
  # exec the last command in the list, but it's not a big deal.
  #
  # The following Rococo construction is to try to make
  # the job output for the backgrounded command descriptive.
  # Otherwise it's equivalent to removing the eval and all the quotes,
  # including the (q) flags.
  if [[ -n $stdin ]]; then
    eval cat ${(q)argv} "|" ${(q)execargs} "&"
  else
    eval ${(q)execargs} "&"
  fi
fi