fortrangoingonforty/fortsh / 816d375

Browse files

add execute_and_capture_tabs for space-preserving filename completion

New execute_and_capture_tabs converts newlines to tabs (not spaces),
preserving spaces in filenames. scan_directory uses it with tab-
delimited parse_ls_output for correct completion of paths like
'/tmp/test dir/' and '/tmp/quoted file.txt'.

parse_ls_output now accepts optional use_tab_delim parameter.

Fixes the final 2 completion tests: paths with spaces (9) and
quoted filenames (29). completion: 36/36 PERFECT.
Authored by espadonne
SHA
816d375f7430d464baf1fa3db2e86982542a134a
Parents
4dea3db
Tree
a9039ff

2 changed files

StatusFile+-
M src/io/readline.f90 24 15
M src/system/interface.f90 60 0
src/io/readline.f90modified
@@ -3533,20 +3533,20 @@ contains
35333533
     ! When pattern is non-empty, filter with grep to avoid buffer overflow on large directories
35343534
     ! Use tr to convert newlines to spaces for easier parsing
35353535
     ! Filter with grep when pattern exists to limit output size
3536
+    ! No tr needed — execute_and_capture_tabs converts newlines to tabs
35363537
     if (pattern_len > 0) then
35373538
       ls_command = 'ls -1aF "' // trim(expanded_dir) // '/" 2>/dev/null | grep -i "^' // &
3538
-        trim(pattern) // '" | tr ' // "'" // char(92) // 'n' // "' ' '"
3539
+        trim(pattern) // '"'
35393540
     else
3540
-      ls_command = 'ls -1aF "' // trim(expanded_dir) // '/" 2>/dev/null | tr ' // &
3541
-        "'" // char(92) // 'n' // "' ' '"
3541
+      ls_command = 'ls -1aF "' // trim(expanded_dir) // '/" 2>/dev/null'
35423542
     end if
35433543
 
35443544
     ! Debug output
35453545
     if (debug_enabled) then
35463546
     end if
35473547
 
3548
-    ! Get output from command (allocatable result)
3549
-    ls_output_alloc = execute_and_capture(ls_command)
3548
+    ! Get output from command — use tab-delimited capture to preserve spaces in filenames
3549
+    ls_output_alloc = execute_and_capture_tabs(ls_command)
35503550
 
35513551
     ! Copy to fixed buffer (avoids flang-new issues with allocatable strings)
35523552
     ls_output = ls_output_alloc(:min(len(ls_output), len(ls_output_alloc)))
@@ -3554,8 +3554,8 @@ contains
35543554
     ! Clean up allocatable
35553555
     if (allocated(ls_output_alloc)) deallocate(ls_output_alloc)
35563556
 
3557
-    ! Parse ls output into individual entries
3558
-    call parse_ls_output(ls_output, entries, num_entries)
3557
+    ! Parse ls output — tab-delimited to preserve spaces in filenames
3558
+    call parse_ls_output(ls_output, entries, num_entries, use_tab_delim=.true.)
35593559
 
35603560
     ! Debug output
35613561
     if (debug_enabled) then
@@ -3658,12 +3658,19 @@ contains
36583658
   end function
36593659
 
36603660
   ! Parse ls output into individual entries
3661
-  subroutine parse_ls_output(output, entries, num_entries)
3661
+  subroutine parse_ls_output(output, entries, num_entries, use_tab_delim)
36623662
     character(len=*), intent(in) :: output
36633663
     character(len=MAX_LINE_LEN), allocatable, intent(out) :: entries(:)
36643664
     integer, intent(out) :: num_entries
3665
+    logical, intent(in), optional :: use_tab_delim
36653666
 
36663667
     integer :: pos, start, output_len, count_pass
3668
+    logical :: tab_mode
3669
+    character :: delim
3670
+
3671
+    tab_mode = .false.
3672
+    if (present(use_tab_delim)) tab_mode = use_tab_delim
3673
+    delim = merge(char(9), ' ', tab_mode)
36673674
 
36683675
     output_len = len_trim(output)
36693676
 
@@ -3671,8 +3678,9 @@ contains
36713678
     num_entries = 0
36723679
     pos = 1
36733680
     do while (pos <= output_len)
3674
-      ! Skip whitespace
3675
-      do while (pos <= output_len .and. (output(pos:pos) == ' ' .or. output(pos:pos) == char(9)))
3681
+      ! Skip delimiter characters
3682
+      do while (pos <= output_len .and. (output(pos:pos) == delim .or. &
3683
+                (.not. tab_mode .and. output(pos:pos) == char(9))))
36763684
         pos = pos + 1
