2023年6月

像 fzf 和 fzy 这样的现代工具将 Linux 终端中的文件搜索提升到了一个新的水平。

在 Linux 命令行中,如何 搜索文件?你可以使用 find 命令。这是标准答案,没有问题。

通常,你键入带有搜索参数的命令,按回车键,然后它会显示搜索结果。

你可以通过模糊搜索来提升终端中的文件搜索体验。

模糊搜索是一种近似搜索算法或技术。在这种搜索中,通过名称搜索指定位置的文件,并实时显示结果给用户。

模糊搜索在网络搜索引擎中很受欢迎,用户开始输入术语后,它会开始显示与该术语相关的结果。

在本文中,我将讨论两个命令行工具,它们可以让你在 Linux 中执行模糊搜索:

  • fzf:模糊查找工具
  • fzy:模糊选择工具

fzf:Linux 中的模糊查找工具

fzf 是一款可用于 Linux 的模糊搜索工具,你可以通过它进行交互式文件搜索。

在 Ubuntu 中安装 fzf,打开终端并运行以下命令:

sudo apt install fzf

虽然 fzf 本身可以正常工作,但最好与其他工具配合使用,以充分发挥其功能。

使用 fzf

打开终端并运行:

fzf

这将打开一个 fzf 提示符,在当前工作目录中搜索文件。

在终端中运行 fzf 命令

为 fzf 应用边框

你可以使用 --border 选项为 fzf 应用边框,有多种边框可用,如 rounded(圆角)、sharp(尖角)等。

fzf --border=rounded

运行带有边框选项设置为 rounded 和 sharp 的 fzf 命令

应用背景和前景颜色

使用颜色属性,你可以为 fzf 设置 ANSI 颜色,可以作为背景、前景或两者都设置。

为 fzf 应用颜色,用户指定了颜色

fzf --color="bg:black,fg:yellow"

你可以串联这些选项,使 fzf 在视觉上更加美观。

现在,让我展示一些 fzf 模糊搜索的实际用法。

使用 fzf 在 Bash 历史中进行搜索

当然,Bash 历史记录中有 CTRL+R 的反向搜索功能。但如果你想使用 fzf 来获得更好的外观,可以运行以下命令:

history | fzf

使用 fzf 模糊搜索在 bash 历史中进行搜索

使用 fzf 结合 tree 命令

tree 命令 会列出文件和目录,并显示它们的层级关系。

使用 fzf 结合 tree 命令可以帮助你找到特定文件的绝对路径。

tree -afR /home/$USER | fzf

运行 Tree 命令并将输出传递给模糊搜索

? 上述命令会调用 tree 并以递归方式列出包括隐藏文件在内的所有文件(-a)。同时,-f 选项告诉 tree 列出完整路径。

在 fzf 中预览文件

有时,如果你可以获得你搜索的文件的小型预览,那会很有帮助。

幸运的是,fzf 提供了一个预览选项。你可以使用 --preview 来访问它。我在这里使用 find 命令使其更加有用。

find /home/$USER -type f | fzf --preview 'less {}'

在这里,当你滚动浏览结果时,它将使用 less 显示文本文件。

? 如果你使用其他命令如 ls 等,请不要使用 -l 等选项,因为这将显示额外的详细信息(文件权限)。这些额外的详细信息会破坏 fzf 预览所需的格式。在使用预览功能时,输入到 fzf 的应该只是文件名。

如果你已安装了 bat,也可以使用它来预览文件。

find /home/$USER -type f | fzf --preview 'bat --color always {}'

使用 bat 作为 FZF 预览功能的文本查看器

对于 Ubuntu 用户,可以使用 batcat 来调用 bat。因此运行:

find /home/$USER -type f | fzf --preview 'batcat --color always {}'
? 为这些命令创建别名,这样你就不需要反复输入它们。

从任何地方使用 fzf 进入任何目录(高级技巧)

这比以前要复杂一些。在这里,你不能直接将 fzfcd 连接在一起,因为它们是不同的进程。

你可以创建一个别名并使用以下命令:

cd $(find /home/$USER -type d | fzf)

或者,你可以按照下面解释的方法进行操作。

为此,你可能需要在 bashrc 中添加一个函数。让我将这个函数称为 finder。现在请添加以下行到你的 bashrc 中。

finder() {
  local dir
  dir=$(find required/location/to/search/and/enter -type d | fzf)
  if [[ -n "$dir" ]]; then
    cd "$dir" || return
  fi
}

现在,你应该 输入路径,其中包含你要搜索并进入的目录。

例如,我已经使用 /home/$USER 替换了该部分,表示我要从任何位置进入我的主目录中的任何目录。

保存你的 bashrc 文件后,要么重启终端,要么运行以下命令:

source ~/.bashrc

之后,你可以在终端上运行 finder 命令,一旦找到要进入的目录,按回车键即可。

使用 fzf 命令结合 cd 命令进入任意目录

将选择内容复制到剪贴板

到目前为止,你已经了解了如何使用 fzf,它提供了搜索结果或预览。

现在,如果你想要复制某个项目的位置,你不必手动执行此操作。也有相应的解决方案。

首先,确保你已经安装了 xclip

sudo apt install xclip

然后像这样将其传递给 xclip

fzf | xclip -selection clipboard

这将复制你按下回车键的那些行到剪贴板上。

其他用途

正如我之前所说,你可以使用任何涉及大量文本,并希望交互式搜索特定内容的命令。

  • cat ~/.bashrc | fzf - 在 Bashrc 文件中搜索
  • lsblk | fzf - 在锁定设备列表中搜索
  • ps -aux | fzf - 在进程列表中搜索

另一个选择:Fzy,模糊选择器

fzf 不同,fzy 是一个模糊选择器,它会根据输入提供一个菜单供你选择。

例如,如果你将 fzyls 命令一起使用,它将给你提供一个类似菜单的界面。

使用 ls 命令的 fzy 命令

默认情况下,它会显示十个条目。

使用 fzy 进入目录

fzf 类似,你也可以使用 fzy 进入当前工作目录中的目录:

cd $(find -type d | fzy)

使用任何编辑器打开文件

