/** \file highlight.c Functions for syntax highlighting */ #include "config.h" #include #include #include #include #include #include #include #include #include #include "fallback.h" #include "util.h" #include "wutil.h" #include "highlight.h" #include "tokenizer.h" #include "proc.h" #include "parser.h" #include "parse_util.h" #include "parser_keywords.h" #include "builtin.h" #include "function.h" #include "env.h" #include "expand.h" #include "sanity.h" #include "common.h" #include "complete.h" #include "output.h" #include "halloc.h" #include "halloc_util.h" #include "wildcard.h" #include "path.h" /** Number of elements in the highlight_var array */ #define VAR_COUNT ( sizeof(highlight_var)/sizeof(wchar_t *) ) static void highlight_universal_internal( const wchar_t * buff, int *color, int pos ); /** The environment variables used to specify the color of different tokens. */ static const wchar_t *highlight_var[] = { L"fish_color_normal", L"fish_color_error", L"fish_color_command", L"fish_color_end", L"fish_color_param", L"fish_color_comment", L"fish_color_match", L"fish_color_search_match", L"fish_color_operator", L"fish_color_escape", L"fish_color_quote", L"fish_color_redirection", L"fish_color_valid_path" } ; /** Tests if the specified string is the prefix of any valid path in the system. \return zero it this is not a valid prefix, non-zero otherwise */ // PCA DOES_IO static int is_potential_path( const wcstring &cpath ) { ASSERT_IS_BACKGROUND_THREAD(); const wchar_t *unescaped, *in; wcstring cleaned_path; int has_magic = 0; int res = 0; wcstring path(cpath); expand_tilde(path); unescape_string(path, 1); unescaped = path.c_str(); // debug( 1, L"%ls -> %ls ->%ls", path, tilde, unescaped ); for( in = unescaped; *in; in++ ) { switch( *in ) { case PROCESS_EXPAND: case VARIABLE_EXPAND: case VARIABLE_EXPAND_SINGLE: case BRACKET_BEGIN: case BRACKET_END: case BRACKET_SEP: case ANY_CHAR: case ANY_STRING: case ANY_STRING_RECURSIVE: { has_magic = 1; break; } case INTERNAL_SEPARATOR: { break; } default: { cleaned_path += *in; break; } } } if( !has_magic && cleaned_path.length() ) { int must_be_dir = 0; DIR *dir; must_be_dir = cleaned_path[cleaned_path.length()-1] == L'/'; if( must_be_dir ) { dir = wopendir( cleaned_path.c_str() ); res = !!dir; if( dir ) { closedir( dir ); } } else { wcstring dir_name = wdirname(cleaned_path); wcstring base_name = wbasename(cleaned_path); if( dir_name == L"/" && base_name == L"/" ) { res = 1; } else if( (dir = wopendir( dir_name.c_str() )) ) { wcstring ent; while (wreaddir(dir, ent)) { if( wcsncmp( base_name.c_str(), base_name.c_str(), base_name.length() ) == 0 ) { res = 1; break; } } closedir( dir ); } } } return res; } int highlight_get_color( int highlight ) { size_t i; int idx=0; int result = 0; if( highlight < 0 ) return FISH_COLOR_NORMAL; if( highlight >= (1< %d -> %ls", highlight, idx, val ); if( val == 0 ) val = env_get( highlight_var[0]); if( val ) result = output_color_code( val ); if( highlight & HIGHLIGHT_VALID_PATH ) { wchar_t *val2 = env_get( L"fish_color_valid_path" ); int result2 = output_color_code( val2 ); if( result == FISH_COLOR_NORMAL ) result = result2; else { if( result2 & FISH_COLOR_BOLD ) result |= FISH_COLOR_BOLD; if( result2 & FISH_COLOR_UNDERLINE ) result |= FISH_COLOR_UNDERLINE; } } return result; } /** Highligt operators (such as $, ~, %, as well as escaped characters. */ static void highlight_param( const wchar_t * buff, int *color, int pos, array_list_t *error ) { int mode = 0; int in_pos, len = wcslen( buff ); int bracket_count=0; wchar_t c; int normal_status = *color; for( in_pos=0; in_pos^ \\#;|&", buff[in_pos] ) ) { color[start_pos]=HIGHLIGHT_ESCAPE; color[in_pos+1]=normal_status; } else if( wcschr( L"c", buff[in_pos] ) ) { color[start_pos]=HIGHLIGHT_ESCAPE; color[in_pos+2]=normal_status; } else if( wcschr( L"uUxX01234567", buff[in_pos] ) ) { int i; long long res=0; int chars=2; int base=16; int byte = 0; wchar_t max_val = ASCII_MAX; switch( buff[in_pos] ) { case L'u': { chars=4; max_val = UCS2_MAX; break; } case L'U': { chars=8; max_val = WCHAR_MAX; break; } case L'x': { break; } case L'X': { byte=1; max_val = BYTE_MAX; break; } default: { base=8; chars=3; in_pos--; break; } } for( i=0; i= EXPAND_RESERVED && *str <= EXPAND_RESERVED_END ) { return 1; } str++; } return 0; } // PCA DOES_IO void tokenize( const wchar_t * const buff, int * const color, const int pos, array_list_t *error, void *context, const env_vars &vars) { ASSERT_IS_BACKGROUND_THREAD(); int had_cmd=0; int i; wchar_t *last_cmd=0; int len; wchar_t *cmd=0; int accept_switches = 1; int use_function = 1; int use_command = 1; int use_builtin = 1; CHECK( buff, ); CHECK( color, ); len = wcslen(buff); if( !len ) return; for( i=0; buff[i] != 0; i++ ) color[i] = -1; tokenizer tok; for( tok_init( &tok, buff, TOK_SHOW_COMMENTS ); tok_has_next( &tok ); tok_next( &tok ) ) { int last_type = tok_last_type( &tok ); int prev_argc=0; switch( last_type ) { case TOK_STRING: { if( had_cmd ) { /*Parameter */ wchar_t *param = tok_last( &tok ); if( param[0] == L'-' ) { if (wcscmp( param, L"--" ) == 0 ) { accept_switches = 0; color[ tok_get_pos( &tok ) ] = HIGHLIGHT_PARAM; } else if( accept_switches ) { if( complete_is_valid_option( last_cmd, param, error ) ) color[ tok_get_pos( &tok ) ] = HIGHLIGHT_PARAM; else color[ tok_get_pos( &tok ) ] = HIGHLIGHT_ERROR; } else { color[ tok_get_pos( &tok ) ] = HIGHLIGHT_PARAM; } } else { color[ tok_get_pos( &tok ) ] = HIGHLIGHT_PARAM; } if( cmd && (wcscmp( cmd, L"cd" ) == 0) ) { wchar_t *dir = expand_one( context, wcsdup(tok_last( &tok )), EXPAND_SKIP_CMDSUBST ); if( dir ) { int is_long_help = wcsncmp(dir,L"--help", wcslen(dir) ); int is_short_help = wcsncmp(dir,L"-h", wcslen(dir) ); if( !is_long_help && !is_short_help && !path_get_cdpath( context, dir ) ) { color[ tok_get_pos( &tok ) ] = HIGHLIGHT_ERROR; } } } highlight_param( param, &color[tok_get_pos( &tok )], pos-tok_get_pos( &tok ), error ); } else { prev_argc=0; /* Command. First check that the command actually exists. */ cmd = expand_one( context, wcsdup(tok_last( &tok )), EXPAND_SKIP_CMDSUBST | EXPAND_SKIP_VARIABLES); if( (cmd == 0) || has_expand_reserved( cmd ) ) { color[ tok_get_pos( &tok ) ] = HIGHLIGHT_ERROR; } else { int is_cmd = 0; int is_subcommand = 0; int mark = tok_get_pos( &tok ); color[ tok_get_pos( &tok ) ] = HIGHLIGHT_COMMAND; if( parser_keywords_is_subcommand( cmd ) ) { int sw; if( wcscmp( cmd, L"builtin" )==0) { use_function = 0; use_command = 0; use_builtin = 1; } else if( wcscmp( cmd, L"command" )==0) { use_command = 1; use_function = 0; use_builtin = 0; } tok_next( &tok ); sw = parser_keywords_is_switch( tok_last( &tok ) ); if( !parser_keywords_is_block( cmd ) && sw == ARG_SWITCH ) { /* The 'builtin' and 'command' builtins are normally followed by another command, but if they are invoked with a switch, they aren't. */ use_command = 1; use_function = 1; use_builtin = 2; } else { if( sw == ARG_SKIP ) { color[ tok_get_pos( &tok ) ] = HIGHLIGHT_PARAM; mark = tok_get_pos( &tok ); } is_subcommand = 1; } tok_set_pos( &tok, mark ); } if( !is_subcommand ) { /* OK, this is a command, it has been successfully expanded and everything looks ok. Lets check if the command exists. */ /* First check if it is a builtin or function, since we don't have to stat any files for that */ if( use_builtin ) is_cmd |= builtin_exists( cmd ); if( use_function ) is_cmd |= function_exists_no_autoload( cmd ); /* Moving on to expensive tests */ /* Check if this is a regular command */ if( use_command ) { wcstring tmp; is_cmd |= !!path_get_path_string( cmd, tmp, vars ); } /* Could not find the command. Maybe it is a path for a implicit cd command. */ if( use_builtin || (use_function && function_exists( L"cd") ) ) { wcstring tmp; is_cmd |= !!path_get_cdpath_string( cmd, tmp, vars ); } if( is_cmd ) { color[ tok_get_pos( &tok ) ] = HIGHLIGHT_COMMAND; } else { if( error ) al_push( error, wcsdupcat ( L"Unknown command \'", cmd, L"\'" )); color[ tok_get_pos( &tok ) ] = (HIGHLIGHT_ERROR); } had_cmd = 1; } if( had_cmd ) { last_cmd = halloc_wcsdup( context, tok_last( &tok ) ); } } } break; } case TOK_REDIRECT_NOCLOB: case TOK_REDIRECT_OUT: case TOK_REDIRECT_IN: case TOK_REDIRECT_APPEND: case TOK_REDIRECT_FD: { if( !had_cmd ) { color[ tok_get_pos( &tok ) ] = HIGHLIGHT_ERROR; if( error ) al_push( error, wcsdup ( L"Redirection without a command" ) ); break; } wchar_t *target=0; color[ tok_get_pos( &tok ) ] = HIGHLIGHT_REDIRECTION; tok_next( &tok ); /* Check that we are redirecting into a file */ switch( tok_last_type( &tok ) ) { case TOK_STRING: { target = expand_one( context, wcsdup( tok_last( &tok ) ), EXPAND_SKIP_CMDSUBST); /* Redirect filename may contain a cmdsubst. If so, it will be ignored/not flagged. */ } break; default: { color[ tok_get_pos( &tok ) ] = HIGHLIGHT_ERROR; if( error ) al_push( error, wcsdup ( L"Invalid redirection" ) ); } } if( target != 0 ) { wchar_t *dir = halloc_wcsdup( context, target ); wchar_t *dir_end = wcsrchr( dir, L'/' ); struct stat buff; /* If file is in directory other than '.', check that the directory exists. */ if( dir_end != 0 ) { *dir_end = 0; if( wstat( dir, &buff ) == -1 ) { color[ tok_get_pos( &tok ) ] = HIGHLIGHT_ERROR; if( error ) al_push( error, wcsdupcat( L"Directory \'", dir, L"\' does not exist" ) ); } } /* If the file is read from or appended to, check if it exists. */ if( last_type == TOK_REDIRECT_IN || last_type == TOK_REDIRECT_APPEND ) { if( wstat( target, &buff ) == -1 ) { color[ tok_get_pos( &tok ) ] = HIGHLIGHT_ERROR; if( error ) al_push( error, wcsdupcat( L"File \'", target, L"\' does not exist" ) ); } } if( last_type == TOK_REDIRECT_NOCLOB ) { if( wstat( target, &buff ) != -1 ) { color[ tok_get_pos( &tok ) ] = HIGHLIGHT_ERROR; if( error ) al_push( error, wcsdupcat( L"File \'", target, L"\' exists" ) ); } } } break; } case TOK_PIPE: case TOK_BACKGROUND: { if( had_cmd ) { color[ tok_get_pos( &tok ) ] = HIGHLIGHT_END; had_cmd = 0; use_command = 1; use_function = 1; use_builtin = 1; accept_switches = 1; } else { color[ tok_get_pos( &tok ) ] = HIGHLIGHT_ERROR; if( error ) al_push( error, wcsdup ( L"No job to put in background" ) ); } break; } case TOK_END: { color[ tok_get_pos( &tok ) ] = HIGHLIGHT_END; had_cmd = 0; use_command = 1; use_function = 1; use_builtin = 1; accept_switches = 1; break; } case TOK_COMMENT: { color[ tok_get_pos( &tok ) ] = HIGHLIGHT_COMMENT; break; } case TOK_ERROR: default: { /* If the tokenizer reports an error, highlight it as such. */ if( error ) al_push( error, wcsdup ( tok_last( &tok) ) ); color[ tok_get_pos( &tok ) ] = HIGHLIGHT_ERROR; break; } } } tok_destroy( &tok ); } // PCA DOES_IO (calls is_potential_path, path_get_path, maybe others) void highlight_shell( const wchar_t *buff, int *color, int pos, array_list_t *error, const env_vars &vars ) { ASSERT_IS_BACKGROUND_THREAD(); int i; int len; int last_val; void *context; CHECK( buff, ); CHECK( color, ); len = wcslen(buff); if( !len ) return; context = halloc( 0, 0 ); for( i=0; buff[i] != 0; i++ ) color[i] = -1; /* Tokenize the string */ tokenize(buff, color, pos, error, context, vars); /* Locate and syntax highlight cmdsubsts recursively */ wchar_t *buffcpy = halloc_wcsdup( context, buff ); wchar_t *subpos=buffcpy; int done=0; while( 1 ) { wchar_t *begin, *end; if( parse_util_locate_cmdsubst( subpos, &begin, &end, 1) <= 0) { break; } if( !*end ) done=1; else *end=0; highlight_shell( begin+1, color +(begin-buffcpy)+1, -1, error, vars ); color[end-buffcpy]=HIGHLIGHT_OPERATOR; if( done ) break; subpos = end+1; } /* The highlighting code only changes the first element when the color changes. This fills in the rest. */ last_val=0; for( i=0; buff[i] != 0; i++ ) { if( color[i] >= 0 ) last_val = color[i]; else color[i] = last_val; } /* Color potentially valid paths in a special path color if they are the current token. */ if( pos >= 0 && pos <= len ) { wchar_t *tok_begin, *tok_end; wchar_t *token; parse_util_token_extent( buff, pos, &tok_begin, &tok_end, 0, 0 ); if( tok_begin && tok_end ) { token = halloc_wcsndup( context, tok_begin, tok_end-tok_begin ); if( is_potential_path( token ) ) { for( i=tok_begin-buff; i < (tok_end-buff); i++ ) { color[i] |= HIGHLIGHT_VALID_PATH; } } } } highlight_universal_internal( buff, color, pos ); /* Spaces should not be highlighted at all, since it makes cursor look funky in some terminals */ for( i=0; buff[i]; i++ ) { if( iswspace(buff[i]) ) { color[i]=0; } } halloc_free( context ); } /** Perform quote and parenthesis highlighting on the specified string. */ static void highlight_universal_internal( const wchar_t * buff, int *color, int pos ) { if( (pos >= 0) && (pos < wcslen(buff)) ) { /* Highlight matching quotes */ if( (buff[pos] == L'\'') || (buff[pos] == L'\"') ) { array_list_t l; al_init( &l ); int level=0; wchar_t prev_q=0; const wchar_t *str=buff; int match_found=0; while(*str) { switch( *str ) { case L'\\': str++; break; case L'\"': case L'\'': if( level == 0 ) { level++; al_push_long( &l, (long)(str-buff) ); prev_q = *str; } else { if( prev_q == *str ) { long pos1, pos2; level--; pos1 = al_pop_long( &l ); pos2 = str-buff; if( pos1==pos || pos2==pos ) { color[pos1]|=HIGHLIGHT_MATCH<<16; color[pos2]|=HIGHLIGHT_MATCH<<16; match_found = 1; } prev_q = *str==L'\"'?L'\'':L'\"'; } else { level++; al_push_long( &l, (long)(str-buff) ); prev_q = *str; } } break; } if( (*str == L'\0')) break; str++; } al_destroy( &l ); if( !match_found ) color[pos] = HIGHLIGHT_ERROR<<16; } /* Highlight matching parenthesis */ if( wcschr( L"()[]{}", buff[pos] ) ) { int step = wcschr(L"({[", buff[pos])?1:-1; wchar_t dec_char = *(wcschr( L"()[]{}", buff[pos] ) + step); wchar_t inc_char = buff[pos]; int level = 0; const wchar_t *str = &buff[pos]; int match_found=0; while( (str >= buff) && *str) { if( *str == inc_char ) level++; if( *str == dec_char ) level--; if( level == 0 ) { int pos2 = str-buff; color[pos]|=HIGHLIGHT_MATCH<<16; color[pos2]|=HIGHLIGHT_MATCH<<16; match_found=1; break; } str+= step; } if( !match_found ) color[pos] = HIGHLIGHT_ERROR<<16; } } } void highlight_universal( const wchar_t *buff, int *color, int pos, array_list_t *error, const env_vars &vars ) { int i; for( i=0; buff[i]; i++ ) color[i] = 0; highlight_universal_internal( buff, color, pos ); }