Showing posts with label builtin. Show all posts
Showing posts with label builtin. Show all posts

KSH93 Stat Builtin

One of the builtin commands that is missing in ksh93, in my humble opinion, is a builtin similar to stat(1) which would return information about a file.  Here is my initial implementation of a stat builtin.  The output is a compound variable whose subvariables contain the contents of the various fields of the stat(2) structure.  If you are unfamilar with compound variables, see my previous post for an eluridation.
/*
** FPMurphy 2009-01-03
**
** License: Common Public License Version 1.0
**
*/

#pragma prototyped

#include "defs.h"
#include "builtins.h"
#include "path.h"
#include <tm.h>

/* macro to create subvariables */
#define CREATE_CVE(X,Y,Z) \
strcpy(b,(X)); \
np = nv_open(buf, shp->var_tree, NV_NOASSIGN|NV_VARNAME); \
nv_putval(np, (char*)(Y), (Z) ); \
nv_close(np)

static char strperms_buf[30];

static const
char sh_optstat[] =
"[-?\n@(#)$Id: stat 2009-01-03 $\n]"
"[-author?Finnbarr P. Murphy fpm at hotmail.com ]"
"[-license?http://www.opensource.org/licenses/cpl1.0.txt]"
"[+NAME? stat - get file status]"
"[+DESCRIPTION?\bstat\b creates the compound variable \avar\a corresponding"
" to the file given by the pathname \afile\a. The elements of \avar\a"
" are the names of fields in the \astat\a structure with the \bst_\b"
" prefix removed, together with the basename of \afile\a.]"
"\n"
"\nvar file\n"
"\n"
"[+EXIT STATUS?]{"
"[+0?Success.]"
"[+>0?An error occurred.]"
"}"
"[+SEE ALSO?\bstat\b(1),\bstat\b(2)]"
;

/* stringify the permission bits */
static char *
strperms(char * p, mode_t mode)
{
char ftype = '?';

if (S_ISBLK(mode)) ftype = 'b';
if (S_ISCHR(mode)) ftype = 'c';
if (S_ISDIR(mode)) ftype = 'd';
if (S_ISFIFO(mode)) ftype = '|';
if (S_ISLNK(mode)) ftype = 'l';
if (S_ISREG(mode)) ftype = '-';

sfsprintf(p, 30, "\\0%010lo %c%c%c%c%c%c%c%c%c%c %c%c%c\0",
mode, ftype,
mode & S_IRUSR ? 'r' : '-',
mode & S_IWUSR ? 'w' : '-',
mode & S_IXUSR ? 'x' : '-',
mode & S_IRGRP ? 'r' : '-',
mode & S_IWGRP ? 'w' : '-',
mode & S_IXGRP ? 'x' : '-',
mode & S_IROTH ? 'r' : '-',
mode & S_IWOTH ? 'w' : '-',
mode & S_IXOTH ? 'x' : '-',
mode & S_ISUID ? 'U' : '-',
mode & S_ISGID ? 'G' : '-',
mode & S_ISVTX ? 'S' : '-');

return(p);
}


