第八章. 文件和目录,输入和输出

Files and Directories, Input and Output

声明

本章翻译仅用于 Raku 学习和研究, 请支持电子版或纸质版

第八章. 文件和目录, 输入和输出

读写文本是许多你想要编写的程序的基础。你将数据存储在文件中并稍后检索该数据。本章是关于你需要执行此操作的所有功能。在此过程中,你将看到如何处理文件路径,移动文件以及使用目录。大多数情况都是使用你已经看过的相同语法完成的,但现在使用不同对象的类型。

本章中的许多任务可能由于程序之外的原因而失败。如果你希望在不同的目录中工作或希望某个文件存在,那么如果这些条件不为真,你可能不希望继续。这只是一个处理外部资源的程序的事实。

文件路径

IO::Path 对象表示文件路径。它知道如何根据文件系统的规则组合和拆分路径。只要路径的形式符合这些规则,该路径与实际存在的文件无关。你马上会看到如何处理丢失的文件。现在,在任何字符串上调用 .IO 将其转换为 IO::Path 对象:

my $unix-path = '/home'.IO;
my $windows-path = 'C:/Users'.IO;

要构建更深的路径,请使用 .add。你一次可以有多个层级。 .add 不会改变原始对象;它为你提供了一个新对象:

my $home-directory = $unix-path.add: 'hamadryas';
my $file = $unix-path.add: 'hamadryas/file.txt';

如果要在那里构建路径,请赋值回原始对象:

$unix-path  = $unix-path.add: 'hamadryas/file.txt';

二元赋值形式可能更有用:

$unix-path .= add: 'hamadryas/file.txt';

.basename.parent 方法拆分路径:

my $home = '/home'.IO;
my $user = 'hamadryas';    # Str or IO::File will work
my $file = 'file.txt'.IO;

my $path = $home.add( $user ).add( $file );

put 'Basename: ', $path.basename;  # Basename: file.txt
put 'Dirname: ',  $path.parent;    # Dirname: /home/hamadryas
注意

.basename返回一个字符串,而不是另一个 IO::Path。如果需要,可以再次使用 .IO

使用 .parent,你可以决定向上升多少个层级:

my $home = '/home'.IO;
my $user = 'hamadryas';
my $file = 'file.txt'.IO;

my $path = $home.add( $user ).add( $file );

put $path;                        # /home/hamadryas/file.txt
put 'One up:', $path.parent;      # One up: /home/hamadryas
put 'Two up: ', $path.parent(2);  # Two up: /home

你可以提出问题,以确定你是否有绝对路径或相对路径:

my $home = '/home'.IO;
my $user = 'hamadryas';
my $file = 'file.txt'.IO;

for $home, $file {
    put "$_ is ", .is-absolute ?? 'absolute' !! 'relative';
    # put "$_ is ", .is-relative ?? 'relative' !! 'absolute';
}

使相对路径成为绝对路径。没有参数 .absolute 在你创建 IO::Path 对象时使用当前工作目录。如果你想要一些其他的基目录,给它一个参数。无论哪种方式,你得到一个字符串而不是另一个 IO::Path 对象。 .absolute 方法不关心该路径是否实际存在:

my $file = 'file.txt'.IO;
put $file.absolute;           # /home/hamadryas/file.txt
put $file.absolute( '/etc' ); # /etc/file.txt
put $file.absolute( '/etc/../etc' ); # /etc/../etc/file.txt

调用 .resolve 会检查文件系统。它弄清楚了 ... 并将符号链接转换为目标。请注意 /etc/.. 被替换为 /private,因为 /etc 是 macOS 上 /private/etc 的符号链接:

my $file = 'file.txt'.IO;
put $file.absolute( '/etc/..' ); # /etc/../file.txt
put $file.absolute( '/etc/..' ).IO.resolve; # /private/file.txt

你可以使用 :completely 强调该文件存在。如果路径的任何部分(除了最后的部分)不存在或无法解析,则会收到错误:

my $file = 'file.txt'.IO;

{
CATCH {
    default { put "Caught {.^name}" }   # Caught X::IO::Resolve
    }
put $file.absolute( '/homer/..' ).IO.resolve: :completely; # fails
}

文件测试操作符

文件测试操作符回答有关文件路径的问题。他们中的大多数都返回 TrueFalse。从字符串开始调用 .IO 方法来创建 IO::Path 对象。使用 .e 检查文件是否存在(表8-1显示了其他文件测试):

my $file = '/some/path';

unless $file.IO.e {
    put "The file <$file> does not exist!";
}

