fortrangoingonforty/fgof-process / afad548

Browse files

Stabilize process API

Authored by espadonne
SHA
afad5481c62901707dfcb2af70c314947a908c28
Parents
02c19e3
Tree
ac4961c

8 changed files

StatusFile+-
M .gitignore 1 0
M README.md 22 10
M docs/ROADMAP.md 11 13
M src/fgof_process.f90 87 33
A src/fgof_process_types.f90 66 0
M test/test_command.f90 2 1
A test/test_shell.f90 14 0
A test/test_validation.f90 33 0
.gitignoremodified
@@ -1,2 +1,3 @@
11
 build/
2
+.docs/
23
 .DS_Store
README.mdmodified
@@ -2,17 +2,20 @@
22
 
33
 POSIX-first process and subprocess helpers for modern Fortran applications.
44
 
5
-`fgof-process` is intended to be a small, standalone library that gives Fortran tools a more ergonomic process API than raw `execute_command_line` or ad hoc C interop.
5
+`fgof-process` is intended to be a small, standalone library that gives Fortran tools a more ergonomic process API than raw `execute_command_line`, thin POSIX wrappers, or experimental process surfaces that still feel too low-level for real tooling.
66
 
7
-Initial scope:
7
+Current v1 target:
88
 
99
 - argv-first command construction
10
-- synchronous process execution
10
+- synchronous process execution on macOS and Linux
11
+- explicit shell-command convenience through `/bin/sh -c`
1112
 - environment overrides
1213
 - working-directory overrides
14
+- stdin support
1315
 - stdout and stderr capture
1416
 - exit-status reporting
1517
 - timeout-aware execution
18
+- structured, result-first errors
1619
 
1720
 Future scope:
1821
 
@@ -20,12 +23,13 @@ Future scope:
2023
 - async spawn and wait
2124
 - signal helpers
2225
 - PTY-friendly integration points for a future `fgof-pty`
26
+- a dedicated `fgof-proc-test` companion package
2327
 
2428
 ## Status
2529
 
26
-Early scaffold.
30
+Sprint 00 and 01 scaffold.
2731
 
28
-This repository is being created as the first package in the FortranGoingOnForty reusable library family and is intended to be consumed standalone or via the umbrella catalog repo at `lib-modules`.
32
+This repository is the first package in the FortranGoingOnForty reusable library family and is intended to be consumed standalone or via the umbrella catalog repo at `lib-modules`.
2933
 
3034
 ## Package Goals
3135
 
@@ -33,24 +37,32 @@ This repository is being created as the first package in the FortranGoingOnForty
3337
 - prefer argv-based execution over shell-string execution
3438
 - make tests easy to write
3539
 - stay useful for shells, editors, TUI apps, and developer tooling
40
+- close a real ecosystem gap rather than mirroring `stdlib_system`
3641
 
37
-## Planned API Shape
42
+## Public API Shape
3843
 
3944
 Primary module:
4045
 
4146
 - `fgof_process`
4247
 
43
-Initial public types:
48
+Public types:
4449
 
4550
 - `process_command`
46
-- `process_result`
4751
 - `process_options`
52
+- `process_result`
4853
 
49
-Initial public procedures:
54
+Public procedures:
5055
 
5156
 - `command`
57
+- `shell`
5258
 - `run`
5359
 
60
+## Current Boundaries
61
+
62
+- Direct POSIX backend is planned for v1.
63
+- `stdlib_system` can inform behavior, but is not a required backend.
64
+- Async process handles are explicitly deferred until after the sync-first release.
65
+
5466
 ## Development Notes
5567
 
5668
 - POSIX first: macOS and Linux
@@ -59,4 +71,4 @@ Initial public procedures:
5971
 
6072
 ## License
6173
 
62
-TBD
74
+MIT
docs/ROADMAP.mdmodified
@@ -2,22 +2,20 @@
22
 
33
 ## v0.1
44
 
5
-- establish standalone repo and `fpm` package
6
-- define initial public types
7
-- implement argv-first command construction
8
-- implement synchronous `run`
9
-- add basic tests
5
+- establish the stable public API
6
+- implement synchronous argv-first execution
7
+- add shell convenience via explicit `shell()`
8
+- support cwd, env, stdin, stdout, stderr, and timeouts
9
+- ship a documented sync-first library that tool authors actually want to use
1010
 
1111
 ## v0.2
1212
 
13
-- stdout and stderr capture
14
-- cwd and env overrides
15
-- timeout handling
16
-- richer errors
13
+- async process handles
14
+- richer process control
15
+- signal helpers
16
+- stronger process-fixture testing support
1717
 
1818
 ## v0.3