或者使用你喜欢的编辑器打开文件:

nano $(find -type f | fzy)

附加内容:自定义文件和图像预览

下面的命令将在 Ubuntu 中打开一个专门的自定义提示符,用于模糊搜索,你可以通过滚动来预览文本文件。

find /home/$USER -type f | fzf --color="bg:black,fg:yellow" --preview 'batcat --color always {}' --preview-window=bottom

为了方便访问,可以在你的 bashrc 文件中为此创建一个别名。

或者在使用 timg 命令行图像查看器时,在 fzf 中进行图像预览并滚动。使用以下命令进行安装:

sudo apt install timg
? 请注意,图像查看器无法正确显示图像,因为这不是 fzf 预览的主要目的。
fzf --preview 'timg -g 200x100 {}' --preview-window=right:90

对于那些喜欢折腾的人,可以尝试对此部分进行优化。

现代化的替代方案

大多数 Linux 命令都是从 UNIX 时代继承下来的。它们虽然老旧,但功能如预期。但这并不意味着它们不能改进。

我的意思是,你不需要重新发明轮子,但你总是可以努力改进轮子。

fzffzy 这样的现代化工具将 Linux 终端中的文件搜索提升到了一个新的水平。以下是一些其他有趣的命令行工具。

我尝试给出了这些模糊搜索工具的一些实际示例。希望你对它们感到足够有启发性。如果你打算使用它们,请在评论中告诉我。

(题图:MJ/d25e71fa-f24e-49be-9579-e0520a8f6e18)


via: https://itsfoss.com/fuzzy-file-search-linux/

作者:Sreenath 选题:lkxed 译者:ChatGPT 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

AlmaLinux 确认受到 RHEL 源代码发布变化影响

针对 昨天 红帽发布的改变 RHEL 源代码发布方式的公告,AlmaLinux 发布声明称,这将限制他们 1:1 复制 RHEL 构建的能力。因为 CentOS Stream 虽然是 RHEL 的上游,但并不总是包括 RHEL 软件包中的所有补丁和更新。想要做到完全复制,需要有 RHEL 构建版本时的源代码。而要获得该源代码,AlmaLinux 称,“不幸的是,按照我们今天的理解,红帽公司的用户界面协议表明,重新发布通过客户门户获得的资源将违反这些协议。”在短期内,他们将专注于通过跟踪 CentOS Stream 的变化和 Oracle Linux 等来为 AlmaLinux 提供安全更新。但从长远来看,他们将与其他 RHEL 下游产品和合作伙伴合作,寻找一条合适的发展道路。

消息来源:AlmaLinux
老王点评:对于这件事红帽背后的动机我们就不妄自揣测了,毕竟红帽和 AlmaLinux 曾经“建立了令人难以置信的工作关系”。但事实就是,社区构建的 AlmaLinux 想要跟上 RHEL 的构建会越来越麻烦和越来越慢。所以,你怎么看呢?

哈佛给计算机学生们配备了一位聊天机器人老师

哈佛大学将为今年秋季的计算机科学导论 CS50 课程的学生配置一位聊天机器人老师。这个聊天机器人将不使用 ChatGPT 或 GitHub Copilot,因为它们“太有帮助了”。相反,哈佛大学已经开发了自己的大型语言模型 “CS50 机器人”,它将 “具有类似的精神”,但将专注于 “引导学生找到答案,而不是将答案交给他们”。

消息来源:PCMag
老王点评:我认为哈佛大学走在了正确的道路,拥抱 AI,而不是让 AI 替代人类,让 AI 帮助学习,而不是让学生抄袭 AI 的作业。

E3 或许再也不会举办

之前,我们 报道 过,今年的 E3 2023 已经取消,这本应是 2019 年后的第一次 E3 线下盛会。现在,E3 已经取消了 2024 年和 2025 年在洛杉矶的活动。这表明有些事情阻碍了 E3 的回归,或者其组织者正在考虑将其迁出洛杉矶。

消息来源:The Verge
老王点评:一场大流行,改变了很多。很多事情再也回不到过去了。

到目前为止,我们已经讲解了包括 变量、可变性、常量数据类型函数if-else 语句循环 在内的一些关于 Rust 编程的基础知识。

在 Rust 基础系列的最后一章里,让我们现在用 Rust 编写一个程序,使用这些主题,以便更好地理解它们在现实世界中的用途。让我们来编写一个相对简单的程序,用来从水果市场订购水果。

我们程序的基本结构

来让我们首先向用户问好,并告诉他们如何与程序交互。

fn main() {
    println!("欢迎来到水果市场!");
    println!("请选择要购买的水果。\n");

    println!("\n可以购买的水果:苹果、香蕉、橘子、芒果、葡萄");
    println!("购买完成后,请输入“quit”或“q”。\n");
}

获取用户输入

上面的代码非常简单。目前,你不知道接下来该做什么,因为你不知道用户接下来想做什么。

所以让我们添加一些代码,接受用户输入并将其存储在某个地方以便稍后解析,然后根据用户输入采取适当的操作。

use std::io;

fn main() {
    println!("欢迎来到水果市场!");
    println!("请选择要购买的水果。\n");

    println!("\n可以购买的水果:苹果、香蕉、橘子、芒果、葡萄");
    println!("购买完成后,请输入“quit”或“q”。\n");

    // 获取用户输入
    let mut user_input = String::new();
    io::stdin()
        .read_line(&mut user_input)
        .expect("无法读取用户输入。");
}

有三个新元素需要告诉你。所以让我们对这些新元素进行浅层次的探索。

1. 理解 use 关键字

在这个程序的第一行,你可能已经注意到我们“使用”(哈哈!)了一个叫做 use 的新关键字。Rust 中的 use 关键字类似于 C/C++ 中的 #include 指令和 Python 中的 import 关键字。使用 use 关键字,我们从 Rust 标准库 std 中“导入”了 io(输入输出)模块。

LCTT 译注:“使用”在原文中为“use”,与新介绍的关键字一样。

