From 38e2337d2bc997048edb17a3ad3e8aa6aa2e7039 Mon Sep 17 00:00:00 2001
From: Federico Perini <federico.perini@gmail.com>
Date: Fri, 7 Mar 2025 12:34:07 +0100
Subject: [PATCH 1/6] implement `is_directory`

---
 src/stdlib_system.F90            | 38 +++++++++++++++++++++++++++++++-
 src/stdlib_system_subprocess.F90 |  2 +-
 src/stdlib_system_subprocess.c   | 24 ++++++++++++++++++++
 3 files changed, 62 insertions(+), 2 deletions(-)

diff --git a/src/stdlib_system.F90 b/src/stdlib_system.F90
index 576f72273..ca3571c3f 100644
--- a/src/stdlib_system.F90
+++ b/src/stdlib_system.F90
@@ -1,6 +1,7 @@
 module stdlib_system
 use, intrinsic :: iso_c_binding, only : c_int, c_long, c_null_ptr, c_int64_t
-use stdlib_kinds, only: int64, dp
+use stdlib_kinds, only: int64, dp, c_bool, c_char
+use stdlib_strings, only: to_c_char
 implicit none
 private
 public :: sleep
@@ -81,6 +82,22 @@ module stdlib_system
 public :: elapsed
 public :: is_windows
      