1919
 
20
-- async spawn or wait
21
-- process handles
22
-- kill and terminate helpers
23
-- integration-test utilities
20
+- companion packages such as `fgof-proc-test`
21
+- possible PTY-facing integrations
src/fgof_process.f90modified
@@ -1,48 +1,47 @@
11
 module fgof_process
2
+  use fgof_process_types, only : &
3
+    FGOF_PROCESS_ERR_INVALID_COMMAND, &
4
+    FGOF_PROCESS_ERR_INVALID_OPTION, &
5
+    FGOF_PROCESS_ERR_NOT_IMPLEMENTED, &
6
+    FGOF_PROCESS_MODE_ARGV, &
7
+    FGOF_PROCESS_MODE_NONE, &
8
+    FGOF_PROCESS_MODE_SHELL, &
9
+    FGOF_PROCESS_OK, &
10
+    process_command, &
11
+    process_options, &
12
+    process_result
213
   implicit none
314
   private
415
 
516
   public :: process_command
617
   public :: process_options
718
   public :: process_result
19
+  public :: FGOF_PROCESS_MODE_NONE
20
+  public :: FGOF_PROCESS_MODE_ARGV
21
+  public :: FGOF_PROCESS_MODE_SHELL
22
+  public :: FGOF_PROCESS_OK
23
+  public :: FGOF_PROCESS_ERR_INVALID_COMMAND
24
+  public :: FGOF_PROCESS_ERR_INVALID_OPTION
25
+  public :: FGOF_PROCESS_ERR_NOT_IMPLEMENTED
826
   public :: command
27
+  public :: shell
928
   public :: run
1029
 
11
-  type :: process_command
12
-    character(len=:), allocatable :: program
13
-    character(len=:), allocatable :: argv(:)
14
-  end type process_command
15
-
16
-  type :: process_options
17
-    character(len=:), allocatable :: cwd
18
-    logical :: capture_stdout = .false.
19
-    logical :: capture_stderr = .false.
20
-    integer :: timeout_ms = 0
21
-    character(len=:), allocatable :: env(:)
22
-  end type process_options
23
-
24
-  type :: process_result
25
-    integer :: exit_code = -1
26
-    logical :: timed_out = .false.
27
-    character(len=:), allocatable :: stdout
28
-    character(len=:), allocatable :: stderr
29
-    character(len=:), allocatable :: error_message
30
-  end type process_result
31
-
3230
 contains
3331
 
3432
   function command(program, argv) result(cmd)
3533
     character(len=*), intent(in) :: program
3634
     character(len=*), intent(in), optional :: argv(:)
3735
     type(process_command) :: cmd
36
+    integer :: arg_len
3837
     integer :: i
3938
 
39
+    cmd%mode = FGOF_PROCESS_MODE_ARGV
4040
     cmd%program = trim(program)
4141
 
4242
     if (present(argv)) then
43
-      allocate(character(len=len_trim(program)) :: cmd%argv(0))
44
-      deallocate(cmd%argv)
45
-      allocate(character(len=max(1, max_trimmed_length(argv))) :: cmd%argv(size(argv)))
43
+      arg_len = max_trimmed_length(argv)
44
+      allocate(character(len=arg_len) :: cmd%argv(size(argv)))
4645
       do i = 1, size(argv)
4746
         cmd%argv(i) = trim(argv(i))
4847
       end do
@@ -51,30 +50,85 @@ contains
5150
     end if
5251
   end function command
5352
 
53
+  function shell(command_line) result(cmd)
54
+    character(len=*), intent(in) :: command_line
55
+    type(process_command) :: cmd
56
+
57
+    cmd%mode = FGOF_PROCESS_MODE_SHELL
58
+    cmd%command_line = trim(command_line)
59
+    allocate(character(len=1) :: cmd%argv(0))
60
+  end function shell
61
+
5462
   function run(cmd, options) result(res)
5563
     type(process_command), intent(in) :: cmd
5664
     type(process_options), intent(in), optional :: options
5765
     type(process_result) :: res
5866
 
