!fnsplitpth.bsi [107] - split, combine, display, format, check, create paths
!-------------------------------------------------------------------------
!EDIT HISTORY
! [107] 16-Apr-26 / jdm / Fix spurious "Error #2 in stat(...)" traces triggered by Fn'PathExists()
! [106] 18-Aug-25 / jdm / Add filename keyword (for FUNCIDX); make Fn'MergePath$ args optional
! [105] 18-Feb-25 / jdm / Add Fn'PathExists(), Fn'CreatePath()
! [104] 12-Nov-23 / jdm / Add Fn'CombineDev'PPN'File$()
! [103] 06-Sep-23 / jdm / Add keywords for funcidx, minor modernization
! [102] 07-Jan-19 / jdm / Add Fn'AbbrPath$(path$)
! [101] 05-Jan-19 / jdm / Add Fn'MergePaths$(root$,dir$,fname$,flags),  
!                             Fn'ConcatPaths$(path1$,path2$,flags),
!                             misc modernization
! [100] 24-Jul-12 / jdm / Add Fn'SplitPath$(path$,fname$)
!-------------------------------------------------------------------------
!KEYWORDS: path directory parsing filename
!
!FUNCTIONS
!  Fn'MergePaths$(root$,dir$,fname$,flags)    - merge parts into one fqfs
!  Fn'SplitPath$(path$,fname$)                - separate fqfs into dir, fname.ext
!  Fn'ConcatPaths$(path1$,path2$,flags)       - concat two paths
!  Fn'AutoQuotePath$(path$)                   - quote path$ if it contains spaces
!  Fn'AbbrPath$(path$,maxlen)                 - shorten path$ for display (middle ellipsis)
!  Fn'Combine'Dev'PPN'File$(dev$,ppn$,fname$) - combine dev: and [p,pn] and file.ext into dev:file.ext[p,pn] 
!  Fn'PathExists(path)                        - test if path exists
!  Fn'CreatePath(path)                        - create path
!-------------------------------------------------------------------------
!REQUIREMENTS
!  6.3.1530+ for INSTRF_ANY option in INSTR()
!-------------------------------------------------------------------------
!ALSO SEE
!   fnsplitpth.bp[907,11]        : test routines
!
!   fnameext.bsi 
!       - Fn'Name'Ext$(path$)    : extract name.ext from path$
!       - Fn'Name'Only$(path$)   : extract name from path$
!   fnfqfs.bsi
!       - Fn'FQFS$(spec$, flags) : form FQFS from AMOS or partial spec$
!   fnquote.bsi
!       - Fn'Unquote$(s$)        : unquote string
!       - Fn'Quote$(s$,flags)    : quote string
!-------------------------------------------------------------------------
++ifnlbl Fn'SplitPath$()
  ! flags
  define FPTHF_QT_AUTO = &h0001 ! quote result if necessary (spaces, ctrl chars)
  ++include'once ashinc:ashell.def
  ++include'once ashinc:types.def
  ++include'once sosfunc:fnquote.bsi
  ++include'once sosfunc:fnfqfs.bsi
  ++include'once sosfunc:fnquote.bsi
  
