/********************************************************************
* Program : despace                                                 *
* File    : despace.c                                               *
* Description :                                                     *
*   Under Linix, filenames with spaces escape the spaces, and the   *
*   filename remains a unity.  Under DOS, spaces in filenames are   *
*   not escaped, so the separate bits of a filename are seen by     *
*   globbing separately.  This causes problems for scripts, etc.    *
*   So having spaces in DOS filenames is bad.  Use this program     *
*   to bulk rename files in a directory to replace spaces with      *
*   underscores.                                                    *
*                                                                   *
*   This will work under Linux as well, just to be complete, since  *
*   spaces in filenames isn't a good idea anywheres.  Code for      *
*   "getch" was pulled from GNU manual for libc, for noncanonical   *
*   terminal I/O.                                                   *
*                                                                   *
*   No warranties implied.  Released under Gnu Public License.      *
*   http://www.gnu.org/licenses/gpl.html                            *
*                                                                   *
*   To compile under GCC for DJGPP under DOS, use command:          *
*      gcc -Wall -o despace.exe despace.c                           *
*   To compile under GCC for MinGW under Windows, use command:      *
*      gcc -Wall -DMINGW -o despace.exe despace.c                   *
*   To compile under GCC for under Linux, use command:              *
*      gcc -Wall -DLINUX -o despace despace.c                       *
*                                                                   *
* Modification Record :                                             *
*   090519 : DLO  Initial Release                                   *
********************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <dirent.h>
#include <unistd.h>
#include <string.h>
#include <ctype.h>

#ifdef DJGPP
#include <conio.h>
#include <dir.h>
#endif  /* DJGPP */
#ifdef MINGW
#include <conio.h>
#include <io.h>
#endif  /* MINGW */
#ifdef LINUX
#include <termios.h>
#include <sys/types.h>
#include <sys/stat.h>
#endif  /* LINUX */

/*************************** Constants *****************************/
/* Default pwd path */
const char *default_path = ".";

/* flag bit values */
#define FL_RECURSE      0x01
#define FL_PROMPT       0x02

/* File type identity */
#define FT_NORMAL  1
#define FT_SUBDIR  2
#define FT_IGNORE  3

/*********************** Local Prototypes **************************/
int dirscan(char *topath, char *frompath);
void help(void);
#ifdef LINUX
void reset_input_mode(void);
int set_input_mode(void);
int getche(void);
#endif  /* LINUX */

/* Global Variables */
/*********************** Global Variables **************************/
int flags;               /* Program flags word */
char *to_dirname;           /* Root of scanned directory tree */
#ifdef LINUX
/* Saved terminal characteristics, to be restored upon program exit */
struct termios saved_attributes;
#endif  /* LINUX */

