mirror of
https://github.com/uutils/sed.git
synced 2026-06-10 16:14:15 -07:00
508d74fe4a
* exit with code 1 when no arguments * add a test for no arguments. * Update test_sed.rs * cargo fmt * pass -n for missing script arguments test Issue: #274 PR: #409
1326 lines
32 KiB
Rust
1326 lines
32 KiB
Rust
// Integration tests
|
||
//
|
||
// SPDX-License-Identifier: MIT
|
||
// Copyright (c) 2025 Diomidis Spinellis
|
||
//
|
||
// This file is part of the uutils sed package.
|
||
// It is licensed under the MIT License.
|
||
// For the full copyright and license information, please view the LICENSE
|
||
// file that was distributed with this source code.
|
||
|
||
use std::fs;
|
||
use std::io::{Read, Write};
|
||
|
||
#[cfg(unix)]
|
||
use assert_fs::fixture::{FileWriteStr, PathChild};
|
||
|
||
use sha2::{Digest, Sha256};
|
||
use tempfile::NamedTempFile;
|
||
use uutests::new_ucmd;
|
||
|
||
////////////////////////////////////////////////////////////
|
||
// Test application's invocation
|
||
#[test]
|
||
fn test_invalid_arg() {
|
||
new_ucmd!().arg("--definitely-invalid").fails().code_is(1);
|
||
}
|
||
|
||
#[test]
|
||
fn test_version() {
|
||
let short = new_ucmd!()
|
||
.arg("-V")
|
||
.succeeds()
|
||
.no_stderr()
|
||
.stdout_str()
|
||
.to_owned();
|
||
|
||
new_ucmd!()
|
||
.arg("--version")
|
||
.succeeds()
|
||
.no_stderr()
|
||
.stdout_is(short);
|
||
}
|
||
|
||
#[test]
|
||
fn test_debug() {
|
||
new_ucmd!().args(&["--debug", ""]).succeeds();
|
||
}
|
||
|
||
#[test]
|
||
fn test_silent_alias() {
|
||
new_ucmd!().args(&["--silent", ""]).succeeds();
|
||
}
|
||
|
||
#[test]
|
||
fn test_missing_script_argument() {
|
||
new_ucmd!()
|
||
.args(&["-n"])
|
||
.fails()
|
||
.code_is(1)
|
||
.stderr_contains("missing script");
|
||
}
|
||
|
||
#[test]
|
||
fn test_no_arguments() {
|
||
new_ucmd!()
|
||
.fails()
|
||
.code_is(1)
|
||
.stdout_contains("Stream editor for filtering and transforming text");
|
||
}
|
||
|
||
#[test]
|
||
fn test_positional_script_ok() {
|
||
new_ucmd!().arg("l").succeeds().code_is(0);
|
||
}
|
||
|
||
#[test]
|
||
fn test_empty_positional_script_ok() {
|
||
new_ucmd!().arg("").succeeds().code_is(0);
|
||
}
|
||
|
||
#[test]
|
||
fn test_e_script_ok() {
|
||
new_ucmd!().args(&["-e", "l"]).succeeds();
|
||
}
|
||
|
||
#[test]
|
||
fn test_f_script_ok() {
|
||
new_ucmd!().arg("-f").arg("script/hanoi.sed").succeeds();
|
||
}
|
||
|
||
////////////////////////////////////////////////////////////
|
||
// Test simple I/O processing
|
||
const INPUT_FILES: &[&str] = &[
|
||
"input/two-lines.txt",
|
||
"input/no-new-line.txt",
|
||
"input/dots-4k.txt",
|
||
"input/dots-8k.txt",
|
||
"input/dots-64k.txt",
|
||
];
|
||
|
||
#[test]
|
||
fn test_no_script_stdin() {
|
||
for fixture in INPUT_FILES {
|
||
new_ucmd!()
|
||
.arg("")
|
||
.pipe_in_fixture(fixture)
|
||
.succeeds()
|
||
.stdout_is_fixture(fixture);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_no_script_file() {
|
||
for fixture in INPUT_FILES {
|
||
new_ucmd!()
|
||
.args(&["-e", "", fixture])
|
||
.succeeds()
|
||
.stdout_is_fixture(fixture);
|
||
}
|
||
}
|
||
|
||
/// Test correct functioning of copy_file_range.
|
||
#[test]
|
||
fn test_multiple_input_files() {
|
||
new_ucmd!()
|
||
.args(&[
|
||
"-e",
|
||
"",
|
||
"input/dots-64k.txt",
|
||
"input/no-new-line.txt",
|
||
"input/dots-64k.txt",
|
||
])
|
||
.succeeds()
|
||
.stdout_is_fixture("output/multiple_input_files");
|
||
}
|
||
|
||
#[test]
|
||
fn test_delete_stdin() {
|
||
for fixture in INPUT_FILES {
|
||
new_ucmd!()
|
||
.arg("d")
|
||
.pipe_in_fixture(fixture)
|
||
.succeeds()
|
||
.no_stdout();
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_delete_file() {
|
||
for fixture in INPUT_FILES {
|
||
new_ucmd!()
|
||
.args(&["-e", "d", fixture])
|
||
.succeeds()
|
||
.no_stdout();
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
#[cfg(unix)]
|
||
fn test_special_file() {
|
||
for fixture in INPUT_FILES {
|
||
// To test path avoiding copy_file_range
|
||
let devnull = fs::File::create("/dev/null").unwrap();
|
||
|
||
new_ucmd!()
|
||
.args(&["-e", "", fixture])
|
||
.set_stdout(devnull)
|
||
.succeeds();
|
||
}
|
||
}
|
||
|
||
/// Create a new test function to verify an execution for specified output.
|
||
macro_rules! check_output {
|
||
($name:ident, $args:expr) => {
|
||
#[test]
|
||
fn $name() {
|
||
new_ucmd!()
|
||
.args(&$args)
|
||
.succeeds()
|
||
.stdout_is_fixture(&format!("output/{}", stringify!($name)));
|
||
}
|
||
};
|
||
}
|
||
|
||
// Run ucmd twice to test POSIX conformance: Once where posix is "--posix"
|
||
// and once where it has the dummy value "--follow-symlinks".
|
||
// This shall be used to test commands that behave differently under POSIX.
|
||
macro_rules! check_output_posix {
|
||
($name:ident, [$($args:expr),* $(,)?]) => {
|
||
#[test]
|
||
fn $name() {
|
||
for posix in ["--posix", "--follow-symlinks"] {
|
||
new_ucmd!()
|
||
.args(&[posix $(, $args)*]) // prepend posix, then add args
|
||
.succeeds()
|
||
.stdout_is_fixture(&format!("output/{}", stringify!($name)));
|
||
}
|
||
}
|
||
};
|
||
}
|
||
|
||
////////////////////////////////////////////////////////////
|
||
// Individual command tests
|
||
|
||
// Input files
|
||
const LINES1: &str = "input/lines1";
|
||
const LINES2: &str = "input/lines2";
|
||
const NO_NEW_LINE: &str = "input/no-new-line.txt";
|
||
|
||
////////////////////////////////////////////////////////////
|
||
// Comments
|
||
check_output!(comment, ["-n", "-e", "4p;# comment", LINES1]);
|
||
check_output!(comment_silent, ["-e", "#n", "-e", "4p", LINES1]);
|
||
check_output!(comment_no_silent, ["-e", "4p;#n comment", LINES1]);
|
||
|
||
////////////////////////////////////////////////////////////
|
||
// Address ranges: One and two, numeric and pattern
|
||
check_output!(addr_one_line, ["-n", "-e", "4p", LINES1]);
|
||
check_output!(addr_straddle, ["-n", "-e", "20p", LINES1, LINES2]);
|
||
check_output!(addr_last_one_file, ["-n", "-e", "$p", LINES1]);
|
||
check_output!(addr_last_two_files, ["-n", "-e", "$p", LINES1, LINES2]);
|
||
|
||
check_output!(addr_append_empty, ["-e", "$a\\\nhello", "input/empty"]);
|
||
|
||
check_output!(
|
||
addr_last_empty,
|
||
["-n", "-e", "$p", LINES1, "input/empty", LINES2]
|
||
);
|
||
|
||
check_output!(addr_past_last, ["-n", "-e", "20p", LINES1]);
|
||
check_output!(addr_not_found, ["-n", "-e", "/NOTFOUND/p", LINES1]);
|
||
check_output!(addr_found, ["-n", "/l1_7/p", LINES1]);
|
||
check_output!(addr_found_space, ["-n", " /l1_7/ p", LINES1]);
|
||
check_output!(addr_escaped_delimiter, ["-n", "\\_l1\\_7_p", LINES1]);
|
||
check_output!(addr_range_numeric, ["-n", "1,4p", LINES1]);
|
||
check_output!(addr_range_to_last, ["-n", "1,$p", LINES1, LINES2]);
|
||
check_output!(addr_range_to_pattern, ["-n", "1,/l2_9/p", LINES1, LINES2]);
|
||
check_output!(
|
||
addr_range_straddle,
|
||
["-n", "/l1_3/,/l2_3/p", LINES1, LINES2]
|
||
);
|
||
check_output!(
|
||
addr_range_separate,
|
||
["-n", "--separate", "/l1_3/,/l2_3/p", LINES1, LINES2]
|
||
);
|
||
check_output!(addr_range_from_zero_to_pattern, ["-n", "0,/_1/p", LINES1]);
|
||
check_output!(addr_pattern_to_last, ["-n", "/4/,$p", LINES1, LINES2]);
|
||
check_output!(addr_pattern_to_straddle, ["-n", "/4/,20p", LINES1, LINES2]);
|
||
check_output!(addr_pattern_to_pattern, ["-n", "/4/,/10/p", LINES1, LINES2]);
|
||
check_output!(
|
||
addr_pattern_straddle,
|
||
["-n", "/l2_3/,/l1_8/p", LINES1, LINES2]
|
||
);
|
||
check_output!(addr_range_reverse, ["-n", "12,3p", LINES1, LINES2]);
|
||
check_output!(
|
||
addr_pattern_range_reverse,
|
||
["-n", "/l1_7/,3p", LINES1, LINES2]
|
||
);
|
||
check_output!(addr_numeric_to_relative, ["-n", "13,+4p", LINES1, LINES2]);
|
||
check_output!(
|
||
addr_pattern_to_relative,
|
||
["-n", "/l1_6/,+2p", LINES1, LINES2]
|
||
);
|
||
check_output!(addr_numeric_relative_straddle, ["-n", "12,+1p", LINES1]);
|
||
check_output!(
|
||
addr_first_separate,
|
||
["-n", "--separate", "1p", LINES1, LINES2]
|
||
);
|
||
check_output!(addr_last_separate, ["-ns", "$p", LINES1, LINES2]);
|
||
check_output!(addr_two_lines_semicolon, ["-n", "-e", "4p;8p", LINES1]);
|
||
check_output!(addr_two_lines_newline, ["-n", "-e", "4p\n8p", LINES1]);
|
||
check_output!(addr_three_lines_semicolon, ["-n", "-e", "4p;8p;1p", LINES1]);
|
||
check_output!(addr_one_line_negate, ["-n", "-e", "4!p", LINES1]);
|
||
check_output!(addr_range_numeric_negate, ["-n", "1,4!p", LINES1]);
|
||
check_output!(
|
||
addr_pattern_to_pattern_negate,
|
||
["-n", "/1_4/,/10/!p", LINES1]
|
||
);
|
||
check_output!(addr_empty_re_reuse, ["-n", "/_2/,//p", LINES1, LINES2]);
|
||
check_output!(addr_simple_negation, ["-e", r"4,12!s/^/^/", LINES1]);
|
||
check_output!(addr_range_even, ["-n", "0~2p", LINES1]);
|
||
check_output!(addr_range_odd, ["-n", "1~2p", LINES1]);
|
||
check_output!(addr_range_step_zero, ["-n", "10~0p", LINES1]);
|
||
check_output!(addr_range_end_multiple, ["-n", "/l1_2/,~10p", LINES1]);
|
||
|
||
////////////////////////////////////////////////////////////
|
||
// Substitution: s
|
||
check_output!(subst_any, ["-e", r"s/./X/g", LINES1]);
|
||
check_output!(subst_any_global, ["-e", r"s,.,X,g", LINES1]);
|
||
check_output!(subst_escaped_magic_separator, ["-e", r"s.\..X.g", LINES1]);
|
||
check_output!(subst_escaped_braced_separator, ["-e", r"s/[\/]/Q/", LINES1]);
|
||
check_output!(subst_escaped_separator, ["-e", r"s_\__X_", LINES1]);
|
||
check_output!(subst_whole_match_group, ["-e", r"s/./(&)/g", LINES1]);
|
||
check_output!(subst_print, ["-ne", "s/1_1/S&/p", LINES1]);
|
||
check_output!(
|
||
subst_escaped_whole_match_group,
|
||
["-e", r"s/./(\&)/g", LINES1]
|
||
);
|
||
check_output!(
|
||
subst_numerical_groups,
|
||
["-e", r"s/\(.\)\(.\)\(.\)/x\3x\2x\1/g", LINES1]
|
||
);
|
||
check_output!(
|
||
subst_ere_numerical_groups,
|
||
[
|
||
"--regexp-extended",
|
||
"-e",
|
||
r"s/(.)(.)(.)/x\3x\2x\1/g",
|
||
LINES1
|
||
]
|
||
);
|
||
check_output!(
|
||
subst_quantifier_zero_or_one,
|
||
["-e", r"s/_[0-9]\([0-9]\)\?/_x\1/g", LINES1]
|
||
);
|
||
check_output!(
|
||
subst_quantifier_one_or_more,
|
||
["-e", r"s/_[0-9]\+/_x/g", LINES1]
|
||
);
|
||
check_output!(subst_alternation_operator, ["-e", r"s/0\|1/x/g", LINES1]);
|
||
check_output!(subst_multiline, ["-e", "s/_/u0\\\nu1\\\nu2/g", LINES1]);
|
||
check_output!(subst_numbered_replacement, ["-e", r"s/./X/4", LINES1]);
|
||
check_output!(subst_brace, ["-e", r"s/[123]/X/g", LINES1]);
|
||
check_output!(subst_case_insensitive, ["-e", r"s/L/Line/", LINES1]);
|
||
check_output!(subst_no_new_line, ["-e", r"s/l/L/g", NO_NEW_LINE]);
|
||
check_output!(subst_re_reuse, ["-e", r"2s//M/;1s/l/L/", LINES1]);
|
||
check_output!(subst_newline_class, ["-n", r"1{;N;s/[\n]/X/;p;}", LINES1]);
|
||
check_output!(subst_newline_re, ["-n", r"1{;N;s/\n/X/;p;}", LINES1]);
|
||
|
||
// Check appropriate selection and behavior of fast_Regex matcher
|
||
// Literal matcher
|
||
check_output!(subst_literal_start, ["-e", r"s/^l1/L1/", LINES1]);
|
||
check_output!(subst_literal_end, ["-e", r"s/2$/TWO/", LINES1]);
|
||
check_output!(subst_literal, ["-e", r"s/_/-/", LINES1]);
|
||
|
||
// Fancy matcher
|
||
check_output!(subst_backref, ["-e", r"s/l\(.\)_\1/same-number/", LINES1]);
|
||
|
||
// Bytes matcher with Unicode
|
||
check_output!(subst_greek, ["-e", r"s/[α-ω]/G/g", "input/unicode"]);
|
||
check_output!(subst_any_unicode, ["-e", r"s/.$/:-)/", "input/unicode"]);
|
||
check_output!(subst_lcase, ["-e", r"s/κ/*/gi", "input/unicode"]);
|
||
check_output!(subst_word, ["-E", "-e", r"s/\w+/WORD/g", "input/unicode"]);
|
||
|
||
#[test]
|
||
fn subst_write_file() -> std::io::Result<()> {
|
||
let temp = NamedTempFile::new()?;
|
||
let path = temp.path();
|
||
let cmd = format!("s/_1/S_1/w {}", path.display());
|
||
|
||
new_ucmd!().args(&["-n", &cmd, LINES1]).succeeds();
|
||
|
||
let mut actual = String::new();
|
||
temp.reopen()?.read_to_string(&mut actual)?;
|
||
|
||
let expected = fs::read_to_string("tests/fixtures/sed/output/subst_write_file")?;
|
||
assert_eq!(actual, expected, "Output did not match fixture");
|
||
|
||
Ok(())
|
||
}
|
||
|
||
////////////////////////////////////////////////////////////
|
||
// Transliteration: y
|
||
check_output!(trans_simple, ["-e", r"y/0123456789/9876543210/", LINES1]);
|
||
check_output!(
|
||
trans_delimiter,
|
||
["-e", r"y10\123456789198765432\101", LINES1]
|
||
);
|
||
check_output!(trans_no_new_line, ["-e", r"y/l/L/", NO_NEW_LINE]);
|
||
check_output!(trans_newline, ["-e", r"1N;2y/\n/X/", LINES1]);
|
||
|
||
////////////////////////////////////////////////////////////
|
||
// Pattern space manipulation: D, d, H, h, N, n, P, p, q, x
|
||
check_output!(pattern_print_to_newline, ["-n", r"1{;N;P;P;p;}", LINES1]);
|
||
check_output!(pattern_next_print, ["-n", r"N;N;P", LINES1]);
|
||
check_output!(pattern_delete_to_newline, ["-n", r"2N;3p;3D;3p", LINES1]);
|
||
check_output!(pattern_delete_no_newline, ["-e", r"2D", LINES1]);
|
||
check_output!(pattern_delete_print, ["-n", r"4d;p", LINES1]);
|
||
|
||
// FreeBSD sed does not produce any output for the following two
|
||
check_output!(pattern_append_delete, ["-e", r"N;N;N;D", LINES1]);
|
||
check_output!(pattern_append_delete_2, ["-e", r"N;N;N;D", LINES1, LINES2]);
|
||
|
||
check_output!(
|
||
pattern_append_delete2_separate,
|
||
["-s", r"N;N;N;D", LINES1, LINES2]
|
||
);
|
||
check_output!(
|
||
pattern_hold_append_swap,
|
||
["-e", r"2h;3H;4g;5G;6x;6p;6x;6p", LINES1]
|
||
);
|
||
check_output!(
|
||
pattern_swap_separate,
|
||
["--separate", r"4x;6x", LINES1, LINES2]
|
||
);
|
||
check_output!(pattern_next_output, ["-e", r"4n", LINES1]);
|
||
check_output!(pattern_next_no_output, ["-n", "-e", r"4n", LINES1]);
|
||
check_output!(pattern_next_print_output, ["-e", r"4n;p", LINES1]);
|
||
check_output!(pattern_next_print_no_output, ["-n", "-e", r"4n;p", LINES1]);
|
||
check_output!(pattern_quit, [r"5q", LINES1]);
|
||
check_output!(pattern_quit_2, [r"5q", LINES1, LINES2]);
|
||
check_output!(pattern_re_reuse, ["-n", r"/_1/p;//p", LINES1]);
|
||
check_output!(pattern_subst_re_reuse, ["-n", r"/_1/p;s//-N/p", LINES1]);
|
||
|
||
#[test]
|
||
fn test_quit_exit_code() {
|
||
new_ucmd!()
|
||
.args(&["5q 42", LINES1])
|
||
.fails()
|
||
.code_is(42)
|
||
.stdout_is_fixture("output/pattern_quit");
|
||
}
|
||
|
||
#[test]
|
||
fn test_quit_now_exit_code() {
|
||
new_ucmd!()
|
||
.args(&["6Q 12", LINES1])
|
||
.fails()
|
||
.code_is(12)
|
||
.stdout_is_fixture("output/pattern_quit");
|
||
}
|
||
|
||
////////////////////////////////////////////////////////////
|
||
// Command blocks: {}
|
||
check_output!(
|
||
block_simple_range,
|
||
[
|
||
"-e",
|
||
r#"
|
||
4,12 {
|
||
s/^/^/
|
||
s/$/$/
|
||
s/_/T/
|
||
}"#,
|
||
LINES1
|
||
]
|
||
);
|
||
|
||
check_output!(
|
||
block_negative_range,
|
||
[
|
||
"-e",
|
||
r#"
|
||
4,12 !{
|
||
s/^/^/
|
||
s/$/$/
|
||
s/_/T/
|
||
}"#,
|
||
LINES1
|
||
]
|
||
);
|
||
|
||
check_output!(
|
||
block_negative_range_2,
|
||
[
|
||
"-e",
|
||
r#"
|
||
4,12 !{
|
||
s/^/^/
|
||
s/$/$/
|
||
s/_/T/
|
||
}"#,
|
||
LINES1,
|
||
LINES2
|
||
]
|
||
);
|
||
|
||
check_output!(
|
||
block_nested_selection,
|
||
[
|
||
"-e",
|
||
r#"
|
||
4,12 {
|
||
s/^/^/
|
||
/6/,/10/ {
|
||
s/$/$/
|
||
/8/ s/_/T/
|
||
}
|
||
}"#,
|
||
LINES1
|
||
]
|
||
);
|
||
|
||
check_output!(
|
||
block_nested_negative_selection,
|
||
[
|
||
"-e",
|
||
r#"
|
||
4,12 !{
|
||
s/^/^/
|
||
/6/,/10/ !{
|
||
s/$/$/
|
||
/8/ !s/_/T/
|
||
}
|
||
}"#,
|
||
LINES1
|
||
]
|
||
);
|
||
|
||
check_output!(
|
||
branch_plain,
|
||
[
|
||
"-n",
|
||
"-e",
|
||
r#"
|
||
b label4
|
||
:label3
|
||
s/^/label3_/p
|
||
b end
|
||
:label4
|
||
2,12b label1
|
||
b label2
|
||
:label1
|
||
s/^/label1_/p
|
||
b
|
||
:label2
|
||
s/^/label2_/p
|
||
b label3
|
||
:end
|
||
"#,
|
||
LINES1
|
||
]
|
||
);
|
||
|
||
////////////////////////////////////////////////////////////
|
||
// Branching: :, b, t
|
||
check_output!(
|
||
branch_conditional_simple,
|
||
[
|
||
"-n",
|
||
"-e",
|
||
r#"
|
||
s/l1_/l2_/
|
||
t ok
|
||
b
|
||
:ok
|
||
s/^/tested /p
|
||
"#,
|
||
LINES1,
|
||
LINES2
|
||
]
|
||
);
|
||
|
||
// SunOS and GNU sed behave as follows: lines 9-$ aren"#,t printed at all
|
||
check_output!(
|
||
branch_to_block,
|
||
[
|
||
"-n",
|
||
"-e",
|
||
r#"
|
||
5,8b inside
|
||
1,5 {
|
||
s/^/^/p
|
||
:inside
|
||
s/$/$/p
|
||
}
|
||
"#,
|
||
LINES1
|
||
]
|
||
);
|
||
|
||
// Check that t clears the substitution done flag
|
||
check_output!(
|
||
branch_test_clears,
|
||
[
|
||
"-n",
|
||
"-e",
|
||
r#"
|
||
1,8s/^/^/
|
||
t l1
|
||
:l1
|
||
t l2
|
||
s/$/$/p
|
||
b
|
||
:l2
|
||
s/^/ERROR/
|
||
"#,
|
||
LINES1
|
||
]
|
||
);
|
||
|
||
// Check that reading a line clears the substitution done flag
|
||
check_output!(
|
||
branch_cycle_clears,
|
||
[
|
||
"-n",
|
||
"-e",
|
||
r#"
|
||
t l2
|
||
1,8s/^/^/p
|
||
2,7N
|
||
b
|
||
:l2
|
||
s/^/ERROR/p
|
||
"#,
|
||
LINES1
|
||
]
|
||
);
|
||
|
||
check_output!(
|
||
branch_conditional_boundary,
|
||
[
|
||
"-e",
|
||
r#"
|
||
{
|
||
:b
|
||
}
|
||
s/l/m/
|
||
tb"#,
|
||
LINES1
|
||
]
|
||
);
|
||
|
||
////////////////////////////////////////////////////////////
|
||
// Text: a, c, i
|
||
|
||
// Check both POSIX and GNU parsing routines.
|
||
|
||
check_output!(
|
||
text_simple_insert,
|
||
[
|
||
"-e",
|
||
r#"
|
||
2,4i\
|
||
extra
|
||
"#,
|
||
LINES1
|
||
]
|
||
);
|
||
|
||
check_output!(
|
||
text_simple_append,
|
||
[
|
||
"-e",
|
||
r#"
|
||
2,4a\
|
||
extra
|
||
"#,
|
||
LINES1
|
||
]
|
||
);
|
||
|
||
check_output_posix!(
|
||
text_insert_quit,
|
||
[
|
||
"-e",
|
||
r#"
|
||
5i\
|
||
hello
|
||
5q
|
||
"#,
|
||
LINES1
|
||
]
|
||
);
|
||
|
||
check_output_posix!(
|
||
text_insert_between_subst,
|
||
[
|
||
"-n",
|
||
"-e",
|
||
r#"
|
||
s/^/before_i/p
|
||
20i\
|
||
inserted
|
||
s/^/after_i/p
|
||
"#,
|
||
LINES1,
|
||
LINES2
|
||
]
|
||
);
|
||
|
||
check_output_posix!(
|
||
text_append_between_subst,
|
||
[
|
||
"-n",
|
||
"-e",
|
||
r#"
|
||
5,12s/^/5-12/
|
||
s/^/before_a/p
|
||
/5-12/a\
|
||
appended
|
||
s/^/after_a/p
|
||
"#,
|
||
LINES1,
|
||
LINES2
|
||
]
|
||
);
|
||
|
||
check_output_posix!(
|
||
text_append_before_next,
|
||
[
|
||
"-n",
|
||
"-e",
|
||
r#"
|
||
s/^/^/p
|
||
/l1_/a\
|
||
appended
|
||
8,10N
|
||
s/$/$/p
|
||
"#,
|
||
LINES1,
|
||
LINES2
|
||
]
|
||
);
|
||
|
||
check_output_posix!(
|
||
text_change_global,
|
||
[
|
||
"-n",
|
||
"-e",
|
||
r#"
|
||
c\
|
||
hello
|
||
"#,
|
||
LINES1
|
||
]
|
||
);
|
||
|
||
check_output_posix!(
|
||
text_change_line,
|
||
[
|
||
"-e",
|
||
r#"
|
||
8c\
|
||
hello
|
||
"#,
|
||
LINES1
|
||
]
|
||
);
|
||
|
||
check_output_posix!(
|
||
text_change_range,
|
||
[
|
||
"-e",
|
||
r#"
|
||
3,14c\
|
||
hello
|
||
"#,
|
||
LINES1
|
||
]
|
||
);
|
||
|
||
check_output_posix!(
|
||
text_change_reverse_range,
|
||
[
|
||
"-e",
|
||
r#"
|
||
8,3c\
|
||
hello
|
||
"#,
|
||
LINES1
|
||
]
|
||
);
|
||
|
||
check_output!(text_delete, ["d", LINES1]);
|
||
|
||
// Check that the pattern space is deleted.
|
||
check_output_posix!(
|
||
text_change_print,
|
||
[
|
||
"-n",
|
||
"-e",
|
||
r#"
|
||
c\
|
||
changed
|
||
p
|
||
"#,
|
||
LINES1
|
||
]
|
||
);
|
||
|
||
// GNU syntax extensions:
|
||
// Text can follow the initial \.
|
||
// Character escapes are supported.
|
||
// Invalid escapes result in the escaped character.
|
||
check_output!(
|
||
text_insert_gnu,
|
||
["-e", "i\\>\\h\\elll\x08o\\nto\\\nall\\a", LINES1]
|
||
);
|
||
|
||
////////////////////////////////////////////////////////////
|
||
// r, w commands
|
||
check_output!(read_ok, [format!("4r {LINES2}"), LINES1.to_string()]);
|
||
check_output!(read_missing, ["5r /xyzzyxyzy42", LINES1]);
|
||
check_output!(read_empty, ["6r input/empty", LINES1]);
|
||
|
||
#[test]
|
||
fn write_single_file() -> std::io::Result<()> {
|
||
let temp = NamedTempFile::new()?;
|
||
let cmd = format!("3,12w {}", temp.path().display());
|
||
|
||
new_ucmd!().args(&["-e", &cmd, LINES1]).succeeds();
|
||
|
||
let mut actual = String::new();
|
||
temp.reopen()?.read_to_string(&mut actual)?;
|
||
|
||
let expected = fs::read_to_string("tests/fixtures/sed/output/write_single_file")?;
|
||
assert_eq!(actual, expected, "Output did not match fixture");
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn write_two_files() -> std::io::Result<()> {
|
||
let temp1 = NamedTempFile::new()?;
|
||
let temp2 = NamedTempFile::new()?;
|
||
let cmd = format!(
|
||
"3,12w {}\n1,2w {}",
|
||
temp1.path().display(),
|
||
temp2.path().display()
|
||
);
|
||
|
||
new_ucmd!().args(&["-e", &cmd, LINES1]).succeeds();
|
||
|
||
let mut actual = String::new();
|
||
|
||
temp1.reopen()?.read_to_string(&mut actual)?;
|
||
let expected = fs::read_to_string("tests/fixtures/sed/output/write_two_files_1")?;
|
||
assert_eq!(actual, expected, "Output 1 did not match fixture");
|
||
|
||
actual.clear();
|
||
|
||
temp2.reopen()?.read_to_string(&mut actual)?;
|
||
let expected = fs::read_to_string("tests/fixtures/sed/output/write_two_files_2")?;
|
||
assert_eq!(actual, expected, "Output 2 did not match fixture");
|
||
|
||
Ok(())
|
||
}
|
||
|
||
////////////////////////////////////////////////////////////
|
||
// =, l commands
|
||
check_output!(number_continuous, ["/l2_/=", LINES1, LINES2]);
|
||
check_output!(number_separate, ["-s", "/l._8/=", LINES1, LINES2]);
|
||
check_output!(number_range, ["-e", "10,12=", LINES1]);
|
||
check_output!(number_range_out_of_bounds, ["-e", "47,60=", LINES1]);
|
||
|
||
check_output!(list_ascii, ["-n", "l 60", "input/ascii"]);
|
||
check_output!(list_empty, ["-n", "l 60", "input/empty"]);
|
||
check_output!(list_unicode, ["l 60", "input/unicode"]);
|
||
|
||
////////////////////////////////////////////////////////////
|
||
// In-place editing
|
||
#[test]
|
||
fn in_place_edit_replace() -> std::io::Result<()> {
|
||
let mut temp = NamedTempFile::new()?;
|
||
writeln!(temp.as_file_mut(), "hello, world")?;
|
||
|
||
// Get the file path before converting to TempPath
|
||
let path = temp.path().to_path_buf();
|
||
|
||
// Close temp file and preserve path
|
||
let temp_path = temp.into_temp_path();
|
||
|
||
// Call your tool on the path
|
||
new_ucmd!()
|
||
.args(&["-i", "-e", "s/world/universe/", path.to_str().unwrap()])
|
||
.succeeds();
|
||
|
||
// Read the file using standard fs
|
||
let actual = std::fs::read_to_string(&path)?;
|
||
temp_path.close()?; // Clean up
|
||
|
||
assert_eq!(actual, "hello, universe\n");
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn in_place_edit_backup() -> std::io::Result<()> {
|
||
let mut temp = NamedTempFile::new()?;
|
||
writeln!(temp.as_file_mut(), "hello, world")?;
|
||
|
||
let path = temp.path().to_path_buf();
|
||
let temp_path = temp.into_temp_path();
|
||
|
||
// Run the sed-like command with -i (in-place edit)
|
||
new_ucmd!()
|
||
.args(&[
|
||
"-i",
|
||
".bak",
|
||
"-e",
|
||
"s/world/universe/",
|
||
path.to_str().unwrap(),
|
||
])
|
||
.succeeds();
|
||
|
||
// Read edited file
|
||
let actual = std::fs::read_to_string(&path)?;
|
||
assert_eq!(actual, "hello, universe\n");
|
||
|
||
// Read backup file
|
||
let backup_path = path.with_file_name(format!(
|
||
"{}.bak",
|
||
path.file_name().unwrap().to_string_lossy()
|
||
));
|
||
let backup = std::fs::read_to_string(&backup_path)?;
|
||
assert_eq!(backup, "hello, world\n");
|
||
|
||
temp_path.close()?; // Cleanup
|
||
std::fs::remove_file(backup_path)?; // Cleanup backup
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[cfg(unix)]
|
||
#[test]
|
||
fn in_place_edit_follow_symlink_edits_target() -> Result<(), Box<dyn std::error::Error>> {
|
||
let temp_dir = assert_fs::TempDir::new()?;
|
||
let target = temp_dir.child("target.txt");
|
||
let link = temp_dir.child("link.txt");
|
||
|
||
target.write_str("hello, world\n")?;
|
||
|
||
std::os::unix::fs::symlink(target.path(), link.path())?;
|
||
|
||
new_ucmd!()
|
||
.args(&[
|
||
"--follow-symlinks",
|
||
"-i",
|
||
"-e",
|
||
"s/world/universe/",
|
||
link.path().to_str().unwrap(),
|
||
])
|
||
.succeeds();
|
||
|
||
let actual = std::fs::read_to_string(target.path())?;
|
||
assert_eq!(actual, "hello, universe\n");
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[cfg(unix)]
|
||
#[test]
|
||
fn in_place_edit_symlink_replaced_when_not_following() -> Result<(), Box<dyn std::error::Error>> {
|
||
let temp_dir = assert_fs::TempDir::new()?;
|
||
let target = temp_dir.child("target.txt");
|
||
let link = temp_dir.child("link.txt");
|
||
|
||
target.write_str("hello, world\n")?;
|
||
|
||
std::os::unix::fs::symlink(target.path(), link.path())?;
|
||
|
||
// Run command without --follow-symlinks
|
||
new_ucmd!()
|
||
.args(&[
|
||
"-i",
|
||
"-e",
|
||
"s/world/universe/",
|
||
link.path().to_str().unwrap(),
|
||
])
|
||
.succeeds();
|
||
|
||
// The original target should be untouched
|
||
let original = std::fs::read_to_string(target.path())?;
|
||
assert_eq!(original, "hello, world\n");
|
||
|
||
// The symlink path should now contain the edited content
|
||
let edited = std::fs::read_to_string(link.path())?;
|
||
assert_eq!(edited, "hello, universe\n");
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[cfg(unix)]
|
||
#[test]
|
||
fn in_place_edit_follow_symlink_with_backup() -> Result<(), Box<dyn std::error::Error>> {
|
||
let temp_dir = assert_fs::TempDir::new()?;
|
||
let target = temp_dir.child("target.txt");
|
||
let link = temp_dir.child("link.txt");
|
||
|
||
target.write_str("hello, world\n")?;
|
||
|
||
std::os::unix::fs::symlink(target.path(), link.path())?;
|
||
|
||
new_ucmd!()
|
||
.args(&[
|
||
"--follow-symlinks",
|
||
"-i",
|
||
".bak",
|
||
"-e",
|
||
"s/world/universe/",
|
||
link.path().to_str().unwrap(),
|
||
])
|
||
.succeeds();
|
||
|
||
// Verify target was modified
|
||
let edited = std::fs::read_to_string(target.path())?;
|
||
assert_eq!(edited, "hello, universe\n");
|
||
|
||
// Backup file is created alongside the target
|
||
let backup_path = target.path().with_file_name(format!(
|
||
"{}.bak",
|
||
target.file_name().unwrap().to_string_lossy()
|
||
));
|
||
let backup = std::fs::read_to_string(&backup_path)?;
|
||
assert_eq!(backup, "hello, world\n");
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[cfg(unix)]
|
||
#[test]
|
||
fn in_place_edit_symlink_replaced_with_backup() -> Result<(), Box<dyn std::error::Error>> {
|
||
let temp_dir = assert_fs::TempDir::new()?;
|
||
let target = temp_dir.child("target.txt");
|
||
let link = temp_dir.child("link.txt");
|
||
|
||
target.write_str("hello, world\n")?;
|
||
|
||
std::os::unix::fs::symlink(target.path(), link.path())?;
|
||
|
||
new_ucmd!()
|
||
.args(&[
|
||
"-i",
|
||
".bak",
|
||
"-e",
|
||
"s/world/universe/",
|
||
link.path().to_str().unwrap(),
|
||
])
|
||
.succeeds();
|
||
|
||
// Target should remain untouched
|
||
let unchanged = std::fs::read_to_string(target.path())?;
|
||
assert_eq!(unchanged, "hello, world\n");
|
||
|
||
// Symlink path should now contain the updated content
|
||
let edited = std::fs::read_to_string(link.path())?;
|
||
assert_eq!(edited, "hello, universe\n");
|
||
|
||
// Backup of the symlink file (not the target) should exist
|
||
let backup_path = link.path().with_file_name(format!(
|
||
"{}.bak",
|
||
link.file_name().unwrap().to_string_lossy()
|
||
));
|
||
let backup = std::fs::read_to_string(&backup_path)?;
|
||
assert_eq!(backup, "hello, world\n");
|
||
|
||
Ok(())
|
||
}
|
||
|
||
////////////////////////////////////////////////////////////
|
||
// Large complex scripts
|
||
|
||
// Math expression evaluation
|
||
check_output!(math1, ["-f", "script/math.sed", "input/expression1"]);
|
||
|
||
// Calculate π (scaled) to several decimal places
|
||
check_output!(pi, ["-f", "script/math.sed", "input/pi"]);
|
||
|
||
// Solve the Towers of Hanoi puzzle
|
||
check_output!(hanoi, ["-f", "script/hanoi.sed", "input/hanoi"]);
|
||
|
||
////////////////////////////////////////////////////////////
|
||
// Long-running scripts
|
||
// Test with cargo test -- --ignored
|
||
|
||
// Check the output of Bach's prelude in C major from WTC book I.
|
||
// Run with cargo test test_bach_prelude_matches -- --ignored.
|
||
#[test]
|
||
#[ignore] // Slow; produces 5.8 MB of raw audio.
|
||
fn test_bach_prelude_matches() {
|
||
let res = new_ucmd!()
|
||
.args(&["-E", "-f", "script/bach.sed"])
|
||
.pipe_in("\n")
|
||
.succeeds();
|
||
|
||
// Compare SHA-256 output against GNU sed output.
|
||
let mut hasher = Sha256::new();
|
||
hasher.update(res.stdout());
|
||
let digest = hasher.finalize();
|
||
|
||
let got = hex::encode(digest);
|
||
assert_eq!(
|
||
got,
|
||
"c4e50d6791a60692745e958dc48d43a40bccea2ce5cea31b7125a40604cc3219"
|
||
);
|
||
}
|
||
|
||
// Draw the Mandelbrot set.
|
||
#[ignore] // Slow; takes > 15" on an i7 CPU
|
||
#[test]
|
||
fn test_mandelbrod() {
|
||
new_ucmd!()
|
||
.args(&["-En", "-f", "script/mandelbrot.sed", "input/newline"])
|
||
.succeeds()
|
||
.stdout_is_fixture("output/mandelbrot");
|
||
}
|
||
|
||
////////////////////////////////////////////////////////////
|
||
// Error handling
|
||
#[test]
|
||
fn test_invalid_backreference() {
|
||
new_ucmd!()
|
||
.args(&["-n", "-e", r"s/./X/;s//\1/", LINES1])
|
||
.fails()
|
||
.code_is(2)
|
||
.stderr_is("sed: <script argument 1>:1:8: error: invalid reference \\1 on command's RHS\n");
|
||
}
|
||
|
||
#[test]
|
||
fn test_duplicate_label() {
|
||
new_ucmd!()
|
||
.args(&[":foo;:foo"])
|
||
.fails()
|
||
.code_is(1)
|
||
.stderr_is("sed: <script argument 1>:1:6: error: duplicate label `foo'\n");
|
||
}
|
||
|
||
#[test]
|
||
fn test_undefined_label() {
|
||
new_ucmd!()
|
||
.args(&["b foo"])
|
||
.fails()
|
||
.code_is(1)
|
||
.stderr_is("sed: <script argument 1>:1:1: error: undefined label `foo'\n");
|
||
}
|
||
|
||
#[test]
|
||
fn test_incomplete_test_command_posix() {
|
||
new_ucmd!()
|
||
.args(&["--posix", "i\\"])
|
||
.fails()
|
||
.code_is(1)
|
||
.stderr_is("sed: :0:3: error: incomplete command\n");
|
||
}
|
||
|
||
#[test]
|
||
fn test_empty_text_commands_fail() {
|
||
for command in ["a", "c", "i"] {
|
||
new_ucmd!()
|
||
.args(&["-e", command])
|
||
.fails()
|
||
.code_is(1)
|
||
.stderr_contains(format!("command `{command}' expects \\ followed by text"));
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_addr0_non_posix() {
|
||
new_ucmd!()
|
||
.args(&["--posix", "0,/foo/p"])
|
||
.fails()
|
||
.code_is(1)
|
||
.stderr_is("sed: <script argument 1>:1:2: error: address 0 is invalid in POSIX mode\n");
|
||
}
|
||
|
||
#[test]
|
||
fn test_addr0_second_required() {
|
||
new_ucmd!()
|
||
.args(&["0p"])
|
||
.fails()
|
||
.code_is(1)
|
||
.stderr_is("sed: <script argument 1>:1:2: error: address 0 requires a second address\n");
|
||
}
|
||
|
||
#[test]
|
||
fn test_addr0_second_re_only() {
|
||
new_ucmd!()
|
||
.args(&["0,4p"])
|
||
.fails()
|
||
.code_is(1)
|
||
.stderr_is("sed: <script argument 1>:1:4: error: address 0 can only be used with a regular expression or ~step\n");
|
||
}
|
||
|
||
#[test]
|
||
fn test_step_match_non_posix() {
|
||
new_ucmd!()
|
||
.args(&["--posix", "3~2p"])
|
||
.fails()
|
||
.code_is(1)
|
||
.stderr_is("sed: <script argument 1>:1:3: error: ~step is invalid in POSIX mode\n");
|
||
}
|
||
|
||
#[test]
|
||
fn test_step_end_non_posix() {
|
||
new_ucmd!()
|
||
.args(&["--posix", "3,~2p"])
|
||
.fails()
|
||
.code_is(1)
|
||
.stderr_is("sed: <script argument 1>:1:4: error: ~step is invalid in POSIX mode\n");
|
||
}
|
||
|
||
// The following test diverse ways in which regexes are matched.
|
||
// Search for 'regex\.' to find them in the code.
|
||
#[test]
|
||
fn test_fancy_regex_is_match_error() {
|
||
new_ucmd!()
|
||
.args(&["-E", r"/(\.+)+\1b$/p", "input/dots-4k.txt"])
|
||
.fails()
|
||
.code_is(2)
|
||
.stderr_is("sed: <script argument 1>:1:1: 'input/dots-4k.txt':1 error: Error executing regex: Max limit for backtracking count exceeded\n");
|
||
}
|
||
|
||
#[test]
|
||
fn test_fancy_regex_find_error() {
|
||
new_ucmd!()
|
||
.args(&["-E", r"p;s/(\.+)+\1b$/X/", "input/dots-4k.txt"])
|
||
.fails()
|
||
.code_is(2)
|
||
.stderr_is("sed: <script argument 1>:1:3: 'input/dots-4k.txt':1 error: Error executing regex: Max limit for backtracking count exceeded\n");
|
||
}
|
||
|
||
#[test]
|
||
fn test_fancy_regex_captures_error() {
|
||
new_ucmd!()
|
||
.args(&["-E", r"p;s/(\.+)+\1b$/\1/", "input/dots-4k.txt"])
|
||
.fails()
|
||
.code_is(2)
|
||
.stderr_is("sed: <script argument 1>:1:3: 'input/dots-4k.txt':1 error: Error executing regex: Max limit for backtracking count exceeded\n");
|
||
}
|
||
|
||
#[test]
|
||
fn test_fancy_regex_captures_iter_error() {
|
||
new_ucmd!()
|
||
.args(&["-E", r"p;s/(\.+)+\1b$/\1/3", "input/dots-4k.txt"])
|
||
.fails()
|
||
.code_is(2)
|
||
.stderr_is("sed: <script argument 1>:1:3: 'input/dots-4k.txt':1 error: error retrieving RE captures: Error executing regex: Max limit for backtracking count exceeded\n");
|
||
}
|
||
|
||
#[test]
|
||
fn test_write_file_failure() {
|
||
new_ucmd!()
|
||
.args(&["w /xyzzy/xyzy", LINES1])
|
||
.fails()
|
||
.code_is(2)
|
||
.stderr_contains("sed: <script argument 1>:1:1: error: creating file '/xyzzy/xyzy':");
|
||
}
|
||
|
||
#[test]
|
||
fn test_missing_substitute_re() {
|
||
new_ucmd!()
|
||
.args(&["l;s//foo/", LINES1])
|
||
.fails()
|
||
.code_is(2)
|
||
.stderr_is("sed: <script argument 1>:1:3: 'input/lines1':1 error: no previous regular expression\n");
|
||
}
|
||
|
||
#[test]
|
||
fn test_missing_address_re() {
|
||
new_ucmd!()
|
||
.args(&["l\np;//s/foo/bar/", LINES1])
|
||
.fails()
|
||
.code_is(2)
|
||
.stderr_is("sed: <script argument 1>:2:3: 'input/lines1':1 error: no previous regular expression\n");
|
||
}
|
||
|
||
////////////////////////////////////////////////////////////
|
||
// issue #143: p with no trailing newline
|
||
#[test]
|
||
fn test_print_command_adds_newline() {
|
||
new_ucmd!()
|
||
.args(&["-e", "p"])
|
||
.pipe_in("foo")
|
||
.succeeds()
|
||
.stdout_is("foo\nfoo");
|
||
}
|
||
|
||
#[test]
|
||
fn test_print_command_multiline_no_newline() {
|
||
new_ucmd!()
|
||
.args(&["-e", "p"])
|
||
.pipe_in("a\nfoo")
|
||
.succeeds()
|
||
.stdout_is("a\na\nfoo\nfoo");
|
||
}
|
||
|
||
// -n p must not add a trailing newline when input has none
|
||
#[test]
|
||
fn test_print_command_silent_no_newline() {
|
||
new_ucmd!()
|
||
.args(&["-n", "p"])
|
||
.pipe_in("foo")
|
||
.succeeds()
|
||
.stdout_is("foo");
|
||
}
|
||
|
||
// sanity: normal newline-terminated input is unaffected
|
||
#[test]
|
||
fn test_print_command_with_newline() {
|
||
new_ucmd!()
|
||
.args(&["-e", "p"])
|
||
.pipe_in("foo\n")
|
||
.succeeds()
|
||
.stdout_is("foo\nfoo\n");
|
||
}
|
||
|
||
// issue #254: 2x missing newline
|
||
#[test]
|
||
fn test_exchange_command_adds_newline() {
|
||
new_ucmd!()
|
||
.args(&["2x"])
|
||
.pipe_in("a\nb\nc\n")
|
||
.succeeds()
|
||
.stdout_is("a\n\nc\n");
|
||
}
|
||
|
||
// issue #306: 1x with no-newline input should output an empty line
|
||
#[test]
|
||
fn test_exchange_no_newline_outputs_empty_line() {
|
||
new_ucmd!()
|
||
.args(&["1x"])
|
||
.pipe_in("abc")
|
||
.succeeds()
|
||
.stdout_is("\n");
|
||
}
|
||
|
||
// q with no newline input must not drop the trailing newline
|
||
#[test]
|
||
fn test_quit_no_newline() {
|
||
new_ucmd!()
|
||
.args(&["q"])
|
||
.pipe_in("foo")
|
||
.succeeds()
|
||
.stdout_is("foo\n");
|
||
}
|
||
|
||
// P with single line no newline input
|
||
#[test]
|
||
fn test_print_first_line_no_newline() {
|
||
new_ucmd!()
|
||
.args(&["-n", "P"])
|
||
.pipe_in("foo")
|
||
.succeeds()
|
||
.stdout_is("foo");
|
||
}
|