bool autosuggest_suggest_special(const wcstring &str, const wcstring &working_directory, wcstring &outSuggestion) { if (str.empty()) return false; ASSERT_IS_BACKGROUND_THREAD(); /* Parse the string */ wcstring parsed_command; wcstring_list_t parsed_arguments; int parsed_last_arg_pos = -1; if (! autosuggest_parse_command(str, &parsed_command, &parsed_arguments, &parsed_last_arg_pos)) return false; bool result = false; if (parsed_command == L"cd" && ! parsed_arguments.empty()) { /* We can possibly handle this specially */ wcstring dir = parsed_arguments.back(); wcstring suggested_path; /* We always return true because we recognized the command. This prevents us from falling back to dumber algorithms; for example we won't suggest a non-directory for the cd command. */ result = true; outSuggestion.clear(); if (is_potential_cd_path(dir, working_directory, &suggested_path)) { /* Success */ outSuggestion = str; outSuggestion.erase(parsed_last_arg_pos); outSuggestion.append(suggested_path); } } else { /* Either an error or some other command, so we don't handle it specially */ } return result; }
/* We have to return an escaped string here */ bool autosuggest_suggest_special(const wcstring &str, const wcstring &working_directory, wcstring &outSuggestion) { if (str.empty()) return false; ASSERT_IS_BACKGROUND_THREAD(); /* Parse the string */ wcstring parsed_command; wcstring_list_t parsed_arguments; int parsed_last_arg_pos = -1; if (! autosuggest_parse_command(str, &parsed_command, &parsed_arguments, &parsed_last_arg_pos)) return false; bool result = false; if (parsed_command == L"cd" && ! parsed_arguments.empty()) { /* We can possibly handle this specially */ const wcstring escaped_dir = parsed_arguments.back(); wcstring suggested_path; /* We always return true because we recognized the command. This prevents us from falling back to dumber algorithms; for example we won't suggest a non-directory for the cd command. */ result = true; outSuggestion.clear(); /* Unescape the parameter */ wcstring unescaped_dir = escaped_dir; bool unescaped = unescape_string(unescaped_dir, UNESCAPE_INCOMPLETE); /* Determine the quote type we got from the input directory. */ wchar_t quote = L'\0'; parse_util_get_parameter_info(escaped_dir, 0, "e, NULL, NULL); /* Big hack to avoid expanding a tilde inside quotes */ path_flags_t path_flags = (quote == L'\0') ? PATH_EXPAND_TILDE : 0; if (unescaped && is_potential_cd_path(unescaped_dir, working_directory, path_flags, &suggested_path)) { /* Note: this looks really wrong for strings that have an "unescapable" character in them, e.g. a \t, because parse_util_escape_string_with_quote will insert that character */ wcstring escaped_suggested_path = parse_util_escape_string_with_quote(suggested_path, quote); /* Return it */ outSuggestion = str; outSuggestion.erase(parsed_last_arg_pos); if (quote != L'\0') outSuggestion.push_back(quote); outSuggestion.append(escaped_suggested_path); if (quote != L'\0') outSuggestion.push_back(quote); } } else { /* Either an error or some other command, so we don't handle it specially */ } return result; }
int file_detection_context_t::perform_file_detection(bool test_all) { ASSERT_IS_BACKGROUND_THREAD(); valid_paths.clear(); int result = 1; for (path_list_t::const_iterator iter = potential_paths.begin(); iter != potential_paths.end(); ++iter) { if (path_is_valid(*iter, working_directory)) { /* Push the original (possibly relative) path */ valid_paths.push_front(*iter); } else { /* Not a valid path */ result = 0; if (! test_all) break; } } valid_paths.reverse(); return result; }
static int threaded_perform_file_detection(file_detection_context_t *ctx) { ASSERT_IS_BACKGROUND_THREAD(); assert(ctx != NULL); return ctx->perform_file_detection(true /* test all */); }
// This function does I/O static void tokenize( const wchar_t * const buff, std::vector<int> &color, const int pos, wcstring_list_t *error, const wcstring &working_directory, const env_vars_snapshot_t &vars) { ASSERT_IS_BACKGROUND_THREAD(); wcstring cmd; int had_cmd=0; wcstring last_cmd; int len; int accept_switches = 1; int use_function = 1; int use_command = 1; int use_builtin = 1; CHECK( buff, ); len = wcslen(buff); if( !len ) return; std::fill(color.begin(), color.end(), -1); tokenizer tok; for( tok_init( &tok, buff, TOK_SHOW_COMMENTS | TOK_SQUASH_ERRORS ); tok_has_next( &tok ); tok_next( &tok ) ) { int last_type = tok_last_type( &tok ); 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.at(tok_get_pos( &tok )) = HIGHLIGHT_PARAM; } else if( accept_switches ) { if( complete_is_valid_option( last_cmd.c_str(), param, error, false /* no autoload */ ) ) color.at(tok_get_pos( &tok )) = HIGHLIGHT_PARAM; else color.at(tok_get_pos( &tok )) = HIGHLIGHT_ERROR; } else { color.at(tok_get_pos( &tok )) = HIGHLIGHT_PARAM; } } else { color.at(tok_get_pos( &tok )) = HIGHLIGHT_PARAM; } if( cmd == L"cd" ) { wcstring dir = tok_last( &tok ); if (expand_one(dir, EXPAND_SKIP_CMDSUBST)) { int is_help = string_prefixes_string(dir, L"--help") || string_prefixes_string(dir, L"-h"); if( !is_help && ! is_potential_cd_path(dir, working_directory, PATH_EXPAND_TILDE, NULL)) { color.at(tok_get_pos( &tok )) = HIGHLIGHT_ERROR; } } } /* Highlight the parameter. highlight_param wants to write one more color than we have characters (hysterical raisins) so allocate one more in the vector. But don't copy it back. */ const wcstring param_str = param; int tok_pos = tok_get_pos(&tok); std::vector<int>::const_iterator where = color.begin() + tok_pos; std::vector<int> subcolors(where, where + param_str.size()); subcolors.push_back(-1); highlight_param(param_str, subcolors, pos-tok_pos, error); /* Copy the subcolors back into our colors array */ std::copy(subcolors.begin(), subcolors.begin() + param_str.size(), color.begin() + tok_pos); } else { /* Command. First check that the command actually exists. */ cmd = tok_last( &tok ); bool expanded = expand_one(cmd, EXPAND_SKIP_CMDSUBST | EXPAND_SKIP_VARIABLES); if (! expanded || has_expand_reserved(cmd.c_str())) { color.at(tok_get_pos( &tok )) = HIGHLIGHT_ERROR; } else { bool is_cmd = false; int is_subcommand = 0; int mark = tok_get_pos( &tok ); color.at(tok_get_pos( &tok )) = HIGHLIGHT_COMMAND; if( parser_keywords_is_subcommand( cmd ) ) { int sw; if( cmd == L"builtin") { use_function = 0; use_command = 0; use_builtin = 1; } else if( cmd == L"command") { 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.at(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 (! is_cmd && use_builtin ) is_cmd = builtin_exists( cmd ); if (! is_cmd && use_function ) is_cmd = function_exists_no_autoload( cmd, vars ); /* Moving on to expensive tests */ /* Check if this is a regular command */ if (! is_cmd && use_command ) { is_cmd = path_get_path( cmd, NULL, vars ); } /* Maybe it is a path for a implicit cd command. */ if (! is_cmd) { if (use_builtin || (use_function && function_exists_no_autoload( L"cd", vars))) is_cmd = path_can_be_implicit_cd(cmd, NULL, working_directory.c_str(), vars); } if( is_cmd ) { color.at(tok_get_pos( &tok )) = HIGHLIGHT_COMMAND; } else { if( error ) { error->push_back(format_string(L"Unknown command \'%ls\'", cmd.c_str())); } color.at(tok_get_pos( &tok )) = (HIGHLIGHT_ERROR); } had_cmd = 1; } if( had_cmd ) { last_cmd = 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.at(tok_get_pos( &tok )) = HIGHLIGHT_ERROR; if( error ) error->push_back(L"Redirection without a command"); break; } wcstring target_str; const wchar_t *target=NULL; color.at(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_str = tok_last( &tok ); if (expand_one(target_str, EXPAND_SKIP_CMDSUBST)) { target = target_str.c_str(); } /* Redirect filename may contain a cmdsubst. If so, it will be ignored/not flagged. */ } break; default: { size_t pos = tok_get_pos(&tok); if (pos < color.size()) { color.at(pos) = HIGHLIGHT_ERROR; } if( error ) error->push_back(L"Invalid redirection"); } } if( target != 0 ) { wcstring dir = target; size_t slash_idx = dir.find_last_of(L'/'); struct stat buff; /* If file is in directory other than '.', check that the directory exists. */ if( slash_idx != wcstring::npos ) { dir.resize(slash_idx); if( wstat( dir, &buff ) == -1 ) { color.at(tok_get_pos( &tok )) = HIGHLIGHT_ERROR; if( error ) error->push_back(format_string(L"Directory \'%ls\' does not exist", dir.c_str())); } } /* 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.at(tok_get_pos( &tok )) = HIGHLIGHT_ERROR; if( error ) error->push_back(format_string(L"File \'%ls\' does not exist", target)); } } if( last_type == TOK_REDIRECT_NOCLOB ) { if( wstat( target, &buff ) != -1 ) { color.at(tok_get_pos( &tok )) = HIGHLIGHT_ERROR; if( error ) error->push_back(format_string(L"File \'%ls\' exists", target)); } } } break; } case TOK_PIPE: case TOK_BACKGROUND: { if( had_cmd ) { color.at(tok_get_pos( &tok )) = HIGHLIGHT_END; had_cmd = 0; use_command = 1; use_function = 1; use_builtin = 1; accept_switches = 1; } else { color.at(tok_get_pos( &tok )) = HIGHLIGHT_ERROR; if( error ) error->push_back(L"No job to put in background" ); } break; } case TOK_END: { color.at(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.at(tok_get_pos( &tok )) = HIGHLIGHT_COMMENT; break; } case TOK_ERROR: default: { /* If the tokenizer reports an error, highlight it as such. */ if( error ) error->push_back(tok_last( &tok)); color.at(tok_get_pos( &tok )) = HIGHLIGHT_ERROR; break; } } } tok_destroy( &tok ); }
bool autosuggest_validate_from_history(const history_item_t &item, file_detection_context_t &detector, const wcstring &working_directory, const env_vars_snapshot_t &vars) { ASSERT_IS_BACKGROUND_THREAD(); bool handled = false, suggestionOK = false; /* Parse the string */ wcstring parsed_command; wcstring_list_t parsed_arguments; int parsed_last_arg_pos = -1; if (! autosuggest_parse_command(item.str(), &parsed_command, &parsed_arguments, &parsed_last_arg_pos)) return false; if (parsed_command == L"cd" && ! parsed_arguments.empty()) { /* We can possibly handle this specially */ wcstring dir = parsed_arguments.back(); if (expand_one(dir, EXPAND_SKIP_CMDSUBST)) { handled = true; bool is_help = string_prefixes_string(dir, L"--help") || string_prefixes_string(dir, L"-h"); if (is_help) { suggestionOK = false; } else { wcstring path; bool can_cd = path_get_cdpath(dir, &path, working_directory.c_str(), vars); if (! can_cd) { suggestionOK = false; } else if (paths_are_same_file(working_directory, path)) { /* Don't suggest the working directory as the path! */ suggestionOK = false; } else { suggestionOK = true; } } } } /* If not handled specially, handle it here */ if (! handled) { bool cmd_ok = false; if (path_get_path(parsed_command, NULL)) { cmd_ok = true; } else if (builtin_exists(parsed_command) || function_exists_no_autoload(parsed_command, vars)) { cmd_ok = true; } if (cmd_ok) { const path_list_t &paths = item.get_required_paths(); if (paths.empty()) { suggestionOK= true; } else { detector.potential_paths = paths; suggestionOK = detector.paths_are_valid(paths); } } } return suggestionOK; }
/* Tests whether the specified string cpath is the prefix of anything we could cd to. directories is a list of possible parent directories (typically either the working directory, or the cdpath). This does I/O! We expect the path to already be unescaped. */ bool is_potential_path(const wcstring &const_path, const wcstring_list_t &directories, path_flags_t flags, wcstring *out_path) { ASSERT_IS_BACKGROUND_THREAD(); const bool require_dir = !! (flags & PATH_REQUIRE_DIR); wcstring clean_path; int has_magic = 0; bool result = false; wcstring path(const_path); if (flags & PATH_EXPAND_TILDE) expand_tilde(path); // debug( 1, L"%ls -> %ls ->%ls", path, tilde, unescaped ); for( size_t i=0; i < path.size(); i++) { wchar_t c = path.at(i); switch( c ) { 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: { clean_path.push_back(c); break; } } } if( ! has_magic && ! clean_path.empty() ) { /* Don't test the same path multiple times, which can happen if the path is absolute and the CDPATH contains multiple entries */ std::set<wcstring> checked_paths; /* Keep a cache of which paths / filesystems are case sensitive */ case_sensitivity_cache_t case_sensitivity_cache; for (size_t wd_idx = 0; wd_idx < directories.size() && ! result; wd_idx++) { const wcstring &wd = directories.at(wd_idx); const wcstring abs_path = apply_working_directory(clean_path, wd); /* Skip this if it's empty or we've already checked it */ if (abs_path.empty() || checked_paths.count(abs_path)) continue; checked_paths.insert(abs_path); /* If we end with a slash, then it must be a directory */ bool must_be_full_dir = abs_path.at(abs_path.size()-1) == L'/'; if (must_be_full_dir) { struct stat buf; if (0 == wstat(abs_path, &buf) && S_ISDIR(buf.st_mode)) { result = true; /* Return the path suffix, not the whole absolute path */ if (out_path) *out_path = clean_path; } } else { DIR *dir = NULL; /* We do not end with a slash; it does not have to be a directory */ const wcstring dir_name = wdirname(abs_path); const wcstring base_name = wbasename(abs_path); if (dir_name == L"/" && base_name == L"/") { result = true; if (out_path) *out_path = clean_path; } else if ((dir = wopendir(dir_name))) { // We opened the dir_name; look for a string where the base name prefixes it wcstring ent; // Check if we're case insensitive bool case_insensitive = fs_is_case_insensitive(dir_name, dirfd(dir), case_sensitivity_cache); // Don't ask for the is_dir value unless we care, because it can cause extra filesystem acces */ bool is_dir = false; while (wreaddir_resolving(dir, dir_name, ent, require_dir ? &is_dir : NULL)) { /* Determine which function to call to check for prefixes */ bool (*prefix_func)(const wcstring &, const wcstring &); if (case_insensitive) { prefix_func = string_prefixes_string_case_insensitive; } else { prefix_func = string_prefixes_string; } if (prefix_func(base_name, ent) && (! require_dir || is_dir)) { result = true; if (out_path) { /* We want to return the path in the same "form" as it was given. Take the given path, get its basename. Append that to the output if the basename actually prefixes the path (which it won't if the given path contains no slashes), and isn't a slash (so we don't duplicate slashes). Then append the directory entry. */ out_path->clear(); const wcstring path_base = wdirname(const_path); if (prefix_func(path_base, const_path)) { out_path->append(path_base); if (! string_suffixes_string(L"/", *out_path)) out_path->push_back(L'/'); } out_path->append(ent); /* We actually do want a trailing / for directories, since it makes autosuggestion a bit nicer */ if (is_dir) out_path->push_back(L'/'); } break; } } closedir(dir); } } } } return result; }
// PCA This function does I/O, (calls is_potential_path, path_get_path, maybe others) and so ought to only run on a background thread void highlight_shell( const wcstring &buff, std::vector<int> &color, int pos, wcstring_list_t *error, const env_vars_snapshot_t &vars ) { ASSERT_IS_BACKGROUND_THREAD(); const size_t length = buff.size(); assert(buff.size() == color.size()); if( length == 0 ) return; std::fill(color.begin(), color.end(), -1); /* Do something sucky and get the current working directory on this background thread. This should really be passed in. Note that we also need this as a vector (of one directory). */ const wcstring working_directory = get_working_directory(); /* Tokenize the string */ tokenize(buff.c_str(), color, pos, error, working_directory, vars); /* Locate and syntax highlight cmdsubsts recursively */ wchar_t * const subbuff = wcsdup(buff.c_str()); wchar_t * subpos = subbuff; 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; //our subcolors start at color + (begin-subbuff)+1 size_t start = begin - subbuff + 1, len = wcslen(begin + 1); std::vector<int> subcolors(len, -1); highlight_shell( begin+1, subcolors, -1, error, vars ); // insert subcolors std::copy(subcolors.begin(), subcolors.end(), color.begin() + start); // highlight the end of the subcommand assert(end >= subbuff); if ((size_t)(end - subbuff) < length) { color.at(end-subbuff)=HIGHLIGHT_OPERATOR; } if( done ) break; subpos = end+1; } free(subbuff); /* The highlighting code only changes the first element when the color changes. This fills in the rest. */ int last_val=0; for( size_t i=0; i < buff.size(); i++ ) { if( color.at(i) >= 0 ) last_val = color.at(i); else color.at(i) = last_val; } /* Color potentially valid paths in a special path color if they are the current token. For reasons that I don't yet understand, it's required that pos be allowed to be length (e.g. when backspacing). */ if( pos >= 0 && (size_t)pos <= length ) { const wchar_t *cbuff = buff.c_str(); const wchar_t *tok_begin, *tok_end; parse_util_token_extent( cbuff, pos, &tok_begin, &tok_end, 0, 0 ); if( tok_begin && tok_end ) { wcstring token(tok_begin, tok_end-tok_begin); const wcstring_list_t working_directory_list(1, working_directory); if (unescape_string(token, 1) && is_potential_path(token, working_directory_list, PATH_EXPAND_TILDE)) { for( ptrdiff_t i=tok_begin-cbuff; i < (tok_end-cbuff); i++ ) { // Don't color HIGHLIGHT_ERROR because it looks dorky. For example, trying to cd into a non-directory would show an underline and also red. if (! (color.at(i) & HIGHLIGHT_ERROR)) { color.at(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( size_t i=0; i < buff.size(); i++ ) { if( iswspace(buff.at(i)) ) { color.at(i)=0; } } }