/*
*   This file released to public domain by the copyright holders 
*   including ArrayComm, Inc.
*   Author: Ted Merrill
*/

/* detab.c -- Program to convert tabs into spaces in files in place. */

char * HelpLines[] =
{
"NAME   ",
"        detab -- Program to convert tabs into spaces in files in place. ",
"  ",
"SYNOPSIS  ",
"        detab [-<nspaces>] [-binok] [-f] <file>...  ",
"        detab -h              -- for this message.   ",
"  ",
"DESCRIPTION  ",
"------------------------------------------------------------------------------  ",
"Where <nspaces> is the number of spaces per tab (typically 4 or 8).  ",
"This perhaps should be required, since there doesn't seem to be   ",
"universal agreement on what the default should be.  ",
"However, the traditional standard of 8 is the default here.  ",
"  ",
"The -f option allows disenables checking for options for all succeeding  ",
"arguments, thus allowing filenames beginning with a hyphen to be detabbed.  ",
"  ",
"Only regular files that have write permission enabled are detabbed.  ",
"The files are overwritten in place, but only if they contain tabs.  ",
"(Actually, the program writes a temp file, and then does rename  ",
"to move it to the desired name.  ",
"Thus this will fail if you don't have directory write permission.)  ",
"  ",
"If a file does not contain tabs, it is not altered.  ",
"For each eligible file, if the file contained a tab,  ",
"the line is written to the stdout:  ",
"    Detabbed <file>  ",
"Otherwise (for eligible files), this output line is written:  ",
"    No tabs in <file>  ",
"Ineligible files (e.g. directories, readonly files, device files, etc.)  ",
"are indicated by various messages.  ",
"Binary files (files with a byte with high bit set) are not eligible unless ",
"the -binok option is used.  ",
"  ",
"Locks out QUIT and INT signals during each file operation,   ",
"aborting only between files.  ",
"However, it is possible that a system failure may leave a harmless  ",
"temporary file in the same directory as the target file.  ",
"This program uses the rename system call, which should atomically  "
"replace the old file with the detabbed file.  ",
0       /* terminator */
};

#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <signal.h>

int BinaryOk = 0;       /* 0 = don't change binary files, 1 = do change them */
volatile int signalCaught = 0;
void signalHandler(int unused)
{
    signalCaught = 1;   /* flag for later */
}

void PrintHelp(void)
{
    int Index;
    for ( Index = 0; HelpLines[Index]; Index++ )
        printf("%s\n", HelpLines[Index] );
}

int detabInner( FILE *inFile, FILE *outFile, int nspaces )
{   /* returns -1 if binary file, < -1 if error, 0 if no tabs, >0 if tabs converted*/
    int nCol = 0;
    int count = 0;
    int Ch;
    int tabFlag = 0;        /* nonzero when we get at least one tab */
    while ( (Ch = getc(inFile)) != EOF )
    {
	count++;
        if ( ! BinaryOk && (Ch & 0x80) != 0 )
        {   /* apparent binary character */
            return -count;
        }
        if ( Ch == '\n' )
        {   /* newline */
            putc(Ch,outFile);
            nCol = 0;
            /* Check occasionally for abort... */
            if ( signalCaught ) return 0x80000000;
        }
        else
        if ( Ch != '\t' )
        {   /* non-tab, non-newline*/
            nCol++;
            putc(Ch,outFile);
        }
        else
        {   /* here for tab */
            tabFlag = 1;
            do
            {   /* put at least one space! */
                putc(' ',outFile);
                nCol++;
            }
            while ( nCol % nspaces );
        }
    }
    return tabFlag;
}

