diff options
author | Joe Hunkeler <jhunk@roden.local.stsci.edu> | 2021-05-04 17:34:38 -0400 |
---|---|---|
committer | Joseph Hunkeler <jhunkeler@gmail.com> | 2021-05-04 21:14:16 -0400 |
commit | ea6e2613aca23c84373e686d739498969062f79e (patch) | |
tree | ba9cf39409056cad0b65a4ee953856230e73196b | |
download | cleanpath-ea6e2613aca23c84373e686d739498969062f79e.tar.gz |
Initial commit
-rw-r--r-- | .gitignore | 8 | ||||
-rw-r--r-- | CMakeLists.txt | 33 | ||||
-rw-r--r-- | LICENSE | 27 | ||||
-rw-r--r-- | README.md | 71 | ||||
-rw-r--r-- | docs/man/cleanpath.1 | 116 | ||||
-rw-r--r-- | include/cleanpath.h | 27 | ||||
-rw-r--r-- | lib/cleanpath.c | 110 | ||||
-rw-r--r-- | src/main.c | 167 |
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 @@ -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(®ex, pattern, REG_EXTENDED | REG_NOSUB) != 0) { + return; + } + match = regexec(®ex, path->part[i], 0, NULL, 0) == 0 ? 1 : 0; + regfree(®ex); + 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; +} |