TLCL之第三章(5)


第三章

8. 格式化输出

在这一章中,主要关注那些用来格式化输出的程序。
nl - 添加行号
fold - 限制文件列宽
fmt - 一个简单的文件格式转换器
pr - 让文本为打印做好准备
printf - 格式化数据并打印出来
groff - 一个文件格式化系统

1. 简单的格式化工具

nl - 添加行号

nl它为文件添加行数,在它最简单的用途中,它相当于cat -n:

[me@linuxbox ~]$ nl distros.txt | head

像cat,nl既能接受多个文件作为命令行参数,也能接受标准输入。然而,nl有一个相当数量的选项并支持一个简单的标记方式去允许更多复杂的方式的计算。
nl在计算文件行数的时候支持一个叫"逻辑页面"的概念。这允许nl在计算的时候去重设(再一次开始)可数的序列。用到那些选项的时候,可以设置一个特殊的开始值,并且在某个可限定的程度上还能设置它的格式。一个逻辑页面被进一步分为header,body和footer这样的元素。在每一个部分中,数行数可以被重设,并且/或被设置成另一个格式。如果nl同时处理多个文件,它会把他们当成一个单一的文本流。文本流中的部分被一些相当古怪的标记的存在加进了文本。

nl标记

标记含义
\:\:\:逻辑页页眉开始处
\:\:逻辑页主体开始处
\:逻辑页页脚开始处

每一个上述的标记元素肯定在自己的行中单独出现。在处理完一个标记元素之后,nl把它从文本流中删除。

常用的nl选项

选项含义
-b style把body按被要求方式数行,可以是一下方式:a=数所有行。t=数非空行,这是默认设置。n=无,pregexp=只数哪些匹配了正则表达式的行
-f style将footer按要求设置数,默认无
-h style将header按要求设置数,默认无
-i number将页面增加量设置为数字。默认是1
-n format设置数的格式,格式可以是 ln=左偏,没有前导0;rn=右偏,没有前导0;rz=右偏,有前导零
-p不要在没一个逻辑页面的开始重设页面数
-s string在每一个行的末尾加字符作为分割符号。默认是单个的tab
-v number将每一个逻辑页面的第一行设置成数字。默认是1
-w width将行数的宽度设置。默认是6

fold - 限制文件行宽

折叠是将文本的行限制到特定的宽的过程。fold接受一个或多个文件及标准输入。将一个简单的文本流fold:

[me@linuxbox ~]$ echo "The quick brown fox jumped over the lazy dog." | fold -w 12
The quick br
own fox jump
ed over the
lazy dog.

默认行宽是80,注意到文本行不会因为单词边界而不会被分解,增加的-s选项将让fold分解到最后可用的空白字符,即会考虑单词边界:

[me@linuxbox ~]$ echo "The quick brown fox jumped over the lazy dog."
| fold -w 12 -s
The quick
brown fox
jumped over
the lazy
dog.

fmt - 一个简单的文本格式器

fmt程序同样折叠文本,但添加了很多功能。它接受文本或标准输入并且在文本流中呈现段落转换。它主要是填充和连接文本行,同时保留空白符和缩进。

‘fmt’ reads from the specified FILE arguments (or standard input if
none are given), and writes to standard output.

   By default, blank lines, spaces between words, and indentation are
preserved in the output; successive input lines with different
indentation are not joined; tabs are expanded on input and introduced on
output.

   ‘fmt’ prefers breaking lines at the end of a sentence, and tries to
avoid line breaks after the first word of a sentence or before the last
word of a sentence.  A "sentence break" is defined as either the end of
a paragraph or a word ending in any of ‘.?!’, followed by two spaces or
end of line, ignoring any intervening parentheses or quotes.  Like TeX,
‘fmt’ reads entire “paragraphs” before choosing line breaks; the
algorithm is a variant of that given by Donald E. Knuth and Michael F.
Plass in “Breaking Paragraphs Into Lines”, ‘Software—Practice &
Experience’ 11, 11 (November 1981), 1119–1184.

把这段文本复制进我们的文本编辑器并且保存文件名为fmt-info.txt。现在,重新格式这个文本并且让它成为一个50个字符宽的项目。