+!! version: experimental
+!!
+!! Tests if a given path matches an existing directory.
+!! ([Specification](../page/specs/stdlib_io.html#is_directory-test-if-a-path-is-a-directory))
+!!
+!!### Summary
+!! Function to evaluate whether a specified path corresponds to an existing directory.
+!!
+!!### Description
+!! 
+!! This function checks if a given file system path is a directory. It is cross-platform and avoids reliance 
+!! on external C libraries by utilizing system calls. It supports common operating systems such as Linux, macOS, 
+!! Windows, and various UNIX-like environments. On unsupported operating systems, the function will return `.false.`.
+!!
+public :: is_directory
+     
 ! CPU clock ticks storage
 integer, parameter, private :: TICKS = int64
 integer, parameter, private :: RTICKS = dp
@@ -618,4 +635,23 @@ pure function OS_NAME(os)
     end select
 end function OS_NAME
 
+!! Tests if a given path matches an existing directory.
+!! Cross-platform implementation without using external C libraries.
+logical function is_directory(path)
+    !> Input path to evaluate
+    character(*), intent(in) :: path
+
+    interface
+        
+        logical(c_bool) function stdlib_is_directory(path) bind(c, name="stdlib_is_directory")
+            import c_bool, c_char
+            character(kind=c_char), intent(in) :: path(*)
+        end function stdlib_is_directory
+
+    end interface        
+    
+    is_directory = logical(stdlib_is_directory(to_c_char(path)))
+    
+end function is_directory
+
 end module stdlib_system
diff --git a/src/stdlib_system_subprocess.F90 b/src/stdlib_system_subprocess.F90
index 00f5d759a..9f9448cb2 100644
--- a/src/stdlib_system_subprocess.F90
+++ b/src/stdlib_system_subprocess.F90
@@ -1,7 +1,7 @@
 submodule (stdlib_system) stdlib_system_subprocess
     use iso_c_binding  
     use iso_fortran_env, only: int64, real64
-    use stdlib_strings, only: to_c_char, join
+    use stdlib_strings, only: join
     use stdlib_linalg_state, only: linalg_state_type, LINALG_ERROR, linalg_error_handling
     implicit none(type, external)
     
diff --git a/src/stdlib_system_subprocess.c b/src/stdlib_system_subprocess.c
index 59f010ddd..012877d8c 100644
--- a/src/stdlib_system_subprocess.c
+++ b/src/stdlib_system_subprocess.c
@@ -10,6 +10,7 @@
 #else
 #define _POSIX_C_SOURCE 199309L
 #include <sys/wait.h>
+#include <sys/stat.h>  
 #include <unistd.h>
 #include <time.h>
 #include <errno.h>
@@ -220,6 +221,12 @@ bool process_kill_windows(stdlib_pid pid) {
     return true;
 }
 
+// Check if input path is a directory
+bool stdlib_is_directory_windows(const char *path) {
+    DWORD attrs = GetFileAttributesA(path);
+    return    (attrs != INVALID_FILE_ATTRIBUTES)  // Path exists
+           && (attrs & FILE_ATTRIBUTE_DIRECTORY); // Path is a directory
+}
 
 #else // _WIN32
 
@@ -292,12 +299,29 @@ void process_create_posix(stdlib_pid* pid)
     (*pid) = (stdlib_pid) fork();
 }
 
+// On UNIX systems: check if input path is a directory
+bool stdlib_is_directory_posix(const char *path) {
+    struct stat sb;
+    return stat(path, &sb) == 0 && S_ISDIR(sb.st_mode);
+}
+
 #endif // _WIN32
 
 /////////////////////////////////////////////////////////////////////////////////////
 // Cross-platform interface
 /////////////////////////////////////////////////////////////////////////////////////
 
+// Cross-platform interface: query directory state
+bool stdlib_is_directory(const char *path) {
+    // Invalid input
+    if (path == NULL || strlen(path) == 0) return false;  
+#ifdef _WIN32
+    return stdlib_is_directory_windows(path);
+#else
+    return stdlib_is_directory_posix(path);
+#endif // _WIN32
+}
+
 // Create or fork process
 void process_create(const char* cmd, const char* stdin_stream, const char* stdin_file, 
                     const char* stdout_file, const char* stderr_file, 

From ff422485103c35908c2a9313f7f3f7c1e3bd0b64 Mon Sep 17 00:00:00 2001
From: Federico Perini <federico.perini@gmail.com>
Date: Fri, 7 Mar 2025 12:38:48 +0100
Subject: [PATCH 2/6] add tests

---
 test/system/CMakeLists.txt      |  1 +
 test/system/test_filesystem.f90 | 99 +++++++++++++++++++++++++++++++++
 2 files changed, 100 insertions(+)
 create mode 100644 test/system/test_filesystem.f90

diff --git a/test/system/CMakeLists.txt b/test/system/CMakeLists.txt
index 9434abeee..b7623ea83 100644
--- a/test/system/CMakeLists.txt
+++ b/test/system/CMakeLists.txt
@@ -1,3 +1,4 @@
+ADDTEST(filesystem)
 ADDTEST(os)
 ADDTEST(sleep)
 ADDTEST(subprocess)
diff --git a/test/system/test_filesystem.f90 b/test/system/test_filesystem.f90
new file mode 100644
index 000000000..843763af9
--- /dev/null
+++ b/test/system/test_filesystem.f90
@@ -0,0 +1,99 @@
+module test_filesystem
+    use testdrive, only : new_unittest, unittest_type, error_type, check, skip_test
+    use stdlib_system, only: is_directory
+
+    implicit none
+
+contains
+
+    !> Collect all exported unit tests
+    subroutine collect_suite(testsuite)
+        !> Collection of tests
+        type(unittest_type), allocatable, intent(out) :: testsuite(:)
+
+        testsuite = [ &
+            new_unittest("fs_is_directory_dir", test_is_directory_dir), &
+            new_unittest("fs_is_directory_file", test_is_directory_file) &
+        ]
+    end subroutine collect_suite
+
+    ! Test `is_directory` for a directory
+    subroutine test_is_directory_dir(error)
+        type(error_type), allocatable, intent(out) :: error
+        character(len=256) :: dirname
+        integer :: ios, iocmd
+        character(len=512) :: msg
+
+        dirname = "this_test_dir_tmp"
+
+        ! Create a directory
+        call execute_command_line("mkdir " // dirname, exitstat=ios, cmdstat=iocmd, cmdmsg=msg)
+        call check(error, ios == 0 .and. iocmd == 0, "Cannot create test directory: " // trim(msg))
+        if (allocated(error)) return
+
+        ! Verify `is_directory` identifies it as a directory
+        call check(error, is_directory(dirname), "is_directory did not recognize a valid directory")
+        if (allocated(error)) return
+
+        ! Clean up: remove the directory
+        call execute_command_line("rmdir " // dirname, exitstat=ios, cmdstat=iocmd, cmdmsg=msg)
+        call check(error, ios == 0 .and. iocmd == 0, "Cannot remove test directory: " // trim(msg))
+    end subroutine test_is_directory_dir
+
+    ! Test `is_directory` for a regular file
+    subroutine test_is_directory_file(error)
+        type(error_type), allocatable, intent(out) :: error
+        character(len=256) :: filename
+        logical :: result
+        integer :: ios, iunit
+        character(len=512) :: msg
+
+        filename = "test_file.txt"
+
+        ! Create a file
+        open(newunit=iunit, file=filename, status="replace", iostat=ios, iomsg=msg)
+        call check(error, ios == 0, "Cannot create test file: " // trim(msg))
+        if (allocated(error)) return        
+
+        ! Verify `is_directory` identifies it as not a directory
+        result = is_directory(filename)
+        call check(error, .not. result, "is_directory falsely recognized a regular file as a directory")
+        if (allocated(error)) return
+
+        ! Clean up: remove the file
+        close(iunit,status='delete',iostat=ios,iomsg=msg)
+        call check(error, ios == 0, "Cannot delete test file: " // trim(msg))
+        if (allocated(error)) return                
+
+    end subroutine test_is_directory_file
+
+
+end module test_filesystem
+
+program tester
+    use, intrinsic :: iso_fortran_env, only : error_unit
+    use testdrive, only : run_testsuite, new_testsuite, testsuite_type
+    use test_filesystem, only : collect_suite
+
+    implicit none
+
+    integer :: stat, is
+    type(testsuite_type), allocatable :: testsuites(:)
+    character(len=*), parameter :: fmt = '("#", *(1x, a))'
+
+    stat = 0
+
+    testsuites = [ &
+        new_testsuite("filesystem", collect_suite) &
+    ]
+
+    do is = 1, size(testsuites)
+        write(error_unit, fmt) "Testing:", testsuites(is)%name
+        call run_testsuite(testsuites(is)%collect, error_unit, stat)
+    end do
+
+    if (stat > 0) then
+        write(error_unit, '(i0, 1x, a)') stat, "test(s) failed!"
+        error stop
+    end if
+end program

From f33e46af253c6e7aaea0ed13f60043da420a2139 Mon Sep 17 00:00:00 2001
From: Federico Perini <federico.perini@gmail.com>
Date: Fri, 7 Mar 2025 12:42:13 +0100
Subject: [PATCH 3/6] add specs

---
 doc/specs/stdlib_system.md | 35 +++++++++++++++++++++++++++++++++++
 1 file changed, 35 insertions(+)

diff --git a/doc/specs/stdlib_system.md b/doc/specs/stdlib_system.md
index 3dbe434fe..f0eabb555 100644
--- a/doc/specs/stdlib_system.md
+++ b/doc/specs/stdlib_system.md
@@ -417,3 +417,38 @@ Returns one of the `integer` `OS_*` parameters representing the OS type, from th
 ```fortran
 {!example/system/example_os_type.f90!}
 ```
+
+## `is_directory` - Test if a path is a directory
+
+### Status
+
+Experimental
+
+### Description
+
+This function checks if a specified file system path is a directory. 
+It is designed to work across multiple platforms. On Windows, paths with both forward `/` and backward `\` slashes are accepted.
+
+### Syntax
+
+`result = [[stdlib_io(module):is_directory(function)]] (path)`
+
+### Class
+Function
+
+### Arguments
+
+`path`: Shall be a character string containing the file system path to evaluate. It is an `intent(in)` argument.
+
+### Return values
+
+The function returns a `logical` value:
+
+- `.true.` if the path matches an existing directory.
+- `.false.` otherwise, or if the operating system is unsupported.
+
+### Example
+
+```fortran
+{!example/system/example_is_directory.f90!}
+```

From 3debb1fabe4d7fcead0f2eecd47c226cf5e005ff Mon Sep 17 00:00:00 2001
From: Federico Perini <federico.perini@gmail.com>
Date: Fri, 7 Mar 2025 12:43:26 +0100
Subject: [PATCH 4/6] add example

---
 example/system/CMakeLists.txt           |  1 +
 example/system/example_is_directory.f90 | 11 +++++++++++
 2 files changed, 12 insertions(+)
 create mode 100644 example/system/example_is_directory.f90

diff --git a/example/system/CMakeLists.txt b/example/system/CMakeLists.txt
index f5518b74b..800a5369b 100644
--- a/example/system/CMakeLists.txt
+++ b/example/system/CMakeLists.txt
@@ -1,4 +1,5 @@
 ADD_EXAMPLE(get_runtime_os)
+ADD_EXAMPLE(is_directory)
 ADD_EXAMPLE(os_type)
 ADD_EXAMPLE(process_1)
 ADD_EXAMPLE(process_2)
diff --git a/example/system/example_is_directory.f90 b/example/system/example_is_directory.f90
new file mode 100644
index 000000000..5976fb65c
--- /dev/null
+++ b/example/system/example_is_directory.f90
@@ -0,0 +1,11 @@
+! Demonstrate usage of `is_directory`
+program example_is_directory
+  use stdlib_system, only: is_directory
+  implicit none
+  ! Test a directory path
+  if (is_directory("/path/to/check")) then 
+    print *, "The specified path is a directory."
+  else
+    print *, "The specified path is not a directory."
+  end if
+end program example_is_directory

From 8dfaac708c03bb87a196c1cdc78d60ef3ce1f1d7 Mon Sep 17 00:00:00 2001
From: Federico Perini <federico.perini@gmail.com>
Date: Fri, 7 Mar 2025 13:01:50 +0100
Subject: [PATCH 5/6] ensure trimmed path

---
 src/stdlib_system.F90 | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/stdlib_system.F90 b/src/stdlib_system.F90
index ca3571c3f..24c23f02f 100644
--- a/src/stdlib_system.F90
+++ b/src/stdlib_system.F90
@@ -650,7 +650,7 @@ end function stdlib_is_directory
 
     end interface        
     
-    is_directory = logical(stdlib_is_directory(to_c_char(path)))
+    is_directory = logical(stdlib_is_directory(to_c_char(trim(path))))
     
 end function is_directory
 

From 8ff26bec6274bebf2c0ce0c7ff6c08398b2da6f7 Mon Sep 17 00:00:00 2001
From: Federico Perini <federico.perini@gmail.com>
Date: Fri, 21 Mar 2025 09:13:07 +0100
Subject: [PATCH 6/6] Update src/stdlib_system.F90

---
 src/stdlib_system.F90 | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/stdlib_system.F90 b/src/stdlib_system.F90
index 3073ebc83..5dade255a 100644
--- a/src/stdlib_system.F90
+++ b/src/stdlib_system.F90
@@ -93,8 +93,8 @@ module stdlib_system
 !!
 !!### Description
 !! 
-!! This function checks if a given file system path is a directory. It is cross-platform and avoids reliance 
-!! on external C libraries by utilizing system calls. It supports common operating systems such as Linux, macOS, 
+!! This function checks if a given file system path is a directory. It is cross-platform and utilizes
+!! native system calls. It supports common operating systems such as Linux, macOS, 
 !! Windows, and various UNIX-like environments. On unsupported operating systems, the function will return `.false.`.
 !!
 public :: is_directory