├── .dictionary ├── .github └── settings.yml ├── .gitignore ├── .gitsv └── config.yml ├── .golangci.yml ├── .markdownlint.yml ├── .prettierignore ├── .woodpecker ├── build-package.yml ├── docs.yml ├── notify.yml └── test.yml ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── cmd └── url-parser │ └── main.go ├── command ├── commands.go ├── commands_test.go ├── fragment.go ├── fragment_test.go ├── host.go ├── host_test.go ├── password.go ├── password_test.go ├── path.go ├── path_test.go ├── port.go ├── port_test.go ├── query.go ├── query_test.go ├── run.go ├── run_test.go ├── scheme.go ├── scheme_test.go ├── user.go └── user_test.go ├── config └── config.go ├── go.mod ├── go.sum └── renovate.json /.dictionary: -------------------------------------------------------------------------------- 1 | url-parser 2 | herloct 3 | multiarch 4 | (P|p)rebuilt 5 | -------------------------------------------------------------------------------- /.github/settings.yml: -------------------------------------------------------------------------------- 1 | repository: 2 | name: url-parser 3 | description: Simple command-line URL parser 4 | topics: cli, tools, url, parser 5 | 6 | private: false 7 | has_issues: true 8 | has_wiki: false 9 | has_downloads: true 10 | 11 | default_branch: main 12 | 13 | allow_squash_merge: true 14 | allow_merge_commit: true 15 | allow_rebase_merge: true 16 | 17 | labels: 18 | - name: bug 19 | color: d73a4a 20 | description: Something isn't working 21 | - name: documentation 22 | color: 0075ca 23 | description: Improvements or additions to documentation 24 | - name: duplicate 25 | color: cfd3d7 26 | description: This issue or pull request already exists 27 | - name: enhancement 28 | color: a2eeef 29 | description: New feature or request 30 | - name: good first issue 31 | color: 7057ff 32 | description: Good for newcomers 33 | - name: help wanted 34 | color: 008672 35 | description: Extra attention is needed 36 | - name: invalid 37 | color: e4e669 38 | description: This doesn't seem right 39 | - name: question 40 | color: d876e3 41 | description: Further information is requested 42 | - name: wontfix 43 | color: ffffff 44 | description: This will not be worked on 45 | 46 | branches: 47 | - name: main 48 | protection: 49 | required_pull_request_reviews: null 50 | required_status_checks: 51 | strict: false 52 | contexts: 53 | - ci/woodpecker/pr/test 54 | - ci/woodpecker/pr/build-package 55 | - ci/woodpecker/pr/docs 56 | enforce_admins: false 57 | required_linear_history: true 58 | restrictions: null 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /release 3 | /url-parser* 4 | 5 | coverage.out 6 | CHANGELOG.md 7 | -------------------------------------------------------------------------------- /.gitsv/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: "1.1" 3 | 4 | versioning: 5 | update-major: [] 6 | update-minor: [feat] 7 | update-patch: [fix, perf, refactor, chore, test, ci, docs] 8 | 9 | tag: 10 | pattern: "v%d.%d.%d" 11 | 12 | release-notes: 13 | sections: 14 | - name: Features 15 | commit-types: [feat] 16 | section-type: commits 17 | - name: Bug Fixes 18 | commit-types: [fix] 19 | section-type: commits 20 | - name: Performance Improvements 21 | commit-types: [perf] 22 | section-type: commits 23 | - name: Code Refactoring 24 | commit-types: [refactor] 25 | section-type: commits 26 | - name: Others 27 | commit-types: [chore] 28 | section-type: commits 29 | - name: Testing 30 | commit-types: [test] 31 | section-type: commits 32 | - name: CI Pipeline 33 | commit-types: [ci] 34 | section-type: commits 35 | - name: Documentation 36 | commit-types: [docs] 37 | section-type: commits 38 | - name: BREAKING CHANGES 39 | section-type: breaking-changes 40 | 41 | commit-message: 42 | footer: 43 | issue: 44 | key: issue 45 | add-value-prefix: "#" 46 | issue: 47 | regex: "#?[0-9]+" 48 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | run: 3 | timeout: 5m 4 | 5 | linters: 6 | default: none 7 | enable: 8 | - asasalint 9 | - asciicheck 10 | - bidichk 11 | - bodyclose 12 | - containedctx 13 | - contextcheck 14 | - copyloopvar 15 | - decorder 16 | - dogsled 17 | - dupl 18 | - dupword 19 | - durationcheck 20 | - err113 21 | - errcheck 22 | - errchkjson 23 | - errname 24 | - errorlint 25 | - exhaustive 26 | - forcetypeassert 27 | - ginkgolinter 28 | - gocheckcompilerdirectives 29 | - gochecknoglobals 30 | - gochecknoinits 31 | - gocognit 32 | - goconst 33 | - gocritic 34 | - gocyclo 35 | - godot 36 | - godox 37 | - goheader 38 | - gomoddirectives 39 | - gomodguard 40 | - goprintffuncname 41 | - gosec 42 | - govet 43 | - grouper 44 | - importas 45 | - ineffassign 46 | - interfacebloat 47 | - ireturn 48 | - lll 49 | - loggercheck 50 | - maintidx 51 | - makezero 52 | - misspell 53 | - mnd 54 | - musttag 55 | - nakedret 56 | - nestif 57 | - nilerr 58 | - nilnil 59 | - nlreturn 60 | - noctx 61 | - nolintlint 62 | - nonamedreturns 63 | - nosprintfhostport 64 | - prealloc 65 | - predeclared 66 | - promlinter 67 | - reassign 68 | - revive 69 | - staticcheck 70 | - tagliatelle 71 | - testableexamples 72 | - thelper 73 | - tparallel 74 | - unconvert 75 | - unparam 76 | - unused 77 | - usestdlibvars 78 | - usetesting 79 | - whitespace 80 | - wsl 81 | - zerologlint 82 | exclusions: 83 | generated: lax 84 | presets: 85 | - comments 86 | - common-false-positives 87 | - legacy 88 | - std-error-handling 89 | rules: 90 | - linters: 91 | - goconst 92 | path: (.+)_test.go 93 | paths: 94 | - third_party$ 95 | - builtin$ 96 | - examples$ 97 | 98 | formatters: 99 | enable: 100 | - gofmt 101 | - gofumpt 102 | - goimports 103 | settings: 104 | gofumpt: 105 | extra-rules: true 106 | exclusions: 107 | generated: lax 108 | paths: 109 | - third_party$ 110 | - builtin$ 111 | - examples$ 112 | -------------------------------------------------------------------------------- /.markdownlint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | default: True 3 | MD013: False 4 | MD041: False 5 | MD004: 6 | style: dash 7 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | *.tpl.md 2 | LICENSE 3 | -------------------------------------------------------------------------------- /.woodpecker/build-package.yml: -------------------------------------------------------------------------------- 1 | --- 2 | when: 3 | - event: [pull_request, tag] 4 | - event: [push, manual] 5 | branch: 6 | - ${CI_REPO_DEFAULT_BRANCH} 7 | 8 | steps: 9 | - name: build 10 | image: docker.io/techknowlogick/xgo:go-1.24.4 11 | commands: 12 | - ln -s $(pwd) /source 13 | - make release 14 | 15 | - name: executable 16 | image: quay.io/thegeeklab/alpine-tools 17 | commands: 18 | - $(find dist/ -executable -type f -iname ${CI_REPO_NAME}-linux-amd64) --help 19 | 20 | - name: changelog 21 | image: quay.io/thegeeklab/git-sv 22 | commands: 23 | - git sv current-version 24 | - git sv release-notes -t ${CI_COMMIT_TAG:-next} -o CHANGELOG.md 25 | - cat CHANGELOG.md 26 | 27 | - name: publish-github 28 | image: docker.io/plugins/github-release 29 | settings: 30 | api_key: 31 | from_secret: github_token 32 | files: 33 | - dist/* 34 | note: CHANGELOG.md 35 | overwrite: true 36 | title: ${CI_COMMIT_TAG} 37 | when: 38 | - event: [tag] 39 | 40 | depends_on: 41 | - test 42 | -------------------------------------------------------------------------------- /.woodpecker/docs.yml: -------------------------------------------------------------------------------- 1 | --- 2 | when: 3 | - event: [pull_request, tag] 4 | - event: [push, manual] 5 | branch: 6 | - ${CI_REPO_DEFAULT_BRANCH} 7 | 8 | steps: 9 | - name: markdownlint 10 | image: quay.io/thegeeklab/markdownlint-cli 11 | commands: 12 | - markdownlint 'README.md' 'CONTRIBUTING.md' 13 | 14 | - name: spellcheck 15 | image: quay.io/thegeeklab/alpine-tools 16 | commands: 17 | - spellchecker --files 'README.md' 'CONTRIBUTING.md' -d .dictionary -p spell indefinite-article syntax-urls 18 | environment: 19 | FORCE_COLOR: "true" 20 | 21 | depends_on: 22 | - build-package 23 | -------------------------------------------------------------------------------- /.woodpecker/notify.yml: -------------------------------------------------------------------------------- 1 | --- 2 | when: 3 | - event: [tag] 4 | - event: [push, manual] 5 | branch: 6 | - ${CI_REPO_DEFAULT_BRANCH} 7 | 8 | runs_on: [success, failure] 9 | 10 | steps: 11 | - name: matrix 12 | image: quay.io/thegeeklab/wp-matrix 13 | settings: 14 | homeserver: 15 | from_secret: matrix_homeserver 16 | room_id: 17 | from_secret: matrix_room_id 18 | user_id: 19 | from_secret: matrix_user_id 20 | access_token: 21 | from_secret: matrix_access_token 22 | when: 23 | - status: [success, failure] 24 | 25 | depends_on: 26 | - docs 27 | -------------------------------------------------------------------------------- /.woodpecker/test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | when: 3 | - event: [pull_request, tag] 4 | - event: [push, manual] 5 | branch: 6 | - ${CI_REPO_DEFAULT_BRANCH} 7 | 8 | steps: 9 | - name: lint 10 | image: docker.io/library/golang:1.24.4 11 | commands: 12 | - make lint 13 | 14 | - name: test 15 | image: docker.io/library/golang:1.24.4 16 | commands: 17 | - make test 18 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Security 4 | 5 | If you think you have found a **security issue**, please do not mention it in this repository. 6 | Instead, send an email to `security@thegeeklab.de` with as many details as possible so it can be handled confidential. 7 | 8 | ## Bug Reports and Feature Requests 9 | 10 | If you have found a **bug** or have a **feature request** please use the search first in case a similar issue already exists. 11 | If not, please create an issue in this repository 12 | 13 | ## Code 14 | 15 | If you would like to fix a bug or implement a feature, please fork the repository and create a Pull Request. 16 | 17 | Before you start any Pull Request, it is recommended that you create an issue to discuss first if you have any 18 | doubts about requirement or implementation. That way you can be sure that the maintainer(s) agree on what to change and how, 19 | and you can hopefully get a quick merge afterwards. 20 | 21 | Pull Requests can only be merged once all status checks are green. 22 | 23 | ## Do not force push to your Pull Request branch 24 | 25 | Please do not force push to your Pull Requests branch after you have created your Pull Request, as doing so makes it harder for us to review your work. 26 | Pull Requests will always be squashed by us when we merge your work. Commit as many times as you need in your Pull Request branch. 27 | 28 | ## Re-requesting a review 29 | 30 | Please do not ping your reviewer(s) by mentioning them in a new comment. Instead, use the re-request review functionality. 31 | Read more about this in the [GitHub docs, Re-requesting a review](https://docs.github.com/en/free-pro-team@latest/github/collaborating-with-issues-and-pull-requests/incorporating-feedback-in-your-pull-request#re-requesting-a-review). 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Robert Kaussow 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is furnished 10 | to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice (including the next 13 | paragraph) shall be included in all copies or substantial portions of the 14 | Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 18 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 19 | OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF 21 | OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # renovate: datasource=github-releases depName=mvdan/gofumpt 2 | GOFUMPT_PACKAGE_VERSION := v0.8.0 3 | # renovate: datasource=github-releases depName=golangci/golangci-lint 4 | GOLANGCI_LINT_PACKAGE_VERSION := v2.1.6 5 | # renovate: datasource=docker depName=docker.io/techknowlogick/xgo 6 | XGO_PACKAGE_VERSION := go-1.24.4 7 | 8 | EXECUTABLE := url-parser 9 | 10 | DIST := dist 11 | DIST_DIRS := $(DIST) 12 | IMPORT := github.com/thegeeklab/$(EXECUTABLE) 13 | 14 | GO ?= go 15 | CWD ?= $(shell pwd) 16 | PACKAGES ?= $(shell go list ./...) 17 | SOURCES ?= $(shell find . -name "*.go" -type f) 18 | 19 | GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@$(GOFUMPT_PACKAGE_VERSION) 20 | GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(GOLANGCI_LINT_PACKAGE_VERSION) 21 | XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest 22 | GOTESTSUM_PACKAGE ?= gotest.tools/gotestsum@latest 23 | 24 | GENERATE ?= 25 | XGO_TARGETS ?= linux/amd64,linux/arm-6,linux/arm-7,linux/arm64 26 | 27 | TARGETOS ?= linux 28 | TARGETARCH ?= amd64 29 | ifneq ("$(TARGETVARIANT)","") 30 | GOARM ?= $(subst v,,$(TARGETVARIANT)) 31 | endif 32 | TAGS ?= netgo 33 | 34 | ifndef VERSION 35 | ifneq ($(CI_COMMIT_TAG),) 36 | VERSION ?= $(subst v,,$(CI_COMMIT_TAG)) 37 | else 38 | VERSION ?= $(shell git rev-parse --short HEAD) 39 | endif 40 | endif 41 | 42 | ifndef DATE 43 | DATE := $(shell date -u +"%Y-%m-%dT%H:%M:%S%z") 44 | endif 45 | 46 | LDFLAGS += -s -w -X "main.BuildVersion=$(VERSION)" -X "main.BuildDate=$(DATE)" 47 | 48 | .PHONY: all 49 | all: clean build 50 | 51 | .PHONY: clean 52 | clean: 53 | $(GO) clean -i ./... 54 | rm -rf $(DIST_DIRS) 55 | 56 | .PHONY: fmt 57 | fmt: 58 | $(GO) run $(GOFUMPT_PACKAGE) -extra -w $(SOURCES) 59 | 60 | .PHONY: golangci-lint 61 | golangci-lint: 62 | $(GO) run $(GOLANGCI_LINT_PACKAGE) run 63 | 64 | .PHONY: lint 65 | lint: golangci-lint 66 | 67 | .PHONY: generate 68 | generate: 69 | $(GO) generate $(GENERATE) 70 | 71 | .PHONY: test 72 | test: 73 | $(GO) run $(GOTESTSUM_PACKAGE) --no-color=false -- -coverprofile=coverage.out $(PACKAGES) 74 | 75 | .PHONY: build 76 | build: $(DIST)/$(EXECUTABLE) 77 | 78 | $(DIST)/$(EXECUTABLE): $(SOURCES) 79 | GOOS=$(TARGETOS) GOARCH=$(TARGETARCH) GOARM=$(GOARM) $(GO) build -v -tags '$(TAGS)' -ldflags '-extldflags "-static" $(LDFLAGS)' -o $@ ./cmd/$(EXECUTABLE) 80 | 81 | $(DIST_DIRS): 82 | mkdir -p $(DIST_DIRS) 83 | 84 | .PHONY: xgo 85 | xgo: | $(DIST_DIRS) 86 | $(GO) run $(XGO_PACKAGE) -go $(XGO_PACKAGE_VERSION) -v -ldflags '-extldflags "-static" $(LDFLAGS)' -tags '$(TAGS)' -targets '$(XGO_TARGETS)' -out $(EXECUTABLE) --pkg cmd/$(EXECUTABLE) . 87 | cp /build/* $(CWD)/$(DIST) 88 | ls -l $(CWD)/$(DIST) 89 | 90 | .PHONY: checksum 91 | checksum: 92 | cd $(DIST); $(foreach file,$(wildcard $(DIST)/$(EXECUTABLE)-*),sha256sum $(notdir $(file)) > $(notdir $(file)).sha256;) 93 | ls -l $(CWD)/$(DIST) 94 | 95 | .PHONY: release 96 | release: xgo checksum 97 | 98 | .PHONY: deps 99 | deps: 100 | $(GO) mod download 101 | $(GO) install $(GOFUMPT_PACKAGE) 102 | $(GO) install $(GOLANGCI_LINT_PACKAGE) 103 | $(GO) install $(XGO_PACKAGE) 104 | $(GO) install $(GOTESTSUM_PACKAGE) 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # url-parser 2 | 3 | Simple command-line URL parser 4 | 5 | [![Build Status](https://ci.thegeeklab.de/api/badges/thegeeklab/url-parser/status.svg)](https://ci.thegeeklab.de/repos/thegeeklab/url-parser) 6 | [![Go Report Card](https://goreportcard.com/badge/github.com/thegeeklab/url-parser)](https://goreportcard.com/report/github.com/thegeeklab/url-parser) 7 | [![GitHub contributors](https://img.shields.io/github/contributors/thegeeklab/url-parser)](https://github.com/thegeeklab/url-parser/graphs/contributors) 8 | [![License: MIT](https://img.shields.io/github/license/thegeeklab/url-parser)](https://github.com/thegeeklab/url-parser/blob/main/LICENSE) 9 | 10 | Inspired by [herloct/url-parser](https://github.com/herloct/url-parser), a simple command-line utility for parsing URLs. 11 | 12 | ## Installation 13 | 14 | Prebuilt multiarch binaries are available for Linux only. 15 | 16 | ```Shell 17 | curl -SsfL https://github.com/thegeeklab/url-parser/releases/latest/download/url-parser-linux-amd64 -o /usr/local/bin/url-parser 18 | chmod +x /usr/local/bin/url-parser 19 | ``` 20 | 21 | ## Build 22 | 23 | Build the binary from source with the following command: 24 | 25 | ```Shell 26 | make build 27 | ``` 28 | 29 | ## Usage 30 | 31 | ```Shell 32 | $ url-parser --help 33 | NAME: 34 | url-parser - Parse URL and shows the part of it. 35 | 36 | USAGE: 37 | url-parser [global options] command [command options] 38 | 39 | VERSION: 40 | devel 41 | 42 | COMMANDS: 43 | all, a Get all parts from url 44 | scheme, s Get scheme from url 45 | user, u Get username from url 46 | password, pw Get password from url 47 | path, pt Get path from url 48 | host, ht Get hostname from url 49 | port, p Get port from url 50 | query, q Get query from url 51 | fragment, f Get fragment from url 52 | help, h Shows a list of commands or help for one command 53 | 54 | GLOBAL OPTIONS: 55 | --url value source url to parse [$URL_PARSER_URL] 56 | --help, -h show help 57 | --version, -v print the version 58 | ``` 59 | 60 | ## Examples 61 | 62 | ```Shell 63 | $ url-parser --url https://somedomain.com host 64 | somedomain.com 65 | 66 | $ url-parser --url https://herloct@somedomain.com user 67 | herloct 68 | 69 | $ url-parser --url https://somedomain.com/path/to path 70 | /path/to 71 | 72 | $ url-parser --url https://somedomain.com/path/to path --path-index=1 73 | to 74 | 75 | $ url-parser --url https://somedomain.com/?some-key=somevalue query 76 | some-key=somevalue 77 | 78 | $ url-parser --url https://somedomain.com/?some-key=somevalue query --query-field=some-key 79 | somevalue 80 | 81 | # It is also possible to read the URL from stdin 82 | $ echo "https://somedomain.com" | url-parser host 83 | somedomain.com 84 | 85 | # Get json output or all parsed parts 86 | $ url-parser --url https://somedomain.com/?some-key=somevalue all --json 87 | {"scheme":"https","hostname":"somedomain.com","port":"","path":"/","fragment":"","rawQuery":"some-key=somevalue","queryParams":[{"key":"some-key","value":"somevalue"}],"username":"","password":""} 88 | ``` 89 | 90 | ## Contributors 91 | 92 | Special thanks to all [contributors](https://github.com/thegeeklab/url-parser/graphs/contributors). If you would like to contribute, please see the [instructions](https://github.com/thegeeklab/url-parser/blob/main/CONTRIBUTING.md). 93 | 94 | ## License 95 | 96 | This project is licensed under the MIT License - see the [LICENSE](https://github.com/thegeeklab/url-parser/blob/main/LICENSE) file for details. 97 | -------------------------------------------------------------------------------- /cmd/url-parser/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "os" 8 | "strings" 9 | 10 | "github.com/rs/zerolog" 11 | "github.com/rs/zerolog/log" 12 | "github.com/thegeeklab/url-parser/command" 13 | "github.com/thegeeklab/url-parser/config" 14 | "github.com/urfave/cli/v3" 15 | ) 16 | 17 | //nolint:gochecknoglobals 18 | var ( 19 | BuildVersion = "devel" 20 | BuildDate = "00000000" 21 | ) 22 | 23 | func main() { 24 | log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) 25 | 26 | cli.VersionPrinter = func(c *cli.Command) { 27 | fmt.Printf("%s version=%s date=%s\n", c.Name, c.Version, BuildDate) 28 | } 29 | 30 | cfg := &config.Config{} 31 | 32 | app := &cli.Command{ 33 | Name: "url-parser", 34 | Usage: "Parse URL and shows the part of it.", 35 | Version: BuildVersion, 36 | Action: command.Run(cfg), 37 | Flags: []cli.Flag{ 38 | &cli.StringFlag{ 39 | Name: "url", 40 | Usage: "source url to parse", 41 | Sources: cli.EnvVars("URL_PARSER_URL"), 42 | Destination: &cfg.URL, 43 | }, 44 | }, 45 | Commands: []*cli.Command{ 46 | { 47 | Name: "all", 48 | Aliases: []string{"a"}, 49 | Usage: "Get all parts from url", 50 | Action: command.Run(cfg), 51 | Flags: command.AllFlags(cfg), 52 | }, 53 | { 54 | Name: "scheme", 55 | Aliases: []string{"s"}, 56 | Usage: "Get scheme from url", 57 | Action: command.Scheme(cfg), 58 | }, 59 | { 60 | Name: "user", 61 | Aliases: []string{"u"}, 62 | Usage: "Get username from url", 63 | Action: command.User(cfg), 64 | }, 65 | { 66 | Name: "password", 67 | Aliases: []string{"pw"}, 68 | Usage: "Get password from url", 69 | Action: command.Password(cfg), 70 | }, 71 | { 72 | Name: "path", 73 | Aliases: []string{"pt"}, 74 | Usage: "Get path from url", 75 | Action: command.Path(cfg), 76 | Flags: command.PathFlags(cfg), 77 | }, 78 | { 79 | Name: "host", 80 | Aliases: []string{"ht"}, 81 | Usage: "Get hostname from url", 82 | Action: command.Host(cfg), 83 | }, 84 | { 85 | Name: "port", 86 | Aliases: []string{"p"}, 87 | Usage: "Get port from url", 88 | Action: command.Port(cfg), 89 | }, 90 | { 91 | Name: "query", 92 | Aliases: []string{"q"}, 93 | Usage: "Get query from url", 94 | Action: command.Query(cfg), 95 | Flags: command.QueryFlags(cfg), 96 | }, 97 | { 98 | Name: "fragment", 99 | Aliases: []string{"f"}, 100 | Usage: "Get fragment from url", 101 | Action: command.Fragment(cfg), 102 | }, 103 | }, 104 | Before: func(ctx context.Context, _ *cli.Command) (context.Context, error) { 105 | if cfg.URL == "" { 106 | stat, _ := os.Stdin.Stat() 107 | if (stat.Mode() & os.ModeCharDevice) == 0 { 108 | stdin, err := io.ReadAll(os.Stdin) 109 | if err != nil { 110 | return ctx, fmt.Errorf("error: %w: %w", config.ErrReadStdin, err) 111 | } 112 | cfg.URL = strings.TrimSuffix(string(stdin), "\n") 113 | } 114 | } 115 | 116 | if cfg.URL == "" { 117 | return ctx, fmt.Errorf("error: %w", config.ErrEmptyURL) 118 | } 119 | 120 | return ctx, nil 121 | }, 122 | } 123 | 124 | if err := app.Run(context.Background(), os.Args); err != nil { 125 | log.Fatal().Err(err).Msg("Execution error") 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /command/commands.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "net/url" 5 | "strings" 6 | 7 | "github.com/rs/zerolog/log" 8 | "github.com/thegeeklab/url-parser/config" 9 | ) 10 | 11 | type QueryParam struct { 12 | Key string `json:"key"` 13 | Value string `json:"value"` 14 | } 15 | 16 | type URL struct { 17 | url *url.URL 18 | 19 | Scheme string `json:"scheme"` 20 | Hostname string `json:"hostname"` 21 | Port string `json:"port"` 22 | Path string `json:"path"` 23 | Fragment string `json:"fragment"` 24 | RawQuery string `json:"rawQuery"` 25 | Query string `json:"-"` 26 | QueryParams []QueryParam `json:"queryParams"` 27 | Username string `json:"username"` 28 | Password string `json:"password"` 29 | } 30 | 31 | func (u *URL) String() string { 32 | return u.url.String() 33 | } 34 | 35 | type Parser struct { 36 | URL string 37 | QueryField string 38 | QuerySplit bool 39 | } 40 | 41 | func NewURLParser(url, queryField string, querySplit bool) *Parser { 42 | return &Parser{ 43 | URL: url, 44 | QueryField: queryField, 45 | QuerySplit: querySplit, 46 | } 47 | } 48 | 49 | func (p *Parser) parse() *URL { 50 | urlString := strings.TrimSpace(p.URL) 51 | 52 | parts, err := url.Parse(urlString) 53 | if err != nil { 54 | log.Fatal().Err(err).Msg(config.ErrParseURL.Error()) 55 | } 56 | 57 | extURL := &URL{ 58 | url: parts, 59 | Scheme: parts.Scheme, 60 | Hostname: parts.Hostname(), 61 | Path: parts.Path, 62 | Fragment: parts.Fragment, 63 | QueryParams: []QueryParam{}, 64 | } 65 | 66 | if len(parts.Scheme) > 0 { 67 | extURL.Hostname = parts.Hostname() 68 | extURL.Port = parts.Port() 69 | } 70 | 71 | if parts.User != nil { 72 | if len(parts.User.Username()) > 0 { 73 | extURL.Username = parts.User.Username() 74 | } 75 | } 76 | 77 | if parts.User != nil { 78 | pw, _ := parts.User.Password() 79 | if len(pw) > 0 { 80 | extURL.Password = pw 81 | } 82 | } 83 | 84 | // Handle query field extraction 85 | if parts.RawQuery != "" { 86 | extURL.RawQuery = parts.RawQuery 87 | } 88 | 89 | if p.QueryField != "" { 90 | if result := parts.Query().Get(p.QueryField); result != "" { 91 | extURL.Query = result 92 | } 93 | } else { 94 | extURL.Query = parts.RawQuery 95 | } 96 | 97 | // Handle query parameter splitting 98 | values := parts.Query() 99 | for k, v := range values { 100 | if len(v) > 0 { 101 | extURL.QueryParams = append(extURL.QueryParams, QueryParam{ 102 | Key: k, 103 | Value: v[0], 104 | }) 105 | } 106 | } 107 | 108 | return extURL 109 | } 110 | -------------------------------------------------------------------------------- /command/commands_test.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/thegeeklab/url-parser/config" 8 | ) 9 | 10 | func TestParse(t *testing.T) { 11 | urlString := "postgres://user:pass@host.com:5432/path/to?key=value&other=other%20value#some-fragment" 12 | 13 | tests := []struct { 14 | name string 15 | config *config.Config 16 | expected *URL 17 | }{ 18 | { 19 | name: "parse url", 20 | config: &config.Config{ 21 | URL: urlString, 22 | QuerySplit: true, 23 | }, 24 | expected: &URL{ 25 | Scheme: "postgres", 26 | Username: "user", 27 | Password: "pass", 28 | Hostname: "host.com", 29 | Port: "5432", 30 | Path: "/path/to", 31 | Query: "key=value&other=other%20value", 32 | RawQuery: "key=value&other=other%20value", 33 | QueryParams: []QueryParam{ 34 | { 35 | Key: "key", 36 | Value: "value", 37 | }, 38 | { 39 | Key: "other", 40 | Value: "other value", 41 | }, 42 | }, 43 | Fragment: "some-fragment", 44 | }, 45 | }, 46 | } 47 | 48 | for _, tt := range tests { 49 | t.Run(tt.name, func(t *testing.T) { 50 | result := NewURLParser(urlString, "", false).parse() 51 | assert.Equal(t, tt.expected.Scheme, result.Scheme) 52 | assert.Equal(t, tt.expected.Username, result.Username) 53 | assert.Equal(t, tt.expected.Password, result.Password) 54 | assert.Equal(t, tt.expected.Hostname, result.Hostname) 55 | assert.Equal(t, tt.expected.Port, result.Port) 56 | assert.Equal(t, tt.expected.Path, result.Path) 57 | assert.Equal(t, tt.expected.Fragment, result.Fragment) 58 | assert.Equal(t, tt.expected.RawQuery, result.RawQuery) 59 | assert.Equal(t, tt.expected.Query, result.Query) 60 | assert.ElementsMatch(t, tt.expected.QueryParams, result.QueryParams) 61 | }) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /command/fragment.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/thegeeklab/url-parser/config" 8 | "github.com/urfave/cli/v3" 9 | ) 10 | 11 | // Fragment prints out the fragment part from the url. 12 | func Fragment(cfg *config.Config) cli.ActionFunc { 13 | return func(_ context.Context, _ *cli.Command) error { 14 | parts := NewURLParser(cfg.URL, cfg.QueryField, cfg.QuerySplit).parse() 15 | 16 | if len(parts.Scheme) > 0 { 17 | fmt.Println(parts.Fragment) 18 | } 19 | 20 | return nil 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /command/fragment_test.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/thegeeklab/url-parser/config" 9 | "github.com/urfave/cli/v3" 10 | "github.com/zenizh/go-capturer" 11 | ) 12 | 13 | func TestFragment(t *testing.T) { 14 | urlString := "postgres://user:pass@host.com:5432/path/to?key=value&other=other%20value#some-fragment" 15 | 16 | tests := []struct { 17 | name string 18 | config *config.Config 19 | expected string 20 | }{ 21 | { 22 | name: "get fragment", 23 | config: &config.Config{URL: urlString}, 24 | expected: "some-fragment", 25 | }, 26 | } 27 | 28 | for _, tt := range tests { 29 | app := &cli.Command{} 30 | 31 | t.Run(tt.name, func(t *testing.T) { 32 | result := strings.TrimSpace(capturer.CaptureStdout(func() { _ = Fragment(tt.config)(t.Context(), app) })) 33 | assert.Equal(t, tt.expected, result) 34 | }) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /command/host.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/thegeeklab/url-parser/config" 8 | "github.com/urfave/cli/v3" 9 | ) 10 | 11 | // Host prints out the host part from the url. 12 | func Host(cfg *config.Config) cli.ActionFunc { 13 | return func(_ context.Context, _ *cli.Command) error { 14 | parts := NewURLParser(cfg.URL, cfg.QueryField, cfg.QuerySplit).parse() 15 | 16 | if len(parts.Scheme) > 0 { 17 | fmt.Println(parts.Hostname) 18 | } 19 | 20 | return nil 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /command/host_test.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/thegeeklab/url-parser/config" 9 | "github.com/urfave/cli/v3" 10 | "github.com/zenizh/go-capturer" 11 | ) 12 | 13 | func TestHost(t *testing.T) { 14 | urlString := "postgres://user:pass@host.com:5432/path/to?key=value&other=other%20value#some-fragment" 15 | 16 | tests := []struct { 17 | name string 18 | config *config.Config 19 | expected string 20 | }{ 21 | { 22 | name: "get host", 23 | config: &config.Config{URL: urlString}, 24 | expected: "host.com", 25 | }, 26 | } 27 | 28 | for _, tt := range tests { 29 | app := &cli.Command{} 30 | 31 | t.Run(tt.name, func(t *testing.T) { 32 | result := strings.TrimSpace(capturer.CaptureStdout(func() { _ = Host(tt.config)(t.Context(), app) })) 33 | assert.Equal(t, tt.expected, result) 34 | }) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /command/password.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/thegeeklab/url-parser/config" 8 | "github.com/urfave/cli/v3" 9 | ) 10 | 11 | // Password prints out the password part from url. 12 | func Password(cfg *config.Config) cli.ActionFunc { 13 | return func(_ context.Context, _ *cli.Command) error { 14 | parts := NewURLParser(cfg.URL, cfg.QueryField, cfg.QuerySplit).parse() 15 | 16 | if parts.Password != "" { 17 | fmt.Println(parts.Password) 18 | } 19 | 20 | return nil 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /command/password_test.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/thegeeklab/url-parser/config" 9 | "github.com/urfave/cli/v3" 10 | "github.com/zenizh/go-capturer" 11 | ) 12 | 13 | func TestPassword(t *testing.T) { 14 | urlString := "postgres://user:pass@host.com:5432/path/to?key=value&other=other%20value#some-fragment" 15 | 16 | tests := []struct { 17 | name string 18 | config *config.Config 19 | expected string 20 | }{ 21 | { 22 | name: "get password", 23 | config: &config.Config{URL: urlString}, 24 | expected: "pass", 25 | }, 26 | } 27 | 28 | for _, tt := range tests { 29 | app := &cli.Command{} 30 | 31 | t.Run(tt.name, func(t *testing.T) { 32 | result := strings.TrimSpace(capturer.CaptureStdout(func() { _ = Password(tt.config)(t.Context(), app) })) 33 | assert.Equal(t, tt.expected, result) 34 | }) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /command/path.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/thegeeklab/url-parser/config" 9 | "github.com/urfave/cli/v3" 10 | ) 11 | 12 | // PathFlags defines flags for path subcommand. 13 | func PathFlags(cfg *config.Config) []cli.Flag { 14 | return []cli.Flag{ 15 | &cli.IntFlag{ 16 | Name: "path-index", 17 | Usage: "filter parsed path by index", 18 | Sources: cli.EnvVars("URL_PARSER_PATH_INDEX"), 19 | Value: -1, 20 | Destination: &cfg.PathIndex, 21 | }, 22 | } 23 | } 24 | 25 | // Path prints out the path part from url. 26 | func Path(cfg *config.Config) cli.ActionFunc { 27 | return func(_ context.Context, _ *cli.Command) error { 28 | parts := NewURLParser(cfg.URL, cfg.QueryField, cfg.QuerySplit).parse() 29 | i := cfg.PathIndex 30 | 31 | if len(parts.Path) > 0 { 32 | if i > -1 { 33 | path := strings.Split(parts.Path, "/") 34 | 35 | if i++; i < len(path) { 36 | fmt.Println(path[i]) 37 | } 38 | } else { 39 | fmt.Println(parts.Path) 40 | } 41 | } 42 | 43 | return nil 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /command/path_test.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/thegeeklab/url-parser/config" 9 | "github.com/urfave/cli/v3" 10 | "github.com/zenizh/go-capturer" 11 | ) 12 | 13 | func TestPath(t *testing.T) { 14 | urlString := "postgres://user:pass@host.com:5432/path/to?key=value&other=other%20value#some-fragment" 15 | 16 | tests := []struct { 17 | name string 18 | config *config.Config 19 | expected string 20 | }{ 21 | { 22 | name: "get path", 23 | config: &config.Config{URL: urlString, PathIndex: -1}, 24 | expected: "/path/to", 25 | }, 26 | { 27 | name: "get path at index", 28 | config: &config.Config{URL: urlString, PathIndex: 0}, 29 | expected: "path", 30 | }, 31 | } 32 | 33 | for _, tt := range tests { 34 | app := &cli.Command{} 35 | 36 | t.Run(tt.name, func(t *testing.T) { 37 | result := strings.TrimSpace(capturer.CaptureStdout(func() { _ = Path(tt.config)(t.Context(), app) })) 38 | assert.Equal(t, tt.expected, result) 39 | }) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /command/port.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/thegeeklab/url-parser/config" 8 | "github.com/urfave/cli/v3" 9 | ) 10 | 11 | // Port prints out the port from the url. 12 | func Port(cfg *config.Config) cli.ActionFunc { 13 | return func(_ context.Context, _ *cli.Command) error { 14 | parts := NewURLParser(cfg.URL, cfg.QueryField, cfg.QuerySplit).parse() 15 | 16 | if len(parts.Scheme) > 0 { 17 | fmt.Println(parts.Port) 18 | } 19 | 20 | return nil 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /command/port_test.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/thegeeklab/url-parser/config" 9 | "github.com/urfave/cli/v3" 10 | "github.com/zenizh/go-capturer" 11 | ) 12 | 13 | func TestPort(t *testing.T) { 14 | urlString := "postgres://user:pass@host.com:5432/path/to?key=value&other=other%20value#some-fragment" 15 | 16 | tests := []struct { 17 | name string 18 | config *config.Config 19 | expected string 20 | }{ 21 | { 22 | name: "get port", 23 | config: &config.Config{URL: urlString}, 24 | expected: "5432", 25 | }, 26 | } 27 | 28 | for _, tt := range tests { 29 | app := &cli.Command{} 30 | 31 | t.Run(tt.name, func(t *testing.T) { 32 | result := strings.TrimSpace(capturer.CaptureStdout(func() { _ = Port(tt.config)(t.Context(), app) })) 33 | assert.Equal(t, tt.expected, result) 34 | }) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /command/query.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/thegeeklab/url-parser/config" 8 | "github.com/urfave/cli/v3" 9 | ) 10 | 11 | // QueryFlags defines flags for query subcommand. 12 | func QueryFlags(cfg *config.Config) []cli.Flag { 13 | return []cli.Flag{ 14 | &cli.StringFlag{ 15 | Name: "query-field", 16 | Usage: "filter parsed query string by field name", 17 | Sources: cli.EnvVars("URL_PARSER_QUERY_FIELD"), 18 | Destination: &cfg.QueryField, 19 | }, 20 | } 21 | } 22 | 23 | // Query prints out the query part from url. 24 | func Query(cfg *config.Config) cli.ActionFunc { 25 | return func(_ context.Context, _ *cli.Command) error { 26 | parts := NewURLParser(cfg.URL, cfg.QueryField, cfg.QuerySplit).parse() 27 | 28 | if parts.Query != "" { 29 | fmt.Println(parts.Query) 30 | } 31 | 32 | return nil 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /command/query_test.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/thegeeklab/url-parser/config" 9 | "github.com/urfave/cli/v3" 10 | "github.com/zenizh/go-capturer" 11 | ) 12 | 13 | func TestQuery(t *testing.T) { 14 | urlString := "postgres://user:pass@host.com:5432/path/to?key=value&other=other%20value#some-fragment" 15 | 16 | tests := []struct { 17 | name string 18 | config *config.Config 19 | QueryField string 20 | expected string 21 | }{ 22 | { 23 | name: "get query", 24 | config: &config.Config{URL: urlString}, 25 | expected: "key=value&other=other%20value", 26 | }, 27 | { 28 | name: "get query field", 29 | config: &config.Config{URL: urlString, QueryField: "other"}, 30 | expected: "other value", 31 | }, 32 | } 33 | 34 | for _, tt := range tests { 35 | app := &cli.Command{} 36 | 37 | t.Run(tt.name, func(t *testing.T) { 38 | result := strings.TrimSpace(capturer.CaptureStdout(func() { _ = Query(tt.config)(t.Context(), app) })) 39 | assert.Equal(t, tt.expected, result) 40 | }) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /command/run.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | 8 | "github.com/thegeeklab/url-parser/config" 9 | "github.com/urfave/cli/v3" 10 | ) 11 | 12 | // Run default command and print out full url. 13 | func Run(cfg *config.Config) cli.ActionFunc { 14 | return func(_ context.Context, _ *cli.Command) error { 15 | parts := NewURLParser(cfg.URL, cfg.QueryField, cfg.QuerySplit).parse() 16 | 17 | if len(parts.String()) > 0 { 18 | if cfg.JSONOutput { 19 | json, _ := json.Marshal(parts) 20 | fmt.Println(string(json)) 21 | } else { 22 | fmt.Println(parts) 23 | } 24 | } 25 | 26 | return nil 27 | } 28 | } 29 | 30 | // AllFlags defines flags for all subcommand. 31 | func AllFlags(cfg *config.Config) []cli.Flag { 32 | return []cli.Flag{ 33 | &cli.BoolFlag{ 34 | Name: "json", 35 | Usage: "output json", 36 | Sources: cli.EnvVars("URL_PARSER_JSON"), 37 | Destination: &cfg.JSONOutput, 38 | }, 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /command/run_test.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "encoding/json" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/thegeeklab/url-parser/config" 10 | "github.com/urfave/cli/v3" 11 | "github.com/zenizh/go-capturer" 12 | ) 13 | 14 | func TestRun(t *testing.T) { 15 | urlString := "postgres://user:pass@host.com:5432/path/to?key=value&other=other%20value#some-fragment" 16 | 17 | tests := []struct { 18 | name string 19 | config *config.Config 20 | expected string 21 | }{ 22 | { 23 | name: "get url", 24 | config: &config.Config{URL: urlString}, 25 | expected: urlString, 26 | }, 27 | { 28 | name: "get url with query split", 29 | config: &config.Config{ 30 | URL: urlString, 31 | QuerySplit: true, 32 | JSONOutput: true, 33 | }, 34 | expected: `{ 35 | "scheme": "postgres", 36 | "hostname": "host.com", 37 | "port": "5432", 38 | "path": "/path/to", 39 | "fragment": "some-fragment", 40 | "rawQuery": "key=value&other=other%20value", 41 | "queryParams": [ 42 | { 43 | "key": "key", 44 | "value": "value" 45 | }, 46 | { 47 | "key": "other", 48 | "value": "other value" 49 | } 50 | ], 51 | "username": "user", 52 | "password": "pass" 53 | }`, 54 | }, 55 | } 56 | 57 | for _, tt := range tests { 58 | app := &cli.Command{} 59 | 60 | t.Run(tt.name, func(t *testing.T) { 61 | result := strings.TrimSpace(capturer.CaptureStdout(func() { _ = Run(tt.config)(t.Context(), app) })) 62 | 63 | if tt.config.JSONOutput { 64 | got := &URL{} 65 | expected := &URL{} 66 | 67 | _ = json.Unmarshal([]byte(result), &got) 68 | _ = json.Unmarshal([]byte(tt.expected), &expected) 69 | 70 | assert.Equal(t, expected.Scheme, got.Scheme) 71 | assert.Equal(t, expected.Username, got.Username) 72 | assert.Equal(t, expected.Password, got.Password) 73 | assert.Equal(t, expected.Hostname, got.Hostname) 74 | assert.Equal(t, expected.Port, got.Port) 75 | assert.Equal(t, expected.Path, got.Path) 76 | assert.Equal(t, expected.Fragment, got.Fragment) 77 | assert.Equal(t, expected.RawQuery, got.RawQuery) 78 | assert.Equal(t, expected.Query, got.Query) 79 | assert.ElementsMatch(t, expected.QueryParams, got.QueryParams) 80 | 81 | return 82 | } 83 | 84 | assert.Equal(t, tt.expected, result) 85 | }) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /command/scheme.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/thegeeklab/url-parser/config" 8 | "github.com/urfave/cli/v3" 9 | ) 10 | 11 | // Scheme prints out the scheme part from the url. 12 | func Scheme(cfg *config.Config) cli.ActionFunc { 13 | return func(_ context.Context, _ *cli.Command) error { 14 | parts := NewURLParser(cfg.URL, cfg.QueryField, cfg.QuerySplit).parse() 15 | 16 | if len(parts.Scheme) > 0 { 17 | fmt.Println(parts.Scheme) 18 | } 19 | 20 | return nil 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /command/scheme_test.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/thegeeklab/url-parser/config" 9 | "github.com/urfave/cli/v3" 10 | "github.com/zenizh/go-capturer" 11 | ) 12 | 13 | func TestScheme(t *testing.T) { 14 | urlString := "postgres://user:pass@host.com:5432/path/to?key=value&other=other%20value#some-fragment" 15 | 16 | tests := []struct { 17 | name string 18 | config *config.Config 19 | expected string 20 | }{ 21 | { 22 | name: "get scheme", 23 | config: &config.Config{URL: urlString}, 24 | expected: "postgres", 25 | }, 26 | } 27 | 28 | for _, tt := range tests { 29 | app := &cli.Command{} 30 | 31 | t.Run(tt.name, func(t *testing.T) { 32 | result := strings.TrimSpace(capturer.CaptureStdout(func() { _ = Scheme(tt.config)(t.Context(), app) })) 33 | assert.Equal(t, tt.expected, result) 34 | }) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /command/user.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/thegeeklab/url-parser/config" 8 | "github.com/urfave/cli/v3" 9 | ) 10 | 11 | // User prints out the user part from url. 12 | func User(cfg *config.Config) cli.ActionFunc { 13 | return func(_ context.Context, _ *cli.Command) error { 14 | parts := NewURLParser(cfg.URL, cfg.QueryField, cfg.QuerySplit).parse() 15 | 16 | if parts.Username != "" { 17 | fmt.Println(parts.Username) 18 | } 19 | 20 | return nil 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /command/user_test.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/thegeeklab/url-parser/config" 9 | "github.com/urfave/cli/v3" 10 | "github.com/zenizh/go-capturer" 11 | ) 12 | 13 | func TestUser(t *testing.T) { 14 | urlString := "postgres://user:pass@host.com:5432/path/to?key=value&other=other%20value#some-fragment" 15 | 16 | tests := []struct { 17 | name string 18 | config *config.Config 19 | expected string 20 | }{ 21 | { 22 | name: "get user", 23 | config: &config.Config{URL: urlString}, 24 | expected: "user", 25 | }, 26 | } 27 | 28 | for _, tt := range tests { 29 | app := &cli.Command{} 30 | 31 | t.Run(tt.name, func(t *testing.T) { 32 | result := strings.TrimSpace(capturer.CaptureStdout(func() { _ = User(tt.config)(t.Context(), app) })) 33 | assert.Equal(t, tt.expected, result) 34 | }) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrEmptyURL = errors.New("no url provided either by \"url\" or \"stdin\"") 7 | ErrReadStdin = errors.New("failed to read \"stdin\"") 8 | ErrParseURL = errors.New("failed to parse url") 9 | ) 10 | 11 | type Config struct { 12 | URL string 13 | QueryField string 14 | QuerySplit bool 15 | PathIndex int 16 | JSONOutput bool 17 | } 18 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/thegeeklab/url-parser 2 | 3 | go 1.24.4 4 | 5 | require ( 6 | github.com/rs/zerolog v1.34.0 7 | github.com/stretchr/testify v1.10.0 8 | github.com/urfave/cli/v3 v3.3.3 9 | github.com/zenizh/go-capturer v0.0.0-20211219060012-52ea6c8fed04 10 | ) 11 | 12 | require ( 13 | github.com/davecgh/go-spew v1.1.1 // indirect 14 | github.com/mattn/go-colorable v0.1.13 // indirect 15 | github.com/mattn/go-isatty v0.0.19 // indirect 16 | github.com/pmezard/go-difflib v1.0.0 // indirect 17 | golang.org/x/sys v0.12.0 // indirect 18 | gopkg.in/yaml.v3 v3.0.1 // indirect 19 | ) 20 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 5 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 6 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 7 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 8 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= 9 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 10 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 11 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 12 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 13 | github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= 14 | github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= 15 | github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= 16 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 17 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 18 | github.com/urfave/cli/v3 v3.3.3 h1:byCBaVdIXuLPIDm5CYZRVG6NvT7tv1ECqdU4YzlEa3I= 19 | github.com/urfave/cli/v3 v3.3.3/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo= 20 | github.com/zenizh/go-capturer v0.0.0-20211219060012-52ea6c8fed04 h1:qXafrlZL1WsJW5OokjraLLRURHiw0OzKHD/RNdspp4w= 21 | github.com/zenizh/go-capturer v0.0.0-20211219060012-52ea6c8fed04/go.mod h1:FiwNQxz6hGoNFBC4nIx+CxZhI3nne5RmIOlT/MXcSD4= 22 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 23 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 24 | golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= 25 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 26 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 27 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 28 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 29 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 30 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["github>thegeeklab/renovate-presets:golang"] 4 | } 5 | --------------------------------------------------------------------------------