@@ -835,8 +835,47 @@ fn extract_redirections_from_tokens(tokens: &[Token]) -> Result<(Vec<Token>, Vec
835835 i += 2 ;
836836 }
837837
838- Token :: HereDoc | Token :: HereDocDash | Token :: HereString => {
839- return Err ( anyhow ! ( "Here documents and here strings not yet implemented (M9)" ) ) ;
838+ Token :: HereDoc | Token :: HereDocDash => {
839+ // Here document: << DELIMITER or <<- DELIMITER
840+ let strip_tabs = matches ! ( & tokens[ i] , Token :: HereDocDash ) ;
841+ let delimiter = expect_word ( & tokens, i + 1 , "here document delimiter" ) ?;
842+
843+ // Check if delimiter is quoted (disables expansion)
844+ let ( delimiter_clean, expand) = if delimiter. starts_with ( '\'' ) || delimiter. starts_with ( '"' ) {
845+ ( delimiter. trim_matches ( |c| c == '\'' || c == '"' ) . to_string ( ) , false )
846+ } else {
847+ ( delimiter. clone ( ) , true )
848+ } ;
849+
850+ // Content will be provided by REPL after reading subsequent lines
851+ // For now, create placeholder - will be filled by execute_with_heredoc
852+ redirects. push ( Redirection :: HereDoc {
853+ delimiter : delimiter_clean,
854+ content : String :: new ( ) , // Filled later
855+ expand,
856+ strip_tabs,
857+ } ) ;
858+ i += 2 ;
859+ }
860+
861+ Token :: HereString => {
862+ // Here string: <<< word
863+ let content_word = expect_word ( & tokens, i + 1 , "here string content" ) ?;
864+
865+ // Check if quoted (disables expansion)
866+ let ( content, expand) = if content_word. starts_with ( '\'' ) {
867+ ( content_word. trim_matches ( '\'' ) . to_string ( ) , false )
868+ } else if content_word. starts_with ( '"' ) {
869+ ( content_word. trim_matches ( '"' ) . to_string ( ) , true )
870+ } else {
871+ ( content_word. clone ( ) , true )
872+ } ;
873+
874+ redirects. push ( Redirection :: HereString {
875+ content,
876+ expand,
877+ } ) ;
878+ i += 2 ;
840879 }
841880
842881 Token :: Pipe => {
@@ -966,8 +1005,47 @@ pub fn parse_command(input: &str) -> Result<Command> {
9661005 i += 2 ;
9671006 }
9681007
969- Token :: HereDoc | Token :: HereDocDash | Token :: HereString => {
970- return Err ( anyhow ! ( "Here documents and here strings not yet implemented (M9)" ) ) ;
1008+ Token :: HereDoc | Token :: HereDocDash => {
1009+ // Here document: << DELIMITER or <<- DELIMITER
1010+ let strip_tabs = matches ! ( & tokens[ i] , Token :: HereDocDash ) ;
1011+ let delimiter = expect_word ( & tokens, i + 1 , "here document delimiter" ) ?;
1012+
1013+ // Check if delimiter is quoted (disables expansion)
1014+ let ( delimiter_clean, expand) = if delimiter. starts_with ( '\'' ) || delimiter. starts_with ( '"' ) {
1015+ ( delimiter. trim_matches ( |c| c == '\'' || c == '"' ) . to_string ( ) , false )
1016+ } else {
1017+ ( delimiter. clone ( ) , true )
1018+ } ;
1019+
1020+ // Content will be provided by REPL after reading subsequent lines
1021+ // For now, create placeholder - will be filled by execute_with_heredoc
1022+ redirects. push ( Redirection :: HereDoc {
1023+ delimiter : delimiter_clean,
1024+ content : String :: new ( ) , // Filled later
1025+ expand,
1026+ strip_tabs,
1027+ } ) ;
1028+ i += 2 ;
1029+ }
1030+
1031+ Token :: HereString => {
1032+ // Here string: <<< word
1033+ let content_word = expect_word ( & tokens, i + 1 , "here string content" ) ?;
1034+
1035+ // Check if quoted (disables expansion)
1036+ let ( content, expand) = if content_word. starts_with ( '\'' ) {
1037+ ( content_word. trim_matches ( '\'' ) . to_string ( ) , false )
1038+ } else if content_word. starts_with ( '"' ) {
1039+ ( content_word. trim_matches ( '"' ) . to_string ( ) , true )
1040+ } else {
1041+ ( content_word. clone ( ) , true )
1042+ } ;
1043+
1044+ redirects. push ( Redirection :: HereString {
1045+ content,
1046+ expand,
1047+ } ) ;
1048+ i += 2 ;
9711049 }
9721050
9731051 Token :: Pipe => {
@@ -2596,3 +2674,158 @@ mod tests {
25962674 }
25972675 }
25982676}
2677+
2678+ /// Process here document content: strip tabs if needed, expand if needed
2679+ pub fn process_heredoc_content (
2680+ content : & str ,
2681+ strip_tabs : bool ,
2682+ expand : bool ,
2683+ state : & mut crate :: state:: ShellState ,
2684+ ) -> Result < String > {
2685+ let mut processed = String :: new ( ) ;
2686+
2687+ for line in content. lines ( ) {
2688+ let line_to_add = if strip_tabs {
2689+ line. trim_start_matches ( '\t' )
2690+ } else {
2691+ line
2692+ } ;
2693+
2694+ if expand {
2695+ // Perform variable expansion, command substitution, arithmetic
2696+ let expanded = expand_with_command_sub ( line_to_add, state) ?;
2697+ processed. push_str ( & expanded) ;
2698+ } else {
2699+ processed. push_str ( line_to_add) ;
2700+ }
2701+ processed. push ( '\n' ) ;
2702+ }
2703+
2704+ // Remove trailing newline if content didn't end with one
2705+ if !content. ends_with ( '\n' ) && processed. ends_with ( '\n' ) {
2706+ processed. pop ( ) ;
2707+ }
2708+
2709+ Ok ( processed)
2710+ }
2711+
2712+ /// Fill in here document content after reading from input
2713+ pub fn fill_heredoc_content (
2714+ cmd : & mut Command ,
2715+ heredoc_contents : & [ ( String , String ) ] , // (delimiter, content) pairs
2716+ ) -> Result < ( ) > {
2717+ // Find redirections that need content filled
2718+ let redirects = match cmd {
2719+ Command :: External { redirects, .. } => redirects,
2720+ Command :: Mkdir { redirects, .. }
2721+ | Command :: Rmdir { redirects, .. }
2722+ | Command :: Touch { redirects, .. }
2723+ | Command :: Rm { redirects, .. }
2724+ | Command :: Ls { redirects, .. }
2725+ | Command :: Pwd { redirects, .. }
2726+ | Command :: Pipeline { redirects, .. } => redirects,
2727+ _ => return Ok ( ( ) ) , // No redirects (Cd, Undo, History, etc.)
2728+ } ;
2729+
2730+ let mut heredoc_index = 0 ;
2731+ for redirect in redirects. iter_mut ( ) {
2732+ if let Redirection :: HereDoc { ref mut content, .. } = redirect {
2733+ if heredoc_index < heredoc_contents. len ( ) {
2734+ * content = heredoc_contents[ heredoc_index] . 1 . clone ( ) ;
2735+ heredoc_index += 1 ;
2736+ }
2737+ }
2738+ }
2739+
2740+ Ok ( ( ) )
2741+ }
2742+
2743+ /// Check if command contains here documents and extract delimiters
2744+ pub fn extract_heredoc_delimiters ( input : & str ) -> Result < Vec < String > > {
2745+ let mut delimiters = Vec :: new ( ) ;
2746+ let tokens = tokenize ( input) ?;
2747+
2748+ for i in 0 ..tokens. len ( ) {
2749+ if matches ! ( tokens[ i] , Token :: HereDoc | Token :: HereDocDash ) {
2750+ if i + 1 < tokens. len ( ) {
2751+ if let Token :: Word ( ref word) = tokens[ i + 1 ] {
2752+ let delimiter = quoted_word_to_string ( word) ;
2753+ // Remove quotes if present
2754+ let clean = delimiter. trim_matches ( |c| c == '\'' || c == '"' ) ;
2755+ delimiters. push ( clean. to_string ( ) ) ;
2756+ }
2757+ }
2758+ }
2759+ }
2760+
2761+ Ok ( delimiters)
2762+ }
2763+
2764+ #[ cfg( test) ]
2765+ mod heredoc_tests {
2766+ use super :: * ;
2767+
2768+ #[ test]
2769+ fn test_process_heredoc_literal ( ) {
2770+ let mut state = crate :: state:: ShellState :: new ( "/tmp" ) . unwrap ( ) ;
2771+ let content = "Line 1\n Line 2\n Line 3" ;
2772+
2773+ let processed = process_heredoc_content ( content, false , false , & mut state) . unwrap ( ) ;
2774+ // Content without trailing newline adds newlines for each line
2775+ assert_eq ! ( processed, "Line 1\n Line 2\n Line 3" ) ;
2776+ }
2777+
2778+ #[ test]
2779+ fn test_process_heredoc_with_expansion ( ) {
2780+ let mut state = crate :: state:: ShellState :: new ( "/tmp" ) . unwrap ( ) ;
2781+ state. set_variable ( "name" , "World" ) ;
2782+
2783+ let content = "Hello $name\n Result: $((5 + 3))" ;
2784+
2785+ let processed = process_heredoc_content ( content, false , true , & mut state) . unwrap ( ) ;
2786+ assert_eq ! ( processed, "Hello World\n Result: 8" ) ;
2787+ }
2788+
2789+ #[ test]
2790+ fn test_process_heredoc_strip_tabs ( ) {
2791+ let mut state = crate :: state:: ShellState :: new ( "/tmp" ) . unwrap ( ) ;
2792+ let content = "\t Line 1\n \t \t Line 2\n \t \t \t Line 3" ;
2793+
2794+ let processed = process_heredoc_content ( content, true , false , & mut state) . unwrap ( ) ;
2795+ // <<- strips ALL leading tabs from each line
2796+ assert_eq ! ( processed, "Line 1\n Line 2\n Line 3" ) ;
2797+ }
2798+
2799+ #[ test]
2800+ fn test_tokenize_herestring ( ) {
2801+ let tokens = tokenize ( "cat <<<word" ) . unwrap ( ) ;
2802+ assert_eq ! ( tokens. len( ) , 3 ) ;
2803+ assert ! ( matches!( tokens[ 1 ] , Token :: HereString ) ) ;
2804+ }
2805+
2806+ #[ test]
2807+ fn test_tokenize_heredoc ( ) {
2808+ let tokens = tokenize ( "cat <<EOF" ) . unwrap ( ) ;
2809+ assert_eq ! ( tokens. len( ) , 3 ) ;
2810+ assert ! ( matches!( tokens[ 1 ] , Token :: HereDoc ) ) ;
2811+ }
2812+
2813+ #[ test]
2814+ fn test_tokenize_heredoc_dash ( ) {
2815+ let tokens = tokenize ( "cat <<-EOF" ) . unwrap ( ) ;
2816+ assert_eq ! ( tokens. len( ) , 3 ) ;
2817+ assert ! ( matches!( tokens[ 1 ] , Token :: HereDocDash ) ) ;
2818+ }
2819+
2820+ #[ test]
2821+ fn test_extract_heredoc_delimiters ( ) {
2822+ let delimiters = extract_heredoc_delimiters ( "cat <<EOF" ) . unwrap ( ) ;
2823+ assert_eq ! ( delimiters, vec![ "EOF" ] ) ;
2824+
2825+ let delimiters2 = extract_heredoc_delimiters ( "cat <<'EOF'" ) . unwrap ( ) ;
2826+ assert_eq ! ( delimiters2, vec![ "EOF" ] ) ;
2827+
2828+ let delimiters3 = extract_heredoc_delimiters ( "cmd <<END1 arg <<END2" ) . unwrap ( ) ;
2829+ assert_eq ! ( delimiters3, vec![ "END1" , "END2" ] ) ;
2830+ }
2831+ }
0 commit comments