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.txtComo 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 headry 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 testTodas 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>, 1
    lines: u64, 2
    bytes: Option<u64>, 3
}
1

files será un vector de cadenas.

2

El número de lines a imprimir será del tipo u64.

3

bytes será opcional u64.

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: [
        "-", 1
    ],
    lines: 10, 2
    bytes: None, 3
}
1

files debe tener por defecto un guión (-) como nombre de archivo.

2

El número de lines debe ser por defecto 10.

3

bytes debe ser None.

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", 1
    ],
    lines: 3, 2
    bytes: None, 3
}
1

El argumento posicional tests/inputs/one.txt se interpreta como uno de los files.

2

La opción -n para lines lo establece en 3.

3

La opción -b para bytes es por defecto None.

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", 1
        "tests/inputs/one.txt",
        "tests/inputs/three.txt",
        "tests/inputs/twelve.txt",
        "tests/inputs/two.txt",
    ],
    lines: 10, 2
    bytes: Some( 3
        4,
    ),
}
1

Hay cuatro archivos que terminan en .txt.

2

lines sigue fijado en el valor por defecto de 10.

3

El -c 4 hace que el bytes sea ahora Some(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") 1
                .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") 2
                .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") 3
                .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(),
    }
}
1

La opción lines toma un valor y por defecto es 10.

2

La opción bytes toma un valor, y entra en conflicto con el parámetro lines, de modo que se excluyen mutuamente.

3

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 { 1
        match open(&filename) { 2
            Err(err) => eprintln!("{filename}: {err}"), 3
            Ok(_) => println!("Opened {filename}"), 4
        }
    }
    Ok(())
}
1

Recorre cada uno de los nombres de archivo.

2

Intenta abrir el archivo dado.

3

Imprimir errores a STDERR.

4

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 1
tests/inputs/one.txt:    UTF-8 Unicode text 2
tests/inputs/three.txt:  ASCII text, with CRLF, LF line terminators 3
tests/inputs/twelve.txt: ASCII text 4
tests/inputs/two.txt:    ASCII text 5
1

Se trata de un archivo vacío para garantizar que tu programa no se caiga.

2

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.

3

Este archivo tiene finales de línea al estilo de Windows.

4

Este archivo tiene 12 líneas para garantizar que se muestra el valor predeterminado de 10 líneas.

5

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::Filey 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) { 1
                    println!("{}", line?); 2
                }
            }
        }
    }
    Ok(())
}
1

Utiliza Iterator::take para seleccionar el número deseado de líneas del fichero.

2

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 testel 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_lineque, 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) => { 1
                let mut line = String::new(); 2
                for _ in 0..args.lines { 3
                    let bytes = file.read_line(&mut line)?; 4
                    if bytes == 0 { 5
                        break;
                    }
                    print!("{line}"); 6
                    line.clear(); 7
                }
            }
        };
    }
    Ok(())
}
1

Acepta el filehandle como valor mutable.

2

Utiliza String::new para crear una nueva memoria intermedia mutable vacía que contenga cada línea.

3

Utiliza for para iterar a través de un std::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.

4

Utiliza BufRead::read_line para leer la siguiente línea en el búfer de cadena.

5

El filehandle devolverá cero bytes cuando llegue al final del archivo, así que break fuera del bucle.

6

Imprime la línea, incluido el final de línea original.

7

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 { 1
                let mut buffer = vec![0; num_bytes as usize]; 2
                let bytes_read = file.read(&mut buffer)?; 3
                print!(
                    "{}",
                    String::from_utf8_lossy(&buffer[..bytes_read]) 4
                );
            } else {
                ... // Same as before
            }
        }
    };
}
1

Utiliza la concordancia de patrones para comprobar si args.bytes es Some número de bytes a leer.

2

Crea un búfer mutable de longitud fija num_bytes lleno de ceros para contener los bytes leídos del archivo.

3

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.

4

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(); 1
file.read_to_string(&mut contents)?; // Danger here 2
let bytes = contents.as_bytes(); 3
print!(
    "{}",
    String::from_utf8_lossy(&bytes[..num_bytes as usize]) // More danger 4
);
1

Crea un nuevo búfer de cadena para guardar el contenido del archivo.

2

Lee todo el contenido del archivo en el búfer de cadena.

3

Utiliza str::as_bytes para convertir el contenido en bytes (u8 oenteros de 8 bits sin signo).

4

Utiliza String::from_utf8_lossy para convertir un trozo de bytes 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); 1

    let output = Command::cargo_bin(PRG)?.args(args).output().expect("fail");
    assert!(output.status.success());
    assert_eq!(String::from_utf8_lossy(&output.stdout), expected); 2

    Ok(())
}
1

Maneja cualquier UTF-8 no válido en expected_file.

2

Compara la salida y los valores esperados como cadenas con pérdida.

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(); 1

    for (file_num, filename) in args.files.iter().enumerate() { 2
        match open(filename) {
            Err(err) => eprintln!("{filename}: {err}"),
            Ok(mut file) => {
                if num_files > 1 { 3
                    println!(
                        "{}==> {filename} <==",
                        if file_num > 0 { "\n" } else { "" }, 4
                    );
                }

                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(())
}
1

Utiliza el métodoVec::len para obtener el número de archivos.

2

Utiliza el método Iterator::enumerate para rastrear el número y losnombres de los archivos.

3

Imprime sólo las cabeceras cuando haya varios archivos.

4

Imprime una nueva línea cuando file_num sea mayor que 0, 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úmero 3.

  • 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.