All patches and comments are welcome. Please squash your changes to logical
commits before using git-format-patch and git-send-email to
patches@git.madduck.net.
If you'd read over the Git project's submission guidelines and adhered to them,
I'd be especially grateful.
1 " Author: w0rp <devw0rp@gmail.com>
2 " Description: Functions for formatting command strings, running commands, and
3 " managing files during linting and fixing cycles.
5 " This dictionary holds lists of files and directories to remove later.
6 if !exists('s:buffer_data')
10 " The regular expression used for formatting filenames with modifiers.
11 let s:path_format_regex = '\v\%s(%(:h|:t|:r|:e)*)'
13 " Used to get the data in tests.
14 function! ale#command#GetData() abort
15 return deepcopy(s:buffer_data)
18 function! ale#command#ClearData() abort
19 let s:buffer_data = {}
22 function! ale#command#InitData(buffer) abort
23 if !has_key(s:buffer_data, a:buffer)
24 let s:buffer_data[a:buffer] = {
27 \ 'directory_list': [],
32 " Set the cwd for commands that are about to run.
34 function! ale#command#SetCwd(buffer, cwd) abort
35 call ale#command#InitData(a:buffer)
36 let s:buffer_data[a:buffer].cwd = a:cwd
39 function! ale#command#ResetCwd(buffer) abort
40 if has_key(s:buffer_data, a:buffer)
41 let s:buffer_data[a:buffer].cwd = v:null
45 function! ale#command#ManageFile(buffer, file) abort
46 call ale#command#InitData(a:buffer)
47 call add(s:buffer_data[a:buffer].file_list, a:file)
50 function! ale#command#ManageDirectory(buffer, directory) abort
51 call ale#command#InitData(a:buffer)
52 call add(s:buffer_data[a:buffer].directory_list, a:directory)
55 function! ale#command#CreateFile(buffer) abort
56 " This variable can be set to 1 in tests to stub this out.
57 if get(g:, 'ale_create_dummy_temporary_file')
61 let l:temporary_file = ale#util#Tempname()
62 call ale#command#ManageFile(a:buffer, l:temporary_file)
64 return l:temporary_file
67 " Create a new temporary directory and manage it in one go.
68 function! ale#command#CreateDirectory(buffer) abort
69 " This variable can be set to 1 in tests to stub this out.
70 if get(g:, 'ale_create_dummy_temporary_file')
74 let l:temporary_directory = ale#util#Tempname()
75 " Create the temporary directory for the file, unreadable by 'other'
77 call mkdir(l:temporary_directory, '', 0750)
78 call ale#command#ManageDirectory(a:buffer, l:temporary_directory)
80 return l:temporary_directory
83 function! ale#command#RemoveManagedFiles(buffer) abort
84 let l:info = get(s:buffer_data, a:buffer, {})
86 if !empty(l:info) && empty(l:info.jobs)
87 " We can't delete anything in a sandbox, so wait until we escape from
88 " it to delete temporary files and directories.
89 if ale#util#InSandbox()
93 " Delete files with a call akin to a plan `rm` command.
94 for l:filename in l:info.file_list
95 call delete(l:filename)
98 " Delete directories like `rm -rf`.
99 " Directories are handled differently from files, so paths that are
100 " intended to be single files can be set up for automatic deletion
101 " without accidentally deleting entire directories.
102 for l:directory in l:info.directory_list
103 call delete(l:directory, 'rf')
106 call remove(s:buffer_data, a:buffer)
110 function! ale#command#CreateTempFile(buffer, temporary_file, input) abort
111 if empty(a:temporary_file)
112 " There is no file, so we didn't create anything.
116 " Use an existing list of lines of input if we have it, or get the lines
118 let l:lines = a:input isnot v:null ? a:input : getbufline(a:buffer, 1, '$')
120 let l:temporary_directory = fnamemodify(a:temporary_file, ':h')
121 " Create the temporary directory for the file, unreadable by 'other'
123 call mkdir(l:temporary_directory, '', 0750)
124 " Automatically delete the directory later.
125 call ale#command#ManageDirectory(a:buffer, l:temporary_directory)
126 " Write the buffer out to a file.
127 call ale#util#Writefile(a:buffer, l:lines, a:temporary_file)
132 function! s:TemporaryFilename(buffer) abort
133 let l:filename = fnamemodify(bufname(a:buffer), ':t')
136 " If the buffer's filename is empty, create a dummy filename.
137 let l:ft = getbufvar(a:buffer, '&filetype')
138 let l:filename = 'file' . ale#filetypes#GuessExtension(l:ft)
141 " Create a temporary filename, <temp_dir>/<original_basename>
142 " The file itself will not be created by this function.
143 return ale#util#Tempname() . (has('win32') ? '\' : '/') . l:filename
146 " Given part of a command, replace any % with %%, so that no characters in
147 " the string will be replaced with filenames, etc.
148 function! ale#command#EscapeCommandPart(command_part) abort
149 return substitute(a:command_part, '%', '%%', 'g')
152 " Format a filename, converting it with filename mappings, if non-empty,
153 " and escaping it for putting into a command string.
155 " The filename can be modified.
156 function! s:FormatFilename(filename, mappings, modifiers) abort
157 let l:filename = a:filename
159 if !empty(a:mappings)
160 let l:filename = ale#filename_mapping#Map(l:filename, a:mappings)
163 if !empty(a:modifiers)
164 let l:filename = fnamemodify(l:filename, a:modifiers)
167 return ale#Escape(l:filename)
170 " Produce a command prefix to check to a particular directory for a command.
171 " %s format markers with filename-modifiers can be used as the directory, and
172 " will be returned verbatim for formatting in paths relative to files.
173 function! ale#command#CdString(directory) abort
174 let l:match = matchstrpos(a:directory, s:path_format_regex)
175 " Do not escape the directory here if it's a valid format string.
176 " This allows us to use sequences like %s:h, %s:h:h, etc.
177 let l:directory = l:match[1:] == [0, len(a:directory)]
179 \ : ale#Escape(a:directory)
182 return 'cd /d ' . l:directory . ' && '
185 return 'cd ' . l:directory . ' && '
188 " Given a command string, replace every...
189 " %s -> with the current filename
190 " %t -> with the name of an unused file in a temporary directory
191 " %% -> with a literal %
192 function! ale#command#FormatCommand(
196 \ pipe_file_if_needed,
201 let l:temporary_file = ''
202 let l:command = a:command
205 let l:command = ale#command#CdString(a:cwd) . l:command
208 " First replace all uses of %%, used for literal percent characters,
209 " with an ugly string.
210 let l:command = substitute(l:command, '%%', '<<PERCENTS>>', 'g')
212 " Replace %e with the escaped executable, if available.
213 if !empty(a:executable) && l:command =~# '%e'
214 let l:command = substitute(l:command, '%e', '\=ale#Escape(a:executable)', 'g')
217 " Replace all %s occurrences in the string with the name of the current
219 if l:command =~# '%s'
220 let l:filename = fnamemodify(bufname(a:buffer), ':p')
221 let l:command = substitute(
223 \ s:path_format_regex,
224 \ '\=s:FormatFilename(l:filename, a:mappings, submatch(1))',
229 if a:input isnot v:false && l:command =~# '%t'
230 " Create a temporary filename, <temp_dir>/<original_basename>
231 " The file itself will not be created by this function.
232 let l:temporary_file = s:TemporaryFilename(a:buffer)
233 let l:command = substitute(
235 \ '\v\%t(%(:h|:t|:r|:e)*)',
236 \ '\=s:FormatFilename(l:temporary_file, a:mappings, submatch(1))',
241 " Finish formatting so %% becomes %.
242 let l:command = substitute(l:command, '<<PERCENTS>>', '%', 'g')
244 if a:pipe_file_if_needed && empty(l:temporary_file)
245 " If we are to send the Vim buffer to a command, we'll do it
246 " in the shell. We'll write out the file to a temporary file,
247 " and then read it back in, in the shell.
248 let l:temporary_file = s:TemporaryFilename(a:buffer)
249 let l:command = l:command . ' < ' . ale#Escape(l:temporary_file)
252 let l:file_created = ale#command#CreateTempFile(
258 return [l:temporary_file, l:command, l:file_created]
261 function! ale#command#StopJobs(buffer, job_type) abort
262 let l:info = get(s:buffer_data, a:buffer, {})
267 for [l:job_id, l:job_type] in items(l:info.jobs)
268 let l:job_id = str2nr(l:job_id)
270 if a:job_type is# 'all' || a:job_type is# l:job_type
271 call ale#job#Stop(l:job_id)
273 let l:new_map[l:job_id] = l:job_type
277 let l:info.jobs = l:new_map
281 function! s:GatherOutput(line_list, job_id, line) abort
282 call add(a:line_list, a:line)
285 function! s:ExitCallback(buffer, line_list, Callback, data) abort
286 if !has_key(s:buffer_data, a:buffer)
290 let l:jobs = s:buffer_data[a:buffer].jobs
292 if !has_key(l:jobs, a:data.job_id)
296 let l:job_type = remove(l:jobs, a:data.job_id)
298 if g:ale_history_enabled
299 call ale#history#SetExitCode(a:buffer, a:data.job_id, a:data.exit_code)
301 " Log the output of the command for ALEInfo if we should.
302 if g:ale_history_log_output && a:data.log_output is 1
303 call ale#history#RememberOutput(
311 " If the callback starts any new jobs, use the same job type for them.
312 call setbufvar(a:buffer, 'ale_job_type', l:job_type)
313 let l:value = a:Callback(a:buffer, a:line_list, {
314 \ 'exit_code': a:data.exit_code,
315 \ 'temporary_file': a:data.temporary_file,
318 let l:result = a:data.result
319 let l:result.value = l:value
321 " Set the default cwd for this buffer in this call stack.
322 call ale#command#SetCwd(a:buffer, l:result.cwd)
325 if get(l:result, 'result_callback', v:null) isnot v:null
326 call call(l:result.result_callback, [l:value])
329 call ale#command#ResetCwd(a:buffer)
333 function! ale#command#Run(buffer, command, Callback, ...) abort
334 let l:options = get(a:000, 0, {})
337 throw 'Too many arguments!'
340 let l:output_stream = get(l:options, 'output_stream', 'stdout')
342 let l:cwd = get(l:options, 'cwd', v:null)
345 " Default the working directory to whatever it was for the last
346 " command run in the chain.
347 let l:cwd = get(get(s:buffer_data, a:buffer, {}), 'cwd', v:null)
350 let [l:temporary_file, l:command, l:file_created] = ale#command#FormatCommand(
352 \ get(l:options, 'executable', ''),
354 \ get(l:options, 'read_buffer', 0),
355 \ get(l:options, 'input', v:null),
357 \ get(l:options, 'filename_mappings', []),
359 let l:command = ale#job#PrepareCommand(a:buffer, l:command)
360 let l:job_options = {
361 \ 'exit_cb': {job_id, exit_code -> s:ExitCallback(
367 \ 'exit_code': exit_code,
368 \ 'temporary_file': l:temporary_file,
369 \ 'log_output': get(l:options, 'log_output', 1),
370 \ 'result': l:result,
376 if l:output_stream is# 'stdout'
377 let l:job_options.out_cb = function('s:GatherOutput', [l:line_list])
378 elseif l:output_stream is# 'stderr'
379 let l:job_options.err_cb = function('s:GatherOutput', [l:line_list])
380 elseif l:output_stream is# 'both'
381 let l:job_options.out_cb = function('s:GatherOutput', [l:line_list])
382 let l:job_options.err_cb = function('s:GatherOutput', [l:line_list])
385 let l:status = 'failed'
387 if get(g:, 'ale_run_synchronously') == 1
388 if get(g:, 'ale_emulate_job_failure') == 1
391 " Generate a fake job ID for tests.
392 let s:fake_job_id = get(s:, 'fake_job_id', 0) + 1
393 let l:job_id = s:fake_job_id
396 let l:job_id = ale#job#StartWithCmd(l:command, l:job_options)
398 let l:job_id = ale#job#Start(l:command, l:job_options)
402 let l:status = 'started'
403 let l:job_type = getbufvar(a:buffer, 'ale_job_type', 'all')
405 call ale#command#InitData(a:buffer)
406 let s:buffer_data[a:buffer].jobs[l:job_id] = l:job_type
409 if g:ale_history_enabled
410 call ale#history#Add(a:buffer, l:status, l:job_id, l:command)
417 " We'll return this Dictionary. A `result_callback` can be assigned to it
418 " later for capturing the result of a:Callback.
420 " The `_deferred_job_id` is used for both checking the type of object, and
421 " for checking the job ID and status.
423 " The cwd is kept and used as the default value for the next command in
426 " The original command here is used in tests.
428 \ '_deferred_job_id': l:job_id,
429 \ 'executable': get(l:options, 'executable', ''),
431 \ 'command': a:command,
434 if get(g:, 'ale_run_synchronously') == 1 && l:job_id
435 if !exists('g:ale_run_synchronously_callbacks')
436 let g:ale_run_synchronously_callbacks = []
439 if get(g:, 'ale_run_synchronously_emulate_commands', 0)
441 \ g:ale_run_synchronously_callbacks,
442 \ {exit_code, output -> [
443 \ extend(l:line_list, output),
444 \ l:job_options.exit_cb(l:job_id, exit_code),
448 " Run a command synchronously if this test option is set.
449 call extend(l:line_list, systemlist(
450 \ type(l:command) is v:t_list
451 \ ? join(l:command[0:1]) . ' ' . ale#Escape(l:command[2])
455 " Don't capture output when the callbacks aren't set.
456 if !has_key(l:job_options, 'out_cb')
457 \&& !has_key(l:job_options, 'err_cb')
462 \ g:ale_run_synchronously_callbacks,
463 \ {-> l:job_options.exit_cb(l:job_id, v:shell_error)}
471 function! ale#command#IsDeferred(value) abort
472 return type(a:value) is v:t_dict && has_key(a:value, '_deferred_job_id')