!-------------------------------------------------------------------------
! Function:
!   Split path into directory and filename
! Params
!   path$ (s0) [in] full path (native or AMOS format)
!   fname$ (s0) [out] (optional) fname output here
! Returns
!   directory (with trailing dir separator)
! Notes
!   If path$ does not contain a directory separator, then it is
!   treated as just a fname.ext and the directory is set to the
!   current directory.
!
!   Result will be absolute if path$ is absolute; else relative
!-------------------------------------------------------------------------
Function Fn'SplitPath$(path$ as s0:inputonly, fname$ as s0:outputonly) as s0

    map1 locals
        map2 x,i,2
        map2 y,i,2
        
    path$ = Fn'FQFS$(Fn'Unquote$(path$)) ! convert to native if necessary  [101] 
    ! loop searching for last dir separator
    do
        y = x 
        ![101] x = instr(y+1,path$,"/")            ! locate next /
        ![101] if x = 0 x = instr(y+1,path$,"\")   ! or \
        x = instr(y+1,path$,"/\",INSTRF_ANY)       ! [101] locate next \ or /
    loop until x = 0        
    if y > 0 then
        .fn = path$[1,y]
        xputarg 2,path$[y+1,-1]
    elseif path$[2;1] = ":" then        ! e.g c:file.ext
        .fn = path$[1,2]
        xputarg 2,path$[3,-1]
    else                                ! no directory
        xputarg 2,path$
    endif
EndFunction

!---------------------------------------------------------------------
!Function:
!   Combine 1 or 2 directories and filename into one native filespec
!Params:
!   root$  (str) [in] - initial part of directory (optional)
!   dir$   (str) [in] - subdirectory (optional)
!   fname$ (str) [in] - filename.ext (optional)
!   flags  (num) [in] - options
!                       FPTHF_QT_AUTO - quote result if necessary (spaces, ctrl chars)
!Returns:
!   absolute or relative complete filespec (see notes)
!Globals:
!Notes:
!   This is just a higher level wrapper for Fn'ConcatPaths$()
!
!   Result will be absolute if the first non-blank argument starts
!   with a leading directory separator. Otherwise it will be relative.
!   (Normally root$ would be something like /vm or c:\ )
!
!   Routine will take care of adding or removing internal directory
!   separators as required. 
!
!   If fname$ is blank, then the result will have a trailing 
!   separator only if the root$ or dir$ args (whichever is
!   the last non-blank one) contains a trailing separator.
!
!   If you want to create just a directory spec and force it to have
!   a trailing separator, you can set fname$ to just the separator.
!
!   Leading/trailing spaces always trimmed from all args and result;
!   internal spaces will be preserved (you may want to quote result -
!   see flags)
!---------------------------------------------------------------------
Function Fn'MergePaths$(root$="" as T_NATIVEPATH:inputonly, &
                       dir$="" as T_NATIVEPATH:inputonly, &
                       fname$="" as T_NATIVEPATH:inputonly, &
                       flags as b4:inputonly) as T_NATIVEPATH
                       
    .fn = Fn'ConcatPaths$(root$,dir$,flags)
    .fn = Fn'ConcatPaths$(.fn,fname$,flags)
EndFunction

!---------------------------------------------------------------------
!Function:
!   concatenate two native path parts, ensuring one and only
!   one directory separator between them
!Params:
!   path1$ (str) [in] - first path part
!   path2$ (str) [in] - second path part
!   flags (num) [in] - optional flags
!                       FPTHF_QT_AUTO - quote result if necessary (spaces, ctrl chars)
!Returns:
!   path1$ + dirsep$ + path2$ (only 1 dirsep between the parts)
!Globals:
!Notes:
!   Removes any quotes from the args; re-applies them only if
!   flag FNQF_FSPEC set. Leading/trailing spaces on both input
!   paths and result are always trimmed (unless they were enclosed
!   within the original quotes.)
!
!   If we need to add a separator, we decide which type based on:
!   a) If path1 contains any separators, use the first one
!   b) Else if path2 contains any separators, use the first one
!   c) Else use the local separator
!---------------------------------------------------------------------
Function Fn'ConcatPaths$(path1$ as T_NATIVEPATH:inputonly, &
                         path2$ as T_NATIVEPATH:inputonly, &
                         flags as b4:inputonly) as T_NATIVEPATH
    map1 locals
        map2 dirsep$,s,1
        map2 trailing'sep,i,2
        map2 leading'sep,i,2
        map2 combined'seps,i,2
        map2 x,i,2
        
    ! remove any quotes
    path1$ = Fn'Unquote$(path1$)
    path2$ = Fn'Unquote$(path2$)
    ! if one or both of the args are blank, simple concatenation will suffice
    if path1$ = "" or path2$ = "" then
        .fn = path1$ + path2$
    else  
        ! size up the state of the trailing/leading separator combo        
        ! note that instr fails if the pattern string is null
        trailing'sep = instr(1,"\/",path1$[-1,-1]) min 1        ! 0 or 1
        leading'sep = instr(1,"\/",path2$[1,1]) min 1           ! 0 or 1
        combined'seps = trailing'sep + leading'sep              ! 0, 1 or 2
        if combined'seps = 1 then                   ! use existing single sep
            .fn = path1$ + path2$
        elseif combined'seps = 2 then               ! use the trailing sep, drop the leading one
            .fn = path1$ + path2$[2,-1]
        else                                        ! insert a sep
            x = instr(1,path1$,"\/",INSTRF_ANY)     ! use first sep in path1$ if it exists
            if x then
                dirsep$ = path1$[x;1]
            else
                x = instr(1,path2$,"\/",INSTRF_ANY)     ! else use first sep in path2$ if it exists
                if x then
                    dirsep$ = path2$[x;1]
                else
                    xcall MIAMEX, MX_DIRSEP, dirsep$    ! else use local dirsep
                endif
            endif
            .fn = path1$ + dirsep$ + path2$
        endif
    endif
    xcall TRIM, .fn     ! always trim 
    if flags and FPTHF_QT_AUTO then
        .fn = Fn'AutoQuotePath$(.fn)
    endif
