Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 22 additions & 18 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,35 +33,25 @@ There are several ways to contribute to posixutils-rs:
Small, lightweight utility with command line processing,
core algorithm, and zero external crate dependencies.
3. "only std" When an external crate is required, avoid mega-crates. Prefer
std-only, or, tiny crates such as `atty` that perform a single,
std-only, or, tiny crates such as `tempfile` that perform a single,
lightweight function.
4. Correctness, readability, performance, in that order.
Code should be readable by unfamiliar developers. Avoid dense,
uncommented code.
5. Write small functions. Break up large functions.
The compiler can inline as needed.

### Testing, POSIX compliance and programmatic goals

* All utilities should have tests.
* Use plib's TestPlan framework for integration tests.
* Test pattern:
1. Pre-generate input data files.
2. Run system OS utility on input data files,
generating known-good output.
3. Store input and output in-tree, as known-good data.
4. Write a TestPlan that executes our utility, using
static input data, and compares output with
static output data.
* Only "quick" tests should be run automatically in `cargo test`
* Longer tests, or tests requiring root access, should be triggered
via special environment variables.
* POSIX compliance
* Support the most widely used GNU/BSD extensions
* If a system has an OS-specific feature that _must_ be
exposed through a given utility, do so.
* Race-free userland. See `ftw` internal crate.
* Full integration test coverage
* Support widely used GNU/BSD extensions
* Race-free userland. (See `ftw` internal crate.)
* Push small crates out: Create tiny, light-dep crates from common
functionality (such as Lex or Yacc file parsing), and publish via cargo.
Remove from main posixutils tree and set of crates.
* If a system has an OS-specific feature that _must_ be
exposed through a given utility, do so.

### Testing and Bug Reporting: Info to provide in GH issue

Expand All @@ -70,3 +60,17 @@ There are several ways to contribute to posixutils-rs:
* Provide any error output from the utility.
* Describe expected results: What did you expect to happen, and did not?

### Writing tests

* Test pattern:
1. Pre-generate input data files.
2. Run system OS utility on input data files,
generating known-good output.
3. Store input and output in-tree, as known-good data.
4. Write a TestPlan that executes our utility, using
static input data, and compares output with
static output data (OS reference data).
* Use plib's TestPlan framework for integration tests.
* Only "quick" tests should be run automatically in `cargo test`
* Longer tests, or tests requiring root access, should be triggered
via special environment variables.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ Because it is a FAQ, the major differences between this project and uutils are:
## Stage 3 - Test coverage

- [x] ar (Development)
- [x] asa
- [x] awk
- [x] basename
- [x] bc
Expand All @@ -70,6 +71,7 @@ Because it is a FAQ, the major differences between this project and uutils are:
- [x] cut
- [x] diff
- [x] dirname
- [x] echo
- [x] ex (Editors)
- [x] expand
- [x] expr
Expand Down Expand Up @@ -97,6 +99,7 @@ Because it is a FAQ, the major differences between this project and uutils are:
- [x] paste
- [x] pax
- [x] pr
- [x] printf
- [x] readlink
- [x] realpath
- [x] rm
Expand All @@ -108,6 +111,7 @@ Because it is a FAQ, the major differences between this project and uutils are:
- [x] strings
- [x] strip (Development)
- [x] tail
- [x] test
- [x] time
- [x] timeout
- [x] tr
Expand Down Expand Up @@ -158,22 +162,18 @@ Because it is a FAQ, the major differences between this project and uutils are:

## Stage 1 - Rough draft

- [x] asa
- [x] cal
- [x] df
- [x] du
- [x] echo
- [x] dd
- [x] getconf
- [x] id
- [x] ipcs (IPC)
- [x] kill
- [x] logger
- [x] printf
- [x] ps
- [x] stty
- [x] tabs
- [x] test
- [x] tput
- [x] tsort
- [x] who
Expand Down
2 changes: 1 addition & 1 deletion clippy.toml
Original file line number Diff line number Diff line change
@@ -1 +1 @@
msrv = "1.80.0"
msrv = "1.84.0"
95 changes: 67 additions & 28 deletions display/echo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,65 +6,104 @@
// file in the root directory of this project.
// SPDX-License-Identifier: MIT
//
// TODO:
// - echo needs to translate backslash-escaped octal numbers:
// ```
// \0num
// Write an 8-bit value that is the 0, 1, 2 or 3-digit octal number _num_.
//

use gettextrs::{bind_textdomain_codeset, setlocale, textdomain, LocaleCategory};
use std::io::{self, Write};

fn translate_str(skip_nl: bool, s: &str) -> String {
let mut output = String::with_capacity(s.len());

let mut in_bs = false;
fn translate_str(skip_nl: bool, s: &str) -> Vec<u8> {
let mut output = Vec::with_capacity(s.len());
let mut nl = true;

for ch in s.chars() {
let chars: Vec<char> = s.chars().collect();
let mut i = 0;

while i < chars.len() {
let ch = chars[i];
if ch == '\\' {
in_bs = true;
} else if in_bs {
in_bs = false;
match ch {
if i + 1 >= chars.len() {
// Trailing backslash - preserve it
output.push(b'\\');
i += 1;
continue;
}

let next = chars[i + 1];
match next {
'a' => {
output.push('\x07');
output.push(0x07);
i += 2;
}
'b' => {
output.push('\x08');
output.push(0x08);
i += 2;
}
'c' => {
nl = false;
break;
}
'f' => {
output.push('\x12');
output.push(0x0c);
i += 2;
}
'n' => {
output.push('\n');
output.push(b'\n');
i += 2;
}
'r' => {
output.push('\r');
output.push(b'\r');
i += 2;
}
't' => {
output.push('\t');
output.push(b'\t');
i += 2;
}
'v' => {
output.push('\x11');
output.push(0x0b);
i += 2;
}
'\\' => {
output.push('\\');
output.push(b'\\');
i += 2;
}
'0' => {
// Octal escape: \0num where num is 0-3 octal digits
i += 2; // Skip \0
let mut octal_value: u8 = 0;
let mut digits = 0;

while digits < 3 && i < chars.len() {
let digit = chars[i];
if ('0'..='7').contains(&digit) {
octal_value = octal_value * 8 + (digit as u8 - b'0');
i += 1;
digits += 1;
} else {
break;
}
}
// Push raw byte value directly - octal escapes produce single bytes
output.push(octal_value);
}
_ => {
// Unknown escape - preserve the character after backslash
// Encode the char as UTF-8 bytes
let mut buf = [0u8; 4];
let encoded = next.encode_utf8(&mut buf);
output.extend_from_slice(encoded.as_bytes());
i += 2;
}
_ => {}
}
} else {
output.push(ch);
// Encode the char as UTF-8 bytes
let mut buf = [0u8; 4];
let encoded = ch.encode_utf8(&mut buf);
output.extend_from_slice(encoded.as_bytes());
i += 1;
}
}

if nl && !skip_nl {
output.push('\n');
output.push(b'\n');
}

output
Expand All @@ -87,9 +126,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}
};

let echo_str = translate_str(skip_nl, &args.join(" "));
let echo_bytes = translate_str(skip_nl, &args.join(" "));

io::stdout().write_all(echo_str.as_bytes())?;
io::stdout().write_all(&echo_bytes)?;

Ok(())
}
Loading