你可能会想知道为什么我们在可以使用 println 宏来将某些内容输出到标准输出时,导入 io 模块是必要的。Rust 的标准库有一个叫做 prelude 的模块,它会自动被包含。该模块包含了 Rust 程序员可能需要使用的所有常用函数,比如 println 宏。(你可以在 这里 阅读更多关于 std::prelude 模块的内容。)

Rust 标准库 std 中的 io 模块是接受用户输入所必需的。因此,我们在程序的第一行添加了一个 use 语句。

2. 理解 Rust 中的 String 类型

在第 11 行,我创建了一个新的可变变量 user_input,正如它的名字所表示的那样,它将被用来存储用户输入。但是在同一行,你可能已经注意到了一些“新的”东西(哈哈,又来了!)。

LCTT 译注:“新的”在原文中为“new”,在第 11 行的代码中,原作者使用了 String::new() 函数,所以此处的梗与“使用”一样,原作者使用了一个在代码中用到的单词。

我没有使用双引号("")声明一个空字符串,而是使用 String::new() 函数来创建一个新的空字符串。

""String::new() 的区别是你将在 Rust 系列的后续文章中学习到的。现在,只需要知道,使用 String::new() 函数,你可以创建一个可变的,位于堆上的字符串。

如果我使用 "" 创建了一个字符串,我将得到一个叫做“字符串切片”的东西。字符串切片的内容也位于堆上,但是字符串本身是不可变的。所以,即使变量本身是可变的,作为字符串存储的实际数据是不可变的,需要被覆盖而不是修改。

3. 接受用户输入

在第 12 行,我调用了 std::iostdin() 函数。如果我在程序的开头没有导入 std::io 模块,那么这一行将是 std::io::stdin() 而不是 io::stdin()

sdtin() 函数返回一个终端的输入句柄。read_line() 函数抓住这个输入句柄,然后,正如它的名字所暗示的那样,读取一行输入。这个函数接受一个可变字符串的引用。所以,我传入了 user_input 变量,通过在它前面加上 &mut,使它成为一个可变引用。

⚠️ read_line() 函数有一个 怪癖。这个函数在用户按下回车键之后 停止 读取输入。因此,这个函数也会记录换行符(\n),并将一个换行符存储在你传入的可变字符串变量的结尾处。

所以,请在处理它时要么考虑到这个换行符,要么将它删除。

Rust 中的错误处理入门

最后,在这个链的末尾有一个 expect() 函数。让我们稍微偏题一下,来理解为什么要调用这个函数。

read_line() 函数返回一个叫做 Result 的枚举。我会在后面的文章中讲解 Rust 中的枚举,但是现在只需要知道,枚举在 Rust 中是非常强大的。这个 Result 枚举返回一个值,告诉程序员在读取用户输入时是否发生了错误。

expect() 函数接受这个 Result 枚举,并检查结果是否正常。如果没有发生错误,什么都不会发生。但是如果发生了错误,我传入的消息(无法读取用户输入。)将会被打印到 STDERR,程序将会退出

? 所有我简要提及的新概念将会在后续的新 Rust 系列文章中讲解。

现在我希望你应该已经理解了这些新概念,让我们添加更多的代码来增加程序的功能。

验证用户输入

我接受了用户的输入,但是我没有对其进行验证。在当前的上下文中,验证意味着用户输入了一些“命令”,我们希望能够处理这些命令。目前,这些命令有两个“类别”。

第一类用户可以输入的命令是用户希望购买的水果的名称。第二个命令表示用户想要退出程序。

我们的任务现在是确保用户输入不会偏离 可接受的命令

use std::io;

fn main() {
    println!("欢迎来到水果市场!");
    println!("请选择要购买的水果。\n");

    println!("\n可以购买的水果:苹果、香蕉、橘子、芒果、葡萄");
    println!("购买完成后,请输入“quit”或“q”。\n");

    // 获取用户输入
    let mut user_input = String::new();
    io::stdin()
        .read_line(&mut user_input)
        .expect("无法读取用户输入。");

    // 验证用户输入
    let valid_inputs = ["苹果", "香蕉", "橘子", "芒果", "葡萄", "quit", "q"];
    user_input = user_input.trim().to_lowercase();
    let mut input_error = true;
    for input in valid_inputs {
        if input == user_input {
            input_error = false;
            break;
        }
    }
}

要使验证更容易,我创建了一个叫做 valid_inputs 的字符串切片数组(第 17 行)。这个数组包含了所有可以购买的水果的名称,以及字符串切片 qquit,让用户可以传达他们是否希望退出。

用户可能不知道我们希望输入是什么样的。用户可能会输入“Apple”、“apple”或 “APPLE” 来表示他们想要购买苹果。我们的工作是正确处理这些输入。

在第 18 行,我通过调用 trim() 函数从 user_input 字符串中删除了尾部的换行符。为了处理上面提到的问题,我使用 to_lowercase() 函数将所有字符转换为小写,这样 “Apple”、“apple” 和 “APPLE” 都会变成 “apple”。

现在,来看第 19 行,我创建了一个名为 input_error 的可变布尔变量,初始值为 true。稍后在第 20 行,我创建了一个 for 循环,它遍历了 valid_inputs 数组的所有元素(字符串切片),并将迭代的模式存储在 input 变量中。

在循环内部,我检查用户输入是否等于其中一个有效字符串,如果是,我将 input_error 布尔值的值设置为 false,并跳出 for 循环。

处理无效输入

现在是时候处理无效输入了。这可以通过将一些代码移动到无限循环中来完成,如果用户给出无效输入,则 继续 该无限循环。

use std::io;

fn main() {
    println!("欢迎来到水果市场!");
    println!("请选择要购买的水果。\n");

    let valid_inputs = ["苹果", "香蕉", "橘子", "芒果", "葡萄", "quit", "q"];

    'mart: loop {
        let mut user_input = String::new();

        println!("\n可以购买的水果:苹果、香蕉、橘子、芒果、葡萄");
        println!("购买完成后,请输入“quit”或“q”。\n");

        // 读取用户输入
        io::stdin()
            .read_line(&mut user_input)
            .expect("无法读取用户输入。");
        user_input = user_input.trim().to_lowercase();

        // 验证用户输入
        let mut input_error = true;
        for input in valid_inputs {
            if input == user_input {
                input_error = false;
                break;
            }
        }

        // 处理无效输入
        if input_error {
            println!("错误: 请输入有效的输入");
            continue 'mart;
        }
    }
}