为什么是 .e?它来自 Unix 测试程序,它使用命令行开关(例如 -e)来回答有关路径的问题。那些相同的字母成为方法的名称。表8-1显示了文件测试。其中大多数与类似语言中的相同,尽管一些多字母将多个测试组合成一个。

Method The question it answers
e 存在
d 是一个目录
f 是一个普通文件
s 文件大小(字节)
l 是一个符号连接
r 对当前用户是可读的
w 对当前用户是可写的
rw 对当前用户是可读和可写的
x 对当前用户是可执行的
rwx 对当前用户是可读,可写,可执行的
z 文件存在,零字节

Almost all of the file tests return a Boolean value. The one odd test is .s, which asks for the file size in bytes. That’s not a Boolean, so how would it note a problem such as a missing file? It might return 0 in that case, because a file can have nothing in it (hence the .z method to ask if it exists with zero size). .s returns a Failureinstead of False if there’s a problem:

几乎所有文件测试都返回一个布尔值。一个奇怪的测试是 .s,它以字节为单位询问文件大小。这不是布尔值,那么它会如何记录丢失文件等问题?在这种情况下,它可能返回 0,因为文件中可能没有任何内容(因此 .z 方法询问它是否存在,大小为零)。如果出现问题,.s 会返回 Failure 而不是 False

my $file = 'not-there';
given $file.IO {
    CATCH {
        # $_ in here is the exception
        when X::IO::NotAFile
            { put "$file is not a plain file" }
        when X::IO::DoesNotExist
            { put "$file does not exist"      }
        }
    put "Size is { .s }";
    }

在尝试获取其大小之前,你可能会检查文件是否存在并且是纯文件(尽管 .f 表示 .e ),但这种方式可能不太安全,因为文件可能会在你进入和当你尝试获取文件大小之间消失:

my $file = 'not-there';
given $file.IO {
    when .e && .f { put "Size is { .s }"   }
    when .e       { put "Not a plain file" }
    default       { put "Does not exist"   }
}

但这不是文件测试的唯一语法。还有副词版本。你可以智能地匹配你想要的测试。此示例使用 Junction 来组合测试,即使你在第14章之前不会看到这些测试:

if $file.IO ~~ :e & :f {  # Junction!
    put "Size is { .s }"
}

练习8.1 创建一个程序,该程序从命令行参数中获取文件列表,并报告当前用户是否可读,可写或可执行。如果文件不存在,你会怎么做?

文件元数据

文件记录的不仅仅是其内容。他们保留有关自己的额外信息;这就是元数据。 .mode 方法返回文件的 POSIX 权限(如果你的文件系统支持这样的事情)。这是一个整数,表示用户,组和其他所有人的设置:

my $file = '/etc/hosts';
my $mode = $file.IO.mode;
put $mode.fmt: '%04o';   # 0644
注意

某些 POSIX 或 Unix 特定的想法不适用于 Windows。在我写的时候,没有特定于 Windows 的模块来填补这些空白。

每组权限需要三位:一个用于读,一个用于写和一个用于执行。你使用位运算符(你还没有看到它们)从单个数字中提取单独的权限。

按位 AND 运算符 +&,使用位掩码(例如以下示例中的 0o700)隔离集合。按位右移运算符 +>,提取正确的数字:

my $file = '/etc/hosts';
my $mode = $file.IO.mode;
put $mode.fmt: '%04o';   # 0644

my $user  = ( $mode +& 0o700 ) +> 6;
my $group = ( $mode +& 0o070 ) +> 3;
my $all   = ( $mode +& 0o007 );

在每个权限集内,你可以使用另一个掩码来隔离你想要的位。在这部分你将最终得到 TrueFalse

put qq:to/END/;
mode: { $mode.fmt: '%04o' }
  user:  $user
    read:    { ($user +& 0b100).so }
    write:   { ($user +& 0b010).so }
    execute: { ($user +& 0b001).so }
  group: { $group }
  all:   { $all }
END

你可以使用 chmod 子例程更改这些权限。给它相同的数字。将它表示为十进制数字可能最简单:

chmod $file.IO.chmod: 0o755;

FILE TIMES

.modified.accessed.changed 方法返回表示文件的修改,访问和 inode 更改时间的 Instant 对象(如果你的系统支持这些)。你可以使用 .DateTime 方法将 Instant 转换为人类可读的日期:

my $file = '/home/hamadryas/.bash_profile';