/********************************************************************
* Routine : main                                                    *
* Description :                                                     *
*   Executive routine                                               *
* Inputs   :                                                        *
*   argc - int, number of command-line arguments                    *
*   argv - char **, array of pointers to command-line arguments     *
* Outputs  :                                                        *
*   int, 0 if success, nonzero otherwise (see dirscan return codes) *
********************************************************************/
int main(int argc, char **argv)
{
    int opt;
    int retval;
#ifdef DJGPP
    int i;
    struct ffblk info;
#endif  /* DJGPP */
#ifdef MINGW
    long dir_handle;
    struct _finddata_t info;
#endif  /* MINGW */
#ifdef LINUX
    int i;
    struct stat info;
#endif  /* LINUX */
    char from_dirname[FILENAME_MAX+1];

    /* Extract command options from command-line */
    flags = 0;
    to_dirname = default_path;
    strcpy(from_dirname, default_path);
    retval = 0;
    opterr = 0;
    while((opt = getopt(argc, argv, "rph")) != -1) {
        switch(opt) {
            case 'r' :
                flags |= FL_RECURSE;
                break;
            case 'p' :
                flags |= FL_PROMPT;
                break;
            case 'h' :
                help();
                exit(0);
                break;
            default :
                fprintf(stderr,
                        "despace: unrecognized option '%c'. Use -h for help\n",
                        optopt);
                exit(0);
                break;
        }
    }
#ifdef LINUX
    /* In case "prompt" mode requested, set up terminal for "getche" */
    set_input_mode();
#endif  /* LINUX */

    /* If path argument present, set "to_dirname" to point to it */
    if(optind < argc) {
        to_dirname = argv[optind];
        /* Check if its a valid directory */
#ifdef DJGPP
        i = findfirst(to_dirname, &info, FA_DIREC);
        if(i != 0) {
            fprintf(stderr, "despace: cannot find %s\n", to_dirname);
            retval = -1;
        }
        else if(!(info.ff_attrib & FA_DIREC)) {
            fprintf(stderr, "despace: %s not a directory\n", to_dirname);
            retval = -1;
        }
#endif /* DJGPP */
#ifdef MINGW
        dir_handle = _findfirst(to_dirname, &info);
        if(dir_handle == 0) {
            fprintf(stderr, "despace: cannot find %s\n", to_dirname);
            retval = -1;
        }
        else {
            if(!(info.attrib & _A_SUBDIR)) {
                fprintf(stderr, "despace: %s not a directory\n", to_dirname);
                retval = -1;
            }
            _findclose(dir_handle);
        }
#endif /* MINGW */
#ifdef LINUX
        i = lstat(to_dirname, &info);
        if(i != 0) {
            fprintf(stderr, "despace: cannot find %s\n", to_dirname);
            retval = -1;
        }
        else {
            if(!S_ISDIR(info.st_mode)) {
                fprintf(stderr, "despace: %s not a directory\n", to_dirname);
                retval = -1;
            }
        }
#endif /* LINUX */
        /* Get the current working directory */
        if(!retval) {
            if(NULL == getcwd(from_dirname, FILENAME_MAX)) {
                fprintf(stderr, "despace: cannot obtain pwd\n");
            }
        }
    }

    if(!retval) {
        return dirscan(to_dirname, from_dirname);
    }
    else {
        return retval;
    }
}

