Package OS_Services
(Documentation)


General

Files in this distribution:

os_services.ads A library unit package renaming, that allows you to chose the desired implementation
os_services_gnat.ad[sb] specification and body of package OS_Services, Gnat implementation
os_services_tcl.ad[sb] specification and body of package OS_Services, Tcl (Tash) implementation
calendar_utilities.ad[sb] specification and body of package Calendar_Utilities, a reusable component for formatting dates. Used by TCL implementation only.
os_services.htm (this file) package OS_Services documentation
os_services.tcl Supporting Tcl functions for the Tcl implementation. You can ignore this file if you just want to use the package; it is provided as a convenience for those who would like to modify the Tcl implementation.
purge.adb An example program that uses OS_Services

Portability

Portability conditions depend on the selected implementation, and are documented in the body of this documentation.

Copyright, etc.

Package OS_Services, the various implementations provided for it, and the accompanying documentation are Copyright © 2000 ADALOG.

This software is distributed under Adalog's "advertiseware" license. The goal of this license is to make this product widely available as an advertisement for Adalog activities and a demonstration of Adalog's know-how in Ada development, without imposing any constraint on the user, except that code not made by Adalog should not be confused with original code (we cannot accept responsability for your bugs). Therefore:

Rights to use, distribute or modify in any way this software and its documentation are hereby granted, provided:

  1. You do not remove or change the initial comment in the source files that contains the copyright notice and the reference to Adalog's activities. Similarly, you do not remove this copyright notice and the reference to Adalog's activities from this documentation. Additionnal headers in source files, or changes to the documentation are otherwise allowed.
  2. You distribute this documentation with any copy of the software (whether as source or compiled form).
  3. If you make modifications to the software or this documentation, these modifications must be properly identified as additions or modifications not originating from Adalog. If you make a valuable addition, we would appreciate (but do not require) to be kept informed by sending a message to rosen@adalog.fr.

This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

Although we do not require it, you are very welcome, if you find this software useful, to send us a note at rosen@adalog.fr. We also welcome critics, suggestions for improvement, etc.

About Adalog

ADALOG is providing training, consultancy and expertise in Ada and related software engineering techniques. For more info about our services:

ADALOG

19-21 rue du 8 mai 1945
94110 ARCUEIL
FRANCE
Tel: +33 1 41 24 31 40
Fax: +33 1 41 24 07 36
E-m: info@adalog.fr
URL: http://www.adalog.fr/adalog2.htm

Introduction

The goal of package OS_Services is to provide access to the commonly used features of operating systems, such as browsing a file list, moving files between directories, accessing environment variables, etc. If you want a short example of what it can do, have a look at the provided example. Since OS vary greatly, it is difficult to provide a general interface that can be fully supported on every OS (except, of course, the empty interface ;-). Similarly, there are several ways to implement such specifications, with varying trade-offs between efficiency, compiler independence, and OS independence.

For these reasons, we provide various packages, that share (more or less) the same specification, with different implementation techniques. The user is invited to have a library package renaming of the appropriate implementation, and then to "with" that renaming in all his application. This way, it is easy to move a project from one implementation to another one, by simply changing the renaming. Such a package renaming is provided in file os_services.ads; if you don't understand what we mean, here is the content of os_services.ads (here, we have chosen the Gnat implementation):

-- Choose which implementation of OS_Services you want to use by
-- uncommenting the appropriate lines (and commenting out the other ones)

-- Gnat implementation:
with OS_Services_Gnat;
package OS_Services renames OS_Services_Gnat;

-- Tcl implementation:
--with OS_Services_Tcl;
--package OS_Services renames OS_Services_Tcl;

As mentioned in the previous paragraph, all implementations do not have exactly the same specification. However, we were able to design a mechanism such that variations in implementations capabilities simply result in some values being provided, or not, in the definition of an enumeration type. In the following descriptions, we will describe the most general case, and document how much of it is supported by each implementation. If you use only the elements supported by all implementations, you will be able to switch implementations freely; on the other hand, if you switch to an implementation that does not provide some element, the compiler will tell you.

Provided implementations

This release includes two implementations of OS_Services: one which uses packages provided with the Gnat compiler, and will therefore not compile on any other compiler, and one which is based on Tcl (Tash). The following table should help you decide which implementation better fits your needs:

 