given $file.IO {
    if .e {
        put qq:to/HERE/
            Name:    $_
                Modified: { .modified.DateTime }
                Accessed: { .accessed.DateTime }
                Changed:  { .changed.DateTime  }
                Mode:     { .mode     }
            HERE
    }
}

这给出了这样的东西:

Name:    /home/hamadryas/.bash_profile
    Modified: 2018-08-15T01:19:09Z
    Accessed: 2018-08-16T10:07:00Z
    Changed:  2018-08-15T01:19:09Z
    Mode:     0664

Linking and Unlinking Files

文件名是你存储在某处的某些数据的标签。重要的是要记住名称不是数据。同样,目录或文件夹的隐喻就是这样。它并不真正“包含”文件。它知道它应该记住的文件名列表。牢记这一点应该使下一部分更容易掌握。

名称是数据的链接,相同的数据可以有多个链接。只要有链接,你就可以获得该数据。这并不意味着数据在没有链接时会消失。存储的那些部分仅用于其他部分。这就是你有时可以恢复数据的原因。你的特定文件系统可能会以不同的方式执行操作,但这是基本的想法。

注意

通常,你删除链接的能力取决于目录权限,而不是文件权限。你实际上是从目录所包含的文件列表中删除该文件。

要删除文件,请使用 .unlink 删除指向它的链接。你没有删除数据;这就是为什么它不被称为 .delete 或类似的东西的原因。其他指向相同数据的链接可能仍然存在。如果 .unlink 可以删除该文件,则返回 True。如果失败则返回 X::IO::Unlink

my $file = '/etc/hosts'.IO;

try {
CATCH {
    when X::IO::Unlink { put .message }
    }
$file.unlink;
}

你可以使用子例程形式同时删除多个文件。它返回你必须从备份还原的文件的名称(也称为成功取消链接的文件):

my @unlinked-files = unlink @files;

集合 的差集在这里很有用,虽然在第 14 章之前你不会看到 Set。请注意,你可以取消链接不存在的文件,它们不会出现在 @error-files 中:

my @error-files = @files.Set (-) @unlinked-files.Set;

你可以删除原始文件名,但数据仍然存在。文件背后的数据一直存在,直到所有链接消失。这些都不能删除目录,但你马上会看到如何做到这一点。

使用 .link 为某些数据创建新标签。该路径必须与数据位于同一磁盘或分区上。如果这不起作用,则失败并抛出 X::IO::Link 异常:

my $file = '/Users/hamadryas/test.txt'.IO;

{
CATCH {
    when X::IO::Unlink { ... }
    when X::IO::Link { ... }
    }

$file.link: '/Users/hamadryas/test2.txt';
$file.unlink;
}

还有另一种链接,称为符号链接(简称“符号链接”)。这不是一个实际的链接;它是指向另一个文件名(“目标”)的文件。当文件系统遇到符号链接时,它会使用目标路径。

目标是最终文件名。你创建的符号链接指向该文件名。在目标上调用 .symlink 来创建指向它的文件:

{
CATCH {
    when X::IO::Symlink { ... }
    }

$target.symlink: '/opt/different/disk/test.txt';
}

重命名和复制文件

要更改文件名,请使用 .rename。与 .link 一样,这仅适用于同一磁盘或分区。它会在不移动数据的情况下更改标签。如果它无法做到这一点,则会失败并抛出 X::IO::Rename 异常:

my $file = '/Users/hamadryas/test.txt'.IO;

{
CATCH {
    when X::IO::Rename { put .message }
    }
$file.rename: '/home/hamadryas/other-dir/new-name.txt';
}

你可以将数据复制(.copy)到其他设备或分区。这会将数据物理地放在磁盘上的新位置上。原始数据及其链接保留在原地,复制的数据有自己的链接。之后,两者没有连接,你有两个单独的数据副本。如果它不起作用,则会失败并抛出 X::IO::Copy 异常:

my $file = '/Users/hamadryas/test.txt'.IO;

{
CATCH {
    when X::IO::Copy { put .message }
    }
$file.copy: '/opt/new-name.txt';
}

使用 .move 首先复制数据然后删除原始数据。如果文件已经存在, .copy 将替换新文件(并且它具有正确的权限)):

my $file = '/Users/hamadryas/test.txt'.IO;

{
CATCH {
    when X::IO::Move { put .message }
    }
$file.copy: '/opt/new-name.txt';
}

使用 :create-only 副词来阻止替换:

$file.copy: '/opt/new-name.txt', :create-only;

.move 方法结合了 .copy.unlink

$file.move: '/opt/new-name.txt';