[me@linuxbox ~]$ fmt -w 50 fmt-info.txt | head
'fmt' reads from the specified FILE arguments
(or standard input if 
none are given), and writes to standard output.
By default, blank lines, spaces between words,
and indentation are
preserved in the output; successive input lines
with different indentation are not joined; tabs
are expanded on input and introduced on output.

默认情况下,输出会保留空行,单词之间的空格和缩进。持续输入的具有不同缩进的文本行不会连接在一起。tab字符在输入时会展开,在输出时复原。
通过添加-c选项,可以更正这种问题。
-p选项,通过它我们可以格式文件选中的部分,通过在开头使用一样的符号。很多编程语言使用锚标记(#)去提醒注释的开始,而且它可以通过这个选项来被格式。让我们创建一个有用到注释的程序。

[me@linuxbox ~]$ cat > fmt-code.txt
# This file contains code with comments.

# This line is a comment.
# Followed by another comment line.
# And another.

This, on the other hand, is a line of code.
And another line of code.
And another.

我们的示例文件包含了用 “#” 开始的注释(一个 # 后跟着一个空白符)和代码。现在,使用 fmt,我们能格式注释并且 不让代码被触及。

[me@linuxbox ~]$ fmt -w 50 -p '# ' fmt-code.txt
# This file contains code with comments.

# This line is a comment. Followed by another
# comment line. And another.

This, on the other hand, is a line of code.
And another line of code.
And another.

注意相邻的注释行被合并了,空行和非注释行被保留了。

pr - 格式化打印文本

pr程序用来把文本分页。当打印文本的时候,经常希望用几个空行在输出页面的顶部或底部添加空行。此外,这些空行能够用来插入到每个页面的页眉或页脚。

[me@linuxbox ~]$ pr -l 15 -w 65 distros.txt
2008-12-11 18:27        distros.txt         Page 1


SUSE        10.2     12/07/2006
Fedora      10       11/25/2008
SUSE        11.0     06/19/2008
Ubuntu      8.04     04/24/2008
Fedora      8        11/08/2007


2008-12-11 18:27        distros.txt         Page 2


SUSE        10.3     10/04/2007
Ubuntu      6.10     10/26/2006
Fedora      7        05/31/2007
Ubuntu      7.10     10/18/2007
Ubuntu      7.04     04/19/2007

在上面的例子中,我们用 -l 选项(页长)和 -w 选项(页宽)定义了宽65列,长15行的一个“页面”。 pr 为 distros.txt 中的内容编订页码,用空行分开各页面,生成了包含文件修改时间、文件名、页码的默认页眉。 pr 指令拥有很多调整页面布局的选项,我们将在下一章中进一步探讨。

printf - 格式化和打印数据

在bash中,printf是内置的。它这样工作:
printf “format” arguments
首先,发送包含有格式化描述的字符串的指令,接着,这些描述被应用于参数列表上。格式化的结果在标准输出中显示。

[me@linuxbox ~]$ printf "I formatted the string: %s\n" foo
I formatted the string: foo

printf转换规范组件

组件描述
d将数字格式化为带符号的十进制整数
f格式化并输出浮点数
o将整数格式化为八进制数
s将字符串格式化
x将整数格式化为十六进制数,必要时使用小写a-f
X与x相同,但变为大写
%打印%符号(比如,指定%%)

完整的转换规范包含如下内容:

%[flags][width][.precision]conversion_specification

使用多个可选组件时,必须按照上面指定的顺序,以便准确编译,以下是每个可选组件的描述:

printf转换规范组件

组件描述
flags有5种不同的标志:# – 使用“备用格式”输出。这取决于数据类型。对于o(八进制数)转换,输出以0为前缀.对于x和X(十六进制数)转换,输出分别以0x或0X为前缀。0–(零) 用零填充输出。这意味着该字段将填充前导零,比如“000380”。- – (破折号) 左对齐输出。默认情况下,printf右对齐输出。‘ ’ – (空格) 在正数前空一格。+ – (加号) 在正数前添加加号。默认情况下,printf 只在负数前添加符号。
width指定最小字段宽度的数
.precision对于浮点数,指定小数点后的精度位数。对于字符串,指定要输出的字符数

printf主要用在脚本中,用于格式化表格数据,而不是直接用于命令行。但是仍然可以展示如何使用它解决各种格式化问题。

2.文件格式化系统

两个文件格式化程序的主要家族占据了该领域:继承自原始roff程序的,包括nroff和troff;以及TEX排版系统。
nroff程序用于格式化文档以输出到使用等宽字体的设备,如字符终端和打字机式打印机。
troff程序格式化用于排版机输出的文档。
TEX在某种程度上取代了troff最为排版机输出的首选工具。

groff

一套用GUN实现troff的程序,它还包括一个脚本,用来模仿nroff和其他roff家族。


9. 打印


10. 编译程序

在这一章中,我们将看一下如何编译源代码来创建程序。
为什么要编译:

  1. 可用性。尽管系统发行版仓库中已经包含了大量的预编译程序,但是一些发行版本不可能包含所有期望的应用。在这种情况下,得到所期望程序的唯一方式是编译程序源码。
  2. 及时性。虽然一些系统发行版专门打包前沿版本的应用程序,但是很多不是,这意味着,为了拥有一个最新版本的程序,编译是必须的。

从源码编译软件可以变得非常复杂且具有技术性,许多用户难以企及。然而,许多编译任务是相当简单的,只涉及到几个步骤。
命令
make - 维护程序的工具

1. 什么是编译

用高级语言编写的程序,经过另一个称为编译器的程序的处理,会转换成机器语言。一些编译器把高级指令翻译成汇编语言,然后使用一个汇编器将其转换成机器语言。
一个称为链接的过程经常与编译结合在一起。有许多常见的由程序执行的任务。以打开文件为例。许多程序执行这个任务, 但是让每个程序实现它自己的打开文件功能,是很浪费资源的。更有意义的是,拥有单独的一段知道如何打开文件的程序, 并允许所有需要它的程序共享它。对常见任务提供支持由所谓的库完成。这些库包含多个程序,每个程序执行 一些可以由多个程序共享的常见任务。如果我们看一下 /lib 和 /usr/lib 目录,我们可以看到许多库定居在那里。 一个叫做链接器的程序用来在编译器的输出结果和要编译的程序所需的库之间建立连接。这个过程的最终结果是 一个可执行程序文件,准备使用。

不是所有程序都是可编译的。有些程序比如shell脚本就不需要编译,它们直接运行。这些程序是用所谓的脚本或解释型语言编写的。比如:Perl,Python,PHP,Ruby等等。
脚本语言由一个叫做解释器的特殊程序执行。一个解释器输入程序文件,读取并执行程序中包含的每一条指令。通常来说,解释型程序执行起来要比编译程序慢很多,这是因为每次解释型程序执行时,程序中每一条源码指令都需要翻译,而一个已经编译好的程序,一条源码指令只翻译了一次,翻译后的指令会永远地记录到最终的执行文件中。
那么为什么解释型程序这样流行呢?对于许多编程任务来说,原因是“足够快”,但是真正的优势是一般来说开发解释型程序 要比编译程序快速且容易。通常程序开发需要经历一个不断重复的写码、编译和测试周期。随着程序变得越来越大, 编译阶段会变得相当耗时。解释型语言删除了编译步骤,这样就加快了程序开发。

2.编译一个C语言

在Linux环境中,普遍使用的C编译器叫做gcc(GUN C编译器),大多数Linux系统发行版默认不安装gcc。
查看是否安装了gcc

[me@linuxbox ~]$ which gcc
/usr/bin/gcc

首先创建一个名为src的目录来存放我们的源码,然后使用ftp协议把源码下载下来:

[me@linuxbox ~]$ mkdir src
[me@linuxbox ~]$ cd src
[me@linuxbox src]$ ftp ftp.gnu.org
Connected to ftp.gnu.org.
220 GNU FTP server ready.
Name (ftp.gnu.org:me): anonymous
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.
ftp> cd gnu/diction
250 Directory successfully changed.
ftp> ls
200 PORT command successful. Consider using PASV.
150 Here comes the directory listing.
-rw-r--r-- 1 1003 65534 68940 Aug 28 1998 diction-0.7.tar.gz
-rw-r--r-- 1 1003 65534 90957 Mar 04 2002 diction-1.02.tar.gz
-rw-r--r-- 1 1003 65534 141062 Sep 17 2007 diction-1.11.tar.gz
226 Directory send OK.
ftp> get diction-1.11.tar.gz
local: diction-1.11.tar.gz remote: diction-1.11.tar.gz
200 PORT command successful. Consider using PASV.
150 Opening BINARY mode data connection for diction-1.11.tar.gz
(141062 bytes).
226 File send OK.
141062 bytes received in 0.16 secs (847.4 kB/s)
ftp> bye
221 Goodbye.
[me@linuxbox src]$ ls
diction-1.11.tar.gz

注意:因为我们是这个源码的“维护者”,当我们编译它的时候,我们把它保存在~/src目录下。由你的系统发行版源码会把源码安装在/usr/src目录下,而供多个用户使用的源码,通常安装在/usr/local/src目录下。
正如我们所看到的,通常提供的源码形式是一个压缩的 tar 文件。有时候称为 tarball,这个文件包含源码树, 或者是组成源码的目录和文件的层次结构。当到达 ftp 站点之后,我们检查可用的 tar 文件列表,然后选择最新版本,下载。 使用 ftp 中的 get 命令,我们把文件从 ftp 服务器复制到本地机器。
一旦tar文件下载下来之后,必须解包,通过tar程序可以完成:

[me@linuxbox src]$ tar xzf diction-1.11.tar.gz
[me@linuxbox src]$ ls
diction-1.11
diction-1.11.tar.gz

小提示:该 diction 程序,像所有的 GNU 项目软件,遵循着一定的源码打包标准。其它大多数在 Linux 生态系统中 可用的源码也遵循这个标准。该标准的一个条目是,当源码 tar 文件打开的时候,会创建一个目录,该目录包含了源码树, 并且这个目录将会命名为 project-x.xx,其包含了项目名称和它的版本号两项内容。这种方案能在系统中方便安装同一程序的多个版本。 然而,通常在打开 tarball 之前检验源码树的布局是个不错的主意。一些项目不会创建该目录,反而,会把文件直接传递给当前目录。 这会把你的(除非组织良好的)src 目录弄得一片狼藉。为了避免这个,使用下面的命令,检查 tar 文件的内容:

tar tzvf tarfile | head ---

检查源码树

打开该tar文件,会创建一个新的目录,名为diction-1.11,这个目录包含了源码树。让我们看一下里面的内容。

[me@linuxbox src]$ cd diction-1.11
[me@linuxbox diction-1.11]$ ls
config.guess     diction.c          getopt.c      nl
config.h.in      diction.pot        getopt.h      nl.po
config.sub       diction.spec       getopt_int.h  README
configure        diction.spec.in    INSTALL       sentence.c
configure.in     diction.texi.in    install-sh    sentence.h
COPYING en       Makefile.in        style.1.in
de               en_GB              misc.c        style.c
de.po            en_GB.po           misc.h        test
diction.1.in     getopt1.c          NEWS

在源码树中,我们看到大量的文件,属于GUN项目的程序,还有其他许多程序都会,提供文档文件README,INSTALL,NEWS,和COPYING。
这些文件包含了程序描述,如何建立和安装它的信息,还有其它许可条款。在试图建立程序之前,仔细阅读 README 和 INSTALL 文件,总是一个不错的主意。
在这个目录中,其它有趣的文件是那些以 .c 和 .h 为后缀的文件:

[me@linuxbox diction-1.11]$ ls *.c
diction.c getopt1.c getopt.c misc.c sentence.c style.c
[me@linuxbox diction-1.11]$ ls *.h
getopt.h getopt_int.h misc.h sentence.h

这些 .c 文件包含了由该软件包提供的两个 C 程序(style 和 diction),被分割成模块。这是一种常见做法,把大型程序 分解成更小,更容易管理的代码块。源码文件都是普通文本,可以用 less 命令查看:

[me@linuxbox diction-1.11]$ less diction.c

这些 .h 文件被称为头文件。它们也是普通文件。头文件包含了程序的描述,这些程序被包括在源码文件或库中。 为了让编译器链接到模块,编译器必须接受所需的所有模块的描述,来完成整个程序。在 diction.c 文件的开头附近, 我们看到这行代码:

#include "getopt.h"

当它读取 diction.c 中的源码的时候,这行代码指示编译器去读取文件 getopt.h, 为的是“知道” getopt.c 中的内容。 getopt.c 文件提供由 style 和 diction 两个程序共享的例行程序。
在 getopt.h 的 include 语句上面,我们看到一些其它的 include 语句,比如这些:

#include <regex.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

这些文件也是头文件,但是这些头文件在当前源码树的外面。它们由操作系统供给,来支持每个程序的编译。 如果我们看一下 /usr/include 目录,能看到它们:

[me@linuxbox diction-1.11]$ ls /usr/include

当我们安装编译器的时候,这个目录中的头文件会被安装。

构建程序

大多数程序通过一个简单的,两个命令的序列构建:

./configure
make

这个 configure 程序是一个 shell 脚本,由源码树提供。它的工作是分析程序构建环境。大多数源码会设计为可移植的。 也就是说,它被设计成能够在不止一种类 Unix 系统中进行构建。但是为了做到这一点,在建立程序期间,为了适应系统之间的差异, 源码可能需要经过轻微的调整。configure 也会检查是否安装了必要的外部工具和组件。让我们运行 configure 命令。 因为 configure 命令所在的位置通常不是位于 shell 期望程序所呆的地方,我们必须明确地告诉 shell 它的位置,通过 在命令之前加上 ./ 字符,来表明程序位于当前工作目录:

[me@linuxbox diction-1.11]$ ./configure

configure 将会输出许多信息,随着它测试和配置整个构建过程。当结束后,输出结果看起来像这样:

checking libintl.h presence... yes
checking for libintl.h... yes
checking for library containing gettext... none required
configure: creating ./config.status
config.status: creating Makefile
config.status: creating diction.1
config.status: creating diction.texi
config.status: creating diction.spec
config.status: creating style.1
config.status: creating test/rundiction
config.status: creating config.h
[me@linuxbox diction-1.11]$

这里最重要的事情是没有错误信息。如果有错误信息,整个配置过程失败,然后程序不能构建直到修正了错误。
我们看到在我们的源码目录中 configure 命令创建了几个新文件。最重要一个是 Makefile。Makefile 是一个配置文件, 指示 make 程序究竟如何构建程序。没有它,make 程序就不能运行。Makefile 是一个普通文本文件,所以我们能查看它:

[me@linuxbox diction-1.11]$ less Makefile

这个 make 程序把一个 makefile 文件作为输入(通常命名为 Makefile),makefile 文件 描述了包括最终完成的程序的各组件之间的关系和依赖性。
makefile 文件的第一部分定义了变量,这些变量在该 makefile 后续章节中会被替换掉。例如我们看看这一行代码:

CC=                 gcc

其定义了所用的 C 编译器是 gcc。文件后面部分,我们看到一个使用该变量的实例:

diction:        diction.o sentence.o misc.o getopt.o getopt1.o
                $(CC) -o $@ $(LDFLAGS) diction.o sentence.o misc.o \
                getopt.o getopt1.o $(LIBS)

这里完成了一个替换操作,在程序运行时,$(CC) 的值会被替换成 gcc。大多数 makefile 文件由行组成,每行定义一个目标文件, 在这种情况下,目标文件是指可执行文件 diction,还有目标文件所依赖的文件。剩下的行描述了从目标文件的依赖组件中 创建目标文件所需的命令。在这个例子中,我们看到可执行文件 diction(最终的成品之一)依赖于文件 diction.o,sentence.o,misc.o,getopt.o,和 getopt1.o都存在。在 makefile 文件后面部分,我们看到 diction 文件所依赖的每一个文件做为目标文件的定义:

diction.o:       diction.c config.h getopt.h misc.h sentence.h
getopt.o:        getopt.c getopt.h getopt_int.h
getopt1.o:       getopt1.c getopt.h getopt_int.h
misc.o:          misc.c config.h misc.h
sentence.o:      sentence.c config.h misc.h sentence.h
style.o:         style.c config.h getopt.h misc.h sentence.h

然而,我们不会看到针对它们的任何命令。这个由一个通用目标解决,在文件的前面,描述了这个命令,用来把任意的 .c 文件编译成 .o 文件:

.c.o:
            $(CC) -c $(CPPFLAGS) $(CFLAGS) $<

这些看起来非常复杂。为什么不简单地列出编译每个部分的步骤,那样不就行了?一会儿就知道答案了。同时, 让我们运行 make 命令并构建我们的程序:

[me@linuxbox diction-1.11]$ make

这个 make 程序将会运行,使用 Makefile 文件的内容来指导它的行为。它会产生很多信息。
当 make 程序运行结束后,现在我们将看到所有的目标文件出现在我们的目录中。

[me@linuxbox diction-1.11]$ ls
config.guess  de.po             en              en_GB           sentence.c
config.h      diction           en_GB.mo        en_GB.po        sentence.h
config.h.in   diction.1         getopt1.c       getopt1.o       sentence.o
config.log    diction.1.in      getopt.c        getopt.h        style
config.status diction.c         getopt_int.h    getopt.o        style.1
config.sub    diction.o         INSTALL         install-sh      style.1.in
configure     diction.pot       Makefile        Makefile.in     style.c
configure.in  diction.spec      misc.c          misc.h          style.o
COPYING       diction.spec.in   misc.o          NEWS            test
de            diction.texi      nl              nl.mo
de.mo         diction.texi.i    nl.po           README

在这些文件之中,我们看到 diction 和 style,我们开始要构建的程序。恭喜一切正常!我们刚才源码编译了 我们的第一个程序。但是出于好奇,让我们再运行一次 make 程序:

[me@linuxbox diction-1.11]$ make
make: Nothing to be done for `all'.

make 只是构建 需要构建的部分,而不是简单地重新构建所有的内容。由于所有的目标文件都存在,make 确定没有任何事情需要做。 我们可以证明这一点,通过删除一个目标文件,然后再次运行 make 程序,看看它做些什么。让我们去掉一个中间目标文件:

[me@linuxbox diction-1.11]$ rm getopt.o
[me@linuxbox diction-1.11]$ make

我们看到 make 重新构建了 getopt.o 文件,并重新链接了 diction 和 style 程序,因为它们依赖于丢失的模块。 这种行为也指出了 make 程序的另一个重要特征:它保持目标文件是最新的。make 坚持目标文件要新于它们的依赖文件。 这个非常有意义,做为一名程序员,经常会更新一点儿源码,然后使用 make 来构建一个新版本的成品。make 确保 基于更新的代码构建了需要构建的内容。如果我们使用 touch 程序,来“更新”其中一个源码文件,我们看到发生了这样的事情:

[me@linuxboxdiction-1.11]$ ls -l diction getopt.c
-rwxr-xr-x 1 me me 37164 2009-03-05 06:14 diction
-rw-r--r-- 1 me me 33125 2007-03-30 17:45 getopt.c
[me@linuxboxdiction-1.11]$ touch getopt.c
[me@linuxboxdiction-1.11]$ ls -l diction getopt.c
-rwxr-xr-x 1 me me 37164 2009-03-05 06:14 diction
-rw-r--r-- 1 me me 33125 2009-03-05 06:23 getopt.c
[me@linuxbox diction-1.11]$ make

运行 make 之后,我们看到目标文件已经更新于它的依赖文件:

[me@linuxbox diction-1.11]$ ls -l diction getopt.c
-rwxr-xr-x 1 me me 37164 2009-03-05 06:24 diction
-rw-r--r-- 1 me me 33125 2009-03-05 06:23 getopt.c

make 程序这种智能地只构建所需要构建的内容的特性,对程序来说,是巨大的福利。虽然在我们的小项目中,节省的时间可能 不是非常明显,在庞大的工程中,它具有非常重大的意义。记住,Linux 内核(一个经历着不断修改和改进的程序)包含了几百万行代码。

安装程序

打包良好的源码经常包括一个特别的 make 目标文件,叫做 install。这个目标文件将在系统目录中安装最终的产品,以供使用。 通常,这个目录是 /usr/local/bin,为在本地所构建软件的传统安装位置。然而,通常普通用户不能写入该目录,所以我们必须变成超级用户, 来执行安装操作:

[me@linuxbox diction-1.11]$ sudo make install

执行了安装后,我们可以检查下程序是否已经可用:

[me@linuxbox diction-1.11]$ which diction
/usr/local/bin/diction
[me@linuxbox diction-1.11]$ man diction
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值