//-----------------------------------------------------------------------------
// Copyright © 2006 - Philip Howard - All rights reserved
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program 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.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
//-----------------------------------------------------------------------------
// author	Phil Howard
// email	echo cGhpbC1saWJoLXJsc0BpcGFsLm5ldAo= | mimencode -u
//-----------------------------------------------------------------------------
// program	rls
//
// purpose	Scan a file tree, similar to the common "find" command.
//		The options are different and simplified for more casual use
//		despite there still being too many options to choose from.
//
// usage	Shell command
//
// syntax	rls  [options]	[names]
//
//		-a      append '/' to directory names
//		-b	show blocks allocated
//		-d -D   show date (-d for local, -D for UTC)
//		-g -G   show group name (-G for group number)
//		-h -H   show this help and quit
//		-i      show inode
//		-l      show long format
//		-L      show number of links
//		-m      show mode
//		-n      no recursion (maxdepth=0)
//		-N      append nanoseconds to time (if available)
//		-p      show where link points to
//		-q      output nothing
//		-s      show size
//		-S      show time/date as raw seconds
//		-t -T   show time and date (-t for local, -T for UTC)
//		-u -U   show user name (-U for number)
//		-V      show version number and quit
//		+a      select directories (ascending)
//		+b      select block devices
//		+c      select character devices
//		+d      select directories (descending)
//		+h +H   show this help and quit
//		+f      select regular files
//		+l      select symlinks
//		+p      select pipes
//		+s      select sockets
//		+V      show version number and quit
//
//		cd=<directory>  change to this directory (multiple)
//		maxdepth=<num>  maximum recursion depth
//
// note		Assignment options must be entirely in their own one command
//		line argument.
//
// note		Single letter options may be combined in a single command line
//		token.  A "+" or "-" is required at the beginning to indicate
//		these are option letters.  A "+" or "-" may also be embedded
//		inside the command line token.  The "+" or "-" defines which
//		meaning that the following letters have.
//
// note		A "++" or "--" by itself as a command line token ends parsing
//		of all options and everythere on the command line after that
//		is taken to be file names.
//
// requires	LIBH (avl, ftr, io, map, string)
//
// compile	gcc -O3 -o rls rls.c -lh
//-----------------------------------------------------------------------------

#define	VERSION_STRING	"0.8.5"

#define _GNU_SOURCE

#include <linux/unistd.h>

#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <grp.h>
#include <limits.h>
#include <pwd.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/select.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>

#include <libh/ftr.h>
#include <libh/io.h>
#include <libh/map.h>
#include <libh/string.h>


//-----------------------------------------------------------------------------
// function	show_version
//
// purpose	Show version info.
//
// arguments	-none-
//
// returns	(int) 1
//-----------------------------------------------------------------------------
static
int
show_version ()
{
    fputs( "rls version " VERSION_STRING "\n", stderr );
    return 1;
}

//-----------------------------------------------------------------------------
// function	show_help
//
// purpose	Show help info.
//
// arguments	-none-
//
// returns	(int) 1
//-----------------------------------------------------------------------------
static
int
show_help ()
{
    fputc( '\n', stderr );
    show_version();
    {
	fputs( "\n"
	       "syntax:\n"
	       "        rls  [options]  [names]\n"
	       "\n"
	       "single letter options may be combined in a single command line token:\n"
	       "        -a      append '/' to directory names\n"
	       "        -b      show blocks allocated\n"
	       "        -d -D   show date (-d for local, -D for UTC)\n"
	       "        -g -G   show group name (-G for group number)\n"
	       "        -h -H   show this help and quit\n"
	       "        -i      show inode\n"
	       "        -l      show long format\n"
	       "        -L      show number of links\n"
	       "        -m      show mode\n"
	       "        -n      no recursion (maxdepth=0)\n"
	       "        -N      append nanoseconds to time (if available)\n"
	       "        -p      show where link points to\n"
	       "        -q      output nothing\n"
	       "        -s      show size\n"
	       "        -S      show time/date as raw seconds\n"
	       "        -t -T   show time and date (-t for local, -T for UTC)\n"
	       "        -u -U   show user name (-U for number)\n"
	       "        -V      show version number and quit\n"
	       "        +a      select directories (ascending)\n"
	       "        +b      select block devices\n"
	       "        +c      select character devices\n"
	       "        +d      select directories (descending)\n"
	       "        +h +H   show this help and quit\n"
	       "        +f      select regular files\n"
	       "        +l      select symlinks\n"
	       "        +p      select pipes\n"
	       "        +s      select sockets\n"
	       "        +V      show version number and quit\n"
	       "\n"
	       "assignment options must be in separate command line tokens:\n"
	       "        cd=<directory>  change to this directory (multiple)\n"
	       "        maxdepth=<num>  maximum recursion depth\n"
	       "\n",
	       stderr
	    );
    }
    return 1;
}

