草庐IT

【Linux】在Linux上写一个进度条小程序

安 度 因 2023-08-11 原文

👑作者主页:@安 度 因
🏠学习社区:StackFrame
📖专栏链接:Linux

文章目录

如果无聊的话,就来逛逛 我的博客栈 吧! 🌹

一、前言

在前三篇文章中,我们分别学习了 vim 、gcc 以及 make/makefile 。而在今天,我们将基于前三节课认识的基础上,并结合一些与回车换行、缓冲区有关的知识,在 Linux 上写下一个简易的进度条小程序。

成品展示

今天的内容比较轻松,只需要了解两个知识点,这个小程序就很容易写出来了,让我们开始今天的学习。

二、理解 ‘\r’ 与 ‘\n’

C 语言中有很多字符,而字符大体分为两类:可显字符、控制字符。

控制字符不可显示,例如 \r 和 \n 就是控制字符。

而在我们平时打字时,一行写满了需要换行,但是新起一行有很多种,例如:

这样虽然新起一行了,但是不是我们想要的结果。

我们通常新起一行是在第二行的最左端,但是对于这个结果其实有两个操作:

  1. 跳转到第二行
  2. 回到第二行的最左端

有了这个基本概念,再来谈 \r 和 \n 的作用:

  • \r :回车 - 回到文本行的开头
  • \n:换行 - 新起一行

所以,其实我们 平时泛指的换行实际上是 回车 + 换行

且在语言范畴下,例如 C 语言,换行就可以达到 回车 + 换行 的效果。在平常,这一操作还是两个步骤。

三、行缓冲

行缓冲这个概念认识。

1、提出问题

首先先了解一下两个库函数:

  • sleep : Linux 下的休眠函数,单位是秒。头文件为 #include <unistd.h>
  • fflush :刷新缓冲区

代码1:

#include <stdio.h>
int main()
{
    printf("hello xxx");
    sleep(3);
    return 0;
}

现象:

分析:

光标停留在文本行的开头,但是字符串没有被打印。

反而像是 sleep 函数先起作用,然后 printf 函数再从光标处开始打印。

打印完之后,shell 提示符紧跟着字符串后显示。

代码2:

#include <stdio.h>
int main()
{
    printf("hello xxx\n");
    sleep(3);
    return 0;
}

现象:

分析:

printf 打印的字符串先显示在终端上,光标位于字符串的下一行。

sleep 函数使程序休眠 3 秒后,shell 提示符从光标位置开始显示。

代码3:

#include <stdio.h>
int main()
{
    printf("hello xxx\r");
    sleep(3);
    return 0;
}

现象:

分析:

printf 打印的字符串没有显示到终端,光标一直停留在该打印字符串的一行

sleep 函数休眠三秒后,shell 提示符直接打印在了屏幕上。

并没有看到字符串。

观察上面的现象,我们提出几个问题:

  1. 代码 1 好像是先执行了 sleep ,在执行 printf ,是这样吗?
  2. 代码 2 加上了 ‘\n’ ,字符串一开始就显示了,为什么?
  3. 代码 3 好像什么都没打印,这是为什么?

在解答这些问题之后,我们先了解一下行缓冲。

2、认识行缓冲

在内存中预留了一块空间,用来缓冲输入或输出的数据,这个保留的空间被称为缓冲区。

我们之前或多或少都听说过缓冲区。

在代码 1 中,由于程序是按照数据执行的,所以必定是先执行 printf 。

但是数据没有显示,所以这时候,数据就一定被保存在某个位置,保存的位置就是缓冲区。

而要让数据显示,是需要刷新缓冲区的。

行缓冲是缓冲区刷新策略的一种,在行缓冲模式下,当输入和输出中遇到 ‘\n’ 换行时,就会刷新缓冲区

有了这个概念,我们继续分析问题。

3、解答与拓展

解答

问题1:代码 1 好像是先执行了 sleep ,在执行 printf ,是这样吗?

当然不是。

由于程序是按照顺序执行的,所以必定是先执行完 printf 在执行 sleep 。

