Skip to content

Commit 75f9d99

Browse files
authored
Merge pull request #426 from rustcoreutils/updates
asa: rewrite and add tests
2 parents 04f3bd1 + b7c5575 commit 75f9d99

File tree

3 files changed

+217
-74
lines changed

3 files changed

+217
-74
lines changed

text/asa.rs

Lines changed: 52 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,12 @@
66
// file in the root directory of this project.
77
// SPDX-License-Identifier: MIT
88
//
9-
// TODO:
10-
// - fix correctness
11-
// - add tests
12-
//
139

1410
use std::io::{self, BufRead};
1511
use std::path::PathBuf;
1612

1713
use clap::Parser;
18-
use gettextrs::{bind_textdomain_codeset, gettext, setlocale, textdomain, LocaleCategory};
14+
use gettextrs::{bind_textdomain_codeset, setlocale, textdomain, LocaleCategory};
1915
use plib::io::input_reader;
2016

2117
/// asa - interpret carriage-control characters
@@ -26,102 +22,84 @@ struct Args {
2622
files: Vec<PathBuf>,
2723
}
2824

29-
struct AsaState {
30-
first_line: bool,
31-
lines: Vec<String>,
32-
}
33-
34-
impl Default for AsaState {
35-
fn default() -> Self {
36-
Self {
37-
first_line: true,
38-
lines: Default::default(),
39-
}
40-
}
41-
}
42-
43-
impl AsaState {
44-
fn push(&mut self, line: &str) {
45-
self.lines.push(line.to_string());
46-
if self.first_line {
47-
self.first_line = false;
48-
}
49-
}
50-
51-
fn formfeed(&mut self) {
52-
if !self.first_line {
53-
print!("\x0c"); // formfeed
54-
}
55-
}
56-
57-
fn flush(&mut self) {
58-
let mut nl = String::new();
59-
for line in &self.lines {
60-
print!("{}{}", nl, line);
61-
62-
// do not prefix with newline on first line
63-
if nl.is_empty() {
64-
nl = "\n".to_string();
65-
}
66-
}
67-
68-
self.lines.clear();
69-
}
70-
}
71-
7225
fn asa_file(pathname: &PathBuf) -> io::Result<()> {
7326
let mut reader = input_reader(pathname, false)?;
74-
let mut line_no: usize = 0;
75-
let mut state = AsaState::default();
27+
let mut first_line = true;
28+
let mut had_output = false;
7629

7730
loop {
78-
line_no += 1;
79-
8031
let mut raw_line = String::new();
8132
let n_read = reader.read_line(&mut raw_line)?;
8233
if n_read == 0 {
8334
break;
8435
}
8536

86-
if raw_line.len() < 2 {
87-
eprintln!("{} {}", gettext("malformed line"), line_no);
88-
continue;
89-
}
37+
// Get first character as control character
38+
let ch = match raw_line.chars().next() {
39+
Some(c) => c,
40+
None => continue, // empty line shouldn't happen, but handle gracefully
41+
};
9042

91-
let ch = raw_line.chars().next().unwrap();
43+
// Extract line content: skip first char, exclude trailing newline
44+
let line_end = if raw_line.ends_with('\n') {
45+
raw_line.len() - 1
46+
} else {
47+
raw_line.len()
48+
};
49+
let line = if line_end > 1 {
50+
&raw_line[1..line_end]
51+
} else {
52+
"" // control char only, no content
53+
};
9254

93-
// exclude first char, and trailing newline
94-
let mut line_len = raw_line.len() - 1;
95-
if raw_line.ends_with('\n') {
96-
line_len -= 1;
97-
}
98-
let line = &raw_line[1..line_len];
55+
// POSIX: '+' as first character in input is equivalent to space
56+
let effective_ch = if first_line && ch == '+' { ' ' } else { ch };
9957

100-
match ch {
58+
match effective_ch {
10159
'+' => {
102-
state.push(line);
60+
// Overprint: return to column 1 of current line
61+
print!("\r{}", line);
10362
}
10463
'0' => {
105-
state.flush();
64+
// Double-space: newline before content (blank line)
65+
if !first_line {
66+
println!();
67+
}
10668
println!();
107-
state.push(line);
69+
print!("{}", line);
10870
}
10971
'-' => {
110-
state.flush();
72+
// Triple-space (non-POSIX extension): two blank lines before
73+
if !first_line {
74+
println!();
75+
}
11176
println!();
11277
println!();
113-
state.push(line);
78+
print!("{}", line);
11479
}
11580
'1' => {
116-
state.flush();
117-
state.formfeed();
118-
state.push(line);
81+
// New page: form-feed
82+
if !first_line {
83+
println!();
84+
}
85+
print!("\x0c{}", line);
11986
}
12087
_ => {
121-
state.flush();
122-
state.push(line);
88+
// Space and other chars: normal single-spaced output
89+
if !first_line {
90+
println!();
91+
}
92+
print!("{}", line);
12393
}
12494
};
95+
96+
first_line = false;
97+
had_output = true;
98+
}
99+
100+
// Final newline if we had any output
101+
if had_output {
102+
println!();
125103
}
126104

127105
Ok(())

text/tests/asa/mod.rs

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
//
2+
// Copyright (c) 2024 Hemi Labs, Inc.
3+
//
4+
// This file is part of the posixutils-rs project covered under
5+
// the MIT License. For the full license text, please see the LICENSE
6+
// file in the root directory of this project.
7+
// SPDX-License-Identifier: MIT
8+
//
9+
10+
use plib::testing::{run_test, TestPlan};
11+
12+
fn asa_test(test_data: &str, expected_output: &str) {
13+
run_test(TestPlan {
14+
cmd: String::from("asa"),
15+
args: vec![],
16+
stdin_data: String::from(test_data),
17+
expected_out: String::from(expected_output),
18+
expected_err: String::from(""),
19+
expected_exit_code: 0,
20+
});
21+
}
22+
23+
// Test empty input
24+
#[test]
25+
fn asa_empty() {
26+
asa_test("", "");
27+
}
28+
29+
// Test basic space control character (normal single-spacing)
30+
#[test]
31+
fn asa_space_single_line() {
32+
asa_test(" hello\n", "hello\n");
33+
}
34+
35+
#[test]
36+
fn asa_space_multiple_lines() {
37+
asa_test(" line1\n line2\n line3\n", "line1\nline2\nline3\n");
38+
}
39+
40+
// Test '0' control character (double-spacing - blank line before)
41+
#[test]
42+
fn asa_zero_first_line() {
43+
// '0' as first line: outputs blank line, then content
44+
asa_test("0hello\n", "\nhello\n");
45+
}
46+
47+
#[test]
48+
fn asa_zero_second_line() {
49+
// '0' on second line: previous line, blank line, then content
50+
asa_test(" line1\n0line2\n", "line1\n\nline2\n");
51+
}
52+
53+
#[test]
54+
fn asa_zero_multiple() {
55+
asa_test("0first\n0second\n", "\nfirst\n\nsecond\n");
56+
}
57+
58+
// Test '1' control character (form-feed/new page)
59+
#[test]
60+
fn asa_one_first_line() {
61+
// '1' as first line: form-feed, then content
62+
asa_test("1page1\n", "\x0cpage1\n");
63+
}
64+
65+
#[test]
66+
fn asa_one_second_line() {
67+
// '1' on second line: previous line ends, form-feed, then content
68+
asa_test(" line1\n1page2\n", "line1\n\x0cpage2\n");
69+
}
70+
71+
// Test '+' control character (overprint - carriage return)
72+
#[test]
73+
fn asa_plus_overprint() {
74+
// '+' causes overprint: carriage return instead of newline
75+
asa_test(" line1\n+over\n", "line1\rover\n");
76+
}
77+
78+
#[test]
79+
fn asa_plus_multiple_overprint() {
80+
// Multiple overprints on same logical line
81+
asa_test(" base\n+mid\n+top\n", "base\rmid\rtop\n");
82+
}
83+
84+
#[test]
85+
fn asa_plus_first_line() {
86+
// POSIX: '+' as first character in input is equivalent to space
87+
asa_test("+first\n", "first\n");
88+
}
89+
90+
#[test]
91+
fn asa_plus_first_line_then_normal() {
92+
// '+' as first, then normal lines
93+
asa_test("+first\n line2\n", "first\nline2\n");
94+
}
95+
96+
// Test '-' control character (triple-spacing - non-POSIX extension)
97+
#[test]
98+
fn asa_dash_first_line() {
99+
asa_test("-content\n", "\n\ncontent\n");
100+
}
101+
102+
#[test]
103+
fn asa_dash_second_line() {
104+
asa_test(" line1\n-line2\n", "line1\n\n\nline2\n");
105+
}
106+
107+
// Test other/unknown control characters (treated as space)
108+
#[test]
109+
fn asa_other_char() {
110+
// Unknown control chars treated as space (normal single-spacing)
111+
asa_test("Xhello\n", "hello\n");
112+
}
113+
114+
#[test]
115+
fn asa_digit_as_control() {
116+
// '2' is not a special control, treated as space
117+
asa_test("2hello\n", "hello\n");
118+
}
119+
120+
// Test empty content (control char only)
121+
#[test]
122+
fn asa_space_empty_content() {
123+
asa_test(" \n", "\n");
124+
}
125+
126+
#[test]
127+
fn asa_zero_empty_content() {
128+
asa_test("0\n", "\n\n");
129+
}
130+
131+
// Test mixed control characters
132+
#[test]
133+
fn asa_mixed_controls() {
134+
asa_test(
135+
" line1\n0double\n1newpage\n+over\n line2\n",
136+
"line1\n\ndouble\n\x0cnewpage\rover\nline2\n",
137+
);
138+
}
139+
140+
// Test content without trailing newline (EOF without newline)
141+
#[test]
142+
fn asa_no_trailing_newline() {
143+
asa_test(" hello", "hello\n");
144+
}
145+
146+
#[test]
147+
fn asa_plus_no_trailing_newline() {
148+
asa_test(" line1\n+over", "line1\rover\n");
149+
}
150+
151+
// Test lines with only control character (no content, no newline)
152+
#[test]
153+
fn asa_control_only_no_newline() {
154+
asa_test(" ", "\n");
155+
}
156+
157+
// Test complex FORTRAN-style output simulation
158+
#[test]
159+
fn asa_fortran_style_report() {
160+
// Simulate a simple FORTRAN report with page header and data
161+
let input = "1REPORT TITLE\n \n DATA LINE 1\n DATA LINE 2\n0SECTION 2\n DATA LINE 3\n";
162+
let expected = "\x0cREPORT TITLE\n\nDATA LINE 1\nDATA LINE 2\n\nSECTION 2\nDATA LINE 3\n";
163+
asa_test(input, expected);
164+
}

text/tests/text-tests.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
// SPDX-License-Identifier: MIT
88
//
99

10+
mod asa;
1011
mod comm;
1112
mod csplit;
1213
mod cut;

0 commit comments

Comments
 (0)