Fortran · 30962 bytes Raw Blame History
1 ! ==============================================================================
2 ! Module: printf_builtin
3 ! Purpose: Printf built-in command with full POSIX format string support
4 ! ==============================================================================
5 module printf_builtin
6 use shell_types
7 use iso_fortran_env, only: output_unit, error_unit
8 implicit none
9
10 ! Format specifier components
11 type :: format_info_t
12 logical :: left_align = .false. ! '-' flag
13 logical :: zero_pad = .false. ! '0' flag
14 logical :: show_sign = .false. ! '+' flag
15 logical :: space_sign = .false. ! ' ' flag
16 logical :: alternate = .false. ! '#' flag
17 integer :: width = 0 ! field width
18 integer :: precision = -1 ! precision (-1 = default)
19 logical :: width_from_arg = .false. ! '*' for width
20 logical :: prec_from_arg = .false. ! '*' for precision
21 character :: conversion = 's' ! conversion specifier
22 end type
23
24 contains
25
26 subroutine builtin_printf(cmd, shell)
27 use iso_fortran_env, only: error_unit
28 type(command_t), intent(in) :: cmd
29 type(shell_state_t), intent(inout) :: shell
30
31 character(len=4096) :: format_string
32 character(len=:), allocatable :: output_buffer
33 integer :: arg_index, prev_arg_index, output_len, format_string_len
34 integer, allocatable :: arg_lengths(:)
35 integer :: i, buf_size
36 logical :: fmt_error
37
38 if (cmd%num_tokens < 2) then
39 write(error_unit, '(a)') 'printf: usage: printf FORMAT [ARGUMENTS...]'
40 shell%last_exit_status = 2
41 return
42 end if
43
44 format_string = cmd%tokens(2)
45 ! Determine format string length:
46 ! - token_lengths can be wrong for strings with shell-processed escapes
47 ! - len_trim strips intentional trailing spaces
48 ! Solution: Use len_trim as base. For quoted tokens ending with space,
49 ! try to preserve trailing spaces if token_lengths is close to len_trim
50 ! (difference of 1-2 suggests trailing spaces, not escape processing)
51 format_string_len = len_trim(format_string)
52 if (allocated(cmd%token_quoted) .and. size(cmd%token_quoted) >= 2) then
53 if (cmd%token_quoted(2)) then
54 ! Quoted token - check for trailing spaces to preserve
55 if (allocated(cmd%token_lengths) .and. size(cmd%token_lengths) >= 2) then
56 ! Only extend for small differences (1-2 chars) that suggest trailing spaces
57 ! Larger differences likely indicate escape processing mismatch
58 if (cmd%token_lengths(2) > format_string_len .and. &
59 cmd%token_lengths(2) <= format_string_len + 2 .and. &
60 cmd%token_lengths(2) <= len(format_string)) then
61 ! Verify the extra chars are spaces
62 if (format_string(format_string_len+1:cmd%token_lengths(2)) == &
63 repeat(' ', cmd%token_lengths(2) - format_string_len)) then
64 format_string_len = cmd%token_lengths(2)
65 end if
66 end if
67 end if
68 end if
69 end if
70
71 ! Build array of argument lengths for preserving trailing spaces
72 allocate(arg_lengths(cmd%num_tokens))
73 do i = 1, cmd%num_tokens
74 if (allocated(cmd%token_lengths) .and. i <= size(cmd%token_lengths)) then
75 arg_lengths(i) = cmd%token_lengths(i)
76 else
77 arg_lengths(i) = len_trim(cmd%tokens(i))
78 end if
79 end do
80
81 arg_index = 3
82 fmt_error = .false.
83
84 ! Size buffer based on argument content — each format cycle can produce
85 ! up to sum(arg_lengths) + format_string_len of output
86 buf_size = format_string_len
87 do i = 3, cmd%num_tokens
88 buf_size = buf_size + arg_lengths(i)
89 end do
90 buf_size = max(buf_size * 2, 65536)
91 allocate(character(len=buf_size) :: output_buffer)
92
93 ! POSIX behavior: always output format string at least once,
94 ! then repeat for any remaining arguments
95 do
96 prev_arg_index = arg_index
97 call process_printf_format(format_string, format_string_len, cmd%tokens, &
98 cmd%num_tokens, arg_lengths, arg_index, &
99 output_buffer, output_len, fmt_error)
100 ! Output exactly output_len characters to preserve trailing spaces
101 if (output_len > 0) then
102 write(output_unit, '(a)', advance='no') output_buffer(1:output_len)
103 flush(output_unit)
104 end if
105
106 ! If no arguments were consumed, we're done (format has no specifiers or no more args)
107 if (arg_index == prev_arg_index .or. arg_index > cmd%num_tokens) exit
108 end do
109
110 deallocate(arg_lengths)
111 if (fmt_error) then
112 shell%last_exit_status = 1
113 else
114 shell%last_exit_status = 0
115 end if
116 end subroutine
117
118 subroutine process_printf_format(format_str, format_str_len, args, num_args, &
119 arg_lengths, start_arg, output, output_len, had_error)
120 character(len=*), intent(in) :: format_str
121 integer, intent(in) :: format_str_len
122 character(len=*), intent(in) :: args(:)
123 integer, intent(in) :: num_args
124 integer, intent(in) :: arg_lengths(:)
125 integer, intent(inout) :: start_arg
126 character(len=*), intent(out) :: output
127 integer, intent(out) :: output_len
128 logical, intent(inout), optional :: had_error
129
130 integer :: pos, output_pos, arg_index, format_len, fmt_len
131 integer :: current_arg_len
132 logical :: arg_error
133 character :: current_char, next_char
134 type(format_info_t) :: fmt_info
135 character(len=4096) :: arg_value, formatted_value
136
137 pos = 1
138 output_pos = 1
139 arg_index = start_arg
140 output = ''
141 arg_error = .false.
142 ! Use actual format string length to preserve trailing spaces
143 format_len = format_str_len
144
145 do while (pos <= format_len)
146 current_char = format_str(pos:pos)
147
148 if (current_char == '%' .and. pos < format_len) then
149 next_char = format_str(pos+1:pos+1)
150
151 if (next_char == '%') then
152 ! Escaped percent
153 if (output_pos <= len(output)) then
154 output(output_pos:output_pos) = '%'
155 output_pos = output_pos + 1
156 end if
157 pos = pos + 2
158 else
159 ! Parse format specifier
160 call parse_format_specifier(format_str, pos, fmt_info)
161
162 ! Handle dynamic width from argument
163 if (fmt_info%width_from_arg) then
164 if (arg_index <= num_args) then
165 read(args(arg_index), *, err=10, end=10) fmt_info%width
166 10 arg_index = arg_index + 1
167 end if
168 end if
169
170 ! Handle dynamic precision from argument
171 if (fmt_info%prec_from_arg) then
172 if (arg_index <= num_args) then
173 read(args(arg_index), *, err=20, end=20) fmt_info%precision
174 20 arg_index = arg_index + 1
175 end if
176 end if
177
178 ! Get argument value and its length
179 if (arg_index <= num_args) then
180 arg_value = args(arg_index)
181 if (arg_index <= size(arg_lengths)) then
182 current_arg_len = arg_lengths(arg_index)
183 else
184 current_arg_len = len_trim(arg_value)
185 end if
186 arg_index = arg_index + 1
187 else
188 arg_value = ''
189 current_arg_len = 0
190 end if
191
192 call format_argument(fmt_info, arg_value, current_arg_len, formatted_value, fmt_len, arg_error)
193 if (arg_error .and. present(had_error)) had_error = .true.
194
195 ! Append formatted value to output (use exact length to preserve padding)
196 call append_to_output_len(output, output_pos, formatted_value, fmt_len)
197 end if
198 else if (current_char == char(92) .and. pos < format_len) then
199 ! Handle escape sequences (backslash)
200 call process_escape_sequence(format_str, pos, output, output_pos)
201 else
202 ! Regular character
203 if (output_pos <= len(output)) then
204 output(output_pos:output_pos) = current_char
205 output_pos = output_pos + 1
206 end if
207 pos = pos + 1
208 end if
209 end do
210
211 ! Update start_arg to reflect how many arguments were consumed
212 start_arg = arg_index
213 ! Return actual output length (output_pos - 1 is the last written position)
214 output_len = output_pos - 1
215 end subroutine
216
217 subroutine parse_format_specifier(format_str, pos, fmt_info)
218 character(len=*), intent(in) :: format_str
219 integer, intent(inout) :: pos
220 type(format_info_t), intent(out) :: fmt_info
221
222 integer :: format_len, width_start
223 character :: c
224 logical :: parsing_flags, parsing_width, parsing_precision
225
226 ! Initialize
227 fmt_info%left_align = .false.
228 fmt_info%zero_pad = .false.
229 fmt_info%show_sign = .false.
230 fmt_info%space_sign = .false.
231 fmt_info%alternate = .false.
232 fmt_info%width = 0
233 fmt_info%precision = -1
234 fmt_info%width_from_arg = .false.
235 fmt_info%prec_from_arg = .false.
236 fmt_info%conversion = 's'
237
238 format_len = len_trim(format_str)
239 pos = pos + 1 ! Skip %
240
241 parsing_flags = .true.
242 parsing_width = .false.
243 parsing_precision = .false.
244
245 do while (pos <= format_len)
246 c = format_str(pos:pos)
247
248 ! Parse flags
249 if (parsing_flags) then
250 select case (c)
251 case ('-')
252 fmt_info%left_align = .true.
253 pos = pos + 1
254 cycle
255 case ('+')
256 fmt_info%show_sign = .true.
257 pos = pos + 1
258 cycle
259 case (' ')
260 fmt_info%space_sign = .true.
261 pos = pos + 1
262 cycle
263 case ('#')
264 fmt_info%alternate = .true.
265 pos = pos + 1
266 cycle
267 case ('0')
268 ! Only a flag if at start of width
269 if (.not. parsing_width) then
270 fmt_info%zero_pad = .true.
271 pos = pos + 1
272 cycle
273 end if
274 case default
275 parsing_flags = .false.
276 end select
277 end if
278
279 ! Parse width
280 if (c == '*') then
281 fmt_info%width_from_arg = .true.
282 pos = pos + 1
283 c = format_str(pos:pos)
284 else if (c >= '0' .and. c <= '9') then
285 width_start = pos
286 do while (pos <= format_len)
287 c = format_str(pos:pos)
288 if (c < '0' .or. c > '9') exit
289 pos = pos + 1
290 end do
291 read(format_str(width_start:pos-1), '(I10)') fmt_info%width
292 c = format_str(pos:pos)
293 end if
294
295 ! Parse precision
296 if (c == '.') then
297 pos = pos + 1
298 if (pos > format_len) exit
299 c = format_str(pos:pos)
300
301 if (c == '*') then
302 fmt_info%prec_from_arg = .true.
303 pos = pos + 1
304 else if (c >= '0' .and. c <= '9') then
305 width_start = pos
306 do while (pos <= format_len)
307 c = format_str(pos:pos)
308 if (c < '0' .or. c > '9') exit
309 pos = pos + 1
310 end do
311 read(format_str(width_start:pos-1), '(I10)') fmt_info%precision
312 else
313 fmt_info%precision = 0
314 end if
315 if (pos > format_len) exit
316 c = format_str(pos:pos)
317 end if
318
319 ! Check for conversion specifier
320 if (index('diouxXeEfFgGaAcspbq', c) > 0) then
321 fmt_info%conversion = c
322 pos = pos + 1
323 return
324 end if
325
326 ! Unknown character, skip
327 pos = pos + 1
328 end do
329 end subroutine
330
331 subroutine format_argument(fmt_info, arg_value, arg_len, formatted_value, formatted_len, had_error)
332 type(format_info_t), intent(in) :: fmt_info
333 character(len=*), intent(in) :: arg_value
334 integer, intent(in) :: arg_len ! Actual length of arg_value (to preserve trailing spaces)
335 character(len=*), intent(out) :: formatted_value
336 integer, intent(out) :: formatted_len
337 logical, intent(out), optional :: had_error
338
339 character(len=4096) :: raw_value, temp_value
340 integer :: int_val, status, val_len, pad_len, prec, actual_len
341 real(8) :: real_val
342 character :: pad_char
343
344 formatted_value = ''
345 raw_value = ''
346 formatted_len = 0
347 if (present(had_error)) had_error = .false.
348
349 ! Use provided arg_len to preserve trailing spaces
350 actual_len = arg_len
351 if (actual_len <= 0 .or. actual_len > len(arg_value)) then
352 actual_len = len_trim(arg_value)
353 end if
354
355 select case (fmt_info%conversion)
356 case ('s')
357 ! String - use actual_len to preserve trailing spaces
358 if (actual_len > 0 .and. actual_len <= len(raw_value)) then
359 raw_value = arg_value(1:actual_len)
360 else
361 raw_value = arg_value
362 end if
363 ! Apply precision (truncation for strings)
364 if (fmt_info%precision >= 0 .and. fmt_info%precision < actual_len) then
365 raw_value = raw_value(1:fmt_info%precision)
366 actual_len = fmt_info%precision
367 end if
368
369 case ('b')
370 ! %b: interpret backslash escapes in argument
371 call interpret_escapes(arg_value, raw_value)
372
373 case ('c')
374 ! Character
375 if (actual_len > 0) then
376 raw_value = arg_value(1:1)
377 else
378 raw_value = ''
379 end if
380
381 case ('d', 'i')
382 ! Integer
383 call parse_integer(arg_value, int_val, status)
384 if (status == 0) then
385 call format_integer(int_val, fmt_info, raw_value)
386 else
387 write(error_unit, '(a,a,a)') 'fortsh: printf: ', trim(arg_value), ': invalid number'
388 if (present(had_error)) had_error = .true.
389 raw_value = '0'
390 end if
391
392 case ('o')
393 ! Octal
394 call parse_integer(arg_value, int_val, status)
395 if (status == 0) then
396 write(temp_value, '(O0)') int_val
397 raw_value = trim(temp_value)
398 if (fmt_info%alternate .and. int_val /= 0) then
399 raw_value = '0' // trim(raw_value)
400 end if
401 else
402 write(error_unit, '(a,a,a)') 'fortsh: printf: ', trim(arg_value), ': invalid number'
403 if (present(had_error)) had_error = .true.
404 raw_value = '0'
405 end if
406
407 case ('x')
408 ! Hex lowercase
409 call parse_integer(arg_value, int_val, status)
410 if (status == 0) then
411 write(temp_value, '(Z0)') int_val
412 raw_value = to_lowercase(trim(temp_value))
413 if (fmt_info%alternate .and. int_val /= 0) then
414 raw_value = '0x' // trim(raw_value)
415 end if
416 else
417 write(error_unit, '(a,a,a)') 'fortsh: printf: ', trim(arg_value), ': invalid number'
418 if (present(had_error)) had_error = .true.
419 raw_value = '0'
420 end if
421
422 case ('X')
423 ! Hex uppercase
424 call parse_integer(arg_value, int_val, status)
425 if (status == 0) then
426 write(temp_value, '(Z0)') int_val
427 raw_value = to_uppercase(trim(temp_value))
428 if (fmt_info%alternate .and. int_val /= 0) then
429 raw_value = '0X' // trim(raw_value)
430 end if
431 else
432 write(error_unit, '(a,a,a)') 'fortsh: printf: ', trim(arg_value), ': invalid number'
433 if (present(had_error)) had_error = .true.
434 raw_value = '0'
435 end if
436
437 case ('u')
438 ! Unsigned integer (treat as regular integer in Fortran)
439 call parse_integer(arg_value, int_val, status)
440 if (status == 0) then
441 if (int_val < 0) int_val = int_val + 2147483647 + 1 ! Approximate unsigned
442 write(raw_value, '(I0)') int_val
443 else
444 write(error_unit, '(a,a,a)') 'fortsh: printf: ', trim(arg_value), ': invalid number'
445 if (present(had_error)) had_error = .true.
446 raw_value = '0'
447 end if
448
449 case ('f', 'F')
450 ! Fixed-point notation
451 read(arg_value, *, iostat=status) real_val
452 prec = 6
453 if (fmt_info%precision >= 0) prec = fmt_info%precision
454 if (status == 0) then
455 call format_float_fixed(real_val, prec, raw_value)
456 else
457 if (len_trim(arg_value) > 0) then
458 write(error_unit, '(a,a,a)') 'fortsh: printf: ', trim(arg_value), ': invalid number'
459 if (present(had_error)) had_error = .true.
460 end if
461 raw_value = '0.' // repeat('0', prec)
462 end if
463
464 case ('e')
465 ! Scientific notation lowercase
466 read(arg_value, *, iostat=status) real_val
467 prec = 6
468 if (fmt_info%precision >= 0) prec = fmt_info%precision
469 if (status == 0) then
470 call format_float_exp(real_val, prec, raw_value)
471 raw_value = to_lowercase(raw_value)
472 else
473 if (len_trim(arg_value) > 0) then
474 write(error_unit, '(a,a,a)') 'fortsh: printf: ', trim(arg_value), ': invalid number'
475 if (present(had_error)) had_error = .true.
476 end if
477 raw_value = '0.' // repeat('0', prec) // 'e+00'
478 end if
479
480 case ('E')
481 ! Scientific notation uppercase
482 read(arg_value, *, iostat=status) real_val
483 prec = 6
484 if (fmt_info%precision >= 0) prec = fmt_info%precision
485 if (status == 0) then
486 call format_float_exp(real_val, prec, raw_value)
487 raw_value = to_uppercase(raw_value)
488 else
489 if (len_trim(arg_value) > 0) then
490 write(error_unit, '(a,a,a)') 'fortsh: printf: ', trim(arg_value), ': invalid number'
491 if (present(had_error)) had_error = .true.
492 end if
493 raw_value = '0.' // repeat('0', prec) // 'E+00'
494 end if
495
496 case ('g', 'G')
497 ! General format
498 read(arg_value, *, iostat=status) real_val
499 prec = 6
500 if (fmt_info%precision >= 0) prec = fmt_info%precision
501 if (status == 0) then
502 if (abs(real_val) >= 0.0001d0 .and. abs(real_val) < 1000000.0d0) then
503 call format_float_fixed(real_val, prec, raw_value)
504 else
505 call format_float_exp(real_val, prec, raw_value)
506 end if
507 if (fmt_info%conversion == 'g') then
508 raw_value = to_lowercase(raw_value)
509 else
510 raw_value = to_uppercase(raw_value)
511 end if
512 else
513 if (len_trim(arg_value) > 0) then
514 write(error_unit, '(a,a,a)') 'fortsh: printf: ', trim(arg_value), ': invalid number'
515 if (present(had_error)) had_error = .true.
516 end if
517 raw_value = '0'
518 end if
519
520 case ('q')
521 ! Shell-quoted string: escape special characters with backslash
522 block
523 integer :: qi, qo
524 qo = 1
525 raw_value = ''
526 do qi = 1, actual_len
527 select case (arg_value(qi:qi))
528 case (' ', '!', '"', '#', '$', '&', "'", '(', ')', '*', ';', '<', '>', '?', &
529 '[', '\', ']', '^', '`', '{', '|', '}', '~')
530 raw_value(qo:qo) = '\'
531 qo = qo + 1
532 raw_value(qo:qo) = arg_value(qi:qi)
533 case default
534 raw_value(qo:qo) = arg_value(qi:qi)
535 end select
536 qo = qo + 1
537 end do
538 actual_len = qo - 1
539 end block
540
541 case default
542 ! Unknown format, treat as string
543 raw_value = arg_value
544 end select
545
546 ! Apply width padding
547 ! For string types, use actual_len to preserve trailing spaces
548 if (fmt_info%conversion == 's') then
549 val_len = actual_len
550 else
551 val_len = len_trim(raw_value)
552 end if
553
554 if (fmt_info%width > val_len) then
555 pad_len = fmt_info%width - val_len
556 if (fmt_info%zero_pad .and. .not. fmt_info%left_align .and. &
557 index('diouxXeEfFgG', fmt_info%conversion) > 0) then
558 pad_char = '0'
559 else
560 pad_char = ' '
561 end if
562
563 if (fmt_info%left_align) then
564 ! For strings, use actual length; for others, use trim
565 if (fmt_info%conversion == 's' .and. val_len > 0) then
566 formatted_value = raw_value(1:val_len) // repeat(' ', pad_len)
567 else
568 formatted_value = trim(raw_value) // repeat(' ', pad_len)
569 end if
570 formatted_len = fmt_info%width
571 else
572 ! For zero padding with sign, put sign before zeros
573 if (pad_char == '0' .and. len_trim(raw_value) > 0) then
574 if (raw_value(1:1) == '-' .or. raw_value(1:1) == '+') then
575 formatted_value = raw_value(1:1) // repeat('0', pad_len) // trim(raw_value(2:))
576 else
577 formatted_value = repeat('0', pad_len) // trim(raw_value)
578 end if
579 else
580 if (fmt_info%conversion == 's' .and. val_len > 0) then
581 formatted_value = repeat(pad_char, pad_len) // raw_value(1:val_len)
582 else
583 formatted_value = repeat(pad_char, pad_len) // trim(raw_value)
584 end if
585 end if
586 formatted_len = fmt_info%width
587 end if
588 else
589 ! No padding needed - use exact length
590 if (fmt_info%conversion == 's' .and. val_len > 0) then
591 formatted_value = raw_value(1:val_len)
592 else
593 formatted_value = trim(raw_value)
594 end if
595 formatted_len = val_len
596 end if
597 end subroutine
598
599 subroutine parse_integer(arg_value, int_val, status)
600 character(len=*), intent(in) :: arg_value
601 integer, intent(out) :: int_val
602 integer, intent(out) :: status
603
604 character(len=256) :: clean_arg
605
606 clean_arg = adjustl(arg_value)
607 int_val = 0
608 status = 0
609
610 if (len_trim(clean_arg) == 0) then
611 int_val = 0
612 return
613 end if
614
615 ! Handle character constants like 'A
616 if (clean_arg(1:1) == "'" .and. len_trim(clean_arg) >= 2) then
617 int_val = ichar(clean_arg(2:2))
618 return
619 end if
620
621 ! Handle hex (0x...) and octal (0...) prefixes
622 if (len_trim(clean_arg) >= 2) then
623 if (clean_arg(1:2) == '0x' .or. clean_arg(1:2) == '0X') then
624 read(clean_arg(3:), '(Z20)', iostat=status) int_val
625 return
626 else if (clean_arg(1:1) == '0' .and. len_trim(clean_arg) > 1) then
627 ! Could be octal, try it
628 read(clean_arg(2:), '(O20)', iostat=status) int_val
629 if (status == 0) return
630 end if
631 end if
632
633 ! Standard decimal
634 read(clean_arg, *, iostat=status) int_val
635 end subroutine
636
637 subroutine format_integer(int_val, fmt_info, result)
638 integer, intent(in) :: int_val
639 type(format_info_t), intent(in) :: fmt_info
640 character(len=*), intent(out) :: result
641
642 character(len=32) :: temp
643
644 if (int_val >= 0) then
645 write(temp, '(I0)') int_val
646 if (fmt_info%show_sign) then
647 result = '+' // trim(temp)
648 else if (fmt_info%space_sign) then
649 result = ' ' // trim(temp)
650 else
651 result = trim(temp)
652 end if
653 else
654 write(temp, '(I0)') int_val
655 result = trim(temp)
656 end if
657 end subroutine
658
659 subroutine format_float_fixed(val, precision, result)
660 real(8), intent(in) :: val
661 integer, intent(in) :: precision
662 character(len=*), intent(out) :: result
663
664 character(len=64) :: fmt_str, temp
665
666 write(fmt_str, '(a,i0,a)') '(F0.', precision, ')'
667 write(temp, fmt_str) val
668 result = adjustl(temp)
669 end subroutine
670
671 subroutine format_float_exp(val, precision, result)
672 real(8), intent(in) :: val
673 integer, intent(in) :: precision
674 character(len=*), intent(out) :: result
675
676 character(len=64) :: fmt_str, temp
677
678 write(fmt_str, '(a,i0,a,i0,a)') '(ES', precision+8, '.', precision, ')'
679 write(temp, fmt_str) val
680 result = adjustl(temp)
681 end subroutine
682
683 subroutine interpret_escapes(input, output)
684 character(len=*), intent(in) :: input
685 character(len=*), intent(out) :: output
686
687 integer :: pos, out_pos, input_len, octal_val, i
688 character :: c
689 character(len=3) :: octal_str
690
691 pos = 1
692 out_pos = 1
693 input_len = len_trim(input)
694 output = ''
695
696 do while (pos <= input_len .and. out_pos <= len(output))
697 c = input(pos:pos)
698
699 if (c == char(92) .and. pos < input_len) then ! backslash
700 pos = pos + 1
701 c = input(pos:pos)
702
703 select case (c)
704 case ('n')
705 output(out_pos:out_pos) = char(10)
706 case ('t')
707 output(out_pos:out_pos) = char(9)
708 case ('r')
709 output(out_pos:out_pos) = char(13)
710 case ('b')
711 output(out_pos:out_pos) = char(8)
712 case ('a')
713 output(out_pos:out_pos) = char(7)
714 case ('f')
715 output(out_pos:out_pos) = char(12)
716 case ('e', 'E')
717 output(out_pos:out_pos) = char(27)
718 case ('v')
719 output(out_pos:out_pos) = char(11)
720 case (char(92)) ! backslash
721 output(out_pos:out_pos) = char(92)
722 case ('x')
723 ! Hex escape: \xHH
724 block
725 integer :: hv, hd
726 character :: hc
727 hv = 0; hd = 0
728 pos = pos + 1
729 do while (pos <= input_len .and. hd < 2)
730 hc = input(pos:pos)
731 if (hc >= '0' .and. hc <= '9') then
732 hv = hv * 16 + (ichar(hc) - ichar('0'))
733 else if (hc >= 'a' .and. hc <= 'f') then
734 hv = hv * 16 + (ichar(hc) - ichar('a') + 10)
735 else if (hc >= 'A' .and. hc <= 'F') then
736 hv = hv * 16 + (ichar(hc) - ichar('A') + 10)
737 else
738 exit
739 end if
740 pos = pos + 1
741 hd = hd + 1
742 end do
743 if (hd > 0 .and. hv <= 255) then
744 output(out_pos:out_pos) = char(hv)
745 end if
746 out_pos = out_pos + 1
747 cycle
748 end block
749 case ('0', '1', '2', '3', '4', '5', '6', '7')
750 ! Octal escape
751 octal_str = c
752 do i = 2, 3
753 if (pos + i - 1 <= input_len) then
754 c = input(pos + i - 1:pos + i - 1)
755 if (c >= '0' .and. c <= '7') then
756 octal_str(i:i) = c
757 else
758 exit
759 end if
760 else
761 exit
762 end if
763 end do
764 read(octal_str, '(O3)', err=30) octal_val
765 output(out_pos:out_pos) = char(mod(octal_val, 256))
766 pos = pos + len_trim(octal_str) - 1
767 go to 40
768 30 output(out_pos:out_pos) = c
769 40 continue
770 case default
771 output(out_pos:out_pos) = c
772 end select
773 out_pos = out_pos + 1
774 pos = pos + 1
775 else
776 output(out_pos:out_pos) = c
777 out_pos = out_pos + 1
778 pos = pos + 1
779 end if
780 end do
781 end subroutine
782
783 subroutine process_escape_sequence(format_str, pos, output, output_pos)
784 character(len=*), intent(in) :: format_str
785 integer, intent(inout) :: pos
786 character(len=*), intent(inout) :: output
787 integer, intent(inout) :: output_pos
788
789 character :: escape_char
790 integer :: format_len, octal_val, i
791 character(len=3) :: octal_str
792
793 format_len = len_trim(format_str)
794
795 if (pos >= format_len) then
796 pos = pos + 1
797 return
798 end if
799
800 pos = pos + 1 ! Skip backslash
801 escape_char = format_str(pos:pos)
802
803 select case (escape_char)
804 case ('n')
805 output(output_pos:output_pos) = char(10) ! newline
806 case ('t')
807 output(output_pos:output_pos) = char(9) ! tab
808 case ('r')
809 output(output_pos:output_pos) = char(13) ! carriage return
810 case ('b')
811 output(output_pos:output_pos) = char(8) ! backspace
812 case ('a')
813 output(output_pos:output_pos) = char(7) ! bell
814 case ('f')
815 output(output_pos:output_pos) = char(12) ! form feed
816 case ('e', 'E')
817 output(output_pos:output_pos) = char(27) ! escape
818 case ('v')
819 output(output_pos:output_pos) = char(11) ! vertical tab
820 case (char(92)) ! backslash
821 output(output_pos:output_pos) = char(92)
822 case ('"')
823 output(output_pos:output_pos) = '"'
824 case ("'")
825 output(output_pos:output_pos) = "'"
826 case ('0', '1', '2', '3', '4', '5', '6', '7')
827 ! Octal escape sequence \NNN
828 octal_str = escape_char
829 do i = 2, 3
830 if (pos + i - 1 <= format_len) then
831 escape_char = format_str(pos + i - 1:pos + i - 1)
832 if (escape_char >= '0' .and. escape_char <= '7') then
833 octal_str(i:i) = escape_char
834 else
835 exit
836 end if
837 else
838 exit
839 end if
840 end do
841 read(octal_str, '(O3)', err=50) octal_val
842 output(output_pos:output_pos) = char(mod(octal_val, 256))
843 pos = pos + len_trim(octal_str) - 1
844 go to 60
845 50 output(output_pos:output_pos) = format_str(pos:pos)
846 60 continue
847 case ('x')
848 ! Hex escape sequence \xNN
849 block
850 integer :: hval, hd, hc
851 hval = 0
852 hd = 0
853 do while (hd < 2 .and. pos + hd + 1 <= format_len)
854 hc = ichar(format_str(pos+hd+1:pos+hd+1))
855 if (hc >= ichar('0') .and. hc <= ichar('9')) then
856 hval = hval * 16 + hc - ichar('0')
857 hd = hd + 1
858 else if (hc >= ichar('a') .and. hc <= ichar('f')) then
859 hval = hval * 16 + hc - ichar('a') + 10
860 hd = hd + 1
861 else if (hc >= ichar('A') .and. hc <= ichar('F')) then
862 hval = hval * 16 + hc - ichar('A') + 10
863 hd = hd + 1
864 else
865 exit
866 end if
867 end do
868 if (hd > 0) then
869 output(output_pos:output_pos) = achar(mod(hval, 256))
870 pos = pos + hd
871 else
872 output(output_pos:output_pos) = char(92)
873 output_pos = output_pos + 1
874 output(output_pos:output_pos) = 'x'
875 end if
876 end block
877 case default
878 ! Unknown escape - per POSIX, output both backslash and character
879 output(output_pos:output_pos) = char(92) ! backslash
880 output_pos = output_pos + 1
881 output(output_pos:output_pos) = escape_char
882 end select
883
884 output_pos = output_pos + 1
885 pos = pos + 1
886 end subroutine
887
888 subroutine append_to_output(output, output_pos, value)
889 character(len=*), intent(inout) :: output
890 integer, intent(inout) :: output_pos
891 character(len=*), intent(in) :: value
892
893 integer :: val_len
894
895 val_len = len_trim(value)
896 if (val_len == 0) return
897
898 if (output_pos + val_len - 1 <= len(output)) then
899 output(output_pos:output_pos + val_len - 1) = value(1:val_len)
900 output_pos = output_pos + val_len
901 end if
902 end subroutine
903
904 subroutine append_to_output_len(output, output_pos, value, value_len)
905 character(len=*), intent(inout) :: output
906 integer, intent(inout) :: output_pos
907 character(len=*), intent(in) :: value
908 integer, intent(in) :: value_len
909
910 if (value_len <= 0) return
911
912 if (output_pos + value_len - 1 <= len(output)) then
913 output(output_pos:output_pos + value_len - 1) = value(1:value_len)
914 output_pos = output_pos + value_len
915 end if
916 end subroutine
917
918 function to_lowercase(str) result(lower_str)
919 character(len=*), intent(in) :: str
920 character(len=len(str)) :: lower_str
921 integer :: i
922
923 lower_str = str
924 do i = 1, len_trim(str)
925 if (str(i:i) >= 'A' .and. str(i:i) <= 'Z') then
926 lower_str(i:i) = char(ichar(str(i:i)) + 32)
927 end if
928 end do
929 end function
930
931 function to_uppercase(str) result(upper_str)
932 character(len=*), intent(in) :: str
933 character(len=len(str)) :: upper_str
934 integer :: i
935
936 upper_str = str
937 do i = 1, len_trim(str)
938 if (str(i:i) >= 'a' .and. str(i:i) <= 'z') then
939 upper_str(i:i) = char(ichar(str(i:i)) - 32)
940 end if
941 end do
942 end function
943
944 end module printf_builtin