From b031531e79d409af3b143a1f40431cd868cd78fb Mon Sep 17 00:00:00 2001 From: w0rp Date: Mon, 28 Aug 2017 19:16:23 +0100 Subject: [PATCH] #869 - Detect the shell dialect from the hashbang for shellcheck --- ale_linters/sh/shell.vim | 14 +--- ale_linters/sh/shellcheck.vim | 30 ++++--- autoload/ale/handlers/sh.vim | 20 +++++ .../test_shellcheck_command_callback.vader | 47 +++++++++++ test/test_shell_detection.vader | 83 +++++++++++++++++++ 5 files changed, 173 insertions(+), 21 deletions(-) create mode 100644 autoload/ale/handlers/sh.vim create mode 100644 test/command_callback/test_shellcheck_command_callback.vader create mode 100644 test/test_shell_detection.vader diff --git a/ale_linters/sh/shell.vim b/ale_linters/sh/shell.vim index 8f5ca3ae..cf5e4e6c 100644 --- a/ale_linters/sh/shell.vim +++ b/ale_linters/sh/shell.vim @@ -17,18 +17,10 @@ if !exists('g:ale_sh_shell_default_shell') endif function! ale_linters#sh#shell#GetExecutable(buffer) abort - let l:banglines = getbufline(a:buffer, 1) + let l:shell_type = ale#handlers#sh#GetShellType(a:buffer) - " Take the shell executable from the hashbang, if we can. - if len(l:banglines) == 1 && l:banglines[0] =~# '^#!' - " Remove options like -e, etc. - let l:line = substitute(l:banglines[0], '--\?[a-zA-Z0-9]\+', '', 'g') - - for l:possible_shell in ['bash', 'tcsh', 'csh', 'zsh', 'sh'] - if l:line =~# l:possible_shell . '\s*$' - return l:possible_shell - endif - endfor + if !empty(l:shell_type) + return l:shell_type endif return ale#Var(a:buffer, 'sh_shell_default_shell') diff --git a/ale_linters/sh/shellcheck.vim b/ale_linters/sh/shellcheck.vim index 5353683d..3a2d33f2 100644 --- a/ale_linters/sh/shellcheck.vim +++ b/ale_linters/sh/shellcheck.vim @@ -19,25 +19,35 @@ function! ale_linters#sh#shellcheck#GetExecutable(buffer) abort return ale#Var(a:buffer, 'sh_shellcheck_executable') endfunction -function! s:GetDialectArgument() abort - if exists('b:is_bash') && b:is_bash - return '-s bash' - elseif exists('b:is_sh') && b:is_sh - return '-s sh' - elseif exists('b:is_kornshell') && b:is_kornshell - return '-s ksh' +function! ale_linters#sh#shellcheck#GetDialectArgument(buffer) abort + let l:shell_type = ale#handlers#sh#GetShellType(a:buffer) + + if !empty(l:shell_type) + return l:shell_type + endif + + " If there's no hashbang, try using Vim's buffer variables. + if get(b:, 'is_bash') + return 'bash' + elseif get(b:, 'is_sh') + return 'sh' + elseif get(b:, 'is_kornshell') + return 'ksh' endif return '' endfunction function! ale_linters#sh#shellcheck#GetCommand(buffer) abort + let l:options = ale#Var(a:buffer, 'sh_shellcheck_options') let l:exclude_option = ale#Var(a:buffer, 'sh_shellcheck_exclusions') + let l:dialect = ale_linters#sh#shellcheck#GetDialectArgument(a:buffer) return ale_linters#sh#shellcheck#GetExecutable(a:buffer) - \ . ' ' . ale#Var(a:buffer, 'sh_shellcheck_options') - \ . ' ' . (!empty(l:exclude_option) ? '-e ' . l:exclude_option : '') - \ . ' ' . s:GetDialectArgument() . ' -f gcc -' + \ . (!empty(l:options) ? ' ' . l:options : '') + \ . (!empty(l:exclude_option) ? ' -e ' . l:exclude_option : '') + \ . (!empty(l:dialect) ? ' -s ' . l:dialect : '') + \ . ' -f gcc -' endfunction call ale#linter#Define('sh', { diff --git a/autoload/ale/handlers/sh.vim b/autoload/ale/handlers/sh.vim new file mode 100644 index 00000000..894879ee --- /dev/null +++ b/autoload/ale/handlers/sh.vim @@ -0,0 +1,20 @@ +" Author: w0rp + +" Get the shell type for a buffer, based on the hashbang line. +function! ale#handlers#sh#GetShellType(buffer) abort + let l:bang_line = get(getbufline(a:buffer, 1), 0, '') + + " Take the shell executable from the hashbang, if we can. + if l:bang_line[:1] is# '#!' + " Remove options like -e, etc. + let l:command = substitute(l:bang_line, ' --\?[a-zA-Z0-9]\+', '', 'g') + + for l:possible_shell in ['bash', 'tcsh', 'csh', 'zsh', 'sh'] + if l:command =~# l:possible_shell . '\s*$' + return l:possible_shell + endif + endfor + endif + + return '' +endfunction diff --git a/test/command_callback/test_shellcheck_command_callback.vader b/test/command_callback/test_shellcheck_command_callback.vader new file mode 100644 index 00000000..0d8fef66 --- /dev/null +++ b/test/command_callback/test_shellcheck_command_callback.vader @@ -0,0 +1,47 @@ +Before: + Save g:ale_sh_shellcheck_exclusions + Save g:ale_sh_shellcheck_executable + Save g:ale_sh_shellcheck_options + + unlet! g:ale_sh_shellcheck_exclusions + unlet! g:ale_sh_shellcheck_executable + unlet! g:ale_sh_shellcheck_options + + runtime ale_linters/sh/shellcheck.vim + +After: + Restore + + unlet! b:ale_sh_shellcheck_exclusions + unlet! b:ale_sh_shellcheck_executable + unlet! b:ale_sh_shellcheck_options + unlet! b:is_bash + + call ale#linter#Reset() + +Execute(The default shellcheck command should be correct): + AssertEqual + \ 'shellcheck -f gcc -', + \ ale_linters#sh#shellcheck#GetCommand(bufnr('')) + +Execute(The shellcheck command should accept options): + let b:ale_sh_shellcheck_options = '--foobar' + + AssertEqual + \ 'shellcheck --foobar -f gcc -', + \ ale_linters#sh#shellcheck#GetCommand(bufnr('')) + +Execute(The shellcheck command should accept options and exclusions): + let b:ale_sh_shellcheck_options = '--foobar' + let b:ale_sh_shellcheck_exclusions = 'foo,bar' + + AssertEqual + \ 'shellcheck --foobar -e foo,bar -f gcc -', + \ ale_linters#sh#shellcheck#GetCommand(bufnr('')) + +Execute(The shellcheck command should include the dialect): + let b:is_bash = 1 + + AssertEqual + \ 'shellcheck -s bash -f gcc -', + \ ale_linters#sh#shellcheck#GetCommand(bufnr('')) diff --git a/test/test_shell_detection.vader b/test/test_shell_detection.vader new file mode 100644 index 00000000..37cf43ce --- /dev/null +++ b/test/test_shell_detection.vader @@ -0,0 +1,83 @@ +Before: + runtime ale_linters/sh/shell.vim + runtime ale_linters/sh/shellcheck.vim + +After: + call ale#linter#Reset() + + unlet! b:is_bash + unlet! b:is_sh + unlet! b:is_kornshell + +Given(A file with a Bash hashbang): + #!/bin/bash + +Execute(/bin/bash should be detected appropriately): + AssertEqual 'bash', ale#handlers#sh#GetShellType(bufnr('')) + AssertEqual 'bash', ale_linters#sh#shell#GetExecutable(bufnr('')) + AssertEqual 'bash', ale_linters#sh#shellcheck#GetDialectArgument(bufnr('')) + +Given(A file with /bin/sh): + #!/usr/bin/env sh -eu --foobar + +Execute(/bin/sh should be detected appropriately): + AssertEqual 'sh', ale#handlers#sh#GetShellType(bufnr('')) + AssertEqual 'sh', ale_linters#sh#shell#GetExecutable(bufnr('')) + AssertEqual 'sh', ale_linters#sh#shellcheck#GetDialectArgument(bufnr('')) + +Given(A file with bash as an argument to env): + #!/usr/bin/env bash + +Execute(/usr/bin/env bash should be detected appropriately): + AssertEqual 'bash', ale#handlers#sh#GetShellType(bufnr('')) + AssertEqual 'bash', ale_linters#sh#shell#GetExecutable(bufnr('')) + AssertEqual 'bash', ale_linters#sh#shellcheck#GetDialectArgument(bufnr('')) + +Given(A file with a tcsh hash bang and arguments): + #!/usr/bin/env tcsh -eu --foobar + +Execute(tcsh should be detected appropriately): + AssertEqual 'tcsh', ale#handlers#sh#GetShellType(bufnr('')) + AssertEqual 'tcsh', ale_linters#sh#shell#GetExecutable(bufnr('')) + AssertEqual 'tcsh', ale_linters#sh#shellcheck#GetDialectArgument(bufnr('')) + +Given(A file with a zsh hash bang and arguments): + #!/usr/bin/env zsh -eu --foobar + +Execute(zsh should be detected appropriately): + AssertEqual 'zsh', ale#handlers#sh#GetShellType(bufnr('')) + AssertEqual 'zsh', ale_linters#sh#shell#GetExecutable(bufnr('')) + AssertEqual 'zsh', ale_linters#sh#shellcheck#GetDialectArgument(bufnr('')) + +Given(A file with a csh hash bang and arguments): + #!/usr/bin/env csh -eu --foobar + +Execute(zsh should be detected appropriately): + AssertEqual 'csh', ale#handlers#sh#GetShellType(bufnr('')) + AssertEqual 'csh', ale_linters#sh#shell#GetExecutable(bufnr('')) + AssertEqual 'csh', ale_linters#sh#shellcheck#GetDialectArgument(bufnr('')) + +Given(A file with a sh hash bang and arguments): + #!/usr/bin/env sh -eu --foobar + +Execute(sh should be detected appropriately): + AssertEqual 'sh', ale#handlers#sh#GetShellType(bufnr('')) + AssertEqual 'sh', ale_linters#sh#shell#GetExecutable(bufnr('')) + AssertEqual 'sh', ale_linters#sh#shellcheck#GetDialectArgument(bufnr('')) + +Given(A file without a hashbang): + +Execute(The bash dialect should be used for shellcheck if b:is_bash is 1): + let b:is_bash = 1 + + AssertEqual 'bash', ale_linters#sh#shellcheck#GetDialectArgument(bufnr('')) + +Execute(The sh dialect should be used for shellcheck if b:is_sh is 1): + let b:is_sh = 1 + + AssertEqual 'sh', ale_linters#sh#shellcheck#GetDialectArgument(bufnr('')) + +Execute(The ksh dialect should be used for shellcheck if b:is_kornshell is 1): + let b:is_kornshell = 1 + + AssertEqual 'ksh', ale_linters#sh#shellcheck#GetDialectArgument(bufnr(''))