/********************************************************************
* Routine : dirscan                                                 *
* Description :                                                     *
*   Scan the given directory, processing the files within it.       *
*                                                                   *
* Inputs   :                                                        *
*   to_path, char *, directory to process                           *
*   from_path, char *, directory to return to afterwards            *
*     Note: No checking is done on to_path or from_path; they are   *
*     assumed to be valid directories.                              *
* Outputs  :                                                        *
*   int, zero if success                                            *
*        -1 if path is not a valid directory name                   *
*        -2 if memory allocation error                              *
*        -3 if an illegal filename length encountered               *
********************************************************************/
int dirscan(char *topath, char *frompath)
{
    DIR *curdir;
    struct dirent *dptr;
    int filetype;
    int rdonly;
    int no_rename;
    int retval;
    int i;
    char *path;
    char *newpath;
#ifdef DJGPP
    struct ffblk info;
#endif  /* DJGPP */
#ifdef MINGW
    struct _finddata_t info;
#endif  /* MINGW */
#ifdef LINUX
    struct stat info;
#endif  /* LINUX */

    retval = 0;

    /* Attempt to change to the target directory */
    if(chdir(topath)) {
        return -1;
    }

    /* Start with POSIX opendir */
    curdir = opendir(".");

    /* Scan files in directory with POSIX readdir.
     *   For DJGPP, have to use DOS functions to get file info.
     *   MinGW has similar functions to do the same job. */
    while(!retval && (dptr = readdir(curdir)) != NULL) {
        /* current name of file */
        path = dptr->d_name;

        /* Ignore the "current directory" and "one-up" directory */
        if(!strcmp(path, ".") || !strcmp(path, "..")) {
            continue;
        }

        /* Assume normal, writeable file */
        no_rename = 0;
        rdonly = 0;
        filetype = FT_NORMAL;

        /* indicate no pathname has been allocated (yet) */
        newpath = NULL;

#ifdef DJGPP
        /* Get the info on the current file/directory */
        i = findfirst(path, &info, FA_DIREC);
        if(info.ff_attrib & FA_RDONLY) {
            rdonly = 1;
        }
        if((info.ff_attrib & FA_HIDDEN) ||
                (info.ff_attrib & FA_SYSTEM) ||
                (info.ff_attrib & FA_LABEL)) {
            filetype = FT_IGNORE;
        }
        if(info.ff_attrib & FA_DIREC) {
            filetype = FT_SUBDIR;
        }
#endif /* DJGPP */
#ifdef MINGW
        /* Get the info on the current file/directory */
        i = _findfirst(path, &info);
        if(info.attrib & _A_RDONLY) {
            rdonly = 1;
        }
        if((info.attrib & _A_HIDDEN) ||
                (info.attrib & _A_SYSTEM) ||
                (info.attrib & _A_VOLID)) {
            filetype = FT_IGNORE;
        }
        if(info.attrib & _A_SUBDIR) {
            filetype = FT_SUBDIR;
        }
#endif /* MINGW */
#ifdef LINUX
        /* Get the info on the current file/directory */
        i = lstat(path, &info);
        if(!(info.st_mode & S_IWUSR)) {
            rdonly = 1;
        }
        if(S_ISCHR(info.st_mode) ||
           S_ISBLK(info.st_mode) ||
           S_ISFIFO(info.st_mode) ||
           S_ISSOCK(info.st_mode)) {
            filetype = FT_IGNORE;
        }
        if(S_ISDIR(info.st_mode)) {
            filetype = FT_SUBDIR;
        }
#endif /* LINUX */

        /* If the "file" isn't something we want to rename, skip it. */
        if(filetype == FT_IGNORE) {
            continue;
        }

        /* If the file is read-only, then it can't be renamed, so
         * notify the user.  Otherwise, proceed to rename. */
        if(rdonly) {
            fprintf(stdout, "despace: %s is read-only!\n", path);
            no_rename = 1;
        }

        if(!no_rename) {
            /* Scan the name for spaces.  If none, indicate not renaming;
             * otherwise, proceed with the renaming procedure. */
            for(i=0; path[i] != '\0'; i++) {
                if(path[i] == ' ') {
                    i = 0;  /* Indicate space found */
                    break;
                }
            }
            if(i) {
                no_rename = 1;
            }
        }

        if(!no_rename && (flags & FL_PROMPT)) {
            /* If interactive mode is requested, ask the user if the
             * file should be renamed. */
            fprintf(stdout, "despace: rename %s ?(y/n/a/q)", path);
            /* "getche" is now defined for all environments.
             *  Flush stdout just to make sure user sees the prompt */
            fflush(stdout);
            i = getche();
            switch(toupper(i)) {
                case 'Y' :
                    break;
                case 'A' :
                    flags &= ~FL_PROMPT;
                    break;
                case 'Q' :
                    retval = -4;
                    break;
                default:
                    no_rename = 1;
                    break;
            }
            fprintf(stdout, "\n");
            if(retval == -4) {
                break;
            }
        }

        if(!no_rename) {
            /* Make a copy of the filename, and replace the spaces */
            i = strlen(path);
            if(i > FILENAME_MAX) {
                retval = -3;
                break;
            }
            newpath = (char *)malloc((i + 1) * sizeof(char));
            if(newpath == NULL) {
                retval = -2;
                break;
            }
            strncpy(newpath, path, i+1);
            for(; i >= 0; i--) {
                if(newpath[i] == ' ') {
                    newpath[i] = '_';
                }
            }

            /* rename() returns 0 if success, otherwise -1 */
            /* Note: If a file named "newpath" already exists, it
             * will get destroyed as part of the rename.  We could
             * check for a preexisting filename...
             * but we're not just now */
#if 0
            printf("rename %s to %s\n", path, newpath);
#endif
            if(rename(path, newpath)) {
                fprintf(stderr, "despace: rename of %s failed: " , path);
                perror("");
                no_rename = 1;
            }
            else {
                /* old name doesn't exist anymore */
                path = newpath;
            }
        }

        /* If file is a subdirectory and recursive rename was
         * requested, recurse on this subdirectory. */
        if((filetype == FT_SUBDIR) && (flags & FL_RECURSE)) {
            fprintf(stdout, "despace: entering directory %s\n", path);
            retval = dirscan(path, "..");
            if(retval) {
                /* abort upon fatal error */
                break;
            }
        }

        if(newpath != NULL) {
            free(newpath);
        }
    }
    closedir(curdir);

    /* If we "cd"ed to a path, we need to "cd" back out */
    chdir(frompath);
    return retval;
}