复制文件后 .move 可能无法删除原始文件。你可能希望在开始之前检查该权限,但无法保证权限不会更改。

操作目录

程序启动时,会知道它的当前工作目录。当前工作目录存储在特殊变量 $*CWD 中。处理相对文件路径时,程序会在当前目录中查找:

put "Current working directory is $*CWD";

要更改该目录,请使用 chdir。给它一个绝对路径来改变到那个目录:

chdir( '/some/other/path' );

给它一个相对路径来改变到当前工作目录的子目录:

chdir( 'a/relative/path' );

如果失败,则返回带有X::IO::Chdir 异常Failure

unless my $dir = chdir $subdir {
    ... # handle the error
}

不带参数的chdir会给你一个错误。你可能希望转到你的家目录。如果需要,请使用 $*HOME 作为参数。这是存储家目录的特殊变量:

chdir( $*HOME );

如何设置 $*HOME 取决于你的特定系统。在类 Unix 系统上,这可能是 HOME 环境变量。在 Windows 上,它可能是 HOMEPATH

练习8.2 输出你的家目录路径。创建一个现有子目录的新路径并切换到该目录。输出当前工作目录的值。如果子目录不存在会发生什么?

有时你只需要为程序的一小部分更改目录,之后你就想回到你开始的地方。 indir 子例程接收目录和代码块并运行该代码,就好像它是当前的工作目录一样。它实际上并没有弄乱 $*CWD

my $result = indir $dir, { ... };
unless $result {
    ... # handle the error
}

如果一切正常,则 indir 返回块的结果,尽管可能是 False 值甚至是 Failure。如果 indir 无法更改到目录,则返回 Failure。小心你正在处理的情况!

目录清单

dir获取目录中的文件序列作为 IO::Path 对象。它包含隐藏文件(但不包括 ... 虚拟文件)。如果不带参数则它使用当前目录:

my @files = dir();
my $files = dir();

With an argument it gets a Seq of the files in the specified directory:

如果 dir 带参数,则它获取指定目录中的文件序列

my @files = dir( '/etc' );

for dir( '/etc' ) -> $file {
    put $file;
}

序列中的元素包含该路径组件。相对目录参数返回相对路径。如果在创建序列后更改工作目录,那些路径可能无效:

say dir( '/etc' ); # ("/etc/emond.d".IO ...)
say dir( 'lib' ); # ("lib/raku".IO ...)

如果遇到问题,dir 会返回 Failure,例如不存在的目录。

dir 的另一个不错的功能是:它知道要跳过哪些条目。有一个可选的第二个参数,可以测试条目以决定它们是否应该成为结果的一部分。默认情况下,测试是一个 Junction(第14章),它排除了 ... 虚拟目录:

say dir( 'lib', test => none( <. ..> ) );

练习8.3 输出另一个目录中所有文件的列表。每行显示一个并给每行编号。你能对文件列表进行排序吗?如果你没有想要浏览的目录,请在类 Unix 系统上尝试 /etc,或在 Windows 上尝试 C:\rakudo

练习8.4 创建一个接收目录名并列出其中所有文件的程序。下降到子目录并列出他们的文件。稍后你将在第19章中使用该程序。

创建目录

你可以使用 mkdir 创建自己的目录。如果这是你要求的,它可以立即为你创建多层级的子目录。如果 mkdir 无法创建目录,则会抛出 X::IO::Mkdir 异常

try {
    CATCH {
        when X::IO::Mkdir { put "Exception is {.message}" }
    }
    my $subdir = 'Butterflies'.IO.add: 'Hamadryas';
    mkdir $subdir;
}

可选的第二个参数是 Unix 风格的八进制模式(Windows 忽略此参数)。 Unix 权限最容易读作八进制数:

mkdir $subdir, 0o755;

你也可以从字符串开始,然后使用 .IO 将其转换为 IO::Path 对象,然后在所有这些对象上调用 .mkdir。你省略或不省略模式都可以:

$subdir.IO.mkdir;
$subdir.IO.mkdir: 0o755;

练习8.5 编写一个程序来创建一个在命令行中指定的子目录。将完整路径指定为参数时会发生什么?如果目录已经存在怎么办?

移除目录

有两种方法可以删除目录,但你可能只想使用其中一种。在开始使用这些之前,你可能会考虑使用虚拟机的快照或在无法删除任何重要内容的特殊帐户中工作。小心!

第一个是 rmdir,只要目录是空的(没有文件或子目录)就删除一个或多个目录:

my @directories-removed = rmdir @dirs;

使用方法形式,你可以一次删除一个。如果失败则抛出 X::IO::Rmdir 异常

try {
    CATCH {
        when X::IO::Rmdir { ... }
    }
    $directory.IO.rmdir;
}

这有点不方便。通常,你希望删除目录及其包含的所有内容。 File::Directory::Tree 中的 rmtree 子例程对此非常有用:

use File::Directory::Tree;
my $result = try rmtree $directory;

格式化输出

你可以在输出值之前格式化值,也可以将值插入到字符串中。选项遵循你在其他语言中已经看到的内容,因此你只能在这里体验它们。

将模板字符串赋予 .fmt 以描述值的显示方式。模板使用指令; 这些指令以 % 开头,并用字符来描述格式。下面这些是以十六进制(%x),八进制(%o)和二进制(%b)格式化的同一个数字:

$_ = 108;

put .fmt: '%x';    # 6c
put .fmt: '%X';    # 6C (uppercase!)
put .fmt: '%o';    # 154
put .fmt: '%b';    # 1101100

某些指令有额外的选项,这些选项出现在 和字母之间。数字指定列的最小宽度(尽管可能会溢出)。前导零使用零填充未使用的列。插值字符串时可以看到这种情况;格式化输出周围的字符清楚地表明 .fmt 创建了什么:

put "$_ is ={.fmt: '%b'}=";    # 108 is =1101100=
put "$_ is ={.fmt: '%8b'}=";   # 108 is = 1101100=
put "$_ is ={.fmt: '%08b'}=";  # 108 is =01101100=

模板文本可以包含其他字符。如果这些不是指令的一部分,则它们是字面字符。这会将前面的示例翻出来,因此所有字符都在模板中:

put .fmt: "$_ is =%08b=";  # 108 is =01101100=

如果你想要一个字面的 符号则用另一个 转义它。 %f 指令格式化一个浮点数,这对百分比很方便。你可以指定总宽度(包括小数点)和小数位数:

my $n = 1;
my $d = 7;
put (100*$n/$d).fmt: "$n/$d is %5.2f%%";  # 1/7 is 14.29%

省略总宽度仍然有效,并允许你仅指定小数位数。这会将最终显示的十进制数字舍入:

put (100*$n/$d).fmt: "$n/$d is %.2f%%";  # 1/7 is 14.29%

Calling .fmt on a Positional formats each element according to the template, joins them with a space, and gives you a single Str:

Positional 上调用 .fmt 会根据模板格式化每个元素,将它们与空格连接,并为你提供单个字符串

put ( 222, 173, 190, 239 ).fmt: '%02x';  # de ad be ef

.fmt 的第二个参数更改分隔符:

put ( 222, 173, 190, 239 ).fmt: '%02x', '';  # deadbeef

sprintf可以通过更多的控制来完成同样的工作。这是一个例程,它接收同样的模板作为它的第一个参数,然后是一个值列表。每个值按顺序填充一个指令。你不必输出结果:

my $string = sprintf( '%2d %s', $line-number, $line );

printf执行相同的操作并直接将结果输出到标准输出(不添加换行符):

printf '%2d %s', $line-number, $line;

表8-2列出了一些可用的 sprintf 指令。

Directive Description
%d 十进制有符号整数
%u 十进制无符号整数
%o 八进制无符号整数
%x 十六进制无符号整数 (小写)
%X 十六进制无符号整数 (大写)
%b 二进制无符号整数
%f 浮点数
%s 文本值

练习8.6 创建一个使用 printf 的程序,并将右对齐文本输出到你指定的列数。输出标尺线可能会对你有所帮助。

常见的格式化任务

使用 %f 舍入数字。指定整个模板的宽度和小数位数。小数点和后续数字计为宽度的一部分:

put (2/3).fmt: '%4.2f';  # 0.67;

但是,总宽度不限制列。它至少是那个列数,但可能更多:

put (2/3).fmt: '%4.5f';  # 0.66667;

如果你不关心宽度,可以将其省略。这只是将值舍入为你指定的小数位数:

put (2/3).fmt: '%.3f';  # 0.667;

% 之后的 # 添加了数字系统前缀,但不是 Raku 使用的前缀。它是宇宙其余部分使用的前缀; 八进制数得到前导零:

put 108.fmt: '%#x'; # 0x6c
put 108.fmt: '%#o'; # 0154

%s 格式化文本值。使用宽度它将值向右推,并在必要时用空格填充它。 宽度前面的 - 将文本推向左侧:

put 'Hamadryas'.fmt: '|%s|';    # |Hamadryas|
put 'Hamadryas'.fmt: '|%15s|';  # |      Hamadryas|
put 'Hamadryas'.fmt: '|%-15s|'; # |Hamadryas      |

使用 sprintf 创建柱状输出。宽度使一切排成一行:

my $line = sprintf '%02d %-20s %5d %5d %5d', @values;

练习8.7 输出你在命令行中指定的两个数字的百分比。将输出限制为三位小数。

练习8.8 输出一个12乘12的乘法表。

标准文件句柄

文件句柄是程序和文件之间的连接。你可以免费获得其中的三个。两个用于输出,一个用于输入。标准输出是你从本书开始以来一直使用的输出。它是输出的默认文件句柄。你还使用了标准错误,因为这是用于警告和错误的文件句柄。标准输入将你的程序连接到某人试图提供的数据上。

在继续阅读和编写任意文件的一般过程之前,你可能会发现查看基本文件句柄很有用。如果你已经知道这些事情,那么跳过本节并无不妥。

标准输出

标准输出是大多数输出方法的默认文件句柄。当你在其例程形式中使用其中任何一个时,你就正在使用标准输出:

put $up-the-dishes;
say $some-stuff;
print $some-stuff;
printf $template, $thing1, $thing2;

$*OUT 上调用方法会使其显式化。这是保存默认文件句柄的特殊变量:

$*OUT.put: $up-the-dishes;
$*OUT.say: $some-stuff;
$*OUT.print: $some-stuff;
$*OUT.printf: $template, $thing1, $thing2;

你可能在某些时候在命令行上使用了重定向。 > 将程序的标准输出发送到文件(或其他地方):

% raku program.p6 > output.txt 

如果要运行程序但不关心输出,可以将其发送到空设备。输出无处可去,然后消失了。这在 Unix 系统和 Windows 中略有不同:

% raku program.p6 > /dev/null 
C:\ raku program.p6 > NUL 

练习8.9 创建一个将内容写入标准输出的程序。运行该程序并将输出重定向到文件。再次运行它并将输出重定向到空设备。

标准错误

标准错误是输出的另一种途径。当程序不希望影响正常输出时,程序通常会对警告和其他消息使用标准错误。你可以在不搞乱格式化输出的情况下获得警告。

warn 将其消息输出到标准错误,程序继续。顾名思义,当你遇到一个你可以预料到并且你认为有人应该知道的情况时,它就是为警告而设计的:

warn 'You need to use a number between 0 and 255';

faildie 是相似的。他们将消息发送到标准错误,但他们也可以停止你的程序,除非你捕获或处理它们。

note 就像 say;它在其参数上调用 .gist 并将结果输出到标准错误。这对调试输出很有用:

note $some-object;

通常这种输出是通过某些命令行开关或其他设置启用的:

note $some-object if $debugging > 0;

输出方法适用于 $*ERR,它保存默认错误文件句柄:

$*ERR.put: 'This is a warning message';

当你在终端中工作时,通常会同时看到标准输出和标准错误(或“合并”)。用 2> 重定向错误输出;获取文件描述符编号 2(标准错误)并将其发送到不是终端的某个地方。如果你不理解任何一个,只需按照例子:

% raku program.p6 2> error_output.txt 
C:\ raku program.p6 2> error_output.txt 

% raku program.p6 2> /dev/null 
C:\ raku program.p6 2> NUL 

将文件描述符 2 重定向到文件描述符 1 以合并标准输出和错误。同样,你可以按照示例进行操作,而无需追根究底:

% raku program.p6 2>&1 /dev/null 

练习8.10 创建一个程序,输出标准输出和标准错误。运行它并将标准输出重定向到文件。再次运行它,但将标准错误重定向到空设备。

标准输入

当你使用没有命令行参数的 lines() 时,它会从标准输入读取。数据流入你的程序:

for lines() {
    put ++$, ': ', $_;
}

你的程序会等待你输入内容并将其输出给你:

% raku no-args.p6
Hello Raku
0: Hello Raku
this is the second line
1: this is the second line

If you only want standard input you can explicitly use $*IN. Call .lines as follows:

如果你只想要标准输入,则可以显式地使用 $*IN。像下面这样调用 .lines

for $*IN.lines() {
    put ++$, ': ', $_;
}

标准输入也可以来自另一个程序。你可以将一个程序的输出传递给另一个程序的输入:

% raku out-err.p6 | raku no-args.p6

