aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThomas Voss <mail@thomasvoss.com> 2021-10-08 23:46:05 +0200
committerThomas Voss <mail@thomasvoss.com> 2021-10-08 23:46:05 +0200
commitd4a272edf46ad64b5bb9f1d1305f471d1f15ecfa (patch)
tree8fe297cbb9091eecac286833a375bfcd88850b5d
[Meta] Initial commit
-rw-r--r--LICENSE14
-rw-r--r--README.rst101
-rw-r--r--getgopt.go156
-rw-r--r--go.mod3
-rwxr-xr-xtests/run.sh48
-rw-r--r--tests/tests.go23
6 files changed, 345 insertions, 0 deletions
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..562eb47
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,14 @@
+BSD Zero Clause License
+
+Copyright (c) 2021 Thomas Voss
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+PERFORMANCE OF THIS SOFTWARE.
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..e57c5ec
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,101 @@
+.. vi: tw=100
+
+Getgopt
+=======
+
+**Getgopt** is an extremely very simple implementation of POSIX C's ``getopt(3)`` function for
+golang. The entire library consists of 4 exported global variables, 1 exported function, and less
+than 100 lines of code (``sed -e '/^$/d' -e '/^\s*\*/d' -e '/^\s*\/\*/d' getgopt.go | wc -l``). I
+wrote this library because I didn't like how all the other alternatives for normal command line
+parsing were so overly complex. Go is all about keeping things simple, so let's keep flag parsing
+simple.
+
+
+Usage
+=====
+
+There is only 1 function for you to use, and that is `getgopt.Getopt()`. The function works *almost*
+the same way that the POSIX C ``getopt`` function works. Here is an example of it's usage:
+
+.. code-block:: go
+
+ package main
+
+ import (
+ "fmt"
+ "os"
+
+ "github.com/Mango0x45/getgopt"
+ )
+
+ func main() {
+ for opt := byte(0); getgopt.Getopt(len(os.Args), os.Args, ":a:bcd", &opt); {
+ switch opt {
+ case 'a':
+ fmt.Printf("Parsed the -a flag with the argument '%s'\n",
+ getgopt.Optarg)
+ case 'b':
+ fmt.Println("Parsed the -b flag")
+ case 'c':
+ /* ... */
+ case 'd':
+ /* ... */
+ case '?':
+ fmt.Fprintf(os.Stderr, "Invalid flag '%c', read the manpage\n",
+ getgopt.Optopt)
+ os.Exit(1)
+ case ':':
+ fmt.Fprintf(os.Stderr, "The flag '%c' requires an argument\n",
+ getgopt.Optopt)
+ os.Exit(1)
+ }
+ }
+
+ fmt.Printf("The first non-option argument is '%s'\n", os.Args[getgopt.Optind])
+ }
+
+After parsing a flag the ``Getopt()`` function returns true if there are still more flags to parse,
+or false if there are none more. This means that we can use it in a ``for`` or ``while`` loop to
+iterate over all of our arguments. As its arguments, the ``Getopt()`` function takes (in this
+order), the count of command line arguments, the command line arguments, an *optstring*, and a
+pointer to a byte where the parsed flag will be stored. After parsing a flag, the byte that was
+passed as the last parameter will either have the value of the flag or one of ``':'`` and ``'?'``.
+The value of ``opt`` is set to ``'?'`` if the user attempted to pass a flag that was not specified
+by the given *optstring*. If the user specifies a flag that requires an argument without actually
+passing an argument, then ``opt`` will be set to ``':'`` if the first character in the *optstring*
+is ``':'`` and otherwise it will be ``'?'``.
+
+The *optstring* is a string passed as the 3rd argument to ``Getopt()`` which specified which flags
+you want to be able to handle. Each flag you want to handle is given as a single character in the
+string in any order. For example if you want to support the ``-a``, ``-b``, and ``-x`` flags you can
+do::
+
+ getgopt.Getopt(len(os.Args), os.Args, "abx", &opt)
+ /* or */
+ getgopt.Getopt(len(os.Args), os.Args, "bxa", &opt)
+
+If you want a flag to take an argument, you should suffix the character with a ``':'``. So using the
+above example, if we want the ``-b`` flag to take an argument, we could write::
+
+ getgopt.Getopt(len(os.Args), os.Args, "ab:x", &opt)
+
+Finally, by default the ``Getopt()`` function will print diagnostic error messages to standard
+output when the user fails to provide an argument to a flag that expects one or passes an invalid
+flag. If you would like to not have these diagnostics printed you can either prefix the optstring
+with ``':'`` or you can set the ``Opterr`` global variable to ``false``. Both of the following are
+equivalent::
+
+ getgopt.Getopt(len(os.Args), os.Args, ":ab:x", &opt)
+ /* or */
+ getgopt.Opterr = false
+ getgopt.Getopt(len(os.Args), os.Args, "ab:x", &opt)
+
+There is a *slight* difference in behavior though which was explained above.
+
+Finally, there are 3 other global variables you can access, these are ``Optarg``, ``Optind``, and
+``Optopt``. When you parse a flag which requires an argument, that argument can be found as a string
+in the ``Optarg`` variable. ``Optind`` is a variable which during the parsing of the flags holds the
+index of command line argument being parsed. After the flags are parsed though it holds the index of
+the first non-option argument in the provided argument list. ``Optopt`` functions similarly to the
+byte you pass as the functions final argument, but it holds the flag which caused the last parsing
+error.
diff --git a/getgopt.go b/getgopt.go
new file mode 100644
index 0000000..166430a
--- /dev/null
+++ b/getgopt.go
@@ -0,0 +1,156 @@
+/*
+ * BSD Zero Clause License
+ *
+ * Copyright (c) 2021 Thomas Voss
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+ * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+ * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+ * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+ * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+ * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+ * PERFORMANCE OF THIS SOFTWARE.
+ */
+
+package getgopt
+
+import (
+ "fmt"
+ "os"
+)
+
+const (
+ errNoArg = "option requires an argument -- %c\n"
+ errBadArg = "unknown option -- %c\n"
+)
+
+var (
+ opt = 1
+ optlen int
+ parsed bool
+ opts [255]option
+)
+
+var (
+ /* The argument to the matched flag */
+ Optarg string
+ /* If true, print error messages */
+ Opterr = true
+ /* During parsing this is the current index into os.Args being parsed. After parsing this is
+ * the index to the first non-option element of os.Args.
+ */
+ Optind = 1
+ /* The flag that caused the last parsing error */
+ Optopt byte
+)
+
+type option struct {
+ arg bool
+ used bool
+}
+
+func parseArgs(optstring string) {
+ length := len(optstring)
+
+ if (length > 0) && (optstring[0] == ':') {
+ Opterr = false
+ }
+
+ for i, c := range optstring {
+ if c != ':' {
+ opts[c].used = true
+ opts[c].arg = ((i < length-1) && (optstring[i+1] == ':'))
+ }
+ }
+}
+
+/* A function to parse command line flags. This function takes as it's arguments from first to last,
+ * the count of command line arguments, the array of command line arguments (os.Args), an option
+ * string, and a pointer to a byte where the current flag can be stored. When called the current
+ * flag will be stored in `optptr` and the global variables `Optarg`, `Opterr`, `Optind`, and
+ * `Optopt` may be set.
+ *
+ * If there are still more arguments to be parsed, the function will return true. Otherwise false is
+ * returned. This makes it very easy to incorperate into a for/while loop.
+ */
+func Getopt(argc int, argv []string, optstring string, optptr *byte) bool {
+ /* If we havem't parsed the optstring yet, parse it */
+ if !parsed {
+ parseArgs(optstring)
+ parsed = true
+ }
+
+ /* Instantly return false if the follow cases are met */
+ if Optind >= argc || argv[Optind] == "" || argv[Optind][0] != '-' ||
+ argv[Optind] == "-" || optstring == "" {
+ return false
+ } else if argv[Optind] == "--" {
+ Optind++
+ return false
+ }
+
+ /* For each element of argv we calculate its length */
+ if opt == 1 {
+ optlen = len(argv[Optind])
+ }
+
+ /* The current flag */
+ currFlag := argv[Optind][opt]
+
+ if opts[currFlag].used {
+ if opts[currFlag].arg {
+ if opt == optlen-1 {
+ Optind += 2
+ if Optind > argc {
+ Optopt = currFlag
+ if Opterr {
+ *optptr = '?'
+ fmt.Fprintf(os.Stderr, errNoArg, Optopt)
+ } else {
+ *optptr = ':'
+ }
+ } else {
+ Optarg = argv[Optind-1]
+ *optptr = currFlag
+ }
+ opt = 1
+ return true
+ }
+
+ /* If the opt takes an argument but it's not the last character in the
+ * string
+ */
+ *optptr = currFlag
+ Optarg = string(argv[Optind][opt+1:])
+ Optind++
+ opt = 1
+ } else { /* If the opt doesn't take an argument */
+ if opt == optlen-1 {
+ opt = 1
+ Optind++
+ } else {
+ opt++
+ }
+
+ *optptr = currFlag
+ }
+ } else { /* If the arg isn't in optstring */
+ if Opterr {
+ fmt.Fprintf(os.Stderr, errBadArg, argv[Optind][opt])
+ }
+ Optopt = currFlag
+ *optptr = '?'
+
+ if opt == optlen-1 {
+ Optind++
+ opt = 1
+ } else {
+ opt++
+ }
+ }
+
+ return true
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..e96dbf6
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,3 @@
+module github.com/Mango0x45/getgopt
+
+go 1.17
diff --git a/tests/run.sh b/tests/run.sh
new file mode 100755
index 0000000..509acf0
--- /dev/null
+++ b/tests/run.sh
@@ -0,0 +1,48 @@
+#!/usr/bin/env sh
+
+compare()
+{
+ [ "$2" = "$3" ] && printf "\033[38;5;10mSuccess:\033[39;49m %s\n" "$1" ||
+ printf "\033[38;5;9mFail:\033[39;49m %s <expected \`\`%s'' but got \`\`%s''>\n" \
+ "$1" "$2" "$3"
+}
+
+cd "${0%/*}"
+
+trap 'rm -f tests tests2 tests2.go' EXIT
+go build tests.go
+
+compare "no args" "" "$(./tests)"
+compare "-a with valid arg" "Valid flag 'a' with arg 'testy'" "$(./tests -a testy test)"
+compare "-a with no arg" "Valid flag 'a' with no arg" "$(./tests -a)"
+compare "-a with valid arg and no space" "Valid flag 'a' with arg 'testy'" "$(./tests -atesty test)"
+compare "-x with no args" "Valid flag 'x'" "$(./tests -x)"
+compare "-x with args" "Valid flag 'x'" "$(./tests -x testy test)"
+compare "-x and -a with args" "Valid flag 'x'
+Valid flag 'a' with arg 'testy test'" "$(./tests -x -a 'testy test')"
+compare "-xa with args" "Valid flag 'x'
+Valid flag 'a' with arg 'testy test'" "$(./tests -xa 'testy test')"
+compare "-ax with args" "Valid flag 'a' with arg 'x'" "$(./tests -ax 'testy test')"
+compare "-ax with args" "Valid flag 'a' with arg 'x'" "$(./tests -ax 'testy test')"
+compare "-x after --" "" "$(./tests -- -x)"
+compare "-a with args after --" "" "$(./tests -- -a testy test)"
+compare "-a with args then -x after --" "Valid flag 'a' with arg 'testy'" \
+ "$(./tests -a testy test -- -x)"
+compare "-a with args then -x after empty string" "Valid flag 'a' with arg 'testy'" \
+ "$(./tests -a testy test '' -x)"
+compare "-x chained 3 times" "Valid flag 'x'
+Valid flag 'x'
+Valid flag 'x'" "$(./tests -xxx)"
+compare "-x as arg to -a" "Valid flag 'a' with arg '-x'" "$(./tests -a -x)"
+compare "invalid flag -b" "Invalid flag 'b'" "$(./tests -b)"
+compare "invalid flag -b with args" "Invalid flag 'b'" "$(./tests -b testy test)"
+compare "-x after non option arg" "" "$(./tests testy -x)"
+compare "-x after -" "" "$(./tests testy - -x)"
+
+sed '/Getopt(/s/:a:x/a:x/' tests.go >tests2.go
+go build tests2.go
+
+compare "-a with no arg and optstring[0] != ':'" "option requires an argument -- a
+Invalid flag 'a'" "$(2>&1 ./tests2 -a)"
+compare "invalid flag -b and optstring[0] != ':'" "unknown option -- b
+Invalid flag 'b'" "$(2>&1 ./tests2 -b)"
diff --git a/tests/tests.go b/tests/tests.go
new file mode 100644
index 0000000..47aef19
--- /dev/null
+++ b/tests/tests.go
@@ -0,0 +1,23 @@
+package main
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/Mango0x45/getgopt"
+)
+
+func main() {
+ for opt := byte(0); getgopt.Getopt(len(os.Args), os.Args, ":a:x", &opt); {
+ switch opt {
+ case 'a':
+ fmt.Printf("Valid flag 'a' with arg '%s'\n", getgopt.Optarg)
+ case 'x':
+ fmt.Println("Valid flag 'x'")
+ case ':':
+ fmt.Printf("Valid flag '%c' with no arg\n", getgopt.Optopt)
+ case '?':
+ fmt.Printf("Invalid flag '%c'\n", getgopt.Optopt)
+ }
+ }
+}