EndFunction

!---------------------------------------------------------------------
!Function:
!   Add quotes to path if necessary (i.e. if it contains spaces or
!   control characters). Remove existing quotes if not needed.
!Params:
!   path$  (str) [in] - path
!Returns:
!   path$ or "path$" - (quotes added if necessary)
!Globals:
!Notes:
!   Leading/trailing spaces are stripped and don't count.
!   Removes quotes from path$ if not necessary
!---------------------------------------------------------------------
Function Fn'AutoQuotePath$(path$ as T_NATIVEPATH:inputonly) as T_NATIVEPATH
    ! first remove existing quotes
    .fn = Fn'Unquote$(path$)  
    ! trim leading/trailing spaces, then quote if necessary
    .fn = Fn'Quote$(.fn,FNQF_TRIM or FNQF_FSPEC) 
EndFunction

!---------------------------------------------------------------------
!Function:
!   Shorten a path for display purposes using ellipsis in the middle
!Params:
!   path$ (str) [in] - path to display
!   maxlen (num) [in] - max length for shortened path. (min 10) 
!                       (if 0, cut out all the middle levels)
!Returns:
!   shortened path. Examples:
!       c:\foo\...\bar.txt        ! interior levels removed
!       .../filename.txt          ! all directories removed
!       ...end-of-long-name.ext   ! even part of filename removed 
!Globals:
!Notes:
!   If maxlen > 0, then we remove interior directory levels until
!   it fits or only the first level and filename.ext remain. At that
!   point, if still too long, start removing characters from the end
!   of the top level path, and when that runs out, from the beginning
!   of the filename.ext.
!---------------------------------------------------------------------
Function Fn'AbbrPath$(path$ as T_NATIVEPATH:inputonly, maxlen as b2:inputonly) as T_NATIVEPATH
    map1 locals
        map2 npath,i,2
        map2 nfname,i,2
        map2 x,i,2
        map2 dir$,T_NATIVEPATH
        map2 fnameext$,T_NATIVEPATH
        map2 dirsep$,s,1
        map2 status,f
        map2 flags,b,4
        map2 firstsep,i,2
        map2 lastsep,i,2
        
    if maxlen then
        maxlen = maxlen max 10      ! need at least 10
    endif
    path$ = strip(path$)
    ! special case for maxlen = 0 (cut out everything between first and last separator)
    if maxlen = 0 then
        firstsep = instr(1,path$,"\/",INSTRF_ANY)
        if firstsep then
            dirsep$ = path$[firstsep;1]
            lastsep = .instrr(-1,path$,dirsep$)
            if lastsep > (firstsep+4) then
                .fn = path$[1;firstsep] + "..." + path$[lastsep,-1]
                exitfunction
            endif
        endif
        .fn = path$        ! if we don't have a first and last separator, just use entire path
        exitfunction
    endif
    npath = len(path$)
    if npath <= maxlen then
        .fn = path$
        exitfunction
    endif
    ! check if the path$ actually has a dir separator
    x = instr(1,path$,"\/",INSTRF_ANY)
    if x then
        dirsep$ = path$[x;1]
        ! split the path into dir and filename.ext
        dir$ = Fn'SplitPath$(path$, fnameext$)
        nfname = len(fnameext$)
        ! if filename itself too long, use ... with trailing part of it
        if nfname >= (maxlen + 3) then
            .fn = "..." + fnameext$[-(maxlen-3),-1]
            exitfunction
        endif
        ! else just split the remaining chars between begin and end of dir$
        maxlen -= (nfname+3)        ! amt remaining after ...fnameext$
    else
        maxlen -= 3
        dir$ = path$
    endif
    maxlen = maxlen max 2       ! need this for valid maxlen/2    
    .fn = dir$[1;int(maxlen/2)] + "..." + dir$[-int(maxlen/2),-1] + fnameext$
