-- FILE:     FILE2.E
-- PURPOSE:  More file system routines for Euphoria 3.1.1.
-- AUTHOR:   Shian Lee
-- VERSION:  1.15, February/13/2022
-- LANGUAGE: Euphoria version 3.1.1 (http://www.RapidEuphoria.com)
-- PLATFORM: Any
-- LICENCE:  Free. Use at your own risk.
-- NOTE:     * Be careful: chdir("x:") changes drive on WIN32.
--           * Remember: pathnames are case sensitive on Linux/FreeBSD.
--           * See the attached reference manual FILE2.DOC for details.
--           * Created using edu 3.07 (www.RapidEuphoria311.com).
-- LINKS:    * DOS API (RBIL): https://www.cs.cmu.edu/~ralf/files.html
--             Win API: https://docs.microsoft.com/en-us/windows/win32/api/
--             Unix API (man glibc): https://www.gnu.org/software/libc/manual/
--             FreeBSD API: man [2|3] {function} (2=syscalls|3=library calls)
-- HISTORY:  v1.00 - initial version (December/23/2021).
--           v1.10 - 17 global routines added, besides copy & compare file.
--           v1.11 - full_filename() return name in actual lower/upper case.
--           v1.12 - chdrive() triggers critical-error; using upper().
--           v1.15 - full LFN support using doslfn.e; temp_filename().
-----------------------------------------------------------------------------



--bookmark: include Euphoria 3.1.1 library files;
include file.e
include dll.e
include machine.e
include wildcard.e -- upper()
include graphics.e -- files(), dos_system()
include misc.e

include doslfn.e -- LFN support for DOS32 (see also doslfn.eu)



--bookmark: Locals;

constant FALSE = 0, 
	 TRUE = 1, 
	 EMPTY = {},
	 FAIL = -1,  -- -1 is common for all file I/O errors
	 MAX_DRIVES = 'Z' - 'A' + 1 -- maximum drives on DOS/Windows


type boolean(integer b)
    return b = FALSE or b = TRUE
end type


type string(sequence s)
    return equal(s and 0, repeat(0, length(s))) -- flat sequence?
end type


function iif(integer condition, integer if_true, integer if_false)
    if condition then
	return if_true
    end if
    return if_false
end function


-- PURPOSE: test if s is "string" or {integer, "string"}
type int_str(sequence s)
    if length(s) = 2 then
	if integer(s[1]) and sequence(s[2]) then
	    return string(s[2])
	end if
    end if
    return string(s)
end type


-- PURPOSE: test if i is a DOS32/WIN32 drive letter
type drive(integer i)
    return (i >= 'A' and i <= 'Z') or
	   (i >= 'a' and i <= 'z')
end type



--bookmark: Operating System;

-- PURPOSE: for setting OS_FREEBSD by open_libc()
boolean is_freebsd
	is_freebsd = FALSE -- 0=default


-- PURPOSE:  open the Linux C shared library (libc.so file)
-- PLATFORM: Linux, FreeBSD
-- NOTE:     * it provides better compatibility between FreeBSD and Linux
--           * "ldd $EUDIR/bin/exu" tells what library Euphoria 3.1.1 is using
-- OUTPUT:   is_freebsd
-- RETURN:   atom; same as open_dll()
-- EXAMPLE:  a = open_libc()
global function open_libc()
    atom a

    -- 32/64-bit FreeBSD supports both 32-bit FreeBSD & Linux binaries, so
    -- calling so.6 before so.4 may lead to bug on FreeBSD.

    a = open_dll("libc.so.4") -- FreeBSD (must be first before Linux)
    if a then
	is_freebsd = TRUE -- sets OS_FREEBSD once on start
    else
	a = open_dll("libc.so.6") -- Linux
    end if
    return a
end function



-- PURPOSE:  open the Linux Math shared library (libm.so file)
-- PLATFORM: Linux, FreeBSD
-- NOTE:     * it provides better compatibility between FreeBSD and Linux
--           * "ldd $EUDIR/bin/exu" tells what library Euphoria 3.1.1 is using
-- RETURN:   atom; same as open_dll()
-- EXAMPLE:  a = open_libm()
global function open_libm()
    atom a

    -- 32/64-bit FreeBSD supports both 32-bit FreeBSD & Linux binaries, so
    -- calling so.6 before so.2 may lead to bug on FreeBSD.

    a = open_dll("libm.so.2") -- FreeBSD (must be first before Linux)
    if not a then
	a = open_dll("libm.so.6") -- Linux
    end if
    return a
end function



-- PURPOSE: open shared library for OS Functions on start
function open_lib()
    if platform() = WIN32 then
	return open_dll("kernel32")
    elsif platform() = LINUX then
	return open_libc() -- sets OS_FREEBSD
    else -- DOS32 (call to open_dll() is error on DOS)
	return 0
    end if
end function

constant LIB = open_lib()


-- display warning when LIB = 0:
if LIB = 0 and platform() != DOS32 then
    puts(1, "\n\n Cannot open shared library in module file2.e." &
	    "\n Some functions will not work. Press <Esc> to continue...")
    while get_key() != 27 do
    end while
end if



-- PURPOSE: commonly used constants for platforms
-- NOTE:    since Euphoria for Linux can run also on FreeBSD, open_libc() sets
--          is_freebsd=1 if you're running Euphoria for FreeBSD (doc/bsd64.doc)
global constant OS_DOS32 = (platform() = DOS32),
		OS_WIN32 = (platform() = WIN32),
		OS_LINUX = (platform() = LINUX), -- (Linux & FreeBSD)
		OS_FREEBSD = is_freebsd,         -- (only FreeBSD)
		OS_SLASH =     iif(OS_LINUX, '/', '\\'),
		OS_DELIMITER = iif(OS_LINUX, ':', ';')



-- PURPOSE: check if video status has changed for dos_system()/dos_system_exec()
-- NOTE:    video_config() is "frozen" after system/system_exec(s,2)
-- RETURN:  integer; video_status 0/1/2 (same/VC_MODE changed/VC_LINES changed)
--                      (always return 0 on Windows/Linux)
function dos_video_status(sequence vc)
    integer status
    
    status = 0
    if OS_DOS32 then
	-- peek the BIOS Data Area (BDA)
	if vc[VC_MODE] != peek(#449) then
	    status = 1
	elsif vc[VC_LINES] != (peek(#484) + 1) then
	    status = 2
	end if
    end if
    return status
end function



-- PURPOSE: execute an operating system command line (DOS32)
-- DETAILS: s is for system(s, 2)
-- RETURN:  integer; video_status -- see dos_video_status()
-- EXAMPLE: i = dos_system("dir")  if i then reset = graphics_mode(-1)
global function dos_system(sequence s)
    sequence vc
	
    vc = video_config()
    system(s, 2)
    return dos_video_status(vc)
end function



-- PURPOSE: execute a program and get its exit code (DOS32)
-- DETAILS: s is for system_exec(s, 2)
-- RETURN:  sequence; {exit_code, video_status} -- see dos_video_status()
-- EXAMPLE: s = dos_system_exec("dir")  if s[2] then r = graphics_mode(-1)
global function dos_system_exec(sequence s)
    sequence vc
    integer code
	
    vc = video_config()
    code = system_exec(s, 2)
    return {code, dos_video_status(vc)}
end function



--bookmark: Locals for OS Functions;

constant C_BOOL = C_INT -- for define_c_func(), Windows

integer ID_MKDIR, ID_RMDIR, ID_ERASE, ID_RENAM, ID_DRIVE

if OS_DOS32 then
    -- DOS INT 21 regular functions (in AH register)
    -- LFN id is: #7100 + floor(ID / #100) -- (except ID_DRIVE)
    ID_MKDIR = #3900
    ID_RMDIR = #3A00
    ID_ERASE = #4100
    ID_RENAM = #5600
    ID_DRIVE = #4409

elsif OS_WIN32 then
    ID_MKDIR = define_c_func(LIB, "CreateDirectoryA", {C_POINTER, C_POINTER}, C_BOOL)
    ID_RMDIR = define_c_func(LIB, "RemoveDirectoryA", {C_POINTER}, C_BOOL)
    ID_ERASE = define_c_func(LIB, "DeleteFileA", {C_POINTER}, C_BOOL)
    ID_RENAM = define_c_func(LIB, "MoveFileA",   {C_POINTER, C_POINTER}, C_BOOL)
    ID_DRIVE = define_c_func(LIB, "GetLogicalDrives", {}, C_INT)

elsif OS_LINUX then
    ID_MKDIR = define_c_func(LIB, "mkdir", {C_POINTER, C_INT}, C_INT)
    ID_RMDIR = define_c_func(LIB, "rmdir", {C_POINTER}, C_INT)
    ID_ERASE = define_c_func(LIB, "unlink", {C_POINTER}, C_INT)
    ID_RENAM = define_c_func(LIB, "rename", {C_POINTER, C_POINTER}, C_INT)
    ID_DRIVE = FAIL -- (no drives on Linux)

else -- this case is not possible in Euphoria version 3.1.1
    puts(1, "\n\n  Platform not supported by file2 library! abort.\n\n")
    abort(1)
end if



-- PURPOSE: common OS API call for the similar mkdir/rmdir/erase functions
-- DETAILS: pn is pathname;
--          func_id is ID_MKDIR/ID_RMDIR/ID_ERASE;
--          func_args is for WIN32/LINUX c_func() - excluding the mem-address.
-- RETURN:  boolean; FALSE/TRUE (error/success)
-- EXAMPLE: i = os_func("C:\\TEMP", ID_RMDIR, EMPTY)
function os_func(string pn, integer func_id, sequence func_args)
    sequence r
    atom ad
    boolean ok

    if func_id = FAIL then
	return FALSE -- couldn't find function in C library, etc
    elsif not length(pn) then
	return FALSE

    elsif OS_DOS32 then
	pn &= NULL -- asciiz
	ad = allocate_low(length(pn))
	if not ad then return FALSE end if  -- can't allocate memory
	poke(ad, pn)
	
	r = repeat(0, REG_LIST_SIZE)
	if check_lfn() then
	    r[REG_AX] = #7100 + floor(func_id / #100) -- AX=LFN function
	else
	    r[REG_AX] = func_id -- AH=standard function
	end if
	r[REG_DS] = floor(ad / 16)      -- DS:DX=seg:offs of asciiz pn
	r[REG_DX] = remainder(ad, 16)
	r = dos_interrupt(#21, r)
	free_low(ad)
	return not and_bits(r[REG_FLAGS], 1) -- carry flag set on error

    else -- OS_WIN32, OS_LINUX
	ad = allocate_string(pn) -- assuming enough memory
	ok = c_func(func_id, ad & func_args) and TRUE
	free(ad)
	if OS_LINUX then
	    ok = not ok
	end if
	return ok
    end if
end function



--bookmark: Drive;

-- PURPOSE:  return the letter of the current drive
-- PLATFORM: DOS32, WIN32 (on LINUX return '/')
-- RETURN:   integer; current drive letter upper case ('A'..'Z').
-- EXAMPLE:  i = current_drive() -- i may be 'C'
global function current_drive()
    sequence d

    d = current_dir()
    return upper(d[1])
end function



-- PURPOSE:  change to a new current drive
-- PLATFORM: DOS32, WIN32 (on LINUX return TRUE)
-- DETAILS:  d is drive letter a..z or A..Z.
-- RETURN:   boolean; FALSE/TRUE (error/success)
-- EXAMPLE:  i = chdrive('C') -- C: is now the current drive
global function chdrive(drive d)
    sequence r
    integer ok
    
    d = upper(d)

    if OS_DOS32 then
	-- chdir() triggers a critical-error if drive not ready, before
	-- selecting drive, for check_error(), doserror.e
	if chdir(d & ':') then
	    r = repeat(0, REG_LIST_SIZE)
	    r[REG_AX] = #0E00       -- AH=#0E - Select Drive function
	    r[REG_DX] = d - 'A'     -- DL=drive code (0=A, 1=B, etc)
	    r = dos_interrupt(#21, r)
	end if
	
    elsif OS_WIN32 then
	ok = chdir(d & ':') -- chdir("X:") changes drive on Windows

    else -- OS_LINUX
	return TRUE -- no drives on Unix
    end if

    return d = current_drive()  -- (both upper case)
end function



-- PURPOSE:  return a list of all drives
-- PLATFORM: DOS32, WIN32 (on LINUX return empty, "")
-- NOTE:     * cannot cause a critical error on DOS32
--           * lists all letters used by mapped-drives on DOS32, e.g. A:,B:
-- RETURN:   string; "drives" upper case, or "" on error
-- EXAMPLE:  s = get_drives()  -- s may be "ACZ", "BC", etc
global function get_drives()
    sequence r
    sequence list
    atom bitmask
    integer power2

    list = EMPTY

    -- add all logical drives to list
    if ID_DRIVE = FAIL then
	return EMPTY -- couldn't find function in C library, etc

    elsif OS_DOS32 then
	r = repeat(0, REG_LIST_SIZE)
	for drive = 1 to MAX_DRIVES do
	    -- AH=#44; AL=#09 - IOCTL: Check if Block Device is Remote function
	    r[REG_AX] = ID_DRIVE    
	    r[REG_BX] = drive       -- BL=drive # (0=default, 1=A, 2=B, etc)
	    r = dos_interrupt(#21, r)
	    if not and_bits(r[REG_FLAGS], 1) then -- carry flag set on error
		list &= (drive + '@')
	    end if
	    r = r and 0 -- clear reg list
	end for

    elsif OS_WIN32 then
	-- return bitmask representing the currently available disk drives
	-- bit-0 is drive A, bit-1 is drive B, ... to Z.
	bitmask = c_func(ID_DRIVE, EMPTY)
	power2 = 1
	for drive = 1 to MAX_DRIVES do
	    if and_bits(bitmask, power2) then
		list &= (drive + '@')
	    end if
	    power2 *= 2
	end for
    end if

    return list -- drives list upper case
end function



--bookmark: Directory;

-- make_dir() for Linux/FreeBSD mode is "rwx------" (octal 700 or decimal 448)
constant ONLY_OWNER_MODE = {iif(OS_LINUX, 448, 0)}


-- PURPOSE: create a directory
-- RETURN:  boolean; FALSE/TRUE (error/success)
-- EXAMPLE: i = make_dir("C:\\DATA")
global function make_dir(string pn)
    return os_func(pn, ID_MKDIR, ONLY_OWNER_MODE)
end function



-- PURPOSE: delete an empty directory
-- RETURN:  boolean; FALSE/TRUE (error/success)
-- EXAMPLE: i = remove_dir("C:\\DATA")
global function remove_dir(string pn)
    return os_func(pn, ID_RMDIR, EMPTY)
end function



-- PURPOSE: rename a file or directory
-- NOTE:    also moves a file on the same drive
-- DETAILS: pn is file/dir to rename. pn2 is the new file/dir name.
-- RETURN:  boolean; FALSE/TRUE (error/success)
-- EXAMPLE: i = rename_file("C:\\temp", "C:\\tmp")
global function rename_file(sequence pn, sequence pn2)
    sequence r
    atom ad, ad2
    boolean ok


    if ID_RENAM = FAIL then
	return FALSE    -- couldn't find function in C library, etc
    elsif not length(pn) or not length(pn2) then
	return FALSE

    elsif OS_DOS32 then
	pn &= NULL -- asciiz
	pn2 &= NULL
	ad = allocate_low(length(pn))
	if not ad then
	    return FALSE -- couldn't allocate memory
	else
	    ad2 = allocate_low(length(pn2))
	    if not ad2 then
		free_low(ad)
		return FALSE -- couldn't allocate memory
	    end if
	end if

	-- Rename File or Directory function
	poke(ad, pn)
	poke(ad2, pn2)
	r = repeat(0, REG_LIST_SIZE)
	if check_lfn() then
	    r[REG_AX] = #7156   -- AX=LFN function
	else
	    r[REG_AX] = #5600   -- AH=standard function (ID_RENAM)
	end if
	r[REG_DS] = floor(ad / 16)      -- DS:DX=seg:offs for asciiz pn
	r[REG_DX] = remainder(ad, 16)
	r[REG_ES] = floor(ad2 / 16)     -- ES:DI=seg:offs for asciiz pn2
	r[REG_DI] = remainder(ad2, 16)
	r = dos_interrupt(#21, r)
	free_low(ad)
	free_low(ad2)
	return not and_bits(r[REG_FLAGS], 1) -- carry flag set on error

    else -- OS_WIN32, OS_LINUX
	ad = allocate_string(pn) -- assuming enough memory
	ad2 = allocate_string(pn2)
	ok = c_func(ID_RENAM, {ad, ad2}) and TRUE
	free(ad)
	free(ad2)
	if OS_LINUX then
	    ok = not ok
	end if
	return ok
    end if
end function



--bookmark: File;

-- PURPOSE: delete a file
-- RETURN:  boolean; FALSE/TRUE (error/success)
-- EXAMPLE: i = delete_file("C:\\DATA\\readme.txt")
global function delete_file(string pn)
    return os_func(pn, ID_ERASE, EMPTY)
end function



-- PURPOSE: copy a file
-- DETAILS: pn is existing file to copy. pn2 is the new file pathname.
--          overwrite is 0/1 - don't overwrite new file/overwrite new file.
-- RETURN:  boolean; FALSE/TRUE (error/success)
-- EXAMPLE: i = copy_file("C:\\work\\compress.ex", "D:\\compress.BAK", 0)
global function copy_file(string pn, string pn2, boolean overwrite)
    integer fn, fn2
    object size

    -- don't overwrite pn2 file
    if equal(pn, pn2) then
	return FALSE  -- sanity check, can't copy file to itself
    elsif not overwrite then
	if sequence(lfn_dir(pn2)) then
	    return FALSE
	end if
    end if

    -- open binary files (LFN)
    fn = open(pn, "rb") -- open file for reading ("r" is lfn)
    if fn = FAIL then
	return FALSE
    else
	fn2 = lfn_open(pn2, "wb") -- create file for writing
	if fn2 = FAIL then
	    close(fn)
	    return FALSE
	end if
    end if

    size = lfn_dir(pn)
    size = size[1][D_SIZE]

    -- copy file
    for byte = 1 to size do
	puts(fn2, getc(fn))
    end for

    close(fn)
    close(fn2)
    return TRUE
end function



-- PURPOSE: compare the content of two files
-- DETAILS: pn & pn2 are two existing files to compare
-- RETURN:  integer; -1/0/1 (error/not-identical/identical)
-- EXAMPLE: i = compare_file("C:\\work\\compress.ex", "D:\\compress.ex")
global function compare_file(string pn, string pn2)
    integer fn, fn2
    object size, size2

    -- different file size?
    size = lfn_dir(pn)
    size2 = lfn_dir(pn2)
    if atom(size) or atom(size2) then
	return FAIL -- file(s) not found
    end if

    size = size[1][D_SIZE]
    size2 = size2[1][D_SIZE]
    if size != size2 then
	return FALSE -- different file size
    end if

    -- open binary files for reading (LFN)
    fn = open(pn, "rb") -- ("r" is lfn)
    if fn = FAIL then
	return FAIL -- can't open file 1
    else
	fn2 = open(pn2, "rb") -- ("r" is lfn)
	if fn2 = FAIL then
	    close(fn)
	    return FAIL -- can't open file 2
	end if
    end if

    -- compare files
    for i = 1 to size do
	if getc(fn) != getc(fn2) then
	    close(fn)
	    close(fn2)
	    return FALSE -- different
	end if
    end for

    close(fn)
    close(fn2)
    return TRUE
end function



--bookmark: Path;

-- PURPOSE: global constants for parse_path() return value
global constant PP_DRIVE = 1,
		PP_PATH  = 2,
		PP_DIR   = 3,
		PP_FILE  = 4,
		PP_BASE  = 5,
		PP_EXT   = 6


-- PURPOSE: parse a path string
-- DETAILS: pn is "pathname" or {unix, "pathname"}
--          unix is 0|1: pn is DOS/Win path | pn is Linux/BSD path.
-- RETURN:  sequence; {drive, path, dir, file, base, ext}
-- EXAMPLE: s = parse_path({0, "C:\\DATA\\jan.log"})
--           -- s is {"C", "C:\DATA\", "\DATA\", "jan.log", "jan", "log"}
--          s = parse_path({1, "/home/user/jan.log"})
--           -- s is {"", "/home/user/", "/home/user/", "jan.log", "jan", "log"}
--           --  * on Linux path & dir are always equal and drive is empty.
global function parse_path(int_str pn)
    sequence drive, path, d, file, base, ext
    integer len, slash
    boolean unix

    -- unix path?
    if length(pn) = 2 and sequence(pn[2]) then
	unix = pn[1]
	pn = pn[2]
    else
	unix = OS_LINUX -- use default path mode
    end if

    slash = iif(unix, '/', '\\')

    -- 1. get file name ("ex.exe")
    file = pn
    len = length(file)
    for i = len to 1 by -1 do
	if pn[i] = slash or (not unix and pn[i] = ':') then
	    file = pn[i + 1..len]
	    len = length(file)
	    exit
	end if
    end for
    if equal(file, ".") or equal(file, "..") then
	file = EMPTY -- . and .. are directory symbols
	len = 0
    end if

    -- 2. get file base and extension (ex.exe -> "ex", "exe")
    base = file
    ext = EMPTY
    for i = len to 1 by -1 do
	if file[i] = '.' then
	    base = file[1..i - 1]
	    ext = file[i + 1..len]
	    exit
	end if
    end for

    -- 3. get path, directory, drive ("c:\euphoria\", "\euphoria\", "c")
    path = pn[1..$ - len]
    d = path
    drive = EMPTY
    if not unix and length(path) >= 2 and path[2] = ':' then
	drive = path[1..1]
	d = path[3..$]
    end if

    return {drive, path, d, file, base, ext}
end function



-- PURPOSE: add or remove a slash to the end of a path string
-- DETAILS: pn is directory pathname;
--          slash is 0/1 (remove/append)
-- RETURN:  string;
-- EXAMPLE: s = slash_path("C:\\DATA\\", 0) -- s is "C:\DATA"
global function slash_path(string pn, boolean slash)
    integer len

    len = length(pn)

    -- append slash (DOS/Win drive (x:) or existing slash are ignored)
    if slash then
	if len and pn[len] != OS_SLASH then
	    if OS_LINUX or pn[len] != ':' then
		pn &= OS_SLASH
	    end if
	end if

    -- remove slash (root dir (\, /, x:\) or non-existing slash are ignored)
    elsif len > 1 and pn[len] = OS_SLASH then
	if OS_LINUX or pn[len - 1] != ':' then
	    pn = pn[1..len - 1]
	end if
    end if

    return pn
end function



-- PURPOSE: return a name for creating a temporary file or directory
-- DETAILS: pn is path for temp name
-- RETURN:  object; temp name or -1 on error
-- EXAMPLE: x = temp_filename("C:\\work") -- x is "xidkaudk", "ircpwkfm", etc
global function temp_filename(string pn)
    sequence s, az
    
    if atom(lfn_dir(slash_path(pn, FALSE))) then
	return FAIL -- path not found
    end if
    
    pn = slash_path(pn, TRUE)
    az = repeat('z' - 'a' + 1, 8)
    while TRUE do
	s = rand(az) + ('a' - 1) -- random lower-case-eight-chars name
	if atom(lfn_dir(pn & s)) then
	    return s
	end if
    end while       
end function



-- PURPOSE: return the full pathname of a directory
-- DETAILS: pn is an existing directory name
-- NOTE:    treats * or ? literally in pn (not as wildcards)
-- RETURN:  object; full directory name or -1 on error
-- EXAMPLE: x = full_dirname("..")
global function full_dirname(string pn)
    sequence cdir
    integer nul, cdrive, drv
    object path

    if not length(pn) then
	return FAIL -- directory not specified ("")
    end if
    
    -- change drive?
    if not OS_LINUX and length(pn) >= 2 and pn[2] = ':' then
	drv = upper(pn[1])  -- drive specified
	cdrive = current_drive()
	    
	if drv = cdrive then
	    cdrive = FALSE  -- drive is current drive, don't change it
	elsif drv < 'A' or drv > 'Z' then
	    return FAIL     -- invalid drive specified
	elsif not chdrive(drv) then
	    return FAIL     -- cannot change drive
	end if
    else
	cdrive = FALSE      -- drive not specified, use current drive
    end if
    
    -- change directory to get full path from the system
    cdir = current_dir()
    if lfn_chdir(pn) then
	path = lfn_current_dir()
	nul = chdir(cdir) -- restore directory
    else
	path = FAIL -- cannot change dir / dir not exists
    end if

    -- restore drive?
    if cdrive then
	nul = chdrive(cdrive)
    end if

    return path -- (LFN)
end function



-- PURPOSE: return the full pathname of a file
-- DETAILS: pn is an existing file name
-- NOTE:    treats * or ? literally in pn (not as wildcards)
-- RETURN:  object; full file name or -1 on error
-- EXAMPLE: x = full_filename("..\\get.e")
global function full_filename(string pn)
    sequence s, file
    object path, x

    -- get file name
    s = parse_path(pn)
    file = s[PP_FILE]
    
    if not length(file) then
	return FAIL -- file not specified, ""
    elsif OS_LINUX then
	-- dir() treats * or ? literally on Linux/FreeBSD
    elsif find('*', file) or find('?', file) then
	return FAIL -- ? or * isn't a filename on DOS/Windows
    end if
    
    -- get directory name (LFN)
    if length(s[PP_PATH]) then
	path = full_dirname(s[PP_PATH])
	if atom(path) then 
	    return FAIL -- directory not exist
	end if 
    else
	path = lfn_current_dir() -- using current dir (also on Linux)
    end if
    
    -- check if filename exists
    x = lfn_dir(slash_path(path, TRUE) & file)
    if atom(x) or length(x) != 1 then 
	return FAIL -- file entry not found
    elsif find('d', x[1][D_ATTRIBUTES]) then 
	return FAIL -- is directory
    end if
    
    -- note: return file name in actual lower/upper case on DOS/Windows
    return slash_path(path, TRUE) & x[1][D_NAME] -- (LFN)
end function



-- PURPOSE: return the full pathname of a path
-- DETAILS: pn is an existing directory name, with/without 
--          [non-existing] filename/wildcards
-- RETURN:  object; full path name or -1 on error
-- EXAMPLE: x = full_pathname("..\\*.*")
global function full_pathname(string pn)
    sequence s
    object path

    if not length(pn) then
	return FAIL -- path not specified, ""
    end if
    
    s = parse_path(pn)
    if length(s[PP_PATH]) then
	path = full_dirname(s[PP_PATH])
    else
	path = lfn_current_dir() -- using current dir (also on Linux)
    end if
    
    -- append file name to full path?
    if sequence(path) and length(s[PP_FILE]) then 
	path = slash_path(path, TRUE) & s[PP_FILE]
    end if
    
    return path -- (LFN)
end function



-- PURPOSE: display the contents of a specified directory
-- DETAILS: pn is "pathname" or "" (pn can include filename or ?*)
-- EXAMPLE: files("C:\\EUPHORIA\\BIN\\*.E")
global procedure files(string pn)
    sequence vc, name
    integer width, len, col
    object d, path

    vc = video_config()
    width = vc[VC_COLUMNS] -- width of screen

    if not length(pn) then -- "" is current directory
	pn = "."
    end if
    
    puts(1, ' ')

    path = full_pathname(pn) -- (LFN)
    if atom(path) then
	puts(1, pn & "  (Path not found)\n")
	return
    end if

    d = lfn_dir(path)
    if atom(d) then
	puts(1, path & "  (File not found)\n")
	return
    end if

    puts(1, path & '\n')
    col = 1 -- cursor is on column 1

    -- sort directories first (prefix with ASCII-1)
    len = length(d)
    for i = 1 to len do
	if find('d', d[i][D_ATTRIBUTES]) then
	    d[i] = 1 & '<' & d[i][D_NAME] & '>' -- dir
	else
	    d[i] = 2 & d[i][D_NAME] -- file
	end if
    end for
    d = sort(d)

    -- output sorted list
    for i = 1 to len do
	name = d[i][2..$] -- skip prefix character
	puts(1, name)
	col += length(name) + 2

	if i = len or col + length(d[i + 1]) - 1 >= width then
	    puts(1, '\n') -- last or overflow, start new line
	    col = 1
	else
	    puts(1, "  ") -- move 2 columns
	end if
    end for
end procedure



-- End of file.
