Access and manipulate various forms of time to automatically remove files from the IFS or save them.
In last month's article, "When Was an IFS File Last Used or Changed?", we saw how a CL program could determine the date and time an IFS file was last opened or modified. Today, we'll take that one step further: finding out which files have not been opened in the last X number of days, where X is a user-specified value. This is the type of information that may help you decide what files are ready to be archived and/or deleted on your system. An example of how to automate the removal of such files is provided at the end of this article.
We'll start by creating the command Use Dir Program (USEDIRPGM), which will prompt the user for the directory to analyze and the number of days to use for the analysis. In past articles, we've used the message file USERMSGF to hold messages related to past programs and commands. If you do not currently have this message file on your system, create it now with this command:
CRTMSGF MSGF(USERMSGF)
With the USERMSGF message file in place, add three messages using the following commands:
ADDMSGD MSGID(DIR0001) MSGF(USERMSGF) MSG('Use Dir Program')
ADDMSGD MSGID(DIR0002) MSGF(USERMSGF) MSG('Directory')
ADDMSGD MSGID(DIR0003) MSGF(USERMSGF) MSG('Days')
This is the source for the USERMSGF command:
CMD PROMPT(DIR0001)
PARM KWD(DIR) TYPE(*PNAME) LEN(1024) DFT(*CURDIR) +
SPCVAL((*CURDIR '.')) PROMPT(DIR0002)
PARM KWD(DAYS) TYPE(*UINT4) DFT(3) PROMPT(DIR0003)
The command defines two parameters, both of which are optional. The first parameter, DIR, is the name of the directory to be analyzed. The default is your current working directory. You can specify a directory path up to 1024 bytes in length when not using the default directory. The second parameter, DAYS, permits you to specify the number of days, from the current date, that an object within the directory must not have been opened in order to be reported to the user. The default is three (3) days with the minimum supported value being zero (0) days. You can create the USEDIRPGM command from the above source (assuming the source is in member USEDIRPGM of QCMDSRC) with the command
CRTCMD CMD(USEDIRPGM) PGM(DIR3) PMTFILE(USERMSGF)
The command processing program (CPP) for the USEDIRPGM command is program DIR3. The source for the DIR3 program follows, with the changes from last month's DIR2 program shown in bold. In addition to the highlighted changed and added lines, the monitor for MCH3601 used last month has been removed.
Pgm Parm(&Dir_In &Days)
Dcl Var(&Dir_In) Type(*Char) Len(1024)
Dcl Var(&Days) Type(*UInt)
Dcl Var(&InlPath) Type(*Char) Len(1025)
Dcl Var(&Dir_Ptr) Type(*Ptr)
Dcl Var(&DirEnt_Ptr) Type(*Ptr)
Dcl Var(&DirEnt) Type(*Char) Len(696) +
Stg(*Based) BasPtr(&DirEnt_Ptr)
Dcl Var(&LenOfName) Type(*UInt) Stg(*Defined) +
DefVar(&DirEnt 53)
Dcl Var(&Name) Type(*Char) Len(640) +
Stg(*Defined) DefVar(&DirEnt 57)
Dcl Var(&FileInfo) Type(*Char) Len(128)
Dcl Var(&LstAccess) Type(*Int) +
Stg(*Defined) DefVar(&FileInfo 25) /* Last open */
Dcl Var(&LstDtaChg) Type(*Int) +
Stg(*Defined) DefVar(&FileInfo 29) /* Last changed*/
Dcl Var(&ObjTyp) Type(*Char) Len(10) +
Stg(*Defined) DefVar(&FileInfo 49) /* Object type */
Dcl Var(&DatTim_Ptr) Type(*Ptr)
Dcl Var(&DatTim) Type(*Char) Len(24) +
Stg(*Based) BasPtr(&DatTim_Ptr)
Dcl Var(&SecsInDay) Type(*Int) Value(86400)
Dcl Var(&TimeFilter) Type(*Int)
Dcl Var(&Path) Type(*Char) Len(10000)
Dcl Var(&MsgTxt) Type(*Char) Len(300)
Dcl Var(&Status) Type(*Int)
Dcl Var(&Null) Type(*Char) Len(1) +
Value(x'00')
Dcl Var(&Null_Ptr) Type(*Ptr)
ChgVar Var(&InlPath) Value(&Dir_In)
ChgVar Var(&Path) Value(&InlPath *TCat &Null)
CallPrc Prc('opendir') Parm(&Path) RtnVal(&Dir_Ptr)
If Cond(&Dir_Ptr = &Null_Ptr) Then(Do)
SndPgmMsg Msg('Directory not found') +
ToPgmQ(*Ext)
Return
EndDo
CallPrc Prc('time') Parm((*Omit)) RtnVal(&TimeFilter)
ChgVar Var(&TimeFilter) +
Value(&TimeFilter - (&Days * &SecsInDay))
CallPrc Prc('readdir') +
Parm((&Dir_Ptr *ByVal)) RtnVal(&DirEnt_Ptr)
DoWhile Cond(&DirEnt_Ptr *NE &Null_Ptr)
If Cond(%sst(&Name 1 1) *NE '.') Then(Do)
ChgVar Var(&Path) Value(&InlPath *TCat '/' *TCat +
%sst(&Name 1 &LenOfName) *TCat &Null)
CallPrc Prc('stat') +
Parm(&Path &FileInfo) RtnVal(&Status)
If Cond(&Status = 0) Then(Do)
If Cond(&LstAccess < &TimeFilter) Then(Do)
ChgVar Var(&MsgTxt) +
Value(&ObjTyp *Cat %sst(&Name 1 &LenOfName))
SndPgmMsg Msg(&MsgTxt) ToPgmQ(*Ext)
CallPrc Prc('ctime') +
Parm(&LstDtaChg) RtnVal(&DatTim_Ptr)
ChgVar Var(&MsgTxt) +
Value('Last change: ' *Cat &DatTim)
SndPgmMsg Msg(&MsgTxt) ToPgmQ(*Ext)
CallPrc Prc('ctime') +
Parm(&LstAccess) RtnVal(&DatTim_Ptr)
ChgVar Var(&MsgTxt) +
Value('Last access: ' *Cat &DatTim)
SndPgmMsg Msg(&MsgTxt) ToPgmQ(*Ext)
EndDo
EndDo
Else Cmd(Do)
ChgVar Var(&MsgTxt) Value('** ERROR **' +
*Cat %sst(&Name 1 &LenOfName))
SndPgmMsg Msg(&MsgTxt) ToPgmQ(*Ext)
EndDo
EndDo
CallPrc Prc('readdir') +
Parm((&Dir_Ptr *ByVal)) RtnVal(&DirEnt_Ptr)
EndDo
CallPrc Prc('closedir') Parm((&Dir_Ptr *ByVal))
EndPgm
As you can see, there are not very many changes needed in order to start aging the files within the directory. Two of the changes—extending the length of both &Dir_In and &InlPath—are not even necessary for the purposes of this sample program. But as we will not be calling the DIR3 program directly from the command line, we no longer have to worry about the default of 32 bytes for character string lengths. These two length changes were made in an attempt to make the command more generally useful in terms of handling directory naming.
As with the previous DIR2 program, you can create the DIR3 program with either one or two steps, depending on the release level of the i operating system you are running on. If you are on V6R1 or later, you can use this single command:
CRTBNDCL PGM(DIR3)
If you are on V5R4, you will need to use the following two-step process (as the QC2LE binding directory is not implicitly used by CRTBNDCL if the system release level is prior to V6R1):
CRTCLMOD MODULE(DIR3)
CRTPGM PGM(DIR3) BNDDIR(QC2LE)
Before looking at the DIR3 program, let's quickly review the previous DIR2 program. DIR2 displayed messages listing all user files, within a specified directory, along with the date and time each file was last modified and last accessed. The dates and times displayed were formatted similar to Mon Apr 11 03:15:41 2011. With DIR3, we want the program to display this file information only if the amount of time since the last file open is greater than the number of days specified by the DAYS parameter.
One approach to filter the files to be shown would be to retrieve the current system date and time using a command such as RTVSYSVAL QDATETIME(&DATETIME), subtract three days, and then compare this result with the dates and times found for each file. This solution, however, is complicated by needing to keep in mind the number of days per month, leap days, etc. when subtracting three days. You might recall, though, that the ctime API was used to format dates in DIR2 only because the stat API, which actually retrieves the IFS file dates and times, returns time values as a number of seconds since Jan 1 1970. This format, a simple duration measured in seconds since 1970, is not too meaningful when shown to an end user but is ideal for the type of filtering we want to do in the DIR3 program. Keeping this background material in mind, let's look at the DIR3 program.
In addition to adding the DAYS parameter to the PGM statement and declaring the attributes of the &DAYS variable, two new variables are also declared in the program. The first, &SecsInDay, is defined as an integer with an initial value of 86400. 86400 is the number of seconds in a day (ignoring leap seconds and Daylight Saving Time considerations) and is determined by multiplying 60 seconds in a minute by 60 minutes in an hour by 24 hours in a day. The second variable, &TimeFilter, is defined as an integer and will be used to hold the result of subtracting (&Days * &SecsInDay) from the current day.
After verifying with the opendir API that the user-specified directory exists, DIR3 calls the Determine Current Time (time) API. The time API, documented here, returns the current time as an integer value representing the number of seconds from Jan 1 1970 to "now." This time value is stored in the variable &TimeFilter. The program now subtracts the product of &DAYS multiplied by &SecsInDay from &TimeFilter, storing the result in &TimeFilter. If &DAYS were specified as 3 when running the USEDIRPGM command, &TimeFilter would now represent the time and date three days prior to the current date and time.
All that's left now is to check to see if the last access time is less than the value of &TimeFilter. This is done with the addition of this line of code:
If Cond(&LstAccess < &TimeFilter) Then(Do)
With these few changes, the program now provides a list of those files that have not been opened within the last X days or, to be a bit more accurate, those files that have not been opened within the last X 24-hour periods. What would it take, though, to support days rather than 24-hour periods? If today was a Friday, many people might interpret a three-day period as indicating files not opened since Monday (with the three-day period, meaning not opened on the previous Tuesday, Wednesday, or Thursday).
The localtime and mktime APIs
There are several ways to accomplish this, with the solution shown in this article being based on two additional APIs. The first API we'll use is named localtime and is documented here. The localtime API has one input—a 4-byte integer value representing the number of seconds since January 1 1970—and one output—a pointer to a structure where the input time value has been converted to a structure containing, among other things, the date and time in hours, minutes, seconds, and so on. The second API we'll be using is named mktime and is documented here. The mktime API has one input—a structure such as that returned by the localtime API—and one output—a 4-byte integer value where the input time value has been converted to the number of seconds since January 1 1970. We will use these APIs to…
- reformat the current date and time value that is returned by the time API
- set the returned structure to the start of the current day by changing the returned hour, minute, and second values to 0 while leaving the day portion the same
- reformat this modified structure back to a 4-byte integer value representing the start of the current day
- use this start of current day integer value, rather than the current date and time value, to set the &TimeFilter variable value
Here is the updated DIR3 program with the changes from the earlier version shown in bold.
Pgm Parm(&Dir_In &Days)
Dcl Var(&Dir_In) Type(*Char) Len(1024)
Dcl VAR(&Days) Type(*UInt)
Dcl Var(&InlPath) Type(*Char) Len(1025)
Dcl Var(&Dir_Ptr) Type(*Ptr)
Dcl Var(&DirEnt_Ptr) Type(*Ptr)
Dcl Var(&DirEnt) Type(*Char) Len(696) +
Stg(*Based) BasPtr(&DirEnt_Ptr)
Dcl Var(&LenOfName) Type(*UInt) Stg(*Defined) +
DefVar(&DirEnt 53)
Dcl Var(&Name) Type(*Char) Len(640) +
Stg(*Defined) DefVar(&DirEnt 57)
Dcl Var(&FileInfo) Type(*Char) Len(128)
Dcl Var(&LstAccess) Type(*Int) +
Stg(*Defined) DefVar(&FileInfo 25) /* Last open */
Dcl Var(&LstDtaChg) Type(*Int) +
Stg(*Defined) DefVar(&FileInfo 29) /* Last changed*/
Dcl Var(&ObjTyp) Type(*Char) Len(10) +
Stg(*Defined) DefVar(&FileInfo 49) /* Object type */
Dcl Var(&DatTim_Ptr) Type(*Ptr)
Dcl Var(&DatTim) Type(*Char) Len(24) +
Stg(*Based) BasPtr(&DatTim_Ptr)
Dcl Var(&SecsInDay) Type(*Int) Value(86400)
Dcl Var(&TimeFilter) Type(*Int)
Dcl Var(&Time_Ptr) Type(*Ptr)
Dcl Var(&Time) Type(*Char) Len(36) +
Stg(*Based) BasPtr(&Time_Ptr)
Dcl Var(&SecsAftMin) Type(*Int) +
Stg(*Defined) DefVar(&Time 1)
Dcl Var(&MinsAftHr) Type(*Int) +
Stg(*Defined) DefVar(&Time 5)
Dcl Var(&HrsAftMid) Type(*Int) +
Stg(*Defined) DefVar(&Time 9)
Dcl Var(&DayOfMth) Type(*Int) +
Stg(*Defined) DefVar(&Time 13)
Dcl Var(&MthsAftJan) Type(*Int) +
Stg(*Defined) DefVar(&Time 17)
Dcl Var(&YrsAft1900) Type(*Int) +
Stg(*Defined) DefVar(&Time 21)
Dcl Var(&DaysAftSun) Type(*Int) +
Stg(*Defined) DefVar(&Time 25)
Dcl Var(&DaysAftJn1) Type(*Int) +
Stg(*Defined) DefVar(&Time 29)
Dcl Var(&DST_Flag) Type(*Int) +
Stg(*Defined) DefVar(&Time 33)
Dcl Var(&Path) Type(*Char) Len(10000)
Dcl Var(&MsgTxt) Type(*Char) Len(300)
Dcl Var(&Status) Type(*Int)
Dcl Var(&Null) Type(*Char) Len(1) +
Value(x'00')
Dcl Var(&Null_Ptr) Type(*Ptr)
ChgVar Var(&InlPath) Value(&Dir_In)
ChgVar Var(&Path) Value(&InlPath *TCat &Null)
CallPrc Prc('opendir') Parm(&Path) RtnVal(&Dir_Ptr)
If Cond(&Dir_Ptr = &Null_Ptr) Then(Do)
SndPgmMsg Msg('Directory not found') +
ToPgmQ(*Ext)
Return
EndDo
CallPrc Prc('time') Parm((*Omit)) RtnVal(&TimeFilter)
CallPrc Prc('localtime') Parm(&TimeFilter) +
RtnVal(&Time_Ptr)
ChgVar Var(&SecsAftMin) Value(0)
ChgVar Var(&MinsAftHr) Value(0)
ChgVar Var(&HrsAftMid) Value(0)
CallPrc Prc('mktime') Parm(&Time) RtnVal(&TimeFilter)
ChgVar Var(&TimeFilter) +
Value(&TimeFilter - (&Days * &SecsInDay))
CallPrc Prc('readdir') +
Parm((&Dir_Ptr *ByVal)) RtnVal(&DirEnt_Ptr)
DoWhile Cond(&DirEnt_Ptr *NE &Null_Ptr)
If Cond(%sst(&Name 1 1) *NE '.') Then(Do)
ChgVar Var(&Path) Value(&InlPath *TCat '/' *TCat +
%sst(&Name 1 &LenOfName) *TCat &Null)
CallPrc Prc('stat') +
Parm(&Path &FileInfo) RtnVal(&Status)
If Cond(&Status = 0) Then(Do)
If Cond(&LstAccess < &TimeFilter) Then(Do)
ChgVar Var(&MsgTxt) +
Value(&ObjTyp *Cat %sst(&Name 1 &LenOfName))
SndPgmMsg Msg(&MsgTxt) ToPgmQ(*Ext)
CallPrc Prc('ctime') +
Parm(&LstDtaChg) RtnVal(&DatTim_Ptr)
ChgVar Var(&MsgTxt) +
Value('Last change: ' *Cat &DatTim)
SndPgmMsg Msg(&MsgTxt) ToPgmQ(*Ext)
CallPrc Prc('ctime') +
Parm(&LstAccess) RtnVal(&DatTim_Ptr)
ChgVar Var(&MsgTxt) +
Value('Last access: ' *Cat &DatTim)
SndPgmMsg Msg(&MsgTxt) ToPgmQ(*Ext)
EndDo
EndDo
Else Cmd(Do)
ChgVar Var(&MsgTxt) Value('** ERROR **' +
*Cat %sst(&Name 1 &LenOfName))
SndPgmMsg Msg(&MsgTxt) ToPgmQ(*Ext)
EndDo
EndDo
CallPrc Prc('readdir') +
Parm((&Dir_Ptr *ByVal)) RtnVal(&DirEnt_Ptr)
EndDo
CallPrc Prc('closedir') Parm((&Dir_Ptr *ByVal))
EndPgm
The changes are, as you can see, quite minimal. The APIs introduced (time, ctime, localtime, and mktime) can provide for very flexible date and time support for CL applications. As you have seen, adding and subtracting time durations such as days or hours can be done in a very straightforward manner—without the developer needing to worry about how many days are in a month, how many days are in a year, etc. In the current example program, we've used these APIs to assist us in aging IFS files. But I suspect you have other applications where flexible date manipulation, such as we've seen here, would help simplify the CL program.
A few notes though about the structure returned by the localtime API. The hour, minute, and second fields represent the number of hours, minutes, and seconds of the specified time. So a value of 0 for seconds means it is the start of the minute returned, a value of 0 for minutes means it is the start of the hour returned, and a value of 0 for hour means it is the start of day for the day returned. Seconds, therefore, are typically in the range of 0 to 59, minutes 0 to 59, and hours 0 to 23. As a piece of trivia, seconds can actually be in the range of 0 to 61, allowing for up to two leap seconds. The day of month is the usual 1 through 31, but month reverts to being the month of the year, so month can be in the range of 0 to 11. The year field is the calendar year less 1900, so the year 1901 is represented by the value 1.
Automate Removal or Save of IFS Files
You might be wondering how close we are to being able to automate the removal and/or saving of IFS files based on the number of days since the file was last used. Essentially, we're already there. The most recent version of the DIR3 program is only displaying the files that have not been opened in X days. To remove those files, you could add the following two statements after (or replacing) the SNDPGMMSG commands that are displaying the file attributes.
ChgVar Var(&Path) Value(&InlPath *TCat +
'/' *TCat +
%sst(&Name 1 &LenOfName))
RmvLnk ObjLnk(&Path)
The CHGVAR command is basically removing the trailing null byte (&Null) from the &Path variable that was previously formatted for use by the stat API. The stat API uses a trailing null byte to determine the length of the path parameter. The Remove Link (RMVLNK) command, like most CL commands, expects the path parameter to be ended by a blank, not a null byte. After reformatting the &Path variable without the null byte, the RMVLNK command then removes the specified file.
If you are going to implement additional processing such as the RMVLNK command show above, you may want to take a few items into consideration. These additional items might include the following:
- Changing the USEDIRPGM command to require the DIR parameter as opposed to allowing the parameter to default to *CURDIR. You may not want a user entering the command name USEDIRPGM and pressing Enter, rather than F4 to prompt, to run the RMVLNK commands when the user's current directory is not set correctly.
- Checking the &ObjTyp value for a value such as '*STMF' prior to running the RMVLNK command (as RMVLNK does not support object types such as *DIR).
- Using a MONMSG following the RMVLNK in the event you don't have the proper authority to remove the file.
More CL Questions?
Wondering how to accomplish a function in CL? Send your CL-related questions to me at
as/400, os/400, iseries, system i, i5/os, ibm i, power systems, 6.1, 7.1, V7, V6R1
LATEST COMMENTS
MC Press Online