这里,我将一些代码移动到了循环内部,并重新组织了一下代码,以便更好地处理循环的引入。在循环内部,第 31 行,如果用户输入了一个无效的字符串,我将 continue mart 循环。

对用户输入做出反应

现在,所有其他的状况都已经处理好了,是时候写一些代码来让用户从水果市场购买水果了,当用户希望退出时,程序也会退出。

因为你也知道用户选择了哪种水果,所以让我们问一下他们打算购买多少,并告诉他们输入数量的格式。

use std::io;

fn main() {
    println!("欢迎来到水果市场!");
    println!("请选择要购买的水果。\n");

    let valid_inputs = ["苹果", "香蕉", "橘子", "芒果", "葡萄", "quit", "q"];

    'mart: loop {
        let mut user_input = String::new();
        let mut quantity = String::new();

        println!("\n可以购买的水果:苹果、香蕉、橘子、芒果、葡萄");
        println!("购买完成后,请输入“quit”或“q”。\n");

        // 读取用户输入
        io::stdin()
            .read_line(&mut user_input)
            .expect("无法读取用户输入。");
        user_input = user_input.trim().to_lowercase();

        // 验证用户输入
        let mut input_error = true;
        for input in valid_inputs {
            if input == user_input {
                input_error = false;
                break;
            }
        }

        // 处理无效输入
        if input_error {
            println!("错误: 请输入有效的输入");
            continue 'mart;
        }

        // 如果用户想要退出,就退出
        if user_input == "q" || user_input == "quit" {
            break 'mart;
        }

        // 获取数量
        println!(
            "\n你选择购买的水果是 \"{}\"。请输入以千克为单位的数量。
(1 千克 500 克的数量应该输入为 '1.5'。)",
            user_input
        );
        io::stdin()
            .read_line(&mut quantity)
            .expect("无法读取用户输入。");
    }
}

在第 11 行,我声明了另一个可变变量,它的值是一个空字符串,在第 48 行,我接受了用户的输入,但是这次是用户打算购买的水果的数量。

解析数量

我刚刚增加了一些代码,以已知的格式接受数量,但是这些数据被存储为字符串。我需要从中提取出浮点数。幸运的是,这可以通过 parse() 方法来完成。

就像 read_line() 方法一样,parse() 方法返回一个 Result 枚举。parse() 方法返回 Result 枚举的原因可以通过我们试图实现的内容来轻松理解。

我正在接受用户的字符串,并尝试将其转换为浮点数。浮点数有两个可能的值。一个是浮点数本身,另一个是小数。

字符串可以包含字母,但是浮点数不行。所以,如果用户输入的不是浮点数和小数,parse() 函数将会返回一个错误。

因此,这个错误也需要处理。我们将使用 expect() 函数来处理这个错误。

use std::io;

fn main() {
    println!("欢迎来到水果市场!");
    println!("请选择要购买的水果。\n");

    let valid_inputs = ["苹果", "香蕉", "橘子", "芒果", "葡萄", "quit", "q"];

    'mart: loop {
        let mut user_input = String::new();
        let mut quantity = String::new();

        println!("\n可以购买的水果:苹果、香蕉、橘子、芒果、葡萄");
        println!("购买完成后,请输入“quit”或“q”。\n");

        // 读取用户输入
        io::stdin()
            .read_line(&mut user_input)
            .expect("无法读取用户输入。");
        user_input = user_input.trim().to_lowercase();

        // 验证用户输入
        let mut input_error = true;
        for input in valid_inputs {
            if input == user_input {
                input_error = false;
                break;
            }
        }

        // 处理无效输入
        if input_error {
            println!("错误: 请输入有效的输入");
            continue 'mart;
        }

        // 如果用户想要退出,就退出
        if user_input == "q" || user_input == "quit" {
            break 'mart;
        }

        // 获取数量
        println!(
            "\n你选择购买的水果是 \"{}\"。请输入以千克为单位的数量。
(1 千克 500 克的数量应该输入为 '1.5'。)",
            user_input
        );
        io::stdin()
            .read_line(&mut quantity)
            .expect("无法读取用户输入。");

        let quantity: f64 = quantity
            .trim()
            .parse()
            .expect("请输入有效的数量。");

    }
}

如你所见,我通过变量遮蔽将解析后的浮点数存储在变量 quantity 中。为了告诉 parse() 函数,我的意图是将字符串解析为 f64,我手动将变量 quantity 的类型注释为 f64

现在,parse() 函数将会解析字符串并返回一个 f64 或者一个错误,expect() 函数将会处理这个错误。

计算价格 + 最后的修饰

现在我们知道了用户想要购买的水果及其数量,现在是时候进行计算了,并让用户知道结果/总价了。

为了真实起见,我将为每种水果设置两个价格。第一个价格是零售价,我们在购买少量水果时向水果供应商支付的价格。水果的第二个价格是当有人批量购买水果时支付的批发价。

批发价将会在订单数量大于被认为是批发购买的最低订单数量时确定。这个最低订单数量对于每种水果都是不同的。每种水果的价格都是每千克多少卢比。

想好了逻辑,下面是最终的程序。

use std::io;

const APPLE_RETAIL_PER_KG: f64 = 60.0;
const APPLE_WHOLESALE_PER_KG: f64 = 45.0;

const BANANA_RETAIL_PER_KG: f64 = 20.0;
const BANANA_WHOLESALE_PER_KG: f64 = 15.0;

const ORANGE_RETAIL_PER_KG: f64 = 100.0;
const ORANGE_WHOLESALE_PER_KG: f64 = 80.0;

const MANGO_RETAIL_PER_KG: f64 = 60.0;
const MANGO_WHOLESALE_PER_KG: f64 = 55.0;

const GRAPES_RETAIL_PER_KG: f64 = 120.0;
const GRAPES_WHOLESALE_PER_KG: f64 = 100.0;

