# ------------------------------------------------------------------------------------------------- # Copyright (c) 2010-2015 zsh-syntax-highlighting contributors # All rights reserved. # # Redistribution and use in source and binary forms, with or without modification, are permitted # provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this list of conditions # and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, this list of # conditions and the following disclaimer in the documentation and/or other materials provided # with the distribution. # * Neither the name of the zsh-syntax-highlighting contributors nor the names of its contributors # may be used to endorse or promote products derived from this software without specific prior # written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND # FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER # IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT # OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # ------------------------------------------------------------------------------------------------- # -*- mode: zsh; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- # vim: ft=zsh sw=2 ts=2 et # ------------------------------------------------------------------------------------------------- # Define default styles. : ${ZSH_HIGHLIGHT_STYLES[default]:=none} : ${ZSH_HIGHLIGHT_STYLES[unknown-token]:=fg=red,bold} : ${ZSH_HIGHLIGHT_STYLES[reserved-word]:=fg=yellow} : ${ZSH_HIGHLIGHT_STYLES[alias]:=fg=green} : ${ZSH_HIGHLIGHT_STYLES[suffix-alias]:=fg=green,underline} : ${ZSH_HIGHLIGHT_STYLES[builtin]:=fg=green} : ${ZSH_HIGHLIGHT_STYLES[function]:=fg=green} : ${ZSH_HIGHLIGHT_STYLES[command]:=fg=green} : ${ZSH_HIGHLIGHT_STYLES[precommand]:=fg=green,underline} : ${ZSH_HIGHLIGHT_STYLES[commandseparator]:=none} : ${ZSH_HIGHLIGHT_STYLES[hashed-command]:=fg=green} : ${ZSH_HIGHLIGHT_STYLES[path]:=underline} : ${ZSH_HIGHLIGHT_STYLES[path_prefix]:=underline} : ${ZSH_HIGHLIGHT_STYLES[globbing]:=fg=blue} : ${ZSH_HIGHLIGHT_STYLES[history-expansion]:=fg=blue} : ${ZSH_HIGHLIGHT_STYLES[single-hyphen-option]:=none} : ${ZSH_HIGHLIGHT_STYLES[double-hyphen-option]:=none} : ${ZSH_HIGHLIGHT_STYLES[back-quoted-argument]:=none} : ${ZSH_HIGHLIGHT_STYLES[single-quoted-argument]:=fg=yellow} : ${ZSH_HIGHLIGHT_STYLES[double-quoted-argument]:=fg=yellow} : ${ZSH_HIGHLIGHT_STYLES[dollar-quoted-argument]:=fg=yellow} : ${ZSH_HIGHLIGHT_STYLES[dollar-double-quoted-argument]:=fg=cyan} : ${ZSH_HIGHLIGHT_STYLES[back-double-quoted-argument]:=fg=cyan} : ${ZSH_HIGHLIGHT_STYLES[back-dollar-quoted-argument]:=fg=cyan} : ${ZSH_HIGHLIGHT_STYLES[assign]:=none} : ${ZSH_HIGHLIGHT_STYLES[redirection]:=none} : ${ZSH_HIGHLIGHT_STYLES[comment]:=fg=black,bold} # Whether the highlighter should be called or not. _zsh_highlight_main_highlighter_predicate() { # accept-* may trigger removal of path_prefix highlighting [[ $WIDGET == accept-* ]] || _zsh_highlight_buffer_modified } # Helper to deal with tokens crossing line boundaries. _zsh_highlight_main_add_region_highlight() { integer start=$1 end=$2 local style=$3 # The calculation was relative to $PREBUFFER$BUFFER, but region_highlight is # relative to $BUFFER. (( start -= $#PREBUFFER )) (( end -= $#PREBUFFER )) (( end < 0 )) && return # having end<0 would be a bug (( start < 0 )) && start=0 # having start<0 is normal with e.g. multiline strings region_highlight+=("$start $end $style") } # Main syntax highlighting function. _zsh_highlight_main_highlighter() { if [[ -o interactive_comments ]]; then local interactive_comments= # set to empty fi emulate -L zsh setopt localoptions extendedglob bareglobqual local start_pos=0 end_pos highlight_glob=true arg style typeset -a ZSH_HIGHLIGHT_TOKENS_COMMANDSEPARATOR typeset -a ZSH_HIGHLIGHT_TOKENS_PRECOMMANDS typeset -a ZSH_HIGHLIGHT_TOKENS_CONTROL_FLOW local buf="$PREBUFFER$BUFFER" region_highlight=() ZSH_HIGHLIGHT_TOKENS_COMMANDSEPARATOR=( '|' '||' ';' '&' '&&' ) ZSH_HIGHLIGHT_TOKENS_PRECOMMANDS=( 'builtin' 'command' 'exec' 'nocorrect' 'noglob' ) # Tokens that, at (naively-determined) "command position", are followed by # a de jure command position. All of these are reserved words. ZSH_HIGHLIGHT_TOKENS_CONTROL_FLOW=( $'\x7b' # block $'\x28' # subshell '()' # anonymous function 'while' 'until' 'if' 'then' 'elif' 'else' 'do' 'time' 'coproc' '!' # reserved word; unrelated to $histchars[1] ) # State machine # # The states are: # - :start: Command word # - :sudo_opt: A leading-dash option to sudo (such as "-u" or "-i") # - :sudo_arg: The argument to a sudo leading-dash option that takes one, # when given as a separate word; i.e., "foo" in "-u foo" (two # words) but not in "-ufoo" (one word). # - :regular: "Not a command word". # Mainly used to detect premature termination of commands. # # When the kind of a word is not yet known, $this_word / $next_word may contain # multiple states. For example, after "sudo -i", the next word may be either # another --flag or a command name, hence the state would include both :start: # and :sudo_opt:. # # The tokens are always added with both leading and trailing colons to serve as # word delimiters (an improvised array); [[ $x == *:foo:* ]] and x=${x//:foo:/} # will DTRT regardless of how many elements or repetitions $x has.. # # Handling of redirections: upon seeing a redirection token, we must stall # the current state --- that is, the value of $this_word --- for two iterations # (one for the redirection operator, one for the word following it representing # the redirection target). Therefore, we set $in_redirection to 2 upon seeing a # redirection operator, decrement it each iteration, and stall the current state # when it is non-zero. Thus, upon reaching the next word (the one that follows # the redirection operator and target), $this_word will still contain values # appropriate for the word immediately following the word that preceded the # redirection operator. # # The "the previous word was a redirection operator" state is not communicated # to the next iteration via $next_word/$this_word as usual, but via # $in_redirection. The value of $next_word from the iteration that processed # the operator is discarded. # local this_word=':start:' next_word integer in_redirection for arg in ${interactive_comments-${(z)buf}} \ ${interactive_comments+${(zZ+c+)buf}}; do if (( in_redirection )); then (( --in_redirection )) fi if (( in_redirection == 0 )); then next_word=':regular:' fi # $already_added is set to 1 to disable adding an entry to region_highlight # for this iteration. Currently, that is done for "" and $'' strings, # which add the entry early so escape sequences within the string override # the string's color. integer already_added=0 local style_override="" if [[ $this_word == *':start:'* ]] && [[ $arg = 'noglob' ]]; then highlight_glob=false fi # advance $start_pos, skipping over whitespace in $buf. if [[ $arg == ';' ]] ; then # We're looking for either a semicolon or a newline, whichever comes # first. Both of these are rendered as a ";" (SEPER) by the ${(z)..} # flag. # # We can't use the (Z+n+) flag because that elides the end-of-command # token altogether, so 'echo foo\necho bar' (two commands) becomes # indistinguishable from 'echo foo echo bar' (one command with three # words for arguments). local needle=$'[;\n]' integer offset=${${buf[start_pos+1,-1]}[(i)$needle]} (( start_pos += offset - 1 )) (( end_pos = start_pos + $#arg )) else ((start_pos+=${#buf[$start_pos+1,-1]}-${#${buf[$start_pos+1,-1]##([[:space:]]|\\[[:space:]])#}})) ((end_pos=$start_pos+${#arg})) fi if [[ ${interactive_comments+'set'} && $arg[1] == $histchars[3] ]]; then if [[ $this_word == *(':regular:'|':start:')* ]]; then style=$ZSH_HIGHLIGHT_STYLES[comment] else style=$ZSH_HIGHLIGHT_STYLES[unknown-token] # prematurely terminated fi _zsh_highlight_main_add_region_highlight $start_pos $end_pos $style already_added=1 continue fi # Parse the sudo command line if (( ! in_redirection )); then if [[ $this_word == *':sudo_opt:'* ]]; then case "$arg" in # Flag that requires an argument '-'[Cgprtu]) this_word=${this_word//:start:/}; next_word=':sudo_arg:';; # This prevents misbehavior with sudo -u -otherargument '-'*) this_word=${this_word//:start:/}; next_word+=':start:'; next_word+=':sudo_opt:';; *) ;; esac elif [[ $this_word == *':sudo_arg:'* ]]; then next_word+=':sudo_opt:' next_word+=':start:' fi fi if [[ $this_word == *':start:'* ]] && (( in_redirection == 0 )); then # $arg is the command word if [[ -n ${(M)ZSH_HIGHLIGHT_TOKENS_PRECOMMANDS:#"$arg"} ]]; then style=$ZSH_HIGHLIGHT_STYLES[precommand] elif [[ "$arg" = "sudo" ]]; then style=$ZSH_HIGHLIGHT_STYLES[precommand] next_word=${next_word//:regular:/} next_word+=':sudo_opt:' next_word+=':start:' else _zsh_highlight_main_highlighter_expand_path $arg local expanded_arg="$REPLY" local res="$(LC_ALL=C builtin type -w -- ${expanded_arg} 2>/dev/null)" case $res in *': reserved') style=$ZSH_HIGHLIGHT_STYLES[reserved-word];; *': suffix alias') style=$ZSH_HIGHLIGHT_STYLES[suffix-alias] ;; *': alias') style=$ZSH_HIGHLIGHT_STYLES[alias] local aliased_command="${"$(alias -- $arg)"#*=}" [[ -n ${(M)ZSH_HIGHLIGHT_TOKENS_PRECOMMANDS:#"$aliased_command"} && -z ${(M)ZSH_HIGHLIGHT_TOKENS_PRECOMMANDS:#"$arg"} ]] && ZSH_HIGHLIGHT_TOKENS_PRECOMMANDS+=($arg) ;; *': builtin') style=$ZSH_HIGHLIGHT_STYLES[builtin];; *': function') style=$ZSH_HIGHLIGHT_STYLES[function];; *': command') style=$ZSH_HIGHLIGHT_STYLES[command];; *': hashed') style=$ZSH_HIGHLIGHT_STYLES[hashed-command];; *) if _zsh_highlight_main_highlighter_check_assign; then style=$ZSH_HIGHLIGHT_STYLES[assign] if [[ $arg[-1] != '(' ]]; then # assignment to a scalar parameter. # (For array assignments, the command doesn't start until the ")" token.) next_word+=':start:' fi elif [[ $arg[0,1] == $histchars[0,1] || $arg[0,1] == $histchars[2,2] ]]; then style=$ZSH_HIGHLIGHT_STYLES[history-expansion] elif [[ -n ${(M)ZSH_HIGHLIGHT_TOKENS_COMMANDSEPARATOR:#"$arg"} ]] && [[ $this_word == *':regular:'* ]]; then # This highlights empty commands (semicolon follows nothing) as an error. # Zsh accepts them, though. style=$ZSH_HIGHLIGHT_STYLES[commandseparator] elif [[ $arg[1] == '<' || $arg[1] == '>' ]]; then style=$ZSH_HIGHLIGHT_STYLES[redirection] (( in_redirection=2 )) elif [[ $arg[1,2] == '((' ]]; then # Arithmetic evaluation. # # Note: prior to zsh-5.1.1-52-g4bed2cf (workers/36669), the ${(z)...} # splitter would only output the '((' token if the matching '))' had # been typed. Therefore, under those versions of zsh, BUFFER="(( 42" # would be highlighted as an error until the matching "))" are typed. # # We highlight just the opening parentheses, as a reserved word; this # is how [[ ... ]] is highlighted, too. style=$ZSH_HIGHLIGHT_STYLES[reserved-word] _zsh_highlight_main_add_region_highlight $start_pos $((start_pos + 2)) $style already_added=1 elif [[ $arg == '()' ]]; then # anonymous function style=$ZSH_HIGHLIGHT_STYLES[reserved-word] else if _zsh_highlight_main_highlighter_check_path; then style=$ZSH_HIGHLIGHT_STYLES[path] else style=$ZSH_HIGHLIGHT_STYLES[unknown-token] fi fi ;; esac fi else # $arg is a non-command word case $arg in '--'*) style=$ZSH_HIGHLIGHT_STYLES[double-hyphen-option];; '-'*) style=$ZSH_HIGHLIGHT_STYLES[single-hyphen-option];; "'"*) style=$ZSH_HIGHLIGHT_STYLES[single-quoted-argument];; '"'*) style=$ZSH_HIGHLIGHT_STYLES[double-quoted-argument] _zsh_highlight_main_add_region_highlight $start_pos $end_pos $style _zsh_highlight_main_highlighter_highlight_string already_added=1 ;; \$\'*) style=$ZSH_HIGHLIGHT_STYLES[dollar-quoted-argument] _zsh_highlight_main_add_region_highlight $start_pos $end_pos $style _zsh_highlight_main_highlighter_highlight_dollar_string already_added=1 ;; '`'*) style=$ZSH_HIGHLIGHT_STYLES[back-quoted-argument];; [*?]*|*[^\\][*?]*) $highlight_glob && style=$ZSH_HIGHLIGHT_STYLES[globbing] || style=$ZSH_HIGHLIGHT_STYLES[default];; *) if false; then elif [[ $arg[0,1] = $histchars[0,1] ]]; then style=$ZSH_HIGHLIGHT_STYLES[history-expansion] elif [[ -n ${(M)ZSH_HIGHLIGHT_TOKENS_COMMANDSEPARATOR:#"$arg"} ]]; then if [[ $this_word == *':regular:'* ]]; then style=$ZSH_HIGHLIGHT_STYLES[commandseparator] else style=$ZSH_HIGHLIGHT_STYLES[unknown-token] fi elif [[ $arg[1] == '<' || $arg[1] == '>' ]]; then style=$ZSH_HIGHLIGHT_STYLES[redirection] (( in_redirection=2 )) else if _zsh_highlight_main_highlighter_check_path; then style=$ZSH_HIGHLIGHT_STYLES[path] else style=$ZSH_HIGHLIGHT_STYLES[default] fi fi ;; esac fi # if a style_override was set (eg in _zsh_highlight_main_highlighter_check_path), use it [[ -n $style_override ]] && style=$ZSH_HIGHLIGHT_STYLES[$style_override] (( already_added )) || _zsh_highlight_main_add_region_highlight $start_pos $end_pos $style if [[ -n ${(M)ZSH_HIGHLIGHT_TOKENS_COMMANDSEPARATOR:#"$arg"} ]]; then next_word=':start:' elif [[ -n ${(M)ZSH_HIGHLIGHT_TOKENS_CONTROL_FLOW:#"$arg"} && $this_word == *':start:' ]] || [[ -n ${(M)ZSH_HIGHLIGHT_TOKENS_PRECOMMANDS:#"$arg"} && $this_word == *':start:' ]]; then next_word=':start:' fi [[ -n ${(M)ZSH_HIGHLIGHT_TOKENS_COMMANDSEPARATOR:#"$arg"} ]] && highlight_glob=true start_pos=$end_pos (( in_redirection == 0 )) && this_word=$next_word done } # Check if $arg is variable assignment _zsh_highlight_main_highlighter_check_assign() { setopt localoptions extended_glob [[ $arg == [[:alpha:]_][[:alnum:]_]#(|\[*\])(|[+])=* ]] } # Check if $arg is a path. _zsh_highlight_main_highlighter_check_path() { _zsh_highlight_main_highlighter_expand_path $arg; local expanded_path="$REPLY" [[ -z $expanded_path ]] && return 1 [[ -e $expanded_path ]] && return 0 # Search the path in CDPATH local cdpath_dir for cdpath_dir in $cdpath ; do [[ -e "$cdpath_dir/$expanded_path" ]] && return 0 done # If dirname($arg) doesn't exist, neither does $arg. [[ ! -e ${expanded_path:h} ]] && return 1 # If this word ends the buffer, check if it's the prefix of a valid path. if [[ ${BUFFER[1]} != "-" && ${#BUFFER} == $end_pos ]] && [[ $WIDGET != accept-* ]]; then local -a tmp tmp=( ${expanded_path}*(N) ) (( $#tmp > 0 )) && style_override=path_prefix && return 0 fi # It's not a path. return 1 } # Highlight special chars inside double-quoted strings _zsh_highlight_main_highlighter_highlight_string() { setopt localoptions noksharrays local i j k style # Starting quote is at 1, so start parsing at offset 2 in the string. for (( i = 2 ; i < end_pos - start_pos ; i += 1 )) ; do (( j = i + start_pos - 1 )) (( k = j + 1 )) case "$arg[$i]" in '$' ) style=$ZSH_HIGHLIGHT_STYLES[dollar-double-quoted-argument] # Look for an alphanumeric parameter name. if [[ ${arg:$i} =~ ^([A-Za-z_][A-Za-z0-9_]*|[0-9]+) ]] ; then (( k += $#MATCH )) # highlight the parameter name (( i += $#MATCH )) # skip past it else continue fi ;; "\\") style=$ZSH_HIGHLIGHT_STYLES[back-double-quoted-argument] if [[ \\\`\"\$ == *$arg[$i+1]* ]]; then (( k += 1 )) # Color following char too. (( i += 1 )) # Skip parsing the escaped char. else continue fi ;; *) continue ;; esac _zsh_highlight_main_add_region_highlight $j $k $style done } # Highlight special chars inside dollar-quoted strings _zsh_highlight_main_highlighter_highlight_dollar_string() { setopt localoptions noksharrays local i j k style local AA integer c # Starting dollar-quote is at 1:2, so start parsing at offset 3 in the string. for (( i = 3 ; i < end_pos - start_pos ; i += 1 )) ; do (( j = i + start_pos - 1 )) (( k = j + 1 )) case "$arg[$i]" in "\\") style=$ZSH_HIGHLIGHT_STYLES[back-dollar-quoted-argument] for (( c = i + 1 ; c <= end_pos - start_pos ; c += 1 )); do [[ "$arg[$c]" != ([0-9xXuUa-fA-F]) ]] && break done AA=$arg[$i+1,$c-1] # Matching for HEX and OCT values like \0xA6, \xA6 or \012 if [[ "$AA" =~ "^(x|X)[0-9a-fA-F]{1,2}" || "$AA" =~ "^[0-7]{1,3}" || "$AA" =~ "^u[0-9a-fA-F]{1,4}" || "$AA" =~ "^U[0-9a-fA-F]{1,8}" ]]; then (( k += $#MATCH )) (( i += $#MATCH )) else if (( $#arg > $i+1 )) && [[ $arg[$i+1] == [xXuU] ]]; then # \x not followed by hex digits is probably an error style=$ZSH_HIGHLIGHT_STYLES[unknown-token] fi (( k += 1 )) # Color following char too. (( i += 1 )) # Skip parsing the escaped char. fi ;; *) continue ;; esac _zsh_highlight_main_add_region_highlight $j $k $style done } # Called with a single positional argument. # Perform filename expansion (tilde expansion) on the argument and set $REPLY to the expanded value. # # Does not perform filename generation (globbing). _zsh_highlight_main_highlighter_expand_path() { (( $# == 1 )) || echo "zsh-syntax-highlighting: BUG: _zsh_highlight_main_highlighter_expand_path: called without argument" >&2 # The $~1 syntax normally performs filename generation, but not when it's on the right-hand side of ${x:=y}. setopt localoptions nonomatch unset REPLY : ${REPLY:=${(Q)~1}} }