FB II Compiler







Disk I/O














Made with FB


Get a working directory from a pathname

It's much easier than you think, though it's not documented in the FB reference manual (yet) :
wdRefNum% = FOLDER(fullPath$, 0)
Just beware that this statement will also cause wdRefNum% to become the current default directory. If you don't want that to happen, then do it like this :
oldDefault% = FOLDER("", 0) 'Save old default
wdRefNum% = FOLDER(fullPath$, 0)
dummy% = FOLDER("", oldDefault%) 'Restore old default
Sylvain wrote :
<< I asked Frederic Rinaldi to help me about converting a pathname to a VRefNum, and he sent me the following code. >>

From your original post, it looks like what you want is something that will take a folder's pathname and return a working directory reference number that stands for the folder.

In contrast, it looks like the code your friend gave you takes a volume name, and returns the reference number of the volume. It looks like another case of confusion between the two different meanings of the term "volRefNum."

I would do something like this :

1) Use Frederic's function to find the (true) volume reference number of the volume that contains your folder. You'll need this number later.
2) Use FN GETCATINFO to find the directory ID number of the folder.
3) pass the volume reference number (from Step 1) and the directory ID number (from Step 2) to the OPENWD function, to generate a working directory reference number.

Here is an FB translation of Frederic's function, to get you started :

LOCAL FN GetVolRefNum(pathname$, volRefNumAddr&)
'Returns the volume reference number (NOT a
'working directory ref. number!) of the
'volume specified in pathname$.  pathname$ may
'be the name of the volume, or the path of any
'file or folder on the volume.  Call as follows : 
'OSErr = FN GetVolRefNum(pathname$, @volRefNum%)
DIM thePBrec.128, theErr
IF INSTR(1, pathname$, " : ") = 0 THEN pathname$ = pathname$ + " : "
thePBrec.ioVolIndex% = - 1
thePBrec.ioVRefNum% = 0
thePBrec.ioNamePtr& = @pathname$
theErr = FN HGETVINFO(@thePBrec)
POKE WORD volRefNumAddr&, thePBrec.ioVRefNum%
END FN = theErr

Well, I'm going to tell you how to do it, but first I'm going to try to convince you that YOU DON'T NECESSARILY WANT TO DO THIS.

First, using full pathnames is generally a bad idea given the 255 character limit on FB strings and --more generally --on most strings used by the toolbox itself. Using pathnames limited to 255 characters is a naughty Windows - like practice.

In addition, anytime you see a mention of a "volume reference number" in the FB docs, it's probably talking about a working directory number.
Working directory numbers are evil. According to Inside Mac : Files, the system can only have a limited number of directories "open" (as "working" directories) at once, and these numbers are not fixed (in other words, on two successive runs of your program, if you get the working directory number for the same folder, it will probably have changed). My copy of IM : Files also says that Apple strongly recommends NOT using working directory numbers --it says they are used internally by the file manager and should NOT be used by applications. The date on my copy of IM : Files is 1992, so even six years ago Apple considered the use of working directory numbers to be a BAD practice. Unfortunately FB relies on them heavily.

In my own code, my usual strategy is to take the working directory reference number that FB returns and convert it into these three pieces of information, which is how Apple suggests referring to a file :
volume reference number (short integer)
parent directory ID number (long integer)
name (up to 63 characters long --64 bytes total including the string length byte)

Apple calls these three pieces of data a Filesystem Specification (an FSSpec record --it contains these three items in the same order I listed them).

So what's a volume reference number? Whenever a volume is mounted, it receives a volume reference number, which is a negative integer. Which number a particular volume gets depends on when it was mounted. Currently the boot volume is always - 1, although I'm not sure relying on this is a good forward - looking strategy.

And what's a directory ID number? Each directory on a volume has a unique ID number assigned to it. Unlike a volume reference number or a working directory number, this number does * not * change across reboots. It will only change if you delete the directory and recreate it. In an FSSpec record, the directory ID number is the ID of the directory which contains the named item. So what an FSSPec contains is a number that identifies a volume, a number that identifies a folder on that volume, and a name which identifies something inside that folder. As an example, the system folder on most Macs would have this FSSpec record :

vref : - 1
dir ID : 2
name : System Folder

The vref is - 1 because the folder is on the boot volume. The dir ID is 2 because the parent directory of most peoples' system folders is the hard disk's root directory, and the root directory's ID is 2. Note that there's no problem with path name lengths here if the system folder is buried hundreds of folder levels deep.