练习8.11 创建两个程序。第一个应输出包含第一个参数的命令行文件中的所有行。将其输出通过管道输出到第二个程序,读取其输入并以全部大写形式输出。将第一个程序的输出通过管道输入到第二个程序。

读取输入

你已经看到了将数据导入程序的几种方法。prompt 例程输出一条消息并等待一行输入:

my $answer = prompt( 'Enter some stuff> ' );

slurp 一次性读取整个文件。slup 作为方法或例程:

my $entire-file = $filename.IO.slurp;
my $entire-file = slurp $filename;

如果你无法阅读该文件,你将收到 Failure。始终检查你是否能够做你想做的事情:

unless my $entire-file = slurp $filename.IO.slurp {
    ... # handle error
}

读取行

在第6章中,你了解了如何使用 lines() 从你在命令行中指定的文件名中读取行。通过 @*ARGS 并在单个文件上调用 lines 来自己完成此操作。你可以过滤掉不存在或存在其他问题的文件(lines() 不执行的操作):

for @*ARGS {
    put '=' x 20, ' ', $_;

    # maybe more error checking here
    unless .IO.e { put 'Does not exist'; next }

    for .IO.lines() {
        put "$_:", ++$, ' ', $_;
    }
}

这代码有点太多了。 lines()$*ARGFILES 文件句柄读取。这与显式使用它是一样的:

for $*ARGFILES.lines() {
    put ++$, ': ', $_;
}

使用 $*ARGFILES.path 提取当前文件名:

for $*ARGFILES.lines() {
    put "{$*ARGFILES.path}:", ++$, ' ', $_;
}

这不会处理为每个文件开始编号为新鲜的行,但是有一个技巧:$*ARGFILES 知道在切换文件时让你在发生这种情况时运行一些代码。给 .on-switch 一个代码块,以便在文件更改时运行。用它来重置持久计数器:

for lines() {
    state $lines = 1;
    FIRST { $*ARGFILES.on-switch = { $lines = 1 } }

    put "{$*ARGFILES.path}:{$lines++} $_";
}
注意

当我写这篇文章时,如果 lines 遇到一个无法读取的文件,则抛出一个你无法恢复的异常。我会忽略这一点,因为我希望情况很快就会改变。

练习8.12 创建一个程序,输出你在命令行中指定的所有文件的行。在输出每个文件的行之前输出显示其名称的文件横幅。完成上一个文件后会发生什么?

读取文件

Both slurp and lines handle the details implicitly. open lets you do it in whatever manner you like. It returns a filehandle that you use to get the data from the file. If there’s a problem open returns a Failure:

slurplines 都隐式地处理细节。 open 允许你以任何你喜欢的方式来做。它返回一个文件句柄,用于从文件中获取数据。如果出现问题,则 open 返回Failure

my $fh = open 'not-there';
unless $fh {
    put "Error: { $fh.exception }";
    exit;
}

for $fh.lines() { .put }

你可能更喜欢方法形式:

my $fh = $filename.IO.open;

你可以更改编码,行结束处理和特定行结束。 :enc 副词设置输入编码:

my $fh = open 'not-there', :enc('latin1');

To keep the line endings instead of autochomping them, use :chomp:

要保留行结尾而不是自动切除,请使用 :chomp

my $fh = open 'not-there', :chomp(False);

行结尾设置为 :nl-in 并且可以是多个字符串,其中任何一个都可以工作:

my $fh = open 'not-there', :nl-in( "\f" );
my $fh = open 'not-there', :nl-in( [ "\f", "\v" ] );

如果你不想结束行(比如 slurp),空的字符串False 会起作用:

my $fh = open 'not-there', :nl-in( '' );
my $fh = open 'not-there', :nl-in( False );

你可以读取单行。告诉 .lines 你想要多少行:

my $next-line = $fh.lines: 1;

.lines 是惰性的。那实际上没有读取一行。直到你尝试使用 $next-line 时它才会这样做。如果你想让它立即发生,你可以让它急切:

my $next-line = $fh.lines(1).eager;

如果你想要所有的行你仍然可以从文件句柄 .slurp

my $rest-of-data = $fh.slurp;

完成后关闭文件句柄。程序将在某些时候自动为你执行此操作,但你不希望这些事情可能会在程序结束时出现:

$fh.close;

练习8.13 打开在命令行中指定的每个文件。输出第一行和最后一行。在这两者之间报告你遗漏的行数。

写出

写文件的最简单方法是使用 spurt。给它一个文件名和一些数据,它为你完成剩下的工作:

spurt $path, $data;

如果文件已存在,则会覆盖已存在的任何内容。要添加已经存在的内容,请使用 :append 副词:

spurt $path, $data, :append;

仅当文件尚不存在时,你才可以通过指定 :exclusive 来输出数据。如果文件已经存在,则会失败:

spurt $path, $data, :exclusive;

spurt 工作时,它返回 True。如果出现问题则返回 Failure

unless spurt $path, $data {
    ... # handle error
}

打开文件以写入

使用 spurt 可能很方便,但每次使用它时,你真正打开了一个文件,写入文件并关闭它。如果你想继续添加到文件中,你可以自己打开文件并保持打开状态,直到完成为止:

unless my $fh = open $path, :w {
    ...;
    }

$fh.print: $data;
$fh.print: $more-data;

任何输出方法都适用于文件句柄:

$fh.put: $data;
$fh.say: $data;

完成文件后调用 .close。这可确保较低级别可能已缓冲的任何数据都会进入文件:

$fh.close;

如果你不喜欢默认行分隔符,则可以指定自己的行分割符。当你有多行的项要包含在一起作为单个记录时,换页符\f,作为“行”分隔符很方便:

unless my $fh = open $path, :w, :nl-out("\f") {
    ...;  # handle the error
    }

$fh.print: ...;

使用 try 可能更干净:

my $fh = try open $path, :w, :exclusive, :enc('latin1'), :nl-out("\f");
if $! {
    ... # handle the error
}

练习8.14 创建一个程序,该程序将你在命令行中指定的两个数字之间的所有素数写入文件。如果文件已存在,你应该怎么做?

二进制文件

二进制文件不是基于字符的。图像,电影等都是例子。你不希望文件阅读器将这些解码为 Perl 的内部字符格式;你想要原始数据。使用带有 :bin 副词的 slurp 来读取。它返回一 Buf 而不是返回一个字符串。你可以像任何其他 Positional 一样处理 Buf

my $buffer = slurp $filename, :bin;  # Buf object
for @$buffer { ... }

使用相同的 :bin 副词打开文件以获取其原始内容:

unless my $fh = open $path, :bin {
    ... # handle the error
}

移动

告诉 .read 要读取多少个八位字节,它返回一个 Buf,其中每个元素是 0 到 255 之间的整数(无符号8位范围):

my Buf $buffer = $fh->read( $count );

Buf 是一种 Positional。每个八位字节都是缓冲区的一个元素,你可以通过它的位置获得一个八位字节:

my $third_byte = $buffer[2];

下次调用 .read 时,你将从文件中你离开的位置开始获取八位字节。使用 .seek 移动到其他位置。指定SeekFromCurrent 从你离开的位置移动:

my $relative_position = 137;
$fh.seek( $relative_position, SeekFromCurrent );

使用负值向后移动:

my $negative_position = -137;
$fh.seek( $negative_position, SeekFromCurrent );

如果指定 SeekFromBeginning,它将从文件的开头开始计数并移动到你指定的绝对位置:

my $absolute_position = 1370;
$fh.seek( $absolute_position, SeekFromBeginning );

EXERCISE 8.15 写一个小的十六进制转储程序。一次读取16个八位字节的原始文件。打印每个八位字节的十六进制值,它们之间有空格,末尾有换行符。每行应该有这样的形式:20 50 65 72 6c 20 36 2c 20 4d 6f 61 72 56 4d 20

写二进制文件

另一方面,你可以将八位字节写入文件。使用相同的 :bin 副词打开文件进行写入:

unless my $fh = open $path, :w, :bin {
    ...;
}

使用 .write 并给它一个 Buf 对象。每个元素必须是 0 到 255 之间的整数:

my $buf = Buf.new: 82, 97, 107, 117, 100, 111, 10;
$fh.write: $buf;

用十六进制表示它们可能更容易:

my $buf = Buf.new: <52 61 6b 75 64 6f 0a>.map: *.parse-base: 16;

练习8.16 实现程序将 Buf 写入文件。最终文件中的内容是什么?

总结

你在本章中看到的功能可能是你编写的许多有用程序的核心。你可以将数据放入文件中,以后再检索该数据。你可以创建目录,将文件移动到这些目录中,或者将它们全部删除。大多数操作简单明了;一旦你知道了正确的对象,你就能轻松找到所需的方法。然而,这些东西中的大多数都与外部世界相互作用,并在事情无法解决时抱怨。不要忽视那些抱怨!

comments powered by Disqus