vis

a vi-like editor based on Plan 9's structural regular expressions

git clone https://9o.is/git/vis.git

bash.lua

(5762B)


      1 -- Copyright 2006-2025 Mitchell. See LICENSE.
      2 -- Shell LPeg lexer.
      3 
      4 local lexer = lexer
      5 local P, S, B = lpeg.P, lpeg.S, lpeg.B
      6 
      7 local lex = lexer.new(...)
      8 
      9 -- Keywords.
     10 lex:add_rule('keyword', lex:tag(lexer.KEYWORD, lex:word_match(lexer.KEYWORD)))
     11 
     12 -- Builtins.
     13 lex:add_rule('builtin',
     14 	lex:tag(lexer.FUNCTION_BUILTIN, lex:word_match(lexer.FUNCTION_BUILTIN)) * -P('='))
     15 
     16 -- Variable assignment.
     17 local assign = lex:tag(lexer.VARIABLE, lexer.word) * lex:tag(lexer.OPERATOR, '=')
     18 lex:add_rule('assign', lexer.starts_line(assign, true))
     19 
     20 -- Identifiers.
     21 lex:add_rule('identifier', lex:tag(lexer.IDENTIFIER, lexer.word))
     22 
     23 -- Strings.
     24 local sq_str = -B('\\') * lexer.range("'", false, false)
     25 local dq_str = -B('\\') * lexer.range('"')
     26 local heredoc = '<<' * P(function(input, index)
     27 	local _, e, minus, _, delimiter = input:find('^(%-?)%s*(["\']?)([%w_]+)%2[^\n]*[\n\r\f;]+', index)
     28 	if not delimiter then return nil end
     29 	-- If the starting delimiter of a here-doc begins with "-", then spaces are allowed to come
     30 	-- before the closing delimiter.
     31 	_, e =
     32 		input:find((minus == '' and '[\n\r\f]+' or '[\n\r\f]+[ \t]*') .. delimiter .. '%f[^%w_]', e)
     33 	return e and e + 1 or #input + 1
     34 end)
     35 local ex_str = -B('\\') * '`'
     36 lex:add_rule('string',
     37 	lex:tag(lexer.STRING, sq_str + dq_str + heredoc) + lex:tag(lexer.EMBEDDED, ex_str))
     38 
     39 -- Comments.
     40 lex:add_rule('comment', lex:tag(lexer.COMMENT, -B('\\') * lexer.to_eol('#')))
     41 
     42 -- Numbers.
     43 lex:add_rule('number', lex:tag(lexer.NUMBER, lexer.number))
     44 
     45 -- Variables.
     46 local builtin_var = lex:tag(lexer.OPERATOR, '$' * P('{')^-1) * lex:tag(lexer.VARIABLE_BUILTIN,
     47 	lex:word_match(lexer.VARIABLE_BUILTIN) + S('!#?*@$-') * -lexer.alnum + lexer.digit^1)
     48 local var_ref = lex:tag(lexer.OPERATOR, '$' * ('{' * S('!#')^-1)^-1) *
     49 	lex:tag(lexer.VARIABLE, lexer.word)
     50 local patt_expansion = lex:tag(lexer.DEFAULT, '/#' + '#' * P('#')^-1)
     51 lex:add_rule('variable', builtin_var + var_ref * patt_expansion^-1)
     52 
     53 -- Operators.
     54 local op = S('!<>&|;$()[]{}') + lpeg.B(lexer.space) * S('.:') * #lexer.space
     55 
     56 local function in_expr(constructs)
     57 	return P(function(input, index)
     58 		local line = input:sub(1, index):match('[^\r\n]*$')
     59 		for k, v in pairs(constructs) do
     60 			local s = line:find(k, 1, true)
     61 			if not s then goto continue end
     62 			local e = line:find(v, 1, true)
     63 			if not e or e < s then return true end
     64 			::continue::
     65 		end
     66 		return nil
     67 	end)
     68 end
     69 
     70 local file_op = '-' * (S('abcdefghkprstuwxGLNOS') + 'ef' + 'nt' + 'ot')
     71 local shell_op = '-o'
     72 local var_op = '-' * S('vR')
     73 local string_op = '-' * S('zn') + S('!=')^-1 * '=' + S('<>')
     74 local num_op = '-' * lexer.word_match('eq ne lt le gt ge')
     75 local in_cond_expr = in_expr{['[[ '] = ' ]]', ['[ '] = ' ]'}
     76 local conditional_op = (num_op + file_op + shell_op + var_op + string_op) * #lexer.space *
     77 	in_cond_expr
     78 
     79 local in_arith_expr = in_expr{['(('] = '))'}
     80 local arith_op = (S('+!~*/%<>=&^|?:,') + '--' + '-' * #S(' \t')) * in_arith_expr
     81 
     82 -- TODO: performance is terrible on large files.
     83 -- lex:add_rule('operator', lex:tag(lexer.OPERATOR, op + conditional_op + arith_op))
     84 lex:add_rule('operator', lex:tag(lexer.OPERATOR, op))
     85 
     86 -- Flags/options.
     87 lex:add_rule('flag', lex:tag(lexer.DEFAULT, '-' * P('-')^-1 * lexer.word * ('-' * lexer.word)^0))
     88 
     89 -- Fold points.
     90 lex:add_fold_point(lexer.KEYWORD, 'if', 'fi')
     91 lex:add_fold_point(lexer.KEYWORD, 'case', 'esac')
     92 lex:add_fold_point(lexer.KEYWORD, 'do', 'done')
     93 lex:add_fold_point(lexer.OPERATOR, '{', '}')
     94 
     95 -- Word lists.
     96 lex:set_word_list(lexer.KEYWORD, {
     97 	'if', 'then', 'elif', 'else', 'fi', 'time', 'for', 'in', 'until', 'while', 'do', 'done', 'case',
     98 	'esac', 'coproc', 'select', 'function'
     99 })
    100 
    101 lex:set_word_list(lexer.FUNCTION_BUILTIN, {
    102 	-- Shell built-ins.
    103 	'break', 'cd', 'continue', 'eval', 'exec', 'exit', 'export', 'getopts', 'hash', 'pwd', 'readonly',
    104 	'return', 'shift', 'test', 'times', 'trap', 'umask', 'unset',
    105 	-- Bash built-ins.
    106 	'alias', 'bind', 'builtin', 'caller', 'command', 'declare', 'echo', 'enable', 'help', 'let',
    107 	'local', 'logout', 'mapfile', 'printf', 'read', 'readarray', 'source', 'type', 'typeset',
    108 	'ulimit', 'unalias', --
    109 	'set', 'shopt', -- shell behavior
    110 	'dirs', 'popd', 'pushd', -- directory stack
    111 	'bg', 'fg', 'jobs', 'kill', 'wait', 'disown', 'suspend', -- job control
    112 	'fc', 'history' -- history
    113 })
    114 
    115 lex:set_word_list(lexer.VARIABLE_BUILTIN, {
    116 	-- Shell built-ins.
    117 	'CDPATH', 'HOME', 'IFS', 'MAIL', 'MAILPATH', 'OPTARG', 'OPTIND', 'PATH', 'PS1', 'PS2',
    118 	-- Bash built-ins.
    119 	'BASH', 'BASHOPTS', 'BASHPID', 'BASH_ALIASES', 'BASH_ARGC', 'BASH_ARGV', 'BASH_ARGV0',
    120 	'BASH_CMDS', 'BASH_COMMAND', 'BASH_COMPAT', 'BASH_ENV', 'BASH_EXECUTION_STRING', 'BASH_LINENO',
    121 	'BASH_LOADABLES_PATH', 'BASH_REMATCH', 'BASH_SOURCE', 'BASH_SUBSHELL', 'BASH_VERSINFO',
    122 	'BASH_VERSION', 'BASH_XTRACEFD', 'CHILD_MAX', 'COLUMNS', 'COMP_CWORD', 'COMP_LINE', 'COMP_POINT',
    123 	'COMP_TYPE', 'COMP_KEY', 'COMP_WORDBREAKS', 'COMP_WORDS', 'COMP_REPLY', 'COPROC', 'DIRSTACK',
    124 	'EMACS', 'ENV', 'EPOCHREALTIME', 'EPOCHSECONDS', 'EUID', 'EXECIGNORE', 'FCEDIT', 'FIGNORE',
    125 	'FUNCNAME', 'FUNCNEST', 'GLOBIGNORE', 'GROUPS', 'histchars', 'HISTCMD', 'HISTCONTROL', 'HISTFILE',
    126 	'HISTFILESIZE', 'HISTIGNORE', 'HISTSIZE', 'HISTTIMEFORMAT', 'HOSTFILE', 'HOSTNAME', 'HOSTTYPE',
    127 	'IGNOREEOF', 'INPUTRC', 'INSIDE_EMACS', 'LANG', 'LC_ALL', 'LC_COLLATE', 'LC_CTYPE', 'LC_MESSAGES',
    128 	'LC_NUMERIC', 'LC_TIME', 'LINENO', 'LINES', 'MACHTYPE', 'MAILCHECK', 'MAPFILE', 'OLDPWD',
    129 	'OPTERR', 'OSTYPE', 'PIPESTATUS', 'POSIXLY_CORRECT', 'PPID', 'PROMPT_COMMAND', 'PROMPT_DIRTRIM',
    130 	'PSO', 'PS3', 'PS4', 'PWD', 'RANDOM', 'READLINE_LINE', 'READLINE_MARK', 'READLINE_POINT', 'REPLY',
    131 	'SECONDS', 'SHELL', 'SHELLOPTS', 'SHLVL', 'SRANDOM', 'TIMEFORMAT', 'TMOUT', 'TMPDIR', 'UID',
    132 	-- Job control.
    133 	'auto_resume'
    134 })
    135 
    136 lexer.property['scintillua.comment'] = '#'
    137 
    138 return lex