fn main() {
    println!("欢迎来到水果市场!");
    println!("请选择要购买的水果。\n");

    let valid_inputs = ["苹果", "香蕉", "橘子", "芒果", "葡萄", "quit", "q"];

    'mart: loop {
        let mut user_input = String::new();
        let mut quantity = String::new();

        println!("\n可以购买的水果:苹果、香蕉、橘子、芒果、葡萄");
        println!("购买完成后,请输入“quit”或“q”。\n");

        // 读取用户输入
        io::stdin()
            .read_line(&mut user_input)
            .expect("无法读取用户输入。");
        user_input = user_input.trim().to_lowercase();

        // 验证用户输入
        let mut input_error = true;
        for input in valid_inputs {
            if input == user_input {
                input_error = false;
                break;
            }
        }

        // 处理无效输入
        if input_error {
            println!("错误: 请输入有效的输入");
            continue 'mart;
        }

        // 如果用户想要退出,就退出
        if user_input == "q" || user_input == "quit" {
            break 'mart;
        }

        // 获取数量
        println!(
            "\n你选择购买的水果是 \"{}\"。请输入以千克为单位的数量。
(1 千克 500 克的数量应该输入为 '1.5'。)",
            user_input
        );
        io::stdin()
            .read_line(&mut quantity)
            .expect("无法读取用户输入。");

        let quantity: f64 = quantity
            .trim()
            .parse()
            .expect("请输入有效的数量。");

        total += calc_price(quantity, user_input);
    }

    println!("\n\n总价是 {} 卢比。", total);
}

fn calc_price(quantity: f64, fruit: String) -> f64 {
    if fruit == "apple" {
        price_apple(quantity)
    } else if fruit == "banana" {
        price_banana(quantity)
    } else if fruit == "orange" {
        price_orange(quantity)
    } else if fruit == "mango" {
        price_mango(quantity)
    } else {
        price_grapes(quantity)
    }
}

fn price_apple(quantity: f64) -> f64 {
    if quantity > 7.0 {
        quantity * APPLE_WHOLESALE_PER_KG
    } else {
        quantity * APPLE_RETAIL_PER_KG
    }
}

fn price_banana(quantity: f64) -> f64 {
    if quantity > 4.0 {
        quantity * BANANA_WHOLESALE_PER_KG
    } else {
        quantity * BANANA_RETAIL_PER_KG
    }
}

fn price_orange(quantity: f64) -> f64 {
    if quantity > 3.5 {
        quantity * ORANGE_WHOLESALE_PER_KG
    } else {
        quantity * ORANGE_RETAIL_PER_KG
    }
}

fn price_mango(quantity: f64) -> f64 {
    if quantity > 5.0 {
        quantity * MANGO_WHOLESALE_PER_KG
    } else {
        quantity * MANGO_RETAIL_PER_KG
    }
}

fn price_grapes(quantity: f64) -> f64 {
    if quantity > 2.0 {
        quantity * GRAPES_WHOLESALE_PER_KG
    } else {
        quantity * GRAPES_RETAIL_PER_KG
    }
}

对比之前的版本,我做了一些改动……

水果的价格可能会波动,但是在我们程序的生命周期内,这些价格不会波动。所以我将每种水果的零售价和批发价存储在常量中。我将这些常量定义在 main() 函数之外(即全局常量),因为我不会在 main() 函数内计算每种水果的价格。这些常量被声明为 f64,因为它们将与 quantity 相乘,而 quantityf64。记住,Rust 没有隐式类型转换 ?

当水果名称和用户想要购买的数量被存下来之后,calc_price() 函数被调用来计算用户指定数量的水果的价格。这个函数接受水果名称和数量作为参数,并将价格作为 f64 返回。

当你看到 calc_price() 函数的内部时,你会发现它是许多人所说的包装函数。它被称为包装函数,因为它调用其他函数来完成它的脏活。

因为每种水果都有不同的最低订单数量,才能被认为是批发购买,为了确保代码在未来可以轻松维护,每种水果都有单独的函数负责计算价格。

所以,calc_price() 函数所做的就是确定用户选择了哪种水果,并调用相应的函数来计算所选水果的价格。这些水果特定的函数只接受一个参数:数量。这些水果特定的函数将价格作为 f64 返回。

现在,price_*() 函数只做一件事。它们检查订单数量是否大于被认为是批发购买的最低订单数量。如果是这样,quantity 将会乘以水果的每千克批发价格。否则,quantity 将会乘以水果的每千克零售价格。

由于乘法行末尾没有分号,所以函数返回乘积。

如果你仔细看看 calc_price() 函数中水果特定函数的函数调用,这些函数调用在末尾没有分号。这意味着,price_*() 函数返回的值将会被 calc_price() 函数返回给它的调用者。

而且 calc_price() 函数只有一个调用者。这个调用者在 mart 循环的末尾,这个调用者使用这个函数返回的值来增加 total 的值。

最终,当 mart 循环结束(当用户输入 qquit 时),存储在变量 total 中的值将会被打印到屏幕上,并且用户将会被告知他/她需要支付的价格。

总结

这篇文章中,我使用了之前讲解的 Rust 编程语言的所有主题来创建一个简单的程序,这个程序仍然在某种程度上展示了一个现实世界的问题。

现在,我写的代码肯定可以用一种更符合编程习惯的方式来写,这种方式最好地使用了 Rust 的喜爱特性,但是我还没有讲到它们!

所以,敬请关注后续的 将 Rust 带入下一个层次 系列,并学习更多 Rust 编程语言的内容!

Rust 基础系列到此结束。欢迎你的反馈。

(题图:MJ/6d486f23-e6fe-4bef-a28d-df067ef2ec06)


via: https://itsfoss.com/milestone-rust-program/

作者:Pratham Patel 选题:lkxed 译者:Cubik65536 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

Openbox 是一款适用于 Linux 的轻量级、可配置的窗口堆叠式管理器。它支持许多标准,适用于任何桌面环境。

可能让你惊讶的是,LXDE 和 LXQT 桌面环境都是基于 Openbox 构建的。你甚至可以用它替换桌面环境的窗口管理器。

当然,你几乎可以在任何 Linux 发行版上安装 Openbox。不过,配置它需要时间和精力。

