fix(cli): use logging infra instead of print(ln), do not mess up progress bars

Fixes #84

Change-Id: I3ae21bb5353d5d9ad592831526a48ae391f9843d
Reviewed-on: https://cl.snix.dev/c/snix/+/30234
Tested-by: besadii
Autosubmit: Márton Boros <martonboros@gmail.com>
Reviewed-by: Florian Klink <flokli@flokli.de>
This commit is contained in:
Márton Boros 2025-03-21 15:19:28 +02:00 committed by clbot
parent fd9c9572e9
commit 357004b20d
6 changed files with 87 additions and 32 deletions

View file

@ -82,7 +82,8 @@ pub struct EvalResult {
/// Interprets the given code snippet, printing out warnings and errors and returning the result /// Interprets the given code snippet, printing out warnings and errors and returning the result
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn evaluate( pub fn evaluate<E: std::io::Write + Clone + Send>(
stderr: &mut E,
snix_store_io: Rc<SnixStoreIO>, snix_store_io: Rc<SnixStoreIO>,
code: &str, code: &str,
path: Option<PathBuf>, path: Option<PathBuf>,
@ -126,13 +127,12 @@ pub fn evaluate(
let source_map = eval_builder.source_map().clone(); let source_map = eval_builder.source_map().clone();
let (result, globals) = { let (result, globals) = {
let mut compiler_observer = let mut compiler_observer = DisassemblingObserver::new(source_map.clone(), stderr.clone());
DisassemblingObserver::new(source_map.clone(), std::io::stderr());
if args.dump_bytecode { if args.dump_bytecode {
eval_builder.set_compiler_observer(Some(&mut compiler_observer)); eval_builder.set_compiler_observer(Some(&mut compiler_observer));
} }
let mut runtime_observer = TracingObserver::new(std::io::stderr()); let mut runtime_observer = TracingObserver::new(stderr.clone());
if args.trace_runtime { if args.trace_runtime {
if args.trace_runtime_timing { if args.trace_runtime_timing {
runtime_observer.enable_timing() runtime_observer.enable_timing()
@ -162,17 +162,17 @@ pub fn evaluate(
if args.display_ast { if args.display_ast {
if let Some(ref expr) = result.expr { if let Some(ref expr) = result.expr {
eprintln!("AST: {}", snix_eval::pretty_print_expr(expr)); writeln!(stderr, "AST: {}", snix_eval::pretty_print_expr(expr)).unwrap();
} }
} }
for error in &result.errors { for error in &result.errors {
error.fancy_format_stderr(); error.fancy_format_write(stderr);
} }
if !args.no_warnings { if !args.no_warnings {
for warning in &result.warnings { for warning in &result.warnings {
warning.fancy_format_stderr(&source_map); warning.fancy_format_write(stderr, &source_map);
} }
} }
@ -212,8 +212,8 @@ impl InterpretResult {
} }
} }
pub fn finalize(self) -> bool { pub fn finalize<E: std::io::Write>(self, stderr: &mut E) -> bool {
print!("{}", self.output); write!(stderr, "{}", self.output).unwrap();
self.success self.success
} }
@ -231,7 +231,8 @@ impl InterpretResult {
/// evaluation succeeded. /// evaluation succeeded.
#[instrument(skip_all, fields(indicatif.pb_show=tracing::field::Empty))] #[instrument(skip_all, fields(indicatif.pb_show=tracing::field::Empty))]
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn interpret( pub fn interpret<E: std::io::Write + Clone + Send>(
stderr: &mut E,
snix_store_io: Rc<SnixStoreIO>, snix_store_io: Rc<SnixStoreIO>,
code: &str, code: &str,
path: Option<PathBuf>, path: Option<PathBuf>,
@ -244,6 +245,7 @@ pub fn interpret(
) -> Result<InterpretResult, IncompleteInput> { ) -> Result<InterpretResult, IncompleteInput> {
let mut output = String::new(); let mut output = String::new();
let result = evaluate( let result = evaluate(
stderr,
snix_store_io, snix_store_io,
code, code,
path, path,

View file

@ -6,6 +6,7 @@ use snix_cli::{init_io_handle, interpret, AllowIncomplete};
use snix_eval::observer::DisassemblingObserver; use snix_eval::observer::DisassemblingObserver;
use snix_eval::EvalMode; use snix_eval::EvalMode;
use snix_glue::snix_store_io::SnixStoreIO; use snix_glue::snix_store_io::SnixStoreIO;
use std::io::Write;
use std::rc::Rc; use std::rc::Rc;
use std::{fs, path::PathBuf}; use std::{fs, path::PathBuf};
@ -14,7 +15,12 @@ static GLOBAL: MiMalloc = MiMalloc;
/// Interpret the given code snippet, but only run the Svix compiler /// Interpret the given code snippet, but only run the Svix compiler
/// on it and return errors and warnings. /// on it and return errors and warnings.
fn lint(code: &str, path: Option<PathBuf>, args: &Args) -> bool { fn lint<E: Write + Clone + Send>(
stderr: &mut E,
code: &str,
path: Option<PathBuf>,
args: &Args,
) -> bool {
let mut eval_builder = snix_eval::Evaluation::builder_impure(); let mut eval_builder = snix_eval::Evaluation::builder_impure();
if args.strict { if args.strict {
@ -23,14 +29,18 @@ fn lint(code: &str, path: Option<PathBuf>, args: &Args) -> bool {
let source_map = eval_builder.source_map().clone(); let source_map = eval_builder.source_map().clone();
let mut compiler_observer = DisassemblingObserver::new(source_map.clone(), std::io::stderr()); let mut compiler_observer = DisassemblingObserver::new(source_map.clone(), stderr.clone());
if args.dump_bytecode { if args.dump_bytecode {
eval_builder.set_compiler_observer(Some(&mut compiler_observer)); eval_builder.set_compiler_observer(Some(&mut compiler_observer));
} }
if args.trace_runtime { if args.trace_runtime {
eprintln!("warning: --trace-runtime has no effect with --compile-only!"); writeln!(
stderr,
"warning: --trace-runtime has no effect with --compile-only"
)
.unwrap();
} }
let eval = eval_builder.build(); let eval = eval_builder.build();
@ -38,16 +48,16 @@ fn lint(code: &str, path: Option<PathBuf>, args: &Args) -> bool {
if args.display_ast { if args.display_ast {
if let Some(ref expr) = result.expr { if let Some(ref expr) = result.expr {
eprintln!("AST: {}", snix_eval::pretty_print_expr(expr)); writeln!(stderr, "AST: {}", snix_eval::pretty_print_expr(expr)).unwrap();
} }
} }
for error in &result.errors { for error in &result.errors {
error.fancy_format_stderr(); error.fancy_format_write(stderr);
} }
for warning in &result.warnings { for warning in &result.warnings {
warning.fancy_format_stderr(&source_map); warning.fancy_format_write(stderr, &source_map);
} }
// inform the caller about any errors // inform the caller about any errors
@ -57,18 +67,22 @@ fn lint(code: &str, path: Option<PathBuf>, args: &Args) -> bool {
fn main() { fn main() {
let args = Args::parse(); let args = Args::parse();
snix_tracing::TracingBuilder::default() let tracing_handle = snix_tracing::TracingBuilder::default()
.enable_progressbar() .enable_progressbar()
.build() .build()
.expect("unable to set up tracing subscriber"); .expect("unable to set up tracing subscriber");
let mut stdout = tracing_handle.get_stdout_writer();
let mut stderr = tracing_handle.get_stderr_writer();
let tokio_runtime = tokio::runtime::Runtime::new().expect("failed to setup tokio runtime"); let tokio_runtime = tokio::runtime::Runtime::new().expect("failed to setup tokio runtime");
let io_handle = init_io_handle(&tokio_runtime, &args); let io_handle = init_io_handle(&tokio_runtime, &args);
if let Some(file) = &args.script { if let Some(file) = &args.script {
run_file(io_handle, file.clone(), &args) run_file(&mut stdout, &mut stderr, io_handle, file.clone(), &args)
} else if let Some(expr) = &args.expr { } else if let Some(expr) = &args.expr {
if !interpret( if !interpret(
&mut stderr,
io_handle, io_handle,
expr, expr,
None, None,
@ -80,26 +94,33 @@ fn main() {
None, None,
) )
.unwrap() .unwrap()
.finalize() .finalize(&mut stdout)
{ {
std::process::exit(1); std::process::exit(1);
} }
} else { } else {
let mut repl = Repl::new(io_handle, &args); let mut repl = Repl::new(io_handle, &args);
repl.run() repl.run(&mut stdout, &mut stderr)
} }
} }
fn run_file(io_handle: Rc<SnixStoreIO>, mut path: PathBuf, args: &Args) { fn run_file<O: Write, E: Write + Clone + Send>(
stdout: &mut O,
stderr: &mut E,
io_handle: Rc<SnixStoreIO>,
mut path: PathBuf,
args: &Args,
) {
if path.is_dir() { if path.is_dir() {
path.push("default.nix"); path.push("default.nix");
} }
let contents = fs::read_to_string(&path).expect("failed to read the input file"); let contents = fs::read_to_string(&path).expect("failed to read the input file");
let success = if args.compile_only { let success = if args.compile_only {
lint(&contents, Some(path), args) lint(stderr, &contents, Some(path), args)
} else { } else {
interpret( interpret(
stderr,
io_handle, io_handle,
&contents, &contents,
Some(path), Some(path),
@ -111,7 +132,7 @@ fn run_file(io_handle: Rc<SnixStoreIO>, mut path: PathBuf, args: &Args) {
None, None,
) )
.unwrap() .unwrap()
.finalize() .finalize(stdout)
}; };
if !success { if !success {

View file

@ -1,3 +1,4 @@
use std::io::Write;
use std::path::PathBuf; use std::path::PathBuf;
use std::rc::Rc; use std::rc::Rc;
@ -74,8 +75,8 @@ pub struct CommandResult {
} }
impl CommandResult { impl CommandResult {
pub fn finalize(self) -> bool { pub fn finalize<E: Write>(self, stdout: &mut E) -> bool {
print!("{}", self.output); write!(stdout, "{}", self.output).unwrap();
self.continue_ self.continue_
} }
@ -111,9 +112,17 @@ impl<'a> Repl<'a> {
} }
} }
pub fn run(&mut self) { pub fn run<O: Write + Clone + Send, E: Write + Clone + Send>(
&mut self,
stdout: &mut O,
stderr: &mut E,
) {
if self.args.compile_only { if self.args.compile_only {
eprintln!("warning: `--compile-only` has no effect on REPL usage!"); writeln!(
stderr,
"warning: `--compile-only` has no effect on REPL usage!"
)
.unwrap();
} }
let history_path = match state_dir() { let history_path = match state_dir() {
@ -139,14 +148,14 @@ impl<'a> Repl<'a> {
let readline = self.rl.readline(prompt); let readline = self.rl.readline(prompt);
match readline { match readline {
Ok(line) => { Ok(line) => {
if !self.send(line).finalize() { if !self.send(stderr, line).finalize(stdout) {
break; break;
} }
} }
Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => break, Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => break,
Err(err) => { Err(err) => {
eprintln!("error: {}", err); writeln!(stderr, "error: {}", err).unwrap();
break; break;
} }
} }
@ -159,7 +168,7 @@ impl<'a> Repl<'a> {
/// Send a line of user input to the REPL. Returns a result indicating the output to show to the /// Send a line of user input to the REPL. Returns a result indicating the output to show to the
/// user, and whether or not to continue /// user, and whether or not to continue
pub fn send(&mut self, line: String) -> CommandResult { pub fn send<E: Write + Clone + Send>(&mut self, stderr: &mut E, line: String) -> CommandResult {
if line.is_empty() { if line.is_empty() {
return CommandResult { return CommandResult {
output: String::new(), output: String::new(),
@ -187,6 +196,7 @@ impl<'a> Repl<'a> {
Ok(InterpretResult::empty_success(None)) Ok(InterpretResult::empty_success(None))
} }
ReplCommand::Expr(input) => interpret( ReplCommand::Expr(input) => interpret(
stderr,
Rc::clone(&self.io_handle), Rc::clone(&self.io_handle),
input, input,
None, None,
@ -199,6 +209,7 @@ impl<'a> Repl<'a> {
), ),
ReplCommand::Assign(Assignment { ident, value }) => { ReplCommand::Assign(Assignment { ident, value }) => {
match evaluate( match evaluate(
stderr,
Rc::clone(&self.io_handle), Rc::clone(&self.io_handle),
&value.to_string(), /* FIXME: don't re-parse */ &value.to_string(), /* FIXME: don't re-parse */
None, None,
@ -218,6 +229,7 @@ impl<'a> Repl<'a> {
} }
} }
ReplCommand::Explain(input) => interpret( ReplCommand::Explain(input) => interpret(
stderr,
Rc::clone(&self.io_handle), Rc::clone(&self.io_handle),
input, input,
None, None,
@ -229,6 +241,7 @@ impl<'a> Repl<'a> {
Some(self.source_map.clone()), Some(self.source_map.clone()),
), ),
ReplCommand::Print(input) => interpret( ReplCommand::Print(input) => interpret(
stderr,
Rc::clone(&self.io_handle), Rc::clone(&self.io_handle),
input, input,
None, None,

View file

@ -15,8 +15,9 @@ macro_rules! test_repl {
OsString::from("nixpkgs=/tmp"), OsString::from("nixpkgs=/tmp"),
]); ]);
let mut repl = snix_cli::Repl::new(init_io_handle(&tokio_runtime, &args), &args); let mut repl = snix_cli::Repl::new(init_io_handle(&tokio_runtime, &args), &args);
let mut buffer = std::io::Cursor::new(Vec::new());
$({ $({
let result = repl.send($send.into()); let result = repl.send(&mut buffer, $send.into());
$expect.assert_eq(result.output()) $expect.assert_eq(result.output())
; ;
})* })*

View file

@ -1,5 +1,6 @@
use std::error; use std::error;
use std::io; use std::io;
use std::io::Write;
use std::path::PathBuf; use std::path::PathBuf;
use std::rc::Rc; use std::rc::Rc;
use std::str::Utf8Error; use std::str::Utf8Error;
@ -150,7 +151,7 @@ to a missing value in the attribute set(s) included via `with`."#
BytecodeError(Box<Error>), BytecodeError(Box<Error>),
/// Given type can't be coerced to a string in the respective context /// Given type can't be coerced to a string in the respective context
#[error("cannot ({}) coerce {from} to a string{}", #[error("cannot ({}) coerce {from} to a string{}",
(if .kind.strong { "strongly" } else { "weakly" }), (if .kind.strong { "strongly" } else { "weakly" }),
(if *.from == "set" { (if *.from == "set" {
", missing a `__toString` or `outPath` attribute" ", missing a `__toString` or `outPath` attribute"
@ -634,6 +635,12 @@ impl Error {
Emitter::stderr(ColorConfig::Auto, Some(&*self.source.codemap())).emit(&self.diagnostics()); Emitter::stderr(ColorConfig::Auto, Some(&*self.source.codemap())).emit(&self.diagnostics());
} }
/// Render a fancy, human-readable output of this error and print
/// it to a std::io::Write.
pub fn fancy_format_write<E: Write + Send>(&self, stderr: &mut E) {
Emitter::new(Box::new(stderr), Some(&*self.source.codemap())).emit(&self.diagnostics());
}
/// Create the optional span label displayed as an annotation on /// Create the optional span label displayed as an annotation on
/// the underlined span of the error. /// the underlined span of the error.
fn span_label(&self) -> Option<String> { fn span_label(&self) -> Option<String> {

View file

@ -49,6 +49,17 @@ impl EvalWarning {
.emit(&[self.diagnostic(source)]); .emit(&[self.diagnostic(source)]);
} }
/// Render a fancy, human-readable output of this warning and
/// print it to a std::io::Write. If rendered in a terminal that supports
/// colours and font styles, the output will include those.
pub fn fancy_format_write<E: std::io::Write + std::marker::Send>(
&self,
stderr: &mut E,
source: &SourceCode,
) {
Emitter::new(Box::new(stderr), Some(&*source.codemap())).emit(&[self.diagnostic(source)]);
}
/// Create the optional span label displayed as an annotation on /// Create the optional span label displayed as an annotation on
/// the underlined span of the warning. /// the underlined span of the warning.
fn span_label(&self) -> Option<String> { fn span_label(&self) -> Option<String> {