Gnat implementation

Tcl implementation

Dependencies Requires the Gnat compiler Requires Tcl and Tash (the Ada binding to Tcl)
Supported OS DOS, NT, OS_2 (untested), Unix Every system that supports Tcl and Tash. This should include (at least):
DOS, NT, OS_2 (untested), Unix, MacOS (untested)
Reentrancy No known reentrancy problems Not all functionalities are reentrant; see package specification before using in multi-tasks programs
Properties Some properties not supported All properties supported

It is expected to provide more implementations in the future; good candidates are implementations based on Posix or Win32. If you are interested in writing an implementation, or if you think that some feature is badly missing from this package, please drop a note to rosen@adalog.fr .

File names

A number of services accept file names as input, or produce file names as output. As a general principle, OS_Services is tolerant on input, and rigorous on output. This means that an input file name can use, for example, either '\' or '/' as the directory separator, but that functions that return file names will always use the separator which is appropriate for the OS.

The most general file name is made up of several parts:

Not all of these parts exist on every OS; for example, Device exists only on DOS-like systems, and Version exists only on VMS. However, if you ask for a part of a file name that is not meaningful for your OS, the corresponding function will simply return the empty string. This should make the writing of OS independent programs easier.

A Path is considered absolute if it starts from a root directory; otherwise, it is considered relative (its interpretation depends on the current directory).

Patterns

A number of services feature a String parameter called Pattern. A pattern is used to describe a set of files that match the pattern (wild-cards). The wild-carding used is the so-called "glob" pattern matching in every case (even on Dos-like systems), summarized in the following table:

This sequence: Matches:
* any string of 0 or more characters
? any single character
[char char ...] any character listed
[char - char] any character in given range
{elmt, elmt, ...} alternation (matches any of elmt, where elmt is any sequence)

