src/cline.c

Sun, 05 Jul 2026 12:27:42 +0200

author
Mike Becker <universe@uap-core.de>
date
Sun, 05 Jul 2026 12:27:42 +0200
changeset 105
abce66443ab7
parent 104
1b7716b156a4
permissions
-rw-r--r--

release 1.6.0

/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 * Copyright 2018 Mike Becker. All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 * 1. Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 * 
 * 2. Redistributions in binary form must reproduce the above copyright
 * notice, this list of conditions and the following disclaimer in the
 * documentation and/or other materials provided with the distribution.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#ifndef VERSION
#error "VERSION macro must be set by build system"
#endif

#include "stdinc.h"
#include "settings.h"
#include "scanner.h"
#include "arguments.h"
#include "regex_parser.h"

static void print_help() {
  printf(
    "\nUsage:"
    "\n      cline [Options] [Directories...]"
    "\n\nCounts the line terminator characters (\\n) within all"
    " files in the specified\ndirectories."
    "\n\nOptions:"
    "\n  -b <level>          - Binary file heuristics level (default medium)"
    "\n                        One of: ignore low medium high"
    "\n  -c                  - Count non-whitespace characters instead of lines"
    "\n  -d                  - Report only directory sums"
    "\n  -D <path>           - Excludes the directory at the specified <path>"
    "\n  -D <name>           - Excludes all directories with the given <name>"
    "\n  -E <pattern>        - Excludes any line matching the <pattern>"
    "\n  -e <start> <end>    - Excludes lines between <start> and <end>"
    "\n                        You may use these options multiple times"
    "\n  -h, --help          - This help text"
    "\n  -i                  - Print out individual sums per file extension"
    "\n                        (cannot be used together with -V)"
    "\n  -m                  - Print information about matching files only"
    "\n  -s <suffixes>       - Only count files with these suffixes (separated"
    "\n                        by commas)"
    "\n  -S <suffixes>       - Count any file except those with these suffixes"
    "\n                        (separated by commas)"
    "\n  -r, -R              - Includes subdirectories"
    "\n  -v, --version       - Print out version information"
    "\n  -V                  - Turn verbose output off, print the result only"
    "\n\nShortcuts:"
    "\n  --exclude-cstyle-comments : -E '\\s*//' -e '\\s*/\\*' '\\*/\\s*'"
    "\n  --exclude-blank-lines     : -E '^\\s*$'"
    "\n"
    "\nMore infos are available in the man page"
    "\nor at https://uap-core.de/man/cline.html"
    "\n");
}

static int exit_with_version(settings *settings) {
  printf("cline - Version: " VERSION "\n");
  destroy_settings(settings);
  return 0;
}

static int exit_with_help(settings *settings, int code) {
  printf("cline - Version: " VERSION "\n");
  print_help();
  destroy_settings(settings);
  return code;
}

static void normalize_excluded_dirs(settings *settings) {
  /* normalize all paths */
  for (size_t i = 0 ; i < settings->exclude_dirs->count ; i++) {
    char *arg = strdup(settings->exclude_dirs->items[i]);
    if (strpbrk(arg, "/\\") == NULL) {
      /* do not normalize names */
      settings->exclude_dirs->items[i] = arg;
    } else {
      size_t arglen = strlen(arg);
      /* fix file separators */
      char fs = FILE_SEPARATOR;
      char badfs = FILE_SEPARATOR == '/' ? '\\' : '/';
      for (size_t j = 0 ; j < arglen ; j++) {
        if (arg[j] == badfs) {
          arg[j] = fs;
        }
      }
      /* make path absolute */
      settings->exclude_dirs->items[i] = make_path_absolute(arg);
      free(arg);
    }
  }

  /* tell the string list to free the items */
  settings->exclude_dirs->free_item = free;
}

static const char *sepline_double = "===============================================================================\n";
static const char *sepline_single = "-------------------------------------------------------------------------------\n";