Now, if you decide you want to proceed using working directory numbers instead, here's how you can do it (but be careful). There's a short snip of code on page 186 of the handbook that shows you how to get the working directory ID number of a directory (by name) as long as you already know the working directory ID number of its parent directory. It involves using the FOLDER statement. If you look at the FOLDER statement docs in the reference manual, it explains that using this function in this way will "open" a working directory number for the folder, and that only so many working directory numbers can be open at once. You can use CLOSE FOLDER to close working directory numbers, but the reference manual cautions against this : "Do not close a [working directory number] unless you are the only one who could have opened it." So if you run out of working directory numbers, it's almost impossible to know which existing numbers can be safely closed! Perhaps now you are beginning to see why this system is such a mess! It's very annoying that FB relies on it.

Also, this method won't get you a working directory number for the root directory. But the working directory number for a volume's root directory has the same value as its volume reference number, so you can just pass that instead. The next question you might have is how to get a volume's vref% number. You should be able to do that using the GET VOLUME INFO command (see the FB Reference manual) if you clear the entire parameter block, set .ioNamePtr& to the address of a string containing the volume name, and then make the GET VOLUME INFO call. But --and here is another warning against path names --I believe that MacOS allows multiple volumes to have the same name. So you could get the wrong volume name back, and that's not as farfetched a scenario as it might seem : what if two people have drives named "Macintosh HD" and one of them mounts the other drive over the network? This approach might identify the WRONG VOLUME (very bad)!