In addition, the pattern may include several sub-patterns, separated by space characters. A file name will match the pattern if it matches any of the sub-patterns inside it. This rule, which is natural in many cases, creates a problem for sub-patterns that include a space character. For that reason, a sub-pattern may be enclosed in double-quotes ("). In short, the following:

   File_Copy (Pattern => "*.ads *.adb ""* *""", To => "My_Dir");

will copy all files whose extension is ".ads", ".adb", or any file with a space in its name, to the directory "My_Dir" (remember that quotes are doubled in an Ada string).

Exception

The package defines only one exception, Service_Error, which is raised by any operation that cannot be correctly completed. Whenever this exception is raised, the package provides an explanatory message that can be retrieved with the Ada.Exception.Exception_Message subprogram.

Installation

There is no special installation requirement; unzip and compile the packages as you see fit. You don't need to compile the packages corresponding to implementations that you don't want, but obviously, you'll need to compile at least one implementation!

Both currently provided implementations do their best to automatically recognize the operating system. Should this fail, you can force the package to a given operating system; see the comment in the first lines of the package body to learn how to do that.

Note that if you have -say- a directory for software components, and separate directories for each of your projects, you should compile the implementation packages in your software components directory, and put (a copy of) os_services.ads in each of your project files: this would allow you to use different implementations for different projects.

Don't forget in any case to comment/uncomment the lines in os_services.ads that allow you to select the desired implementation.

Note for the GNAT implementation

The package is set for GNAT 3.13 and above. Due to some (now corrected) bugs, it will not work correctly with older versions of GNAT.

Note for the Tcl implementation

The packages compiles just fine as provided. However, if you want to make some changes, you may want to alter the file os_services.tcl, which contains the source of some useful TCL functions. Merging this file into the Ada code can be done automatically with the utility tcl2ada, which is available from Adalog's components page.

What package OS_Services does

General services

   Default_Ignore_Case : Boolean;

This variable defines the default value for the Ignore_Case parameter used in some subprograms that do file name comparisons. If it is set to True, case is ignored (by default) when comparing file names. The variable is initialized to the normal behavior of the OS (i.e. True for DOS-like systems, False for Unix-like systems).

   type OS_Kind is (Unknown, Dos, NT, OS_2, Unix, VMS, MacOS);
   subtype Dos_OS is OS_Kind range Dos .. OS_2;
   function Operating_System return OS_Kind;

The type OS_Kind defines the (currently) supported operating systems for the implementation; some implementations may not declare some values of OS_Kind if the implementation is not supported on the corresponding OS. Note that the Dos value covers basic MS-Dos, Windows 3.x, and Windows 9x (but not NT). The subtype Dos_OS is a convenience to designate the OSes that follow (more or less) MS-DOS naming system (i.e case not signficant, .EXE for executables, etc.).

The function Operating_System returns the current OS. The value Unknown is returned if and only if the implementation was unable to recognize the OS, but is still able to provide (most of) the functionnalities. If an operation is not available due to the failure to recognize the OS, every call to this operation will raise Service_Error with a message of "Not implemented".

Supported operating systems:
Gnat implementation Dos, NT, OS_2 (untested), Unix
Tcl implementation Dos, NT, OS_2 (untested), Unix, MacOS (untested)
   function Get_Environment (Name : String) return String;

Get the value of an environment variable. Name is interpreted as an environment variable name, according to the OS rules for environment variables. If the environment variable is undefined, an empty string is returned.

   function Executable_Path return String;

Get the name of the executable which is currently running. This includes the full (absolute) path to where the executable is located, and the full name of the executable, irrespectively of how the user typed it (for example under Dos, a ".exe" extension will be added).

   procedure OS_Execute (Command  : in     String;
                         Result   :    out Ada.Command_Line.Exit_Status;
                         Blocking : in     Boolean := True);

   procedure Shell_Execute (Command  : in     String;
                            Result   :    out Ada.Command_Line.Exit_Status
                            Blocking : in     Boolean := True);

These procedures perform the execution of an OS command. OS_Execute calls the OS directly, while Shell_Execute executes the command in a sub-shell (using COMMAND.COM under Dos/Windows9X, CMD.EXE under Windows-NT/OS2, and sh under Unix systems). Note that under DOS-like systems, built-in commands like copy and dir are only available in a sub-shell.

If the Blocking parameter is set to True (the default), the procedure will not return until the command is completed, and Result will be set to the error code returned by the command. If Blocking is set to False, the procedure will return immediately and the command will be run in parallel; in this case, Result is set to Success if the command was successfully started, or to Failure if the command could not be started by the OS.

File name pattern matching utilitities

These subprograms provide facilities for matching names against patterns. They are not strictly speaking related to files, but the matching mechanism corresponds to what is needed for dealing with file names; more specifically, these functions can be handy in writing filters.

   function Match (Name : String; Pattern : String; Ignore_Case : Boolean := Default_Ignore_Case) return Boolean;

   type Compiled_Pattern is private;
   function Compile (Pattern : String; Ignore_Case : Boolean := Default_Ignore_Case) return Compiled_Pattern;
   function Match   (Name : String; Pattern : Compiled_Pattern) return Boolean;

The first function checks if Name matches Pattern, according to the "glob" style of pattern matching (see Patterns). If the parameter Ignore_Case is True, case is not considered in the matching (i.e. both the name and the pattern are converted to lower case before doing the comparison).

On some implementations, it is possible to pre-compile a pattern to get more efficient pattern matching operations when matching several strings against the same pattern. The Compile function is used to compile a pattern into the compiled form, and the second form of the Match function can be used to match strings against this precompiled pattern.

Note that the second form is always provided, even on implementations that do not provide the precompilation facility. Using the second form on such implementations will work, but there will be no gain in efficiency. In practice, this means that there is no risk for portability to use the precompiled form. As a rule of thumb, use the regular form if you are making a single match, and use the precompiled form if you are matching many strings against the same pattern.

Support for precompiled patterns:
Gnat implementation Effective
Tcl implementation Ineffective

File properties services

These subprograms provide facilities for manipulating file names, extracting fields from full (or partial) names, etc, as well as querying information about files.

   function Join (Path : String; Name : String) return String;

Assembles a path (list of directories) with a file name to make a full name.

   type String_Property  is private;
   function "+" (Left, Right : String_Property) return String_Property;

   Expanded    : constant String_Property;
   As_File     : constant String_Property;
   Device      : constant String_Property;
   Path        : constant String_Property;
   Name        : constant String_Property;
   Extension   : constant String_Property;
   Version     : constant String_Property;

   Native_Form : constant String_Property; -- = Device + Path + Name + Extension + Version

   function Property (Query       : String_Property; 
                      Name        : String; 
                      Default     : String := "";
                      Relative_To : String := "") return String;

This function extracts various parts from a given file name; a value of type String_Property defines which part are to be included in the result. Note that Name needs not be the name of an existing file. If a (requested) part is not present in Name, then the same part (if any) extracted from Default is returned.

The Relative_To parameter indicates how to interpret relative file names; any relative name will be assumed to be starting from this directory. If Relative_To is itself relative, it will be interpreted according to the current directory. The default value ("") means that relative values will be interpreted according to the current directory. This feature is especially useful for multi-tasking programs where tasks operate under different directories, since the notion of current directory is global to the program, not to individual tasks.

Constants are provided that identify the basic elements of a file name; properties can be joined by the "+" operator. The result will include all the parts specified and only those, so for example:

   Property (Device + Name + Extension, "C:\a\b\foo.bar")= "C:foo.bar"

If you retrieve separately the various parts, note that:

It is therefore guaranteed that the various parts can be concatenated with "&" to form a legal name.

Native_Form is a useful constant, equivalent to Device + Path + Name + Extension + Version, i.e. it returns all the fields from the given name. It is however not equivalent to doing nothing, since the returned string always uses the proper separators for the operating system. Its effect is therefore to return the original name with the convention of your operating system. This is quite handy when you want to pass a file name to a system command, but want to allow the user to use other separators... or when you are working with programs like Emacs, that insist on putting Unix separators, even on Dos!

Two values of type String_Property do not designate parts of a file name, but are used to modify the form of the result:

Property (Path, "/a/b/c/") = "/a/b/c/"
Property (Name, "/a/b/c/") = ""
Property (Path + As_File, "/a/b/c/") = "/a/b/"
Property (Name + As_File, "/a/b/c/") = "c"

 

Note that it is possible to give only Expanded or As_File, without specifying any other part; in this case, it is considered to apply to the full input string. This means that Property (Expanded, S) will return the full (absolute) name of the file name contained in S, and that Property (As_File, S) will return the input string, except that if there was a trailing directory separator in it, it is removed.

The remaining Property functions query various aspects of a file, given by its name; the corresponding file must therefore exist. Note that this mechanism is intended to allow future implementations to define more properties, without sacrificing the compatibility with previous implementations.

   type Boolean_Property is (Exists, Is_Directory, Is_Regular_File, Is_Owned, Is_Readable, Is_Writable, Is_Executable);
   function Property (Query : Boolean_Property; Name : String) return Boolean;

   type Time_Property    is (Access_Time, Change_Time);
   function Property (Query : Time_Property; Name : String) return Ada.Calendar.Time;

   type Long_Property    is (Size);
   function Property (Query : Long_Property; Name : String) return Long_Integer;

   type Enum_Property    is (File_Kind, Path_Kind);
   type Enum_Values is
     (File,     Directory, CharacterSpecial, BlockSpecial, Fifo, Link, Socket, -- Values for File_Kind
      Absolute, Relative,  VolumeRelative);                                    -- Values for Path_Kind
   subtype File_Kind_Enums    is Enum_Values range File..Socket;
   subtype Path_Kind_Enums    is Enum_Values range Absolute..VolumeRelative;
   function Property (Query : Enum_Property; Name : String) return Enum_Values;

Note that for properties that return an enumeration, the type Enum_Values gathers all possible values, and subtypes correspond to each possible query; therefore Property (File_Kind, Name) will return a value in the subtype File_Kind_Enums, etc.

The definition of the various properties is given by the following table:

Result subtype

Property

Effect if file exists

Effect if file does not exist
Boolean Exists Returns True if Name identifies an existing file, directory, etc. Returns False
Is_Directory Returns True if Name identifies an existing directory Returns False
Is_Regular_File Returns True if Name identifies an existing regular file Returns False
Is_Owned Returns True if Name identifies an existing file owned by the user. Returns False
Is_Readable Returns True if Name identifies an existing file, and the user has read access to it Returns False
Is_Writable Returns True if Name identifies an existing file, and the user has write access to it Returns False
Is_Executable Returns True if Name identifies an existing file, and the user has execute access to it Returns False
Time Access_Time Time the file was last accessed. Raises Service_error
Change_Time Time the file was last modified Raises Service_error
Long_Integer Size Size of the file, or 0 if the file does not exist. Raises Service_error
File_Kind_Enums File_Kind Returns the kind of the file (regular file, directory, socket, etc). Raises Service_error
Path_Kind_Enums Path_Kind Returns whether the Name is absolute, relative, or volume-relative (i.e., on DOS systems, the path is relative and includes an explicit drive letter). Returns correct result

 

Support for properties:
Gnat implementation Unsupported properties:
Is_Owned, Is_Readable, Is_Writable, Is_Executable, Access_Time

Unsupported values (defined, but never returned):
CharacterSpecial, BlockSpecial, Fifo, Link, Socket

Tcl implementation All properties and values are supported
  type File_Properties (Kind : File_Kind_Enums := File) is
      record
         Name : Ada.Strings.Unbounded.Unbounded_String;
         Path : Ada.Strings.Unbounded.Unbounded_String;
         case Kind is
         when File | Directory =>
            Access_Time : Ada.Calendar.Time;
            Change_Time : Ada.Calendar.Time;
            Size        : Long_Integer;
         when others =>
            null;
         end case;
      end record;
   function Properties (Name : String) return File_Properties;

This function returns a record containing all the properties of the file. The specification of the type File_Properties above is the most general one; an implementation will include in its declaration only the fields corresponding to supported properties.

Supported fields:
Gnat implementation All except Access_Time
Tcl implementation All fields supported

Directory iterator functions

These operations allow to build lists of files corresponding to certain properties, together with operations to get the properties of any file in the list. The first element of the list has Index one. If a variable of type File_List is declared without explicit initialization, it is automatically set to the "empy list" state.

   type File_List is private;
   Empty_File_List : constant File_List;
   Sort_Mapping : Ada.Strings.Maps.Character_Mapping := Ada.Strings.Maps.Constants.Lower_Case_Map;
   function Build_List
      (Pattern     : in  String  := "*";
       Relative_To : in String  := Current_Dir;
       Sort        : in  Boolean := False) return File_List;
   function Length (List : File_List) return Natural;
   function To_String (List : File_List; Separator : Character := ' ') return String;

The Build_List procedure returns a list of all files that match the given Pattern. The Relative_To parameter indicates how to interpret relative file names; see File properties services for details. If Sort is set to False (the default), the files appear in an unspecified order. If Sort is set to True, the files are sorted in dictionnary order of their absolute names, since the list may include files from different directories. (dictionnary order means that casing is not considered when sorting, except to break a tie).

The constant Empty_File_List is a value of type File_List that contains no files.

The function Length returns the number of files in a file list.

The function To_String returns a string representation of the list. File names are in absolute form, and separated with the Separator character. If a file name contains (at least) one Separator, it is put in quotes. Note that the list returned when Separator is the space character (the default) is appropriate for passing to the file operations.

   function "+" (Left, Right : File_List) return File_List;

The "+" function merges two lists. If both lists are sorted, then the result list is sorted and duplicates (files present in both lists) are removed. If at least one of the lists is not sorted, the left list is appended to the right list, and possible duplicates are not removed; the resulting list is considered as not sorted. Note that a list that contains zero or one element is always considered sorted.

   generic
      with function Is_Selected (List : File_List; Index : Positive) return Boolean;
   function Generic_Filter (List : File_List) return File_List;

This generic allows for the definitions of file list "filters", i.e. functions that take a list and return a different list containing only files that correspond to certain criteria. An instantiation of Generic_Filter will call the actual for the function Is_Selected with all values of Index in the list, and will keep in the resulting list only the entries for which the function returned True. Not that the function is passed the full list, and can access other elements in the list; this can be useful if you want to filter a list and keep, for example, only the two most recent versions of the files.

Of course, when you want to write a filter, you would like to pass some parameters to the selection function to determine which files to retain. This can be easily done by following the scheme below, which is an example of a filter that keeps only files above a given size:

   function Filter (List : File_List; Min_Size : Long_Integer) return File_List is
      function Is_Selected (List : File_List; Index : Natural) return Boolean is
      begin
         return Property (Size, List, Index) >= Min_Size;
      end Is_Selected;
      function Local_Filter is new Generic_Filter (Is_Selected);
   begin
      return Local_Filter (List);
   end Filter;

 

   subtype Iter_Enum_Property is Enum_Property range File_Kind..File_Kind;
   function Property (Query : String_Property;    List : File_List; Index : Positive) return String;
   function Property (Query : Boolean_Property;   List : File_List; Index : Positive) return Boolean;
   function Property (Query : Time_Property;      List : File_List; Index : Positive) return Ada.Calendar.Time;
   function Property (Query : Long_Property;      List : File_List; Index : Positive) return Long_Integer;
   function Property (Query : Iter_Enum_Property; List : File_List; Index : Positive) return Enum_Values;

   function Properties (List : File_List; Index : Positive) return File_Properties;

These functions operate exactly like the corresponding operations on file names, but return information about the element in the List at position Index. Constraint_Error is raised if Index is greater than Length(List). The only differences are that, for enumeration properties, only a subset of enumeration properties is provided, since Path_Type would make no sense;, and that the String_Property function does not include a default, since it always operates on the full name of the file.

Note that when called, these operations store locally the corresponding information, so if you query the same information more than once, subsequent calls will be much faster. For this reason, this form should be preferred to calling the corresponding property operation with the name (as a string) of the corresponding file. There is an exception however: if you want to check, for example, how far an FTP transfer has been. In that case, you would like to reevaluate the size of the file each time; use the property operation with the file name, rather than the list entry.

File and directory operations

   procedure Copy_File   (Pattern : String; To : String);
   procedure Move_File   (Pattern : String; To : String);
   procedure Rename_File (File    : String; To : String);
   procedure Delete_File (Pattern : String);

These subprograms provide basic facilities for file manipulations. Names should be self-explanatory :-). Note that the value returned by Current_Dir has the form of a file name, not a directory (i.e. it does not include a trailing directory separator).