EndFunction

!---------------------------------------------------------------------
!Function:
!  Combine dev: and [p,pn] and file.ext into dev:file.ext[p,pn] 
!Params:
!  dev$  (str) [in] - optional device or ersatz (eg DSK0:, SYS:)
!                       make include [p,pn]
!  ppn$  (str) [in] - optional [p,pn]
!  fname$ (str) [in] - file.ext
!Returns:
!   dev:file.ext[p,pn]
!Globals:
!Notes:
!---------------------------------------------------------------------
Function Fn'Combine'Dev'PPN'File$(dev$="" as s30:inputonly, ppn$="" as s10:inputonly, &
                fname$="" as s100:inputonly) as s100 
                
    map1 locals
        map2 x,i,2

    if dev$ # "" then
        x = instr(1,dev$,"[")
        if x then
            if ppn$ = "" then
                ppn$ = dev$[x,-1]
            endif
            dev$ = dev$[1,x-1]
        endif
        .fn = trim$(dev$)
        if .fn[-1,-1] # ":" then
            .fn += ":"
        endif
    endif
    
    .fn += fname$       ! assuming here just fname.ext
    
    if ppn$ # "" then
        .fn += ppn$
    endif
    
    trace.print (99,"path") dev$, ppn$, fname$, .fn
EndFunction


!---------------------------------------------------------------------
!Function:
!   Check if path exists
!Params:
!   path$ (str) [in] - path (see notes)
!Returns:
!   .TRUE or .FALSE
!Globals:
!Notes:
!   If testing whether a directory exists, path$ must end with
!   a directory separator; else it will be treated as a file.
!   (Under Windows it makes no difference, but under Linux is does.)
!   For convenience though, if it fails to exist as a file, under
!   Linux it will re-test with "/." 
!---------------------------------------------------------------------
Function Fn'PathExists(path$ as T_NATIVEPATH:inputonly) as BOOLEAN
    map1 locals
        map2 dirsep$,s,1
        map2 status,i,4
        
    dirsep$ = path$[-1,-1]
    if dirsep$ = "/" or dirsep$ = "\" then
        path$ += "."
    else
        xcall MIAMEX, MX_DIRSEP, dirsep$    ! [107] 
        path$ += dirsep$ + "."
    endif
    
    xcall MIAMEX, MX_FINDFIRST, path$, status, "", 0, 0
    xcall MIAMEX, MX_FINDEND
    if status = 0 then
        .fn = .TRUE 
    elseif path$[-1,-1] # "." then          ! if  not testing "."
        xcall MIAMEX, MX_DIRSEP, dirsep$    ! if linux, try again with "."
        if dirsep$ = "/" then
            xcall MIAMEX, MX_FINDFIRST, path$+"/.", status, "", 0, 0
            xcall MIAMEX, MX_FINDEND
            if status = 0 then
                .fn = .TRUE
            endif
        endif
    endif

EndFunction


!---------------------------------------------------------------------
!Function:
!   Create path
!Params:
!   path$  (str) [in] - path to create (directory only - no filename;
!                           trailing slash is optional)
!   errmsg$ (str) [out] - error message on error
!Returns:
!   1 on success, 0 if path already exists, else error
!Globals:
!Notes:
!   Attempts to create only the bottom level of the path
!---------------------------------------------------------------------
Function Fn'CreatePath(path$ as T_NATIVEPATH:inputonly, errmsg$ as s0:outputonly) as i4
    map1 locals
        map2 status,i,4
        map2 dirsep$,s,1
        
    xcall MIAMEX, MX_MKPATH, path$, status
    if status = 0 then              ! created
        .fn = 1
    else
        xcall MIAMEX, MX_DIRSEP, dirsep$
        if path$[-1,-1] # dirsep$ and path$[-1,-1] # "/" then
            path$ += dirsep$
        endif
        if not Fn'PathExists(path$) then    ! failed
            xcall MIAMEX, MX_ERRNOMSG, status, errmsg$
            xputarg @errmsg$
            .fn = status
        endif
    endif
EndFunction


++endif