59
-    res%exit_code = -1
60
-    res%timed_out = .false.
61
-    res%stdout = ""
62
-    res%stderr = ""
63
-    res%error_message = "run() is not implemented yet"
67
+    call init_result(res)
68
+
69
+    select case (cmd%mode)
70
+    case (FGOF_PROCESS_MODE_ARGV)
71
+      if (.not. allocated(cmd%program)) then
72
+        call set_error(res, FGOF_PROCESS_ERR_INVALID_COMMAND, "argv command program is not set")
73
+        return
74
+      end if
75
+
76
+      if (len_trim(cmd%program) == 0) then
77
+        call set_error(res, FGOF_PROCESS_ERR_INVALID_COMMAND, "argv command program must not be empty")
78
+        return
79
+      end if
80
+
81
+    case (FGOF_PROCESS_MODE_SHELL)
82
+      if (.not. allocated(cmd%command_line)) then
83
+        call set_error(res, FGOF_PROCESS_ERR_INVALID_COMMAND, "shell command line is not set")
84
+        return
85
+      end if
86
+
87
+      if (len_trim(cmd%command_line) == 0) then
88
+        call set_error(res, FGOF_PROCESS_ERR_INVALID_COMMAND, "shell command line must not be empty")
89
+        return
90
+      end if
6491
 
65
-    if (.not. allocated(cmd%program)) then
66
-      res%error_message = "command program is not set"
92
+    case default
93
+      call set_error(res, FGOF_PROCESS_ERR_INVALID_COMMAND, "command mode is not set")
6794
       return
68
-    end if
95
+    end select
6996
 
7097
     if (present(options)) then
7198
       if (options%timeout_ms < 0) then
72
-        res%error_message = "timeout_ms must be >= 0"
99
+        call set_error(res, FGOF_PROCESS_ERR_INVALID_OPTION, "timeout_ms must be >= 0")
73100
         return
74101
       end if
75102
     end if
103
+
104
+    call set_error(res, FGOF_PROCESS_ERR_NOT_IMPLEMENTED, "run() is not implemented yet")
76105
   end function run
77106
 
107
+  subroutine init_result(res)
108
+    type(process_result), intent(out) :: res
109
+
110
+    res%launched = .false.
111
+    res%completed = .false.
112
+    res%timed_out = .false.
113
+    res%exited_normally = .false.
114
+    res%exit_code = -1
115
+    res%term_signal = 0
116
+    res%stdout = ""
117
+    res%stderr = ""
118
+    res%error_code = FGOF_PROCESS_OK
119
+    res%error_message = ""
120
+    res%elapsed_ms = 0
121
+  end subroutine init_result
122
+
123
+  subroutine set_error(res, code, message)
124
+    type(process_result), intent(inout) :: res
125
+    integer, intent(in) :: code
126
+    character(len=*), intent(in) :: message
127
+
128
+    res%error_code = code
129
+    res%error_message = trim(message)
130
+  end subroutine set_error
131
+
78132
   integer function max_trimmed_length(values) result(max_len)
79133
     character(len=*), intent(in) :: values(:)
80134
     integer :: i
src/fgof_process_types.f90added
@@ -0,0 +1,66 @@
1
+module fgof_process_types
2
+  implicit none
3
+  private
4
+
5
+  public :: FGOF_PROCESS_MODE_NONE
6
+  public :: FGOF_PROCESS_MODE_ARGV
7
+  public :: FGOF_PROCESS_MODE_SHELL
8
+  public :: FGOF_PROCESS_OK
9
+  public :: FGOF_PROCESS_ERR_INVALID_COMMAND
10
+  public :: FGOF_PROCESS_ERR_INVALID_OPTION
11
+  public :: FGOF_PROCESS_ERR_SPAWN_FAILED
12
+  public :: FGOF_PROCESS_ERR_EXEC_FAILED
13
+  public :: FGOF_PROCESS_ERR_PIPE_FAILED
14
+  public :: FGOF_PROCESS_ERR_TIMEOUT
15
+  public :: FGOF_PROCESS_ERR_INTERNAL
16
+  public :: FGOF_PROCESS_ERR_NOT_IMPLEMENTED
17
+  public :: process_command
18
+  public :: process_options
19
+  public :: process_result
20
+
21
+  integer, parameter :: FGOF_PROCESS_MODE_NONE = 0
22
+  integer, parameter :: FGOF_PROCESS_MODE_ARGV = 1
23
+  integer, parameter :: FGOF_PROCESS_MODE_SHELL = 2
24
+
25
+  integer, parameter :: FGOF_PROCESS_OK = 0
26
+  integer, parameter :: FGOF_PROCESS_ERR_INVALID_COMMAND = 10
27
+  integer, parameter :: FGOF_PROCESS_ERR_INVALID_OPTION = 11
28
+  integer, parameter :: FGOF_PROCESS_ERR_SPAWN_FAILED = 20
29
+  integer, parameter :: FGOF_PROCESS_ERR_EXEC_FAILED = 21
30
+  integer, parameter :: FGOF_PROCESS_ERR_PIPE_FAILED = 22
31
+  integer, parameter :: FGOF_PROCESS_ERR_TIMEOUT = 23
32
+  integer, parameter :: FGOF_PROCESS_ERR_INTERNAL = 99
33
+  integer, parameter :: FGOF_PROCESS_ERR_NOT_IMPLEMENTED = 1000
34
+
35
+  type :: process_command
36
+    integer :: mode = FGOF_PROCESS_MODE_NONE
37
+    character(len=:), allocatable :: program
38
+    character(len=:), allocatable :: argv(:)
39
+    character(len=:), allocatable :: command_line
40
+  end type process_command
41
+
42
+  type :: process_options
43
+    character(len=:), allocatable :: cwd
44
+    character(len=:), allocatable :: stdin
45
+    logical :: capture_stdout = .false.
46
+    logical :: capture_stderr = .false.
47
+    integer :: timeout_ms = 0
48
+    character(len=:), allocatable :: env_set(:)
49
+    character(len=:), allocatable :: env_unset(:)
50
+  end type process_options
51
+
52
+  type :: process_result
53
+    logical :: launched = .false.
54
+    logical :: completed = .false.
55
+    logical :: timed_out = .false.
56
+    logical :: exited_normally = .false.
57
+    integer :: exit_code = -1
58
+    integer :: term_signal = 0
59
+    character(len=:), allocatable :: stdout
60
+    character(len=:), allocatable :: stderr
61
+    integer :: error_code = FGOF_PROCESS_OK
62
+    character(len=:), allocatable :: error_message
63
+    integer :: elapsed_ms = 0
64
+  end type process_result
65
+
66
+end module fgof_process_types
test/test_command.f90modified
@@ -1,11 +1,12 @@
11
 program test_command