void detabfile(char *filepath, int nspaces)
{
    char newpath[512];
    FILE *inFile = 0;
    FILE *outFile = 0;
    int nReturn;
    int nFileMode;
    int n;
    struct stat statbuf;

    if ( nspaces <= 0 ) 
    {
        fprintf(stderr,
            "You must specify no. of spaces per tab (-h for help)!\n");
        exit(1);
    }

    if ( lstat(filepath,&statbuf) )
    {
        fprintf(stderr,"Not found: %s\n", filepath );
        return;
    }
    if ( S_ISLNK(statbuf.st_mode) )
    {
        /* Note: if we DID want to detab a soft link, perhaps the file should
        *   be detabbed in its "true" directory!
        */
        printf("Not detabbing soft link: %s\n", filepath );
        return;
    }
    if ( S_ISDIR(statbuf.st_mode) )
    {
        printf("Not detabbing directory: %s\n", filepath );
        return;
    }
    if ( ! S_ISREG(statbuf.st_mode) )
    {
        printf("Not detabbing device/pipe/socket file: %s\n", filepath );
        return;
    }
    if ( access( filepath, W_OK ) )
    {
        /* NOTE: without this check, this program does a fine job
        *   of detabbing readonly files, provided the directory is readable.
        */
        printf("Not detabbing readonly file: %s\n", filepath );
        return;
    }
    if ( (n = strlen(filepath)) > 7 
	 && tolower(filepath[n-8]) == 'm' 
	 && strncmp(filepath+n-7, "akefile", 7) == 0 
	 && (n == 8 || filepath[n-9] == '/') )
    {
	printf("Not detabbing make file %s\n", filepath );
	return;
    }

    /* Ignore signals */
    signal(SIGQUIT,signalHandler);
    signal(SIGINT,signalHandler);

    sprintf(newpath,"%s__temp_detab",filepath);
    inFile = fopen(filepath,"r");
    if ( ! inFile )
    {
        fprintf(stderr, "Could not open %s for reading.\n", filepath);
        exit(1);
    }
    if (fstat(fileno(inFile), &statbuf))
    {
        fprintf(stderr, "Stat error on %s\n", filepath);
        exit(1);
    }
    nFileMode = statbuf.st_mode;
    outFile = fopen(newpath,"w");
    if ( ! outFile )
    {
        fprintf(stderr, "Could not create %s .\n", newpath);
        exit(1);
    }
    nReturn = detabInner(inFile,outFile,nspaces);
    if ( nReturn == 0x80000000 )
    {   /* error -- already reported */
        fclose(inFile);
        fclose(outFile);
        unlink(newpath);
        fprintf(stderr,"Abort.\n");
        exit(1);
    }
    else
    if ( nReturn < 0 )
    {
        printf("Not detabbing binary file %s position %d\n", 
	       filepath, -nReturn );
        fclose(inFile);
        fclose(outFile);
        unlink(newpath);
        goto CleanUpSignals;
    }
    else
    if ( nReturn == 0 )
    {   /* no tabs */
        printf("No tabs in %s\n", filepath );
        fclose(inFile);
        fclose(outFile);
        unlink(newpath);
        goto CleanUpSignals;
    }
    /* else tabs were replaced */
    if ( ferror(inFile ) )
    {
        fprintf(stderr, "Read error on %s .\n", filepath );
        fclose(inFile);
        fclose(outFile);
        unlink(newpath);
        exit(1);
    }
    fclose(inFile);
    if ( ferror(outFile ) )
    {
        fprintf(stderr, "Write error on %s .\n", newpath );
        fclose(outFile);
        unlink(newpath);
        exit(1);
    }
    fclose(outFile);
    if ( chmod(newpath,nFileMode) )
    {
        fprintf(stderr, "Could not change mode of %s to 0%o\n",
                    newpath, nFileMode );
        unlink(newpath);
        exit(1);
    }
    if ( rename(newpath,filepath) )
    {   /* This really shouldn't fail at this point, but... */
        fprintf(stderr, "Could not rename %s to %s\n", newpath, filepath );
        fprintf(stderr, "Untabbed file should be in %s\n", filepath );
        fprintf(stderr, "Tabbed file should be in %s\n", newpath );
        fprintf(stderr, "Please check your files...\n");
        exit(1);
    }
    printf("Detabbed %s\n", filepath );

    CleanUpSignals:
    if ( signalCaught ) 
    {
        fprintf(stderr, "Abort!\n");
        exit(1);
    }
    /* Reenable signals */
    signal(SIGQUIT,SIG_DFL);
    signal(SIGINT,SIG_DFL);
    return;
}

int main(int argc, char **argv)
{
    /* NOTE ! Standard no. for nspaces is 8, NOT 4 !!! */
    int nspaces = 8;        /* zero means unspecified, which is fatal */
    int nfiles = 0;  /* no. of files processed */
    int checkdash = 1;  /* processing options enabled? */
    char *arg;
    for ( argv++; (arg = *argv) != NULL; argv++ )
    {
        if ( checkdash && *arg == '-' )
        {
            if ( ! strcmp( arg, "-h" )) 
            {
                PrintHelp();
                exit(0);
            }
            if ( ! strcmp( arg, "-f" )) 
            {
                checkdash = 0;
                continue;
            }
            if ( ! strcmp( arg, "-binok" )) 
            {
                BinaryOk = 1;
                continue;
            }
            if ( ! isdigit(arg[1]) ) 
            {
                fprintf(stderr, "Unknown option `%s': use -h for help.\n",
                    arg );
            }
            nspaces = atol(arg+1);
            /* validity of nspaces is checked by detabfile() */
        }
        else
        {
            detabfile(arg, nspaces);
            nfiles++;
        }
    }
    if ( ! nfiles ) printf("No files were specified; use -h for help.\n");
    exit(0);
    return 0;
}