int
b_stat(int argc, char *argv[], void *extra)
{
register Shell_t *shp = ((Shbltin_t*)extra)->shp;
register Namval_t *np;
register int n;
struct stat statb;
char buf[100];
char *b;

while (n = optget(argv, sh_optstat)) switch (n) {
case ':':
errormsg(SH_DICT, 2, "%s", opt_info.arg);
break;
case '?':
errormsg(SH_DICT, ERROR_usage(2), "%s", opt_info.arg);
break;
}

argc -= opt_info.index;
argv += opt_info.index;
if (argc!=2)
errormsg(SH_DICT, ERROR_usage(2), optusage((char*)0));

/* stat the file */
if (stat(argv[1], &statb) < 0)
errormsg(SH_DICT, ERROR_system(1), "%s: stat failed", argv[1]);

strcpy(buf, argv[0]);
b = buf;
while (*b) b++;

/* create compound variable */
np = nv_open(buf, shp->var_tree, NV_NOASSIGN|NV_VARNAME|NV_ARRAY );
if (!nv_isnull(np))
nv_unset(np);
nv_setvtree(np);
nv_close(np);

/* create compound variable elements */
CREATE_CVE(".name", path_basename(argv[1]) , NV_RDONLY);
CREATE_CVE(".atime", &statb.st_atime, NV_RDONLY|NV_INTEGER);
CREATE_CVE(".ctime", &statb.st_ctime, NV_RDONLY|NV_INTEGER);
CREATE_CVE(".mtime", &statb.st_mtime, NV_RDONLY|NV_INTEGER);
CREATE_CVE(".uid", &statb.st_uid, NV_RDONLY|NV_INTEGER);
CREATE_CVE(".gid", &statb.st_gid, NV_RDONLY|NV_INTEGER);
CREATE_CVE(".size", &statb.st_size, NV_RDONLY|NV_INTEGER|NV_LONG);
CREATE_CVE(".dev", &statb.st_dev, NV_RDONLY|NV_INTEGER);
CREATE_CVE(".ino", &statb.st_ino, NV_RDONLY|NV_INTEGER|NV_LONG);
CREATE_CVE(".nlink", &statb.st_nlink, NV_RDONLY|NV_INTEGER);
CREATE_CVE(".mode", strperms(strperms_buf, statb.st_mode), NV_RDONLY);

return(0);
}
This code was tested using ksh93t+.

You can embed a man page into the builtin as done in the above source code.  Most, if not all, of the ksh93 commands have such embedded man pages.

Here is the output from stat --help.
$ stat --help
Usage: stat [ options ] var file
$
and here is the output from stat --man.
$ stat --man
NAME
stat - get file status

SYNOPSIS
stat [ options ] var file

DESCRIPTION
stat creates the compound variable var corresponding to the file given
by the pathname file. The elements of var are the names of fields in the
stat structure with the st_ prefix removed, together with the basename
of file.

EXIT STATUS
0 Success.
>0 An error occurred.

SEE ALSO
stat(1),stat(2)

IMPLEMENTATION
version stat 2009-01-03
author Finnbarr P. Murphy fpm at hotmail.com
license http://www.opensource.org/licenses/cpl1.0.txt
$
Here is an example of using the stat builtin to get information about a file called tksh.
$ stat fileinfo ./tksh
$ print $fileinfo
( atime=1231192796 ctime=1231192291 dev=2065 gid=500 ino=50897
mode='0100755 -rwxr-xr-x ---' mtime=1231192291 name=tksh nlink=1 size=2171604 uid=500 )
$ print ${fileinfo.atime}
1231192796
$ printf "%(%Y-%m-%d %H:%M:%S)T\n" "#${fileinfo.atime}"
2009-01-05 16:59:56
$
As you can see the stat builtin gets the file statistics using stat(2), creates a compound variable with the specified name, i.e. fileinfo, and then creates a series of subvariables (atime, ctime, mtime, etc. ) whose names correspond to the fields of the stat(2) structure.  This compound variable is then available to you to use as necessary within your shell script.  Rather than having to handle a series of variables, one per stat(2) structure field, you simply have to deal with single compound variable, i.e. fileinfo.

As always, email me if you have any questions.

The Mysterious KSH93 Alarm Built-in

Sometimes it is useful to have part of a shell script run periodically, e.g. once a millisecond or every 10 minutes.  Although this is possible by starting a process in the background, ksh93 has an easier (but undocumented) feature which allow a script writer to set up interval timers.  This undocumented feature is a built-in called alarm and a corresponding discipline also called alarm. I am not going to attempt to explain what a discipline or a compund variable is in this post as I assume that you have read the ksh93 man page.  If you list your built-ins using the builtin command, you will see the alarm built-in listed along with the other built-ins.  According to Dave Korn, ksh93 has the ability to handle multiple timeout events using alarm but this feature remains undocumented since "I have not decided what interface I want for this functionality."

