diff --git a/README.md b/README.md index 770574d..fb51350 100644 --- a/README.md +++ b/README.md @@ -30,11 +30,14 @@ Go-based tooling to manipulate (e.g., normalize/decode) Microsoft Office 365 - [`usl`](#usl) - [Flags](#flags) - [Positional Argument](#positional-argument) + - [Standard input (e.g., "piping")](#standard-input-eg-piping) - [Just Hit Enter](#just-hit-enter) - [Examples](#examples) - - [Using positional argument](#using-positional-argument) - - [Using flag](#using-flag) + - [Using url positional argument](#using-url-positional-argument) + - [Using url flag](#using-url-flag) - [Using input prompt](#using-input-prompt) + - [Using standard input (e.g., "piping")](#using-standard-input-eg-piping) + - [Using filename flag](#using-filename-flag) - [Verbose output](#verbose-output) - [License](#license) - [References](#references) @@ -58,8 +61,13 @@ This repo is intended to provide various tools used to monitor processes. Small CLI tool for decoding a given Safe Links URL. -- Specify Safe Links URL via CLI argument or flag - +- Specify single Safe Links URL + - via positional argument + - via flag + - via interactive prompt +- Specify multiple Safe Links URLs + - via standard input ("piping") + - via file (using flag) - Optional verbose listing of query parameter values within a given Safe Links URL. @@ -167,15 +175,16 @@ binaries. ##### Flags -| Flag | Required | Default | Repeat | Possible | Description | -| -------------- | -------- | ------- | ------ | -------------- | ----------------------------------------------------------------------------- | -| `h`, `help` | No | `false` | No | `h`, `help` | Show Help text along with the list of supported flags. | -| `version` | No | `false` | No | `version` | Whether to display application version and then immediately exit application. | -| `v`, `verbose` | No | `false` | No | `v`, `verbose` | Display additional information about a given Safe Links URL. | -| `u`, `url` | *maybe* | | No | `u`, `url` | Safe Links URL to decode | +| Flag | Required | Default | Repeat | Possible | Description | +| ---------------- | -------- | ------- | ------ | -------------------- | ----------------------------------------------------------------------------- | +| `h`, `help` | No | `false` | No | `h`, `help` | Show Help text along with the list of supported flags. | +| `version` | No | `false` | No | `version` | Whether to display application version and then immediately exit application. | +| `v`, `verbose` | No | `false` | No | `v`, `verbose` | Display additional information about a given Safe Links URL. | +| `u`, `url` | *maybe* | | No | `u`, `url` | Safe Links URL to decode | +| `f`, `inputfile` | *maybe* | | No | *valid path to file* | Path to file containing Safe Links URL to decode | -NOTE: If the `url` flag is not specified a prompt is provided to enter a Safe -Links URL. +NOTE: If an input `url` is not specified (e.g., via flag, positional argument +or standard input) a prompt is provided to enter a Safe Links URL. ##### Positional Argument @@ -184,34 +193,37 @@ or `url` flag. It is recommended that you quote the URL pattern to help prevent some of the characters from being interpreted as shell commands (e.g., `&` as an attempt to background a command). +##### Standard input (e.g., "piping") + +One or more URL patterns can be provided by piping them to the `usl` tool. + +An attempt is made to decode all input URLs (no early exit). Successful +decoding results are emitted to `stdout` with decoding failures emitted to +`stderr`. This allows for splitting success results and error output across +different files (e.g., for later review). + ##### Just Hit Enter -The `usl` tool can also be called without any flags or positional argument. In -this scenario it will prompt you to insert/paste the URL pattern (quoted or -otherwise). +The `usl` tool can also be called without any input (e.g., flags, positional +argument, standard input). In this scenario it will prompt you to insert/paste +the URL pattern (quoted or otherwise). ## Examples Though probably not required for all terminals, we quote the Safe Links URL to prevent unintended interpretation of characters in the URL. -### Using positional argument +### Using url positional argument ```console -$ ./usl 'SafeLinksURLHere' - -Original URL: - +$ usl 'SafeLinksURLHere' https://go.dev/dl/ ``` -### Using flag +### Using url flag ```console -$ ./usl --url 'SafeLinksURLHere' - -Original URL: - +$ usl --url 'SafeLinksURLHere' https://go.dev/dl/ ``` @@ -221,18 +233,38 @@ In this example we just press enter so that we will be prompted for the input URL pattern. ```console -$ ./usl +$ usl Enter URL: SafeLinksURLHere +https://go.dev/dl/ +``` -Original URL: +### Using standard input (e.g., "piping") +```console +$ cat file.with.links | usl +https://go.dev/dl/ +http://example.com +http://example.net +``` + +```console +$ echo 'SafeLinksURLHere' | usl +https://go.dev/dl/ +``` + +### Using filename flag + +```console +$ usl --filename file.with.links https://go.dev/dl/ +http://example.com +http://example.net ``` ### Verbose output ```console -$ ./usl --verbose --url 'SafeLinksURLHere' +$ usl --verbose --url 'SafeLinksURLHere' Expanded values from the given link: diff --git a/cmd/usl/config.go b/cmd/usl/config.go index 23d2fee..e30e19a 100644 --- a/cmd/usl/config.go +++ b/cmd/usl/config.go @@ -26,9 +26,10 @@ const ( // Config represents configuration details for this application. type Config struct { - URL string - Verbose bool - Version bool + URL string + Filename string + Verbose bool + Version bool } // NewConfig processes flag values and returns an application configuration. @@ -56,6 +57,8 @@ func usage() { func setupFlags(c *Config) { flag.StringVar(&c.URL, "url", "", "Safe Links URL to decode") flag.StringVar(&c.URL, "u", "", "Safe Links URL to decode"+" (shorthand)") + flag.StringVar(&c.Filename, "inputfile", "", "Path to file containing Safe Links URLs to decode") + flag.StringVar(&c.Filename, "f", "", "Path to file containing Safe Links URL to decode"+" (shorthand)") flag.BoolVar(&c.Verbose, "verbose", false, "Display additional information about a given Safe Links URL") flag.BoolVar(&c.Verbose, "v", false, "Display additional information about a given Safe Links URL"+" (shorthand)") flag.BoolVar(&c.Version, "version", false, "Display version information and immediately exit") diff --git a/cmd/usl/main.go b/cmd/usl/main.go index 059fa16..2262acc 100644 --- a/cmd/usl/main.go +++ b/cmd/usl/main.go @@ -11,7 +11,6 @@ package main import ( "fmt" - "net/url" "os" ) @@ -24,29 +23,38 @@ func main() { os.Exit(0) } - inputURL, err := processInputAsURL(cfg.URL) - if err != nil { - fmt.Printf("Failed to parse input as URL: %v\n", err) - os.Exit(1) - } + var inputURLs []string - safelink, err := url.Parse(inputURL) - if err != nil { - fmt.Printf("Failed to parse URL: %v\n", err) - os.Exit(1) - } + switch { + case cfg.Filename != "": + f, err := os.Open(cfg.Filename) + if err != nil { + fmt.Printf("Failed to open %q: %v\n", cfg.Filename, err) + os.Exit(1) + } - if err := assertValidURLParameter(safelink); err != nil { - fmt.Printf("Invalid Safelinks URL: %v\n", err) - os.Exit(1) - } + input, err := readURLsFromFile(f) + if err != nil { + fmt.Printf("Failed to read URLs from %q: %v\n", cfg.Filename, err) + os.Exit(1) + } - switch { - case cfg.Verbose: - verboseOutput(safelink, os.Stdout) + inputURLs = input default: - simpleOutput(safelink, os.Stdout) + input, err := processInputAsURL(cfg.URL) + if err != nil { + fmt.Printf("Failed to parse input as URL: %v\n", err) + os.Exit(1) + } + + inputURLs = input } + hasErr := processInputURLs(inputURLs, os.Stdout, os.Stderr, cfg.Verbose) + + // Ensure unsuccessful error code if one encountered. + if hasErr { + os.Exit(1) + } } diff --git a/cmd/usl/output.go b/cmd/usl/output.go index 1008965..b8f4a6a 100644 --- a/cmd/usl/output.go +++ b/cmd/usl/output.go @@ -14,13 +14,25 @@ import ( "sort" ) +// emitOutput emits a given URL to the specified output sink. If specified, +// verbose output is used. +func emitOutput(u *url.URL, w io.Writer, verbose bool) { + switch { + case verbose: + verboseOutput(u, w) + + default: + simpleOutput(u, w) + } +} + // simpleOutput handles generating reduced or "simple" output when verbose // mode is not invoked. func simpleOutput(u *url.URL, w io.Writer) { urlValues := u.Query() maskedURL := urlValues.Get("url") - fmt.Fprintf(w, "\nOriginal URL:\n\n%v\n", maskedURL) + fmt.Fprintln(w, maskedURL) } // verboseOutput handles generating extended or "verbose" output when diff --git a/cmd/usl/url.go b/cmd/usl/url.go index fbb68d9..05365b3 100644 --- a/cmd/usl/url.go +++ b/cmd/usl/url.go @@ -12,6 +12,7 @@ import ( "flag" "fmt" "html" + "io" "net/url" "os" "strings" @@ -34,40 +35,99 @@ func readURLFromUser() (string, error) { return scanner.Text(), scanner.Err() } -// processInputAsURL processes a given input string as a URL value. If not -// provided, this function will attempt to read the input URL from the first -// positional argument. The URL is unescaped and quoting removed. -func processInputAsURL(inputURL string) (string, error) { +// readURLsFromFile attempts to read URL patterns from a given file +// (io.Reader). +// +// The collection of input URLs is returned or an error if one occurs. +func readURLsFromFile(r io.Reader) ([]string, error) { + var inputURLs []string + + // Loop over input "reader" and attempt to collect each item. + scanner := bufio.NewScanner((r)) + for scanner.Scan() { + txt := scanner.Text() + + if strings.TrimSpace(txt) == "" { + continue + } + + inputURLs = append(inputURLs, txt) + } + + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("error reading URLs: %w", err) + } + + if len(inputURLs) == 0 { + return nil, ErrInvalidURL + } + + return inputURLs, nil +} + +// processInputAsURL processes a given input string as a URL value. This +// input string represents a single URL given via CLI flag. +// +// If an input string is not provided, this function will attempt to read +// input URLs from stdin. Each input URL is unescaped and quoting removed. +// +// The collection of input URLs is returned or an error if one occurs. +func processInputAsURL(inputURL string) ([]string, error) { + var inputURLs []string + + // https://stackoverflow.com/questions/22744443/check-if-there-is-something-to-read-on-stdin-in-golang + // https://stackoverflow.com/a/26567513/903870 + // stat, _ := os.Stdin.Stat() + // if (stat.Mode() & os.ModeCharDevice) == 0 { + // fmt.Println("data is being piped to stdin") + // } else { + // fmt.Println("stdin is from a terminal") + // } + + stat, _ := os.Stdin.Stat() + switch { - // We received a URL via positional argument. + // We received one or more URLs via standard input. + case (stat.Mode() & os.ModeCharDevice) == 0: + // fmt.Fprintln(os.Stderr, "Received URL via standard input") + return readURLsFromFile(os.Stdin) + + // We received a URL via positional argument. We ignore all but the first + // one. case len(flag.Args()) > 0: + // fmt.Fprintln(os.Stderr, "Received URL via positional argument") if strings.TrimSpace(flag.Args()[0]) == "" { - return "", ErrInvalidURL + return nil, ErrInvalidURL } - inputURL = cleanURL(flag.Args()[0]) + inputURLs = append(inputURLs, cleanURL(flag.Args()[0])) // We received a URL via flag. case inputURL != "": - inputURL = cleanURL(inputURL) + // fmt.Fprintln(os.Stderr, "Received URL via flag") + + inputURLs = append(inputURLs, cleanURL(inputURL)) // Input URL not given via positional argument, not given via flag either. + // We prompt the user for a single input value. default: + // fmt.Fprintln(os.Stderr, "default switch case triggered") + input, err := readURLFromUser() if err != nil { - return "", fmt.Errorf("error reading URL: %w", err) + return nil, fmt.Errorf("error reading URL: %w", err) } if strings.TrimSpace(input) == "" { - return "", ErrInvalidURL + return nil, ErrInvalidURL } - inputURL = cleanURL(input) + inputURLs = append(inputURLs, cleanURL(input)) } - return inputURL, nil + return inputURLs, nil } // cleanURL strips away quoting or escaping of characters in a given URL. @@ -96,3 +156,36 @@ func assertValidURLParameter(u *url.URL) error { return nil } + +// processInputURLs processes a given collection of input URL strings and +// emits successful decoding results to the specified results output sink. +// Errors are emitted to the specified error output sink if encountered but +// bulk processing continues until all input URLs have been evaluated. +// +// If requested decoded URLs are emitted in verbose format. +// +// A boolean value is returned indicating whether any errors occurred. +func processInputURLs(inputURLs []string, okOut io.Writer, errOut io.Writer, verbose bool) bool { + var errEncountered bool + + for _, inputURL := range inputURLs { + safelink, err := url.Parse(inputURL) + if err != nil { + fmt.Printf("Failed to parse URL: %v\n", err) + + errEncountered = true + continue + } + + if err := assertValidURLParameter(safelink); err != nil { + fmt.Fprintf(errOut, "Invalid Safelinks URL %q: %v\n", safelink, err) + + errEncountered = true + continue + } + + emitOutput(safelink, okOut, verbose) + } + + return errEncountered +}