Operations whose first parameter is Pattern operate on all files that match Pattern. Copy_File and Delete_File treat directories as normal files, i.e. they are deleted (even if non empty) or fully copied (including -recursively- their content). These operations behave as much as possible as the usual commands of the system, i.e. creation date is kept during a Copy_File, etc.

   procedure Create_Dir (Name : String);
   procedure Change_Dir (To : String);
   function  Current_Dir return String;

These subprograms provide basic facilities for directory manipulations. Names should be self-explanatory :-) :-). Note that the value returned by Current_Dir has the form of a file name, not a directory (i.e. there is no trailing directory separator).

Example

The file purge.adb contains an example program that demonstrates typical usage of OS_Services. It is a program that purges backup files. Usage:

   purge [-r] [-i]

The program removes any "*.bak", "*.backup.*", and "*~" files. if the option -r is given, purge applies recursively to all subdirectories. If the option -i is given, the user is asked for permission before deleting the file.

Questions and answers

Q: How do I simply provide a default extension for a name?
A: Suppose you want to add ".adb" to a name only if there is no extension already. Use:

   Name_Ext : constant String := Property (Native_Name, Raw_Name, ".adb");

Q: With the (string) property As_File, it is easy to turn a directory specification into a file specification. How do I do it the other way round ?
A: Make a Join with an empty string as the file name.

Q: Why is the default value for parameter Relative_To in (string) property "" rather than Current_Directory ?
A: That's what we did first, but we discovered that evaluating Current_Directory can have a terrible performance hit. With this convention, we save this evaluation when not needed.

Q: Why aren't there more "questions and answers" ?
A: Because we're waiting for your questions :-). You are welcome to send them to rosen@adalog.fr.

Implementation notes

[More to be supplied later]

Dealing with file names can be tricky, especially since we wanted to handle blanks in file names properly.

We tried to optimize queries by having a kind of lazy evaluation of file properties. File properties are evaluated, and then stored, only when requested. This may not be noticeable locally, but it has a tremendous effect on speed when you are dealing with files over a network.

A final note...

If you found this package useful...
If you think that it would have taken an awful time to write it yourself...
If it showed you some usages of Ada that you didn't think about...

Maybe you should consider using Adalog's consulting and training services !