If you try to retrieve information about alarm, using the standard ksh93 builtin options (--man or --help) only a single usage line is outputted.
$ alarm --man
Usage: alarm [-r] [varname interval]
$
Dave Korn has stated that -r means repeat the alarm every interval, varname is the name of the variable which is invoked, and interval can be the absolute time from the Epoch or of the form +nseconds.   Milliseconds are also supposedly supported but I have not tested this functionality.

Internally, alarm is an ksh93 built-in which adds an alarm discipline to a function or variable. This built-in sets up an interval timer that will call the alarm discipline as needed.

For our first example, consider the following trivial example which uses alarm to display the time every 2 seconds
alarm -r mytime +2
function mytime.alarm { print -n "$(date +%H:%S)\r"; }
read dummy # dummy wait
The first line specifies that the function mytime is to be called every 2 seconds.  The second line defines the mytime function i.e. print the current time in HH:SS format.  The third line is just a hack so that you can see the results of the previous 2 lines.

Note that ksh93 is very picky as regards how statements are written when discipline functions are used.  For example, if you place the mytime function definition before the alarm statement, ksh93 will produce an error, i.e. "mytime.alarm: invalid discipline function" and exit the script.

Next is an example that demonstrates how to abort a script after a waiting 10 seconds for input from a user.  Note that the ksh93 read command has a timeout option but we are not using it here.  This example also shows you how to remove an alarm using the unset built-in.
alarm -r abortscript +10

abortscript.alarm()
{
print "Exceeded allowed time. Aborting script ......"
kill -9 $$
}

print "waiting for input from read"
read dummy # dummy wait - press return to continue

unset abortscript
print "abortscript unset. Press return to exit"

read dummy # dummy wait - press return to continue

exit 0
Here is the output when this example is run.
$ ./example2
waiting for input from read
Exceeded allowed time. Aborting script ......
Killed
$
$ ./example2
waiting for input from read

abortscript unset. Press return to exit

$
My final example shows how alarm can be used to update a progress bar once a second on a terminal.
alarm -r progressbar +1

progressbar.alarm()
{
tput cup 2 10
(( ++progressbar.pvalue > 50 )) && {
tput cup 2 10; tput el
progressbar.pvalue=0
}
printf "Progress: %s" "${progressbar.pstr:1:${progressbar.pvalue}}"
}

progressbar.pvalue=0
progressbar.pstr="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"

tput clear
read dummy?"Press RETURN to exit"

unset progressbar

exit 0
Again the order of things in the script is very important.  First we specify the function that alarm is going to use, next we define the function itself and then we set the compound variable values.

Use alarm with caution since Dave Korn has also stated that "alarm hasn't been documented because it is not working in all cases."  The examples shown in this post were tested using ksh93 version s.

KSH93 Date Manipulation

While bash is the default shell on most, if not all, Linux distributions, there are times when using ksh93 is more efficient and thus makes more sense. A classic problem in shell scripting is the manipulation of dates and times. Most shells do not include support for date/time string manipulation and the user is left to roll their own routines as needed. Typically this involves parsing date/time strings and using lookup tables and/or using a version of date with support for formatting date/time strings other than current date.

Since 1999, when version h of ksh93 (the 1993 version of the Korn Shell) was released, ksh93 has included such support via the printf builtin function. However examples on using this this feature are scarce and I have written this short article in an attempt to make more shell scripters aware of this extremely useful and powerfull feature in ksh93.

The ksh93 builtin printf (not printf(1)) includes a %T formatting option.
%T                                Treat argument as a date/time string and 
                  format it accordingly.

%(dateformat)T    T can be preceded by dateformat, where                   dateformat is any date format supported
                  date by the date(1) command.
Some examples will illustrate the power of this feature.

Output the current date just like the date(1) command.
$ printf "%T\n" now
Sat Mar 22 10:01:35 EST 2008
Output the current hour, minute and second.
$ printf "%(%H:%M:%S)T\n" now
10:02:07
Note that ksh93 does not fork/exec the date(1) command to process this statement. It is built into ksh93. This results in faster shell script execution and less load on the operating system.

