File: PathConvert.pm

package info (click to toggle)
wml 2.0.12ds1-10
  • links: PTS, VCS
  • area: main
  • in suites: stretch
  • size: 3,568 kB
  • ctags: 117
  • sloc: perl: 5,631; sh: 3,663; makefile: 1,003
file content (1098 lines) | stat: -rw-r--r-- 36,582 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
#
# Copyright (c) 1996, 1997, 1998 Shigio Yamaguchi. All rights reserved.
# This program is free software; you can redistribute it and/or
# modify it under the same terms as Perl itself.
#
#       File::PathConvert.pm
#

package File::PathConvert;
require 5.002;

use strict ;

BEGIN {
   use Exporter   ();
   use vars       qw($VERSION @ISA @EXPORT_OK);
   $VERSION       = 0.85;
   @ISA           = qw(Exporter);
   @EXPORT_OK     = qw(setfstype splitpath joinpath splitdirs joindirs realpath abs2rel rel2abs $maxsymlinks $verbose $SL $resolved );
}

use vars      qw( $maxsymlinks $verbose $SL $resolved ) ;
use Cwd;

#
# Initialize @EXPORT_OK vars
#
$maxsymlinks   = 32;       # allowed symlink number in a path
$verbose       = 0;        # 1: verbose on, 0: verbose off
$SL            = '' ;      # Separator char export
$resolved      = '' ;      # realpath() intermediate value export

#############################################################################
#
#  Package Globals
#

my $fstype        ; # A name indicating the type of filesystem currently in use
my $sep           ; # separator
my $sepRE         ; # RE to match spearator
my $notsepRE      ; # RE to match anything else
my $volumeRE      ; # RE to match the volume name
my $directoryRE   ; # RE to match the directory name
my $isrootRE      ; # RE to match root path: applied to directory portion only
my $thisDir       ; # Name of this directory
my $thisDirRE     ; # Name of this directory
my $parentDir     ; # Name of parent directory
my $parentDirRE   ; # RE to match parent dir name
my $casesensitive ; # Set to non-zero for case sensitive name comprisions.  Only
                    # affects names, not any other REs, so $isrootRE for Win32
                    # must be case insensitive
my $idempotent    ; # Set to non-zero if '//' is equivalent to '/'.  This
                    # does not affect leading '//' and '\\' under Win32,
                    # but will fold '///' and '////', etc, in to '//' on this
                    # Win32



###########
#
# The following globals are regexs used in the indicated routines.  These
# are initialized by setfstype, so they don't need to be rebuilt each time
# the routine that uses them is called.

my $basenamesplitRE ; # Used in realpath() to split filenames.


###########
#
# This RE matches (and saves) the portion of the string that is just before
# the beginning of a name
#
my $beginning_of_name ;

#
# This whopper of an RE looks for the pattern "name/.." if it occurs
# after the beginning of the string or after the root RE, or after a separator.
# We don't assume that the isrootRE has a trailing separator.
# It also makes sure that we aren't eliminating '../..' and './..' patterns
# by using the negative lookahead assertion '(?!' ... ')' construct.  It also
# ignores 'name/..name'.
#
my $name_sep_parentRE ;

#
# Matches '..$', '../' after a root
my $leading_parentRE ;

#
# Matches things like '/(./)+' and '^(./)+'
#
my $dot_sep_etcRE ;

#
# Matches trailing '/' or '/.'
#
my $trailing_sepRE ;


#############################################################################
#
#     Functions
#


