24

I am attempting to write the cat command to learn Rust, but I can't seem to convert command line arguments into reader structs.

use std::{env, io};
use std::fs::File;

fn main() {
    for arg in env::args().skip(1) {
        let reader = match arg.as_str() {
            "-" => io::stdin(),
            path => File::open(&path).unwrap(),
        };
    }
}

Error:

error[E0308]: match arms have incompatible types
 --> src/main.rs:6:22
  |
6 |         let reader = match arg.as_str() {
  |                      ^ expected struct `std::io::Stdin`, found struct `std::fs::File`
  |
  = note: expected type `std::io::Stdin`
  = note:    found type `std::fs::File`
note: match arm with an incompatible type
 --> src/main.rs:8:21
  |
8 |             path => File::open(&path).unwrap(),
  |                     ^^^^^^^^^^^^^^^^^^^^^^^^^^

It doesn't seem like it's possible to polymorphically match trait implementers (related). How can I use either a File or Stdin as a reader?

Community
  • 1
  • 1
AJcodez
  • 31,780
  • 20
  • 84
  • 118

5 Answers5

27

The problem is that stdin() returns an object of type Stdio and File::open(...).unwrap() returns an object of type File. In Rust, all arms of a match have to return values of the same type.

In this case you probably wanted to return a common Read object. Unfortunately Read is a trait so you cannot pass it by value. The easiest alternative is to resort to heap allocation:

use std::{env, io};
use std::io::prelude::*;
use std::fs::File;

fn main() {
    for arg in env::args().skip(1) {
        let reader = match arg.as_str() {
            "-" => Box::new(io::stdin()) as Box<Read>,
            path => Box::new(File::open(&path).unwrap()) as Box<Read>,
        };
    }
}
Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
PEPP
  • 917
  • 9
  • 7
16

Here's a variation on Lukas's answer that avoids boxing:

use std::io::{self, Read};
use std::fs::File;
use std::path::Path;

fn main() {
    if let Some(arg) = std::env::args().nth(1).as_ref() {
        let stdin;
        let file;
        let reader = match arg.as_ref() {
            "-"  => {
                stdin = io::stdin();
                &stdin as &Read
            }
            path => {
                file = File::open(&Path::new(path)).unwrap();
                &file as &Read
            }
        };
    }
}

The trick here is to use let bindings that are only initialized on some code paths, while still having a long enough lifetime to be able to use them as the target of a borrowed pointer.

Community
  • 1
  • 1
Francis Gagné
  • 60,274
  • 7
  • 180
  • 155
15

The accepted answer does not work with Rust v1.0 anymore. The main statement is still true though: Match arms have to return the same types. Allocating the objects on the heap solves the problem.

use std::io::{self, Read};
use std::fs::File;
use std::path::Path;

fn main() {
    if let Some(arg) = std::env::args().nth(1).as_ref() {
        let reader = match arg.as_ref() {
            "-"  => Box::new(io::stdin()) as Box<Read>,
            path => Box::new(File::open(&Path::new(path)).unwrap()) as Box<Read>,
        };
    }
}
Lukas Kalbertodt
  • 79,749
  • 26
  • 255
  • 305
7

The coalesce crate provides a way to do this without boxing and in a way that is less verbose than my other answer. The idea is to use a simple enum that can hold the concrete type corresponding to each arm and a macro (coalesce!) that expands to a match where the body expression is the same for each arm.

#[macro_use]
extern crate coalesce;

use std::io::{self, Read};
use std::fs::File;
use std::path::Path;
use coalesce::Coalesce2;

fn main() {
    if let Some(arg) = std::env::args().nth(1).as_ref() {
        let reader = match arg.as_ref() {
            "-"  => Coalesce2::A(io::stdin()),
            path => Coalesce2::B(File::open(&Path::new(path)).unwrap()),
        };

        let reader = coalesce!(2 => |ref reader| reader as &Read);

        // the previous line is equivalent to:
        let reader = match reader {
            Coalesce2::A(ref reader) => reader as &Read,
            Coalesce2::B(ref reader) => reader as &Read,
        };
    }
}
Community
  • 1
  • 1
Francis Gagné
  • 60,274
  • 7
  • 180
  • 155
0

A variation on @FrancisGagné's answer: While the coalesce crate is more general, in this case, either will also do, and is a little less verbose:

use std::{env, fs::File, io};
use either::Either;

fn main() {
    for arg in env::args().skip(1) {
        let mut reader = match arg.as_str() {
            "-" => Either::Right(io::stdin()),
            path => Either::Left(File::open(&path).unwrap()),
        };
        // just for show: Either implements Read!
        let reader: &mut dyn io::Read = &mut reader;
    }
}
Caesar
  • 6,733
  • 4
  • 38
  • 44