Capítulo 4. Dolores de cabeza
Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com
Párate sobre tu propia cabeza para variar / Dame algo de piel para llamarla mía
They Might Be Giants, "Stand on Your Own Head" (1988)
El reto de este capítulo consiste en implementar el programa head
, que imprimirá las primeras líneas o bytes de uno o varios archivos.Es una buena forma de echar un vistazo al contenido de un archivo de texto normal y, a menudo, es una opción mucho mejor que cat
.
Cuando te enfrentes a un directorio con algo así como archivos de salida de algún proceso, utilizar head
puede ayudarte a explorar rápidamente en busca de posibles problemas. Es especialmente útil cuando se trata de archivos extremadamente grandes, ya que sólo leerá los primeros bytes o líneas de un archivo (a diferencia de cat
, que siempre leerá el archivo completo).
En este capítulo aprenderás a hacer lo siguiente:
-
Crea argumentos opcionales en la línea de comandos que acepten valores numéricos
-
Convertir entre tipos utilizando
as
-
Utiliza
take
en un iterador o un filehandle -
Conservar los finales de línea al leer un fichero
-
Leer bytes frente a caracteres de un mango de archivo
-
Utiliza el operador turbofish
Cómo funciona la cabeza
Empezaré con una visión general de head
para que sepas qué se espera de tu programa.Existen muchas implementaciones del sistema operativo Unix original de AT&T, como Berkeley Standard Distribution (BSD), SunOS/Solaris, HP-UX y Linux.
La mayoría de estos sistemas operativos tienen alguna versión de un programa head
que mostrará por defecto las 10 primeras líneas de 1 o más archivos. La mayoría probablemente tendrá opciones -n
para controlar el número de líneas mostradas y -c
para mostrar en su lugar algún número de bytes. La versión BSD sólo tiene estas dos opciones, que puedo ver mediante 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
Con la versión GNU , puedo ejecutar head --help
para leer el uso:
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.
Ten en cuenta que la versión GNU puede especificar números negativos para -n
y -c
y con sufijos como K
, M
, etc., que el programa de desafío no implementará. Tanto en la versión BSD como en la GNU, los archivos son argumentos posicionales opcionales que leerá STDIN
por defecto o cuando un nombre de archivo sea un guión.
Para demostrar cómo funciona head
, utilizaré los archivos que se encuentran en 04_headr/tests/inputs:
-
empty.txt: un archivo vacío
-
un.txt: un archivo con una línea de texto
-
dos.txt: un archivo con dos líneas de texto
-
tres.txt: un archivo con tres líneas de texto y terminaciones de línea Windows
-
doce.txt: un archivo con 12 líneas de texto
Dado un archivo vacío, no hay salida, lo que puedes verificar con head tests/inputs/empty.txt
Como ya hemos dicho, head
imprimirá por defecto las 10 primeras líneas de un archivo:
$ head tests/inputs/twelve.txt one two three four five six seven eight nine ten
La opción -n
te permite controlar el número de líneas que se muestran. Por ejemplo, puedo elegir mostrar sólo las dos primeras líneas con el siguiente comando:
$ head -n 2 tests/inputs/twelve.txt one two
La opción -c
muestra sólo el número determinado de bytes de un archivo. Por ejemplo, puedo mostrar sólo los dos primeros bytes:
$ head -c 2 tests/inputs/twelve.txt on
Curiosamente, la versión GNU te permitirá proporcionar tanto -n
como -c
y por defecto muestra bytes. La versión BSD rechazará ambos argumentos:
$ head -n 1 -c 2 tests/inputs/one.txt head: can't combine line and byte counts
Cualquier valor de -n
o -c
que no sea un número entero positivo generará un error que detendrá el programa, y el mensaje de error incluirá el valor ilegal:
$ head -n 0 tests/inputs/one.txt head: illegal line count -- 0 $ head -c foo tests/inputs/one.txt head: illegal byte count -- foo
Cuando hay varios argumentos, head
añade una cabecera e inserta una línea en blanco entre cada archivo. Observa en la siguiente salida que el primer carácter de pruebas/entradas/uno.txt es un Ö, un tonto carácter multibyte que inserté para obligar al programa a discernir entre bytes y caracteres:
$ 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.
Sin argumentos de archivo, head
leerá de STDIN
:
$ cat tests/inputs/twelve.txt | head -n 2 one two
Al igual que con cat
en el Capítulo 3, cualquier archivo inexistente o ilegible se salta y se imprime una advertencia en STDERR
. En el siguiente comando, utilizaré blargh como archivo inexistente y crearé un archivo ilegible llamado 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.
Esto es todo lo que el programa de desafíos de este capítulo tendrá que implementar.
Cómo empezar
Ya habrás anticipado que el programa que quiero que escribas se llamará headr
.Empieza ejecutando cargo new headr
y añade las siguientes dependencias a tu Cargo.toml:
[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"
Copia mi directorio 04_headr/tests en el directorio de tu proyecto, y luego ejecuta cargo test
Todas las pruebas deberían fallar. Tu misión, si decides aceptarla, es escribir un programa que supere estas pruebas. Te propongo que empieces src/main.rs con el siguiente código para representar los tres parámetros del programa con una estructura Args
:
#[
derive(Debug)
]
struct
Args
{
files
:
Vec
<
String
>
,
lines
:
u64
,
bytes
:
Option
<
u64
>
,
}
El número de
lines
a imprimir será del tipou64
.
Consejo
Todos los argumentos de la línea de comandos de este programa son opcionales porque files
debe ser por defecto un guión (-
), lines
será por defecto 10
, y bytes
puede omitirse.
El primitivo u64
es un entero sin signo que utiliza 8 bytes de memoria y es similar a un usize
, que es un tipo entero sin signo del tamaño de un puntero con un tamaño que varía de 4 bytes en un sistema operativo de 32 bits a 8 bytes en un sistema de 64 bits. Rust también tiene un tipo isize
, que es un entero con signo del tamaño de un puntero que necesitarías para representar números negativos como hace la versión GNU.
Puesto que sólo quieres almacenar números positivos a la manera de la versión BSD, puedes quedarte con un tipo sin signo. Ten en cuenta los otros tipos de Rust de u32
/i32
(entero de 32 bits sin signo/signado) y u64
/i64
(entero de 64 bits sin signo/signado) si quieres un control más fino sobre lo grandes que pueden ser estos valores.
Los parámetros lines
y bytes
se utilizarán en funciones que esperan los tipos usize
y u64
, por lo que más adelante hablaremos de cómo convertir entre estos tipos. Tu programa debe utilizar 10
como valor por defecto para lines
, pero bytes
será un Option
, que introduje por primera vez en el Capítulo 2. Esto significa que bytes
será Some<u64>
si el usuario proporciona un valor válido o None
si no lo hace.
Te reto a que analices los argumentos de la línea de comandos en esta estructura como quieras. Para utilizar el patrón derivar, anota lo anterior en Args
.Si prefieres seguir el patrón constructor, considera la posibilidad de escribir una función get_args
con el siguiente esquema:
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
:..
.
}
}
Actualiza main
para que analice e imprima los argumentos:
fn
main
()
{
let
args
=
Args
::parse
();
println!
(
"{:#?}"
,
args
);
}
Comprueba si puedes conseguir que tu programa imprima un uso como el siguiente.Ten en cuenta que utilizo los nombres cortos y largos de la versión GNU:
$ 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
Ejecuta el programa sin entradas y comprueba que los valores por defecto están correctamente configurados:
$ cargo run Args { files: [ "-", ], lines: 10, bytes: None, }
files
debe tener por defecto un guión (-
) como nombre de archivo.El número de
lines
debe ser por defecto10
.bytes
debe serNone
.
Ahora ejecuta el programa con argumentos y asegúrate de que se analizan correctamente:
$ cargo run -- -n 3 tests/inputs/one.txt Args { files: [ "tests/inputs/one.txt", ], lines: 3, bytes: None, }
El argumento posicional tests/inputs/one.txt se interpreta como uno de los
files
.La opción
-n
paralines
lo establece en3
.La opción
-b
parabytes
es por defectoNone
.
Si proporciono más de un argumento posicional, todos irán a files
, y el argumento -c
irá a bytes
. En el siguiente comando, vuelvo a confiar en el intérprete de comandos bash
para expandir el archivo glob *.txt
en todos los archivos que terminen en .txt. Los usuarios de PowerShell deben consultar el uso equivalente de Get-ChildItem
que se muestra en la sección "Iterar a través de los argumentos de archivo":
$ 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, ), }
Hay cuatro archivos que terminan en .txt.
lines
sigue fijado en el valor por defecto de10
.El
-c 4
hace que elbytes
sea ahoraSome(4)
.
Cualquier valor de -n
o -c
que no se pueda convertir en un número entero positivo debe hacer que el programa se detenga con un error.Utiliza clap::value_parser
para asegurarte de que los argumentos enteros son válidos y convertirlos en números:
$ 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
El programa debe desautorizar el uso tanto de -n
como de -c
:
$ 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>...
Nota
Sólo analizar y validar los argumentos es un reto, pero sé que puedes hacerlo. Deja de leer aquí y haz que tu programa pase todas las pruebas incluidas con cargo test dies
:
running 3 tests test dies_bad_lines ... ok test dies_bad_bytes ... ok test dies_bytes_and_lines ... ok
Definición de los argumentos
Bienvenido de nuevo. Primero mostraré el patrón constructor con una función get_args
como en el capítulo anterior. Observa que los dos argumentos opcionales, lines
y bytes
, aceptan valores numéricos. Esto es diferente de los argumentos opcionales implementados en el capítulo 3 que se utilizan como indicadores booleanos. Observa que el código siguiente requiere use clap::{Arg, Command}
:
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
(
)
,
}
}
La opción
lines
toma un valor y por defecto es10
.La opción
bytes
toma un valor, y entra en conflicto con el parámetrolines
, de modo que se excluyen mutuamente.El parámetro
files
es posicional, toma cero o más valores, y por defecto es un guión (-
).
Alternativamente, el patrón de derivación clap
requiere que anote la estructura Args
:
#[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
>
,
}
Consejo
En el patrón de derivación, el valor por defecto Arg::long
será el nombre del campo struct, por ejemplo, líneas y bytes. El valor por defecto de Arg::short
será la primera letra del campo struct, por lo que l o b. Especifico los nombres abreviados n y c, respectivamente, para que coincidan con la herramienta original.
Es bastante trabajo validar todas las entradas del usuario, pero ahora tengo cierta seguridad de que puedo proceder con datos buenos.
Procesar los archivos de entrada
Te recomiendo que hagas que tu main
llame a una función run
. Asegúrate de añadiruse anyhow::Result
lo siguiente:
fn
main
()
{
if
let
Err
(
e
)
=
run
(
Args
::parse
())
{
eprintln!
(
"{e}"
);
std
::process
::exit
(
1
);
}
}
fn
run
(
_args
:Args
)
->
Result
<
()
>
{
Ok
(())
}
Este programa de desafío debería manejar los archivos de entrada como en el Capítulo 3, por lo que te sugiero que añadas la misma función open
:
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
)
?
))),
}
}
Asegúrate de añadir todas estas dependencias adicionales:
use
std
::fs
::File
;
use
std
::io
::{
self
,
BufRead
,
BufReader
};
Amplía tu función run
para intentar abrir los archivos, imprimiendo los errores a medida que los encuentres:
fn
run
(
args
:
Args
)
->
Result
<
(
)
>
{
for
filename
in
args
.
files
{
match
open
(
&
filename
)
{
Err
(
err
)
=
>
eprintln!
(
"
{filename}: {err}
"
)
,
Ok
(
_
)
=
>
println!
(
"
Opened {filename}
"
)
,
}
}
Ok
(
(
)
)
}
Recorre cada uno de los nombres de archivo.
Intenta abrir el archivo dado.
Imprimir errores a
STDERR
.Imprime un mensaje indicando que el archivo se ha abierto correctamente.
Ejecuta tu programa con un archivo bueno y otro malo para asegurarte de que parece funcionar. En el comando siguiente, blargh representa un archivo inexistente:
$ cargo run -- blargh tests/inputs/one.txt blargh: No such file or directory (os error 2) Opened tests/inputs/one.txt
Sin anticiparte a mi solución, averigua cómo leer las líneas y luego los bytes de un archivo determinado. A continuación, añade las cabeceras que separan los argumentos de varios archivos. Observa atentamente la salida de error del programa original head
al tratar archivos no válidos, y fíjate en que los archivos legibles tienen primero una cabecera y luego la salida del archivo, pero los archivos no válidos sólo imprimen un error.Además, hay una línea en blanco adicional que separa la salida de los archivos válidos:
$ 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.
He diseñado específicamente algunas entradas desafiantes para que las tengas en cuenta. Para ver a qué te enfrentas, utiliza el comando file
para obtener información sobre el tipo de archivo:
$ 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
Se trata de un archivo vacío para garantizar que tu programa no se caiga.
Este archivo contiene Unicode, ya que he puesto una diéresis sobre la O de Őne para obligarte a considerar las diferencias entre bytes y caracteres.
Este archivo tiene finales de línea al estilo de Windows.
Este archivo tiene 12 líneas para garantizar que se muestra el valor predeterminado de 10 líneas.
Este archivo tiene finales de línea al estilo Unix.
Consejo
En Windows, la nueva línea es la combinación del retorno de carro y el salto de línea, que a menudo se muestra como CRLF o \r\n
. En las plataformas Unix, sólo se utiliza la nueva línea, por lo que LF o \n
. Estos finales de línea deben conservarse en la salida de tu programa, por lo que tendrás que encontrar una forma de leer las líneas de un archivo sin eliminar los finales de línea.
Bytes de lectura frente a personajes
Antes de continuar, debes comprender la diferencia entre leer bytes y caracteres de un archivo. A principios de los años 60, la tabla de 128 caracteres del Código Estándar Americano para el Intercambio de Información (ASCII, pronunciado as-key) representaba todos los elementos de texto posibles en informática.Sólo se necesitan siete bits (27 = 128) para representar tantos caracteres. Normalmente, un byte consta de ocho bits, por lo que las nociones de byte y carácter eran intercambiables.
Desde la creación de Unicode (Conjunto Universal de Caracteres Codificados) para representar todos los sistemas de escritura del mundo (e incluso los emojis), algunos caracteres pueden requerir hasta cuatro bytes.El estándar Unicode define varias formas de codificar caracteres, entre ellas UTF-8 (Formato de Transformación Unicode que utiliza ocho bits).
Como se ha indicado, el archivo tests/inputs/one.txt comienza con el carácter Ő, que tiene dos bytes en UTF-8. Si quieres que head
te muestre este único carácter, debes solicitar dos bytes:
$ head -c 2 tests/inputs/one.txt Ö
Si pides a head
que seleccione sólo el primer byte de este archivo, obtendrás el valor del byte 195
, que no es una cadena UTF-8 válida. La salida es un carácter especial que indica un problema al convertir un carácter a Unicode:
$ head -c 1 tests/inputs/one.txt �
Se espera que el programa de desafío recree este comportamiento. No es un programa fácil de escribir, pero deberías ser capaz de utilizar std::io
, std::fs::File
y std::io::BufReader
para averiguar cómo leer bytes y líneas de cada uno de los archivos. Ten en cuenta que, en Rust, a String
debe ser una cadena válida codificada en UTF-8, por lo que el método String::from_utf8_lossy
puede resultar útil. He incluido un conjunto completo de pruebas en tests/cli.rs que deberías haber copiado en tu árbol de código fuente.
Nota
Deja de leer aquí y termina el programa. Utiliza cargo test
con frecuencia para comprobar tu progreso. Esfuérzate al máximo para superar todas las pruebas antes de consultar mi solución.
Solución
Este reto resultó ser más interesante de lo que esperaba. Pensé que sería poco más que una variación de cat
, pero resultó ser bastante más difícil. Te explicaré cómo llegué a mi solución.
Leer un archivo línea por línea
Después de abrir los archivos válidos, empecé leyendo líneas del mango del archivo. Decidí modificar un poco el código del Capítulo 3:
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
(
(
)
)
}
Utiliza
Iterator::take
para seleccionar el número deseado de líneas del fichero.Imprime la línea en la consola.
Consejo
El método Iterator::take
espera que su argumento sea del tipo usize
, pero yo tengo un u64
. Cast o convierto el valor utilizando la palabra claveas
.
Creo que es una solución divertida porque utiliza el método Iterator::take
para seleccionar el número de líneas deseado. Puedo ejecutar el programa para seleccionar una línea de un archivo, y parece que funciona bien:
$ cargo run -- -n 1 tests/inputs/twelve.txt one
Si ejecuto cargo test
el programa supera casi la mitad de las pruebas, lo que parece bastante bueno para haber implementado sólo una pequeña parte de las especificaciones; sin embargo, está fallando todas las pruebas que utilizan el archivo de entrada codificado en Windows. Para solucionar este problema, tengo que hacer una confesión.
Conservar los finales de línea al leer un archivo
Siento decírtelo, querido lector, pero el programa catr
del Capítulo 3 no reproduce completamente el programa original cat
porque utiliza la función BufRead::lines
para leer los archivos de entrada.La documentación de esa función dice: "Cada cadena devuelta no tendrá un byte de nueva línea (el byte 0xA
) ni CRLF (bytes0xD
, 0xA
) al final". Espero que me perdones porque quería mostrarte lo fácil que puede ser leer las líneas de un archivo, pero debes saber que el programa catr
sustituye los finales de línea CRLF de Windows por nuevas líneas al estilo Unix.
Para solucionarlo, debo utilizar en su lugar BufRead::read_line
que, según la documentación, "leerá bytes del flujo subyacente hasta que encuentre el delimitador de nueva línea (el byte 0xA
) o EOF. Una vez encontrado, todos los bytes hasta el delimitador (si se encuentra) se añadirán a buf
."1
A continuación encontrarás una versión que conservará los finales de línea originales. Con estos cambios, el programa pasará más pruebas de las que falla:
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
(
(
)
)
}
Acepta el filehandle como valor mutable.
Utiliza
String::new
para crear una nueva memoria intermedia mutable vacía que contenga cada línea.Utiliza
for
para iterar a través de unstd::ops::Range
para contar desde cero hasta el número de líneas solicitado. El nombre de la variable_
indica que no tengo intención de utilizarla.Utiliza
BufRead::read_line
para leer la siguiente línea en el búfer de cadena.El filehandle devolverá cero bytes cuando llegue al final del archivo, así que
break
fuera del bucle.Utiliza
String::clear
para vaciar el búfer de líneas.
Si ejecuto cargo test
en este punto, el programa pasará casi todas las pruebas de lectura de líneas y fallará todas las de lectura de bytes y manejo de varios archivos.
Leer bytes de un archivo
A continuación, me ocuparé de leer bytes de un archivo. Después de intentar abrir el archivo, compruebo si args.bytes
es Some
número de bytes; de lo contrario, utilizaré el código anterior que lee líneas. Para el código siguiente, asegúrate de añadir use std::io::Read
a tus importaciones:
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
}
}
}
;
}
Utiliza la concordancia de patrones para comprobar si
args.bytes
esSome
número de bytes a leer.Crea un búfer mutable de longitud fija
num_bytes
lleno de ceros para contener los bytes leídos del archivo.Lee bytes del filehandle en el buffer. El valor
bytes_read
contendrá el número de bytes leídos, que puede ser inferior al númerosolicitado.Convierte los bytes seleccionados en una cadena, que puede no ser UTF-8 válido. Ten en cuenta la operación de rango para seleccionar sólo los bytes realmente leídos.
Como viste en el caso de seleccionar sólo una parte de un carácter multibyte, la conversión de bytes a caracteres podría fallar porque las cadenas en Rust deben ser UTF-8 válidas. La funciónString::from_utf8
devolverá un Ok
sólo si la cadena es válida, pero String::from_utf8_lossy
convertirá las secuencias UTF-8 no válidas al carácter desconocido o de sustitución:
$ cargo run -- -c 1 tests/inputs/one.txt �
Déjame mostrarte otra forma, mucho peor, de leer los bytes de un archivo. Puedes leer todo el archivo en una cadena, convertirla en un vector de bytes y, a continuación, seleccionar el primer num_bytes
:
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
)
;
Crea un nuevo búfer de cadena para guardar el contenido del archivo.
Lee todo el contenido del archivo en el búfer de cadena.
Utiliza
str::as_bytes
para convertir el contenido en bytes (u8
oenteros de 8 bits sin signo).Utiliza
String::from_utf8_lossy
para convertir un trozo debytes
en una cadena.
Como ya he señalado antes, este método puede bloquear tu programa u ordenador si el tamaño del archivo supera la cantidad de memoria de tu máquina. Otro problema grave del código anterior es que asume que la operación de corte bytes[..num_bytes]
tendrá éxito. Si utilizas este código con un archivo vacío, por ejemplo, estarás pidiendo bytes que no existen. Esto hará que tu programa entre en pánico y salga inmediatamente con un mensaje de error:
$ 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
A continuación se describe una forma segura -y quizá la más corta- de leer el número deseado de bytes de un archivo. Asegúrate de añadir el rasgo use std::io::Read
a tus importaciones:
let
bytes
:Result
<
Vec
<
_
>
,
_
>
=
file
.
bytes
().
take
(
num_bytes
as
usize
).
collect
();
print!
(
"{}"
,
String
::from_utf8_lossy
(
&
bytes
?
));
En el código anterior, la anotación de tipo Result<Vec<_>, _>
es necesaria, ya que el compilador infiere el tipo de bytes
como una rebanada, que tiene un tamaño desconocido. Debo indicar que quiero un Vec
, que es un puntero inteligente a memoria asignada al montón.Los guiones bajos (_
) indican una anotación de tipo parcial, lo que hace que el compilador infiera los tipos.Sin ninguna anotación de tipo para bytes
, el compilador se queja así:
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
Nota
Ya has visto que el guión bajo (_
) cumple varias funciones. Como prefijo o nombre de una variable, indica al compilador que no quieres utilizar el valor. En un brazo de match
, es el comodín para manejar cualquier caso. Cuando se utiliza en una anotación de tipo, indica al compilador que infiera el tipo.
También puedes indicar la información del tipo en el lado derecho de la expresión utilizando el operador turbofish (::<>
). A menudo es una cuestión de estilo si indicas el tipo en el lado izquierdo o en el derecho, pero más adelante verás ejemplos en los que el turbofish es necesario para algunas expresiones.Este es el aspecto que tendría el ejemplo anterior con el tipo indicado con el turbofish en su lugar:
let
bytes
=
file
.
bytes
(
)
.
take
(
num_bytes
as
usize
)
.
collect
:
:
<
Result
<
Vec
<
_
>
,
_
>
>
(
)
;
El carácter desconocido producido por String::from_utf8_lossy
(b'\xef\xbf\xbd'
) no es exactamente la misma salida producida por el BSD head
(b'\xc3'
), lo que hace que esto sea algo difícil de probar.Si echas un vistazo a la función de ayuda run
en tests/cli.rs, verás que he leído el valor esperado (la salida de head
) y he utilizado la misma función para convertir lo que podría ser UTF-8 no válido, de modo que pueda comparar las dos salidas. La función run_stdin
funciona de forma similar:
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
(
(
)
)
}
Imprimir los separadores de archivos
La última pieza que hay que manejar son los separadores entre varios archivos. Como ya se ha dicho, los archivos válidos tienen una cabecera que pone el nombre del archivo dentro de los marcadores ==>
y <==
.Los archivos posteriores al primero tienen una nueva línea adicional al principio para separar visualmente la salida.Esto significa que necesitaré saber el número de archivo que estoy manejando, que puedo obtener utilizando el métodoIterator::enumerate
. A continuación se muestra la versión final de mi función run
que superará todas las pruebas:
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
(
(
)
)
}
Utiliza el método
Vec::len
para obtener el número de archivos.Utiliza el método
Iterator::enumerate
para rastrear el número y losnombres de los archivos.Imprime sólo las cabeceras cuando haya varios archivos.
Imprime una nueva línea cuando
file_num
sea mayor que0
, lo que indica que se trata del primer archivo.
Ir más lejos
No hay razón para parar esta fiesta ahora. Considera implementar cómo el GNU head
maneja los valores numéricos con sufijos y los valores negativos.Por ejemplo, -c=1K
significa imprimir los primeros 1.024 bytes del archivo, y -n=-3
significa imprimir todas las líneas del archivo excepto las tres últimas. Tendrás que cambiar lines
y bytes
a valores enteros con signo para almacenar números positivos y negativos. Asegúrate de ejecutar el GNU head
con estos argumentos, capturar la salida en archivos de prueba y escribir pruebas para cubrir las nuevas funciones que añadas.
También podrías añadir una opción para seleccionar caracteres además de bytes. Puedes utilizar la funciónString::chars
para dividir una cadena en caracteres.Por último, copia el archivo de entrada de las pruebas con los finales de línea de Windows(pruebas/entradas/tres.txt) en las pruebas del capítulo 3. Edita el archivo mk-outs.sh de ese programa para incorporar este archivo y, a continuación, amplía las pruebas y el programa para asegurarte de que se conservan los finales de línea.
Resumen
Este capítulo se ha adentrado en algunos temas bastante peliagudos, como la conversión de tipos como las entradas de cadena a u64
y su posterior conversión a usize
. Si sigues sintiéndote confuso, debes saber que no siempre será así. Si sigues leyendo la documentación y escribiendo más código, al final todo tendrá sentido.
Éstas son algunas de las cosas que has conseguido en este capítulo:
-
Aprendiste a crear parámetros opcionales que pueden tomar valores. Antes, las opciones eran banderas.
-
Viste que todos los argumentos de la línea de comandos son cadenas y utilizaste
clap
para intentar convertir una cadena como"3"
en el número3
. -
Has aprendido a convertir tipos utilizando la palabra clave
as
. -
Has descubierto que utilizar
_
como nombre o prefijo de una variable es una forma de indicar al compilador que no tienes intención de utilizar el valor. Cuando se utiliza en una anotación de tipo, indica al compilador que infiera el tipo. -
Has aprendido a utilizar
BufRead::read_line
para conservar los finales de línea mientras lees un filehandle. -
Has descubierto que el método
take
funciona tanto en los iteradores como en los filehandles para limitar el número de elementos que seleccionas. -
Aprendiste a indicar la información de tipo en la parte izquierda de una asignación o en la parte derecha utilizando el operador turbofish.
En el próximo capítulo, aprenderás más sobre los iteradores de Rust y cómo dividir la entrada en líneas, bytes y caracteres.
1 EOF es un acrónimo de fin de archivo.
Get Línea de comandos Óxido 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.