#
# setfstype: takes the name of an operating system and sets up globals that
#            allow the other functions to operate on multiple OSs.  See 
#            %fsconfig for the sets of settings.
#
#            This is run once on module load to configure for the OS named
#            in $^O.
#
# Interface:
#       i)     $osname, as in $^O or plain english: "MacOS", "DOS, etc.
#              This is _not_ usually case sensitive.
#       r)     Name of recognized name on success else undef.  Note that, as
#              shipped, 'unix' is the default is nothing else matches.
#       go)    $fstype and lots of internal parameters and regexs.
#       x)     Dies if a parameter required in @fsconfig is missing.
#
#
# There are some things I couldn't figure a way to parameterize by setting
# globals. $fstype is checked for filesystem type-specific logic, like 
# VMS directory syntax.
#
# Setting up for a particular OS type takes two steps: identify the OS and
# set all of the 'atomic' global variables, then take some of the atomic
# globals which are regexps and build composite values from them.
#
# The atomic regexp terms are generally used to build the larger composite
# regexps that recognize and break apart paths.  This leads to 
# two important rules for the atomic regexp terms:
#
# (1) Do not use '(' ... ')' in the regex terms, since they are used to build
# regexs that use '(' ... ')' to parse paths.
#
# (2) They must be built so that a '?' or other quantifier may be appended.
# This generally means using the '(?:' ... ')' or '[' ... ']' to group
# multicharacter patterns.  Other '(?' ... ')' may also do.
#
# The routines herein strive to preserve the
# original separator and root settings, and, it turns out, never need to
# prepend root to a string (although they do need to insert separators on
# occasion).  This is good, since the Win32 root expressions can be like
# '/', '\', 'A:/', 'a:/', or even '\\' or '//' for UNC style names.
#
# Note that the default root and default notsep are not used, and so are 
# undefined.
#
# For DOS, MacOS, and VMS, we assume that all paths handed in are on the same
# volume.  This is not a significant limitation except for abs2rel, since the
# absolute path is assumed to be on the same volume as the base path.
#
sub setfstype($;) {
   my( $osname ) = @_ ;

   # Find the best match for OS and set up our atomic globals accordingly
   if ( $osname =~ /^(?:(ms)?(dos|win(32|nt)?))/i )
   {
      $fstype           = 'Win32' ;
      $sep              = '/' ;
      $sepRE            = '[\\\\/]' ;
      $notsepRE         = '[^\\\\/]' ;
      $volumeRE         = '(?:^(?:[a-zA-Z]:|(?:\\\\\\\\|//)[^\\\\/]+[\\\\/][^\\\\/]+)?)' ;
      $directoryRE      = '(?:(?:.*[\\\\/](?:\.\.?$)?)?)' ;
      $isrootRE         = '(?:^[\\\\/])' ;
      $thisDir          = '.' ;
      $thisDirRE        = '\.' ;
      $parentDir        = '..' ;
      $parentDirRE      = '(?:\.\.)' ;
      $casesensitive    = 0 ;
      $idempotent       = 1 ;
   }
   elsif ( $osname =~ /^MacOS$/i )
   {
      $fstype           = 'MacOS' ;
      $sep              = ':' ;
      $sepRE            = '\:' ;
      $notsepRE         = '[^:]' ;
      $volumeRE         = '(?:^(?:.*::)?)' ;
      $directoryRE      = '(?:(?:.*:)?)' ;
      $isrootRE         = '(?:^:)' ;
      $thisDir          = '.' ;
      $thisDirRE        = '\.' ;
      $parentDir        = '..' ;
      $parentDirRE      = '(?:\.\.)' ;
      $casesensitive    = 0 ;
      $idempotent       = 1 ;
   }
   elsif ( $osname =~ /^VMS$/i )
   {
      $fstype           = 'VMS' ;
      $sep              = '.' ;
      $sepRE            = '[\.\]]' ;
      $notsepRE         = '[^\.\]]' ;
      # volume is node::volume:, where node:: and volume: are optional 
      # and node:: cannot be present without volume.  node can include
      # an access control string in double quotes.
      # Not supported:
      #     quoted full node names
      #     embedding a double quote in a string ("" to put " in)
      #     support ':' in node names
      #     foreign file specifications
      #     task specifications
      #     UIC Directory format (use the 6 digit name for it, instead)
      $volumeRE         = '(?:^(?:(?:[\w\$-]+(?:"[^"]*")?::)?[\w\$-]+:)?)' ;
      $directoryRE      = '(?:(?:\[.*\])?)' ;

      # Root is the lack of a leading '.', unless string is empty, which
      # means 'cwd', which is relative.
      $isrootRE         = '(?:^[^\.])' ;
      $thisDir          = '' ;
      $thisDirRE        = '\[\]' ;
      $parentDir        = '-' ;
      $parentDirRE      = '-' ;
      $casesensitive    = 0 ;
      $idempotent       = 0 ;
   }
   elsif ( $osname =~ /^URL$/i )
   {
      # URL spec based on RFC2396 (ftp://ftp.isi.edu/in-notes/rfc2396.txt)
      $fstype           = 'URL' ;
      $sep              = '/' ;
      $sepRE            = '/' ;
      $notsepRE         = '[^/]' ;
      # Volume= scheme + authority, both optional
      $volumeRE         = '(?:^(?:[a-zA-Z][a-zA-Z0-9+-.]*:)?(?://[^/?]*)?)' ;

      # Directories do _not_ include the query component: we pretend that 
      # anything after a "?" is the filename or part of it.  So a '/'
      # terminates and is part of the directory spec, while a '?' or '#'
      # terminate and are not part of the directory spec.
      #
      # We pretend that ";param" syntax does not exist
      #
      $directoryRE      = '(?:(?:[^?#]*/(?:\.\.?(?:$|(?=[?#])))?)?)' ;
      $isrootRE         = '(?:^/)' ;
      $thisDir          = '.' ;
      $thisDirRE        = '\.' ;
      $parentDir        = '..' ;
      $parentDirRE      = '(?:\.\.)' ;
      # Assume case sensitive, since many (most?) are.  The user can override 
      # this if they so desire.
      $casesensitive    = 1 ;
      $idempotent       = 1 ;
   }
   else
   { 
      $fstype           = 'Unix' ;
      $sep              = '/' ;
      $sepRE            = '/' ;
      $notsepRE         = '[^/]' ;
      $volumeRE         = '' ;
      $directoryRE      = '(?:(?:.*/(?:\.\.?$)?)?)' ;
      $isrootRE         = '(?:^/)' ;
      $thisDir          = '.' ;
      $thisDirRE        = '\.' ;
      $parentDir        = '..' ;
      $parentDirRE      = '(?:\.\.)' ;
      $casesensitive    = 1 ;
      $idempotent       = 1 ;
   }

   # Now set our composite regexps.

   # Maintain old name for backward compatibility
   $SL= $sep ;

   # Build lots of REs used below, so they don't need to be built every time
   # the routines that use them are called.
   $basenamesplitRE   = '^(.*)' . $sepRE . '(' . $notsepRE . '*)$' ;

   $leading_parentRE  = '(' . $isrootRE . '?)(?:' . $parentDirRE . $sepRE . ')*(?:' . $parentDirRE . '$)?' ;
   $trailing_sepRE    = '(.)' . $sepRE . $thisDirRE . '?$' ;

   $beginning_of_name = '(?:^|' . $isrootRE . '|' . $sepRE . ')' ;

   $dot_sep_etcRE     = 
      '(' . $beginning_of_name . ')(?:' . $thisDirRE . $sepRE . ')+';

   $name_sep_parentRE = 
      '(' . $beginning_of_name . ')'
      . '(?!(?:' . $thisDirRE . '|' . $parentDirRE . ')' . $sepRE . ')'
      . $notsepRE . '+' 
      . $sepRE . $parentDirRE 
      . '(?:' . $sepRE . '|$)'
      ;

   if ( $verbose ) {
      print( <<TOHERE )  ;
fstype        = "$fstype"
sep           = "$sep"
sepRE         = /$sepRE/
notsepRE      = /$notsepRE/
volumeRE      = /$volumeRE/
directoryRE   = /$directoryRE/
isrootRE      = /$isrootRE/
thisDir       = "$thisDir"
thisDirRE     = /$thisDirRE/
parentDir     = "$parentDir"
parentDirRE   = /$parentDirRE/
casesensitive = "$casesensitive"
TOHERE
   }

   return $fstype ;
}