2
-  use fgof_process, only : process_command, command
2
+  use fgof_process, only : FGOF_PROCESS_MODE_ARGV, command, process_command
33
   implicit none
44
 
55
   type(process_command) :: cmd
66
 
77
   cmd = command("printf", ["hello", "world"])
88
 
9
+  if (cmd%mode /= FGOF_PROCESS_MODE_ARGV) error stop "mode mismatch"
910
   if (.not. allocated(cmd%program)) error stop "program not allocated"
1011
   if (cmd%program /= "printf") error stop "program mismatch"
1112
   if (.not. allocated(cmd%argv)) error stop "argv not allocated"
test/test_shell.f90added
@@ -0,0 +1,14 @@
1
+program test_shell
2
+  use fgof_process, only : FGOF_PROCESS_MODE_SHELL, process_command, shell
3
+  implicit none
4
+
5
+  type(process_command) :: cmd
6
+
7
+  cmd = shell("printf 'hello from shell'")
8
+
9
+  if (cmd%mode /= FGOF_PROCESS_MODE_SHELL) error stop "mode mismatch"
10
+  if (.not. allocated(cmd%command_line)) error stop "command_line not allocated"
11
+  if (cmd%command_line /= "printf 'hello from shell'") error stop "command_line mismatch"
12
+  if (.not. allocated(cmd%argv)) error stop "argv not allocated"
13
+  if (size(cmd%argv) /= 0) error stop "shell argv should be empty"
14
+end program test_shell
test/test_validation.f90added
@@ -0,0 +1,33 @@
1
+program test_validation
2
+  use fgof_process, only : &
3
+    FGOF_PROCESS_ERR_INVALID_COMMAND, &
4
+    FGOF_PROCESS_ERR_INVALID_OPTION, &
5
+    FGOF_PROCESS_MODE_ARGV, &
6
+    process_command, &
7
+    process_options, &
8
+    process_result, &
9
+    run, &
10
+    shell
11
+  implicit none
12
+
13
+  type(process_command) :: argv_cmd
14
+  type(process_command) :: shell_cmd
15
+  type(process_options) :: opts
16
+  type(process_result) :: res
17
+
18
+  argv_cmd%mode = FGOF_PROCESS_MODE_ARGV
19
+  argv_cmd%program = ""
20
+  allocate(character(len=1) :: argv_cmd%argv(0))
21
+
22
+  res = run(argv_cmd)
23
+  if (res%error_code /= FGOF_PROCESS_ERR_INVALID_COMMAND) error stop "empty argv command should be invalid"
24
+
25
+  shell_cmd = shell("")
26
+  res = run(shell_cmd)
27
+  if (res%error_code /= FGOF_PROCESS_ERR_INVALID_COMMAND) error stop "empty shell command should be invalid"
28
+
29
+  shell_cmd = shell("printf 'ok'")
30
+  opts%timeout_ms = -1
31
+  res = run(shell_cmd, opts)
32
+  if (res%error_code /= FGOF_PROCESS_ERR_INVALID_OPTION) error stop "negative timeout should be invalid"
33
+end program test_validation