@@ -14,6 +14,7 @@ use crate::ed::error::{EdError, EdResult};
1414use crate :: ed:: parser:: { parse, Address , AddressInfo , Command , PrintMode } ;
1515use regex:: Regex ;
1616use std:: io:: { self , BufRead , Write } ;
17+ use std:: sync:: atomic:: Ordering ;
1718
1819/// The ed editor state.
1920pub 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