setfstype( $^O ) ;


#
# splitpath: Splits a path into component parts: volume, dirpath, and filename.
#
#           Very much like File::Basename::fileparse(), but doesn't concern
#           itself with extensions and knows about volume names.
#
#           Returns ($volume, $directory, $filename ).
#
#           The contents of the returned list varies by operating system.
#
#           Unix:
#              $volume: always ''
#              $directory: up to, and including, final '/'
#              $filename: after final '/'
#
#           Win32:
#              $volume: drive letter and ':', if present
#              $directory and $filename are like on Unix, but '\' and '/' are
#              equivalent and the $volume is not in $directory..
#
#           VMS:
#              $volume: up to and including first ":"
#              $directory: "[...]" component
#              $filename: the rest.
#              $nofile is ignored
#
#           URL:
#              $volume: up to ':', then '//stuff/morestuff'.  No trailing '/'.
#              $directory: after $volume, up to last '/'
#              $filename: the rest.
#              $nofile is ignored
#
# Interface:
#       i)     $path
#       i)     $nofile: if true, then any trailing filename is assumed to
#              belong to the directory for non-VMS systems.
#       r)     list of ( $volume, $directory, $filename ).
#
sub splitpath {
   my( $path, $nofile )= @_ ;
   my( $volume, $directory, $file ) ;
   if ( $fstype ne 'VMS' && $fstype ne 'URL' && $nofile ) {
      $path =~ m/($volumeRE)(.*)$/ ;
      $volume   = $1 ;
      $directory= $2 ;
      $file     = '' ;
   }
   else {
      $path =~ m/($volumeRE)($directoryRE)(.*)$/ ;
      $volume   = $1 ;
      $directory= $2 ;
      $file     = $3 ;
   }

   # For Win32 UNC, force the directory portion to be non-empty. This is
   # because all UNC names are absolute, even if there's no trailing separator
   # after the sharename.
   #
   # This is a bit of a hack, necesitated by the implementation of $isrootRE,
   # which is only applied to the directory portion.
   #
   # A better long term solution might be to make the isroot test a member 
   # function in the future, object-oriented version of this.
   #
   $directory = $1
     if ( $fstype eq 'Win32' && $volume =~ /^($sepRE)$sepRE/ && $directory eq '' ) ;

   return ( $volume, $directory, $file ) ;
}