Output the number of seconds since the UNIX Epoch.
$ printf “%(%s)T\n” now
1206199251
If you know the number of seconds since the UNIX Epoch you can output the corresponding date/time in ctime format.
$ printf “%T\n” ‘#’1206199251
Sat Mar 22 10:22:35 EST 2008
The printf builtin also understands date/time strings like “2:00pm yesterday”, “this Wednesday”, “23 days ago”, “next 9:30am”, “in 6 days”, “+ 5 hours 10 minutes” and lots more. Look at the source code for the printf builtin (cmd/ksh93/bltins/print.c) in the ksh93 sources for more information on the various date/time strings which are supported.

Output the date/time corresponding to “2:00pm yesterday.”
$ printf "%T\n" '2:00pm yesterday'
Fri Mar 21 14:00:00 EST 2008
Output the week day corresponding to the last day of February 2008.
$ printf '%(%a)T\n' "final day Feb 2008"
Fri
Output the date corresponding to the third Wednesday in May 2008.
$ printf '%(%D)T\n' "3rd wednesday may 2008"
05/21/08
Output what date it was 4 weeks ago.
$ printf '%(%D)T\n' "4 weeks ago"
02/18/08
You can assign the output of printf “%T” to a variable. Note that “1997-198” represents the 198th day in 1997.
$ datestr=$(printf '%(%D)T' "1997-198")
$ print $datestr
07/17/97
The printf builtin even understands crontab and at date/time syntax as the following two examples demonstrate.

Output the date/time the command associated with this crontab entry will next execute.
$ printf "%T\n" "0 0 1,15 * 1"
Mon Sep 1 00:00:00 EDT 2008
Output the date/time the command associated with this at date/time string will execute.
$ printf "%T\n" "exactly next hour"
Sun Mar 23 14:07:31 EST 2008
The following example shows how to output the date for the first and last days of last month. Care needs to be taken in the order in which the date string is entered as not all combinations are valid.
$ printf "%(%Y-%m-%d)T\n" "1st last month"
2008-05-01
$ printf "%(%Y-%m-%d)T\n" "final last month"
2008-05-31
Microseconds are also understood by %T. Note %N outputs 9 digits by default unless you limit output using a length specifier as in the following example.
$ datestr="2008-11-24 05:17:00.7043"
$ printf "%(%m-%d-%Y %T.%4N)T\n" "$datestr"
11-24-2008 05:17:00.7043
The next example is a short shell script which tackles a common problem associated with backing up files and deleting logs, i.e. calculate the difference between two given dates.
#
# USAGE: diffdate start-date finish-date
#
# EXAMPLE: diffdate "Tue, Feb 19, 2008 08:00:02 PM" \
# "Wed, Feb 20, 2008 02:19:09 AM"
#
# Note – The version limited to a maximum of 100 hours difference

SDATE=$(printf '%(%s)T' "$1")
FDATE=$(printf '%(%s)T' "$2")

[[ $# -ne 2 ]] && {
   print "Usage: diffdate start-date finish-date"
   exit 1
}

DIFF=$(($FDATE-$SDATE))
SECS=$(($DIFF % 60))
MINS=$(($DIFF % (60 * 60) / 60))
HOURS=$(($DIFF / (60 * 60)))

printf "%02d:%02d:%02d\n" $HOURS $MINS $SECS
My final example shows how to output a range of dates in a specific format incremented by 1 hour each time.
startdate="2008-05-26 01:00:00"
count=71

for ((i=0; i < count; i++))
do
   printf "%(%m%d%Y%H0000)T\n" "${startdate} + $i hour"
done
Well, that is about all there is to the printf %T feature in ksh93. I hope that you have found this short article on date/time manipulation using the printf %T feature to be useful and informative and that you will start using it in your future Korn Shell scripts.