With-output-stream ...?

In certain dynamic languages, we can do something like:

(with-output-stream (file "logs/blah.txt")
  (body-of-code))

and then all stdout-output of (body-of-code) goes to logs/blah.txt

Is it possible to do something like this in Rust ?

A quick Google for "rust redirect stdout" brings up the first hit, gag::Redirect. This seems to do what you want.

However, I would say, doing this is very strange. Since Rust has a proper Write trait (and related traits, like BufWrite), code should generally not just write to stdout blindly. Instead, functions that produce output should accept a generic writer, so one can use them to write anywhere, including stdout, stderr, a file, or even an in-memory buffer.

Is there a way to convert std::io::Stdout to impl std::fmt::Formatter ?

It is not clear to me if the "printing" procedures should take Write or Formatter (like impl Debug).

This takes advantage of lisp's so-called special variables, which are a form of global. You can get a similar behavior by storing a RefCell inside a thread local, and then writing an RAII guard that will replace the stored value and restore the original value on Drop.

Of course, none of the existing Rust I/O mechanisms know about this, so they will be completely unaware of the swap-- You'd need to write your own wrappers that are aware of the context variable.

1 Like

They should probably take std::io::Write, as this is one of the traits you can use write! with to output formatted text[1].


  1. Or you can call write_fmt directly, passing in a format_args! ↩︎

3 Likes

I hacked together a quick example of how this might work:

(Proof-of-concept only; likely has soundness bugs)

// See playground for implementation
def_special! { StdOut: dyn std::io::Write }

pub fn dbg_to_special<T:std::fmt::Debug+?Sized>(x:&T) {
    let mut out = StdOut::borrow();
    write!(out, "{:?}", x);
}

fn main() {
    StdOut::lend(&mut std::io::stdout().lock(), || dbg_to_special("to_stdout"));
    StdOut::lend(&mut std::io::stderr().lock(), || dbg_to_special("to_stderr"));
}

I agree with others that std::io::Write is a better interface to use. Nonetheless I tried how to obtain the Formatter for stdout; it’s only possible in a callback, but then it can be done like this

/// locks stdout and provides a `Formatter` to write to
fn with_locked_stdout_formatter(
    callback: impl FnOnce(&mut std::fmt::Formatter<'_>) -> std::fmt::Result,
) -> std::io::Result<()> {
    use std::io::Write;
    struct Wrapper<F>(std::cell::Cell<Option<F>>);
    impl<F: FnOnce(&mut std::fmt::Formatter<'_>) -> std::fmt::Result> std::fmt::Display for Wrapper<F> {
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            self.0.take().unwrap()(f)
        }
    }
    let wrapped_callback = Wrapper(std::cell::Cell::new(Some(callback)));
    std::io::stdout().write_fmt(format_args!("{wrapped_callback}"))
}

// demonstration
fn main() {
    with_locked_stdout_formatter(|f| f.write_str("Hello World!")).unwrap();
}

I’m not 100% sure whether there isn’t any less convoluted way.

Note that this does lock stdout for the entire duration of the callback. (This should usually be the desired behavior anyway, as each individually call to the Formatter isn’t supposed to be too expensive.)

There's also the old fashioned approach on non windows systems of just closing the stdout file descriptor and replacing it with another of your choice. I'm sure windows has some analogous functionality to open(3) and the close(3).

cargo test redirects stdout so you could look there for inspiration as to how to do it portably.

1 Like

cargo test redirects stdout so you could look there for inspiration as to how to do it portably.

That's actually done by a hook within std that redirects the output of the [e]print[ln]! family only. (It can't work by redirecting stdout because it's capturing output separately from tests on multiple threads, and there's only one stdout per process, not per thread.)

(Unfortunately, that hook is not exposed stably, so it's impossible to replicate this behavior of the built-in test harness in your own code.)

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.