Kapitel 4. Kopfweh
Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com
Steh zur Abwechslung mal auf deinem eigenen Kopf / Gib mir etwas Haut, die ich mein Eigen nennen kann
They Might Be Giants, "Stand on Your Own Head" (1988)
Die Herausforderung in diesem Kapitel besteht darin, das Programm head
zu implementieren, das die ersten paar Zeilen oder Bytes einer oder mehrerer Dateien ausgibt.ist eine gute Möglichkeit, den Inhalt einer regulären Textdatei zu betrachten und oft eine viel bessere Wahl als cat
.
Wenn du mit einem Verzeichnis konfrontiert wirst, das z. B. die Ausgabedateien eines Prozesses enthält, kann head
dir helfen, schnell nach möglichen Problemen zu suchen. Es ist besonders nützlich, wenn du mit extrem großen Dateien zu tun hast, da es nur die ersten paar Bytes oder Zeilen einer Datei liest (im Gegensatz zu cat
, das immer die gesamte Datei liest).
In diesem Kapitel erfährst du, wie du Folgendes tun kannst:
-
Erstelle optionale Befehlszeilenargumente, die numerische Werte akzeptieren
-
Zwischen Typen konvertieren mit
as
-
take
für einen Iterator oder einen Filehandle verwenden -
Zeilenenden beim Lesen eines Filehandles beibehalten
-
Bytes versus Zeichen aus einem Filehandle lesen
-
Verwende den Turbofish-Operator
Wie der Kopf funktioniert
Ich beginne mit einem Überblick über head
, damit du weißt, was von deinem Programm erwartet wird.Es gibt viele Implementierungen des ursprünglichen AT&T Unix-Betriebssystems, wie Berkeley Standard Distribution (BSD), SunOS/Solaris, HP-UX und Linux.
Die meisten dieser Betriebssysteme haben eine Version des Programms head
, das standardmäßig die ersten 10 Zeilen einer oder mehrerer Dateien anzeigt. Die meisten haben wahrscheinlich die Optionen -n
, um die Anzahl der angezeigten Zeilen zu steuern, und -c
, um stattdessen eine bestimmte Anzahl von Bytes anzuzeigen. Die BSD-Version hat nur diese beiden Optionen, die ich über man head
:
HEAD(1) BSD General Commands Manual HEAD(1) NAME head -- display first lines of a file SYNOPSIS head [-n count | -c bytes] [file ...] DESCRIPTION This filter displays the first count lines or bytes of each of the speci- fied files, or of the standard input if no files are specified. If count is omitted it defaults to 10. If more than a single file is specified, each file is preceded by a header consisting of the string ''==> XXX <=='' where ''XXX'' is the name of the file. EXIT STATUS The head utility exits 0 on success, and >0 if an error occurs. SEE ALSO tail(1) HISTORY The head command appeared in PWB UNIX. BSD June 6, 1993 BSD
Mit der GNU Version kann ich head --help
ausführen, um die Verwendung zu lesen:
Usage: head [OPTION]... [FILE]... Print the first 10 lines of each FILE to standard output. With more than one FILE, precede each with a header giving the file name. With no FILE, or when FILE is -, read standard input. Mandatory arguments to long options are mandatory for short options too. -c, --bytes=[-]K print the first K bytes of each file; with the leading '-', print all but the last K bytes of each file -n, --lines=[-]K print the first K lines instead of the first 10; with the leading '-', print all but the last K lines of each file -q, --quiet, --silent never print headers giving file names -v, --verbose always print headers giving file names --help display this help and exit --version output version information and exit K may have a multiplier suffix: b 512, kB 1000, K 1024, MB 1000*1000, M 1024*1024, GB 1000*1000*1000, G 1024*1024*1024, and so on for T, P, E, Z, Y.
Beachte, dass die GNU-Version negative Zahlen für -n
und -c
und mit Suffixen wie K
, M
, usw. angeben kann, die das Challenge-Programm nicht implementiert. Sowohl in der BSD- als auch in der GNU-Version sind die Dateien optionale Positionsargumente, die standardmäßig STDIN
lesen oder wenn ein Dateiname ein Bindestrich ist.
Um zu zeigen, wie head
funktioniert, verwende ich die Dateien in 04_headr/tests/inputs:
-
empty.txt: eine leere Datei
-
one.txt: eine Datei mit einer Textzeile
-
two.txt: eine Datei mit zwei Textzeilen
-
three.txt: eine Datei mit drei Textzeilen und Windows-Zeilenenden
-
twelve.txt: eine Datei mit 12 Textzeilen
Bei einer leeren Datei gibt es keine Ausgabe, was du mit dem folgenden Befehl überprüfen kannst head tests/inputs/empty.txt
Wie bereits erwähnt, gibt head
standardmäßig die ersten 10 Zeilen einer Datei aus:
$ head tests/inputs/twelve.txt one two three four five six seven eight nine ten
Mit der Option -n
kannst du die Anzahl der angezeigten Zeilen festlegen. Mit dem folgenden Befehl kann ich zum Beispiel festlegen, dass nur die ersten beiden Zeilen angezeigt werden:
$ head -n 2 tests/inputs/twelve.txt one two
Die Option -c
zeigt nur die angegebene Anzahl von Bytes aus einer Datei an. Ich kann zum Beispiel nur die ersten beiden Bytes anzeigen:
$ head -c 2 tests/inputs/twelve.txt on
Seltsamerweise erlaubt es die GNU-Version, sowohl -n
als auch -c
anzugeben und zeigt standardmäßig Bytes an. Die BSD-Version weist beide Argumente zurück:
$ head -n 1 -c 2 tests/inputs/one.txt head: can't combine line and byte counts
Jeder Wert für -n
oder -c
, der keine positive ganze Zahl ist, führt zu einem Fehler, der das Programm anhält, und die Fehlermeldung enthält den illegalen Wert:
$ head -n 0 tests/inputs/one.txt head: illegal line count -- 0 $ head -c foo tests/inputs/one.txt head: illegal byte count -- foo
Wenn es mehrere Argumente gibt, fügt head
eine Kopfzeile und eine Leerzeile zwischen den einzelnen Dateien ein. Beachte in der folgenden Ausgabe, dass das erste Zeichen in tests/inputs/one.txt ein Ö ist, ein dummes Multibyte-Zeichen, das ich eingefügt habe, um das Programm zu zwingen, zwischen Bytes und Zeichen zu unterscheiden:
$ head -n 1 tests/inputs/*.txt ==> tests/inputs/empty.txt <== ==> tests/inputs/one.txt <== Öne line, four words. ==> tests/inputs/three.txt <== Three ==> tests/inputs/twelve.txt <== one ==> tests/inputs/two.txt <== Two lines.
Wenn keine Dateiargumente vorhanden sind, liest head
von STDIN
:
$ cat tests/inputs/twelve.txt | head -n 2 one two
Wie bei cat
in Kapitel 3 wird jede nicht existierende oder unlesbare Datei übersprungen und eine Warnung auf STDERR
ausgegeben. Im folgenden Befehl verwende ich blargh als nicht existierende Datei und erstelle eine unlesbare Datei namens cant-touch-this:
$ touch cant-touch-this && chmod 000 cant-touch-this $ head blargh cant-touch-this tests/inputs/one.txt head: blargh: No such file or directory head: cant-touch-this: Permission denied ==> tests/inputs/one.txt <== Öne line, four words.
Mehr muss das Challenge-Programm dieses Kapitels nicht umsetzen.
Erste Schritte
Du hast vielleicht schon geahnt, dass das Programm, das du schreiben sollst, headr
(sprich: head-er) heißen wird.Beginne mit dem Aufruf cargo new headr
und füge dann die folgenden Abhängigkeiten zu deiner Cargo.toml hinzu:
[dependencies]
anyhow
=
"1.0.79"
clap
=
{
version
=
"4.5.0"
,
features
=
[
"derive"
]
}
[dev-dependencies]
assert_cmd
=
"2.0.13"
predicates
=
"3.0.4"
pretty_assertions
=
"1.4.0"
rand
=
"0.8.5"
Kopiere mein 04_headr/tests-Verzeichnis in dein Projektverzeichnis und führe dann den Test aus. cargo test
Alle Tests sollten fehlschlagen. Deine Aufgabe, wenn du sie annimmst, ist es, ein Programm zu schreiben, das diese Tests besteht. Ich schlage vor, dass du src/main.rs mit dem folgenden Code beginnst, um die drei Parameter des Programms mit einer Args
Struktur darzustellen:
#[
derive(Debug)
]
struct
Args
{
files
:
Vec
<
String
>
,
lines
:
u64
,
bytes
:
Option
<
u64
>
,
}
Die Anzahl der zu druckenden
lines
ist von der Artu64
.
Tipp
Alle Befehlszeilenargumente für dieses Programm sind optional, da files
standardmäßig mit einem Bindestrich (-
), lines
standardmäßig mit 10
und bytes
weggelassen werden können.
Die primitive u64
ist eine vorzeichenlose Ganzzahl, die 8 Byte Speicherplatz benötigt und usize
ähnelt. ist eine vorzeichenlose Ganzzahl in Zeigergröße, deren Größe zwischen 4 Byte auf einem 32-Bit-Betriebssystem und 8 Byte auf einem 64-Bit-System variiert. Rust hat auch einen Typ isize
, der eine vorzeichenbehaftete Ganzzahl in Zeigergröße ist, die du benötigst, um negative Zahlen darzustellen, wie es die GNU-Version tut.
Da du nur positive Zahlen à la der BSD-Version speichern willst, kannst du bei einem vorzeichenlosen Typ bleiben. Beachte die anderen Rust-Typen u32
/i32
(vorzeichenlose/vorzeichenbehaftete 32-Bit-Ganzzahl) und u64
/i64
(vorzeichenlose/vorzeichenbehaftete 64-Bit-Ganzzahl), wenn du eine genauere Kontrolle darüber haben willst, wie groß diese Werte sein können.
Die Parameter lines
und bytes
werden in Funktionen verwendet, die die Typen usize
und u64
erwarten, daher werden wir später besprechen, wie man zwischen diesen Typen konvertiert. Dein Programm sollte 10
als Standardwert für lines
verwenden, aber bytes
wird ein Option
Das bedeutet, dass bytes
entweder Some<u64>
ist, wenn der Benutzer einen gültigen Wert angibt, oder None
, wenn er keinen Wert angibt.
Ich fordere dich auf, die Kommandozeilenargumente in dieser Struktur zu parsen, wie du willst. Um das Derive-Muster zu verwenden, kommentiere die vorangehende Args
entsprechend.Wenn du lieber dem Builder-Muster folgst, solltest du eine get_args
Funktion mit der folgenden Gliederung schreiben:
fn
get_args
()
->
Args
{
let
matches
=
Command
::new
(
"headr"
)
.
version
(
"0.1.0"
)
.
author
(
"Ken Youens-Clark <kyclark@gmail.com>"
)
.
about
(
"Rust version of `head`"
)
// What goes here?
.
get_matches
();
Args
{
files
:..
.
lines
:..
.
bytes
:..
.
}
}
Aktualisiere main
, um die Argumente zu parsen und auszudrucken:
fn
main
()
{
let
args
=
Args
::parse
();
println!
(
"{:#?}"
,
args
);
}
Schau, ob du dein Programm dazu bringen kannst, eine Verwendung wie die folgende zu drucken.Beachte, dass ich die kurzen und langen Namen aus der GNU-Version verwende:
$ cargo run -- -h Rust version of `head` Usage: headr [OPTIONS] [FILE]... Arguments: [FILE]... Input file(s) [default: -] Options: -n, --lines <LINES> Number of lines [default: 10] -c, --bytes <BYTES> Number of bytes -h, --help Print help -V, --version Print version
Führe das Programm ohne Eingaben aus und überprüfe, ob die Standardwerte richtig eingestellt sind:
$ cargo run Args { files: [ "-", ], lines: 10, bytes: None, }
files
sollte standardmäßig einen Bindestrich (-
) als Dateinamen enthalten.Die Nummer von
lines
sollte standardmäßig10
lauten.bytes
sollteNone
sein.
Führe nun das Programm mit Argumenten aus und stelle sicher, dass sie richtig geparst werden:
$ cargo run -- -n 3 tests/inputs/one.txt Args { files: [ "tests/inputs/one.txt", ], lines: 3, bytes: None, }
Das Positionsargument tests/inputs/one.txt wird als eines der
files
geparst.Die Option
-n
fürlines
setzt dies auf3
.Die Option
-b
fürbytes
ist standardmäßig aufNone
eingestellt.
Wenn ich mehr als ein Positionsargument angebe, werden sie alle in files
gespeichert, und das -c
-Argument wird in bytes
gespeichert. Im folgenden Befehl verlasse ich mich wieder auf die bash
Shell, um die Datei glob *.txt
in alle Dateien mit der Endung .txt zu expandieren. PowerShell-Benutzer sollten sich die entsprechende Verwendung von Get-ChildItem
ansehen, die im Abschnitt "Iterieren durch die Datei-Argumente" gezeigt wird :
$ cargo run -- -c 4 tests/inputs/*.txt Args { files: [ "tests/inputs/empty.txt", "tests/inputs/one.txt", "tests/inputs/three.txt", "tests/inputs/twelve.txt", "tests/inputs/two.txt", ], lines: 10, bytes: Some( 4, ), }
Es gibt vier Dateien, die auf .txt enden.
lines
ist immer noch auf den Standardwert10
eingestellt.Die
-c 4
führt dazu, dass diebytes
nunSome(4)
ist.
Jeder Wert für -n
oder -c
, der nicht in eine positive ganze Zahl umgewandelt werden kann, sollte dazu führen, dass das Programm mit einem Fehler abbricht.Verwende clap::value_parser
um sicherzustellen, dass die ganzzahligen Argumente gültig sind, und wandle sie in Zahlen um:
$ cargo run -- -n blargh tests/inputs/one.txt error: invalid value 'blargh' for '--lines <LINES>': invalid digit found in string $ cargo run -- -c 0 tests/inputs/one.txt error: invalid value '0' for '--bytes <BYTES>': 0 is not in 1..18446744073709551615
Das Programm sollte sowohl die Verwendung von -n
als auch von -c
verbieten:
$ cargo run -- -n 1 -c 1 tests/inputs/one.txt error: the argument '--lines <LINES>' cannot be used with '--bytes <BYTES>' Usage: headr --lines <LINES> <FILE>...
Hinweis
Allein das Parsen und Validieren der Argumente ist eine Herausforderung, aber ich weiß, dass du es schaffen kannst. Hör hier auf zu lesen und bring dein Programm dazu, alle Tests zu bestehen, die mit cargo test dies
:
running 3 tests test dies_bad_lines ... ok test dies_bad_bytes ... ok test dies_bytes_and_lines ... ok
Definieren der Argumente
Willkommen zurück. Ich zeige zunächst das Baumuster mit einer get_args
Funktion wie im vorherigen Kapitel. Beachte, dass die beiden optionalen Argumente, lines
und bytes
, numerische Werte akzeptieren. Dies unterscheidet sich von den optionalen Argumenten, die in Kapitel 3 implementiert wurden und als boolesche Flags verwendet werden. Beachte, dass der folgende Code use clap::{Arg, Command}
benötigt:
fn
get_args
(
)
->
Args
{
let
matches
=
Command
::
new
(
"
headr
"
)
.
version
(
"
0.1.0
"
)
.
author
(
"
Ken Youens-Clark <kyclark@gmail.com>
"
)
.
about
(
"
Rust version of `head`
"
)
.
arg
(
Arg
::
new
(
"
lines
"
)
.
short
(
'n'
)
.
long
(
"
lines
"
)
.
value_name
(
"
LINES
"
)
.
help
(
"
Number of lines
"
)
.
value_parser
(
clap
::
value_parser
!
(
u64
)
.
range
(
1
..
)
)
.
default_value
(
"
10
"
)
,
)
.
arg
(
Arg
::
new
(
"
bytes
"
)
.
short
(
'c'
)
.
long
(
"
bytes
"
)
.
value_name
(
"
BYTES
"
)
.
conflicts_with
(
"
lines
"
)
.
value_parser
(
clap
::
value_parser
!
(
u64
)
.
range
(
1
..
)
)
.
help
(
"
Number of bytes
"
)
,
)
.
arg
(
Arg
::
new
(
"
files
"
)
.
value_name
(
"
FILE
"
)
.
help
(
"
Input file(s)
"
)
.
num_args
(
0
..
)
.
default_value
(
"
-
"
)
,
)
.
get_matches
(
)
;
Args
{
files
:
matches
.
get_many
(
"
files
"
)
.
unwrap
(
)
.
cloned
(
)
.
collect
(
)
,
lines
:
matches
.
get_one
(
"
lines
"
)
.
cloned
(
)
.
unwrap
(
)
,
bytes
:
matches
.
get_one
(
"
bytes
"
)
.
cloned
(
)
,
}
}
Die Option
lines
nimmt einen Wert an und ist standardmäßig auf10
eingestellt.Die Option
bytes
nimmt einen Wert an und steht im Konflikt mit dem Parameterlines
, so dass sie sich gegenseitig ausschließen.Der Parameter
files
ist positionsabhängig, kann null oder mehr Werte annehmen und ist standardmäßig ein Bindestrich (-
).
Alternativ dazu erfordert das clap
Ableitungsmuster die Annotation der Args
Struktur:
#[derive(Parser, Debug)]
#[command(author, version, about)]
/// Rust version of `head`
struct
Args
{
/// Input file(s)
#[arg(default_value =
"-"
, value_name =
"FILE"
)]
files
:Vec
<
String
>
,
/// Number of lines
#[arg(
short('n'),
long,
default_value =
"10"
,
value_name =
"LINES"
,
value_parser = clap::value_parser!(u64).range(1..)
)]
lines
:u64
,
/// Number of bytes
#[arg(
short('c'),
long,
value_name =
"BYTES"
,
conflicts_with(
"lines"
),
value_parser = clap::value_parser!(u64).range(1..)
)]
bytes
:Option
<
u64
>
,
}
Tipp
Im Ableitungsmuster ist der Standard Arg::long
Wert der Name des Strukturfeldes, z. B. Zeilen und Bytes. Der Standardwert für Arg::short
ist der erste Buchstabe des struct-Feldes, also l oder b. Ich gebe die Kurznamen n bzw. c an, um mit dem ursprünglichen Tool übereinzustimmen.
Es ist ziemlich viel Arbeit, alle Benutzereingaben zu überprüfen, aber jetzt habe ich die Gewissheit, dass ich mit guten Daten fortfahren kann.
Verarbeitung der Eingabedateien
Ich empfehle, dass du deine main
eine run
Funktion aufrufen lässt. Achte darauf, dass use anyhow::Result
für das Folgende:
fn
main
()
{
if
let
Err
(
e
)
=
run
(
Args
::parse
())
{
eprintln!
(
"{e}"
);
std
::process
::exit
(
1
);
}
}
fn
run
(
_args
:Args
)
->
Result
<
()
>
{
Ok
(())
}
Dieses Challenge-Programm sollte die Eingabedateien wie in Kapitel 3 behandeln, also schlage ich vor, dass du die gleiche Funktion open
hinzufügst:
fn
open
(
filename
:&
str
)
->
Result
<
Box
<
dyn
BufRead
>>
{
match
filename
{
"-"
=>
Ok
(
Box
::new
(
BufReader
::new
(
io
::stdin
()))),
_
=>
Ok
(
Box
::new
(
BufReader
::new
(
File
::open
(
filename
)
?
))),
}
}
Vergewissere dich, dass du alle diese zusätzlichen Abhängigkeiten hinzufügst:
use
std
::fs
::File
;
use
std
::io
::{
self
,
BufRead
,
BufReader
};
Erweitere deine run
Funktion, um zu versuchen, die Dateien zu öffnen und Fehler auszudrucken, wenn du sie findest:
fn
run
(
args
:
Args
)
->
Result
<
(
)
>
{
for
filename
in
args
.
files
{
match
open
(
&
filename
)
{
Err
(
err
)
=
>
eprintln!
(
"
{filename}: {err}
"
)
,
Ok
(
_
)
=
>
println!
(
"
Opened {filename}
"
)
,
}
}
Ok
(
(
)
)
}
Iteriere durch die einzelnen Dateinamen.
Versucht, die angegebene Datei zu öffnen.
Druckfehler an
STDERR
.Druckt eine Meldung, dass die Datei erfolgreich geöffnet wurde.
Führe dein Programm mit einer guten und einer schlechten Datei aus, um sicherzustellen, dass es funktioniert. Im folgenden Befehl steht blargh für eine nicht existierende Datei:
$ cargo run -- blargh tests/inputs/one.txt blargh: No such file or directory (os error 2) Opened tests/inputs/one.txt
Ohne einen Blick auf meine Lösung zu werfen, überlege dir, wie du die Zeilen und dann die Bytes einer bestimmten Datei lesen kannst. Als Nächstes fügst du die Kopfzeilen hinzu, die mehrere Dateiargumente trennen. Sieh dir die Fehlerausgabe des Originalprogramms head
bei ungültigen Dateien genau an und stelle fest, dass bei lesbaren Dateien zuerst eine Kopfzeile und dann die Dateiausgabe erscheint, bei ungültigen Dateien aber nur ein Fehler ausgegeben wird.Außerdem gibt es eine zusätzliche Leerzeile, die die Ausgabe für die gültigen Dateien trennt:
$ head -n 1 tests/inputs/one.txt blargh tests/inputs/two.txt ==> tests/inputs/one.txt <== Öne line, four words. head: blargh: No such file or directory ==> tests/inputs/two.txt <== Two lines.
Ich habe speziell für dich einige herausfordernde Eingaben entwickelt. Um zu sehen, was auf dich zukommt, benutze den Befehl file
, um Informationen zum Dateityp zu erhalten:
$ file tests/inputs/*.txt tests/inputs/empty.txt: empty tests/inputs/one.txt: UTF-8 Unicode text tests/inputs/three.txt: ASCII text, with CRLF, LF line terminators tests/inputs/twelve.txt: ASCII text tests/inputs/two.txt: ASCII text
Dies ist eine leere Datei, um sicherzustellen, dass dein Programm nicht umkippt.
Diese Datei enthält Unicode, denn ich habe einen Umlaut über das O in Őne gesetzt, um dich zu zwingen, die Unterschiede zwischen Bytes und Zeichen zu beachten.
Diese Datei hat Zeilenenden im Windows-Stil.
Diese Datei hat 12 Zeilen, damit der Standardwert von 10 Zeilen angezeigt wird.
Diese Datei hat Zeilenenden im Unix-Stil.
Tipp
Unter Windows ist der Zeilenumbruch die Kombination aus Wagenrücklauf und Zeilenvorschub, oft als CRLF oder \r\n
dargestellt. Auf Unix-Plattformen wird nur der Zeilenumbruch verwendet, also LF oder \n
. Diese Zeilenenden müssen in der Ausgabe deines Programms erhalten bleiben, also musst du einen Weg finden, die Zeilen in einer Datei zu lesen, ohne die Zeilenenden zu entfernen.
Lesen von Bytes versus Charaktere
Bevor du fortfährst, solltest du den Unterschied zwischen dem Lesen von Bytes und Zeichen aus einer Datei verstehen. In den frühen 1960er Jahren repräsentierte die ASCII-Tabelle (American Standard Code for Information Interchange) mit 128 Zeichen alle möglichen Textelemente in der Informatik.Um so viele Zeichen darzustellen, braucht man nur sieben Bits (27 = 128). Normalerweise besteht ein Byte aus acht Bits, also waren die Begriffe Byte und Zeichen austauschbar.
Seit der Einführung von Unicode (Universal Coded Character Set) zur Darstellung aller Schriftsysteme der Welt (und sogar der Emojis) benötigen manche Zeichen bis zu vier Bytes.Der Unicode-Standard definiert mehrere Möglichkeiten zur Kodierung von Zeichen, darunter UTF-8 (Unicode Transformation Format mit acht Bits).
Wie bereits erwähnt, beginnt die Datei tests/inputs/one.txt mit dem Zeichen Ő, das in UTF-8 zwei Bytes lang ist. Wenn du möchtest, dass head
dir dieses eine Zeichen anzeigt, musst du zwei Bytes anfordern:
$ head -c 2 tests/inputs/one.txt Ö
Wenn du head
bittest, nur das erste Byte aus dieser Datei auszuwählen, erhältst du den Byte-Wert 195
, der keine gültige UTF-8-Zeichenkette ist. Die Ausgabe ist ein Sonderzeichen, das auf ein Problem bei der Umwandlung eines Zeichens in Unicode hinweist:
$ head -c 1 tests/inputs/one.txt �
Das Challenge-Programm soll dieses Verhalten nachbilden. Es ist nicht einfach, dieses Programm zu schreiben, aber du solltest in der Lage sein, mit std::io
, std::fs::File
, und std::io::BufReader
herauszufinden, wie man Bytes und Zeilen aus jeder der Dateien liest. Beachte, dass in Rust eine String
eine gültige UTF-8-kodierte Zeichenkette sein muss und daher die Methode String::from_utf8_lossy
Ich habe in tests/cli.rs eine ganze Reihe von Tests eingefügt, die du in deinen Quellbaum kopiert haben solltest.
Hinweis
Hör hier auf zu lesen und beende das Programm. Verwende cargo test
häufig, um deinen Fortschritt zu überprüfen. Gib dein Bestes, um alle Tests zu bestehen, bevor du dir meine Lösung ansiehst.
Lösung
Diese Aufgabe war interessanter, als ich erwartet hatte. Ich dachte, es wäre nicht viel mehr als eine Variation von cat
, aber es stellte sich heraus, dass sie um einiges schwieriger war. Ich zeige dir, wie ich zu meiner Lösung gekommen bin.
Eine Datei Zeile für Zeile lesen
Nachdem ich die gültigen Dateien geöffnet hatte, begann ich damit, Zeilen aus dem Filehandle zu lesen. Ich beschloss, einen Code aus Kapitel 3 zu ändern:
fn
run
(
args
:
Args
)
->
Result
<
(
)
>
{
for
filename
in
args
.
files
{
match
open
(
&
filename
)
{
Err
(
err
)
=
>
eprintln!
(
"
{filename}: {err}
"
)
,
Ok
(
file
)
=
>
{
for
line
in
file
.
lines
(
)
.
take
(
args
.
lines
as
usize
)
{
println!
(
"
{}
"
,
line
?
)
;
}
}
}
}
Ok
(
(
)
)
}
Verwende
Iterator::take
wählst du die gewünschte Anzahl von Zeilen aus dem Filehandle aus.Gib die Zeile auf der Konsole aus.
Tipp
Die Methode Iterator::take
erwartet, dass ihr Argument vom Typ usize
ist, aber ich habe einen u64
. Ich caste oder konvertiere den Wert mit dem Schlüsselwortas
.
Ich finde diese Lösung lustig, weil sie die Methode Iterator::take
verwendet, um die gewünschte Anzahl von Zeilen auszuwählen. Ich kann das Programm ausführen, um eine Zeile aus einer Datei auszuwählen, und es scheint gut zu funktionieren:
$ cargo run -- -n 1 tests/inputs/twelve.txt one
Wenn ich cargo test
ausführe, besteht das Programm fast die Hälfte der Tests, was ziemlich gut dafür ist, dass es nur einen kleinen Teil der Spezifikationen implementiert hat; allerdings schlägt es bei allen Tests fehl, die die Windows-kodierte Eingabedatei verwenden. Um dieses Problem zu lösen, muss ich etwas gestehen.
Zeilenenden beim Lesen einer Datei beibehalten
Ich sage es dir nur ungern, lieber Leser, aber das catr
Programm in Kapitel 3 entspricht nicht ganz dem ursprünglichen cat
Programm, denn es verwendet BufRead::lines
verwendet, um die Eingabedateien zu lesen.In der Dokumentation zu dieser Funktion heißt es: "Jede zurückgegebene Zeichenkette hat kein Zeilenumbruch-Byte (das 0xA
Byte) oder CRLF (0xD
, 0xA
Bytes) am Ende." Ich hoffe, du verzeihst mir, denn ich wollte dir zeigen, wie einfach es sein kann, die Zeilen einer Datei zu lesen, aber du solltest wissen, dass das catr
Programm die CRLF-Zeilenenden von Windows durch Zeilenumbrüche im Unix-Stil ersetzt.
Um dies zu beheben, muss ich stattdessen BufRead::read_line
verwenden, das laut der Dokumentation "Bytes aus dem zugrunde liegenden Stream liest, bis das Trennzeichen (das 0xA
Byte) oder EOF gefunden wird. Sobald es gefunden wurde, werden alle Bytes bis einschließlich des Begrenzungszeichens (falls gefunden) an buf
angehängt."1
Es folgt eine Version, die die ursprünglichen Zeilenenden beibehält. Mit diesen Änderungen wird das Programm mehr Tests bestehen als fehlschlagen:
fn
run
(
args
:
Args
)
->
Result
<
(
)
>
{
for
filename
in
args
.
files
{
match
open
(
&
filename
)
{
Err
(
err
)
=
>
eprintln!
(
"
{filename}: {err}
"
)
,
Ok
(
mut
file
)
=
>
{
let
mut
line
=
String
::
new
(
)
;
for
_
in
0
..
args
.
lines
{
let
bytes
=
file
.
read_line
(
&
mut
line
)
?
;
if
bytes
=
=
0
{
break
;
}
print!
(
"
{line}
"
)
;
line
.
clear
(
)
;
}
}
}
;
}
Ok
(
(
)
)
}
Akzeptiere den Filehandle als veränderbaren Wert.
Verwende
String::new
um einen neuen, leeren, veränderbaren String-Puffer zu erstellen, der jede Zeile aufnimmt.Verwende
for
, um durch eine Tabelle zu iterierenstd::ops::Range
zu durchlaufen, um von Null bis zur gewünschten Anzahl von Zeilen zu zählen. Der Variablenname_
zeigt an, dass ich nicht beabsichtige, sie zu verwenden.Verwende
BufRead::read_line
um die nächste Zeile in den String-Puffer zu lesen.Der Filehandle gibt null Bytes zurück, wenn er das Ende der Datei erreicht, also
break
aus der Schleife.Drucke die Zeile, einschließlich des ursprünglichen Zeilenendes.
Verwende
String::clear
um den Zeilenspeicher zu leeren.
Wenn ich das Programm cargo test
ausführe, besteht das Programm fast alle Tests für das Lesen von Zeilen und schlägt bei allen Tests für das Lesen von Bytes und die Handhabung mehrerer Dateien fehl.
Bytes aus einer Datei lesen
Als Nächstes lese ich Bytes aus einer Datei. Nachdem ich versucht habe, die Datei zu öffnen, prüfe ich, ob args.bytes
Some
Anzahl der Bytes ist; andernfalls verwende ich den vorhergehenden Code, der Zeilen liest. Für den folgenden Code musst du use std::io::Read
zu deinen Importen hinzufügen:
for
filename
in
args
.
files
{
match
open
(
&
filename
)
{
Err
(
err
)
=
>
eprintln!
(
"
{filename}: {err}
"
)
,
Ok
(
mut
file
)
=
>
{
if
let
Some
(
num_bytes
)
=
args
.
bytes
{
let
mut
buffer
=
vec!
[
0
;
num_bytes
as
usize
]
;
let
bytes_read
=
file
.
read
(
&
mut
buffer
)
?
;
print!
(
"
{}
"
,
String
::
from_utf8_lossy
(
&
buffer
[
..
bytes_read
]
)
)
;
}
else
{
..
.
// Same as before
}
}
}
;
}
Nutze den Mustervergleich, um zu prüfen, ob
args.bytes
Some
Anzahl der zu lesenden Bytes ist.Erstelle einen veränderbaren Puffer mit einer festen Länge
num_bytes
, der mit Nullen gefüllt wird, um die aus der Datei gelesenen Bytes zu speichern.Liest Bytes aus dem Filehandle in den Puffer. Der Wert
bytes_read
enthält die Anzahl der gelesenen Bytes, die geringer sein kann als dieangeforderte Anzahl.Konvertiere die ausgewählten Bytes in eine Zeichenkette, die möglicherweise nicht gültig ist (UTF-8). Beachte die Bereichsoperation, um nur die tatsächlich gelesenen Bytes auszuwählen.
Wie du gesehen hast, wenn du nur einen Teil eines Multibyte-Zeichens auswählst, kann die Umwandlung von Bytes in Zeichen fehlschlagen, weil Strings in Rust gültiges UTF-8 sein müssen. Die FunktionString::from_utf8
gibt nur dann ein Ok
zurück, wenn der String gültig ist, aber String::from_utf8_lossy
wandelt ungültige UTF-8-Sequenzen in das unbekannte oder Ersatzzeichen um:
$ cargo run -- -c 1 tests/inputs/one.txt �
Ich zeige dir einen anderen, viel schlechteren Weg, die Bytes aus einer Datei zu lesen. Du kannst die gesamte Datei in einen String einlesen, diesen in einen Vektor von Bytes umwandeln und dann das erste num_bytes
auswählen:
let
mut
contents
=
String
::
new
(
)
;
file
.
read_to_string
(
&
mut
contents
)
?
;
// Danger here
let
bytes
=
contents
.
as_bytes
(
)
;
print!
(
"
{}
"
,
String
::
from_utf8_lossy
(
&
bytes
[
..
num_bytes
as
usize
]
)
// More danger
)
;
Erstelle einen neuen String-Puffer, der den Inhalt der Datei enthält.
Lies den gesamten Inhalt der Datei in den String-Puffer.
Verwende
str::as_bytes
um den Inhalt in Bytes umzuwandeln (u8
oder vorzeichenlose8-Bit-Ganzzahlen).Verwandle mit
String::from_utf8_lossy
ein Stück vonbytes
in einen String.
Wie ich bereits erwähnt habe, kann dieser Ansatz dein Programm oder deinen Computer zum Absturz bringen, wenn die Größe der Datei den Arbeitsspeicher deines Rechners übersteigt. Ein weiteres ernsthaftes Problem mit dem obigen Code ist, dass er davon ausgeht, dass die Slice-Operation bytes[..num_bytes]
erfolgreich sein wird. Wenn du diesen Code zum Beispiel mit einer leeren Datei verwendest, fragst du nach Bytes, die nicht existieren. Das führt dazu, dass dein Programm in Panik gerät und sofort mit einer Fehlermeldung beendet wird:
$ cargo run -- -c 1 tests/inputs/empty.txt thread 'main' panicked at src/main.rs:53:55: range end index 1 out of range for slice of length 0 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Im Folgenden findest du einen sicheren - und vielleicht den kürzesten - Weg, um die gewünschte Anzahl von Bytes aus einer Datei zu lesen. Achte darauf, die Eigenschaft use std::io::Read
zu deinen Importen hinzuzufügen:
let
bytes
:Result
<
Vec
<
_
>
,
_
>
=
file
.
bytes
().
take
(
num_bytes
as
usize
).
collect
();
print!
(
"{}"
,
String
::from_utf8_lossy
(
&
bytes
?
));
Im vorangegangenen Code ist die Typ-Annotation Result<Vec<_>, _>
notwendig, da der Compiler den Typ von bytes
als Slice schlussfolgert, das eine unbekannte Größe hat. Ich muss angeben, dass ich ein Vec
haben möchte, das ein intelligenter Zeiger auf im Heap zugewiesenen Speicher ist.Die Unterstriche (_
) zeigen eine partielle Typ-Annotation an, die den Compiler veranlasst, die Typen zu schlussfolgern.Ohne eine Typ-Annotation für bytes
beschwert sich der Compiler entsprechend:
error[E0277]: the size for values of type `[u8]` cannot be known at compilation time --> src/main.rs:50:59 | 95 | print!("{}", String::from_utf8_lossy(&bytes?)); | ^^^^^^^ doesn't | have a size known at compile-time | = help: the trait `Sized` is not implemented for `[u8]` = note: all local variables must have a statically known size = help: unsized locals are gated as an unstable feature
Hinweis
Du hast jetzt gesehen, dass der Unterstrich (_
) verschiedene Funktionen hat. Als Präfix oder Name einer Variablen zeigt er dem Compiler, dass du den Wert nicht verwenden willst. In einem match
Arm ist er der Platzhalter für die Behandlung von Groß- und Kleinschreibung. Wenn es in einer Typ-Annotation verwendet wird, weist es den Compiler an, den Typ abzuleiten.
Du kannst die Typinformation auch auf der rechten Seite des Ausdrucks angeben, indem du den Turbofish-Operator (::<>
) verwendest. Oft ist es eine Frage des Stils, ob du den Typ auf der linken oder rechten Seite angibst, aber später wirst du Beispiele sehen, in denen der Turbofish für einige Ausdrücke erforderlich ist.So würde das vorherige Beispiel aussehen, wenn der Typ stattdessen mit dem Turbofish angegeben würde:
let
bytes
=
file
.
bytes
(
)
.
take
(
num_bytes
as
usize
)
.
collect
:
:
<
Result
<
Vec
<
_
>
,
_
>
>
(
)
;
Das unbekannte Zeichen, das von String::from_utf8_lossy
(b'\xef\xbf\xbd'
) erzeugt wird, ist nicht genau dieselbe Ausgabe, die von der BSD head
(b'\xc3'
) erzeugt wird, was den Test etwas schwierig macht.Wenn du dir die Hilfsfunktion run
in tests/cli.rs ansiehst, wirst du sehen, dass ich den erwarteten Wert (die Ausgabe von head
) gelesen und dieselbe Funktion verwendet habe, um das, was ungültiges UTF-8 sein könnte, zu konvertieren, damit ich die beiden Ausgaben vergleichen kann. Die Funktion run_stdin
funktioniert ähnlich:
fn
run
(
args
:
&
[
&
str
]
,
expected_file
:
&
str
)
->
Result
{
// Extra work here due to lossy UTF
let
mut
file
=
File
::
open
(
expected_file
)
?
;
let
mut
buffer
=
Vec
::
new
(
)
;
file
.
read_to_end
(
&
mut
buffer
)
?
;
let
expected
=
String
::
from_utf8_lossy
(
&
buffer
)
;
let
output
=
Command
::
cargo_bin
(
PRG
)
?
.
args
(
args
)
.
output
(
)
.
expect
(
"
fail
"
)
;
assert!
(
output
.
status
.
success
(
)
)
;
assert_eq!
(
String
::
from_utf8_lossy
(
&
output
.
stdout
)
,
expected
)
;
Ok
(
(
)
)
}
Drucken der Dateitrennzeichen
Wie bereits erwähnt, haben gültige Dateien einen Header, der den Dateinamen in die Markierungen ==>
und <==
einfügt.Dateien nach der ersten haben einen zusätzlichen Zeilenumbruch am Anfang, um die Ausgabe visuell zu trennen.Das bedeutet, dass ich die Dateinummer wissen muss, die ich behandle, was ich mit der MethodeIterator::enumerate
herausfinden kann. Hier ist die endgültige Version meiner run
Funktion, die alle Tests besteht:
fn
run
(
args
:
Args
)
->
Result
<
(
)
>
{
let
num_files
=
args
.
files
.
len
(
)
;
for
(
file_num
,
filename
)
in
args
.
files
.
iter
(
)
.
enumerate
(
)
{
match
open
(
filename
)
{
Err
(
err
)
=
>
eprintln!
(
"
{filename}: {err}
"
)
,
Ok
(
mut
file
)
=
>
{
if
num_files
>
1
{
println!
(
"
{}==> {filename} <==
"
,
if
file_num
>
0
{
"
\n
"
}
else
{
"
"
}
,
)
;
}
if
let
Some
(
num_bytes
)
=
args
.
bytes
{
let
mut
buffer
=
vec!
[
0
;
num_bytes
as
usize
]
;
let
bytes_read
=
file
.
read
(
&
mut
buffer
)
?
;
print!
(
"
{}
"
,
String
::
from_utf8_lossy
(
&
buffer
[
..
bytes_read
]
)
)
;
}
else
{
let
mut
line
=
String
::
new
(
)
;
for
_
in
0
..
args
.
lines
{
let
bytes
=
file
.
read_line
(
&
mut
line
)
?
;
if
bytes
=
=
0
{
break
;
}
print!
(
"
{line}
"
)
;
line
.
clear
(
)
;
}
}
}
}
}
Ok
(
(
)
)
}
Verwende die Methode
Vec::len
, um die Anzahl der Dateien zu ermitteln.Verwende die Methode
Iterator::enumerate
, um die Dateinummer und dieDateinamen zu verfolgen.Drucke nur Kopfzeilen, wenn es mehrere Dateien gibt.
Gib einen Zeilenumbruch aus, wenn
file_num
größer ist als0
, was die erste Datei anzeigt.
Weiter gehen
Es gibt keinen Grund, die Party jetzt zu beenden. Überlege dir, wie GNU head
mit numerischen Werten mit Suffix und negativen Werten umgeht.Zum Beispiel bedeutet -c=1K
, dass die ersten 1.024 Bytes der Datei gedruckt werden, und -n=-3
bedeutet, dass alle bis auf die letzten drei Zeilen der Datei gedruckt werden. Du musst lines
und bytes
in vorzeichenbehaftete Integer-Werte ändern, um sowohl positive als auch negative Zahlen zu speichern. Stelle sicher, dass du GNU head
mit diesen Argumenten ausführst, die Ausgabe in Testdateien aufzeichnest und Tests schreibst, die die neuen Funktionen abdecken, die du hinzufügst.
Du könntest auch eine Option hinzufügen, mit der du nicht nur Bytes, sondern auch Zeichen auswählen kannst. Du kannst die FunktionString::chars
verwenden, um eine Zeichenkette in Zeichen aufzuteilen.Schließlich kopierst du die Testeingabedatei mit den Windows-Zeilenenden(tests/inputs/three.txt) zu den Tests für Kapitel 3. Bearbeite die mk-outs.sh für dieses Programm, um diese Datei einzubinden, und erweitere dann die Tests und das Programm, um sicherzustellen, dass die Zeilenenden erhalten bleiben.
Zusammenfassung
In diesem Kapitel haben wir uns mit einigen schwierigen Themen befasst, z. B. mit der Konvertierung von Typen wie String-Inputs in u64
und dem anschließenden Casting in usize
. Wenn du dich immer noch verwirrt fühlst, solltest du wissen, dass das nicht immer der Fall ist. Wenn du die Dokumentation weiter liest und mehr Code schreibst, wird es irgendwann Sinn machen.
Hier sind einige Dinge, die du in diesem Kapitel erreicht hast:
-
Du hast gelernt, optionale Parameter zu erstellen, die Werte annehmen können. Zuvor waren die Optionen Flags.
-
Du hast gesehen, dass alle Befehlszeilenargumente Zeichenketten sind und hast
clap
benutzt, um die Umwandlung einer Zeichenkette wie"3"
in die Zahl3
zu versuchen. -
Du hast gelernt, wie man Typen mit dem Schlüsselwort
as
umwandelt. -
Du hast herausgefunden, dass die Verwendung von
_
als Name oder Präfix einer Variablen eine Möglichkeit ist, dem Compiler zu zeigen, dass du nicht vorhast, den Wert zu verwenden. Wenn es in einer Typ-Annotation verwendet wird, weist es den Compiler an, den Typ abzuleiten. -
Du hast gelernt, wie du mit
BufRead::read_line
die Zeilenenden beim Lesen eines Filehandles beibehalten kannst. -
Du hast festgestellt, dass die Methode
take
sowohl bei Iteratoren als auch bei Filehandles funktioniert, um die Anzahl der ausgewählten Elemente zu begrenzen. -
Du hast gelernt, dass du mit dem Turbofisch-Operator Typinformationen auf der linken Seite einer Zuweisung oder auf der rechten Seite angeben kannst.
Im nächsten Kapitel erfährst du mehr über Rust-Iteratoren und wie man Eingaben in Zeilen, Bytes und Zeichen aufteilt.
1 EOF ist eine Abkürzung für End of File.
Get Befehlszeilen-Rost now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.