而数据没有被显示出来的原因是:数据保存在缓冲区中,但是没有主动刷新,当程序退出后,保存在缓冲区中的数据被自动刷新出来了。

所以才会造成这种现象。

问题2:代码 2 加上了 \n ,字符串一开始就显示了,为什么?

这里由于是直接往显示器上打印,所以采用的刷新方式为行缓冲。

所以执行碰到 ‘\n’ 时,就会把在缓冲区中的(换行符之前)的内容全部刷新出来。

所以这段代码一开始就会有数据显示,然后再 sleep 休眠。

问题3:代码 3 好像什么都没打印,这是为什么?

之前说过 \r 是换行,所以当 printf 遇到 \r 时,就把光标移到开头。

sleep 睡眠后,当程序退出,shell 打印提示符时,就覆盖了字符串。

拓展

数据真的是临时保留在缓冲区里的吗?光标如何理解?

我们用一段代码来理解这两个问题:

#include <stdio.h>
#include <unistd.h>
int main()
{
    printf("hello xxx\r");
    fflush(stdout);
    sleep(3);
    return 0;
}

现象:

观察现象,我们发现当我们使用 fflush 主动刷新缓冲区后,数据就显示在了屏幕上;且因为 ‘\r’ 的原因,光标指向字符串开头;当打印 shell 提示符时,就直接从光标位置开始覆盖。

所以对于这两个问题,我们已经得到了答案:

  1. 数据被临时保存在于缓冲区中,通过刷新就可以显示
  2. 数据是从光标位置开始打印的。

一句话理解光标:光标和显示器匹配,光标在哪里,显示器打印的时候就从哪里开始打印

4、倒计时

基于对上面的理解,我们先实现一个简单的倒计时。

倒计时就是在屏幕上不断显示数字,每次在同一位置显示,并将之前的数据覆盖。

既然是每次要从头开始覆盖,那么就可以用 ‘\r’ 来实现每次回到行首,并且可以通过相应的格式化控制显示多位打印。

但是 ‘\r’ 不会主动刷新,所以要用 fflush 函数主动刷新缓冲区。

在每次刷新之后,使用 sleep 函数,间隔一定的时间。

由此,我们可以很轻松写出代码,例如写一个从 10 开始的倒计时:

#include <stdio.h>
#include <unistd.h>

int main()
{
    int i = 10;
    for (; i >= 0; i--) {
        // 位宽控制,\r 回到开头
        printf("%2d\r", i);
        fflush(stdout); // 主动刷新
        sleep(1); // 休眠
    }
    printf("\n"); // 换行,打印提示符
    return 0;
}

四、进度条

好了,接下来进入正题,我们开始写 进度条

进度条样式

  • 主体样式为两个中括号包裹,中间 => 推进的方式呈现,比如:[======>]
  • 主体右侧中括号位置保持不变,中间元素不断推进,比如:[=> ]
  • 显示当前加载进度,用 [num%] 显示,num 随着进度条的不断推进而变化
  • 显示加载样式,可以利用一个旋转的字符,例如 [\] 的样式,顺时针不断旋转

大约呈现状态为:[========>] [15%] [\]

采用多文件

文件存放在 proc 目录中

  • proc.h :函数声明
  • proc.c :进度条逻辑
  • main.c :函数调用

makefile 准备

由于采用多文件,所以依赖关系可以写成依赖文件列表的样式:

分块逻辑

  1. 进度条主体

预留进度条大小为 100 个 = ,外加 1 个 > ,加上保存 '\0' 的位置,用数组存储为 102 个单位。

进度条是一行中的,所以需要用到 '\r' ,每次都需要使用 fllush 主动刷新缓冲区。

每次刷新出数据之后,将 = 填充到数组中,并且显示 > 。在最后一次显示时,控制 > 不要显示。

然后休眠一小会。由于休眠用 sleep 函数太慢。所以可以用 usleep 函数休眠,usleep 函数的参数单位是微秒。

根据这个写出代码:

  1. 百分比显示

%% 显示为一个 % ,而 %d 为数字,这步很简单,只要在 printf 语句中加上内容:

printf("[%-100s][%d%%]\r", bar, i);
  1. 旋转光标

光标旋转方向为顺时针旋转,那么旋转时就可以用数组保存。

旋转每次显示内容分别为 | / - \\\ 代表一个 \ ,因为和 \ 结合的会被解析为转义字符,将其保存到字符串中。

而由于字符串一共就四个字符,所以输出的时候需要控制输出位置。

代码:

const char* str = "|/-\\"; // 字符串
printf("[%-100s][%d%%][%c]\r", bar, i, str[i % 4]); // 输出语句

完整代码

proc.h

#pragma once 

#include <stdio.h>

extern void process();

proc.c

#include "proc.h"
#include <string.h>
#include <unistd.h>

#define SIZE 102 // 数组大小
#define STYLE '='
#define FLAG '>'

void process()
{
    const char* str = "|/-\\";
    char bar[SIZE];
    memset(bar, '\0', sizeof(bar));
    int i = 0;
    while (i <= 100) {
      printf("[%-100s][%d%%][%c]\r", bar, i, str[i % 4]); // 格式控制
      fflush(stdout); // 刷新
      bar[i++] = STYLE; // 填充数据
      if (i != 100) {
          bar[i] = FLAG; // 如果不是最后一次则显示 >
      } 
      usleep(100000); // 休眠
    }
    printf("\n");
}

test.c

#include "proc.h"

int main()
{
    process();
    return 0;
}

进度条展示

五、结语

到这里,本篇博客就到此结束了。

今天的内容相对来说还是很简单的,我们的今天核心就是了解 ‘\r’ 和 ‘\n’ 的区别,并认识了行缓冲。只要了这两块知识,写起进度条就很轻松了。

大家感兴趣也可以下去试一试。

如果觉得 a n d u i n anduin anduin 写的不错的话,可以 点赞 + 收藏 + 评论 支持一下哦!我们下期见~