一个更简单的方法是使用提供 Openbox 变种的发行版。在本文中,我列出了一些能够让你轻松体验 Openbox 的发行版。

注意: 列表按字母顺序排列,不代表排名。

1、Archcraft

archcraft live media with neofetch open in terminal

如果你想体验 Openbox 窗口管理器,Archcraft 是一个不错的选择。不像其他发行版,Openbox 是这个发行版的默认桌面环境,所以你可以期待它的表现优秀。

它提供了一个精简且轻量的环境,只需不到 500MB 的内存即可运行,而且外观上也没有降级。其界面元素风格统一。

你可以通过简单点击来切换主题,如果你喜欢,它还提供了类似 Windows 的用户界面。

对于资深用户来说,它内置了对 AUR 和 Chaotic-AUR 的支持。与其他发行版不同的是,它提供了最佳的开箱即用体验。

Archcraft

2、ArcolinuxB Openbox

arcolinuxb openbox live media with neofetch open in terminal

如果你想学习 Arch(Arcolinux 项目的主要目标),ArcolinuxB Openbox 是一个非常优秀的 Linux 桌面发行版。

它是 ArcolinuxB 项目的众多变种之一。你可能会遇到一些学习曲线和不完善的地方。

与 Archcraft 不同,你在这里不会看到完全统一的界面元素,可能需要进行一些调整来获得良好的体验。

ArcolinuxB

3、AV Linux MX Edition

av linux live media with neofetch open in terminal

AV Linux MX Edition 是基于 MX Linux 的一个版本,使用 Openbox 作为窗口管理器。

它使用高性能的 Liquorix 内核,提供了音频方面爱好者所需要的低延迟。它还通过 Wine-staging 支持 Windows 音频。

如果你是一位音频专业人士并且使用 Linux,你可能想尝试一下这个发行版。对于一些用户来说,它可能显得臃肿,因为预装了许多应用程序。

AV Linux

4、Bunsenlabs Linux

bunsenlabs live media with neofetch open in terminal

BunsenLabs Linux 是一个基于 Debian 的发行版,提供了一个轻量且易于定制的 Openbox 桌面环境。该项目是 CrunchBang Linux 的分支。

它仍然基于 Debian 10,所以在软件仓库中你将获取到较旧版本的应用程序。然而,由于包含了硬件和多媒体支持,它具有相当好的开箱即用体验,与 Debian 不同。

它的界面与 Archcraft 类似,还提供了丰富的 Conky 配置选项。

BunsenLabs Linux

5、Crunchbang++

crunchbangplusplus live media with neofetch open in terminal

正如其名字所示,Crunchbang++ 是 Crunchbang 的一个分支,并尽量保持与原版接近。

对于不了解的人来说,Crunchbang 是一个几乎在十年前停止开发的流行 Openbox 发行版。

Crunchbang++ 是一个极简且轻量级的发行版。它可能会让一些用户怀旧。它基于 Debian 11,相比 Bunsenlabs 可以提供更新的软件包。

Crunchbang++

6、Mabox Linux

mabox linux live media with neofetch open in terminal

Mabox Linux 是一个基于 Manjaro 的现代发行版,专注于定制化或 美化 ricing

由于使用了轻量级组件,它是一个极简且快速的系统。由于采用滚动更新机制,你还可以获得更新的软件版本。

该发行版的一些独特功能包括 Colorizer(根据壁纸更改强调颜色)、Quicktiling(用于轻松进行窗口平铺)和可定制的菜单/面板。这么多的定制化功能可能让一些极简主义者感到有些害怕。

Mabox Linux

7、Sparky Linux Openbox

sparky linux openbox live media with neofetch open in terminal

Sparky Linux 是一个基于 Debian 的 Linux 发行版,也提供 Openbox 作为另一种桌面环境选择。

它有一个基于 Debian Testing 的版本,对于需要更新应用程序的用户来说非常有用。它致力于为 Debian 提供开箱即用的体验,并将定制化留给用户。因此,在这里你可能不会看到太多花哨的界面效果。

Sparky Linux

总结

还有其他几个 Linux 发行版可以安装 Openbox。

但在这个列表中,我列出了那些在 立付 Live 介质中提供 Openbox ,其中一些还将 Openbox 作为默认桌面环境。

你最喜欢的 Openbox 发行版是哪个?你喜欢它预先定制好还是更喜欢自己进行定制?欢迎你提出建议。

(题图:MJ/19d5a524-8229-4c14-89d1-f8287c34eeae)


via: https://itsfoss.com/openbox-distros/

作者:Anuj Sharma 选题:lkxed 译者:ChatGPT 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

RHEL 源代码将只放在 CentOS Stream 上

在没有宣布废弃 CentOS 之前,RHEL 的源代码会发布在 git.centos.org 上,那时候 CentOS 还是 RHEL 的下游。之后,在 CentOS Stream 成为 RHEL 的上游后,该仓库得以保留。红帽宣布,“CentOS Stream 将成为公开发布 RHEL 相关源代码的唯一仓库。对于红帽的客户和合作伙伴,源代码将继续通过红帽客户门户网站提供。”并进一步澄清,“这一变化并不意味着CentOS 项目、CentOS Stream 或 CentOS SIG 的源代码可用性有任何变化。”但这一举动引来了社区的一些人的担心,“Alma Linux,Rocky Linux,Oracle Linux 等将更难提供与 RHEL 版本 1:1 的二进制兼容的构建。”

消息来源:Phoronix
老王点评:我认为,从目前的声明上看,这种担心或许是多余的,我们只需要观察之后的 RHEL 和 CentOS Stream 之间差异有多大就好了。但从对这个事情的反应可以看出,社区对红帽的信任在降低。

Reddit 用户涌入 Lemmy

类似马斯克收购 Twitter 后 Twitter 难民涌入 Mastodon,大量 Reddit 难民涌入了 Lemmy 实例。其中,lemmy.world 实例的总用户数达到了 3.8 万。突然间,Lemmy 已经成为最大的联邦宇宙项目之一。在 Reddit 社区重新开放之后,部分用户可能会离开,但也许有更多用户会留下。