//-----------------------------------------------------------------------------
// function	main
//-----------------------------------------------------------------------------
int
main (
    int		argc
    ,
    char * *	argv
    )
{
    FTR			this_ftr		;

    MAP			map_uname		;
    MAP			map_gname		;

    struct group *	group_p			;
    struct passwd *	passwd_p		;
    struct tm *		tm_p			;

    char *		program_name		;
    char *		equ_p			;
    char *		str_p			;

    size_t		len			;

    int			maxdepth		;

    int			argi			;
    int			block_dot		;
    int			error_count		;
    int			ftype			;
    int			id			;
    int			mode_c			;
    int			select			;
    int			save_errno		;

    int			opt_append		;
    int			opt_blocks		;
    int			opt_date		;
    int			opt_gid			;
    int			opt_gname		;
    int			opt_help		;
    int			opt_inode		;
    int			opt_links		;
    int			opt_long		;
    int			opt_mode		;
    int			opt_nano		;
    int			opt_norec		;
    int			opt_ptr			;
    int			opt_quiet		;
    int			opt_size		;
    int			opt_secs		;
    int			opt_time		;
    int			opt_uid			;
    int			opt_uname		;
    int			opt_utc			;
    int			opt_version		;

    int			sel_adir		;
    int			sel_block		;
    int			sel_char		;
    int			sel_dir			;
    int			sel_file		;
    int			sel_link		;
    int			sel_pipe		;
    int			sel_sock		;

    char		temp_str		[PATH_MAX];


    //-----------------------
    // Initialize everything.
    //-----------------------
    error_count = 0;
    block_dot	= 0;

    opt_append	= 0;
    opt_blocks	= 0;
    opt_date	= 0;
    opt_gid	= 0;
    opt_gname	= 0;
    opt_help	= 0;
    opt_inode	= 0;
    opt_links	= 0;
    opt_long	= 0;
    opt_mode	= 0;
    opt_nano	= 0;
    opt_norec	= 0;
    opt_ptr	= 0;
    opt_quiet	= 0;
    opt_size	= 0;
    opt_secs	= 0;
    opt_time	= 0;
    opt_uid	= 0;
    opt_uname	= 0;
    opt_utc	= 0;
    opt_version = 0;

    sel_adir	= 0;
    sel_block	= 0;
    sel_char	= 0;
    sel_dir	= 0;
    sel_file	= 0;
    sel_link	= 0;
    sel_pipe	= 0;
    sel_sock	= 0;

    map_uname	= NULL;
    map_gname	= NULL;

    maxdepth	= -1;

    //-----------------------
    // Identify this program.
    //-----------------------
    program_name = strdup( str_tail_one( argv[0], '/' ) );

    //--------------
    // Scan options.
    //--------------
    for ( argi = 1; argi < argc; ++ argi ) {
	str_p = argv[argi];

	if ( str_p[0] == '-' && str_p[1] == '-' && str_p[2] == 0 ) break;
	if ( str_p[0] == '+' && str_p[1] == '+' && str_p[2] == 0 ) break;

	equ_p = strchr( str_p, '=' );
	if ( equ_p ) {
	    switch ( equ_p - str_p ) {

	    case 2:
		if ( str_p[0] == 'c' && str_p[1] == 'd' ) {
		    if ( * ++ equ_p ) {
			if ( chdir( equ_p ) < 0 ) {
			    fprintf( stderr, "%s: error %u changing directory to \"%s\": %s\n",
				     program_name, errno, equ_p, strerror( errno ) );
			    return 1;
			}
		    }
		}
		break;

	    case 8:
		if ( 0 == memcmp( str_p, "maxdepth", 8 ) ) {
		    if ( * ++ equ_p ) {
			maxdepth = strtoul( equ_p, NULL, 0 );
		    } else {
			maxdepth = 1;
		    }
		}
		break;

	    default:	++error_count;
		fprintf( stderr, "%s: unknown option: %c%c\n",
			 program_name, '-', * str_p );
	    }
	    continue;
	}

	mode_c = str_p[0];
	if ( mode_c == '-' || mode_c == '+' ) {
	    if ( str_p[1] == mode_c && str_p[2] == '0' ) break;

	    while ( * ++ str_p ) {
		if ( mode_c == '-' ) {
		    switch ( * str_p ) {
		    case 'a':	++opt_append;		break;
		    case 'b':	++opt_blocks;		break;
		    case 'd':	++opt_date;		break;
		    case 'D':	++opt_date; ++opt_utc;	break;
		    case 'g':	++opt_gname;		break;
		    case 'G':	++opt_gid;		break;
		    case 'h':	++opt_help;		break;
		    case 'H':	++opt_help;		break;
		    case 'i':	++opt_inode;		break;
		    case 'l':	++opt_long;		break;
		    case 'L':	++opt_links;		break;
		    case 'm':	++opt_mode;		break;
		    case 'n':	++opt_norec;		break;
		    case 'N':	++opt_nano;		break;
		    case 'p':	++opt_ptr;		break;
		    case 'q':	++opt_quiet;		break;
		    case 's':	++opt_size;		break;
		    case 'S':	++opt_secs;		break;
		    case 't':	++opt_time;		break;
		    case 'T':	++opt_time; ++opt_utc;	break;
		    case 'u':	++opt_uname;		break;
		    case 'U':	++opt_uid;		break;
		    case 'V':	++opt_version;		break;
		    case '+':	mode_c = '+';		break;
		    case '-':				break;
		    default:	++error_count;
			fprintf( stderr, "%s: unknown option: %c%c\n",
				 program_name, '-', * str_p );
		    }
		}
		else if ( mode_c == '+' ) {
		    switch ( * str_p ) {
		    case 'a':	++sel_adir;		break;
		    case 'b':	++sel_block;		break;
		    case 'c':	++sel_char;		break;
		    case 'd':	++sel_dir;		break;
		    case 'f':	++sel_file;		break;
		    case 'h':	++opt_help;		break;
		    case 'H':	++opt_help;		break;
		    case 'l':	++sel_link;		break;
		    case 'p':	++sel_pipe;		break;
		    case 's':	++sel_sock;		break;
		    case 'V':	++opt_version;		break;
		    case '-':	mode_c = '-';		break;
		    case '+':				break;
		    default:	++error_count;
			fprintf( stderr, "%s: unknown option: %c%c\n",
				 program_name, '+', * str_p );
		    }
		}
	    }

	}
	else break;
    }

    //-----------------------------------
    // Check for help or version request.
    //-----------------------------------
    if ( opt_help )	return show_help();
    if ( opt_version )	return show_version();

    //-----------------------------------------------------
    // If no file types specified then show all file types.
    //-----------------------------------------------------
    if ( sel_adir + sel_dir + sel_file + sel_link + sel_block + sel_char + sel_pipe + sel_sock == 0 ) {
	sel_dir = sel_file = sel_link = sel_block = sel_char = sel_pipe = sel_sock = 1;
    }

    //-----------------------------------------------------
    // If long display is specified, turn on other options.
    //-----------------------------------------------------
    if ( opt_long ) {
	opt_mode	= 1;
	opt_links	= 1;
	if ( ! opt_uid ) opt_uname = 1;
	if ( ! opt_gid ) opt_gname = 1;
	opt_size	= 1;
	if ( ! opt_date ) opt_time = 1;
	opt_date	= 1;
	opt_ptr		= 1;
    }

    //------------------------------------------------
    // But if quiet is specified, suppress all output.
    //------------------------------------------------
    if ( opt_quiet ) {
	sel_adir = sel_dir = sel_file = sel_link = sel_block = sel_char = sel_pipe = sel_sock = 0;
    }

    //-------------------------------------------------------------
    // Create a file tree recursion object for the given arguments.
    //-------------------------------------------------------------
    if ( argi < argc ) {
	this_ftr = ftr_new_ca( argc - argi, argv + argi );
	if ( ! this_ftr ) {
	    save_errno = errno;
	    for ( ; argi < argc; ++ argi ) {
		this_ftr = ftr_new( argv[argi] );
		if ( ! this_ftr ) {
		    fprintf( stderr, "%s: error %u accessing \"%s\": %s\n",
			     program_name, errno, argv[argi], strerror( errno ) );
		    ++ error_count;
		}
		ftr_end( this_ftr );
	    }
	    if ( error_count == 0 ) {
		fprintf( stderr, "%s: error %u from ftr_new_ca(): %s\n",
			 program_name, errno, strerror( errno ) );
		++ error_count;
	    }
	}
    } else {
	this_ftr = ftr_new( NULL );
	block_dot = 1;
    }

    //----------------------------------------
    // Create name caching mappings if needed.
    //----------------------------------------
    if ( opt_uname ) {
	map_uname = map_ui_new();
	if ( ! map_uname ) {
	    fprintf( stderr, "%s: error creating %s name map: %s\n",
		     program_name, "user", strerror( errno ) );
	    return 1;
	}
    }
    if ( opt_gname ) {
	map_gname = map_ui_new();
	if ( ! map_gname ) {
	    fprintf( stderr, "%s: error creating %s name map: %s\n",
		     program_name, "group", strerror( errno ) );
	    return 1;
	}
    }

    //------------------------
    // Stop now if any errors.
    //------------------------
    if ( error_count ) return 1;

    //----------------------------------
    // If no recursion, set depth limit.
    //----------------------------------
    if ( opt_norec ) {
	ftr_set_depth_limit( this_ftr, 0 );
    }

    //-----------------------------
    // Set the maximum depth limit.
    //-----------------------------
    if ( maxdepth >= 0 ) {
	ftr_set_depth_limit( this_ftr, maxdepth );
    }

    //------------------------------------------
    // Loop through all the file system objects.
    //------------------------------------------
    for (;;) {
	ftype = ftr_get( this_ftr );
	if ( ftype == 0 ) break;
	if ( ftype == 'm' ) ftype = 'd';

	select =
	    ftype == 'f' ? sel_file :
	    ftype == 'd' ? sel_dir :
	    ftype == 'l' ? sel_link :
	    ftype == 'b' ? sel_block :
	    ftype == 'c' ? sel_char :
	    ftype == 's' ? sel_sock :
	    ftype == 'p' ? sel_pipe :
	    ftype == 'a' ? sel_adir :
	    -1;
	     
	if ( select == 0 ) continue;

	if ( block_dot &&
	     ftr_name_full( this_ftr )[0] == '.' &&
	     ftr_name_full( this_ftr )[1] == 0 ) {
	    block_dot = 0;
	    continue;
	}

	if ( select == -1 ) {
	    save_errno = errno;

	    if ( ftype == 'e' ) {
		fprintf( stderr, "%s: error %u reading directory \"", program_name, save_errno );
		eput_bytes_str( ftr_name_full( this_ftr ) );
		fprintf( stderr, "\": %s\n", strerror( save_errno ) );
		continue;
	    }
	    else if ( ftype == '?' ) {
		fprintf( stderr, "%s: unrecognized file type: \"", program_name );
		eput_bytes_str( ftr_name_full( this_ftr ) );
		fprintf( stderr, "\"\n" );
	    }
	    else if ( ftype == '-' ) {
		fprintf( stderr, "%s: no status available for: \"", program_name );
		eput_bytes_str( ftr_name_full( this_ftr ) );
		fprintf( stderr, "\"\n" );
	    }
	    else {
		fprintf( stderr, "%s: error %u from ftr_get() (returned %u): %s\n",
			 program_name, save_errno, ftype, strerror( save_errno ) );
	    }
	    ++ error_count;
	    continue;
	}

	//---------------------
	// Output inode number.
	//---------------------
	if ( opt_inode ) {
	    fprintf( stdout, "%10u ", (unsigned int) ftr_stat( this_ftr ).st_ino );
	}

	//-------------
	// Output mode.
	//-------------
	if ( opt_mode ) {
	    temp_str[0] = ftype;

	    select = ftr_stat( this_ftr ).st_mode;

	    temp_str[1] = ( select & S_IRUSR ) ? 'r' : '-';
	    temp_str[2] = ( select & S_IWUSR ) ? 'w' : '-';
	    temp_str[3] = ( select & S_IXUSR )
		? ( ( select & S_ISUID ) ? 's' : 'x' )
		: ( ( select & S_ISUID ) ? 'S' : '-' );

	    temp_str[4] = ( select & S_IRGRP ) ? 'r' : '-';
	    temp_str[5] = ( select & S_IWGRP ) ? 'w' : '-';
	    temp_str[6] = ( select & S_IXGRP )
		? ( ( select & S_ISGID ) ? 's' : 'x' )
		: ( ( select & S_ISGID ) ? 'S' : '-' );

	    temp_str[7] = ( select & S_IROTH ) ? 'r' : '-';
	    temp_str[8] = ( select & S_IWOTH ) ? 'w' : '-';
	    temp_str[9] = ( select & S_IXOTH )
		? ( ( select & S_ISVTX ) ? 't' : 'x' )
		: ( ( select & S_ISVTX ) ? 'T' : '-' );

	    temp_str[10] = ' ';
	    temp_str[11] = 0;

	    fputs( temp_str, stdout );
	}

	//------------------------
	// Output hard link count.
	//------------------------
	if ( opt_links ) {
	    fprintf( stdout, "%3u ", (unsigned int) ftr_stat( this_ftr ).st_nlink );
	}

	//------------------
	// Output user name.
	//------------------
	if ( opt_uname ) {
	    id = ftr_stat( this_ftr ).st_uid;
	    select = map_ui_insert( map_uname, id );
	    if ( select == 0 ) {
		passwd_p = getpwuid( id );
		if ( passwd_p ) {
		    if ( map_str_store( map_uname, passwd_p->pw_name ) < 0 ) {
			fprintf( stderr, "%s: error storing %s name \"%s\" (%u) in map\n",
				 program_name, "user", passwd_p->pw_name, id );
			return 1;
		    }
		} else {
		    snprintf( temp_str, sizeof temp_str, "[%u]", (unsigned) id );
		    if ( map_str_store( map_uname, temp_str ) < 0 ) {
			fprintf( stderr, "%s: error storing %s number  \"%s\" (%u) in map\n",
				 program_name, "user", temp_str, id );
			return 1;
		    }
		}
		select = MAP_DATA_STR;
	    }
	    if ( select == MAP_DATA_STR ) {
		fprintf( stdout, "%-8s ", map_str_fetch_ptr( map_uname ) );
	    } else {
		fprintf( stderr, "%s: invalid %s name map type: %08lx\n",
			 program_name, "user", (unsigned long) select );
		return 1;
	    }
	}

	//----------------
	// Output user ID.
	//----------------
	if ( opt_uid ) {
	    fprintf( stdout, "%5u ", ftr_stat( this_ftr ).st_uid );
	}

	//------------------
	// Output group name.
	//------------------
	if ( opt_gname ) {
	    id = ftr_stat( this_ftr ).st_gid;
	    select = map_ui_insert( map_gname, id );
	    if ( select == 0 ) {
		group_p = getgrgid( id );
		if ( group_p ) {
		    if ( map_str_store( map_gname, group_p->gr_name ) < 0 ) {
			fprintf( stderr, "%s: error storing %s name \"%s\" (%u) in map\n",
				 program_name, "group", group_p->gr_name, id );
			return 1;
		    }
		} else {
		    snprintf( temp_str, sizeof temp_str, "[%u]", (unsigned) id );
		    if ( map_str_store( map_gname, temp_str ) < 0 ) {
			fprintf( stderr, "%s: error storing %s name \"%s\" (%u) in map\n",
				 program_name, "group", temp_str, id );
			return 1;
		    }
		}
		select = MAP_DATA_STR;
	    }
	    if ( select == MAP_DATA_STR ) {
		fprintf( stdout, "%-8s ", map_str_fetch_ptr( map_gname ) );
	    } else {
		fprintf( stderr, "%s: invalid %s name map type: %08lx\n",
			 program_name, "group", (unsigned long) select );
		return 1;
	    }
	}

	//----------------
	// Output group ID.
	//----------------
	if ( opt_gid ) {
	    fprintf( stdout, "%5u ", ftr_stat( this_ftr ).st_gid );
	}

	//-------------------------
	// Output blocks allocated.
	//-------------------------
	if ( opt_blocks ) {
	    if ( ftype == 'b' || ftype == 'c' ) {
		len = snprintf( temp_str,
				sizeof temp_str,
				"%u,%u",
				(unsigned int) major( ftr_stat( this_ftr ).st_rdev ),
				(unsigned int) minor( ftr_stat( this_ftr ).st_rdev ) );
		fprintf( stdout, "%10s ", temp_str );
	    } else {
		fprintf( stdout, "%10llu ", (unsigned long long) ( ftr_stat( this_ftr ).st_blocks ) );
	    }
	}

	//----------------------------------------
	// Output device major,minor or file size.
	//----------------------------------------
	if ( opt_size ) {
	    if ( ftype == 'b' || ftype == 'c' ) {
		len = snprintf( temp_str,
				sizeof temp_str,
				"%u,%u",
				(unsigned int) major( ftr_stat( this_ftr ).st_rdev ),
				(unsigned int) minor( ftr_stat( this_ftr ).st_rdev ) );
		fprintf( stdout, "%10s ", temp_str );
	    } else {
		fprintf( stdout, "%10llu ", (unsigned long long) ( ftr_stat( this_ftr ).st_size ) );
	    }
	}

	//----------------------
	// Output date and time.
	//----------------------
	if ( opt_date || opt_time || opt_secs ) {
	    if ( opt_secs ) {
		len = snprintf( temp_str,
				sizeof temp_str,
				"%10lu",
				(unsigned long) ftr_stat( this_ftr ).st_mtime );
	    } else {
		tm_p = ( ( opt_utc ) ? gmtime : localtime )( & ftr_stat( this_ftr ).st_mtime );
		len = strftime( temp_str,
				sizeof temp_str,
				( opt_time ) ? "%Y-%m-%d.%H:%M:%S" : "%Y-%m-%d",
				tm_p );
	    }

	    //----------------------------------
	    // Append nanoseconds, if requested.
	    //----------------------------------
	    if ( opt_time && opt_nano && ( ( sizeof temp_str ) - len > 10 ) ) {
		len += snprintf( temp_str + len,
				 sizeof temp_str - len,
				 ".%09lu",
//------------------------------------------
// Check for the GNU kludge to the stat time
// fields and exploit it to get nanoseconds.
//------------------------------------------
#ifdef st_mtime
				 (unsigned long) ( ftr_stat( this_ftr ).st_mtim.tv_nsec )
#else
				 0UL
#endif
		);
	    }

	    temp_str[len] = ' ';
	    temp_str[len+1] = 0;
	    fputs( temp_str, stdout );
	}

	//-------------
	// Output name.
	//-------------
	if ( opt_quiet == 0 ) {
	    if ( ! ftr_name_full( this_ftr ) ) {
		fputs( "<<nil>>", stdout );
	    } else {
		put_bytes_str( ftr_name_full( this_ftr ) );
	    }
	    if ( opt_append && ( ftype == 'd' || ftype == 'a' ) ) fputc( '/', stdout );
	}

	//------------------------
	// Output symlink pointer.
	//------------------------
	if ( opt_ptr && ftype == 'l' ) {
	    select = readlink( ftr_name_full( this_ftr ), temp_str, sizeof temp_str - 1 );
	    if ( select >= 0 ) {
		fputs( " -> ", stdout );
		put_bytes_len( temp_str, select );
	    } else {
		fprintf( stderr, "%s: error %u from readlink for \"%s\": %s\n",
			 program_name, errno, ftr_name_full( this_ftr ), strerror( errno ) );
	    }
	}

	//-------------
	// Finish line.
	//-------------
	fputc( '\n', stdout );
	fflush( stdout );
    }

    //----------
    // Clean up.
    //----------
    ftr_end( this_ftr );
    if ( map_uname ) map_destroy( map_uname );
    if ( map_gname ) map_destroy( map_gname );

    return 0;
}