有关【Linux】在Linux上写一个进度条小程序的更多相关文章

  1. ruby - 在 Ruby 程序执行时阻止 Windows 7 PC 进入休眠状态 - 2

    我需要在客户计算机上运行Ruby应用程序。通常需要几天才能完成(复制大备份文件)。问题是如果启用sleep,它会中断应用程序。否则,计算机将持续运行数周,直到我下次访问为止。有什么方法可以防止执行期间休眠并让Windows在执行后休眠吗?欢迎任何疯狂的想法;-) 最佳答案 Here建议使用SetThreadExecutionStateWinAPI函数,使应用程序能够通知系统它正在使用中,从而防止系统在应用程序运行时进入休眠状态或关闭显示。像这样的东西:require'Win32API'ES_AWAYMODE_REQUIRED=0x0

  2. ruby - 如何指定 Rack 处理程序 - 2

    Rackup通过Rack的默认处理程序成功运行任何Rack应用程序。例如:classRackAppdefcall(environment)['200',{'Content-Type'=>'text/html'},["Helloworld"]]endendrunRackApp.new但是当最后一行更改为使用Rack的内置CGI处理程序时,rackup给出“NoMethodErrorat/undefinedmethod`call'fornil:NilClass”:Rack::Handler::CGI.runRackApp.newRack的其他内置处理程序也提出了同样的反对意见。例如Rack

  3. ruby - 使用 Vim Rails,您可以创建一个新的迁移文件并一次性打开它吗? - 2

    使用带有Rails插件的vim,您可以创建一个迁移文件,然后一次性打开该文件吗?textmate也可以这样吗? 最佳答案 你可以使用rails.vim然后做类似的事情::Rgeneratemigratonadd_foo_to_bar插件将打开迁移生成的文件,这正是您想要的。我不能代表textmate。 关于ruby-使用VimRails,您可以创建一个新的迁移文件并一次性打开它吗?,我们在StackOverflow上找到一个类似的问题: https://sta

  4. ruby-on-rails - Rails - 一个 View 中的多个模型 - 2

    我需要从一个View访问多个模型。以前,我的links_controller仅用于提供以不同方式排序的链接资源。现在我想包括一个部分(我假设)显示按分数排序的顶级用户(@users=User.all.sort_by(&:score))我知道我可以将此代码插入每个链接操作并从View访问它,但这似乎不是“ruby方式”,我将需要在不久的将来访问更多模型。这可能会变得很脏,是否有针对这种情况的任何技术?注意事项:我认为我的应用程序正朝着单一格式和动态页面内容的方向发展,本质上是一个典型的网络应用程序。我知道before_filter但考虑到我希望应用程序进入的方向,这似乎很麻烦。最终从任何

  5. ruby-on-rails - 渲染另一个 Controller 的 View - 2

    我想要做的是有2个不同的Controller,client和test_client。客户端Controller已经构建,我想创建一个test_clientController,我可以使用它来玩弄客户端的UI并根据需要进行调整。我主要是想绕过我在客户端中内置的验证及其对加载数据的管理Controller的依赖。所以我希望test_clientController加载示例数据集,然后呈现客户端Controller的索引View,以便我可以调整客户端UI。就是这样。我在test_clients索引方法中试过这个:classTestClientdefindexrender:template=>

  6. ruby - 在 Ruby 中编写命令行实用程序 - 2

    我想用ruby​​编写一个小的命令行实用程序并将其作为gem分发。我知道安装后,Guard、Sass和Thor等某些gem可以从命令行自行运行。为了让gem像二进制文件一样可用,我需要在我的gemspec中指定什么。 最佳答案 Gem::Specification.newdo|s|...s.executable='name_of_executable'...endhttp://docs.rubygems.org/read/chapter/20 关于ruby-在Ruby中编写命令行实用程序

  7. ruby-on-rails - Rails 应用程序之间的通信 - 2

    我构建了两个需要相互通信和发送文件的Rails应用程序。例如,一个Rails应用程序会发送请求以查看其他应用程序数据库中的表。然后另一个应用程序将呈现该表的json并将其发回。我还希望一个应用程序将存储在其公共(public)目录中的文本文件发送到另一个应用程序的公共(public)目录。我从来没有做过这样的事情,所以我什至不知道从哪里开始。任何帮助,将不胜感激。谢谢! 最佳答案 无论Rails是什么,几乎所有Web应用程序都有您的要求,大多数现代Web应用程序都需要相互通信。但是有一个小小的理解需要你坚持下去,网站不应直接访问彼此

  8. ruby - 无法运行 Rails 2.x 应用程序 - 2

    我尝试运行2.x应用程序。我使用rvm并为此应用程序设置其他版本的ruby​​:$rvmuseree-1.8.7-head我尝试运行服务器,然后出现很多错误:$script/serverNOTE:Gem.source_indexisdeprecated,useSpecification.Itwillberemovedonorafter2011-11-01.Gem.source_indexcalledfrom/Users/serg/rails_projects_terminal/work_proj/spohelp/config/../vendor/rails/railties/lib/r

  9. ruby-on-rails - Rails 应用程序中的 Rails : How are you using application_controller. rb 是新手吗? - 2

    刚入门rails,开始慢慢理解。有人可以解释或给我一些关于在application_controller中编码的好处或时间和原因的想法吗?有哪些用例。您如何为Rails应用程序使用应用程序Controller?我不想在那里放太多代码,因为据我了解,每个请求都会调用此Controller。这是真的? 最佳答案 ApplicationController实际上是您应用程序中的每个其他Controller都将从中继承的类(尽管这不是强制性的)。我同意不要用太多代码弄乱它并保持干净整洁的态度,尽管在某些情况下ApplicationContr

  10. ruby-on-rails - 如果 Object::try 被发送到一个 nil 对象,为什么它会起作用? - 2

    如果您尝试在Ruby中的nil对象上调用方法,则会出现NoMethodError异常并显示消息:"undefinedmethod‘...’fornil:NilClass"然而,有一个tryRails中的方法,如果它被发送到一个nil对象,它只返回nil:require'rubygems'require'active_support/all'nil.try(:nonexisting_method)#noNoMethodErrorexceptionanymore那么try如何在内部工作以防止该异常? 最佳答案 像Ruby中的所有其他对象

随机推荐