In theory, the following function will do what you want (it's modified from some other code of mine, and I haven't tested my modifications). If you want to understand how it works (and a good programmer always understands how the code he's using works :-), then you'll have to read the rest of this rather long message.

'this function converts a path name to a
'working directory number
'Note the total lack of error checking.
DIM 255 paramblock$,pbPtr&,oserr%,workingDirectoryNumber%
LOCAL FN NameToID (fold$,myAppCreatorCode&)                         'fold$ holds a full pathname

  pbPtr& = @paramblock$           'Get address of this block of memory...
  pbPtr&.ioCompletion& = 0        '...and access it as a record.
  pbPtr&.ioNamePtr& = @fold$      'A pointer to the pathname of a directory.
  pbPtr&.ioVRefNum% = 0           'A volume reference number.

  oserr% = FN GETCATINFO (pbPtr&)

  pbPtr&.ioWDProcID& = myAppCreatorCode&
  oserr% = FN OPENWD (pbPtr&)

  workingDirectoryNumber% = pbPtr&.ioVRefNum%

  'you could call CLOSEWD when you're done using this directory, but
  'as I explained previously, that can be dangerous

END FN = workingDirectoryNumber%
Now for the explanation of how this works.

First let me start with an aside on where working directory numbers came from and why FB confuses them with volume reference numbers.

On the original Macintosh, the filesystem was called MFS (Macintosh File System). It did * not * support directories at all. Every file on the disk was stored in the same directory. The system software created the illusion of directories, but in fact there weren't any (one consequence was that no files on the same volume could have the same name --even if they were in different "fake" directories). Then Apple introduced HFS, which allowed real directories. And there was a very big problem. All the existing software was used to identifying files by two pieces of data : the volume reference number and the file's name. But that information wasn't sufficient any more, because on an HFS volume two files can share the same name if they are in different directories. Uh - oh!

So Apple hacked around the problem. Apple made it appear to old applications as if * every folder * on an HFS drive were a separate volume!
It did that by assigning volume reference numbers to folders. Since these weren't * really * volume reference numbers (they refer to folders rather than disk partitions), Apple gave them a new name : working directory numbers. The File Manager keeps track of the difference between the two, but old applications are fooled into thinking they have received a volume reference number when in fact they have received a working directory number. At this point it is probably clear why the confusion exists.

But this solution creates another problem. Volume reference numbers are short integers, and directory ID numbers are long integers. So the File Manager can't just assign a working directory number to every folder. It might run out of numbers. Instead, it assigns them on an as - needed basis.
This is why you must "open" and "close" working directory numbers. When your application quits, modern versions of Mac OS will automatically close working directory numbers that your application used as long as no other programs have been using them.

In summary, working directory numbers are a hack to allow old software programs to access HFS volumes. There is NO GOOD REASON why any application written since the introduction of HFS (a loooong time ago) should use them. Shame on whoever decided to use them heavily in FB's file routines.

Now let's talk about what's going on in the above function. In theory you can get a working directory number with the OpenWD toolbox call (you'd have to convert your path name into a volume reference number and directory ID first) :

FUNCTION OpenWD (vrefnum : Integer; dirID : LongInt; procID : LongInt; VAR wdRefNum : Integer) : OSerr;

This is the Pascal toolbox info. procID& is (quoting IM : Files) "a working directory user identifier. You should use your application's signature as the user identifier." You can use this info to write an assembly language routine in FB that calls OpenWD. See page 239 of the handbook. In FB, the assembly language function would be called like this :

oserr% = myOpenWD (vref%, dirID&, procID&, @workingDirID%)
It happens that FutureBASIC does implement a function called OpenWD, but it does * NOT * implement it as described in IM : Files. (This sort of thing is a very big pain. I really hate FB's file management.) In FB's implementation of OpenWD, you need to pass a parameter block, which includes things like ".ioNamePtr&".

If .ioNamePtr& really is Japanese to you, you'd better learn a new language (don't worry, it's not hard). :-) The toolbox has more friendly routines --such as the version of OpenWD I described above --that do not use parameter blocks, but FutureBasic implements approximately ZERO of them (another reason its file management drives me crazy). The File Manager calls that FB implements use parameter blocks. When you hit a wall with
the built - in FutureBasic file routines, you can either learn parameter blocks or you can write assembly routines to call the friendlier file manager calls that FB doesn't support. The former is less of a pain, which is why I used FB's version of OpenWD in the sample function at the top of this message.

A parameter block is just a chunk of memory... in FB, you end up treating it as if it were a record. The record definitions are available in Inside Mac, and FB gives partial definitions in the appropriate places (like the docs for GET VOLUME INFO in the reference manual). When you create a record in FB, FB creates constants that let you access the data in the record. If you're not familiar with records, you should probably read the appropriate section in the FB manuals before continuing. Here is a sample record definition from one of my own programs (with a lot of entries snipped) :

DIM RECORD myXVolumeParam
 DIM myQElemPtr&
 DIM myQType%
 DIM myioTrap%
 DIM myioCmdAddr&
 DIM myioCompletion&
DIM END RECORD _myXVolumeParam
Now, when you want to create a record of type myXVolumeParam, you do the following in FB :
DIM myRecord.myXVolumeParam
And then you read or assign values as follows :
myRecord.myQElemPtr& = 0
otherVariable& = myRecord.myQElemPtr&
To understand a parameter block, you need to understand how FutureBasic accesses records. In this example, let's say that FB knows that the data for myRecord starts at memory location 100. Since long integers are four bytes long, bytes 100 - 103 hold the value for myQElemPtr& and 104 - 105 hold the value for myQType% (two - byte short integer). FB associates the value zero with .myQElemPtr& and the value 4 with .myQType%. When you access myRecord.myQType%, it adds the value of .myQType% to the starting location of myRecord. That's 100 + 4, so FB knows that the data for myRecord.myQType% starts at memory location 104.

If you access myRecord.myioTrap%, FB adds 100 to 6, and knows that the data for myioTrap% starts at memory location 106.

Now, if you're familiar with how FutureBasic treats pointers, you know that you can treat any long integer variable as a pointer. Here's the important part : *** FB treats a DIMmed record name just like a pointer! *** And if that's true, then you might suspect that you can use a pointer as if it were a record. And, in fact, you can. Therefore, the following two pieces of code accomplish exactly the same thing :

Local FN MyRoutineA
  DIM myRecord.myXVolumeParam

  myRecord.myQElemPtr& = 0
  FN CallSomeOtherFunction (myRecord)

Local FN MyRoutineB
  DIM myRecord.myXVolumeParam,recordPtr&

  recordPtr& = @myRecord
  recordPtr&.myQElemPtr& = 0
  FN CallSomeOtherFunction (recordPtr&)
This also means that you can access any block of memory as any particular type of record! If I have a pointer at memory location 2000, then writing myVariable% = myPointer&.myQType% will copy memory locations 2004 and 2005 into myVariable%. FB does not care whether or not there is a DIMmed record sitting at location 2000.

If I have a pointer to a string (such as myPtr& = @myString$), then myVariable% = myPtr&.myQType% will copy two bytes from the fifth and sixth bytes of the string. (If the string is "Apple", the first byte is the string length byte with value five. The fifth and six bytes are the "le" from Apple.)

When you are dealing with low - level file manager routines, the file manager usually expects you to pass it a pointer to a big record. The record has entries for things like volume reference numbers, directory ID numbers, pointers to file or directory names, file sizes, file types, etc. This is the mysterious "parameter block" --a block of memory with lots of parameters in it. In other words, it's a record. When dealing with this data in FutureBasic, you don't usually bother to DIM a record. You just clear out a large block of memory and then access it through its pointer * as if it were a DIMmed record * . FutureBasic already has most of the entries in the record (such as .ioNamePtr&) defined for you.

So now let's look at the function I'm proposing to you :

'this function converts a path name to a
'working directory number
'Note the total lack of error checking.
  DIM 255 paramblock$,pbPtr&,oserr%,workingDirectoryNumber%
LOCAL FN NameToID (fold$,myAppCreatorCode&)                         'fold$ holds a full pathname

  pbPtr& = @paramblock$           'Get address of this block of memory...
  pbPtr&.ioCompletion& = 0        '...and access it as a record.
  pbPtr&.ioNamePtr& = @fold$      'A pointer to the pathname of a directory.
  pbPtr&.ioVRefNum% = 0           'A volume reference number.

  oserr% = FN GETCATINFO (pbPtr&)

  pbPtr&.ioWDProcID& = myAppCreatorCode&
  oserr% = FN OPENWD (pbPtr&)

  workingDirectoryNumber% = pbPtr&.ioVRefNum%

'you could call CLOSEWD when you're done using this directory, but
'as I explained previously, that can be dangerous

END FN = workingDirectoryNumber%
This code uses a 256 byte string called paramblock$ as the parameter block.
Because the function includes the CLEAR LOCAL line, the parameter block will be zeroed. The parameter block contains a lot of entries whose values we won't be concerned with. However, passing random junk in these entries is a very bad thing, so using CLEAR LOCAL (or otherwise zeroing the parablock) is important.

GETCATINFO is a file manager call that will look up the item whose pathname is given and fill in various other parts of the parameter block. If you're getting info on a file, for example, it will fill in pbPtr&.ioFlCrDat&, which is the creation date of the file. If you are getting information on a directory, it fills in the directory ID number (pbPtr&.ioDrDirID&) and volume reference number (pbPtr&.ioVRefNum%) for the directory. [An aside : GetCatInfo doesn't * require * a path name. You can feed it an empty path name along with other information that uniquely specifies a file and it will fill in the file's name --but not the complete path name --for you. You can get info on a directory with GetCatInfo too. It's a very useful function.]

Next my function calls OPENWD, which takes the information in the paramblock, tries to find the directory it describes, and tries to create a working directory number for that directory. OPENWD overwrites the volume reference number stored in pbPtr&.ioVRefNum% and puts a working directory number there instead. Why does it do that?

As I explained at the beginning of this message, working directory numbers were originally a hack designed to fool old programs into thinking that every folder on a drive was actually an independent volume. That meant that the File Manager returned working directory numbers in variables that were originally supposed to hold volume reference numbers. And since the applications thought they have a vref%, they would keep passing this value back to the file manager as a vref%. So whenever you see a variable that's supposed to hold a volume reference number, you can * probably * stick a working directory number in it instead and the File Manager will sort things out. And when dealing with functions that manipulate working directory numbers, the File Manager just uses the variable that's supposed to hold a volume reference number. This is --in a twisted sort of way --just being consistent with how working directory numbers are used elsewhere.

Another important thing to know about parameter blocks is that the file manager uses various memory locations in them for more than one type of entry. For example, pbPtr&.ioVolIndex% and pbPtr&.ioFDirIndex% actually refer to the SAME memory location in the paramblock! The former entry only applies to volumes, so when you are dealing with a volume, the file manager puts a volume index number (ioVolIndex%) there. When you are dealing with files, the file manager doesn't need a volume index number. Instead, it reuses this space for a directory index number (ioFDirIndex%). If you are having mysterious problems with a paramblock and can't figure out why, there may be junk left over from a previous file manager call sitting in some of these multi - purpose places in the paramblock. Try re - clearing the entire paramblock and see if the problems go away.

Eric Bennett

Eric wrote :
<< In theory, the following function will do what you want (it's modified from some other code of mine, and I haven't tested my modifications). >>

I haven't tried it, but I think your function won't quite work, because FN GETCATINFO does _not_ alter the ioVRefNum field, and therefore doesn't return the volume's reference number. That means there's no volume reference number in there when you pass the same block to OPENWD.

It might "accidentally" work in certain situations. Since you're initializing ioVRefNum to zero, that same zero gets passed to OPENWD.
There it's interpreted as meaning, "the same volume that contains the current default directory." Therefore, if the folder you're interested in happens to reside on the same volume where your current default folder resides, the function will coincidentally work.

But to make it more bulletproof, you need to pass the actual volume reference number. This can be gotten by calling such fuctions as HGETVINFO.


Once I figured out that providing FOLDER with the folderpath was much better than providing it with the filepath, everything works:
LOCAL FN fldrpathFromFilePath$(afilePath$)
  DIM pathsize%, count%, char$, aFldrPath$

  pathsize% = LEN(afilePath$)
  FOR count% = pathsize% TO 1 STEP -1
    char$ = MID$(afilePath$, count%, 1)
    LONG IF char$ = ":"
      aFldrPath$ = LEFT$(afilePath$, count%)
      count% = 1
    END IF
  NEXT count%

END FN = aFldrPath$

  fldrPath$      = FN fldrpathFromFilePath$(filePath$)
  CurrWDrefNum%  = FOLDER(fldrPath$, 0)
Michael Evans