#
# joinpath: joins the results of splitpath().  Not really necessary now, but
# good to have:
#
#     - API completeness
#     - Self documenting code
#     - Future handling of other filesystems
#
# For instance, if you leave the ':' or the '[' and ']' out of VMS $volume
# and $directory strings, this patches it up.  If you leave out the '['
# and provide the ']', or vice versa, it is not cleaned up.  This is
# because it's useful to automatically insert both '[' and ']', but if you
# leave off only one, it's likely that there's a bug elsewhere that needs
# looking in to.
#
# Automatically inserts a separator between directory and filename if needed
# for non-VMS OSs.
#
# Automatically inserts a separator between volume and directory or file 
# if needed for Win32 UNC names.
#
sub joinpath($;$;$;) {
   my( $volume, $directory, $filename )= @_ ;

   # Fix up delimiters for $volume and $directory as needed for various OSs
   if ( $fstype eq 'VMS' ) {
      $volume .= ':'
         if ( $volume ne '' && $volume !~ m/:$/ ) ;

      $directory = join( '', ( '[', $directory, ']' ) )
         if ( $directory ne '' && $directory !~ m/^\[.*\]$/ ) ;
   }
   else {
      # Add trailing separator to directory names that require it and
      # need it.  URLs always require it if there are any directory
      # components.
      $directory .= $sep
         if (  $directory ne '' 
            && ( $fstype eq 'URL' || $filename ne '' )
            && $directory !~ m/$sepRE$/ 
            ) ;

      # Add trailing separator to volume for UNC and HTML volume
      # names that lack it and need it.
      # Note that if a URL volume is a scheme only (ends in ':'),
      # we don't require a separator: it's a relative URL.
      $volume .= $sep
         if (  (  ( $fstype eq 'Win32' && $volume =~ m#^$sepRE{2}# )
               || ( $fstype eq 'URL'   && $volume =~ m#[^:/]$#     )
               )
            && $volume    !~ m#$sepRE$#  
            && $directory !~ m#^$sepRE#      
            && ( $directory ne '' || $filename ne '' )
            ) ;
   }

   return join( '', $volume, $directory, $filename ) ;
}


#
# splitdirs: Splits a string containing directory portion of a path
# in to component parts.  Preserves trailing null entries, unlike split().
#
# "a/b" should get you [ 'a', 'b' ]
#
# "a/b/" should get you [ 'a', 'b', '' ]
#
# "/a/b/" should get you [ '', 'a', 'b', '' ]
#
# "a/b" returns the same array as 'a/////b' for those OSs where
# the seperator is idempotent (Unix and DOS, at least, but not VMS).
#
# Interface:
#     i) directory path string
#
sub splitdirs($;) {
   my( $directorypath )= @_ ;

   $directorypath =~ s/^\[(.*)\]$/$1/
      if ( $fstype eq 'VMS' ) ;

   #
   # split() likes to forget about trailing null fields, so here we
   # check to be sure that there will not be any before handling the
   # simple case.
   #
   return split( $sepRE, $directorypath )
      if ( $directorypath !~ m/$sepRE$/ ) ;

   #
   # since there was a trailing separator, add a file name to the end, then
   # do the split, then replace it with ''.
   #
   $directorypath.= "file" ;
   my( @directories )= split( $sepRE, $directorypath ) ;
   $directories[ $#directories ]= '' ;

   return @directories ;
}

#
# joindirs: Joins an array of directory names in to a string, adding
# OS-specific delimiters, like '[' and ']' for VMS.
#
# Note that empty strings '' are no different then non-empty strings,
# but that undefined strings are skipped by this algorithm.
#
# This is done the hard way to preserve separators that are already
# present in any of the directory names.
#
# Could this be made faster by using a join() followed 
# by s/($sepRE)$sepRE+/$1/g?
#
# Interface:
#     i) array of directory names
#     o) string representation of directory path
#
sub joindirs {
   my $directory_path ;

   $directory_path = shift
      while ( ! defined( $directory_path ) && @_ ) ;

   if ( ! defined( $directory_path ) ) {
      $directory_path = '' ;
   }
   else {
      local $_ ;

      for ( @_ ) {
        next if ( ! defined( $_ ) ) ;

        $directory_path .= $sep
           if ( $directory_path !~ /$sepRE$/ && ! /^$sepRE/ ) ;

        $directory_path .= $_ ;
      }
   }

   $directory_path = join( '', '[', $directory_path, ']' )
      if ( $fstype eq 'VMS' ) ;

   return $directory_path ;
}


#
# realpath: returns the canonicalized absolute path name
#
# Interface:
#       i)      $path   path
#       r)              resolved name on success else undef
#       go)     $resolved
#                       resolved name on success else the path name which
#                       caused the problem.
$resolved = '';
#
#       Note: this implementation is based 4.4BSD version realpath(3).
#
# TODO: Speed up by using Cwd::abs_path()?
#
sub realpath($;) {
    ($resolved) = @_;
    my($backdir) = cwd();
    my($dirname, $basename, $links, $reg);

    $resolved = regularize($resolved);
LOOP:
    {
        #
        # Find the dirname and basename.
        # Change directory to the dirname component.
        #
        if ($resolved =~ /$sepRE/) {
            ($dirname, $basename) = $resolved =~ /$basenamesplitRE/ ;
            $dirname = $sep if ( $dirname eq '' );
            $resolved = $dirname;
            unless (chdir($dirname)) {
                warn("realpath: chdir($dirname) failed: $! (in ${\cwd()}).") if $verbose;
                chdir($backdir);
                return undef;
            }
        } else {
            $dirname = '';
            $basename = $resolved;
        }
        #
        # If it is a symlink, read in the value and loop.
        # If it is a directory, then change to that directory.
        #
        if ( $basename ne '' ) {
            if (-l $basename) {
                unless ($resolved = readlink($basename)) {
                    warn("realpath: readlink($basename) failed: $! (in ${\cwd()}).") if $verbose;
                    chdir($backdir);
                    return undef;
                }
                $basename = '';
                if (++$links > $maxsymlinks) {
                    warn("realpath: too many symbolic links: $links.") if $verbose;
                    chdir($backdir);
                    return undef;
                }
                redo LOOP;
            } elsif (-d _) {
                unless (chdir($basename)) {
                    warn("realpath: chdir($basename) failed: $! (in ${\cwd()}).") if $verbose;
                    chdir($backdir);
                    return undef;
                }
                $basename = '';
            }
        }
    }
    #
    # Get the current directory name and append the basename.
    #
    $resolved = cwd();
    if ( $basename ne '' ) {
        $resolved .= $sep if ($resolved ne $sep);
        $resolved .= $basename
    }
    chdir($backdir);
    return $resolved;
} # end sub realpath


#
# abs2rel: make a relative pathname from an absolute pathname
#
# Interface:
#       i)      $path   absolute path(needed)
#       i)      $base   base directory(optional)
#       r)              relative path of $path
#
#       Note:   abs2rel doesn't check whether the specified path exist or not.
#
sub abs2rel($;$;) {
    my($path, $base) = @_;
    my($reg );

    my( $path_volume, $path_directory, $path_file )= splitpath( $path,'nofile');
    if ( $path_directory !~ /$isrootRE/ ) {
        warn("abs2rel: nothing to do: '$path' is relative.") if $verbose;
        return $path;
    }

    $base = cwd()
       if ( $base eq '' ) ;

    my( $base_volume, $base_directory, $base_file )= splitpath( $base,'nofile');
    # check for a filename, since the nofile parameter does not work for OSs
    # like VMS that have explicit delimiters between the dir and file portions
    warn( "abs2rel: filename '$base_file' passed in \$base" )
       if ( $base_file ne '' && $verbose ) ;

    if ( $base_directory !~ /$isrootRE/ ) {
        # Make $base absolute
        my( $cw_volume, $cw_directory, $dummy ) = splitpath( cwd(), 'nofile' ) ;
        # maybe we should warn if $cw_volume ne $base_volume and both are not ''
        $base_volume= $cw_volume
           if ( $base_volume eq '' && $cw_volume ne '' ) ;
        $base_directory = join( '', $cw_directory, $sep, $base_directory ) ;
    }

#print( "[$path_directory,$base_directory]\n" ) ;
    $path_directory = regularize( $path_directory );
    $base_directory = regularize( $base_directory );
#print( "[$path_directory,$base_directory]\n" ) ;
    # Now, remove all leading components that are the same, so 'name/a'
    # 'name/b' become 'a' and 'b'.
    my @pathchunks = split($sepRE, $path_directory);
    my @basechunks = split($sepRE, $base_directory);

    if ( $casesensitive ) 
    {
        while (@pathchunks && @basechunks && $pathchunks[0] eq $basechunks[0]) 
        {
            shift @pathchunks ;
            shift @basechunks ;
        }
    }
    else {
        while (  @pathchunks 
              && @basechunks 
              && lc( $pathchunks[0] ) eq lc( $basechunks[0] ) 
              ) 
        {
            shift @pathchunks ;
            shift @basechunks ;
        }
    }

    # No need to use joindirs() here, since we know that the arrays
    # are well formed.
    $path_directory= join( $sep, @pathchunks );
    $base_directory= join( $sep, @basechunks );
#print( "[$path_directory,$base_directory]\n" ) ;

    # Convert $base_directory from absolute to relative
    if ( $fstype eq 'VMS' ) {
        $base_directory= $sep . $base_directory
            if ( $base_directory ne '' ) ;
    }
    else {
        $base_directory=~ s/^$sepRE// ;
    }

#print( "[$base_directory]\n" ) ;
    # $base_directory now contains the directories the resulting relative path 
    # must ascend out of before it can descend to $path_directory.  So, 
    # replace all names with $parentDir
    $base_directory =~ s/$notsepRE+/$parentDir/g ;
#print( "[$base_directory]\n" ) ;

    # Glue the two together, using a separator if necessary, and preventing an
    # empty result.
    if ( $path_directory ne '' && $base_directory ne '' ) {
        $path_directory = "$base_directory$sep$path_directory" ;
    } else {
        $path_directory = "$base_directory$path_directory" ;
    }

    $path_directory = regularize( $path_directory ) ;

    # relative URLs should have no name in the volume, only a scheme.
    $path_volume=~ s#/.*##
        if ( $fstype eq 'URL' ) ;
    return joinpath( $path_volume, $path_directory, $path_file ) ;
}

#
# rel2abs: make an absolute pathname from a relative pathname
#
# Assumes no trailing file name on $base.  Ignores it if present on an OS
# like $VMS.
#
# Interface:
#       i)      $path   relative path (needed)
#       i)      $base   base directory  (optional)
#       r)              absolute path of $path
#
#       Note:   rel2abs doesn't check if the paths exist.
#
sub rel2abs($;$;) {
    my( $path, $base ) = @_;
    my( $reg );

    my( $path_volume, $path_directory, $path_file )= splitpath( $path, 'nofile' ) ;
    if ( $path_directory =~ /$isrootRE/ ) {
        warn( "rel2abs: nothing to do: '$path' is absolute" ) 
            if $verbose;
        return $path;
    }

    warn( "rel2abs: volume '$path_volume' passed in relative path: \$path" )
        if ( $path_volume ne '' && $verbose ) ;

    $base = cwd()
        if ( !defined( $base ) || $base eq '' ) ;

    my( $base_volume, $base_directory, $base_file )= splitpath( $base, 'nofile' ) ;
    # check for a filename, since the nofile parameter does not work for OSs
    # like VMS that have explicit delimiters between the dir and file portions
    warn( "rel2abs: filename '$base_file' passed in \$base" )
        if ( $base_file ne '' && $verbose ) ;

    if ( $base_directory !~ /$isrootRE/ ) {
        # Make $base absolute
        my( $cw_volume, $cw_directory, $dummy ) = splitpath( cwd(), 'nofile' ) ;
        # maybe we should warn if $cw_volume ne $base_volume and both are not ''
        $base_volume= $cw_volume
            if ( $base_volume eq '' && $cw_volume ne '' ) ;
        $base_directory = join( '', $cw_directory, $sep, $base_directory ) ;
    }

    $path_directory = regularize( $path_directory );
    $base_directory = regularize( $base_directory );

    my $result_directory ;
    # Avoid using a separator if either directory component is empty.
    if ( $base_directory ne '' && $path_directory ne '' ) {
        $result_directory= joindirs( $base_directory, $path_directory ) ;
    }
    else {
        $result_directory= "$base_directory$path_directory" ;
    }

    $result_directory = regularize( $result_directory );

    return joinpath( $base_volume, $result_directory, $path_file ) ;
}

#
# regularize a path.  
#
#    Removes dubious and redundant information. 
#    should only be called on directory portion on OSs
#    with volumes and with delimiters that separate dir names from file names,
#    since the separators can take on different semantics, like "\\" for UNC
#    under Win32, or '.' in filenames under VMS.
#
sub regularize {
    my( $in )= $_[ 0 ] ;

    # Combine idempotent separators.  Do this first so all other REs only
    # need to match one separator. Use the first sep found instead of
    # sepRE to preserve slashes on Win32.
    $in =~ s/($sepRE)$sepRE+/$1/g
        if ( $idempotent ) ;

    # We do this after deleting redundant separators in order to be consistent.
    # If a Win32 path ended in \/, we want to be sure that the \ is returned,
    # no the /.
    $in =~ /($sepRE)$sepRE*$/ ;
    my $trailing_sep = defined( $1 ) ? $1 : '' ;

    # Delete all occurrences of 'name/..(/|$)'.  This is done with a while
    # loop to get rid of things like 'name1/name2/../..'. We chose the pattern
    # name/../ as the target instead of /name/.. so as to preserve 'rootness'.
    while ($in =~ s/$name_sep_parentRE/$1/g) {}
   
    # Get rid of ./ in '^./' and '/./'
    $in =~ s/$dot_sep_etcRE/$1/g ;

    # Get rid of trailing '/' and '/.' unless it would leave an empty string
    $in =~ s/$trailing_sepRE/$1/ ;

    # Get rid of '../' constructs from absolute paths
    $in =~ s/$leading_parentRE/$1/
      if ( $in =~ /$isrootRE/ ) ;

#    # Default to current directory if it's now empty.
#    $in = $thisDir if $_[0] eq '' ;
#
    # Restore trailing separator if it was lost. We do this to preserve
    # the 'dir-ness' of the path: paths that ended in a separator on entry
    # should leave with one in case the caller is using trailing slashes to
    # indicate paths to directories.
    $in .= $trailing_sep
        if ( $trailing_sep ne '' && $in !~ /$sepRE$/ ) ;

    return $in ;
}

1;

__END__

=head1 NAME

abs2rel - convert an absolute path to a relative path

rel2abs - convert a relative path to an absolute path

realpath - convert a logical path to a physical path (resolve symlinks)

splitpath - split a path in to volume, directory and filename components

joinpath - join volume, directory, and filename components to form a path

splitdirs - split directory specification in to component names

joindirs - join component names in to a directory specification

setfstype - set the file system type


=head1 SYNOPSIS

    use File::PathConvert qw(realpath abs2rel rel2abs setfstype splitpath 
      joinpath splitdirs joindirs $resolved);

    $relpath = abs2rel($abspath);
    $abspath = abs2rel($abspath, $base);

    $abspath = rel2abs($relpath);
    $abspath = rel2abs($relpath, $base);

    $path = realpath($logpath) || die "resolution stopped at $resolved";

    ( $volume, $directory, $filename )= splitpath( $path ) ;
    ( $volume, $directory, $filename )= splitpath( $path, 'nofile' ) ;

    $path= joinpath( $volume, $directory, $filename ) ;

    @directories= splitdirs( $directory ) ;
    $directory= joindirs( @directories ) ;

=head1 DESCRIPTION

File::PathConvert provides functions to convert between absolute and
relative paths, and from logical paths to physical paths on a variety of
filesystems, including the URL 'filesystem'.

Paths are decomposed internally in to volume, directory, and, sometimes
filename portions as appropriate to the operation and filesystem, then
recombined.  This preserves the volume and filename portions so that they may
be returned, and prevents them from interfering with the path conversions.  

Here are some examples of path decomposition.  A '****' in a column indicates
the column is not used in C<abs2rel> and C<rel2abs> functions for that
filesystem type.


    FS      VOLUME                  Directory       filename
    ======= ======================= =============== =============
    URL     http:                   /a/b/           c?query
            http://fubar.com        /a/b/           c?query
            //p.d.q.com             /a/b/c/         ?query

    VMS     Server::Volume:         [a.b]           c
            Server"access spec"::   [a.b]           c
            Volume:                 [a.b]           c

    Win32   A:                      \a\b\c          ****
            \\server\Volume         \a\b\c          ****
            \\server\Volume         \a/b/c          ****

    Unix    ****                    \a\b\c          ****

    MacOS   Volume::                a:b:c           ****

Many more examples abound in the test.pl included with this module.

Only the VMS and URL filesystems indicate if the last name in a path is a
directory or file.  For other filesystems, all non-volume names are assumed to
be directory names.  For URLs, the last name in a path is assumed to be a
filename unless it ends in '/', '/.', or '/..'.   

Other assumptions are made as well, especially MacOS and VMS. THESE MAY CHANGE
BASED ON PROGRAMMER FEEDBACK!

The conversion routines C<abs2rel>, C<rel2abs>, and C<realpath> are the 
main focus of this package.  C<splitpath> and C<joinpath> are provided to 
allow volume oriented filesystems (almost anything non-unixian, actually)
to be accomodated.  C<splitdirs> and C<joindirs> provide directory path
grammar parsing and encoding, which is especially useful for VMS.

=over 4

=item setfstype

This is called automatically on module load to set the filesystem type
according to $^O. The user can call this later set the filesystem type
manually.  If the name is not recognized, unix defaults are used.  Names
matching /^URL$/i, /^VMS$/i, /^MacOS$/i, or /^(ms)?(win|dos)/32|nt)?$/i yield
the appropriate (hopefully) filesystem settings.  These strings may be
generalized in the future.

Examples:

    File::PathConvert::setfstype( 'url' ) ; 
    File::PathConvert::setfstype( 'Win32' ) ;
    File::PathConvert::setfstype( 'HAL9000' ) ; # Results in Unix default

=item abs2rel

C<abs2rel> converts an absolute path name to a relative path:
converting /1/2/3/a/b/c relative to /1/2/3 returns a/b/c

    $relpath= abs2rel( $abspath ) ;
    $relpath= abs2rel( $abspath, $base ) ;

If $abspath is already relative, it is returned unchanged.  Otherwise the
relative path from $base to $abspath is returned.  If $base is undefined the
current directory is used.

The volume and filename portions of $base are ignored if present.  
If $abspath and $base are on different volumes, the volume from $abspath is
used.

No filesystem calls are made except for getting the current working directory
if $base is undefined, so symbolic links are not checked for or resolved, and
no check is done for existence.

Examples

    # Unix
    'a/b/c' == abs2rel( 'a/b/c', $anything )
    'a/b/c' == abs2rel( '/1/2/3/a/b/c', '/1/2/3' )

    # DOS
    'a\\b/c' == abs2rel( 'a\\b/c', $anything )
    'a\\b/c' == abs2rel( '/1\\2/3/a\\b/c', '/1/2/3' )

    # URL
    'http:a/b/c'           == abs2rel( 'http:a/b/c', $anything ) 
    'http:a/b/c'           == abs2rel( 'http:/1/2/3/a/b/c',
                                       'ftp://t.org/1/2/3/?z' )
    'http:a/b/c?q'         == abs2rel( 'http:/1/2/3/a/b/c/?q',
                                       'ftp://t.org/1/2/3?z'  )
    'http://s.com/a/b/c?q' == abs2rel( 'http://s.com/1/2/3/a/b/c?q',
                                       'ftp://t.org/1/2/3/?z')

=item rel2abs

C<rel2abs> makes converts a relative path name to an absolute path: 
converting a/b/c relative to /1/2/3 returns /1/2/3/a/b/c.

    $abspath= rel2abs( $relpath ) ;
    $abspath= rel2abs( $relpath, $base ) ;

If $relpath is already absolute, it is returned unchanged.  Otherwise $relpath
is taken to be relative to $base and the resulting absolute path is returned.
If $base is not supplied, the current working directory is used.

The volume portion of $relpath is ignored.  The filename portion of $base is
also ignored. The volume from $base is returned if present. The filename
portion of $abspath is returned if present.

No filesystem calls are made except for getting the current working directory
if $base is undefined, so symbolic links are not checked for or resolved, and
no check is done for existence.

C<rel2abs> will not return a path of the form "./file".

Examples

    # Unix
    '/a/b/c'       == rel2abs( '/a/b/c', $anything )
    '/1/2/3/a/b/c' == rel2abs( 'a/b/c', '/1/2/3' )

    # DOS
    '\\a\\b/c'                == rel2abs( '\\a\\b/c', $anything )
    '/1\\2/3\\a\\b/c'         == rel2abs( 'a\\b/c', '/1\\2/3' )
    'C:/1\\2/3\\a\\b/c'       == rel2abs( 'D:a\\b/c', 'C:/1\\2/3' )
    '\\\\s\\v/1\\2/3\\a\\b/c' == rel2abs( 'D:a\\b/c', '\\\\s\\v/1\\2/3' )

    # URL
    'http:/a/b/c?q'            == rel2abs( 'http:/a/b/c?q', $anything )
    'ftp://t.org/1/2/3/a/b/c?q'== rel2abs( 'http:a/b/c?q',
                                           'ftp://t.org/1/2/3?z' )


=item realpath

C<realpath> makes a canonicalized absolute pathname and
resolves all symbolic links, extra ``/'' characters, and references
to /./ and /../ in the path.
C<realpath> resolves both absolute and relative paths.
It returns the resolved name on success, otherwise it returns undef
and sets the valiable C<$File::PathConvert::resolved> to the pathname
that caused the problem.

All but the last component of the path must exist.

This implementation is based on 4.4BSD realpath(3).  It is not tested under
other operating systems at this time.

If '/sys' is a symbolic link to '/usr/src/sys':

    chdir('/usr');
    '/usr/src/sys/kern' == realpath('../sys/kern');
    '/usr/src/sys/kern' == realpath('/sys/kern');

=item splitpath

To be written...

=item joinpath

To be written...

Note that C<joinpath( splitpath( $path ) )> usually yields path.  URLs
with directory components ending in '/.' or '/..' will be fixed 
up to end in '/./' and '/../'.

=item splitdirs

To be written...

=item joindirs


=back

=head1 BUGS

C<realpath> is not fully multiplatform.


=head1 LIMITATIONS

=over 4

=item *

In URLs, paths not ending in '/' are split such that the last name in the
path is a filename.  This is not intuitive: many people use such URLs for
directories, and most servers send a redirect.  This may cause programmers
using this package to code in bugs, it may be more pragmatic to always assume
all names are directory names.  (Note that the query portion is always part
of the filename).

=item *

If the relative and base paths are on different volumes, no error is
returned.  A silent, hopefully reasonable assumption is made.

=item *

No detection of unix style paths is done when other filesystems are
selected, like File::Basename does.

=back

=head1 AUTHORS

Barrie Slaymaker <rbs@telerama.com>
Shigio Yamaguchi <shigio@wafu.netgate.net>

=cut