int main(int argc, char **argv) {

  /* Settings */
  settings *settings = new_settings();
  if (settings == NULL) {
    fprintf(stderr, "Memory allocation failed.\n");
    return 1;
  }

  /* Get arguments */
  string_list *directories = new_string_list();
  if (directories == NULL) {
    fprintf(stderr, "Memory allocation failed.\n");
    return 1;
  }
  const char *includeSuffix = NULL;
  const char *excludeSuffix = NULL;
  int checked = 0;

  for (int t = 1 ; t < argc ; t++) {

    int argflags = argument_check(argv[t], "hsSrRmvVbeEicdD");
    int paropt = 0; /* checks if no more than one opt with arg is combined */

    /* h */
    if ((argflags & 1) > 0 || strcmp(argv[t], "--help") == 0) {
      return exit_with_help(settings, 0);
    }
    /* s */
    if ((argflags & 2) > 0) {
      if (paropt++ || argument_register(&checked, 2)) {
        return exit_with_help(settings, 1);
      }
      t++;
      if (t >= argc) {
        return exit_with_help(settings, 1);
      }
      includeSuffix = argv[t];
    }
    /* S */
    if ((argflags & 4) > 0) {
      if (paropt++ || argument_register(&checked, 4)) {
        return exit_with_help(settings, 1);
      }
      t++;
      if (t >= argc) {
        return exit_with_help(settings, 1);
      }
      excludeSuffix = argv[t];
    }
    /* r, R */
    if ((argflags & 24) > 0) {
      if (argument_register(&checked, 24)) {
        return exit_with_help(settings, 1);
      }
      settings->recursive = true;
    }
    /* m */
    if ((argflags & 32) > 0) {
      if (argument_register(&checked, 32)) {
        return exit_with_help(settings, 1);
      }
      settings->matches_only = true;
    }
    /* v */
    if ((argflags & 64) > 0 || strcmp(argv[t], "--version") == 0) {
      return exit_with_version(settings);
    }
    /* V */
    if ((argflags & 128) > 0) {
      if (argument_register(&checked, 128)) {
        return exit_with_help(settings, 1);
      }
      settings->verbose = false;
    }
    /* b */
    if ((argflags & 256) > 0) {
      if (paropt++ || argument_register(&checked, 256)) {
        return exit_with_help(settings, 1);
      }
      t++;
      if (t >= argc) {
        return exit_with_help(settings, 1);
      }
      if (strcasecmp(argv[t], "ignore") == 0) {
        settings->bfile->level = BFILE_IGNORE;
      } else if (strcasecmp(argv[t], "low") == 0) {
        settings->bfile->level = BFILE_LOW_ACCURACY;
      } else if (strcasecmp(argv[t], "medium") == 0) {
        settings->bfile->level = BFILE_MEDIUM_ACCURACY;
      } else if (strcasecmp(argv[t], "high") == 0) {
        settings->bfile->level = BFILE_HIGH_ACCURACY;
      } else {
        return exit_with_help(settings, 1);
      }
    }
    /* e */
    if ((argflags & 512) > 0) {
      if (paropt++ || t + 2 >= argc) {
        return exit_with_help(settings, 1);
      }
      t++; add_string(settings->regex->pattern_list, argv[t]);
      t++; add_string(settings->regex->pattern_list, argv[t]);
    }
    /* E */
    if ((argflags & 1024) > 0) {
      t++;
      if (paropt++ || t >= argc) {
        return exit_with_help(settings, 1);
      }
      add_string(settings->regex->pattern_list, argv[t]);
      add_string(settings->regex->pattern_list, "$");
    }
    /* i */
    if ((argflags & 2048) > 0) {
      /* cannot be used together with -V */
      if ((checked & 128) > 0) {
        return exit_with_help(settings, 1);
      }
      settings->individual_sums = true;
    }
    /* c */
    if ((argflags & 4096) > 0) {
        if (argument_register(&checked, 4096)) {
            return exit_with_help(settings, 1);
        }
        settings->count_chars = true;
        settings->regex->count_chars = true;
    }
    /* d */
    if ((argflags & 8192) > 0) {
      if (argument_register(&checked, 8192)) {
        return exit_with_help(settings, 1);
      }
      /* ignored together with -V */
      if ((checked & 128) == 0) {
        settings->dirs_only = true;
      }
    }
    /* D */
    if ((argflags & 16384) > 0) {
      t++;
      if (paropt++ || t >= argc) {
        return exit_with_help(settings, 1);
      }
      add_string(settings->exclude_dirs, argv[t]);
    }
    if (argflags == 0) {
      /* SHORTCUTS */
      if (strcmp(argv[t], "--exclude-cstyle-comments") == 0) {
        add_string(settings->regex->pattern_list, "\\s*//");
        add_string(settings->regex->pattern_list, "$");
        add_string(settings->regex->pattern_list, "\\s*/\\*");
        add_string(settings->regex->pattern_list, "\\*/\\s*");
      } else if (strcmp(argv[t], "--exclude-blank-lines") == 0) {
        add_string(settings->regex->pattern_list, "^\\s*$");
        add_string(settings->regex->pattern_list, "$");
      }
      /* Unknown option */
      else if (argv[t][0] == '-') {
        fprintf(stderr, "Unrecognized option: %s\n\n", argv[t]);
        return exit_with_help(settings, 1);
      }
      /* Path */
      else {
        add_string(directories, argv[t]);
      }
    }
  }

  /* Find tokens */
  parse_csl(includeSuffix, settings->include_suffixes);
  parse_csl(excludeSuffix, settings->exclude_suffixes);

  /* Compiler regular expressions, if any specified */
  if (!regex_compile_all(settings->regex)) {
    destroy_string_list(directories);
    destroy_settings(settings);
    return 1;
  }

  /* Normalize paths for excluded directories */
  normalize_excluded_dirs(settings);

  /* Scan directories */
  scanresult *result = new_scanresult(settings);
  const char *result_type = settings->count_chars ? "chars" : "lines";
  bool has_output = false;
  unsigned total = 0;
  if (directories->count == 0) {
      add_string(directories, ".");
  }
  for (unsigned t = 0 ; t < directories->count ; t++) {
    /* Don't waste memory when only the total sum is needed */
    string_list *output = NULL;
    if (settings->verbose) {
      output = new_string_list();
      output->free_item = free;
    }
    scan_dir((scanner){directories->items[t], 0},
        settings, output, result);
    total += result->result;
    if (settings->verbose) {
      has_output |= output->count > 0;
      for (size_t i = 0 ; i < output->count ; i++) {
        printf("%s", output->items[i]);
      }
      destroy_string_list(output);
      if (directories->count > 1) {
        has_output = true;
        fputs(sepline_single, stdout);
        printf("%-63s%10u %s\n", directories->items[t],
          result->result, result_type);
        fputs(sepline_single, stdout);
      }
    }
  }
  destroy_string_list(directories);

  /* Print result */
  if (settings->verbose) {
    if (result->ext) {
      if (result->ext->count > 0) {
        has_output = true;
        fwrite(sepline_double, 1, 80, stdout);
        printf("\nIndividual sums:\n");
        for (unsigned t = 0 ; t < result->ext->count ; t++) {
          printf(" %-62s%10u %s\n",
                 result->ext->extensions[t],
                 result->ext->result[t],
                 result_type);
        }
      }
    }

    if (has_output) {
      fwrite(sepline_double, 1, 80, stdout);
      printf("%73d %s\n", total, result_type);
    } else {
      /* If there was not any output so far, skip formatting */
      printf("%d %s\n", total, result_type);
    }

    if (settings->confusing_lnlen &&
        settings->regex->pattern_list->count > 0) {

      printf("\nSome files contain too long lines.\n"
        "The parser currently supports a maximum line length of %u."
        "\nThe result might be wrong.\n", MAX_LINELENGTH);
    }
  } else {
    printf("%u", total);
  }
  destroy_scanresult(result);
  destroy_settings(settings);

  return 0;
}

mercurial