aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore8
-rw-r--r--CMakeLists.txt33
-rw-r--r--LICENSE27
-rw-r--r--README.md71
-rw-r--r--docs/man/cleanpath.1116
-rw-r--r--include/cleanpath.h27
-rw-r--r--lib/cleanpath.c110
-rw-r--r--src/main.c167
8 files changed, 559 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..6df3236
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+.idea
+cmake-build-*
+*.o
+*.cbp
+*.so.*
+*.so
+*.a
+*.dylib*
diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644
index 0000000..096a49e
--- /dev/null
+++ b/CMakeLists.txt
@@ -0,0 +1,33 @@
+cmake_minimum_required(VERSION 3.0)
+project(clean_path)
+
+set(CMAKE_C_STANDARD 99)
+include_directories(${CMAKE_SOURCE_DIR}/include)
+set(LIB_SOURCES lib/cleanpath.c)
+set(MAIN_SOURCES src/main.c)
+set(INCLUDES include/cleanpath.h)
+
+add_library(cleanpath_static STATIC ${LIB_SOURCES})
+set_target_properties(cleanpath_static PROPERTIES PUBLIC_HEADER ${INCLUDES})
+set_target_properties(cleanpath_static PROPERTIES OUTPUT_NAME cleanpath)
+
+add_library(cleanpath_shared SHARED ${LIB_SOURCES})
+set_target_properties(cleanpath_shared PROPERTIES PUBLIC_HEADER ${INCLUDES})
+set_target_properties(cleanpath_shared PROPERTIES OUTPUT_NAME cleanpath)
+
+add_executable(cleanpath ${MAIN_SOURCES})
+target_link_libraries(cleanpath cleanpath_static)
+
+install(TARGETS cleanpath_static
+ PUBLIC_HEADER DESTINATION include
+ LIBRARY DESTINATION lib)
+
+install(TARGETS cleanpath_shared
+ PUBLIC_HEADER DESTINATION include
+ LIBRARY DESTINATION lib)
+
+install(TARGETS cleanpath
+ RUNTIME DESTINATION bin)
+
+install(FILES ${CMAKE_SOURCE_DIR}/docs/man/cleanpath.1
+ DESTINATION share/man/man1 COMPONENT doc) \ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..0d40437
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,27 @@
+Copyright (c) 2021, Joseph Hunkeler
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* 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.
+
+* Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+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. \ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..38ebcbb
--- /dev/null
+++ b/README.md
@@ -0,0 +1,71 @@
+# cleanpath
+
+`cleanpath` is a utility that filters unwanted elements from an environment variable.
+
+# Installation
+
+```shell
+$ git clone https://github.com/jhunkeler/cleanpath
+$ cd cleanpath
+$ mkdir -p build
+$ cmake .. -DCMAKE_INSTALL_PREFIX=$HOME/.local
+$ make install
+```
+
+# Usage
+
+```shell
+usage: cleanpath [-hVelrsv] [pattern ...]
+ --help -h Displays this help message
+ --version -V Displays the program's version
+ --exact -e Filter when pattern is an exact match (default)
+ --loose -l Filter when any part of the pattern matches
+ --regex -r Filter matches with (Extended) Regular Expressions
+ --sep [str] -s Use custom path separator (default: ':')
+ --env [str] -E Use custom environment variable (default: PATH)
+```
+
+# Example
+
+A typical MacOS path with Macports installed:
+```shell
+/opt/local/bin:/opt/local/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/X11/bin:/Library/Apple/usr/bin
+```
+
+## Remove MacPorts from the PATH
+
+### Exact match (default)
+```shell
+$ cleanpath /opt/local/bin /opt/local/sbin
+/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/X11/bin:/Library/Apple/usr/bin
+```
+
+### Loose match
+```shell
+$ cleanpath -l /opt/local
+/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/X11/bin:/Library/Apple/usr/bin
+```
+
+### Regex match
+
+```shell
+$ cleanpath -r ^/opt/local/.*
+/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/X11/bin:/Library/Apple/usr/bin
+```
+
+## Modifying other environment variables
+
+```shell
+$ TESTVAR=a:b/c:d/e:f
+$ cleanpath -E TESTVAR -s / -l c
+a:b/e:f
+```
+
+## Using cleanpath in a script
+
+```shell
+#!/usr/bin/env bash
+# Remove MacPorts and Fink
+PATH=$(cleanpath -r '^/opt/local/.*' '^/opt/sw/.*')
+export PATH
+```
diff --git a/docs/man/cleanpath.1 b/docs/man/cleanpath.1
new file mode 100644
index 0000000..84479c7
--- /dev/null
+++ b/docs/man/cleanpath.1
@@ -0,0 +1,116 @@
+.\" Automatically generated by Pandoc 2.13
+.\"
+.TH "CLEANPATH" "1" "" "" "Utility"
+.hy
+.SH NAME
+.PP
+\f[B]cleanpath\f[R] \[em] Filters unwanted elements from an environment
+variable
+.SH SYNOPSIS
+.PP
+\f[B]cleanpath\f[R] [options] [pattern \&...]
+.SH DESCRIPTION
+.PP
+\f[B]cleanpath\f[R] filters unwanted elements from an environment
+variable.
+By default, this utility reads from the \f[I]PATH\f[R] envrionment
+variable.
+This behavior may be overridden using the \f[B]--env\f[R] option
+described below.
+.SS OPTIONS
+.TP
+\f[B]-h\f[R], \f[B]--help\f[R]
+Prints brief usage information.
+.TP
+\f[B]-V\f[R], \f[B]--version\f[R]
+Prints the current version number.
+.TP
+\f[B]-e\f[R], \f[B]--exact\f[R]
+Filter when pattern is an exact match (default)
+.TP
+\f[B]-l\f[R], \f[B]--loose\f[R]
+Filter when any part of the pattern matches
+.TP
+\f[B]-r\f[R], \f[B]--regex\f[R]
+Filter matches with (Extended) Regular Expressions
+.TP
+\f[B]-s\f[R], \f[B]--sep\f[R] \f[I][str]\f[R]
+Use custom path separator (default: \f[I]`:'\f[R])
+.RS
+.PP
+The separator may be a string longer than one byte.
+.RE
+.TP
+\f[B]-E\f[R], \f[B]--env\f[R] \f[I][str]\f[R]
+Use custom environment variable (default: \f[I]PATH\f[R])
+.RS
+.PP
+The environment variable passed to \f[B]-E\f[R] must exist.
+.RE
+.SH EXAMPLES
+.SS A typical user PATH
+.PP
+/opt/special/bin:/usr/local/programX/bin:/usr/local/bin:/usr/bin:/bin
+.IP "1." 3
+Filter only \f[I]/usr/local/bin\f[R]
+.RS 4
+.PP
+cleanpath -e /usr/local/bin
+.PP
+/opt/special/bin:/usr/local/programX/bin:/usr/bin:/bin
+.RE
+.IP "2." 3
+Filter any paths containing \f[I]/usr/local\f[R]
+.RS 4
+.PP
+cleanpath -l /usr/local
+.PP
+/opt/special/bin:/usr/bin:/bin
+.RE
+.IP "3." 3
+Filter any paths containing \f[I]special\f[R] and \f[I]local\f[R]
+.RS 4
+.PP
+cleanpath -l special local
+.PP
+/usr/bin:/bin
+.RE
+.IP "4." 3
+Filter only \f[I]/usr/local/programX/bin\f[R] using regular expressions
+.RS 4
+.PP
+cleanpath -r `\[ha]/usr/local/p.*X/bin$'
+.PP
+/opt/special/bin:/usr/local/bin:/usr/bin:/bin
+.RE
+.SS Using different environment variables
+.IP "1." 3
+Filter a named environment variable with custom separator
+.RS 4
+.PP
+export TESTVAR=a:b/c:d/e:f
+.PP
+cleanpath -v TESTVAR -s / -l c
+.PP
+a:b/e:f
+.RE
+.SS Shell Scripting
+.PP
+PATH=\[lq]/usr/local/bin:/usr/bin:/bin\[rq]
+.PP
+PATH=\[lq]$(cleanpath -l /usr/local)\[rq]; export PATH
+.PP
+# /usr/local/bin has been filtered from PATH
+.SH ERRORS
+.PP
+\f[B]cleanpath\f[R] will exit non-zero (>0) when an error occurs.
+A value greater than one (1) indicates a fatal error.
+.SH BUGS
+.PP
+See GitHub Issues: <https://github.com/jhunkeler/cleanpath/issues>
+.SH AUTHOR
+.PP
+Joseph Hunkeler <jhunkeler\[at]gmail.com, jhunk\[at]stsci.edu>
+.SH SEE ALSO
+.PP
+\f[B]re_format(7)\f[R]
diff --git a/include/cleanpath.h b/include/cleanpath.h
new file mode 100644
index 0000000..4e7d35d
--- /dev/null
+++ b/include/cleanpath.h
@@ -0,0 +1,27 @@
+#ifndef CLEANPATH_CLEANPATH_H
+#define CLEANPATH_CLEANPATH_H
+
+#define CLEANPATH_VERSION "0.1.0"
+#define CLEANPATH_FILTER_NONE -1
+#define CLEANPATH_FILTER_EXACT 0
+#define CLEANPATH_FILTER_LOOSE 1
+#define CLEANPATH_FILTER_REGEX 2
+#define CLEANPATH_PART_MAX 1024
+#define CLEANPATH_VAR "PATH"
+#define CLEANPATH_SEP ":"
+
+struct CleanPath {
+ char *data; // Pointer to the path string
+ size_t data_len; // Length of the path string
+ char *sep; // Pointer to the separator used to split the data string
+ char *part[CLEANPATH_PART_MAX]; // Array of pointers to path elements
+ size_t part_nelem; // Total number of elements in part array
+};
+
+// Prototypes
+struct CleanPath *cleanpath_init(const char *path, const char *sep);
+void cleanpath_filter(struct CleanPath *path, unsigned mode, const char *pattern);
+char *cleanpath_read(struct CleanPath *path);
+void cleanpath_free(struct CleanPath *path);
+
+#endif //CLEANPATH_CLEANPATH_H \ No newline at end of file
diff --git a/lib/cleanpath.c b/lib/cleanpath.c
new file mode 100644
index 0000000..344c705
--- /dev/null
+++ b/lib/cleanpath.c
@@ -0,0 +1,110 @@
+#include <stdlib.h>
+#include <string.h>
+#include <limits.h>
+#include <regex.h>
+#include "cleanpath.h"
+
+/**
+ * Split path into parts by sep
+ * @param path a string (e.g. "/path1:/path2:/pathN")
+ * @param sep a delimiter string (e.g. ":")
+ * @note CleanPath->data string is modified. part are pointers to addresses of path (i.e. do not free())
+ * @return a pointer to a CleanPath structure
+ */
+struct CleanPath *cleanpath_init(const char *path, const char *sep) {
+ struct CleanPath *result;
+ char *elem;
+ size_t i;
+
+ if (!path || !sep) {
+ return NULL;
+ }
+
+ result = calloc(1, sizeof(*result));
+ result->data = strndup(path, ARG_MAX);
+ result->data_len = strlen(result->data) + 2; // + 2 to handle an empty PATH
+ result->sep = strdup(sep);
+
+ // Split string by separator
+ elem = strtok(result->data, sep);
+ for (i = 0; elem != NULL; i++) {
+ result->part[i] = elem;
+ elem = strtok(NULL, sep);
+ }
+ result->part_nelem = i;
+
+ return result;
+}
+
+/**
+ * Filter elements in CleanPath->part array by pattern
+ * @param path a pointer to a CleanPath structure
+ * @param mode an integer CLEANPATH_FILTER_EXACT, CLEANPATH_FILTER_LOOSE, or CLEANPATH_FILTER_REGEX
+ * @param pattern a string
+ * @see cleanpath_init()
+ * @note CLEANPATH_FILTER_EXACT is the default mode
+ */
+void cleanpath_filter(struct CleanPath *path, unsigned mode, const char *pattern) {
+ int match;
+ regex_t regex;
+
+ for (size_t i = 0; path->part[i]; i++) {
+ match = 0;
+ switch(mode) {
+ case CLEANPATH_FILTER_LOOSE:
+ match = strstr(path->part[i], pattern) != NULL ? 1 : 0;
+ break;
+ case CLEANPATH_FILTER_REGEX:
+ if (regcomp(&regex, pattern, REG_EXTENDED | REG_NOSUB) != 0) {
+ return;
+ }
+ match = regexec(&regex, path->part[i], 0, NULL, 0) == 0 ? 1 : 0;
+ regfree(&regex);
+ break;
+ case CLEANPATH_FILTER_EXACT:
+ default:
+ match = strcmp(path->part[i], pattern) == 0 ? 1 : 0;
+ break;
+ }
+
+ if (match)
+ *path->part[i] = '\0';
+ }
+}
+
+/**
+ * Reconstruct CleanPath->data using CleanPath->part and return a copy of the string
+ * @param path a pointer to a CleanPath structure
+ * @see cleanpath_filter()
+ * @return a string
+ */
+char *cleanpath_read(struct CleanPath *path) {
+ char *result;
+
+ result = calloc(path->data_len, sizeof(char));
+ for (size_t i = 0; path->part[i] != NULL; i++) {
+ // Ignore filtered paths
+ if (*path->part[i] == '\0') {
+ continue;
+ }
+ strcat(result, path->part[i]);
+
+ // Do not append path separator on final element
+ if (path->part[i + 1] != NULL) {
+ strcat(result, path->sep);
+ }
+ }
+
+ return result;
+}
+
+/**
+ * Free memory allocated by cleanpath_init()
+ * @param path a pointer to a CleanPath structure
+ */
+void cleanpath_free(struct CleanPath *path) {
+ free(path->data);
+ free(path->sep);
+ free(path);
+}
+
diff --git a/src/main.c b/src/main.c
new file mode 100644
index 0000000..ba23fcc
--- /dev/null
+++ b/src/main.c
@@ -0,0 +1,167 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include "cleanpath.h"
+
+// Make argument parsing less wordy
+#define ARGM(X) strcmp(argv[i], X) == 0
+
+// Global program path
+static char *program;
+
+static int is_valid_arg(char **args, char *s) {
+ int match;
+
+ match = 0;
+
+ // cause positional arguments to fall through as valid
+ if (*s != '-') {
+ return 1;
+ }
+
+ for (size_t i = 0; args[i] != NULL; i++) {
+ if (strcmp(args[i], s) == 0) {
+ match = 1;
+ }
+ }
+
+ return match;
+}
+
+
+static char *getenv_ex(char *s) {
+ char *env_var;
+ env_var = getenv(s);
+ if (!env_var || !strlen(env_var)) {
+ return NULL;
+ }
+ return env_var;
+}
+
+static void show_version() {
+ puts(CLEANPATH_VERSION);
+}
+
+static void show_usage() {
+ char *bname;
+ bname = strrchr(program, '/');
+
+ printf("usage: %s [-hVelrsv] [pattern ...]\n", bname ? bname + 1 : program);
+ printf(" --help -h Displays this help message\n");
+ printf(" --version -V Displays the program's version\n");
+ printf(" --exact -e Filter when pattern is an exact match (default)\n");
+ printf(" --loose -l Filter when any part of the pattern matches\n");
+ printf(" --regex -r Filter matches with (Extended) Regular Expressions\n");
+ printf(" --sep [str] -s Use custom path separator (default: ':')\n");
+ printf(" --env [str] -E Use custom environment variable (default: PATH)\n");
+}
+
+int main(int argc, char *argv[]) {
+ struct CleanPath *path;
+ char *sep;
+ char *sys_var;
+ char *result;
+ size_t pattern_nelem;
+ int filter_mode;
+ int args_invalid;
+ char *pattern[CLEANPATH_PART_MAX];
+
+ program = argv[0];
+ result = NULL;
+ sys_var = NULL;
+ sep = CLEANPATH_SEP;
+ filter_mode = CLEANPATH_FILTER_NONE;
+ memset(pattern, 0, sizeof(pattern) / sizeof(char *));
+ pattern_nelem = 0;
+
+ args_invalid = 0;
+ char *args_valid[] = {
+ "--help", "-h",
+ "--version", "-V",
+ "--exact", "-e",
+ "--loose", "-l",
+ "--regex", "-r",
+ "--sep", "-s",
+ "--env", "-E",
+ NULL
+ };
+
+ for (int i = 1; i < argc; i++) {
+ if (!is_valid_arg(args_valid, argv[i])) {
+ fprintf(stderr, "Unknown option: %s\n", argv[i]);
+ args_invalid = 1;
+ continue;
+ }
+ if (args_invalid) {
+ exit(1);
+ }
+ if (ARGM("--help") || ARGM("-h")) {
+ show_usage();
+ exit(0);
+ }
+ if (ARGM("--version") || ARGM("-V")) {
+ show_version();
+ exit(0);
+ }
+ if (ARGM("--exact") || ARGM("-e")) {
+ filter_mode = CLEANPATH_FILTER_EXACT;
+ continue;
+ }
+ if (ARGM("--loose") || ARGM("-l")) {
+ filter_mode = CLEANPATH_FILTER_LOOSE;
+ continue;
+ }
+ if (ARGM("--regex") || ARGM("-r")) {
+ filter_mode = CLEANPATH_FILTER_REGEX;
+ continue;
+ }
+ if (ARGM("--sep") || ARGM("-s")) {
+ i++;
+ sep = argv[i];
+ continue;
+ }
+ if (ARGM("--env") || ARGM("-E")) {
+ i++;
+ sys_var = getenv_ex(argv[i]);
+ if (!sys_var) {
+ exit(1);
+ }
+ continue;
+ }
+
+ if (filter_mode == CLEANPATH_FILTER_NONE) {
+ filter_mode = CLEANPATH_FILTER_EXACT;
+ }
+
+ // Record filter patterns
+ pattern[pattern_nelem] = argv[i];
+ pattern_nelem++;
+ }
+
+ // Use default environment variable when not set by --var
+ if (sys_var == NULL) {
+ sys_var = getenv_ex(CLEANPATH_VAR);
+ }
+
+ // Initialize path data
+ path = cleanpath_init(sys_var, sep);
+ if (path == NULL) {
+ exit(1);
+ }
+
+ // Remove patterns from sys_var
+ for (size_t i = 0; pattern[i] != NULL; i++) {
+ cleanpath_filter(path, filter_mode, pattern[i]);
+ }
+
+ // Print filtered result
+ result = cleanpath_read(path);
+ printf("%s\n", result);
+
+ // Clean up
+ free(result);
+ cleanpath_free(path);
+
+ return 0;
+}