4

A lot of command-line tools let you use - for standard input or standard output. Is there an idiomatic way to support that in Rust?

It looks like the most common way to handle command-line arguments is clap. If I just want to handle paths and donʼt want to special-case -, I can use

use std::fs::File;
use std::io::Write;
use std::path::PathBuf;

use clap::Parser;

#[derive(clap::Parser, Debug)]
struct Args {
    #[clap(parse(from_os_str))]
    output: PathBuf,
}

fn main() -> std::io::Result<()> {
    let args = Args::parse();
    let mut file = File::create(args.output)?;
    file.write_all(b"Hello, world!")?;
    Ok(())
} 

However, to handle the case of - being stdout, itʼs more complicated. The type of file now needs to be Box<dyn Write> since stdout() is not a File, and itʼs somewhat complicated to set it correctly. Not difficult, but the sort of boilerplate that needs to be copied several times and is easy to mess up.

Is there an idiomatic way to handle this?

Herohtar
  • 5,347
  • 4
  • 31
  • 41
Daniel H
  • 7,223
  • 2
  • 26
  • 41
  • What boilerplate do you have to copy? – Herohtar Jan 05 '22 at 23:09
  • Not as much as I thought when making the question because I was doing something else dumb at the time, but itʼs still the sort of thing that should be done when parsing the arguments instead of when using them. – Daniel H Jan 05 '22 at 23:49

1 Answers1

2

This is something you just have to write into your code. In many cases, it wouldn't be clear whether - should represent standard input or standard output, and it requires the context of the program to interpret it correctly.

You do in fact need to use a Box<dyn Write> (or &dyn Write or similar) in this case. If you're using Unix, you can create a File object from standard input or output by creating one using FromRawFd (and something similar, but not identical, on Windows), but you should avoid doing that, because when you drop a File, the file descriptor is closed, which is not the right behavior here.

In fact, accidentally closing one of the standard file descriptors can actually lead to data corruption, since if another file is opened, it will be opened with the lowest possible value (e.g., 1, for standard output), and then things like println! might accidentally print to the newly opened file.

So using a dyn Write is the right option here. In most cases, the performance impact will not be noticeable.

bk2204
  • 64,793
  • 6
  • 84
  • 100
  • On Unix one can also use `File::open("/dev/stdin")` and `File::open("/dev/stdout")` which avoids boxing without having the problems associated with `FromRawFd`. – user4815162342 Jan 06 '22 at 00:08
  • Then you're technically opening a new file descriptor, not using stdin or stdout. It also doesn't always work; for example, on Linux, if `/proc` isn't mounted or is restricted (e.g., by AppArmor), then that won't work. There are times when it's useful, but in this case, it isn't necessary. – bk2204 Jan 06 '22 at 02:04
  • I don't agree at all with your advice, generic first I see no reason to not use Write trait as generic, then the next if you don't want/can use generic is to use an enum, then if even enum doesn't fit you, use dynamic dispatch. You jump a lot of step in your conclusion. – Stargateur Jan 06 '22 at 02:14
  • I think we're actually in agreement here that you should use the Write trait as generic. Trait objects use dynamic dispatch. But I've rephrased to make it clearer that using the trait is the right thing to do here and avoid ambiguity about the best approach. – bk2204 Jan 06 '22 at 02:42
  • The file `-` can be unambiguously specified as the path `./-` – Colonel Thirty Two Jan 07 '22 at 20:31