From d4a272edf46ad64b5bb9f1d1305f471d1f15ecfa Mon Sep 17 00:00:00 2001 From: Thomas Voss Date: Fri, 8 Oct 2021 23:46:05 +0200 Subject: [Meta] Initial commit --- LICENSE | 14 ++++++ README.rst | 101 +++++++++++++++++++++++++++++++++++++ getgopt.go | 156 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 3 ++ tests/run.sh | 48 ++++++++++++++++++ tests/tests.go | 23 +++++++++ 6 files changed, 345 insertions(+) create mode 100644 LICENSE create mode 100644 README.rst create mode 100644 getgopt.go create mode 100644 go.mod create mode 100755 tests/run.sh create mode 100644 tests/tests.go 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 \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) + } + } +} -- cgit v1.2.3