Skip to content

Commit 1305006

Browse files
committed
[ed] continue posix compliance work
Completed POSIX Compliance Fixes 1. Phase 1.1: Bare newline as .+1p - Already implemented, added tests to verify 2. Phase 1.2: SIGINT/SIGHUP signal handling - Added proper signal handlers: - SIGINT: Prints "?" and continues - SIGHUP: Saves buffer to ed.hup before exiting 3. Phase 2.1-2.3: Shell command integration - Implemented !command syntax for: - e !command - Read shell output into buffer - r !command - Append shell output after address - w !command - Pipe buffer to shell command stdin 4. Phase 3.2: Nested command check - Added validation to reject g/G/v/V/! in global command lists 5. Phase 5.1: Long line folding - The l command now folds lines at 72 characters
1 parent 949c1ea commit 1305006

File tree

4 files changed

+412
-24
lines changed

4 files changed

+412
-24
lines changed

editors/ed/buffer.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,39 @@ impl Buffer {
269269
Ok(byte_count)
270270
}
271271

272+
/// Load buffer from a string (for shell command output).
273+
/// Does not set pathname.
274+
pub fn load_from_string(&mut self, content: &str) {
275+
let mut lines = Vec::new();
276+
for line in content.lines() {
277+
lines.push(format!("{}\n", line));
278+
}
279+
// Handle case where content doesn't end with newline
280+
if !content.is_empty() && !content.ends_with('\n') {
281+
// Last line already has \n from the loop above
282+
}
283+
self.lines = lines;
284+
self.cur_line = if self.lines.is_empty() {
285+
0
286+
} else {
287+
self.lines.len()
288+
};
289+
self.modified = false;
290+
self.marks.clear();
291+
self.undo_record = None;
292+
}
293+
294+
/// Append content from a string after a line (for shell command output).
295+
pub fn append_from_string(&mut self, after_line: usize, content: &str) {
296+
let mut lines = Vec::new();
297+
for line in content.lines() {
298+
lines.push(format!("{}\n", line));
299+
}
300+
if !lines.is_empty() {
301+
self.append(after_line, &lines);
302+
}
303+
}
304+
272305
/// Read a file and append after a line.
273306
pub fn read_file_at(&mut self, pathname: &str, after_line: usize) -> io::Result<usize> {
274307
let file = fs::File::open(pathname)?;

editors/ed/editor.rs

Lines changed: 233 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use crate::ed::error::{EdError, EdResult};
1414
use crate::ed::parser::{parse, Address, AddressInfo, Command, PrintMode};
1515
use regex::Regex;
1616
use std::io::{self, BufRead, Write};
17+
use std::sync::atomic::Ordering;
1718

1819
/// The ed editor state.
1920
pub struct Editor<R: BufRead, W: Write> {
@@ -107,6 +108,53 @@ impl<R: BufRead, W: Write> Editor<R, W> {
107108
Ok(())
108109
}
109110

111+
/// Execute a shell command and return its stdout as a string.
112+
/// Used for `e !command` and `r !command`.
113+
fn shell_read(&self, command: &str) -> io::Result<(String, usize)> {
114+
let output = std::process::Command::new("sh")
115+
.arg("-c")
116+
.arg(command)
117+
.output()?;
118+
119+
if !output.status.success() {
120+
return Err(io::Error::other(
121+
String::from_utf8_lossy(&output.stderr).to_string(),
122+
));
123+
}
124+
125+
let content = String::from_utf8_lossy(&output.stdout).to_string();
126+
let bytes = content.len();
127+
Ok((content, bytes))
128+
}
129+
130+
/// Execute a shell command with input piped to its stdin.
131+
/// Used for `w !command`. Returns bytes written.
132+
fn shell_write(&self, command: &str, start: usize, end: usize) -> io::Result<usize> {
133+
use std::io::Write as _;
134+
use std::process::{Command, Stdio};
135+
136+
let mut child = Command::new("sh")
137+
.arg("-c")
138+
.arg(command)
139+
.stdin(Stdio::piped())
140+
.stdout(Stdio::inherit())
141+
.stderr(Stdio::inherit())
142+
.spawn()?;
143+
144+
let mut bytes = 0;
145+
if let Some(ref mut stdin) = child.stdin {
146+
for i in start..=end {
147+
if let Some(line) = self.buf.get_line(i) {
148+
stdin.write_all(line.as_bytes())?;
149+
bytes += line.len();
150+
}
151+
}
152+
}
153+
154+
child.wait()?;
155+
Ok(bytes)
156+
}
157+
110158
/// Resolve an address to a line number.
111159
fn resolve_address(&mut self, addr: &Address) -> EdResult<usize> {
112160
self.resolve_address_with_base(addr, self.buf.cur_line)
@@ -355,8 +403,18 @@ impl<R: BufRead, W: Write> Editor<R, W> {
355403
return Err(EdError::NoFilename);
356404
}
357405
let path = path.clone();
358-
let bytes = self.buf.read_file(&path)?;
359-
self.print_bytes(bytes)?;
406+
407+
// POSIX: If path starts with !, execute as shell command
408+
if let Some(command) = path.strip_prefix('!') {
409+
let (content, bytes) = self.shell_read(command)?;
410+
// Load content into buffer
411+
self.buf.load_from_string(&content);
412+
// Don't set pathname for shell commands
413+
self.print_bytes(bytes)?;
414+
} else {
415+
let bytes = self.buf.read_file(&path)?;
416+
self.print_bytes(bytes)?;
417+
}
360418
self.quit_warning_given = false;
361419
Ok(())
362420
}
@@ -434,8 +492,16 @@ impl<R: BufRead, W: Write> Editor<R, W> {
434492
return Err(EdError::NoFilename);
435493
}
436494
let path = path.clone();
437-
let bytes = self.buf.read_file_at(&path, after_line)?;
438-
self.print_bytes(bytes)?;
495+
496+
// POSIX: If path starts with !, execute as shell command
497+
if let Some(command) = path.strip_prefix('!') {
498+
let (content, bytes) = self.shell_read(command)?;
499+
self.buf.append_from_string(after_line, &content);
500+
self.print_bytes(bytes)?;
501+
} else {
502+
let bytes = self.buf.read_file_at(&path, after_line)?;
503+
self.print_bytes(bytes)?;
504+
}
439505
Ok(())
440506
}
441507
Command::Substitute(addr1, addr2, pattern, replacement, flags) => {
@@ -467,7 +533,12 @@ impl<R: BufRead, W: Write> Editor<R, W> {
467533
return Err(EdError::NoFilename);
468534
}
469535
let path = path.clone();
470-
if append {
536+
537+
// POSIX: If path starts with !, execute as shell command
538+
if let Some(command) = path.strip_prefix('!') {
539+
let bytes = self.shell_write(command, start, end)?;
540+
self.print_bytes(bytes)?;
541+
} else if append {
471542
// Append mode - open file for appending
472543
let mut file = std::fs::OpenOptions::new()
473544
.create(true)
@@ -627,25 +698,42 @@ impl<R: BufRead, W: Write> Editor<R, W> {
627698
// Non-printable: three-digit octal with backslash
628699
// $ within text: escaped with backslash
629700
// End of line: marked with $
701+
// Long lines: folded with \ before newline
702+
const FOLD_WIDTH: usize = 72;
630703
let content = line.trim_end_matches('\n');
704+
let mut col = 0;
705+
631706
for ch in content.chars() {
632-
match ch {
633-
'\\' => write!(self.writer, "\\\\")?,
634-
'\x07' => write!(self.writer, "\\a")?, // bell
635-
'\x08' => write!(self.writer, "\\b")?, // backspace
636-
'\x0c' => write!(self.writer, "\\f")?, // form feed
637-
'\r' => write!(self.writer, "\\r")?, // carriage return
638-
'\t' => write!(self.writer, "\\t")?, // tab
639-
'\x0b' => write!(self.writer, "\\v")?, // vertical tab
640-
'$' => write!(self.writer, "\\$")?, // escape $ in text
707+
// Get the escaped representation and its width
708+
let (escaped, width) = match ch {
709+
'\\' => ("\\\\".to_string(), 2),
710+
'\x07' => ("\\a".to_string(), 2),
711+
'\x08' => ("\\b".to_string(), 2),
712+
'\x0c' => ("\\f".to_string(), 2),
713+
'\r' => ("\\r".to_string(), 2),
714+
'\t' => ("\\t".to_string(), 2),
715+
'\x0b' => ("\\v".to_string(), 2),
716+
'$' => ("\\$".to_string(), 2),
641717
c if c.is_control() || !c.is_ascii() => {
642-
// Non-printable as three-digit octal per byte
643-
for byte in c.to_string().as_bytes() {
644-
write!(self.writer, "\\{:03o}", byte)?;
718+
let bytes = c.to_string().into_bytes();
719+
let mut s = String::new();
720+
for byte in &bytes {
721+
s.push_str(&format!("\\{:03o}", byte));
645722
}
723+
let w = bytes.len() * 4;
724+
(s, w)
646725
}
647-
c => write!(self.writer, "{}", c)?,
726+
c => (c.to_string(), 1),
727+
};
728+
729+
// Check if we need to fold (leave room for the char + potential $)
730+
if col + width > FOLD_WIDTH {
731+
writeln!(self.writer, "\\")?;
732+
col = 0;
648733
}
734+
735+
write!(self.writer, "{}", escaped)?;
736+
col += width;
649737
}
650738
writeln!(self.writer, "$")?;
651739
}
@@ -840,6 +928,62 @@ impl<R: BufRead, W: Write> Editor<R, W> {
840928
Ok(())
841929
}
842930

931+
/// Check if a command string contains forbidden commands for global.
932+
/// POSIX: The commands g, G, v, V, and ! cannot be used in the command-list.
933+
fn check_global_commands(&self, commands: &str) -> EdResult<()> {
934+
let cmd_str = commands.trim();
935+
if cmd_str.is_empty() {
936+
return Ok(());
937+
}
938+
939+
// Get the command character (skip any leading address)
940+
let cmd_chars: Vec<char> = cmd_str.chars().collect();
941+
for (i, &ch) in cmd_chars.iter().enumerate() {
942+
// Skip digits, dots, dollars, and address characters
943+
if ch.is_ascii_digit()
944+
|| ch == '.'
945+
|| ch == '$'
946+
|| ch == '+'
947+
|| ch == '-'
948+
|| ch == ','
949+
|| ch == ';'
950+
|| ch == '\''
951+
|| ch == '/'
952+
|| ch == '?'
953+
{
954+
continue;
955+
}
956+
// Found a potential command character
957+
match ch {
958+
'g' | 'G' | 'v' | 'V' => {
959+
// Check if this is actually the command (not part of pattern)
960+
// For g/v, they'd be followed by a delimiter
961+
if i + 1 < cmd_chars.len() {
962+
let next = cmd_chars[i + 1];
963+
if !next.is_alphanumeric() && next != ' ' {
964+
return Err(EdError::Generic(
965+
"cannot nest g, G, v, or V commands".to_string(),
966+
));
967+
}
968+
}
969+
// Single g/G/v/V at end is also forbidden
970+
if i + 1 >= cmd_chars.len() {
971+
return Err(EdError::Generic(
972+
"cannot nest g, G, v, or V commands".to_string(),
973+
));
974+
}
975+
}
976+
'!' => {
977+
return Err(EdError::Generic(
978+
"cannot use ! command within global".to_string(),
979+
));
980+
}
981+
_ => break, // Found a different command, stop checking
982+
}
983+
}
984+
Ok(())
985+
}
986+
843987
/// Execute a global command.
844988
fn execute_global(
845989
&mut self,
@@ -849,6 +993,9 @@ impl<R: BufRead, W: Write> Editor<R, W> {
849993
commands: &str,
850994
invert: bool,
851995
) -> EdResult<()> {
996+
// POSIX: Check for forbidden commands
997+
self.check_global_commands(commands)?;
998+
852999
let (start, end) = self.resolve_range(&addr1, &addr2)?;
8531000

8541001
// Use previous pattern if empty
@@ -980,16 +1127,84 @@ impl<R: BufRead, W: Write> Editor<R, W> {
9801127
Ok(())
9811128
}
9821129

1130+
/// Check for and handle SIGINT signal.
1131+
/// Returns true if SIGINT was received and handled.
1132+
fn check_sigint(&mut self) -> io::Result<bool> {
1133+
if crate::SIGINT_RECEIVED.swap(false, Ordering::SeqCst) {
1134+
// POSIX: Print "?" and continue
1135+
writeln!(self.writer, "?")?;
1136+
self.last_error = Some("Interrupt".to_string());
1137+
// If in input mode, exit input mode without completing the command
1138+
if self.in_input_mode {
1139+
self.in_input_mode = false;
1140+
self.pending_command = None;
1141+
self.input_lines.clear();
1142+
}
1143+
return Ok(true);
1144+
}
1145+
Ok(false)
1146+
}
1147+
1148+
/// Check for and handle SIGHUP signal.
1149+
/// Returns true if SIGHUP was received (caller should exit).
1150+
fn check_sighup(&mut self) -> bool {
1151+
if crate::SIGHUP_RECEIVED.swap(false, Ordering::SeqCst) {
1152+
// POSIX: If buffer is modified, attempt to save to ed.hup
1153+
if self.buf.modified && self.buf.line_count() > 0 {
1154+
self.save_hup_file();
1155+
}
1156+
return true;
1157+
}
1158+
false
1159+
}
1160+
1161+
/// Save buffer to ed.hup file per POSIX requirements.
1162+
/// Tries current directory first, then $HOME.
1163+
fn save_hup_file(&mut self) {
1164+
let paths_to_try = [
1165+
Some("ed.hup".to_string()),
1166+
std::env::var("HOME").ok().map(|h| format!("{}/ed.hup", h)),
1167+
];
1168+
1169+
for path_opt in paths_to_try.iter().flatten() {
1170+
if self
1171+
.buf
1172+
.write_to_file(1, self.buf.line_count(), path_opt)
1173+
.is_ok()
1174+
{
1175+
// Successfully saved
1176+
return;
1177+
}
1178+
}
1179+
// If we couldn't save anywhere, nothing more we can do
1180+
}
1181+
9831182
/// Run the main editor loop.
9841183
pub fn run(&mut self) -> io::Result<()> {
9851184
loop {
1185+
// Check for SIGHUP - exit if received
1186+
if self.check_sighup() {
1187+
break;
1188+
}
1189+
1190+
// Check for SIGINT - print "?" and continue
1191+
self.check_sigint()?;
1192+
9861193
self.print_prompt()?;
9871194

9881195
let line = match self.read_line()? {
9891196
Some(l) => l,
9901197
None => break,
9911198
};
9921199

1200+
// Check signals again after potentially blocking on input
1201+
if self.check_sighup() {
1202+
break;
1203+
}
1204+
if self.check_sigint()? {
1205+
continue; // SIGINT during input - restart loop
1206+
}
1207+
9931208
if !self.process_line(&line)? {
9941209
break;
9951210
}

0 commit comments

Comments
 (0)