Improve case insensitivity consistency (#10884)

# Description

Add an extension trait `IgnoreCaseExt` to nu_utils which adds some case
insensitivity helpers, and use them throughout nu to improve the
handling of case insensitivity. Proper case folding is done via unicase,
which is already a dependency via mime_guess from nu-command.

In actuality a lot of code still does `to_lowercase`, because unicase
only provides immediate comparison and doesn't expose a `to_folded_case`
yet. And since we do a lot of `contains`/`starts_with`/`ends_with`, it's
not sufficient to just have `eq_ignore_case`. But if we get access in
the future, this makes us ready to use it with a change in one place.

Plus, it's clearer what the purpose is at the call site to call
`to_folded_case` instead of `to_lowercase` if it's exclusively for the
purpose of case insensitive comparison, even if it just does
`to_lowercase` still.

# User-Facing Changes

- Some commands that were supposed to be case insensitive remained only
insensitive to ASCII case (a-z), and now are case insensitive w.r.t.
non-ASCII characters as well.

# Tests + Formatting

- 🟢 `toolkit fmt`
- 🟢 `toolkit clippy`
- 🟢 `toolkit test`
- 🟢 `toolkit test stdlib`

---------

Co-authored-by: Stefan Holderbach <sholderbach@users.noreply.github.com>
This commit is contained in:
Christopher Durham
2023-11-08 17:58:54 -05:00
committed by GitHub
parent aed4b626b8
commit 0f600bc3f5
35 changed files with 176 additions and 122 deletions

View File

@ -9,6 +9,7 @@ use nu_protocol::{
SyntaxShape, Type, Value,
};
use nu_system::ForegroundProcess;
use nu_utils::IgnoreCaseExt;
use os_pipe::PipeReader;
use pathdiff::diff_paths;
use std::collections::HashMap;
@ -223,10 +224,10 @@ impl ExternalCommand {
const CMD_INTERNAL_COMMANDS: [&str; 9] = [
"ASSOC", "CLS", "ECHO", "FTYPE", "MKLINK", "PAUSE", "START", "VER", "VOL",
];
let command_name_upper = self.name.item.to_uppercase();
let command_name = &self.name.item;
let looks_like_cmd_internal = CMD_INTERNAL_COMMANDS
.iter()
.any(|&cmd| command_name_upper == cmd);
.any(|&cmd| command_name.eq_ignore_ascii_case(cmd));
if looks_like_cmd_internal {
let (cmd, new_reader) = self.create_process(&input, true, head)?;
@ -252,9 +253,10 @@ impl ExternalCommand {
which::which_in(&self.name.item, Some(path_with_cwd), cwd)
{
if let Some(file_name) = which_path.file_name() {
let file_name_upper =
file_name.to_string_lossy().to_uppercase();
if file_name_upper != command_name_upper {
if !file_name
.to_string_lossy()
.eq_ignore_case(command_name)
{
// which-rs found an executable file with a slightly different name
// than the one the user tried. Let's try running it
let mut new_command = self.clone();
@ -767,11 +769,11 @@ fn trim_expand_and_apply_arg(
/// Given an invalid command name, try to suggest an alternative
fn suggest_command(attempted_command: &str, engine_state: &EngineState) -> Option<String> {
let commands = engine_state.get_signatures(false);
let command_name_lower = attempted_command.to_lowercase();
let command_folded_case = attempted_command.to_folded_case();
let search_term_match = commands.iter().find(|sig| {
sig.search_terms
.iter()
.any(|term| term.to_lowercase() == command_name_lower)
.any(|term| term.to_folded_case() == command_folded_case)
});
match search_term_match {
Some(sig) => Some(sig.name.clone()),