Fortran · 14957 bytes Raw Blame History
1 ! ==============================================================================
2 ! Module: heredoc
3 ! Purpose: Here documents and here strings support
4 ! ==============================================================================
5 module heredoc
6 use shell_types
7 use variables
8 use iso_fortran_env, only: input_unit, output_unit, error_unit
9 implicit none
10
11 integer, parameter :: MAX_HEREDOC_LINES = 1000
12 integer, parameter :: MAX_HEREDOC_LENGTH = 4096
13
14 type :: heredoc_t
15 character(len=256) :: delimiter
16 character(len=MAX_HEREDOC_LENGTH) :: lines(MAX_HEREDOC_LINES)
17 integer :: num_lines
18 logical :: expand_variables
19 logical :: strip_tabs
20 character(len=MAX_PATH_LEN) :: temp_file
21 end type heredoc_t
22
23 contains
24
25 subroutine parse_heredoc_redirection(shell, cmd_line, heredoc_start, cmd_modified)
26 type(shell_state_t), intent(inout) :: shell
27 character(len=*), intent(inout) :: cmd_line
28 integer, intent(out) :: heredoc_start
29 logical, intent(out) :: cmd_modified
30
31 integer :: pos, delimiter_start, delimiter_end
32 character(len=256) :: delimiter
33 logical :: strip_tabs, expand_vars
34
35 ! write(error_unit, '(A,A,A)') 'DEBUG: parse_heredoc_redirection called with cmd_line=|', trim(cmd_line), '|'
36
37 cmd_modified = .false.
38 heredoc_start = 0
39
40 ! Look for << or <<< operators
41 pos = index(cmd_line, '<<')
42 if (pos == 0) return
43
44 ! Check if it's a here string (<<<)
45 if (pos > 0 .and. pos + 2 <= len_trim(cmd_line) .and. cmd_line(pos+2:pos+2) == '<') then
46 call parse_here_string(shell, cmd_line, pos, cmd_modified)
47 return
48 end if
49
50 ! It's a here document (<<)
51 heredoc_start = pos
52
53 ! Check for <<- (strip leading tabs)
54 strip_tabs = .false.
55 if (pos + 2 <= len_trim(cmd_line) .and. cmd_line(pos+2:pos+2) == '-') then
56 strip_tabs = .true.
57 delimiter_start = pos + 3
58 else
59 delimiter_start = pos + 2
60 end if
61
62 ! Skip whitespace after <<
63 do while (delimiter_start <= len_trim(cmd_line) .and. &
64 (cmd_line(delimiter_start:delimiter_start) == ' ' .or. &
65 cmd_line(delimiter_start:delimiter_start) == char(9)))
66 delimiter_start = delimiter_start + 1
67 end do
68
69 ! Extract delimiter
70 delimiter_end = delimiter_start
71 do while (delimiter_end <= len_trim(cmd_line) .and. &
72 cmd_line(delimiter_end:delimiter_end) /= ' ' .and. &
73 cmd_line(delimiter_end:delimiter_end) /= char(9))
74 delimiter_end = delimiter_end + 1
75 end do
76
77 if (delimiter_start >= delimiter_end) then
78 write(error_unit, '(a)') 'heredoc: missing delimiter'
79 shell%last_exit_status = 1
80 return
81 end if
82
83 delimiter = cmd_line(delimiter_start:delimiter_end-1)
84
85 ! write(error_unit, '(A,A,A,A,A,L1)') 'DEBUG: parse_heredoc delimiter=|', &
86 ! trim(delimiter), '| vs pending=|', trim(shell%pending_heredoc_delimiter), '| has=', shell%has_pending_heredoc
87
88 ! Check if delimiter is quoted (affects variable expansion)
89 expand_vars = .true.
90 if (delimiter(1:1) == '"' .or. delimiter(1:1) == "'" .or. delimiter(1:1) == '\') then
91 expand_vars = .false.
92 ! Remove quotes from delimiter
93 if (len_trim(delimiter) > 2) then
94 delimiter = delimiter(2:len_trim(delimiter)-1)
95 end if
96 end if
97
98 ! Process the here document
99 call process_heredoc(shell, delimiter, expand_vars, strip_tabs, cmd_line, pos)
100 cmd_modified = .true.
101 end subroutine
102
103 subroutine parse_here_string(shell, cmd_line, pos, cmd_modified)
104 type(shell_state_t), intent(inout) :: shell
105 character(len=*), intent(inout) :: cmd_line
106 integer, intent(in) :: pos
107 logical, intent(out) :: cmd_modified
108
109 character(len=2048) :: here_string, expanded_string, temp_file
110 integer :: string_start
111
112 cmd_modified = .false.
113
114 ! Find the start of the here string (after <<<)
115 string_start = pos + 3
116
117 ! Skip whitespace
118 do while (string_start <= len_trim(cmd_line) .and. &
119 (cmd_line(string_start:string_start) == ' ' .or. &
120 cmd_line(string_start:string_start) == char(9)))
121 string_start = string_start + 1
122 end do
123
124 if (string_start > len_trim(cmd_line)) then
125 write(error_unit, '(a)') 'here string: missing string'
126 shell%last_exit_status = 1
127 return
128 end if
129
130 ! Extract the here string (rest of the line)
131 here_string = cmd_line(string_start:)
132
133 ! Expand variables in the here string
134 call expand_here_string(shell, here_string, expanded_string)
135
136 ! Create temporary file with the expanded string
137 call create_temp_heredoc_file(expanded_string, temp_file)
138
139 ! Replace the <<< part with redirection from temp file
140 cmd_line = cmd_line(1:pos-1) // ' < ' // trim(temp_file)
141 cmd_modified = .true.
142 end subroutine
143
144 subroutine process_heredoc(shell, delimiter, expand_vars, strip_tabs, cmd_line, pos)
145 type(shell_state_t), intent(inout) :: shell
146 character(len=*), intent(in) :: delimiter
147 logical, intent(in) :: expand_vars, strip_tabs
148 character(len=*), intent(inout) :: cmd_line
149 integer, intent(in) :: pos
150
151 ! Use allocatable array to avoid static storage (was 4MB!)
152 character(len=MAX_HEREDOC_LENGTH), allocatable :: doc_lines(:)
153 character(len=MAX_HEREDOC_LENGTH) :: line, processed_line
154 character(len=MAX_PATH_LEN) :: temp_file
155 integer :: num_lines, i, capacity
156 logical :: found_delimiter, actual_expand_vars
157
158 ! Allocate initial array
159 allocate(doc_lines(20)) ! Start with reasonable size
160 capacity = 20
161 num_lines = 0
162 found_delimiter = .false.
163 actual_expand_vars = expand_vars ! Default to input value
164
165 ! Check if we have pending heredoc content from -c flag
166 if (shell%has_pending_heredoc .and. &
167 trim(shell%pending_heredoc_delimiter) == trim(delimiter)) then
168 ! Use the pre-stored heredoc content
169 ! Use the stored quoted flag to determine expansion
170 actual_expand_vars = .not. shell%pending_heredoc_quoted
171
172 ! DEBUG: Print what we're doing
173 ! write(error_unit, '(A,L1,A,A,A,A)') 'DEBUG: Using pending heredoc, expand_vars=', actual_expand_vars, &
174 ! ', delimiter=', trim(delimiter), ', pending_delim=', trim(shell%pending_heredoc_delimiter)
175
176 ! Split pending content into lines
177 block
178 integer :: line_start, line_end, content_len
179 content_len = len_trim(shell%pending_heredoc)
180 line_start = 1
181
182 do while (line_start <= content_len)
183 ! Find end of line
184 line_end = line_start
185 do while (line_end <= content_len .and. &
186 shell%pending_heredoc(line_end:line_end) /= char(10))
187 line_end = line_end + 1
188 end do
189
190 num_lines = num_lines + 1
191 ! Grow array if needed
192 if (num_lines > capacity) then
193 block
194 character(len=MAX_HEREDOC_LENGTH), allocatable :: temp(:)
195 allocate(temp(capacity * 2))
196 temp(1:capacity) = doc_lines
197 call move_alloc(temp, doc_lines)
198 capacity = capacity * 2
199 end block
200 end if
201
202 ! Store the line
203 if (line_end > line_start) then
204 doc_lines(num_lines) = shell%pending_heredoc(line_start:line_end-1)
205 else
206 doc_lines(num_lines) = ''
207 end if
208
209 line_start = line_end + 1
210 end do
211 end block
212
213 ! Clear the pending heredoc
214 shell%has_pending_heredoc = .false.
215 shell%pending_heredoc = ''
216 found_delimiter = .true.
217
218 else
219 ! Read from stdin as usual
220 write(output_unit, '(a)', advance='no') '> '
221
222 ! Read lines until we find the delimiter
223 do while (.true.) ! Remove MAX_HEREDOC_LINES limit
224 read(input_unit, '(a)', iostat=i) line
225 if (i /= 0) then
226 write(error_unit, '(a)') 'heredoc: unexpected end of input'
227 shell%last_exit_status = 1
228 if (allocated(doc_lines)) deallocate(doc_lines)
229 return
230 end if
231
232 ! Check if this line is the delimiter
233 if (strip_tabs) then
234 ! Remove leading tabs for comparison
235 processed_line = line
236 do while (len_trim(processed_line) > 0 .and. processed_line(1:1) == char(9))
237 processed_line = processed_line(2:)
238 end do
239 else
240 processed_line = line
241 end if
242
243 if (trim(processed_line) == trim(delimiter)) then
244 found_delimiter = .true.
245 exit
246 end if
247
248 num_lines = num_lines + 1
249 ! Grow array if needed
250 if (num_lines > capacity) then
251 call grow_heredoc_array(doc_lines, capacity)
252 end if
253 doc_lines(num_lines) = line
254
255 ! Show continuation prompt
256 write(output_unit, '(a)', advance='no') '> '
257 end do
258 end if ! end of else (reading from stdin vs using pending heredoc)
259
260 if (.not. found_delimiter) then
261 write(error_unit, '(a,a,a)') 'heredoc: delimiter "', trim(delimiter), '" not found'
262 shell%last_exit_status = 1
263 if (allocated(doc_lines)) deallocate(doc_lines)
264 return
265 end if
266
267 ! Process the collected lines
268 call process_heredoc_lines(shell, doc_lines(1:num_lines), num_lines, actual_expand_vars, strip_tabs, temp_file)
269
270 ! Clean up allocatable array
271 if (allocated(doc_lines)) deallocate(doc_lines)
272
273 ! Replace the heredoc part in command line with file redirection
274 cmd_line = cmd_line(1:pos-1) // ' < ' // trim(temp_file)
275 end subroutine
276
277 ! Helper subroutine to grow heredoc array
278 subroutine grow_heredoc_array(array, current_size)
279 character(len=MAX_HEREDOC_LENGTH), allocatable, intent(inout) :: array(:)
280 integer, intent(inout) :: current_size
281 character(len=MAX_HEREDOC_LENGTH), allocatable :: new_array(:)
282 integer :: new_size
283
284 new_size = current_size * 2
285 allocate(new_array(new_size))
286
287 ! Copy existing data
288 new_array(1:current_size) = array(1:current_size)
289
290 ! Swap arrays
291 call move_alloc(new_array, array)
292 current_size = new_size
293 end subroutine
294
295 subroutine process_heredoc_lines(shell, lines, num_lines, expand_vars, strip_tabs, temp_file)
296 type(shell_state_t), intent(in) :: shell
297 character(len=*), intent(in) :: lines(:)
298 integer, intent(in) :: num_lines
299 logical, intent(in) :: expand_vars, strip_tabs
300 character(len=*), intent(out) :: temp_file
301
302 character(len=MAX_HEREDOC_LENGTH) :: processed_line, expanded_line
303 integer :: unit, i
304
305 ! Create temporary file
306 call create_temp_file(temp_file, unit)
307 if (unit <= 0) then
308 write(error_unit, '(a)') 'heredoc: cannot create temporary file'
309 return
310 end if
311
312 ! Write processed lines to temporary file
313 do i = 1, num_lines
314 processed_line = lines(i)
315
316 ! Strip leading tabs if requested
317 if (strip_tabs) then
318 do while (len_trim(processed_line) > 0 .and. processed_line(1:1) == char(9))
319 processed_line = processed_line(2:)
320 end do
321 end if
322
323 ! Expand variables if requested
324 if (expand_vars) then
325 call expand_here_string(shell, processed_line, expanded_line)
326 processed_line = expanded_line
327 end if
328
329 write(unit, '(a)') trim(processed_line)
330 end do
331
332 close(unit)
333 end subroutine
334
335 subroutine expand_here_string(shell, input_string, expanded_string)
336 type(shell_state_t), intent(in) :: shell
337 character(len=*), intent(in) :: input_string
338 character(len=*), intent(out) :: expanded_string
339
340 character(len=len(input_string)) :: work_string
341 integer :: pos, var_start, var_end
342 character(len=256) :: var_name
343 character(len=:), allocatable :: var_value
344
345 work_string = input_string
346 expanded_string = ''
347 pos = 1
348
349 do while (pos <= len_trim(work_string))
350 if (work_string(pos:pos) == '$' .and. pos < len_trim(work_string)) then
351 ! Found variable reference
352 var_start = pos + 1
353 var_end = var_start
354
355 ! Find end of variable name
356 if (work_string(var_start:var_start) == '{') then
357 ! ${variable} format
358 var_start = var_start + 1
359 do while (var_end <= len_trim(work_string) .and. work_string(var_end:var_end) /= '}')
360 var_end = var_end + 1
361 end do
362 if (var_end <= len_trim(work_string) .and. work_string(var_end:var_end) == '}') then
363 var_name = work_string(var_start:var_end-1)
364 pos = var_end + 1
365 else
366 ! Malformed variable reference
367 expanded_string = trim(expanded_string) // '$'
368 pos = pos + 1
369 cycle
370 end if
371 else
372 ! $variable format
373 do while (var_end <= len_trim(work_string) .and. &
374 ((work_string(var_end:var_end) >= 'A' .and. work_string(var_end:var_end) <= 'Z') .or. &
375 (work_string(var_end:var_end) >= 'a' .and. work_string(var_end:var_end) <= 'z') .or. &
376 (work_string(var_end:var_end) >= '0' .and. work_string(var_end:var_end) <= '9') .or. &
377 work_string(var_end:var_end) == '_'))
378 var_end = var_end + 1
379 end do
380 if (var_end > var_start) then
381 var_name = work_string(var_start:var_end-1)
382 pos = var_end
383 else
384 expanded_string = trim(expanded_string) // '$'
385 pos = pos + 1
386 cycle
387 end if
388 end if
389
390 ! Get variable value
391 var_value = get_shell_variable(shell, trim(var_name))
392 expanded_string = trim(expanded_string) // trim(var_value)
393 else
394 expanded_string = trim(expanded_string) // work_string(pos:pos)
395 pos = pos + 1
396 end if
397 end do
398 end subroutine
399
400 subroutine create_temp_file(filename, unit)
401 character(len=*), intent(out) :: filename
402 integer, intent(out) :: unit
403
404 character(len=32) :: pid_str
405 integer :: pid
406
407 ! Create a unique temporary filename
408 pid = 1234 ! In real implementation, would get actual PID
409 write(pid_str, '(I0)') pid
410 filename = '/tmp/fortsh_heredoc_' // trim(pid_str) // '.tmp'
411
412 open(newunit=unit, file=trim(filename), status='replace', action='write', iostat=unit)
413 if (unit /= 0) then
414 unit = -1
415 end if
416 end subroutine
417
418 subroutine create_temp_heredoc_file(content, filename)
419 character(len=*), intent(in) :: content
420 character(len=*), intent(out) :: filename
421
422 integer :: unit
423
424 call create_temp_file(filename, unit)
425 if (unit <= 0) return
426
427 write(unit, '(a)') trim(content)
428 close(unit)
429 end subroutine
430
431 subroutine cleanup_heredoc_temp_files()
432 ! Clean up temporary files (simplified)
433 ! In a real implementation, would maintain a list of temp files to clean up
434 end subroutine
435
436 end module heredoc