Fortran · 6369 bytes Raw Blame History
1 module command_capture
2 use iso_c_binding
3 use iso_fortran_env, only: error_unit
4 use shell_types
5 use system_interface
6 implicit none
7
8 ! Define c_ssize_t if not available
9 integer, parameter :: c_ssize_t = c_long
10
11 ! Interface for the command execution callback
12 abstract interface
13 subroutine execute_callback(shell, command, exit_status)
14 import :: shell_state_t
15 type(shell_state_t), intent(inout) :: shell
16 character(len=*), intent(in) :: command
17 integer, intent(out) :: exit_status
18 end subroutine execute_callback
19 end interface
20
21 ! Module variable to store the callback
22 procedure(execute_callback), pointer :: execute_command_ptr => null()
23
24 interface
25 function pipe(fds) bind(c, name='pipe')
26 import :: c_int
27 integer(c_int), dimension(2), intent(out) :: fds
28 integer(c_int) :: pipe
29 end function
30 function dup(fd) bind(c, name='dup')
31 import :: c_int
32 integer(c_int), value :: fd
33 integer(c_int) :: dup
34 end function
35 function dup2(oldfd, newfd) bind(c, name='dup2')
36 import :: c_int
37 integer(c_int), value :: oldfd, newfd
38 integer(c_int) :: dup2
39 end function
40 function close(fd) bind(c, name='close')
41 import :: c_int
42 integer(c_int), value :: fd
43 integer(c_int) :: close
44 end function
45 function fork() bind(c, name='fork')
46 import :: c_pid_t
47 integer(c_pid_t) :: fork
48 end function
49 function waitpid(pid, stat_loc, options) bind(c, name='waitpid')
50 import :: c_pid_t, c_int, c_ptr
51 integer(c_pid_t), value :: pid
52 type(c_ptr), value :: stat_loc
53 integer(c_int), value :: options
54 integer(c_pid_t) :: waitpid
55 end function
56 function fdopen(fd, mode) bind(c, name='fdopen')
57 import :: c_int, c_ptr, c_char
58 integer(c_int), value :: fd
59 character(kind=c_char), dimension(*) :: mode
60 type(c_ptr) :: fdopen
61 end function
62 function read(fd, buf, count) bind(c, name='read')
63 import :: c_int, c_ptr, c_size_t, c_ssize_t
64 integer(c_int), value :: fd
65 type(c_ptr), value :: buf
66 integer(c_size_t), value :: count
67 integer(c_ssize_t) :: read
68 end function
69 end interface
70
71 contains
72
73 ! Set the execution callback
74 subroutine set_execute_callback(callback)
75 procedure(execute_callback) :: callback
76 execute_command_ptr => callback
77 end subroutine set_execute_callback
78
79 subroutine execute_command_and_capture(shell, command, output, output_len)
80 type(shell_state_t), intent(inout) :: shell
81 character(len=*), intent(in) :: command
82 character(len=:), allocatable, intent(out) :: output
83 integer, intent(out), optional :: output_len ! Actual content length
84
85 integer(c_int) :: pipe_fds(2)
86 integer(c_int) :: ret, exit_status
87 integer(c_pid_t) :: pid
88 character(kind=c_char), target :: buffer(4096)
89 integer(c_ssize_t) :: bytes_read
90 integer :: total_len, i, buf_cap
91 integer(c_int), target :: wstatus
92 type(c_ptr) :: wstatus_ptr
93 character(len=:), allocatable :: buf
94
95 output = ''
96 if (present(output_len)) output_len = 0
97
98 ! Check if callback is set
99 if (.not. associated(execute_command_ptr)) then
100 shell%last_exit_status = 127
101 return
102 end if
103
104 ! Create a pipe
105 ret = pipe(pipe_fds)
106 if (ret /= 0) then
107 shell%last_exit_status = 1
108 return
109 end if
110
111 ! Fork a child process
112 pid = fork()
113
114 if (pid < 0) then
115 ! Fork failed
116 ret = close(pipe_fds(1)) ! Close read end
117 ret = close(pipe_fds(2)) ! Close write end
118 shell%last_exit_status = 1
119 return
120 else if (pid == 0) then
121 ! Child process
122 ! Close read end of pipe
123 ret = close(pipe_fds(1))
124
125 ! Redirect stdout to pipe write end
126 ret = dup2(pipe_fds(2), 1)
127
128 ! Close the original pipe write end
129 ret = close(pipe_fds(2))
130
131 ! Mark that we're in a capture child (suppress errexit messages)
132 shell%in_capture_child = .true.
133
134 ! POSIX: errexit (set -e) IS inherited in command substitution subshells
135 ! When errexit triggers in the subshell, it exits with the failing status
136
137 ! Execute the command
138 call execute_command_ptr(shell, command, exit_status)
139
140 ! Exit child with the command's exit status
141 call c_exit(exit_status)
142 else
143 ! Parent process
144 ! Close write end of pipe
145 ret = close(pipe_fds(2))
146
147 ! Read output from pipe into growing buffer
148 buf_cap = 8192
149 allocate(character(len=buf_cap) :: buf)
150 total_len = 0
151 do
152 bytes_read = read(pipe_fds(1), c_loc(buffer), int(size(buffer), c_size_t))
153 if (bytes_read <= 0) exit
154
155 ! Grow buffer if needed
156 if (total_len + int(bytes_read) > buf_cap) then
157 call grow_capture_buffer(buf, buf_cap, total_len)
158 end if
159
160 ! Copy buffer to output
161 do i = 1, int(bytes_read)
162 total_len = total_len + 1
163 buf(total_len:total_len) = buffer(i)
164 end do
165 end do
166
167 ! Close read end
168 ret = close(pipe_fds(1))
169
170 ! Wait for child to complete
171 wstatus_ptr = c_loc(wstatus)
172 pid = waitpid(pid, wstatus_ptr, 0)
173
174 ! Extract exit status (WEXITSTATUS macro equivalent)
175 if (pid > 0) then
176 shell%last_exit_status = iand(ishft(wstatus, -8), 255)
177 else
178 shell%last_exit_status = 1
179 end if
180 end if
181
182 ! Remove trailing newlines for command substitution
183 do while (total_len > 0)
184 if (buf(total_len:total_len) /= char(10)) exit
185 total_len = total_len - 1
186 end do
187
188 if (total_len > 0) then
189 output = buf(1:total_len)
190 else
191 output = ''
192 end if
193
194 if (allocated(buf)) deallocate(buf)
195
196 ! Return the actual content length (preserves trailing whitespace info)
197 if (present(output_len)) output_len = total_len
198
199 end subroutine execute_command_and_capture
200
201 subroutine grow_capture_buffer(buf, cap, content_len)
202 character(len=:), allocatable, intent(inout) :: buf
203 integer, intent(inout) :: cap
204 integer, intent(in) :: content_len
205 character(len=:), allocatable :: tmp
206 integer :: new_cap
207
208 new_cap = cap * 2
209 allocate(character(len=new_cap) :: tmp)
210 if (content_len > 0) tmp(1:content_len) = buf(1:content_len)
211 call move_alloc(tmp, buf)
212 cap = new_cap
213 end subroutine
214
215 end module command_capture