I think for any CLI the best-tested and most successful model is the shell. You can create shells that call other shells in parallel, asynchronously in the background, extend existing commands internally or by creating a wrapper that calls the existing commands in a novel way and so on. OOP in many ways is just trying to use a programming language to more efficiently do what shells have been doing for years.
So long before Fortran had recursion or OOP features or allocatable variables Fortran programs were written that parsed commands either from a READ or the command line and typically built a table. Since it was hard to allocate and deallocate usually a dictionary was maintained that used the subcommand name as a prefix and the keyword as the suffix and then stored a value, typically as a string in an old-fashioned form of a dictionary, often kept sorted for quick look-up with a binary search. Then, using the subcommand name a routine was called that then looked in the table for the values it needed. Sans looping and conditionals this very simple approach created basic shells with procedures treated much like built-in shell commands; and like a shell could call an external program as well. Things tended to get less portable with external commands that passed back information, as Fortran had no built-in method for doing that except for reading and writing scratch files. Socket connections were often used.
Updating that to today a byte code interpreter could be easily constructed with the post-f77 features now available in Fortran that would lend itself to creating a truly functional shell.
“Yes, Virginia – there really were Fortran shells” one might say. I had one myself that included being able to read and write self-describing vectors and tables that other programs could easily use, like an HDF5 precursor, had built-in graphics and allowed any keyword value on a command to be an expression and included function-based flow control commands by allowing any group of commands to be given a name which could be called directly as a new command or could be used as a name in IF and various looping expressions. Worked great. Still in use too. So that approach is possible using a procedural language (F77 did not give you a lot of choice about that), but it could definitely benefit from more modern features.
But the thing that holds up best to me is that nothing has come along better than a shell interface model for CLI in my experience, and I know from experience it can be done (with some difficulty) even with an old simple procedure language; but I would certainly recommend all the new features Fortran offers if doing that from scratch today. Just cracking a subcommand and options from the command line would work; but there are several resources on-line that discuss how to write interpreters that are amenable to being written in modern Fortran if someone is feeling ambitious. Since you use fpm note that it is written in Fortran and has a command line interface that sounds similiar to what you are describing.
PS:
I haven’t actually read it, but a quick search brought up Jumping Back and Forth · Crafting Interpreters which, although not Fortran-oriented looks really interesting,
and the LA code that lead to matlab was written in old-style Fortran and a derivative of that still exists in the fpm package M_matrix, although it really needs rewritten as it uses some techniques that were appropriate in F77 but are totally outdated today; but it is still inspiring and quite useful for inspecting data from a Fortran program when used as a subroutine.
One of the biggest reasons to use a shell-like interface that resembles commonly used shells is that users are often familar enough with the style that they can quickly start using the program. One of the main reasons to do an actual shell instead of cracking command lines is you can isolate yourself from shell expansion so you can use “magic” characters they way you want, so you can use () in math expressions and even use * for multiplication without having to quote and escape everything all the time.