From 14b79dcb0452b9e4de4229ac84fcb336d18ed6fe Mon Sep 17 00:00:00 2001 From: ilbertt Date: Sun, 19 Oct 2025 18:32:39 +0200 Subject: [PATCH 1/5] fix: do not interleave errors with progress bars --- crates/icp-cli/src/commands/build/mod.rs | 52 +++++++++++++++--------- crates/icp-cli/src/commands/sync/mod.rs | 46 +++++++++++++-------- crates/icp-cli/src/progress.rs | 21 ++++++---- 3 files changed, 73 insertions(+), 46 deletions(-) diff --git a/crates/icp-cli/src/commands/build/mod.rs b/crates/icp-cli/src/commands/build/mod.rs index 2c6cb355..4e401838 100644 --- a/crates/icp-cli/src/commands/build/mod.rs +++ b/crates/icp-cli/src/commands/build/mod.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use anyhow::{Context as _, anyhow}; use camino_tempfile::tempdir; use clap::Args; -use futures::{StreamExt, stream::FuturesOrdered}; +use futures::{StreamExt, stream::FuturesUnordered}; use icp::{ canister::build::{BuildError, Params}, fs::read, @@ -79,7 +79,7 @@ pub async fn exec(ctx: &Context, args: &BuildArgs) -> Result<(), CommandError> { .collect::>(); // Prepare a futures set for concurrent canister builds - let mut futs = FuturesOrdered::new(); + let mut futs = FuturesUnordered::new(); let progress_manager = ProgressManager::new(ProgressManagerSettings { hidden: ctx.debug }); @@ -107,9 +107,8 @@ pub async fn exec(ctx: &Context, args: &BuildArgs) -> Result<(), CommandError> { for (i, step) in c.build.steps.iter().enumerate() { // Indicate to user the current step being executed let current_step = i + 1; - let pb_hdr = format!( - "\nBuilding: step {current_step} of {step_count} {step}" - ); + let pb_hdr = + format!("Building: step {current_step} of {step_count} {step}"); let tx = pb.begin_step(pb_hdr); // Perform build step @@ -161,30 +160,43 @@ pub async fn exec(ctx: &Context, args: &BuildArgs) -> Result<(), CommandError> { ) .await; - // After progress bar is finished, dump the output if build failed - if let Err(e) = &result { - pb.dump_output(ctx); - let _ = ctx - .term - .write_line(&format!("Failed to build canister: {e}")); - } + // If build failed, get the output for later display + let output = if result.is_err() { + Some(pb.dump_output()) + } else { + None + }; - result + (result, output) } }; - futs.push_back(fut); + futs.push(fut); } - // Consume the set of futures and abort if an error occurs - let mut found_error = false; - while let Some(res) = futs.next().await { - if res.is_err() { - found_error = true; + // Consume the set of futures and collect results + let mut failed_outputs = Vec::new(); + + while let Some((res, output)) = futs.next().await { + if let Err(e) = res { + if let Some(output) = output { + failed_outputs.push((output, e)); + } + // Stop iterating and dump output immediately on first failure + break; } } - if found_error { + futs.clear(); + + // If any builds failed, dump the output and abort + if !failed_outputs.is_empty() { + for (output, e) in failed_outputs { + let _ = ctx.term.write_line(""); + let _ = ctx.term.write_line(&output); + let _ = ctx.term.write_line(&format!("Build error: {e}")); + } + return Err(CommandError::Unexpected(anyhow!( "One or more canisters failed to build" ))); diff --git a/crates/icp-cli/src/commands/sync/mod.rs b/crates/icp-cli/src/commands/sync/mod.rs index 2fc3a534..7db60899 100644 --- a/crates/icp-cli/src/commands/sync/mod.rs +++ b/crates/icp-cli/src/commands/sync/mod.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use anyhow::anyhow; use clap::Args; -use futures::{StreamExt, stream::FuturesOrdered}; +use futures::{StreamExt, stream::FuturesUnordered}; use icp::{ agent, canister::sync::{Params, SynchronizeError}, @@ -129,7 +129,7 @@ pub async fn exec(ctx: &Context, args: &SyncArgs) -> Result<(), CommandError> { } // Prepare a futures set for concurrent canister syncs - let mut futs = FuturesOrdered::new(); + let mut futs = FuturesUnordered::new(); let progress_manager = ProgressManager::new(ProgressManagerSettings { hidden: ctx.debug }); @@ -198,30 +198,42 @@ pub async fn exec(ctx: &Context, args: &SyncArgs) -> Result<(), CommandError> { ) .await; - // After progress bar is finished, dump the output if sync failed - if let Err(e) = &result { - pb.dump_output(ctx); - let _ = ctx - .term - .write_line(&format!("Failed to sync canister: {e}")); - } + // If sync failed, get the output for later display + let output = if result.is_err() { + Some(pb.dump_output()) + } else { + None + }; - result + (result, output) } }; - futs.push_back(fut); + futs.push(fut); } - // Consume the set of futures and collect errors - let mut found_error = false; - while let Some(res) = futs.next().await { - if res.is_err() { - found_error = true; + // Collect all results to ensure all progress bars reach their final state + let mut failed_outputs = Vec::new(); + + while let Some((res, output)) = futs.next().await { + if let Err(_e) = res { + if let Some(output) = output { + failed_outputs.push(output); + } } } - if found_error { + // If any syncs failed, dump the output and abort + if !failed_outputs.is_empty() { + // Use MultiProgress println to write without clearing progress bars + let _ = progress_manager.multi_progress.println(""); + + // Dump the output for failed canisters + for output in failed_outputs { + let _ = progress_manager.multi_progress.println(&output); + let _ = progress_manager.multi_progress.println(""); + } + return Err(CommandError::Synchronize(SynchronizeError::Unexpected( anyhow!("One or more canisters failed to sync"), ))); diff --git a/crates/icp-cli/src/progress.rs b/crates/icp-cli/src/progress.rs index bd1ec6c5..aa5790c0 100644 --- a/crates/icp-cli/src/progress.rs +++ b/crates/icp-cli/src/progress.rs @@ -6,8 +6,6 @@ use itertools::Itertools; use tokio::{sync::mpsc, task::JoinHandle}; use tracing::debug; -use crate::commands::Context; - /// The maximum number of lines to display for a step output pub const MAX_LINES_PER_STEP: usize = 10_000; @@ -253,20 +251,25 @@ impl MultiStepProgressBar { self.finished_steps.push(StepOutput { title, output }); } - pub fn dump_output(&self, ctx: &Context) { - let _ = ctx.term.write_line(&format!( + pub fn dump_output(&self) -> String { + let mut lines = Vec::new(); + + lines.push(format!( "{} output for canister {}:", self.output_label, self.canister_name )); + for step_output in self.finished_steps.iter() { - let _ = ctx.term.write_line(&step_output.title); - for line in step_output.output.iter() { - let _ = ctx.term.write_line(line); - } + lines.push(format!("\n{}:", step_output.title)); + if step_output.output.is_empty() { - let _ = ctx.term.write_line(""); + lines.push("".to_string()); + } else { + lines.extend(step_output.output.iter().map(|s| format!("> {s}"))); } } + + lines.join("\n") } } From 2bc91fb8db9baa8bbc514124bb93581352f3dc03 Mon Sep 17 00:00:00 2001 From: ilbertt Date: Mon, 20 Oct 2025 09:19:17 +0200 Subject: [PATCH 2/5] refactor: revert sync output --- crates/icp-cli/src/commands/sync/mod.rs | 47 ++++++++++--------------- 1 file changed, 18 insertions(+), 29 deletions(-) diff --git a/crates/icp-cli/src/commands/sync/mod.rs b/crates/icp-cli/src/commands/sync/mod.rs index 7db60899..c81dd978 100644 --- a/crates/icp-cli/src/commands/sync/mod.rs +++ b/crates/icp-cli/src/commands/sync/mod.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use anyhow::anyhow; use clap::Args; -use futures::{StreamExt, stream::FuturesUnordered}; +use futures::{StreamExt, stream::FuturesOrdered}; use icp::{ agent, canister::sync::{Params, SynchronizeError}, @@ -129,7 +129,7 @@ pub async fn exec(ctx: &Context, args: &SyncArgs) -> Result<(), CommandError> { } // Prepare a futures set for concurrent canister syncs - let mut futs = FuturesUnordered::new(); + let mut futs = FuturesOrdered::new(); let progress_manager = ProgressManager::new(ProgressManagerSettings { hidden: ctx.debug }); @@ -198,42 +198,31 @@ pub async fn exec(ctx: &Context, args: &SyncArgs) -> Result<(), CommandError> { ) .await; - // If sync failed, get the output for later display - let output = if result.is_err() { - Some(pb.dump_output()) - } else { - None - }; + // After progress bar is finished, dump the output if sync failed + if let Err(e) = &result { + let output = pb.dump_output(); + let _ = ctx.term.write_str(&output); + let _ = ctx + .term + .write_line(&format!("Failed to sync canister: {e}")); + } - (result, output) + result } }; - futs.push(fut); + futs.push_back(fut); } - // Collect all results to ensure all progress bars reach their final state - let mut failed_outputs = Vec::new(); - - while let Some((res, output)) = futs.next().await { - if let Err(_e) = res { - if let Some(output) = output { - failed_outputs.push(output); - } + // Consume the set of futures and collect errors + let mut found_error = false; + while let Some(res) = futs.next().await { + if res.is_err() { + found_error = true; } } - // If any syncs failed, dump the output and abort - if !failed_outputs.is_empty() { - // Use MultiProgress println to write without clearing progress bars - let _ = progress_manager.multi_progress.println(""); - - // Dump the output for failed canisters - for output in failed_outputs { - let _ = progress_manager.multi_progress.println(&output); - let _ = progress_manager.multi_progress.println(""); - } - + if found_error { return Err(CommandError::Synchronize(SynchronizeError::Unexpected( anyhow!("One or more canisters failed to sync"), ))); From a2bdd09f7afd83665034d9dca4cb2d362ef14aae Mon Sep 17 00:00:00 2001 From: ilbertt Date: Mon, 20 Oct 2025 10:10:10 +0200 Subject: [PATCH 3/5] refactor: better print output --- crates/icp-cli/src/commands/build/mod.rs | 22 +++-- crates/icp-cli/src/commands/sync/mod.rs | 16 ++-- crates/icp-cli/src/logging.rs | 2 +- crates/icp-cli/src/progress.rs | 21 +++-- crates/icp-cli/tests/build_tests.rs | 114 +++++++++++------------ 5 files changed, 89 insertions(+), 86 deletions(-) diff --git a/crates/icp-cli/src/commands/build/mod.rs b/crates/icp-cli/src/commands/build/mod.rs index 4e401838..aded9987 100644 --- a/crates/icp-cli/src/commands/build/mod.rs +++ b/crates/icp-cli/src/commands/build/mod.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use anyhow::{Context as _, anyhow}; use camino_tempfile::tempdir; use clap::Args; -use futures::{StreamExt, stream::FuturesUnordered}; +use futures::{StreamExt, stream::FuturesOrdered}; use icp::{ canister::build::{BuildError, Params}, fs::read, @@ -79,7 +79,7 @@ pub async fn exec(ctx: &Context, args: &BuildArgs) -> Result<(), CommandError> { .collect::>(); // Prepare a futures set for concurrent canister builds - let mut futs = FuturesUnordered::new(); + let mut futs = FuturesOrdered::new(); let progress_manager = ProgressManager::new(ProgressManagerSettings { hidden: ctx.debug }); @@ -156,7 +156,7 @@ pub async fn exec(ctx: &Context, args: &BuildArgs) -> Result<(), CommandError> { &pb, async { build_result }, || "Built successfully".to_string(), - |err| format!("Failed to build canister: {err}"), + print_build_error, ) .await; @@ -171,7 +171,7 @@ pub async fn exec(ctx: &Context, args: &BuildArgs) -> Result<(), CommandError> { } }; - futs.push(fut); + futs.push_back(fut); } // Consume the set of futures and collect results @@ -182,19 +182,17 @@ pub async fn exec(ctx: &Context, args: &BuildArgs) -> Result<(), CommandError> { if let Some(output) = output { failed_outputs.push((output, e)); } - // Stop iterating and dump output immediately on first failure - break; } } - futs.clear(); - // If any builds failed, dump the output and abort if !failed_outputs.is_empty() { for (output, e) in failed_outputs { + for line in output { + let _ = ctx.term.write_line(&line); + } + let _ = ctx.term.write_line(&print_build_error(&e)); let _ = ctx.term.write_line(""); - let _ = ctx.term.write_line(&output); - let _ = ctx.term.write_line(&format!("Build error: {e}")); } return Err(CommandError::Unexpected(anyhow!( @@ -206,3 +204,7 @@ pub async fn exec(ctx: &Context, args: &BuildArgs) -> Result<(), CommandError> { Ok(()) } + +fn print_build_error(err: &CommandError) -> String { + format!("Failed to build canister: {err}") +} diff --git a/crates/icp-cli/src/commands/sync/mod.rs b/crates/icp-cli/src/commands/sync/mod.rs index c81dd978..be17850d 100644 --- a/crates/icp-cli/src/commands/sync/mod.rs +++ b/crates/icp-cli/src/commands/sync/mod.rs @@ -194,17 +194,17 @@ pub async fn exec(ctx: &Context, args: &SyncArgs) -> Result<(), CommandError> { &pb, async { sync_result }, || format!("Synced successfully: {cid}"), - |err| format!("Failed to sync canister: {err}"), + print_sync_error, ) .await; // After progress bar is finished, dump the output if sync failed if let Err(e) = &result { - let output = pb.dump_output(); - let _ = ctx.term.write_str(&output); - let _ = ctx - .term - .write_line(&format!("Failed to sync canister: {e}")); + for line in pb.dump_output() { + let _ = ctx.term.write_line(&line); + } + let _ = ctx.term.write_line(&print_sync_error(&e)); + let _ = ctx.term.write_line(""); } result @@ -232,3 +232,7 @@ pub async fn exec(ctx: &Context, args: &SyncArgs) -> Result<(), CommandError> { Ok(()) } + +fn print_sync_error(err: &CommandError) -> String { + format!("Failed to sync canister: {err}") +} diff --git a/crates/icp-cli/src/logging.rs b/crates/icp-cli/src/logging.rs index deda4c1d..9fa8a08e 100644 --- a/crates/icp-cli/src/logging.rs +++ b/crates/icp-cli/src/logging.rs @@ -22,7 +22,7 @@ impl Write for TermWriter { if !self.debug { self.writer.write(buf)?; } - debug!("{}", String::from_utf8_lossy(buf)); + debug!("{}", String::from_utf8_lossy(buf).trim()); Ok(buf.len()) } diff --git a/crates/icp-cli/src/progress.rs b/crates/icp-cli/src/progress.rs index aa5790c0..a05ce61f 100644 --- a/crates/icp-cli/src/progress.rs +++ b/crates/icp-cli/src/progress.rs @@ -251,25 +251,32 @@ impl MultiStepProgressBar { self.finished_steps.push(StepOutput { title, output }); } - pub fn dump_output(&self) -> String { + pub fn dump_output(&self) -> Vec { let mut lines = Vec::new(); lines.push(format!( - "{} output for canister {}:", - self.output_label, self.canister_name + "[{}] {} output:", + self.canister_name, self.output_label )); for step_output in self.finished_steps.iter() { - lines.push(format!("\n{}:", step_output.title)); + for line in step_output.title.lines() { + lines.push(format!("[{}] {}:", self.canister_name, line)); + } if step_output.output.is_empty() { - lines.push("".to_string()); + lines.push(format!("[{}] ", self.canister_name)); } else { - lines.extend(step_output.output.iter().map(|s| format!("> {s}"))); + lines.extend( + step_output + .output + .iter() + .map(|s| format!("[{}] > {s}", self.canister_name)), + ); } } - lines.join("\n") + lines } } diff --git a/crates/icp-cli/tests/build_tests.rs b/crates/icp-cli/tests/build_tests.rs index 628740ff..a9c99c1a 100644 --- a/crates/icp-cli/tests/build_tests.rs +++ b/crates/icp-cli/tests/build_tests.rs @@ -119,23 +119,20 @@ fn build_adapter_display_failing_build_output() { // Invoke build let expected_output = indoc! {r#" - Build output for canister my-canister: - - Building: step 1 of 3 (script) - echo "success 1" - success 1 - - Building: step 2 of 3 (script) - echo "success 2" - success 2 - - Building: step 3 of 3 (script) - for i in $(seq 1 5); do echo "failing build step $i"; done; exit 1 - failing build step 1 - failing build step 2 - failing build step 3 - failing build step 4 - failing build step 5 + [my-canister] Build output: + [my-canister] Building: step 1 of 3 (script): + [my-canister] echo "success 1": + [my-canister] > success 1 + [my-canister] Building: step 2 of 3 (script): + [my-canister] echo "success 2": + [my-canister] > success 2 + [my-canister] Building: step 3 of 3 (script): + [my-canister] for i in $(seq 1 5); do echo "failing build step $i"; done; exit 1: + [my-canister] > failing build step 1 + [my-canister] > failing build step 2 + [my-canister] > failing build step 3 + [my-canister] > failing build step 4 + [my-canister] > failing build step 5 Failed to build canister: command 'for i in $(seq 1 5); do echo "failing build step $i"; done; exit 1' failed with status code 1 "#}; @@ -176,15 +173,14 @@ fn build_adapter_display_failing_prebuilt_output() { // Invoke build let expected_output = indoc! {r#" - Build output for canister my-canister: - - Building: step 1 of 2 (script) - echo "initial step succeeded" - initial step succeeded - - Building: step 2 of 2 (pre-built) - path: /nonexistent/path/to/wasm.wasm, sha: invalid - Reading local file: /nonexistent/path/to/wasm.wasm + [my-canister] Build output: + [my-canister] Building: step 1 of 2 (script): + [my-canister] echo "initial step succeeded": + [my-canister] > initial step succeeded + [my-canister] Building: step 2 of 2 (pre-built): + [my-canister] path: /nonexistent/path/to/wasm.wasm, sha: invalid: + [my-canister] > Reading local file: /nonexistent/path/to/wasm.wasm + Failed to build canister: failed to read prebuilt canister file "#}; ctx.icp() @@ -192,8 +188,7 @@ fn build_adapter_display_failing_prebuilt_output() { .args(["build"]) .assert() .failure() - .stdout(contains(expected_output)) - .stdout(contains("Failed to build canister:")); + .stdout(contains(expected_output)); } #[test] @@ -223,15 +218,13 @@ fn build_adapter_display_failing_build_output_no_output() { // Invoke build let expected_output = indoc! {r#" - Build output for canister my-canister: - - Building: step 1 of 2 (script) - echo "step 1 succeeded" - step 1 succeeded - - Building: step 2 of 2 (script) - exit 1 - + [my-canister] Build output: + [my-canister] Building: step 1 of 2 (script): + [my-canister] echo "step 1 succeeded": + [my-canister] > step 1 succeeded + [my-canister] Building: step 2 of 2 (script): + [my-canister] exit 1: + [my-canister] Failed to build canister: command 'exit 1' failed with status code 1 "#}; @@ -277,28 +270,24 @@ fn build_adapter_display_multiple_failing_canisters() { // Invoke build let expected_output_one = indoc! {r#" - Build output for canister canister-one: - - Building: step 1 of 2 (script) - echo "canister-one step 1" - canister-one step 1 - - Building: step 2 of 2 (script) - echo "canister-one error"; exit 1 - canister-one error + [canister-one] Build output: + [canister-one] Building: step 1 of 2 (script): + [canister-one] echo "canister-one step 1": + [canister-one] > canister-one step 1 + [canister-one] Building: step 2 of 2 (script): + [canister-one] echo "canister-one error"; exit 1: + [canister-one] > canister-one error Failed to build canister: command 'echo "canister-one error"; exit 1' failed with status code 1 "#}; let expected_output_two = indoc! {r#" - Build output for canister canister-two: - - Building: step 1 of 2 (script) - echo "canister-two step 1" - canister-two step 1 - - Building: step 2 of 2 (script) - echo "canister-two error"; exit 1 - canister-two error + [canister-two] Build output: + [canister-two] Building: step 1 of 2 (script): + [canister-two] echo "canister-two step 1": + [canister-two] > canister-two step 1 + [canister-two] Building: step 2 of 2 (script): + [canister-two] echo "canister-two error"; exit 1: + [canister-two] > canister-two error Failed to build canister: command 'echo "canister-two error"; exit 1' failed with status code 1 "#}; @@ -380,13 +369,14 @@ fn build_adapter_display_script_multiple_commands_output() { // Invoke build let expected_output = indoc! {r#" - Building: step 1 of 1 (script) - echo "command 1" - echo "command 2" - echo "command 3" - command 1 - command 2 - command 3 + [my-canister] Build output: + [my-canister] Building: step 1 of 1 (script): + [my-canister] echo "command 1": + [my-canister] echo "command 2": + [my-canister] echo "command 3": + [my-canister] > command 1 + [my-canister] > command 2 + [my-canister] > command 3 Failed to build canister: build did not result in output "#}; From 46016f0ab73e01ed7afc51991853c489dfd3f883 Mon Sep 17 00:00:00 2001 From: ilbertt Date: Mon, 20 Oct 2025 10:14:42 +0200 Subject: [PATCH 4/5] fix: avoid empty lines --- crates/icp-cli/src/commands/build/mod.rs | 8 ++++---- crates/icp-cli/src/commands/sync/mod.rs | 2 +- crates/icp-cli/src/progress.rs | 4 +++- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/crates/icp-cli/src/commands/build/mod.rs b/crates/icp-cli/src/commands/build/mod.rs index aded9987..abe4dde2 100644 --- a/crates/icp-cli/src/commands/build/mod.rs +++ b/crates/icp-cli/src/commands/build/mod.rs @@ -178,10 +178,10 @@ pub async fn exec(ctx: &Context, args: &BuildArgs) -> Result<(), CommandError> { let mut failed_outputs = Vec::new(); while let Some((res, output)) = futs.next().await { - if let Err(e) = res { - if let Some(output) = output { - failed_outputs.push((output, e)); - } + if let Err(e) = res + && let Some(output) = output + { + failed_outputs.push((output, e)); } } diff --git a/crates/icp-cli/src/commands/sync/mod.rs b/crates/icp-cli/src/commands/sync/mod.rs index be17850d..d4f115f0 100644 --- a/crates/icp-cli/src/commands/sync/mod.rs +++ b/crates/icp-cli/src/commands/sync/mod.rs @@ -203,7 +203,7 @@ pub async fn exec(ctx: &Context, args: &SyncArgs) -> Result<(), CommandError> { for line in pb.dump_output() { let _ = ctx.term.write_line(&line); } - let _ = ctx.term.write_line(&print_sync_error(&e)); + let _ = ctx.term.write_line(&print_sync_error(e)); let _ = ctx.term.write_line(""); } diff --git a/crates/icp-cli/src/progress.rs b/crates/icp-cli/src/progress.rs index a05ce61f..b83c27ec 100644 --- a/crates/icp-cli/src/progress.rs +++ b/crates/icp-cli/src/progress.rs @@ -261,7 +261,9 @@ impl MultiStepProgressBar { for step_output in self.finished_steps.iter() { for line in step_output.title.lines() { - lines.push(format!("[{}] {}:", self.canister_name, line)); + if !line.is_empty() { + lines.push(format!("[{}] {}:", self.canister_name, line)); + } } if step_output.output.is_empty() { From 2bd3ff947bd0adc6c4fface7f79ce947ce79d2ac Mon Sep 17 00:00:00 2001 From: ilbertt Date: Mon, 20 Oct 2025 11:17:55 +0200 Subject: [PATCH 5/5] fix: PR feedback --- crates/icp-cli/src/commands/build/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/icp-cli/src/commands/build/mod.rs b/crates/icp-cli/src/commands/build/mod.rs index abe4dde2..a894711c 100644 --- a/crates/icp-cli/src/commands/build/mod.rs +++ b/crates/icp-cli/src/commands/build/mod.rs @@ -181,13 +181,13 @@ pub async fn exec(ctx: &Context, args: &BuildArgs) -> Result<(), CommandError> { if let Err(e) = res && let Some(output) = output { - failed_outputs.push((output, e)); + failed_outputs.push((e, output)); } } // If any builds failed, dump the output and abort if !failed_outputs.is_empty() { - for (output, e) in failed_outputs { + for (e, output) in failed_outputs { for line in output { let _ = ctx.term.write_line(&line); }