36773685
       end do
36783686
 
@@ -3680,8 +3688,8 @@ contains
36803688
 
36813689
       start = pos
36823690
 
3683
-      ! Find end of entry (space-separated, since execute_and_capture converts newlines to spaces)
3684
-      do while (pos <= output_len .and. output(pos:pos) /= ' ')
3691
+      ! Find end of entry
3692
+      do while (pos <= output_len .and. output(pos:pos) /= delim)
36853693
         pos = pos + 1
36863694
       end do
36873695
 
@@ -3700,8 +3708,9 @@ contains
37003708
       count_pass = 0
37013709
       pos = 1
37023710
       do while (pos <= output_len .and. count_pass < num_entries)
3703
-        ! Skip whitespace
3704
-        do while (pos <= output_len .and. (output(pos:pos) == ' ' .or. output(pos:pos) == char(9)))
3711
+        ! Skip delimiter characters
3712
+        do while (pos <= output_len .and. (output(pos:pos) == delim .or. &
3713
+                  (.not. tab_mode .and. output(pos:pos) == char(9))))
37053714
           pos = pos + 1
37063715
         end do
37073716
 
@@ -3710,7 +3719,7 @@ contains
37103719
         start = pos
37113720
 
37123721
         ! Find end of entry
3713
-        do while (pos <= output_len .and. output(pos:pos) /= ' ')
3722
+        do while (pos <= output_len .and. output(pos:pos) /= delim)
37143723
           pos = pos + 1
37153724
         end do
37163725
 
src/system/interface.f90modified
@@ -911,6 +911,66 @@ contains
911911
     output = temp_output(:pos-1)
912912
   end function
913913
 
914
+  ! Like execute_and_capture but converts newlines to tabs instead of spaces.
915
+  ! This preserves filenames with spaces for completion parsing.
916
+  function execute_and_capture_tabs(command) result(output)
917
+    character(len=*), intent(in) :: command
918
+    character(len=:), allocatable :: output
919
+
920
+    type(c_ptr) :: pipe_ptr
921
+    character(kind=c_char), target :: buffer(4096)
922
+    character(len=65536) :: temp_output
923
+    type(c_ptr) :: ret_ptr
924
+    integer :: i, pos, bytes_read
925
+    character(len=256), target :: c_command
926
+    character(len=4), target :: c_mode
927
+
928
+    c_command = trim(command)//c_null_char
929
+    c_mode = 'r'//c_null_char
930
+
931
+    pipe_ptr = c_popen(c_loc(c_command), c_loc(c_mode))
932
+    if (.not. c_associated(pipe_ptr)) then
933
+      allocate(character(len=0) :: output)
934
+      return
935
+    end if
936
+
937
+    temp_output = ''
938
+    pos = 1
939
+
940
+    do
941
+      buffer = c_null_char
942
+      ret_ptr = c_fgets(c_loc(buffer), int(4096, c_int), pipe_ptr)
943
+      if (.not. c_associated(ret_ptr)) exit
944
+
945
+      bytes_read = 0
946
+      do i = 1, 4096
947
+        if (buffer(i) == c_null_char) exit
948
+        bytes_read = bytes_read + 1
949
+        if (pos > len(temp_output)) exit
950
+
951
+        if (buffer(i) == char(10)) then
952
+          ! Convert newline to tab (preserves spaces in filenames)
953
+          if (pos > 1 .and. temp_output(pos-1:pos-1) /= char(9)) then
954
+            temp_output(pos:pos) = char(9)
955
+            pos = pos + 1
956
+          end if
957
+        else
958
+          temp_output(pos:pos) = buffer(i)
959
+          pos = pos + 1
960
+        end if
961
+      end do
962
+
963
+      if (pos > len(temp_output)) exit
964
+      if (bytes_read == 0) exit
965
+    end do
966
+
967
+    i = c_pclose(pipe_ptr)
968
+
969
+    if (allocated(output)) deallocate(output)
970
+    allocate(character(len=pos-1) :: output)
971
+    output = temp_output(:pos-1)
972
+  end function
973
+
914974
   ! Terminal control functions
915975
   function enable_raw_mode(original_termios) result(success)
916976
     use iso_fortran_env, only: output_unit, error_unit