mirror of
https://github.com/nushell/nushell.git
synced 2025-08-09 12:25:58 +02:00
Support o>>
, e>>
, o+e>>
to append output to an external file (#10764)
# Description Close: #10278 This pr introduces `o>>`, `e>>`, `o+e>>` to allow redirection to append to a file. Examples: ```nushell echo abc o>> a.txt echo abc o>> a.txt cat asdf e>> a.txt cat asdf e>> a.txt cat asdf o+e>> a.txt ``` ~~TODO:~~ ~~1. currently internal commands with `o+e>` redirect to a variable is broken: `let x = "a.txt"; echo abc o+e> $x`, not sure when it was introduced...~~ ~~2. redirect stdout and stderr with append mode doesn't supported yet: `cat asdf o>>a.txt e>>b.ext`~~ ~~For these 2 items, I'd like to fix them in different prs.~~ Already done in this pr
This commit is contained in:
@ -1,7 +1,7 @@
|
||||
use nu_engine::current_dir;
|
||||
use nu_engine::CallExt;
|
||||
use nu_path::expand_path_with;
|
||||
use nu_protocol::ast::Call;
|
||||
use nu_protocol::ast::{Call, Expr, Expression};
|
||||
use nu_protocol::engine::{Command, EngineState, Stack};
|
||||
use nu_protocol::{
|
||||
Category, Example, PipelineData, RawStream, ShellError, Signature, Span, Spanned, SyntaxShape,
|
||||
@ -67,6 +67,24 @@ impl Command for Save {
|
||||
let append = call.has_flag("append");
|
||||
let force = call.has_flag("force");
|
||||
let progress = call.has_flag("progress");
|
||||
let out_append = if let Some(Expression {
|
||||
expr: Expr::Bool(out_append),
|
||||
..
|
||||
}) = call.get_parser_info("out-append")
|
||||
{
|
||||
*out_append
|
||||
} else {
|
||||
false
|
||||
};
|
||||
let err_append = if let Some(Expression {
|
||||
expr: Expr::Bool(err_append),
|
||||
..
|
||||
}) = call.get_parser_info("err-append")
|
||||
{
|
||||
*err_append
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
let span = call.head;
|
||||
let cwd = current_dir(engine_state, stack)?;
|
||||
@ -87,7 +105,7 @@ impl Command for Save {
|
||||
match input {
|
||||
PipelineData::ExternalStream { stdout: None, .. } => {
|
||||
// Open files to possibly truncate them
|
||||
let _ = get_files(&path, stderr_path.as_ref(), append, force)?;
|
||||
let _ = get_files(&path, stderr_path.as_ref(), append, false, false, force)?;
|
||||
Ok(PipelineData::empty())
|
||||
}
|
||||
PipelineData::ExternalStream {
|
||||
@ -95,7 +113,14 @@ impl Command for Save {
|
||||
stderr,
|
||||
..
|
||||
} => {
|
||||
let (file, stderr_file) = get_files(&path, stderr_path.as_ref(), append, force)?;
|
||||
let (file, stderr_file) = get_files(
|
||||
&path,
|
||||
stderr_path.as_ref(),
|
||||
append,
|
||||
out_append,
|
||||
err_append,
|
||||
force,
|
||||
)?;
|
||||
|
||||
// delegate a thread to redirect stderr to result.
|
||||
let handler = stderr.map(|stderr_stream| match stderr_file {
|
||||
@ -127,7 +152,14 @@ impl Command for Save {
|
||||
PipelineData::ListStream(ls, _)
|
||||
if raw || prepare_path(&path, append, force)?.0.extension().is_none() =>
|
||||
{
|
||||
let (mut file, _) = get_files(&path, stderr_path.as_ref(), append, force)?;
|
||||
let (mut file, _) = get_files(
|
||||
&path,
|
||||
stderr_path.as_ref(),
|
||||
append,
|
||||
out_append,
|
||||
err_append,
|
||||
force,
|
||||
)?;
|
||||
for val in ls {
|
||||
file.write_all(&value_to_bytes(val)?)
|
||||
.map_err(|err| ShellError::IOError(err.to_string()))?;
|
||||
@ -143,7 +175,14 @@ impl Command for Save {
|
||||
input_to_bytes(input, Path::new(&path.item), raw, engine_state, stack, span)?;
|
||||
|
||||
// Only open file after successful conversion
|
||||
let (mut file, _) = get_files(&path, stderr_path.as_ref(), append, force)?;
|
||||
let (mut file, _) = get_files(
|
||||
&path,
|
||||
stderr_path.as_ref(),
|
||||
append,
|
||||
out_append,
|
||||
err_append,
|
||||
force,
|
||||
)?;
|
||||
|
||||
file.write_all(&bytes)
|
||||
.map_err(|err| ShellError::IOError(err.to_string()))?;
|
||||
@ -319,17 +358,19 @@ fn get_files(
|
||||
path: &Spanned<PathBuf>,
|
||||
stderr_path: Option<&Spanned<PathBuf>>,
|
||||
append: bool,
|
||||
out_append: bool,
|
||||
err_append: bool,
|
||||
force: bool,
|
||||
) -> Result<(File, Option<File>), ShellError> {
|
||||
// First check both paths
|
||||
let (path, path_span) = prepare_path(path, append, force)?;
|
||||
let (path, path_span) = prepare_path(path, append || out_append, force)?;
|
||||
let stderr_path_and_span = stderr_path
|
||||
.as_ref()
|
||||
.map(|stderr_path| prepare_path(stderr_path, append, force))
|
||||
.map(|stderr_path| prepare_path(stderr_path, append || err_append, force))
|
||||
.transpose()?;
|
||||
|
||||
// Only if both files can be used open and possibly truncate them
|
||||
let file = open_file(path, path_span, append)?;
|
||||
let file = open_file(path, path_span, append || out_append)?;
|
||||
|
||||
let stderr_file = stderr_path_and_span
|
||||
.map(|(stderr_path, stderr_path_span)| {
|
||||
@ -342,7 +383,7 @@ fn get_files(
|
||||
vec![],
|
||||
))
|
||||
} else {
|
||||
open_file(stderr_path, stderr_path_span, append)
|
||||
open_file(stderr_path, stderr_path_span, append || err_append)
|
||||
}
|
||||
})
|
||||
.transpose()?;
|
||||
|
@ -124,7 +124,7 @@ impl Command for FromNuon {
|
||||
} else {
|
||||
match pipeline.elements.remove(0) {
|
||||
PipelineElement::Expression(_, expression)
|
||||
| PipelineElement::Redirection(_, _, expression)
|
||||
| PipelineElement::Redirection(_, _, expression, _)
|
||||
| PipelineElement::And(_, expression)
|
||||
| PipelineElement::Or(_, expression)
|
||||
| PipelineElement::SameTargetRedirection {
|
||||
@ -132,7 +132,7 @@ impl Command for FromNuon {
|
||||
..
|
||||
}
|
||||
| PipelineElement::SeparateRedirection {
|
||||
out: (_, expression),
|
||||
out: (_, expression, _),
|
||||
..
|
||||
} => expression,
|
||||
}
|
||||
|
@ -12,6 +12,14 @@ fn redirect_err() {
|
||||
);
|
||||
|
||||
assert!(output.out.contains("asdfasdfasdf.txt"));
|
||||
|
||||
// check append mode
|
||||
let output = nu!(
|
||||
cwd: dirs.test(),
|
||||
"cat asdfasdfasdf.txt err>> a.txt; cat a.txt"
|
||||
);
|
||||
let v: Vec<_> = output.out.match_indices("asdfasdfasdf.txt").collect();
|
||||
assert_eq!(v.len(), 2);
|
||||
})
|
||||
}
|
||||
|
||||
@ -25,6 +33,12 @@ fn redirect_err() {
|
||||
);
|
||||
|
||||
assert!(output.out.contains("true"));
|
||||
|
||||
let output = nu!(
|
||||
cwd: dirs.test(),
|
||||
"vol missingdrive err>> a; (open a | str stats).bytes >= 32"
|
||||
);
|
||||
assert!(output.out.contains("true"));
|
||||
})
|
||||
}
|
||||
|
||||
@ -39,6 +53,10 @@ fn redirect_outerr() {
|
||||
let output = nu!(cwd: dirs.test(), "cat a");
|
||||
|
||||
assert!(output.out.contains("asdfasdfasdf.txt"));
|
||||
|
||||
let output = nu!(cwd: dirs.test(), "cat asdfasdfasdf.txt o+e>> a; cat a");
|
||||
let v: Vec<_> = output.out.match_indices("asdfasdfasdf.txt").collect();
|
||||
assert_eq!(v.len(), 2);
|
||||
})
|
||||
}
|
||||
|
||||
@ -53,6 +71,13 @@ fn redirect_outerr() {
|
||||
let output = nu!(cwd: dirs.test(), "(open a | str stats).bytes >= 16");
|
||||
|
||||
assert!(output.out.contains("true"));
|
||||
|
||||
nu!(
|
||||
cwd: dirs.test(),
|
||||
"vol missingdrive out+err>> a"
|
||||
);
|
||||
let output = nu!(cwd: dirs.test(), "(open a | str stats).bytes >= 32");
|
||||
assert!(output.out.contains("true"));
|
||||
})
|
||||
}
|
||||
|
||||
@ -65,6 +90,12 @@ fn redirect_out() {
|
||||
);
|
||||
|
||||
assert!(output.out.contains("hello"));
|
||||
|
||||
let output = nu!(
|
||||
cwd: dirs.test(),
|
||||
"echo 'hello' out>> a; open a"
|
||||
);
|
||||
assert!(output.out.contains("hellohello"));
|
||||
})
|
||||
}
|
||||
|
||||
@ -124,6 +155,25 @@ fn separate_redirection() {
|
||||
let expected_err_file = dirs.test().join("err.txt");
|
||||
let actual = file_contents(expected_err_file);
|
||||
assert!(actual.contains(expect_body));
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
sandbox.with_files(vec![FileWithContent("test.sh", script_body)]);
|
||||
nu!(
|
||||
cwd: dirs.test(),
|
||||
"bash test.sh out>> out.txt err>> err.txt"
|
||||
);
|
||||
// check for stdout redirection file.
|
||||
let expected_out_file = dirs.test().join("out.txt");
|
||||
let actual = file_contents(expected_out_file);
|
||||
let v: Vec<_> = actual.match_indices("message").collect();
|
||||
assert_eq!(v.len(), 2);
|
||||
|
||||
// check for stderr redirection file.
|
||||
let expected_err_file = dirs.test().join("err.txt");
|
||||
let actual = file_contents(expected_err_file);
|
||||
let v: Vec<_> = actual.match_indices("message").collect();
|
||||
assert_eq!(v.len(), 2);
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
@ -152,6 +202,21 @@ fn same_target_redirection_with_too_much_stderr_not_hang_nushell() {
|
||||
let expected_file = dirs.test().join("another_large_file.txt");
|
||||
let actual = file_contents(expected_file);
|
||||
assert_eq!(actual, format!("{large_file_body}\n"));
|
||||
|
||||
// not hangs in append mode either.
|
||||
let cloned_body = large_file_body.clone();
|
||||
large_file_body.push_str(&format!("\n{cloned_body}"));
|
||||
nu!(
|
||||
cwd: dirs.test(), pipeline(
|
||||
"
|
||||
$env.LARGE = (open --raw a_large_file.txt);
|
||||
nu --testbin echo_env_stderr LARGE out+err>> another_large_file.txt
|
||||
"
|
||||
),
|
||||
);
|
||||
let expected_file = dirs.test().join("another_large_file.txt");
|
||||
let actual = file_contents(expected_file);
|
||||
assert_eq!(actual, format!("{large_file_body}\n"));
|
||||
})
|
||||
}
|
||||
|
||||
@ -202,6 +267,16 @@ fn redirection_with_pipeline_works() {
|
||||
let expected_out_file = dirs.test().join("out.txt");
|
||||
let actual = file_contents(expected_out_file);
|
||||
assert!(actual.contains(expect_body));
|
||||
|
||||
// check append mode works
|
||||
nu!(
|
||||
cwd: dirs.test(),
|
||||
"bash test.sh o>> out.txt | describe"
|
||||
);
|
||||
let expected_out_file = dirs.test().join("out.txt");
|
||||
let actual = file_contents(expected_out_file);
|
||||
let v: Vec<_> = actual.match_indices("message").collect();
|
||||
assert_eq!(v.len(), 2);
|
||||
},
|
||||
)
|
||||
}
|
||||
@ -224,6 +299,22 @@ fn redirect_support_variable() {
|
||||
let expected_out_file = dirs.test().join("tmp_file");
|
||||
let actual = file_contents(expected_out_file);
|
||||
assert!(actual.contains("hello there"));
|
||||
|
||||
// append mode support variable too.
|
||||
let output = nu!(
|
||||
cwd: dirs.test(),
|
||||
"let x = 'tmp_file'; echo 'hello' out>> $x; open tmp_file"
|
||||
);
|
||||
let v: Vec<_> = output.out.match_indices("hello").collect();
|
||||
assert_eq!(v.len(), 2);
|
||||
|
||||
let output = nu!(
|
||||
cwd: dirs.test(),
|
||||
"let x = 'tmp_file'; echo 'hello' out+err>> $x; open tmp_file"
|
||||
);
|
||||
// check for stdout redirection file.
|
||||
let v: Vec<_> = output.out.match_indices("hello").collect();
|
||||
assert_eq!(v.len(), 3);
|
||||
})
|
||||
}
|
||||
|
||||
@ -243,7 +334,7 @@ fn separate_redirection_support_variable() {
|
||||
sandbox.with_files(vec![FileWithContent("test.sh", script_body)]);
|
||||
nu!(
|
||||
cwd: dirs.test(),
|
||||
r#"let o_f = "out.txt"; let e_f = "err.txt"; bash test.sh out> $o_f err> $e_f"#
|
||||
r#"let o_f = "out2.txt"; let e_f = "err2.txt"; bash test.sh out> $o_f err> $e_f"#
|
||||
);
|
||||
}
|
||||
#[cfg(windows)]
|
||||
@ -251,18 +342,38 @@ fn separate_redirection_support_variable() {
|
||||
sandbox.with_files(vec![FileWithContent("test.bat", script_body)]);
|
||||
nu!(
|
||||
cwd: dirs.test(),
|
||||
r#"let o_f = "out.txt"; let e_f = "err.txt"; cmd /D /c test.bat out> $o_f err> $e_f"#
|
||||
r#"let o_f = "out2.txt"; let e_f = "err2.txt"; cmd /D /c test.bat out> $o_f err> $e_f"#
|
||||
);
|
||||
}
|
||||
// check for stdout redirection file.
|
||||
let expected_out_file = dirs.test().join("out.txt");
|
||||
let expected_out_file = dirs.test().join("out2.txt");
|
||||
let actual = file_contents(expected_out_file);
|
||||
assert!(actual.contains(expect_body));
|
||||
|
||||
// check for stderr redirection file.
|
||||
let expected_err_file = dirs.test().join("err.txt");
|
||||
let expected_err_file = dirs.test().join("err2.txt");
|
||||
let actual = file_contents(expected_err_file);
|
||||
assert!(actual.contains(expect_body));
|
||||
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
sandbox.with_files(vec![FileWithContent("test.sh", script_body)]);
|
||||
nu!(
|
||||
cwd: dirs.test(),
|
||||
r#"let o_f = "out2.txt"; let e_f = "err2.txt"; bash test.sh out>> $o_f err>> $e_f"#
|
||||
);
|
||||
// check for stdout redirection file.
|
||||
let expected_out_file = dirs.test().join("out2.txt");
|
||||
let actual = file_contents(expected_out_file);
|
||||
let v: Vec<_> = actual.match_indices("message").collect();
|
||||
assert_eq!(v.len(), 2);
|
||||
|
||||
// check for stderr redirection file.
|
||||
let expected_err_file = dirs.test().join("err2.txt");
|
||||
let actual = file_contents(expected_err_file);
|
||||
let v: Vec<_> = actual.match_indices("message").collect();
|
||||
assert_eq!(v.len(), 2);
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
Reference in New Issue
Block a user