Skip to content

Commit

Permalink
Support o>>, e>>, o+e>> to append output to an external file (#…
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
WindSoilder committed Nov 27, 2023
1 parent 1ff8c2d commit 077d1c8
Show file tree
Hide file tree
Showing 13 changed files with 364 additions and 147 deletions.
6 changes: 4 additions & 2 deletions crates/nu-cli/src/completions/completer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,11 +122,13 @@ impl NuCompleter {
for pipeline_element in pipeline.elements {
match pipeline_element {
PipelineElement::Expression(_, expr)
| PipelineElement::Redirection(_, _, expr)
| PipelineElement::Redirection(_, _, expr, _)
| PipelineElement::And(_, expr)
| PipelineElement::Or(_, expr)
| PipelineElement::SameTargetRedirection { cmd: (_, expr), .. }
| PipelineElement::SeparateRedirection { out: (_, expr), .. } => {
| PipelineElement::SeparateRedirection {
out: (_, expr, _), ..
} => {
let flattened: Vec<_> = flatten_expression(&working_set, &expr);
let mut spans: Vec<String> = vec![];

Expand Down
4 changes: 2 additions & 2 deletions crates/nu-cli/src/syntax_highlight.rs
Original file line number Diff line number Diff line change
Expand Up @@ -252,11 +252,11 @@ fn find_matching_block_end_in_block(
for e in &p.elements {
match e {
PipelineElement::Expression(_, e)
| PipelineElement::Redirection(_, _, e)
| PipelineElement::Redirection(_, _, e, _)
| PipelineElement::And(_, e)
| PipelineElement::Or(_, e)
| PipelineElement::SameTargetRedirection { cmd: (_, e), .. }
| PipelineElement::SeparateRedirection { out: (_, e), .. } => {
| PipelineElement::SeparateRedirection { out: (_, e, _), .. } => {
if e.span.contains(global_cursor_offset) {
if let Some(pos) = find_matching_block_end_in_expr(
line,
Expand Down
59 changes: 50 additions & 9 deletions crates/nu-command/src/filesystem/save.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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)?;
Expand All @@ -87,15 +105,22 @@ 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 {
stdout: Some(stream),
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 {
Expand Down Expand Up @@ -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()))?;
Expand All @@ -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()))?;
Expand Down Expand Up @@ -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)| {
Expand All @@ -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()?;
Expand Down
4 changes: 2 additions & 2 deletions crates/nu-command/src/formats/from/nuon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,15 +124,15 @@ 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 {
cmd: (_, expression),
..
}
| PipelineElement::SeparateRedirection {
out: (_, expression),
out: (_, expression, _),
..
} => expression,
}
Expand Down
119 changes: 115 additions & 4 deletions crates/nu-command/tests/commands/redirection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
})
}

Expand All @@ -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"));
})
}

Expand All @@ -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);
})
}

Expand All @@ -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"));
})
}

Expand All @@ -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"));
})
}

Expand Down Expand Up @@ -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);
}
},
)
}
Expand Down Expand Up @@ -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"));
})
}

Expand Down Expand Up @@ -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);
},
)
}
Expand All @@ -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);
})
}

Expand All @@ -243,26 +334,46 @@ 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)]
{
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);
}
},
)
}
Expand Down

0 comments on commit 077d1c8

Please sign in to comment.