/********************************************************************
* Routine : help                                                    *
* Description :                                                     *
*   Print help message to stdout                                    *
*                                                                   *
* Inputs   : None                                                   *
* Outputs  : None                                                   *
********************************************************************/
static char *helptext[] = {
    "despace.  v1.0 (19 May 2009)",
    "Rename files to replace spaces with underscores",
    "syntax: despace [-rp] [pathname]",
    " where -r = recurse into subdirectories",
    "       -p = interactive (prompt user for y/n)",
    "       pathname = directory to despace filenames (default is pwd)",
    ""    /* empty string terminates list */
};

void help(void)
{
    int i;
    for(i=0; *helptext[i]; i++) {
        fprintf(stdout, "%s\n", helptext[i]);
    }
}

#ifdef LINUX
/********************************************************************
* Routine : reset_input_mode                                        *
* Description :                                                     *
*   Restore original terminal characteristics.  This function will  *
*   be called upon program exit.                                    *
*                                                                   *
* Inputs   : None                                                   *
* Outputs  : None                                                   *
********************************************************************/
void reset_input_mode(void)
{
  tcsetattr(STDIN_FILENO, TCSANOW, &saved_attributes);
}

/********************************************************************
* Routine : set_input_mode                                          *
* Description :                                                     *
*   Set "stdin" terminal to mode for unbuffered input.              *
*                                                                   *
* Inputs   : None                                                   *
* Outputs  : zero if success, otherwise if stdio not a terminal     *
********************************************************************/
int set_input_mode(void)
{
  struct termios tattr;

  /* Make sure stdin is a terminal. */
  if (!isatty (STDIN_FILENO))
  {
      fprintf (stderr, "despace: not a terminal.\n");
      return -1;
  }

  /* Save the terminal attributes so we can restore them later. */
  tcgetattr (STDIN_FILENO, &saved_attributes);
  atexit (reset_input_mode);

  /* Set the funny terminal modes. */
  tcgetattr (STDIN_FILENO, &tattr);
  tattr.c_lflag &= ~(ICANON|ECHO); /* Clear ICANON and ECHO. */
  tattr.c_cc[VMIN] = 1;
  tattr.c_cc[VTIME] = 0;
  tcsetattr (STDIN_FILENO, TCSAFLUSH, &tattr);

  return 0;
}

/********************************************************************
* Routine : getche                                                  *
* Description :                                                     *
*   Wait for a key to be pressed from stdin, send it to stdout,     *
*   and then return it.                                             *
*                                                                   *
* Inputs   : None                                                   *
* Outputs  : int, character received from keyboard                  *
********************************************************************/
int getche(void)
{
  char c;

  /* Get the next character (keypress) from stdin */
  read(STDIN_FILENO, &c, 1);
  
  /* Echo it to stdout */
  putchar(c);
  fflush(stdout);

  return (int)c;
}

#endif  /* LINUX */