消息来源:Lemmy
老王点评:这些联邦宇宙社区会得到更多用户,但 Twitter、Reddit 失血更多。

C++ 26 标准正在制定中

C++ 版本以其发布的年份命名,并遵循三年的周期。国际标准化组织 C++ 标准委员会发布了 C++ 26 的进展情况。根据其时间表,新语言功能的截止日期是 2024 年第三季度,功能冻结将在 2025 年第一季度。委员会已经通过了 40 份 “修改文件”,增加了 _ 字符通配符,基本字符集添加了 @$`。此外,在 C++ 26 中还支持了危险指针。危险指针是指 “只有其所有者可以设置其值,而任何数量的线程都可以读取其值” 的指针,它很难导致非常常见的内存安全问题。

消息来源:Dev Class
老王点评:在我看来,C++ 标准的演进有一定的意义,但是这并不能阻止其传统的领域被 Rust、Carbon 等新底层语言的侵蚀。

我们已经学习了 R 语言的基础知识,包括其语法以及语法所对应的语义,现在准备使用 R 向统计学领域进发。本文是 R 系列的第十一篇文章,我们将学习如何使用 R 语言 stats 包中提供的统计函数。

与此系列之前的文章一样,我们将使用安装在 Parabola GNU/Linux-libre(x86-64)上的 R 4.1.2 版本来运行文中的代码。

$ R --version
R version 4.1.2 (2021-11-01) -- "Bird Hippie"
Copyright (C) 2021 The R Foundation for Statistical Computing
Platform: x86_64-pc-linux-gnu (64-bit)

R is free software and comes with ABSOLUTELY NO WARRANTY.
You are welcome to redistribute it under the terms of the
GNU General Public License versions 2 or 3.
For more information about these matters see https://www.gnu.org/licenses/

mean 函数

在 R 中 mean 函数用来计算算术平均值。该函数接受一个 R 对象 x 作为参数,以及一个 trim 选项来在计算均值之前剔除任意比例的数据(LCTT 译注:比如对于一个含有 7 个元素的向量 x,设置 trim 为 0.2 表示分别去掉 x 中最大和最小的前 20% —— 即 1.4 个 —— 的元素,所去掉的元素的个数会向下取整,所以最终会去掉 1 个最大值和 1 个最小值;trim 取值范围为 [0, 0.5],默认为 0)。 逻辑参数 logical argument TRUEFALSEna.rm 可以设置是否忽略空值(NA)。该函数的语法如下:

mean(x, trim = 0, na.rm = FALSE, ...)

该函数支持数值、逻辑值、日期和 时间区间 time intervals 。下面是使用 mean 函数的一些例子:

> mean(c(1, 2, 3))
2

> mean(c(1:5, 10, 20))
6.428571

> mean(c(FALSE, TRUE, FALSE))
0.3333333

> mean(c(TRUE, TRUE, TRUE))
1

我们使用 UCI 机器学习库提供的一个采集自葡萄牙银行机构的“银行营销数据集”作为样本数据。该数据可用于公共研究,包含 4 个 csv 文件,我们使用 read.csv() 函数导入其中的 bank.csv 文件。

> bank <- read.csv(file="bank.csv", sep=";")

> bank[1:3,]
  age        job marital education default balance housing loan  contact day
1  30 unemployed married   primary      no    1787      no   no cellular  19
2  33   services married secondary      no    4789     yes  yes cellular  11
3  35 management  single  tertiary      no    1350     yes   no cellular  16
  month duration campaign pdays previous poutcome  y
1   oct       79        1    -1        0  unknown no
2   may      220        1   339        4  failure no
3   apr      185        1   330        1  failure no

下面是计算 age 列均值的示例:

> mean(bank$age)
41.1701

median 函数

R 语言 stats 包中的 median 函数用来计算样本的中位数。该函数接受一个数值向量 x,以及一个逻辑值 na.rm 用来设置在计算中位数之前是否去除 NA 值。该函数的语法如下:

median(x, na.rm = FALSE, ...)

下面是使用该函数的两个例子:

> median(3:5)
4
> median(c(3:5, 50, 150))
[1] 5

现在我们可以计算银行数据中 age 列的中位数:

> median(bank$age)
39

pair 函数

pair 函数用来合并两个向量,接受向量 x 和向量 y 两个参数。xy 的长度必须相等。

Pair(x, y)

该函数返回一个 Pair 类的列数为 2 的矩阵,示例如下:

> Pair(c(1,2,3), c(4,5,6))
     x y
[1,] 1 4
[2,] 2 5
[3,] 3 6
attr(,"class")
[1] "Pair"

该函数常用于像 T 检验和 Wilcox 检验等的 配对检验 paired test

dist 函数

dist 函数用来计算数据矩阵中各行之间的距离矩阵,接受以下参数:

参数描述
x数值矩阵
method距离测量方法
diag若为 TRUE,则打印距离矩阵的对角线
upper若为 TRUE,则打印距离矩阵的上三角
p闵可夫斯基距离的幂次(见下文 LCTT 译注)

该函数提供的距离测量方法包括: 欧式距离 euclidean 最大距离 maximum 曼哈顿距离 manhattan 堪培拉距离 canberra 二进制距离 binary 闵可夫斯基距离 minkowski ,默认为欧式距离。

LCTT 译注:

  • 欧式距离指两点之间线段的长度,比如二维空间中 A 点 和 B 点 的欧式距离是
  • 最大距离指 n 维向量空间中两点在各维度上的距离的最大值,比如 A 点 (3,6,8,9) 和 B 点 (1,8,9,10) 之间的最大距离是 ,等于 2;
  • 曼哈顿距离指 n 维向量空间中两点在各维度上的距离之和,比如二维空间中 A 点 和 B 点 之间的曼哈顿距离是
  • 堪培拉距离的公式是
  • 二进制距离首先将两个向量中的各元素看作其二进制形式,然后剔除在两个向量中对应值均为 0 的维度,最后计算在剩下的维度上两个向量间的对应值不相同的比例,比如 V1=(1,3,0,5,0) 和 V2=(11,13,0,15,10) 的二进制形式分别是 (1,1,0,1,0) 和 (1,1,0,1,1),其中第 3 个维度的对应值均为 0,剔除该维度之后为 (1,1,1,0) 和 (1,1,1,1),在剩余的 4 个维度中只有最后一个维度在两个向量之间的值不同,最终结果为 0.25;
  • 闵可夫斯基距离是欧式距离和曼哈顿距离的推广,公式是 ,当 p = 1 时相当于曼哈顿距离,当 p = 2 时相当于欧式距离。

下面是使用欧式距离计算 age 列距离矩阵的示例:

> dist(bank$age, method="euclidean", diag=FALSE, upper=FALSE, p=2)
      1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
2     3
3     5  2
4     0  3  5
5    29 26 24 29
6     5  2  0  5 24
7     6  3  1  6 23  1
8     9  6  4  9 20  4  3
9    11  8  6 11 18  6  5  2
10   13 10  8 13 16  8  7  4  2
11    9  6  4  9 20  4  3  0  2  4
12   13 10  8 13 16  8  7  4  2  0  4
13    6  3  1  6 23  1  0  3  5  7  3  7
14   10 13 15 10 39 15 16 19 21 23 19 23 16
15    1  2  4  1 28  4  5  8 10 12  8 12  5 11
16   10  7  5 10 19  5  4  1  1  3  1  3  4 20  9
17   26 23 21 26  3 21 20 17 15 13 17 13 20 36 25 16
18    7  4  2  7 22  2  1  2  4  6  2  6  1 17  6  3 19
19    5  8 10  5 34 10 11 14 16 18 14 18 11  5  6 15 31 12
20    1  2  4  1 28  4  5  8 10 12  8 12  5 11  0  9 25  6  6
21    8  5  3  8 21  3  2  1  3  5  1  5  2 18  7  2 18  1 13  7
22   12  9  7 12 17  7  6  3  1  1  3  1  6 22 11  2 14  5 17 11  4
23   14 11  9 14 15  9  8  5  3  1  5  1  8 24 13  4 12  7 19 13  6  2
     26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
...

改用二进制距离的计算结果如下:

> dist(bank$age, method="binary", diag=FALSE, upper=FALSE, p=2)
     1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
2    0
3    0 0
4    0 0 0
5    0 0 0 0
6    0 0 0 0 0
7    0 0 0 0 0 0
8    0 0 0 0 0 0 0
9    0 0 0 0 0 0 0 0
10   0 0 0 0 0 0 0 0 0
11   0 0 0 0 0 0 0 0 0  0
12   0 0 0 0 0 0 0 0 0  0  0
13   0 0 0 0 0 0 0 0 0  0  0  0
14   0 0 0 0 0 0 0 0 0  0  0  0  0
15   0 0 0 0 0 0 0 0 0  0  0  0  0  0
16   0 0 0 0 0 0 0 0 0  0  0  0  0  0  0
17   0 0 0 0 0 0 0 0 0  0  0  0  0  0  0  0
18   0 0 0 0 0 0 0 0 0  0  0  0  0  0  0  0  0
19   0 0 0 0 0 0 0 0 0  0  0  0  0  0  0  0  0  0
20   0 0 0 0 0 0 0 0 0  0  0  0  0  0  0  0  0  0  0
21   0 0 0 0 0 0 0 0 0  0  0  0  0  0  0  0  0  0  0  0
22   0 0 0 0 0 0 0 0 0  0  0  0  0  0  0  0  0  0  0  0  0
23   0 0 0 0 0 0 0 0 0  0  0  0  0  0  0  0  0  0  0  0  0  0
     29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53

quantile 函数

quantile 函数用于计算数值向量 x 的分位数及其对应的概率。当设置 na.rmTRUE 时,该函数将忽略向量中的 NANaN 值。概率 0 对应最小观测值,概率 1 对应最大观测值。该函数的语法如下:

quantile(x, ...)

quantile 函数接受以下参数:

参数描述
x数值向量
probs概率向量,取值为 [0, 1](LCTT 译注:默认为 (0, 0.25, 0.5, 0.75, 1)
na.rm若为 TRUE,忽略向量中的 NANaN
names若为 TRUE,在结果中包含命名属性
type整数类型,用于选择任意一个九种分位数算法(LCTT 译注:默认为 7)
digits小数精度
传递给其他方法的额外参数

rnorm 函数可用于生成正态分布的随机数。它可以接受要生成的观测值的数量 n,一个均值向量以及一个标准差向量。下面是一个计算 rnorm 函数生成的随机数的四分位数的示例:

> quantile(x <- rnorm(100))
    0%          25%          50%          75%         100%
-1.978171612 -0.746829079 -0.009440368  0.698271134  1.897942805

下面是生成银行年龄数据对应概率下的分位数的示例:

> quantile(bank$age, probs = c(0.1, 0.5, 1, 2, 5, 10, 50)/100)
0.1% 0.5%   1%   2%   5%  10%  50%
20.0 22.6 24.0 25.0 27.0 29.0 39.0

IQR 函数

IQR 函数用于计算向量中数值的 四分位距 interquartile range 。其语法如下:

IQR(x, na.rm = FALSE, type = 7)

参数 type 指定了一个整数以选择分位数算法,该算法在 Hyndman and Fan (1996) 中进行了讨论。下面是计算银行年龄四分位距的示例:

> IQR(bank$age, na.rm = FALSE, type=7)
16

sd 函数

sd 函数用来计算一组数值中的标准差。该函数接受一个 数值向量 numeric vector x 和一个逻辑值 na.rmna.rm 用来设置在计算时是否忽略缺失值。该函数的语法如下:

sd(x, na.rm = FALSE)

对于长度为 0 或 1 的向量,该函数返回 NA。下面是两个例子:

> sd(1:10)
3.02765

> sd(1)
NA

下面是计算 age 列标准差的示例:

> sd(bank$age)
10.57621

R 语言 stats 包中还有很多其他函数,鼓励你自行探索。

(题图:MJ/ee6b533d-69fc-4baa-a985-cc4e499b5029)


via: https://www.opensourceforu.com/2022/08/the-functions-in-the-r-stats-package/

作者:Shakthi Kannan 选题:lkxed 译者:tanloong 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出