From 4b8ecbbb96092cfb642c746b64d2972f66b4b86f Mon Sep 17 00:00:00 2001 From: Peter Pentchev Date: Mon, 14 Jan 2019 12:38:52 +0200 Subject: Import confget_2.2.0.orig.tar.xz [dgit import orig confget_2.2.0.orig.tar.xz] --- .depend | 5 + CHANGES | 145 ++++++++ Makefile | 144 ++++++++ README | 20 ++ TODO | 7 + confget.1 | 262 ++++++++++++++ confget.c | 704 +++++++++++++++++++++++++++++++++++++ confget.h | 63 ++++ confget_common.c | 152 ++++++++ confget_common.h | 36 ++ confget_http_get.c | 158 +++++++++ confget_http_get.h | 32 ++ confget_ini.c | 203 +++++++++++ confget_ini.h | 32 ++ makedep.sh | 6 + python/README.md | 81 +++++ python/confget/__init__.py | 107 ++++++ python/confget/__main__.py | 295 ++++++++++++++++ python/confget/backend/__init__.py | 42 +++ python/confget/backend/abstract.py | 55 +++ python/confget/backend/http_get.py | 110 ++++++ python/confget/backend/ini.py | 176 ++++++++++ python/confget/defs.py | 59 ++++ python/confget/format.py | 221 ++++++++++++ python/confget/py.typed | 0 python/setup.cfg | 2 + python/setup.py | 132 +++++++ python/stubs/urllib/__init__.py | 0 python/stubs/urllib/__init__.pyi | 27 ++ python/stubs/urllib/parse.pyi | 27 ++ python/stubs/urllib/py.typed | 0 python/tox.ini | 143 ++++++++ python/unit_tests/__init__.py | 0 python/unit_tests/data/__init__.py | 31 ++ python/unit_tests/data/defs.py | 295 ++++++++++++++++ python/unit_tests/data/load.py | 106 ++++++ python/unit_tests/data/util.py | 45 +++ python/unit_tests/test_run.py | 136 +++++++ t/01-get-values.t | 90 +++++ t/02-check-values.t | 81 +++++ t/03-list-all.t | 57 +++ t/04-bespoke-test-manpage.t | 39 ++ t/05-match-names.t | 48 +++ t/06-get-default.t | 56 +++ t/07-match-values.t | 63 ++++ t/08-bespoke-shell-quote.t | 49 +++ t/09-bespoke-regexp.t | 59 ++++ t/10-bespoke-qsections.t | 74 ++++ t/11-no-default.t | 71 ++++ t/12-last-value.t | 78 ++++ t/13-bespoke-features.t | 110 ++++++ t/14-bespoke-too-many.t | 69 ++++ t/defs/schema/test-1.0.json | 176 ++++++++++ t/defs/tests/01-get-values.json | 293 +++++++++++++++ t/defs/tests/02-check-values.json | 258 ++++++++++++++ t/defs/tests/03-list-all.json | 118 +++++++ t/defs/tests/05-match-names.json | 81 +++++ t/defs/tests/06-get-default.json | 87 +++++ t/defs/tests/07-match-values.json | 166 +++++++++ t/defs/tests/11-no-default.json | 173 +++++++++ t/defs/tests/12-last-value.json | 239 +++++++++++++ t/defs/tools/encode.py | 105 ++++++ t/defs/tools/generate.py | 146 ++++++++ t/t1.ini | 44 +++ t/t2.ini | 25 ++ t/t3.ini | 13 + test-fgetln.c | 50 +++ test-getline.c | 50 +++ 68 files changed, 7027 insertions(+) create mode 100644 .depend create mode 100644 CHANGES create mode 100644 Makefile create mode 100644 README create mode 100644 TODO create mode 100644 confget.1 create mode 100644 confget.c create mode 100644 confget.h create mode 100644 confget_common.c create mode 100644 confget_common.h create mode 100644 confget_http_get.c create mode 100644 confget_http_get.h create mode 100644 confget_ini.c create mode 100644 confget_ini.h create mode 100644 makedep.sh create mode 100644 python/README.md create mode 100644 python/confget/__init__.py create mode 100755 python/confget/__main__.py create mode 100644 python/confget/backend/__init__.py create mode 100644 python/confget/backend/abstract.py create mode 100644 python/confget/backend/http_get.py create mode 100644 python/confget/backend/ini.py create mode 100644 python/confget/defs.py create mode 100644 python/confget/format.py create mode 100644 python/confget/py.typed create mode 100644 python/setup.cfg create mode 100644 python/setup.py create mode 100644 python/stubs/urllib/__init__.py create mode 100644 python/stubs/urllib/__init__.pyi create mode 100644 python/stubs/urllib/parse.pyi create mode 100644 python/stubs/urllib/py.typed create mode 100644 python/tox.ini create mode 100644 python/unit_tests/__init__.py create mode 100644 python/unit_tests/data/__init__.py create mode 100644 python/unit_tests/data/defs.py create mode 100644 python/unit_tests/data/load.py create mode 100644 python/unit_tests/data/util.py create mode 100644 python/unit_tests/test_run.py create mode 100644 t/01-get-values.t create mode 100644 t/02-check-values.t create mode 100644 t/03-list-all.t create mode 100644 t/04-bespoke-test-manpage.t create mode 100644 t/05-match-names.t create mode 100644 t/06-get-default.t create mode 100644 t/07-match-values.t create mode 100644 t/08-bespoke-shell-quote.t create mode 100644 t/09-bespoke-regexp.t create mode 100644 t/10-bespoke-qsections.t create mode 100644 t/11-no-default.t create mode 100644 t/12-last-value.t create mode 100644 t/13-bespoke-features.t create mode 100644 t/14-bespoke-too-many.t create mode 100644 t/defs/schema/test-1.0.json create mode 100644 t/defs/tests/01-get-values.json create mode 100644 t/defs/tests/02-check-values.json create mode 100644 t/defs/tests/03-list-all.json create mode 100644 t/defs/tests/05-match-names.json create mode 100644 t/defs/tests/06-get-default.json create mode 100644 t/defs/tests/07-match-values.json create mode 100644 t/defs/tests/11-no-default.json create mode 100644 t/defs/tests/12-last-value.json create mode 100644 t/defs/tools/encode.py create mode 100755 t/defs/tools/generate.py create mode 100644 t/t1.ini create mode 100644 t/t2.ini create mode 100644 t/t3.ini create mode 100644 test-fgetln.c create mode 100644 test-getline.c diff --git a/.depend b/.depend new file mode 100644 index 0000000..406c06f --- /dev/null +++ b/.depend @@ -0,0 +1,5 @@ +confget.o: confget.c confget.h confget_http_get.h confget_ini.h +confget_common.o: confget_common.c confget.h confget_common.h readaline.h +confget_http_get.o: confget_http_get.c confget.h confget_common.h \ + confget_http_get.h +confget_ini.o: confget_ini.c confget.h confget_common.h confget_ini.h diff --git a/CHANGES b/CHANGES new file mode 100644 index 0000000..5211665 --- /dev/null +++ b/CHANGES @@ -0,0 +1,145 @@ +Change log for confget, the configuration file variable extractor + +2.2.0 2019/01/13 + - add a Python implementation: a confget library that may + also be invoked from the command line with the same + inteface as the C confget tool + - generate the TAP tests automatically from JSON definitions + +2.1.1 2018/11/27 + - fix the check for more than one requested query type + +2.1.0 2017/11/11 + - allow the installation commands to be overridden, e.g. for + package builds which do not require root privileges + - add "-q features" and "-q feature NAME" with only the "BASE" + feature defined so far + - support "--help" and "--version" + +2.0.0 2016/04/03 + + INCOMPATIBLE CHANGES: + - if a variable is specified more than once in the config file, + only the *last* value will be displayed regardless of the presence or + the order of the confget command-line options; previously, + confget without -l would only display the first value + - the configuration file is always parsed completely and any syntax + errors will cause confget to fail even if they occur after + the specification of the requested variables + - confget now requires a C99 compiler to build. This is activated + by the -std=c99 option passed to the compiler; to override this, + set the STD_CFLAGS environment variable + + Other changes: + - replace _BSD_SOURCE and _GNU_SOURCE with _POSIX_C_SOURCE and + _XOPEN_SOURCE + - install the test scripts themselves in addition to the test + config files as examples + - teach the manual page test about compressed manpage files + - use linker flags for Large File Support, too, if provided + - treat an empty section name (-s '') as requesting only variables + declared before any section has been defined + - remove an outdated -f option specification in the HTTP GET + example in the manual page + - clean up some of the use of the strcmp(3) function + - clean up some internal error handling + - remove the obsolete $Ringlet VCS tags from the source files + - minimize the scope of some variables by using C99 declarations + - add the -O (override) command-line option allowing variables in + a certain section to override those in the unnamed section at + the start of the config file + +1.05 2013/11/03 + - display the version string before the help if both requested + - support building without a .depend file + - use the C99 '%z' printf(3) format specifier and remove + the less portable 'long long' casts + - add 'const' qualifiers to lots of pointers + - make makedep.sh honor CC and quote some expansions there + - build with large file support under Linux by defining + _FILE_OFFSET_BITS to be 64 + +1.04 2012/10/11 + - detect automatically the "read-a-line" C library function + (getline(3) or fgetln(3)) by trying to compile and link simple + programs using either one + - separate the C preprocessor flags from the C compiler flags: + - honor CPPFLAGS if passed by a build system + - rename the PCRE_CFLAGS variable to PCRE_CPPFLAGS, since that + is what it actually is + - pass CPPFLAGS instead of CFLAGS to makedep.sh and use them + - stop misusing LFLAGS, use just LDFLAGS as we should + - add the "-q sections" command-line option to list the names of + the sections in an INI file + - add the 10-qsections test for the above + +1.03 2011/07/06 + - support spaces in INI file section names, as suggested by + green in Debian bug #632400 + - do not make the compiler warnings fatal by default + +1.02 2009/03/20 + - do not fail the regexp tests if confget was compiled without + regular expression support + - explain the Config::IniFiles cross-reference in the manual + page, as discussed with George Danchev on the debian-mentors + mailing list + - split the install target into separate targets for the various + components of the program + - add some comments to the t/t1.ini and t/t2.ini files, describing + the structure of the INI files parsed by confget + - install the t1.ini and t2.ini files as examples, as discussed + with George Danchev on the debian-mentors mailing list + +1.01 2008/11/06 + - if '-' is specified for the configuration file name, read from + the standard input stream + - improve the general description of confget in the manual page + - add an 'http_get' backend for decoding HTTP GET request variables + - make the filename argument non-mandatory, since there are + configuration types (e.g. http_get) that do not read their data + from a file + - completely drop the non-functional stub for a Java properties + backend type + +1.00 2008/10/16 + - add the '-p prefix' and '-P postfix' command-line options + - add the '-S' command-line flag so shell scripts may safely + read individual variables or slurp the contents of whole sections + - add regular expression support through the PCRE library and + the '-x' command-line flag + +0.03 2008/10/14 + - avoid overlong strings in confget.c's usage() function + - add sample high-warning-level compiler flags to the Makefile + - protect argument names in the function declarations + - do not use a generic name such as "fp" for a global variable + - clear out the config file variable after closing the file + - pull in the correct definition for strdup() + +0.02 2008/10/14 + - by default, use the binary files' group for the manpages, too + - add support for different configuration file types + - ini - the already-supported INI file backend + - add the '-t type' and '-T' command-line options + - add a trivial README file + - add a simple TODO list + - automatically track source dependencies + - use fgetln(3) or getline(3) to read lines from a file + - move more variable matching logic into foundvar() + - fix a bug when parsing key/value lines without whitespace + - add the -L command-line option to display all variables with + names matching the specified pattern + - mark some Makefile targets as phony + - allow more than one variable or pattern to be specified on + the command line + - note that either -DHAVE_GETLINE or -DHAVE_FGETLN must be defined + in the C compiler's flags at build time + - make the section argument optional and let the first section in + the INI file be used instead + - add the -m command-line option to match the values against a pattern + +0.01 2008/09/25 + - Initial public release. + +Comments: Peter Pentchev diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..93c998e --- /dev/null +++ b/Makefile @@ -0,0 +1,144 @@ +# Copyright (c) 2008, 2009, 2011 - 2013, 2016, 2017 Peter Pentchev +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. 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. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. + +LOCALBASE?= /usr/local +PREFIX?= ${LOCALBASE} +BINDIR?= ${PREFIX}/bin +MANDIR?= ${PREFIX}/man/man +EXAMPLESDIR?= ${PREFIX}/share/examples/${PROG} + +STD_CPPFLAGS?= -D_POSIX_C_SOURCE=200809L -D_XOPEN_SOURCE=700 + +LFS_CPPFLAGS?= -D_FILE_OFFSET_BITS=64 +LFS_LDFLAGS?= + +PCRE_CPPFLAGS?= -DHAVE_PCRE -I${LOCALBASE}/include +PCRE_LIBS?= -L${LOCALBASE}/lib -lpcre + +STD_CFLAGS?= -std=c99 +WARN_CFLAGS?= -Wall -W + +CC?= gcc +CPPFLAGS?= +CPPFLAGS+= ${STD_CPPFLAGS} ${LFS_CPPFLAGS} ${PCRE_CPPFLAGS} +CFLAGS?= -g -pipe +CFLAGS+= ${STD_CFLAGS} ${WARN_CFLAGS} +LDFLAGS?= +LDFLAGS+= ${LFS_LDFLAGS} +LIBS?= + +RM= rm -f +MKDIR?= mkdir -p +SETENV?= env + +PROG= confget +OBJS= confget.o confget_common.o confget_http_get.o confget_ini.o +SRCS= confget.c confget_common.c confget_http_get.c confget_ini.c +DEPENDFILE= .depend + +MAN1= confget.1 +MAN1GZ= ${MAN1}.gz + +BINOWN?= root +BINGRP?= wheel +BINMODE?= 555 + +SHAREOWN?= ${BINOWN} +SHAREGRP?= ${BINGRP} +SHAREMODE?= 644 + +INSTALL?= install +COPY?= -c +STRIP?= -s + +INSTALL_PROGRAM?= ${INSTALL} ${COPY} ${STRIP} -o ${BINOWN} -g ${BINGRP} -m ${BINMODE} +INSTALL_SCRIPT?= ${INSTALL} ${COPY} -o ${BINOWN} -g ${BINGRP} -m ${BINMODE} +INSTALL_DATA?= ${INSTALL} ${COPY} -o ${SHAREOWN} -g ${SHAREGRP} -m ${SHAREMODE} + +# development/debugging flags, you may safely ignore them +BDECFLAGS= -W -Wall -std=c99 -pedantic -Wbad-function-cast -Wcast-align \ + -Wcast-qual -Wchar-subscripts -Winline \ + -Wmissing-prototypes -Wnested-externs -Wpointer-arith \ + -Wredundant-decls -Wshadow -Wstrict-prototypes -Wwrite-strings +#CFLAGS+= ${BDECFLAGS} +#CFLAGS+= -ggdb -g3 + +TESTEXEC= test-getline test-fgetln + +all: ${PROG} ${MAN1GZ} + +clean: + ${RM} ${PROG} ${OBJS} ${MAN1GZ} + ${RM} ${TESTEXEC} readaline.h + +${PROG}: ${OBJS} + ${CC} ${LDFLAGS} -o ${PROG} ${OBJS} ${PCRE_LIBS} + +.c.o: + ${CC} ${CPPFLAGS} ${CFLAGS} -c $< + +${MAN1GZ}: ${MAN1} + gzip -c9 ${MAN1} > ${MAN1GZ}.tmp + mv ${MAN1GZ}.tmp ${MAN1GZ} + +install: all install-bin install-man install-examples + +install-bin: + -${MKDIR} ${DESTDIR}${BINDIR} + ${INSTALL_PROGRAM} ${PROG} ${DESTDIR}${BINDIR}/ + +install-man: + -${MKDIR} ${DESTDIR}${MANDIR}1 + ${INSTALL_DATA} ${MAN1GZ} ${DESTDIR}${MANDIR}1/ + +install-examples: + -${MKDIR} ${DESTDIR}${EXAMPLESDIR}/tests + ${INSTALL_SCRIPT} t/*.t ${DESTDIR}${EXAMPLESDIR}/tests/ + ${INSTALL_DATA} t/*.ini ${DESTDIR}${EXAMPLESDIR}/tests/ + +depend: + touch readaline.h + ${SETENV} CPPFLAGS="-DCONFGET_MAKEDEP ${CPPFLAGS}" \ + DEPENDFILE="${DEPENDFILE}" sh makedep.sh ${SRCS} + rm -f readaline.h + +test: all + prove t + +readaline.h: test-getline.c Makefile + rm -f readaline.h + if ${CC} ${CPPFLAGS} ${CFLAGS} ${LDFLAGS} -o test-getline test-getline.c ${LIBS}; then \ + echo '#define HAVE_GETLINE' > readaline.h; \ + else if ${CC} ${CPPFLAGS} ${CFLAGS} ${LDFLAGS} -o test-fgetln test-fgetln.c ${LIBS}; then \ + echo '#define HAVE_FGETLN' > readaline.h; \ + else \ + echo 'Neither getline() nor fgetln() found!' 1>&2; \ + false; \ + fi; fi + +.PHONY: all clean depend install test + +ifeq ($(wildcard ${DEPENDFILE}),${DEPENDFILE}) +include ${DEPENDFILE} +endif diff --git a/README b/README new file mode 100644 index 0000000..e5b55c5 --- /dev/null +++ b/README @@ -0,0 +1,20 @@ +This is confget, a simple utility for parsing configuration files and +extracting values from them. It currently parses INI-style files and +HTTP GET variables from CGI requests, but support for other formats +(e.g. HTTP POST, Java property files) is in the works. + +The confget utility is mainly intended to be used in shell scripts for +fetching values out of configuration files. + +To compile confget, your standard C library must provide one of two +functions: either getline(3) on Linux or other systems or fgetln(3) on +BSD-like systems. The confget build procedure should be able to +autodetect the presence of a suitable line reading function; if it fails +on your platform, please report this to the author! + +You may need to tweak the PCRE_CPPFLAGS and PCRE_LIBS variables to +indicate the presence or lack of PCRE support, the location of +the pcre.h header file, and the name and location of the libpcre.so +library itself. + +Comments: Peter Pentchev diff --git a/TODO b/TODO new file mode 100644 index 0000000..bdb3b49 --- /dev/null +++ b/TODO @@ -0,0 +1,7 @@ +A simple to-do list for confget, the configuration file parser: +- implement an HTTP POST configuration backend, deal with file uploads +- let the user give freeform options to the backends, e.g. file dir for + HTTP POST uploaded files +- some automated configuration (autoconf or similar) + +$Ringlet$ diff --git a/confget.1 b/confget.1 new file mode 100644 index 0000000..7bf7962 --- /dev/null +++ b/confget.1 @@ -0,0 +1,262 @@ +.\" Copyright (c) 2008, 2009, 2012, 2016 Peter Pentchev +.\" All rights reserved. +.\" +.\" Redistribution and use in source and binary forms, with or without +.\" modification, are permitted provided that the following conditions +.\" are met: +.\" 1. Redistributions of source code must retain the above copyright +.\" notice, this list of conditions and the following disclaimer. +.\" 2. 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. +.\" +.\" THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. +.\" +.Dd April 4, 2016 +.Dt CONFGET 1 +.Os +.Sh NAME +.Nm confget +.Nd read a variable from a configuration file +.Sh SYNOPSIS +.Nm +.Op Fl cOSx +.Op Fl N | Fl n +.Op Fl f Ar filename +.Op Fl m Ar pattern +.Op Fl P Ar postfix +.Op Fl p Ar prefix +.Op Fl s Ar section +.Op Fl t Ar type +.Ar varname... +.Nm +.Op Fl OSx +.Op Fl N | Fl n +.Op Fl f Ar filename +.Op Fl m Ar pattern +.Op Fl P Ar postfix +.Op Fl p Ar prefix +.Op Fl s Ar section +.Op Fl t Ar type +.Fl L Ar pattern... +.Nm +.Op Fl OSx +.Op Fl N | Fl n +.Op Fl f Ar filename +.Op Fl m Ar pattern +.Op Fl P Ar postfix +.Op Fl p Ar prefix +.Op Fl s Ar section +.Op Fl t Ar type +.Fl l +.Nm +.Op Fl f Ar filename +.Fl q Li sections +.Op Fl t Ar type +.Nm +.Op Fl hTV +.Sh DESCRIPTION +The +.Nm +utility examines a INI-style configuration file and retrieves the value of +the specified variables from the specified section. +Its intended use is to let shell scripts use the same INI-style configuration +files as other programs, to avoid duplication of data. +.Pp +The +.Nm +utility may retrieve the values of one or more variables, list all +the variables in a specified section, list only those whose names or +values match a specified pattern (shell glob or regular expression), or +check if a variable is present in the file at all. +It has a +.Dq shell-quoting +output mode that quotes the variable values in a way suitable for passing +them directly to a Bourne-style shell. +.Pp +Options: +.Bl -tag -width indent +.It Fl c +Check-only mode; exit with a code of 0 if any of the variables are present in +the configuration file, and 1 if there are none. +.It Fl f Ar filename +Specify the configuration file to read from, or +.Dq - +(a single dash) for standard input. +.It Fl h +Display program usage information and exit. +.It Fl L +Variable list mode; display the names and values of all variables in the +specified section with names matching one or more specified patterns. +.It Fl l +List mode; display the names and values of all variables in the specified +section. +.It Fl m Ar pattern +Only display variables with if their values match the specified pattern. +.It Fl N +Always display the variable name along with the value. +.It Fl n +Never display the variable name, only the value. +.It Fl O +Allow variables in the specified section to override variables in +the unnamed section at the start of the file. +This is the only case when +.Nm +even considers variables in more than one section. +.It Fl P Ar postfix +Display this string after the variable name as a postfix. +.It Fl p Ar prefix +Display this string before the variable name as a prefix. +.It Fl q Ar query +Query for a specific type of information. +For the present, the only supported value for the +.Ar query +argument is +.Dq sections , +which lists the names of the sections defined in the configuration file. +.It Fl S +Quote the variable values so that the +.Dq var=value +lines may be passed directly to the Bourne shell. +.It Fl s Ar section +Specify the configuration section to read. +If this option is specified, +.Nm +will only consider variables defined in the specified section; +see the +.Fl O +option for the only exception. +.Pp +If this option is not specified, +.Nm +will use the first section found in the configuration file. +However, if the configuration file contains variable definitions before +a section header, +.Nm +will only examine them instead. +.Pp +If the +.Fl s +option is specified with an empty string as the section name, +.Nm +will +.Em only +examine variables defined before any section header +.Po +a +.Dq real +unnamed default section +.Pc ; +this is incompatible with the +.Fl O +option. +.It Fl T +List the available configuration file types that may be selected by the +.Fl t +option. +.It Fl t Ar type +Specify the configuration file type. +.It Fl V +Display program version information and exit. +.It Fl x +Treat the patterns as regular expressions instead of shell glob patterns. +.El +.Sh ENVIRONMENT +Not taken into consideration. +.Sh EXIT STATUS +If the +.Fl c +option is specified, the +.Nm +utility will exit with a status of 0 if any of the specified variables exist in +the config file and 1 if none of them are present. +.Pp +In normal operation, no matter whether any variables were found in +the configuration file or not, +the +.Nm +utility exits with a status of 0 upon normal completion. +If any errors should occur while accessing or parsing the configuration file, +the +.Nm +utility will display a diagnostic message on the standard error stream and +exit with a status of 1. +.Sh EXAMPLES +Retrieve the variable +.Dv machine_id +from the +.Dv system +section of a configuration file: +.Pp +.Dl confget -f h.conf -s system machine_id +.Pp +Retrieve the +.Dv page_id +variable from an HTTP GET request, but only if it is a valid number: +.Pp +.Dl confget -t http_get -x -m '^\\d+$' page_id +.Pp +Retrieve the variable +.Dv hostname +from the +.Dv db +section, but only if it ends in +.Dq .ringlet.net : +.Pp +.Dl confget -f h.conf -s db -m '*.ringlet.net' hostname +.Pp +Display the names and values of all variables declared before +any section has been defined: +.Pp +.Dl confget -f h.conf -s '' -l +.Pp +Display the names and values of all variables in the +.Dv system +section with names beginning with +.Dq mach +or ending in +.Dq name , +appending a +.Dq cfg_ +at the start of each variable name: +.Pp +.Dl confget -f h.conf -s system -p 'cfg_' -L 'mach*' '*name' +.Pp +Display the names and values of all variables in the +.Dv system +section: +.Pp +.Dl confget -f h.conf -s system -l +.Pp +Safely read the contents of the +.Dv db +section: +.Pp +.Dl eval `confget -f h.conf -s db -p db_ -S -l` +.Sh SEE ALSO +For another way to parse INI files, see the +.Xr Config::IniFiles 3 +Perl module. +.Sh STANDARDS +No standards documentation was harmed in the process of creating +.Nm . +.Sh BUGS +Please report any bugs in +.Nm +to the author. +.Sh AUTHOR +The +.Nm +utility was conceived and written by +.An Peter Pentchev Aq roam@ringlet.net +in 2008. diff --git a/confget.c b/confget.c new file mode 100644 index 0000000..3fa7cd3 --- /dev/null +++ b/confget.c @@ -0,0 +1,704 @@ +/*- + * Copyright (c) 2008, 2009, 2012, 2013, 2016 - 2019 Peter Pentchev + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef HAVE_PCRE +#include +#endif + +#include "confget.h" +#include "confget_http_get.h" +#include "confget_ini.h" + +struct var { + char *name; + char *value; + char *section; + bool display; +}; + +/* The configuration filename specified by the user. */ +const char *filename; +/* The configuration section specified by the user. */ +const char *section; +/* The number of the variable or pattern parameters after the options. */ +static int margc; +/* The variables to extract or patterns to match against. */ +static char * const *margv; +/* Display the variable names, or only the values? */ +static bool showvarname; +/* Look for more variables after a match is found? */ +static bool manyvars; +/* Should section variables override default ones? */ +bool override; +/* The pattern to match the variable value */ +static const char *matchvalue; +/* The prefix to use when displaying the variable name */ +static const char *output_prefix; +/* The postfix to use when displaying the variable name */ +static const char *output_postfix; + +size_t varsalloc, varscount; +struct var *vars; + +static const confget_backend * const backends[] = { + &confget_http_get_backend, + &confget_ini_backend, +}; +#define BACKEND_CNT (sizeof(backends) / sizeof(backends[0])) + +static size_t find_backend(const char * const _name); +static void init(int _argc, char * const *_argv); +static void usage(bool _error); +static void version(void); + +/* The currently used configuration backend (ini, http_get, etc.) */ +const confget_backend *backend; + +/* The currently processed input file */ +FILE *conffile; + +/* Has the "-c" (check only) option been specified? */ +bool cflag; +/* In check-only mode, has the variable been found? */ +bool cfound; +/* Has the "-l" (list the section) option been specified? */ +static bool lflag; +/* Has the "-L" (match varname against patterns) option been specified? */ +static bool Lflag; +/* Query for the names of the sections in the configuration file? */ +bool qsections; +/* Query for a single feature? */ +bool qfeature; +/* Query for all the features? */ +bool qfeatures; +/* Shell-quote the variable values? */ +static bool Sflag; +#ifdef HAVE_PCRE +/* Treat patterns as regular expressions? */ +static bool xflag; + +static const pcre *x_matchvalue, **x_margv; +static const pcre_extra *x_matchvalue_extra, **x_margv_extra; +#endif + +/*** + * Function: + * usage - display program usage information and exit + * Inputs: + * error - is this invoked in error? + * Returns: + * Nothing. + * Modifies: + * Writes the usage information to the standard output or standard + * error depending on the "error" flag. + */ +void +usage(const bool error) +{ + const char * const s1 = "Usage:\n" + "confget [-cOSx] [-N | -n] [-f filename] [-m pattern] [-P postfix]" + " [-p prefix]\n" + " [-s section] [-t type] var...\n\n" + "confget [-OSx] [-N | -n] [-f filename] [-m pattern] [-P postfix]" + " [-p prefix]\n" + " [-s section] [-t type] -L pattern...\n\n" + "confget [-OSx] [-N | -n] [-f filename] [-m pattern] [-P postfix]" + " [-p prefix]\n" + " [-s section] [-t type] -l\n" + "confget [-f filename] -q sections [-t type]\n\n" + "confget -q features\n" + "confget -q feature NAME\n\n" + "confget [-hTV]\n\n"; + const char * const s2 = + "\t-c\tcheck if the variable is defined in the file;\n" + "\t-f\tspecify the configuration file to read from,\n" + "\t\tor \"-\" for standard input;\n" + "\t-h\tdisplay usage information and exit;\n" + "\t-L\tspecify which variables to display;\n" + "\t-l\tlist all variables in the specified section;\n" + "\t-m\tonly display values that match the specified pattern;\n" + "\t-N\talways display the variable name;\n" + "\t-n\tnever display the variable name;\n" + "\t-O\tallow variables in the specified section to override\n" + "\t\tthose placed before any section definitions;\n"; + const char * const s3 = + "\t-P\tdisplay this string after the variable name;\n" + "\t-p\tdisplay this string before the variable name;\n" + "\t-q\tquery for a specific type of information, e.g. the list of\n" + "\t\tsections defined in the configuration file;\n" + "\t-S\tquote the values suitably for the Bourne shell;\n" + "\t-s\tspecify the configuration section to read;\n" + "\t-T\tlist the available configuration file types;\n" + "\t-t\tspecify the configuration file type;\n" + "\t-x\ttreat the match patterns as regular expressions;\n" + "\t-V\tdisplay program version information and exit."; + + if (error) { + fprintf(stderr, "%s%s%s\n", s1, s2, s3); + exit(1); + } + printf("%s%s%s\n", s1, s2, s3); +} + +/*** + * Function: + * version - display program version information + * Inputs: + * None. + * Returns: + * Nothing. + * Modifies: + * Nothing; displays program version information on the standard output. + */ +void +version(void) +{ + puts("confget 2.2.0"); +} + +/*** + * Function: + * init - parse the command-line options + * Inputs: + * argc, argv - the command-line parameter + * Returns: + * 0 on success, -1 on error. + * Modifies: + * Sets section and varname on success. + * Writes to the standard error on error. + */ +static void +init(const int argc, char * const * const argv) +{ + conffile = NULL; + + cflag = Lflag = lflag = Sflag = showvarname = manyvars = override = false; + bool do_help = false, do_list = false, do_version = false, + show_name = false, hide_name = false; + matchvalue = NULL; + output_prefix = output_postfix = ""; + size_t bidx = find_backend("ini"); + int ch; + while ((ch = getopt(argc, argv, "cf:hLlm:NnOP:p:q:Ss:Tt:xV-:")) != EOF) { + switch (ch) { + case 'c': + cflag = true; + manyvars = false; + break; + + case 'f': + filename = optarg; + break; + + case 'h': + do_help = true; + break; + + case 'L': + Lflag = true; + showvarname = true; + manyvars = true; + break; + + case 'l': + lflag = true; + showvarname = true; + manyvars = true; + break; + + case 'm': + matchvalue = optarg; + break; + + case 'N': + show_name = true; + break; + + case 'n': + hide_name = true; + break; + + case 'O': + override = true; + break; + + case 'P': + output_postfix = optarg; + break; + + case 'p': + output_prefix = optarg; + break; + + case 'q': + if (strcmp(optarg, "sections") == 0) { + qsections = true; + } else if (strcmp(optarg, "feature") == 0) { + qfeature = true; + } else if (strcmp(optarg, "features") == 0) { + qfeatures = true; + } else { + errx(1, "Unsupported query type"); + } + break; + + case 'S': + Sflag = true; + break; + + case 's': + section = optarg; + break; + + case 'T': + do_list = true; + break; + + case 't': + bidx = find_backend(optarg); + break; + + case 'x': +#ifdef HAVE_PCRE + xflag = true; + break; +#else + errx(1, "No regular expression support in this confget build"); +#endif + + case 'V': + do_version = true; + break; + + case '-': + if (strcmp(optarg, "help") == 0) + do_help = true; + else if (strcmp(optarg, "version") == 0) + do_version = true; + else + errx(1, "Unknown long option"); + break; + + case '?': + default: + usage(true); + break; + } + } + + if (do_version) + version(); + if (do_help) + usage(false); + if (do_list) { + printf("Supported backends:"); + for (size_t i = 0; i < BACKEND_CNT; i++) + printf(" %s", backends[i]->name); + puts(""); + } + if (do_help || do_list || do_version) + exit(0); + + margc = argc - optind; + margv = argv + optind; + const char * const features[][2] = { + { "BASE", "2.1" }, + { NULL, NULL }, + }; + if (qsections + qfeature + qfeatures + lflag + Lflag + + (margc > 0 && !(Lflag || qfeature)) > 1) { + errx(1, "Only a single query at a time, please!"); + } else if (qfeatures) { + if (margc > 0) + errx(1, "No arguments with -q features"); + bool started = false; + for (size_t i = 0; features[i][0] != NULL; i++) { + if (started) + putchar(' '); + else + started = true; + printf("%s=%s", features[i][0], features[i][1]); + } + putchar('\n'); + exit(0); + } else if (qfeature) { + if (margc != 1) + errx(1, "A single feature name expected"); + for (size_t i = 0; features[i][0] != NULL; i++) + if (strcmp(margv[0], features[i][0]) == 0) { + puts(features[i][1]); + exit(0); + } + /* Feature not found */ + exit(1); + } + + if (cflag && (lflag || Lflag)) + errx(1, "The -c flag may not be used with -l or -L"); + if (margc == 0 && !lflag && !qsections) + errx(1, "No matching criteria specified"); + if ((margc > 0) && lflag) + errx(1, "Too many matching criteria specified"); + if (override && (section == NULL || section[0] == '\0')) + errx(1, "The -O flag only makes sense with a non-empty section name"); + if (margc > 1 && !lflag) { + showvarname = true; + manyvars = true; + } + if (show_name) { + if (hide_name) + errx(1, "The -N and -n flags may not be used together"); + showvarname = true; + } else if (hide_name) { + showvarname = false; + } + +#ifdef HAVE_PCRE + if (xflag) { + if (matchvalue) { + const char *pcre_err; + int pcre_ofs; + x_matchvalue = pcre_compile(matchvalue, 0, &pcre_err, + &pcre_ofs, NULL); + if (pcre_err != NULL) + errx(1, "Invalid match value: %s", pcre_err); + x_matchvalue_extra = pcre_study(x_matchvalue, 0, + &pcre_err); + if (pcre_err != NULL) + errx(1, "Invalid match value: %s", pcre_err); + } else { + x_matchvalue = NULL; + } + x_margv = malloc(margc * sizeof(*x_margv)); + if (x_margv == NULL) + err(1, "Could not allocate memory"); + x_margv_extra = malloc(margc * sizeof(*x_margv_extra)); + if (x_margv_extra == NULL) + err(1, "Could not allocate memory"); + for (size_t t = 0; t < (size_t)margc; t++) { + const char *pcre_err; + int pcre_ofs; + x_margv[t] = pcre_compile(margv[t], 0, &pcre_err, + &pcre_ofs, NULL); + if (pcre_err != NULL) + errx(1, "Invalid match pattern: %s", pcre_err); + x_margv_extra[t] = pcre_study(x_margv[t], 0, + &pcre_err); + if (pcre_err != NULL) + errx(1, "Invalid match pattern: %s", pcre_err); + } + } +#endif + + if (qsections && strcmp(backends[bidx]->name, "ini") != 0) + errx(1, "The query for sections is only supported for the " + "'ini' backend for the present"); + backend = backends[bidx]; +} + +/*** + * Function: + * find_backend - find a confget backend by name + * Inputs: + * name - the name of the backend + * Returns: + * The index in backends[] if found; exits on error. + * Modifies: + * Nothing. + */ +static size_t +find_backend(const char * const name) +{ + const size_t len = strlen(name); + size_t tentative = BACKEND_CNT; + for (size_t i = 0; i < BACKEND_CNT; i++) { + if (strncmp(name, backends[i]->name, len) != 0) + continue; + if (backends[i]->name[len] == '\0') + return (i); + if (tentative != BACKEND_CNT) + errx(1, "Ambiguous backend prefix '%s'", name); + tentative = i; + } + if (tentative == BACKEND_CNT) + errx(1, "Unknown backend '%s'", name); + return (tentative); +} + +/*** + * Function: + * openfile - open the requested file for reading + * Inputs: + * None. + * Returns: + * Nothing; exits on error. + * Modifies: + * Stores the opened file into fp. + */ +static void +openfile(void) +{ + if (backend->openfile == NULL) + errx(2, + "INTERNAL ERROR: backend '%s' does not define a openfile routine\n", + backend->name); + backend->openfile(); +} + +/*** + * Function: + * readfile - scan an INI file for the requested variable + * Inputs: + * None. + * Returns: + * Nothing; exits on error. + * Modifies: + * May write to standard output if the variable has been found. + */ +static void +readfile(void) +{ + + if (backend->readfile == NULL) + errx(2, + "INTERNAL ERROR: backend '%s' does not define a readfile routine\n", + backend->name); + backend->readfile(); +} + +/*** + * Function: + * closefile - close a scanned INI file + * Inputs: + * None. + * Returns: + * Nothing; exits on error. + * Modifies: + * Closes the file pointed to by fp. + */ +static void +closefile(void) +{ + + if (backend->closefile == NULL) + errx(2, + "INTERNAL ERROR: backend '%s' does not define a closefile routine\n", + backend->name); + backend->closefile(); +} + +/*** + * Function: + * foundvar - process the user-requested variable + * Inputs: + * sec - the section the variable was found in + * name - the variable name + * value - the variable value + * Returns: + * Nothing; exits on error. + * Modifies: + * In check-only mode, sets cfound. + * In normal mode, writes to standard output. + */ +void +foundvar(const char * const sec, const char * const name, + const char * const value) +{ + if (!lflag) { + bool found = false; + for (int i = 0; i < margc; i++) + if (Lflag) { +#ifdef HAVE_PCRE + if (xflag) { + const int r = pcre_exec(x_margv[i], + x_margv_extra[i], name, + strlen(name), 0, 0, NULL, 0); + if (r == 0) { + found = true; + break; + } + if (r != PCRE_ERROR_NOMATCH) + errx(1, "Could not match '%s' against the '%s' pattern", name, margv[i]); + } else +#endif + if (fnmatch(margv[i], name, 0) == 0) { + found = true; + break; + } + } else { + if (strcmp(name, margv[i]) == 0) { + found = true; + break; + } + } + if (!found) + return; + } + + bool display = true; + if (matchvalue != NULL) { +#ifdef HAVE_PCRE + if (xflag) { + const int r = pcre_exec(x_matchvalue, x_matchvalue_extra, value, + strlen(value), 0, 0, NULL, 0); + if (r == PCRE_ERROR_NOMATCH) + display = false; + else if (r != 0) + errx(1, "Could not match '%s' against the '%s' pattern", value, matchvalue); + } else +#endif + if (fnmatch(matchvalue, value, 0) == FNM_NOMATCH) + display = false; + if (!display && !override) + return; + } + + for (size_t i = 0; i < varscount; i++) + { + struct var * const var = &vars[i]; + if (strcmp(var->name, name) != 0) + continue; + free(var->value); + var->value = strdup(value); + free(var->section); + var->section = sec == NULL? NULL: strdup(sec); + var->display = display; + return; + } + if (varscount == varsalloc) { + varsalloc += 16; + vars = realloc(vars, varsalloc * sizeof(*vars)); + if (vars == NULL) + err(1, "Could not allocate memory for the variables' values"); + } + vars[varscount].name = strdup(name); + vars[varscount].value = strdup(value); + vars[varscount].section = sec == NULL? NULL: strdup(sec); + vars[varscount].display = display; + varscount++; +} + +static void +displayvars(void) +{ + cfound = false; + for (size_t i = 0; i < varscount; i++) + { + if (!vars[i].display) + continue; + if (cflag) { + cfound = true; + return; + } + const char * const name = vars[i].name; + const char * const value = vars[i].value; + + if (showvarname) + printf("%s%s%s=", output_prefix, name, output_postfix); + if (!Sflag) { + printf("%s\n", value); + } else { + printf("'"); + const char *p = value; + while (*p) { + while (*p && *p != '\'') + putchar(*p++); + if (*p == '\0') + break; + printf("'\""); + while (*p == '\'') + putchar(*p++); + printf("\"'"); + } + printf("'\n"); + } + } +} + +/*** + * Function: + * foundsection - process a new section header + * Inputs: + * sec - the name of the new section + * Returns: + * Nothing; exits on error. + * Modifies: + * In "-q sections" mode, writes to standard output. + */ +void +foundsection(const char * const _sec) +{ + if (!qsections) + return; + + static size_t alloc = 0, count = 0; + static char **names = NULL; + for (size_t i = 0; i < count; i++) + if (strcmp(_sec, names[i]) == 0) + return; + /* A new section! */ + if (alloc == count) { + const size_t nalloc = 2 * alloc + 1; + char ** const newnames = realloc(names, nalloc * sizeof(names[0])); + if (newnames == NULL) + err(1, "Out of memory for the section names list"); + names = newnames; + alloc = nalloc; + } + names[count] = strdup(_sec); + if (names[count] == NULL) + err(1, "Out of memory for the new section name"); + count++; + + /* And finally output it :) */ + puts(_sec); +} + +/*** + * Main routine + */ +int +main(const int argc, char * const * const argv) +{ + + init(argc, argv); + + openfile(); + readfile(); + closefile(); + + displayvars(); + return cflag? !cfound: 0; +} diff --git a/confget.h b/confget.h new file mode 100644 index 0000000..b3aacef --- /dev/null +++ b/confget.h @@ -0,0 +1,63 @@ +#ifndef __INCLUDED_CONFGET_H +#define __INCLUDED_CONFGET_H + +/*- + * Copyright (c) 2008, 2012, 2013, 2016 Peter Pentchev + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. + */ + +#ifndef __unused +#ifdef __GNUC__ +#define __unused __attribute__((unused)) +#else /* __GNUC__ */ +#define __unused +#endif /* __GNUC__ */ +#endif /* __unused */ + +typedef struct { + const char * const name; + void (* const openfile)(void); + void (* const readfile)(void); + void (* const closefile)(void); +} confget_backend; + +/* The currently processed input file */ +extern FILE *conffile; + +/* The configuration filename specified by the user. */ +extern const char *filename; +/* The configuration section specified by the user. */ +extern const char *section; + +/* Should section variables override default ones? */ +extern bool override; +/* Query the section names? */ +extern bool qsections; + +void foundvar(const char * const _sec, + const char * const _name, + const char * const _value); +void foundsection(const char * const _sec); + +#endif /* _INCLUDED */ diff --git a/confget_common.c b/confget_common.c new file mode 100644 index 0000000..cb79fe5 --- /dev/null +++ b/confget_common.c @@ -0,0 +1,152 @@ +/*- + * Copyright (c) 2008, 2012, 2016 Peter Pentchev + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. + */ + +#include +#include +#include +#include +#include + +#include "confget.h" +#include "confget_common.h" +#include "readaline.h" + +/* Is conffile pointing to stdin? */ +static bool is_stdin; + +/*** + * Function: + * common_openfile - open any type of file for reading + * Inputs: + * None. + * Returns: + * 0 on success, -1 on error. + * Modifies: + * Stores the opened file into conffile. + */ +void +common_openfile(void) +{ + if (filename == NULL) + errx(1, "No configuration file name specified"); + if (strcmp(filename, "-") == 0) { + conffile = stdin; + is_stdin = true; + return; + } + + FILE * const ifp = fopen(filename, "r"); + if (ifp == NULL) + err(1, "Opening the input file %s", filename); + conffile = ifp; + is_stdin = false; +} + +/*** + * Function: + * common_openfile_null - do not open any file for methods that do not + * take their input from files + * Inputs: + * None. + * Returns: + * Nothing. + * Modifies: + * Stores a null value into conffile. + */ +void +common_openfile_null(void) +{ + conffile = NULL; + is_stdin = false; +} + +/*** + * Function: + * common_closefile - close a scanned file + * Inputs: + * None. + * Returns: + * Nothing; exits on error. + * Modifies: + * Closes the file pointed to by fp. + */ +void +common_closefile(void) +{ + if (conffile == NULL) + return; + if (!is_stdin) + if (fclose(conffile) == EOF) + err(1, "Could not close the config file"); + conffile = NULL; +} + +/*** + * Function: + * confgetline - read a line from a file + * Inputs: + * fp - the file to read from + * line - a pointer to a location to store the line + * n - the location to store the line length + * Returns: + * The line pointer on success, NULL on error or EOF. + * Modifies: + * Reallocates a buffer at *line to as much as needed. + */ +char * +confgetline(FILE * const fp, char ** const line, size_t * const len) +{ +#if defined(HAVE_GETLINE) + ssize_t r = getline(line, len, fp); + if (r == -1) + return (NULL); + while (r > 0 && ((*line)[r - 1] == '\r' || (*line)[r - 1] == '\n')) + (*line)[--r] = '\0'; + return (*line); +#elif defined(HAVE_FGETLN) + size_t n; + char * const p = fgetln(fp, &n); + if (p == NULL) + return (NULL); + while (n > 0 && (p[n - 1] == '\r' || p[n - 1] == '\n')) + n--; + char *q; + if (*line == NULL || *len < n + 1) { + q = realloc(*line, n + 1); + if (q == NULL) + return (NULL); + *line = q; + *len = n + 1; + } else { + q = *line; + } + memcpy(q, p, n); + q[n] = '\0'; + return (q); +#elif !defined(CONFGET_MAKEDEP) +#error Neither HAVE_GETLINE nor HAVE_FGETLN defined! +#endif +} diff --git a/confget_common.h b/confget_common.h new file mode 100644 index 0000000..4c0410c --- /dev/null +++ b/confget_common.h @@ -0,0 +1,36 @@ +#ifndef _INCLUDED_CONFGET_COMMON_H +#define _INCLUDED_CONFGET_COMMON_H + +/*- + * Copyright (c) 2008, 2016 Peter Pentchev + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. + */ + +void common_openfile(void); +void common_openfile_null(void); +void common_closefile(void); + +char *confgetline(FILE *_fp, char **_line, size_t *_len); + +#endif /* _INCLUDED_CONFGET_COMMON_H */ diff --git a/confget_http_get.c b/confget_http_get.c new file mode 100644 index 0000000..d23a12b --- /dev/null +++ b/confget_http_get.c @@ -0,0 +1,158 @@ +/*- + * Copyright (c) 2008, 2013, 2016 Peter Pentchev + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. + */ + +#include +#include +#include +#include +#include + +#include "confget.h" +#include "confget_common.h" +#include "confget_http_get.h" + +static void readfile_http_get(void); + +confget_backend confget_http_get_backend = { + "http_get", + common_openfile_null, + readfile_http_get, + common_closefile, +}; + +static char *strdup_urldecode(const char * const _s, size_t _len); + +/*** + * Function: + * readfile_http_get - scan CGI GET data for the requested variable + * Inputs: + * None. + * Returns: + * Nothing; exits on error. + * Modifies: + * May write to standard output if the variable has been found. + * May write to standard error and terminate the program. + */ +static void +readfile_http_get(void) +{ + if (section == NULL) + section = "QUERY_STRING"; + const char *qstr = getenv(section); + if (qstr == NULL) + errx(1, "No CGI GET data - undefined environment variable %s", + section); + while (*qstr != '\0') { + const char *start = qstr; + while (*qstr != '\0' && *qstr != '&' && *qstr != '=') + qstr++; + char * const vname = strdup_urldecode(start, qstr - start); + + if (*qstr != '=') { + foundvar("", vname, ""); + free(vname); + if (*qstr == '\0') + return; + if (strncmp(qstr + 1, "amp;", 4) == 0) + qstr += 5; + else + qstr++; + continue; + } + + start = ++qstr; + while (*qstr != '\0' && *qstr != '&') + qstr++; + char * const vvalue = strdup_urldecode(start, qstr - start); + foundvar("", vname, vvalue); + free(vname); + free(vvalue); + if (*qstr == '\0') + return; + if (strncmp(qstr + 1, "amp;", 4) == 0) + qstr += 5; + else + qstr++; + } +} + +/* + * Function: + * strdup_urldecode - duplicate a string, decoding the URL-style + * %XX escape sequences + * Inputs: + * s - the string to duplicate + * Returns: + * A pointer to the duplicated string on success, exits on error. + * Modifies: + * Allocates memory, may write to stderr and terminate the program. + */ +static char * +strdup_urldecode(const char * const s, size_t len) +{ + const char *p; + char *q, *t, *v, *eh; + char h[3]; + long l; + + t = malloc(len + 1); + if (t == NULL) + err(1, "Out of memory"); + + p = s; + q = t; + while (len > 0) { + v = memchr(p, '%', len); + if (v == NULL) { + memcpy(q, p, len); + q += len; + p += len; + len = 0; + break; + } else if (v != p) { + memcpy(q, p, v - p); + q += v - p; + len -= v - p; + p = v; + } + p++; + if (len < 2) + errx(1, "Not enough hex digits in a %%XX URL escape"); + h[0] = p[0]; + h[1] = p[1]; + h[2] = '\0'; + l = strtol(h, &eh, 16); + if (eh != h + 2) + errx(1, "Invalid hex string in a %%XX URL escape"); + else if (l < 0 || l > 255) + errx(1, "Invalid hex value in a %%XX URL escape"); + p += 2; + *q++ = l; + len -= 3; + } + *q++ = '\0'; + return (t); +} diff --git a/confget_http_get.h b/confget_http_get.h new file mode 100644 index 0000000..8e65efb --- /dev/null +++ b/confget_http_get.h @@ -0,0 +1,32 @@ +#ifndef __INCLUDED_CONFGET_HTTP_GET_H +#define __INCLUDED_CONFGET_HTTP_GET_H + +/*- + * Copyright (c) 2008 Peter Pentchev + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. + */ + +extern confget_backend confget_http_get_backend; + +#endif /* _INCLUDED */ diff --git a/confget_ini.c b/confget_ini.c new file mode 100644 index 0000000..0b44ed7 --- /dev/null +++ b/confget_ini.c @@ -0,0 +1,203 @@ +/*- + * Copyright (c) 2008, 2011 - 2013, 2016 Peter Pentchev + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. + */ + +#include +#include +#include +#include +#include +#include + +#include "confget.h" +#include "confget_common.h" +#include "confget_ini.h" + +static void readfile_ini(void); + +confget_backend confget_ini_backend = { + "ini", + common_openfile, + readfile_ini, + common_closefile, +}; + +#define RF_SKIPSPACE while (*p != '\0' && isspace(*p)) p++; +#define RF_SKIPNONSPACE while (*p != '\0' && *p != '=' && !isspace(*p)) p++; + +/*** + * Function: + * readfile_ini - scan an INI file for the requested variable + * Inputs: + * None. + * Returns: + * Nothing; exits on error. + * Modifies: + * May write to standard output if the variable has been found. + * May write to standard error and terminate the program. + */ +static void +readfile_ini(void) +{ + char *grp = NULL; + bool ingroup = false, incont = false, seenvars = false; + char *vname = NULL, *vvalue = NULL; + if (section == NULL || section[0] == '\0' || override) + ingroup = true; + size_t len = 0; + char *line = NULL; + while (confgetline(conffile, &line, &len) != NULL) { + char *p = line; + if (incont) { + if (vvalue == NULL) + errx(2, "INTERNAL ERROR: null vvalue incont"); + size_t vlen = strlen(vvalue); + const size_t plen = strlen(p); + vvalue = realloc(vvalue, vlen + plen + 1); + if (vvalue == NULL) + err(1, "Out of memory for the variable value"); + memcpy(vvalue + vlen, p, plen); + vvalue[vlen + plen] = '\0'; + + vlen += plen; + if (vlen > 0 && vvalue[vlen - 1] == '\\') { + vvalue[vlen - 1] = '\0'; + continue; + } + incont = false; + while (vlen > 0 && isspace(vvalue[vlen - 1])) + vvalue[--vlen] = '\0'; + + if (ingroup) { + seenvars = true; + foundvar(grp, vname, vvalue); + free(vname); + free(vvalue); + vname = vvalue = NULL; + } + continue; + } + RF_SKIPSPACE; + /* Comment? */ + if (*p == '#' || *p == ';' || *p == '\0') + continue; + /* Section? */ + if (*p == '[') { + if (grp != NULL) { + free(grp); + grp = NULL; + } + p++; + RF_SKIPSPACE; + if (*p == '\0') + errx(1, "Invalid group line: %s", line); + const char * const start = p++; + char *end = p; + while (*p != '\0' && *p != ']') { + if (!isspace(*p)) + end = p + 1; + p++; + } + if (*p != ']') + errx(1, "Invalid group line: %s", line); + p++; + RF_SKIPSPACE; + if (*p != '\0') + errx(1, "Invalid group line: %s", line); + + *end = '\0'; + if (section == NULL) { + if (seenvars && !qsections) { + free(line); + return; + } + section = strdup(start); + if (section == NULL) + errx(1, "Out of memory for the " + "group name"); + } + + grp = strdup(start); + if (grp == NULL) + err(1, "Out of memory for the group name"); + foundsection(grp); + + ingroup = strcmp(grp, section) == 0; + } else { + /* Variable! */ + if (vname != NULL) { + free(vname); + vname = NULL; + } + if (vvalue != NULL) { + free(vvalue); + vvalue = NULL; + } + const char * const start = p++; + RF_SKIPNONSPACE; + if (*p == '\0') + errx(1, "No value on line: %s", line); + char *end = p; + RF_SKIPSPACE; + if (*p != '=') + errx(1, "No value on line: %s", line); + p++; + RF_SKIPSPACE; + + *end = '\0'; + vname = strdup(start); + if (vname == NULL) + err(1, "Out of memory for the variable name"); + vvalue = strdup(p); + if (vvalue == NULL) + err(1, "Out of memory for the variable value"); + + size_t vlen = strlen(vvalue); + if (vlen > 0 && vvalue[vlen - 1] == '\\') { + vvalue[vlen - 1] = '\0'; + incont = true; + continue; + } + while (vlen > 0 && isspace(vvalue[vlen - 1])) + vvalue[--vlen] = '\0'; + + if (ingroup) { + seenvars = true; + foundvar(grp, vname, vvalue); + free(vname); + free(vvalue); + vname = vvalue = NULL; + } + } + } + free(grp); + free(vname); + free(vvalue); + free(line); + if (!feof(conffile)) + err(1, "Error reading input file %s", filename); + if (incont) + errx(1, "Invalid input file - continuation at the last line"); +} diff --git a/confget_ini.h b/confget_ini.h new file mode 100644 index 0000000..3654e24 --- /dev/null +++ b/confget_ini.h @@ -0,0 +1,32 @@ +#ifndef __INCLUDED_CONFGET_INI_H +#define __INCLUDED_CONFGET_INI_H + +/*- + * Copyright (c) 2008 Peter Pentchev + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. + */ + +extern confget_backend confget_ini_backend; + +#endif /* _INCLUDED */ diff --git a/makedep.sh b/makedep.sh new file mode 100644 index 0000000..2080b89 --- /dev/null +++ b/makedep.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +# This file is hereby placed in the public domain. + +[ -z "$DEPENDFILE" ] && DEPENDFILE=.depend +${CC-cc} $CPPFLAGS -MM "$@" > "$DEPENDFILE" diff --git a/python/README.md b/python/README.md new file mode 100644 index 0000000..890f138 --- /dev/null +++ b/python/README.md @@ -0,0 +1,81 @@ +confget - parse configuration files +=================================== + +The `confget` library parses configuration files (currently INI-style +files and CGI `QUERY_STRING` environment variable) and allows a program +to use the values defined in them. It provides various options for +selecting the variable names and values to return and the configuration +file sections to fetch them from. + +The `confget` library may also be used as a command-line tool with +the same interface as the C implementation. + +The `confget` library is fully typed. + +Specifying configuration values for the backends +------------------------------------------------ + +The `confget.defs` module defines the `Config` class that is used to +control the behavior of the various `confget` backends. Its main +purpose is to specify the filename and, optionally, the section name for +INI-style files, but other backends may use its fields in different ways. + +A `Config` object is created using the following parameters: +- a list of variable names to query (may be empty) +- `filename` (str, optional): the name of the file to open +- `section` (str, default ""): the name of the section within the file +- `section_specified` (bool, default false): if `section` is an empty + string, only fetch variables from the unnamed section at the start of + the file instead of defaulting to the first section in the file + +Parsing INI-style configuration files +------------------------------------- + +The `confget` library's "ini" backend parses an INI-style configuration +file. Its `read_file()` method parses the file and returns a dictionary +of sections and the variables and their values within them: + + import confget + + cfg = confget.Config([], filename='config.ini') + ini = confget.BACKENDS['ini'](cfg) + data = ini.read_file() + print('Section names: {names}'.format(names=sorted(data.keys()))) + print(data['server']['address']) + +Letting variables in a section override the default ones +-------------------------------------------------------- + +In some cases it is useful to have default values before the first +named section in a file and then override some values in various +sections. This may be useful for e.g. host-specific configuration +kept in a section with the same name as the host. + +The `format` module in the `confget` library allows, among other +filtering modes, to get the list of variables with a section +overriding the default ones: + + from confget import backend, format + + cfg = format.FormatConfig(['foo'], filename='config.ini', section='first', + section_override=True) + ini = backend.BACKENDS['ini'](cfg) + data = ini.read_file() + res = format.filter_vars(cfg, data) + assert len(res) == 1, repr(res) + print(res[0].output_full) + + cfg = format.FormatConfig(['foo'], filename='config.ini', section='second', + section_override=True) + ini = backend.BACKENDS['ini'](cfg) + data = ini.read_file() + res = format.filter_vars(cfg, data) + assert len(res) == 1, repr(res) + print(res[0].output_full) + +See the documentation of the `FormatConfig` class and the `filter_vars()` +function in the `confget.format` module for more information and for +a list of the various other filtering modes, all supported when +the library is used as a command-line tool. + +Comments: Peter Pentchev diff --git a/python/confget/__init__.py b/python/confget/__init__.py new file mode 100644 index 0000000..cbd8ee9 --- /dev/null +++ b/python/confget/__init__.py @@ -0,0 +1,107 @@ +# Copyright (c) 2019 Peter Pentchev +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. 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. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. + +""" +confget - a library for parsing configuration files + +The `confget` library parses configuration files (currently INI-style +files and CGI `QUERY_STRING` environment variable) and allows a program +to use the values defined in them. It provides various options for +selecting the variable names and values to return and the configuration +file sections to fetch them from. + +The `confget` library may also be used as a command-line tool with +the same interface as the C implementation. + +The `confget` library is fully typed. + +Specifying configuration values for the backends +------------------------------------------------ + +The `confget.defs` module defines the `Config` class that is used to +control the behavior of the various `confget` backends. Its main +purpose is to specify the filename and, optionally, the section name for +INI-style files, but other backends may use its fields in different ways. + +A `Config` object is created using the following parameters: +- a list of variable names to query (may be empty) +- `filename` (str, optional): the name of the file to open +- `section` (str, default ""): the name of the section within the file +- `section_specified` (bool, default false): if `section` is an empty + string, only fetch variables from the unnamed section at the start of + the file instead of defaulting to the first section in the file + +Parsing INI-style configuration files +------------------------------------- + +The `confget` library's "ini" backend parses an INI-style configuration +file. Its `read_file()` method parses the file and returns a dictionary +of sections and the variables and their values within them: + + import confget + + cfg = confget.Config([], filename='config.ini') + ini = confget.BACKENDS['ini'](cfg) + data = ini.read_file() + print('Section names: {names}'.format(names=sorted(data.keys()))) + print(data['server']['address']) + +Letting variables in a section override the default ones +-------------------------------------------------------- + +In some cases it is useful to have default values before the first +named section in a file and then override some values in various +sections. This may be useful for e.g. host-specific configuration +kept in a section with the same name as the host. + +The `format` module in the `confget` library allows, among other +filtering modes, to get the list of variables with a section +overriding the default ones: + + from confget import backend, format + + cfg = format.FormatConfig(['foo'], filename='config.ini', section='first', + section_override=True) + ini = backend.BACKENDS['ini'](cfg) + data = ini.read_file() + res = format.filter_vars(cfg, data) + assert len(res) == 1, repr(res) + print(res[0].output_full) + + cfg = format.FormatConfig(['foo'], filename='config.ini', section='second', + section_override=True) + ini = backend.BACKENDS['ini'](cfg) + data = ini.read_file() + res = format.filter_vars(cfg, data) + assert len(res) == 1, repr(res) + print(res[0].output_full) + +See the documentation of the `FormatConfig` class and the `filter_vars()` +function in the `confget.format` module for more information and for +a list of the various other filtering modes, all supported when +the library is used as a command-line tool. +""" + +from .defs import Config, VERSION_STRING # noqa: F401 +from .backend import BACKENDS # noqa: F401 diff --git a/python/confget/__main__.py b/python/confget/__main__.py new file mode 100755 index 0000000..919fcae --- /dev/null +++ b/python/confget/__main__.py @@ -0,0 +1,295 @@ +# Copyright (c) 2018, 2019 Peter Pentchev +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. 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. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. + +""" +The command-line interface to the confget module: specify some parameters +through command-line options, then display variable values or names. +""" + +from __future__ import print_function + +import argparse +import sys + +from . import backend +from . import defs +from . import format as fmt + +try: + from typing import List, Optional + + _TYPING_USED = (defs, List, Optional) +except ImportError: + pass + + +class MainConfig(fmt.FormatConfig): + # pylint: disable=too-few-public-methods,too-many-instance-attributes + """ + Extend the format config class with some output settings. + + Add the following settings: + - check_only (boolean): only check whether a variable is defined + - query_sections (boolean): only display the section names + """ + + def __init__(self, # type: MainConfig + check_only, # type: bool + filename, # type: Optional[str] + list_all, # type: bool + match_regex, # type: bool + match_var_names, # type: bool + match_var_values, # type: Optional[str] + name_prefix, # type: Optional[str] + query_sections, # type: bool + section, # type: str + section_override, # type: bool + section_specified, # type: bool + show_var_name, # type: bool + shell_escape, # type: bool + varnames # type: List[str] + ): # type: (...) -> None + # pylint: disable=too-many-arguments,too-many-locals + """ Store the specified configuration values. """ + super(MainConfig, self).__init__( + filename=filename, + list_all=list_all, + match_regex=match_regex, + match_var_names=match_var_names, + match_var_values=match_var_values, + name_prefix=name_prefix, + section=section, + section_specified=section_specified, + section_override=section_override, + show_var_name=show_var_name, + shell_escape=shell_escape, + varnames=varnames, + ) + self.check_only = check_only + self.query_sections = query_sections + + +def version(): + # type: () -> None + """ + Display program version information. + """ + print('confget ' + defs.VERSION_STRING) + + +def features(name): + # type: (Optional[str]) -> None + """ + Display a list of the features supported by the program. + """ + if name is None: + print(' '.join([ + '{name}={version}'.format(name=item[0], version=item[1]) + for item in defs.FEATURES + ])) + else: + ver = dict(defs.FEATURES).get(name, None) + if ver is None: + sys.exit(1) + print(ver) + + +def output_check_only(cfg, data): + # type: (MainConfig, defs.ConfigData) -> None + """ Check whether the variable is present. """ + if cfg.section not in data: + sys.exit(1) + elif cfg.varnames[0] not in data[cfg.section]: + sys.exit(1) + sys.exit(0) + + +def output_vars(cfg, data): + # type: (MainConfig, defs.ConfigData) -> None + """ Output the variable values. """ + for vitem in fmt.filter_vars(cfg, data): + print(vitem.output_full) + + +def output_sections(data): + # type: (defs.ConfigData) -> None + """ Output the section names. """ + for name in sorted(data.keys()): + if name != '': + print(name) + + +def validate_options(args, backend_name): + # type: (argparse.Namespace, str) -> None + """ + Make sure the command-line options are not used in an invalid combination. + """ + query_sections = args.query == 'sections' + + if args.list_all or query_sections: + if args.varnames: + sys.exit('Only a single query at a time, please!') + elif args.match_var_names: + if not args.varnames: + sys.exit('No patterns to match against') + elif args.check_only and len(args.varnames) > 1: + sys.exit('Only a single query at a time, please!') + elif not args.varnames: + sys.exit('No variables specified to query') + + if query_sections and backend_name != 'ini': + sys.exit("The query for sections is only supported for " + "the 'ini' backend for the present") + + +def check_option_conflicts(args): + # type: (argparse.Namespace) -> None + """ Make sure that the command-line options do not conflict. """ + total = int(args.query is not None) + int(args.match_var_names) + \ + int(args.list_all) + \ + int(bool(args.varnames) and + not (args.match_var_names or args.query == 'feature')) + if total > 1: + sys.exit('Only a single query at a time, please!') + + +def main(): + # type: () -> None + """ + The main program: parse arguments, do things. + """ + parser = argparse.ArgumentParser( + prog='confget', + usage=''' + confget [-t ini] -f filename [-s section] varname... + confget -V | -h | --help | --version + confget -q features''') + parser.add_argument('-c', action='store_true', dest='check_only', + help='check whether the variables are defined in ' + 'the file') + parser.add_argument('-f', type=str, dest='filename', + help='specify the configuration file name') + parser.add_argument('-L', action='store_true', dest='match_var_names', + help='specify which variables to display') + parser.add_argument('-l', action='store_true', dest='list_all', + help='list all variables in the specified section') + parser.add_argument('-m', type=str, dest='match_var_values', + help='only display variables with values that match ' + 'the specified pattern') + parser.add_argument('-N', action='store_true', dest='show_var_name', + help='always display the variable name') + parser.add_argument('-n', action='store_true', dest='hide_var_name', + help='never display the variable name') + parser.add_argument('-O', action='store_true', dest='section_override', + help='allow variables in the specified section to ' + 'override those placed before any ' + 'section definitions') + parser.add_argument('-p', type=str, dest='name_prefix', + help='display this string before the variable name') + parser.add_argument('-q', type=str, dest='query', choices=[ + 'feature', + 'features', + 'sections' + ], help='query for a specific type of information, e.g. the list of ' + 'sections defined in ' 'the configuration file') + parser.add_argument('-S', action='store_true', dest='shell_quote', + help='quote the values suitably for the Bourne shell') + parser.add_argument('-s', type=str, dest='section', + help='specify the configuration file section') + parser.add_argument('-t', type=str, default='ini', dest='backend', + help='specify the configuration file type') + parser.add_argument('-V', '--version', action='store_true', + help='display program version information and exit') + parser.add_argument('-x', action='store_true', dest='match_regex', + help='treat the match patterns as regular expressions') + parser.add_argument('varnames', nargs='*', + help='the variable names to query') + + args = parser.parse_args() + if args.version: + version() + return + + check_option_conflicts(args) + + if args.query == 'features': + if args.varnames: + sys.exit('No arguments to -q features') + features(None) + return + if args.query == 'feature': + if len(args.varnames) != 1: + sys.exit('Only a single feature name expected') + features(args.varnames[0]) + return + + query_sections = args.query == 'sections' + + cfg = MainConfig( + check_only=args.check_only, + filename=args.filename, + list_all=args.list_all, + match_regex=args.match_regex, + match_var_names=args.match_var_names, + match_var_values=args.match_var_values, + name_prefix=args.name_prefix, + query_sections=query_sections, + section=args.section if args.section is not None else '', + section_override=args.section_override, + section_specified=args.section is not None, + shell_escape=args.shell_quote, + show_var_name=args.show_var_name or + ((args.match_var_names or args.list_all or len(args.varnames) > 1) and + not args.hide_var_name), + varnames=args.varnames, + ) + + matched_backends = [name for name in sorted(backend.BACKENDS.keys()) + if name.startswith(args.backend)] + if not matched_backends: + sys.exit('Unknown backend "{name}", use "list" for a list' + .format(name=args.backend)) + elif len(matched_backends) > 1: + sys.exit('Ambiguous backend "{name}": {lst}' + .format(name=args.backend, lst=' '.join(matched_backends))) + back = backend.BACKENDS[matched_backends[0]] + + validate_options(args, matched_backends[0]) + + try: + cfgp = back(cfg) + except Exception as exc: # pylint: disable=broad-except + sys.exit(str(exc)) + data = cfgp.read_file() + + if cfg.check_only: + output_check_only(cfg, data) + elif query_sections: + output_sections(data) + else: + output_vars(cfg, data) + + +if __name__ == '__main__': + main() diff --git a/python/confget/backend/__init__.py b/python/confget/backend/__init__.py new file mode 100644 index 0000000..51be726 --- /dev/null +++ b/python/confget/backend/__init__.py @@ -0,0 +1,42 @@ +# Copyright (c) 2018, 2019 Peter Pentchev +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. 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. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. + +""" +Provide an interface to all the configuration format backends. +""" + +from . import abstract, http_get, ini + +try: + from typing import Dict, Type + + _TYPING_USED = (Dict, Type, abstract) +except ImportError: + pass + + +BACKENDS = { + 'http_get': http_get.HTTPGetBackend, + 'ini': ini.INIBackend, +} # type: Dict[str, Type[abstract.Backend]] diff --git a/python/confget/backend/abstract.py b/python/confget/backend/abstract.py new file mode 100644 index 0000000..1ee2c8d --- /dev/null +++ b/python/confget/backend/abstract.py @@ -0,0 +1,55 @@ +# Copyright (c) 2018, 2019 Peter Pentchev +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. 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. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. + +""" +An abstract metaclass for confget backends. +""" + +import abc + +import six + +from .. import defs + +try: + from typing import Dict + + _TYPING_USED = (defs, Dict) +except ImportError: + pass + + +@six.add_metaclass(abc.ABCMeta) +class Backend(object): + # pylint: disable=too-few-public-methods + """ An abstract confget parser backend. """ + def __init__(self, cfg): + # type: (Backend, defs.Config) -> None + self._cfg = cfg + + @abc.abstractmethod + def read_file(self): + # type: (Backend) -> defs.ConfigData + """ Read and parse the configuration file, invoke the callbacks. """ + raise NotImplementedError('Backend.read_file') diff --git a/python/confget/backend/http_get.py b/python/confget/backend/http_get.py new file mode 100644 index 0000000..a6f1ba4 --- /dev/null +++ b/python/confget/backend/http_get.py @@ -0,0 +1,110 @@ +# Copyright (c) 2018, 2019 Peter Pentchev +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. 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. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. + +""" +A confget backend for reading INI-style files. +""" + +import os +import re +import urllib + +from .. import defs + +from . import abstract + +try: + from typing import Dict, List + + _TYPING_USED = (defs, Dict, List) +except ImportError: + pass + + +try: + import urllib.parse # pylint: disable=ungrouped-imports + + urllib_unquote = urllib.parse.unquote # pylint: disable=invalid-name +except ImportError: + urllib_unquote = urllib.unquote # pylint: disable=invalid-name,no-member + + +RE_ENTITY = re.compile(r'^ (?P [a-zA-Z0-9_]+ ; )', re.X) + + +class HTTPGetBackend(abstract.Backend): + # pylint: disable=too-few-public-methods + """ Parse INI-style configuration files. """ + + def __init__(self, cfg): + # type: (HTTPGetBackend, defs.Config) -> None + super(HTTPGetBackend, self).__init__(cfg) + + if self._cfg.filename is not None: + raise ValueError('No config filename expected') + + qname = 'QUERY_STRING' if self._cfg.section == '' \ + else self._cfg.section + qval = os.environ.get(qname, None) + if qval is None: + raise ValueError('No "{qname}" variable in the environment' + .format(qname=qname)) + self.query_string = qval + + def read_file(self): + # type: (HTTPGetBackend) -> defs.ConfigData + def split_by_amp(line): + # type: (str) -> List[str] + """ Split a line by "&" or "&" tokens. """ + if not line: + return [] + + start = end = 0 + while True: + pos = line[start:].find('&') + if pos == -1: + return [line] + if line[pos + 1:].startswith('amp;'): + end = pos + 4 + break + entity = RE_ENTITY.match(line[pos + 1:]) + if entity is None: + end = pos + break + start = pos + len(entity.group('full')) + + return [line[:pos]] + split_by_amp(line[end + 1:]) + + data = {} # type: Dict[str, str] + fragments = split_by_amp(self.query_string) + for varval in fragments: + fields = varval.split('=') + if len(fields) == 1: + fields.append('') + elif len(fields) != 2: + raise ValueError('Invalid query string component: "{varval}"' + .format(varval=varval)) + data[urllib_unquote(fields[0])] = urllib_unquote(fields[1]) + + return {self._cfg.section: data} diff --git a/python/confget/backend/ini.py b/python/confget/backend/ini.py new file mode 100644 index 0000000..44b9470 --- /dev/null +++ b/python/confget/backend/ini.py @@ -0,0 +1,176 @@ +# Copyright (c) 2018, 2019 Peter Pentchev +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. 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. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. + +""" +A confget backend for reading INI-style files. +""" + +import re +import sys + +from .. import defs + +from . import abstract + +try: + from typing import Callable, Dict, Match, NamedTuple, Pattern + + _TYPING_USED = (defs, Dict) + + MatcherType = NamedTuple('MatcherType', [ + ('regex', Pattern[str]), + ('handle', Callable[[ + Match[str], + Dict[str, str], + defs.Config, + defs.ConfigData, + ], None]), + ]) +except ImportError: + import collections + + MatcherType = collections.namedtuple('MatcherType', [ # type: ignore + 'regex', + 'handle', + ]) + + +class INIBackend(abstract.Backend): + # pylint: disable=too-few-public-methods + """ Parse INI-style configuration files. """ + + def __init__(self, cfg, encoding='UTF-8'): + # type: (INIBackend, defs.Config, str) -> None + super(INIBackend, self).__init__(cfg) + + if self._cfg.filename is None: + raise ValueError('No config filename specified') + elif self._cfg.filename == '-': + infile = sys.stdin + else: + infile = open(self._cfg.filename, mode='r') + + reconfigure = getattr(infile, 'reconfigure', None) + if reconfigure is not None: + reconfigure(encoding=encoding) + + self.infile = infile + + def read_file(self): + # type: (INIBackend) -> defs.ConfigData + state = { + 'section': '', + 'name': '', + 'value': '', + 'cont': '', + 'found': '', + } + res = {'': {}} # type: defs.ConfigData + + def handle_section(match, # type: Match[str] + state, # type: Dict[str, str] + cfg, # type: defs.Config + res # type: defs.ConfigData + ): # type: (...) -> None + """ Handle a section heading: store the name. """ + state['section'] = match.group('name') + if state['section'] not in res: + res[state['section']] = {} + if not (cfg.section_specified or cfg.section or state['found']): + cfg.section = state['section'] + state['found'] = state['section'] + + def handle_comment(_match, # type: Match[str] + _state, # type: Dict[str, str] + _cfg, # type: defs.Config + _res # type: defs.ConfigData + ): # type: (...) -> None + """ Handle a comment line: ignore it. """ + pass # pylint: disable=unnecessary-pass + + def handle_variable(match, # type: Match[str] + state, # type: Dict[str, str] + _cfg, # type: defs.Config + res # type: defs.ConfigData + ): # type: (...) -> None + """ Handle an assignment: store, check for a continuation. """ + state['name'] = match.group('name') + state['value'] = match.group('value') + state['cont'] = match.group('cont') + state['found'] = state['name'] + if not state['cont']: + res[state['section']][state['name']] = state['value'] + + matches = [ + MatcherType( + regex=re.compile(r'^ \s* (?: [#;] .* )? $', re.X), + handle=handle_comment, + ), + MatcherType( + regex=re.compile(r''' + ^ + \s* \[ \s* + (?P [^\]]+? ) + \s* \] \s* + $''', + re.X), + handle=handle_section, + ), + MatcherType( + regex=re.compile(r''' + ^ + \s* + (?P \S+ ) + \s* = \s* + (?P .*? ) + \s* + (?P [\\] )? + $''', re.X), + handle=handle_variable, + ), + ] + + for line in self.infile.readlines(): + line = line.rstrip('\r\n') + if state['cont']: + if line.endswith('\\'): + line, state['cont'] = line[:-1], line[-1] + else: + state['cont'] = '' + state['value'] += line + if not state['cont']: + res[state['section']][state['name']] = state['value'] + continue + + for data in matches: + match = data.regex.match(line) + if match is None: + continue + data.handle(match, state, self._cfg, res) + break + else: + raise ValueError('Unexpected line in {fname}: {line}' + .format(fname=self._cfg.filename, line=line)) + + return res diff --git a/python/confget/defs.py b/python/confget/defs.py new file mode 100644 index 0000000..21ae7c5 --- /dev/null +++ b/python/confget/defs.py @@ -0,0 +1,59 @@ +# Copyright (c) 2018, 2019 Peter Pentchev +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. 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. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. + +""" +Common definitions for the confget configuration parsing library. +""" + +try: + from typing import Dict, List, Optional + + _TYPING_USED = (List, Optional) + + ConfigData = Dict[str, Dict[str, str]] +except ImportError: + pass + + +VERSION_STRING = '2.2.0' +FEATURES = [ + ('BASE', VERSION_STRING), +] + + +class Config(object): + # pylint: disable=too-few-public-methods + """ Base class for the internal confget configuration. """ + + def __init__(self, # type: Config + varnames, # type: List[str] + filename=None, # type: Optional[str] + section='', # type: str + section_specified=False, # type: bool + ): # type: (...) -> None + """ Store the specified configuration values. """ + self.filename = filename + self.section = section + self.section_specified = section_specified + self.varnames = varnames diff --git a/python/confget/format.py b/python/confget/format.py new file mode 100644 index 0000000..d2265b5 --- /dev/null +++ b/python/confget/format.py @@ -0,0 +1,221 @@ +# Copyright (c) 2018, 2019 Peter Pentchev +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. 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. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. + +""" +Filter and format a subset of the configuration variables. +""" + +import fnmatch +import re + +try: + from typing import Callable, Dict, Iterable, NamedTuple, List, Optional + + _TYPING_USED = (Callable, Dict, Iterable, List, Optional) + + FormatOutput = NamedTuple('FormatOutput', [ + ('name', str), + ('value', str), + ('output_name', str), + ('output_value', str), + ('output_full', str), + ]) +except ImportError: + import collections + + FormatOutput = collections.namedtuple('FormatOutput', [ # type: ignore + 'name', + 'value', + 'output_name', + 'output_value', + 'output_full', + ]) + +from . import defs + + +class FormatConfig(defs.Config): + # pylint: disable=too-few-public-methods,too-many-instance-attributes + """ + Extend the config class with some output settings. + + Add the following settings: + - list_all (boolean): list all variables, not just a subset + - match_regex (boolean): for match_var_names and match_var_values, + perform regular expression matches instead of filename pattern ones + - match_var_names (boolean): treat the variable names specified as + patterns and display all variables that match those + - match_var_values (string): display only the variables with values + that match this pattern + - name_prefix (string): when displaying variable names, prefix them + with this string + - show_var_name (boolean): display the variable names, not just + the values + - shell_escape (boolean): format the values in a manner suitable for + the Bourne shell + """ + + def __init__(self, # type: FormatConfig + varnames, # type: List[str] + filename=None, # type: Optional[str] + list_all=False, # type: bool + match_regex=False, # type: bool + match_var_names=False, # type: bool + match_var_values=None, # type: Optional[str] + name_prefix=None, # type: Optional[str] + section='', # type: str + section_override=False, # type: bool + section_specified=False, # type: bool + show_var_name=False, # type: bool + shell_escape=False, # type: bool + ): # type: (...) -> None + # pylint: disable=too-many-arguments + """ Store the specified configuration values. """ + super(FormatConfig, self).__init__( + filename=filename, + section=section, + section_specified=section_specified, + varnames=varnames, + ) + self.list_all = list_all + self.match_regex = match_regex + self.match_var_names = match_var_names + self.match_var_values = match_var_values + self.name_prefix = name_prefix + self.section_override = section_override + self.shell_escape = shell_escape + self.show_var_name = show_var_name + + def __repr__(self): + # type: (FormatConfig) -> str + return '{tname}({varnames}, {attrs})'.format( + tname=type(self).__name__, + varnames=repr(self.varnames), + attrs=', '.join([ + '{name}={value}'.format( + name=name, value=repr(getattr(self, name))) + for name in [ + 'filename', + 'list_all', + 'match_regex', + 'match_var_names', + 'match_var_values', + 'name_prefix', + 'section', + 'section_override', + 'section_specified', + 'show_var_name', + 'shell_escape', + ] + ])) + + +def get_check_function(cfg, patterns): + # type: (FormatConfig, List[str]) -> Callable[[str], bool] + """ + Get a function that determines whether a variable name should be + included in the displayed subset. + """ + if cfg.match_regex: + re_vars = [re.compile(name) for name in patterns] + + def check_re_vars(key): + # type: (str) -> bool + """ Check that the key matches any of the specified regexes. """ + return any(rex.search(key) for rex in re_vars) + + return check_re_vars + + def check_fn_vars(key): + # type: (str) -> bool + """ Check that the key matches any of the specified patterns. """ + return any(fnmatch.fnmatch(key, pattern) + for pattern in patterns) + + return check_fn_vars + + +def get_varnames(cfg, sect_data): + # type: (FormatConfig, Dict[str, str]) -> Iterable[str] + """ Get the variable names that match the configuration requirements. """ + if cfg.list_all: + varnames = sect_data.keys() # type: Iterable[str] + elif cfg.match_var_names: + check_var = get_check_function(cfg, cfg.varnames) + varnames = [name for name in sect_data.keys() if check_var(name)] + else: + varnames = [name for name in cfg.varnames if name in sect_data] + + if not cfg.match_var_values: + return varnames + + check_value = get_check_function(cfg, [cfg.match_var_values]) + return [name for name in varnames if check_value(sect_data[name])] + + +def filter_vars(cfg, data): + # type: (FormatConfig, defs.ConfigData) -> Iterable[FormatOutput] + """ + Filter the variables in the configuration file according to + the various criteria specified in the configuration. + Return an iterable of FormatOutput structures allowing the caller to + process the variable names and values in various ways. + """ + if cfg.section_override: + sect_data = data[''] + else: + sect_data = {} + if cfg.section in data: + sect_data.update(data[cfg.section]) + + varnames = get_varnames(cfg, sect_data) + res = [] # type: List[FormatOutput] + for name in sorted(varnames): + if cfg.name_prefix: + output_name = cfg.name_prefix + name + else: + output_name = name + + value = sect_data[name] + if cfg.shell_escape: + output_value = "'{esc}'".format( + esc="'\"'\"'".join(value.split("'"))) + else: + output_value = value + + if cfg.show_var_name: + output_full = '{name}={value}'.format(name=output_name, + value=output_value) + else: + output_full = output_value + + res.append(FormatOutput( + name=name, + value=value, + output_name=output_name, + output_value=output_value, + output_full=output_full, + )) + + return res diff --git a/python/confget/py.typed b/python/confget/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/python/setup.cfg b/python/setup.cfg new file mode 100644 index 0000000..2a9acf1 --- /dev/null +++ b/python/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal = 1 diff --git a/python/setup.py b/python/setup.py new file mode 100644 index 0000000..2d9f620 --- /dev/null +++ b/python/setup.py @@ -0,0 +1,132 @@ +#!/usr/bin/python3 +# +# Copyright (c) 2019 Peter Pentchev +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. 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. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. + +""" +Setup infrastructure for confget, the configuration file parser. +""" + +import re +import setuptools # type: ignore + + +RE_VERSION = r'''^ + \s* VERSION_STRING \s* = \s* ' + (?P + (?: 0 | [1-9][0-9]* ) # major + \. (?: 0 | [1-9][0-9]* ) # minor + \. (?: 0 | [1-9][0-9]* ) # patchlevel + (?: \. [a-zA-Z0-9]+ )? # optional addendum (dev1, beta3, etc.) + ) + ' \s* + $''' + + +def get_version(): + # type: () -> str + """ Get the version string from the module's __init__ file. """ + found = None + re_semver = re.compile(RE_VERSION, re.X) + with open('confget/defs.py') as init: + for line in init.readlines(): + match = re_semver.match(line) + if not match: + continue + assert found is None + found = match.group('version') + + assert found is not None + return found + + +def get_long_description(): + # type: () -> str + """ Get the package long description from the README file. """ + with open('README.md') as readme: + return readme.read() + + +setuptools.setup( + name='confget', + version=get_version(), + + description='Parse configuration files and extract values from them', + long_description=get_long_description(), + long_description_content_type='text/markdown', + + author='Peter Pentchev', + author_email='roam@ringlet.net', + url='https://devel.ringlet.net/textproc/confget/', + + packages=['confget', 'confget.backend'], + package_data={ + 'confget': [ + # The typed module marker + 'py.typed', + ], + }, + + install_requires=[ + 'six', + ], + + license='BSD-2', + classifiers=[ + 'Development Status :: 5 - Production/Stable', + + 'Environment :: Console', + + 'Intended Audience :: Developers', + 'Intended Audience :: System Administrators', + + 'License :: DFSG approved', + 'License :: Freely Distributable', + 'License :: OSI Approved :: BSD License', + + 'Operating System :: POSIX', + 'Operating System :: Unix', + + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + + 'Topic :: Software Development :: Libraries', + 'Topic :: Software Development :: Libraries :: Python Modules', + 'Topic :: Utilities', + ], + + entry_points={ + 'console_scripts': [ + 'confget=confget.__main__:main', + ], + }, + + zip_safe=True, +) diff --git a/python/stubs/urllib/__init__.py b/python/stubs/urllib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/stubs/urllib/__init__.pyi b/python/stubs/urllib/__init__.pyi new file mode 100644 index 0000000..d86965b --- /dev/null +++ b/python/stubs/urllib/__init__.pyi @@ -0,0 +1,27 @@ +# Copyright (c) 2018, 2019 Peter Pentchev +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. 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. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. + +def unquote(name): + # type: (str) -> str + ... diff --git a/python/stubs/urllib/parse.pyi b/python/stubs/urllib/parse.pyi new file mode 100644 index 0000000..d86965b --- /dev/null +++ b/python/stubs/urllib/parse.pyi @@ -0,0 +1,27 @@ +# Copyright (c) 2018, 2019 Peter Pentchev +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. 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. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. + +def unquote(name): + # type: (str) -> str + ... diff --git a/python/stubs/urllib/py.typed b/python/stubs/urllib/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/python/tox.ini b/python/tox.ini new file mode 100644 index 0000000..57f11d4 --- /dev/null +++ b/python/tox.ini @@ -0,0 +1,143 @@ +# Copyright (c) 2018, 2019 Peter Pentchev +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. 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. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. + +[tox] +envlist = pep8,mypy2,mypy3,prove_2,prove_2_nt,prove_3,unit_tests_2,unit_tests_2_nt,unit_tests_3,pylint +skipsdist = True + +[testenv:pep8] +basepython = python3 +deps = + flake8 +commands = + flake8 confget setup.py unit_tests ../t/defs/tools/generate.py + +[testenv:mypy2] +basepython = python3 +deps = + mypy +setenv = + MYPYPATH={toxinidir}/stubs +commands = + mypy --strict --py2 confget setup.py + mypy --strict --py2 --allow-untyped-decorators unit_tests + +[testenv:mypy3] +basepython = python3 +deps = + mypy +setenv = + MYPYPATH={toxinidir}/stubs +commands = + mypy --strict confget setup.py ../t/defs/tools/generate.py + mypy --strict --allow-untyped-decorators unit_tests + +[testenv:pylint] +basepython = python3 +deps = + ddt + pylint + pytest +commands = + pylint --disable=useless-object-inheritance,duplicate-code confget setup.py unit_tests ../t/defs/tools/generate.py + +[testenv:unit_tests_2] +basepython = python2 +deps = + ddt + pytest + typing +setenv = + TESTDIR={toxinidir}/../t +commands = + pytest -s -vv unit_tests + +[testenv:unit_tests_2_nt] +basepython = python2 +deps = + ddt + pytest +setenv = + TESTDIR={toxinidir}/../t +commands = + pytest -s -vv unit_tests + +[testenv:unit_tests_3] +basepython = python3 +deps = + ddt + pytest +setenv = + TESTDIR={toxinidir}/../t +commands = + pytest -s -vv unit_tests + +[testenv:generate] +basepython = python3 +deps = + six +whitelist_externals = + sh +commands = + sh -c 'cd ../t && env PYTHONPATH={toxinidir} python defs/tools/generate.py' + +[testenv:prove_2] +basepython = python2 +deps = + six + typing +setenv = + CONFGET=python -m confget + MANPAGE={toxinidir}/../confget.1 + TESTDIR={toxinidir}/../t +whitelist_externals = + prove +commands = + prove ../t + +[testenv:prove_2_nt] +basepython = python2 +deps = + six +setenv = + CONFGET=python -m confget + MANPAGE={toxinidir}/../confget.1 + TESTDIR={toxinidir}/../t +whitelist_externals = + prove +commands = + prove ../t + +[testenv:prove_3] +basepython = python3 +deps = + six +setenv = + CONFGET=python -m confget + MANPAGE={toxinidir}/../confget.1 + TESTDIR={toxinidir}/../t +whitelist_externals = + prove +commands = + prove ../t diff --git a/python/unit_tests/__init__.py b/python/unit_tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/unit_tests/data/__init__.py b/python/unit_tests/data/__init__.py new file mode 100644 index 0000000..fbf1975 --- /dev/null +++ b/python/unit_tests/data/__init__.py @@ -0,0 +1,31 @@ +# Copyright (c) 2018, 2019 Peter Pentchev +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. 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. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. + +""" +Data structures and definitions for the confget unit tests. +""" + +from .defs import CMDLINE_OPTIONS, XFORM, TestDef # noqa: F401 +from .load import load_all_tests # noqa: F401 +from .util import shell_escape # noqa: F401 diff --git a/python/unit_tests/data/defs.py b/python/unit_tests/data/defs.py new file mode 100644 index 0000000..759487a --- /dev/null +++ b/python/unit_tests/data/defs.py @@ -0,0 +1,295 @@ +# Copyright (c) 2018, 2019 Peter Pentchev +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. 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. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. + +""" +Class definitions for the confget test suite. +""" + +import abc +import os + +import six + +from confget import backend as cbackend +from confget import format as cformat + +from . import util + + +try: + from typing import Any, Dict, Iterable, List, Optional, Type + + _TYPING_USED = (Any, Dict, Iterable, List, Optional, Type) +except ImportError: + pass + + +CMDLINE_OPTIONS = { + 'check_only': ('-c', False), + 'filename': ('-f', True), + 'hide_var_name': ('-n', False), + 'list_all': ('-l', False), + 'match_var_names': ('-L', False), + 'match_var_values': ('-m', True), + 'section': ('-s', True), + 'section_override': ('-O', False), + 'section_specified': ('', False), + 'show_var_name': ('-N', False), +} + + +@six.add_metaclass(abc.ABCMeta) +class XFormType(object): + """ Transform something to something else with great prejudice. """ + + @abc.abstractproperty + def command(self): + # type: (XFormType) -> str + """ Get the shell command to transform the confget output. """ + raise NotImplementedError( + '{tname}.command'.format(tname=type(self).__name__)) + + @abc.abstractmethod + def do_xform(self, res): + # type: (XFormType, Iterable[cformat.FormatOutput]) -> str + """ Transform the Python representation of the result. """ + raise NotImplementedError( + '{tname}.do_xform()'.format(tname=type(self).__name__)) + + +class XFormNone(XFormType): + """ No transformation, newlines preserved. """ + + @property + def command(self): + # type: (XFormNone) -> str + return '' + + def do_xform(self, res): + # type: (XFormNone, Iterable[cformat.FormatOutput]) -> str + xform = '\n'.join([line.output_full for line in res]) # type: str + return xform + + +class XFormNewlineToSpace(XFormType): + """ Translate newlines to spaces. """ + + @property + def command(self): + # type: (XFormNewlineToSpace) -> str + return '| tr "\\n" " "' + + def do_xform(self, res): + # type: (XFormNewlineToSpace, Iterable[cformat.FormatOutput]) -> str + xform = ''.join([line.output_full + ' ' for line in res]) # type: str + return xform + + +class XFormCountLines(XFormType): + """ Count the lines output by confget. """ + + def __init__(self, sought=None, sought_in=True): + # type: (XFormCountLines, Optional[str], bool) -> None + super(XFormCountLines, self).__init__() + self.sought = sought + self.sought_in = sought_in + + @property + def command(self): + # type: (XFormCountLines) -> str + if self.sought: + prefix = '| fgrep -{inv}e {sought} '.format( + inv='' if self.sought_in else 'v', + sought=util.shell_escape(self.sought)) + else: + prefix = '' + return prefix + "| wc -l | tr -d ' '" + + def do_xform(self, res): + # type: (XFormCountLines, Iterable[cformat.FormatOutput]) -> str + if self.sought: + return str(len( + [line for line in res + if self.sought_in == (self.sought in line.output_full)])) + return str(len([line for line in res])) + + +XFORM = { + '': XFormNone(), + 'count-lines': XFormCountLines(), + 'count-lines-eq': XFormCountLines(sought='='), + 'count-lines-non-eq': XFormCountLines(sought='=', sought_in=False), + 'newline-to-space': XFormNewlineToSpace(), +} + + +@six.add_metaclass(abc.ABCMeta) +class TestOutputDef(object): + """ A definition for a single test's output. """ + + def __init(self): + # type: (TestOutputDef) -> None + """ No initialization at all for the base class. """ + + @abc.abstractmethod + def get_check(self): + # type: (TestOutputDef) -> str + """ Get the check string as a shell command. """ + raise NotImplementedError( + '{name}.get_check()'.format(name=type(self).__name__)) + + @abc.abstractproperty + def var_name(self): + # type: (TestOutputDef) -> str + """ Get the variable name to display. """ + raise NotImplementedError( + '{name}.var_name'.format(name=type(self).__name__)) + + @abc.abstractmethod + def check_result(self, _res): + # type: (TestOutputDef, str) -> None + """ Check whether the processed confget result is correct. """ + raise NotImplementedError( + '{name}.check_result()'.format(name=type(self).__name__)) + + +class TestExactOutputDef(TestOutputDef): + """ Check that the program output this exact string. """ + + def __init__(self, exact): + # type: (TestExactOutputDef, str) -> None + """ Initialize an exact test output object. """ + self.exact = exact + + def get_check(self): + # type: (TestExactOutputDef) -> str + return '[ "$v" = ' + util.shell_escape(self.exact) + ' ]' + + @property + def var_name(self): + # type: (TestExactOutputDef) -> str + return 'v' + + def check_result(self, res): + # type: (TestExactOutputDef, str) -> None + assert res == self.exact + + +class TestExitOKOutputDef(TestOutputDef): + """ Check that the program succeeded or failed as expected. """ + + def __init__(self, success): + # type: (TestExitOKOutputDef, bool) -> None + """ Initialize an "finished successfully" test output object. """ + self.success = success + + def get_check(self): + # type: (TestExitOKOutputDef) -> str + return '[ "$res" {compare} 0 ]'.format( + compare="=" if self.success else "!=") + + @property + def var_name(self): + # type: (TestExitOKOutputDef) -> str + return 'res' + + def check_result(self, res): + # type: (TestExitOKOutputDef, str) -> None + # pylint: disable=useless-super-delegation + super(TestExitOKOutputDef, self).check_result(res) + + +class TestDef: + # pylint: disable=too-few-public-methods + """ A definition for a single test. """ + + def __init__(self, # type: TestDef + args, # type: Dict[str, str] + keys, # type: List[str] + output, # type: TestOutputDef + xform='', # type: str + backend='ini', # type: str + setenv=False, # type: bool + stdin=None, # type: Optional[str] + ): # type: (...) -> None + # pylint: disable=too-many-arguments + """ Initialize a test object. """ + + self.args = args + self.keys = keys + self.xform = xform + self.output = output + self.backend = backend + self.setenv = setenv + self.stdin = stdin + + def get_backend(self): + # type: (TestDef) -> Type[cbackend.abstract.Backend] + """ Get the appropriate confget backend type. """ + return cbackend.BACKENDS[self.backend] + + def get_config(self): + # type: (TestDef) -> cformat.FormatConfig + """ Convert the test's data to a config object. """ + data = {} # type: Dict[str, Any] + for name, value in self.args.items(): + if name == 'hide_var_name': + continue + + opt = CMDLINE_OPTIONS[name] + if opt[1]: + data[name] = value + else: + data[name] = True + + if 'filename' in data: + data['filename'] = os.environ['TESTDIR'] + '/' + data['filename'] + elif self.stdin: + data['filename'] = '-' + + data['show_var_name'] = 'show_var_name' in self.args or \ + (('match_var_names' in self.args or + 'list_all' in self.args or + len(self.keys) > 1) and + 'hide_var_name' not in self.args) + + return cformat.FormatConfig(self.keys, **data) + + def do_xform(self, res): + # type: (TestDef, Iterable[cformat.FormatOutput]) -> str + """ Return the output delimiter depending on the xform property. """ + return XFORM[self.xform].do_xform(res) + + +class TestFileDef: + # pylint: disable=too-few-public-methods + """ A definition for a file defining related tests. """ + + def __init__(self, # type: TestFileDef + tests, # type: List[TestDef] + setenv=None, # type: Optional[Dict[str, str]] + ): # type: (...) -> None + """ Initialize a test file object. """ + self.tests = tests + self.setenv = {} if setenv is None else setenv diff --git a/python/unit_tests/data/load.py b/python/unit_tests/data/load.py new file mode 100644 index 0000000..cf23f54 --- /dev/null +++ b/python/unit_tests/data/load.py @@ -0,0 +1,106 @@ +# Copyright (c) 2018, 2019 Peter Pentchev +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. 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. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. + +""" +Load a test definition from a JSON file. +""" +# pylint: disable=consider-using-dict-comprehension + +import json +import os + +from . import defs + +try: + from typing import Any, Dict + + TESTING_USED = (defs, Any, Dict) +except ImportError: + pass + + +def _load_test_v1(data, _version): + # type: (Dict[str, Any], Dict[str, int]) -> defs.TestFileDef + """ Load the tests from a v1.x test file. """ + build = { + 'setenv': data.get('setenv', {}), + 'tests': [], + } + + for test in data['tests']: + raw = dict([ + (key, value) for key, value in test.items() + if key in ('args', 'keys', 'xform', 'backend', 'setenv', 'stdin') + ]) + + if 'exact' in test['output']: + raw['output'] = defs.TestExactOutputDef( + exact=test['output']['exact']) + elif 'exit' in test['output']: + raw['output'] = defs.TestExitOKOutputDef( + success=test['output']['exit']) + else: + raise ValueError('test output: ' + repr(test['output'])) + + build['tests'].append(defs.TestDef(**raw)) + + return defs.TestFileDef(**build) + + +_PARSERS = { + 1: _load_test_v1, +} + + +def load_test(fname): + # type: (str) -> defs.TestFileDef + """ Load a single test file into a TestFileDef object. """ + with open(fname, mode='r') as testf: + data = json.load(testf) + + version = { + 'major': data['format']['version']['major'], + 'minor': data['format']['version']['minor'], + } + assert isinstance(version['major'], int) + assert isinstance(version['minor'], int) + + parser = _PARSERS.get(version['major'], None) + if parser is not None: + return parser(data, version) + raise NotImplementedError( + 'Unsupported test file format version {major}.{minor} for {fname}' + .format(major=version['major'], minor=version['minor'], fname=fname)) + + +def load_all_tests(testdir): + # type: (str) -> Dict[str, defs.TestFileDef] + """ Load all the tests in the defs/tests/ subdirectory. """ + tdir = testdir + '/defs/tests/' + filenames = sorted(fname for fname in os.listdir(tdir) + if fname.endswith('.json')) + return dict([ + (os.path.splitext(fname)[0], load_test(tdir + fname)) + for fname in filenames + ]) diff --git a/python/unit_tests/data/util.py b/python/unit_tests/data/util.py new file mode 100644 index 0000000..32d7489 --- /dev/null +++ b/python/unit_tests/data/util.py @@ -0,0 +1,45 @@ +# Copyright (c) 2018, 2019 Peter Pentchev +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. 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. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. + +""" +Utility functions for the confget test cases. +""" + +import re + + +_RE_APOS = re.compile("(?P
 [^']* ) (?P '+ ) (?P .* )", re.X)
+
+
+def shell_escape(value):
+    # type: (str) -> str
+    """ Escape a value for the shell. """
+    res = "'"
+    while True:
+        match = _RE_APOS.match(value)
+        if not match:
+            return res + value + "'"
+
+        res += match.group('pre') + '\'"' + match.group('apos') + '"\''
+        value = match.group('post')
diff --git a/python/unit_tests/test_run.py b/python/unit_tests/test_run.py
new file mode 100644
index 0000000..9a3c13b
--- /dev/null
+++ b/python/unit_tests/test_run.py
@@ -0,0 +1,136 @@
+# Copyright (c) 2018, 2019  Peter Pentchev 
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+#    notice, this list of conditions and the following disclaimer.
+# 2. 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.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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.
+
+"""
+Run the confget tests using the Python methods.
+
+Load the test data, then run the tests using the objects provided by
+the Python library, not by executing the command-line tool.
+"""
+
+from __future__ import print_function
+
+import itertools
+import os
+import sys
+import unittest
+
+import ddt  # type: ignore
+import pytest  # type: ignore
+
+from confget import format as cformat
+
+pytest.register_assert_rewrite('unit_tests.data.defs')
+
+from . import data as tdata  # noqa: E402 pylint: disable=wrong-import-position
+
+
+try:
+    from typing import Dict
+
+    _TYPING_USED = (Dict,)
+except ImportError:
+    pass
+
+
+TESTS = tdata.load_all_tests(os.environ['TESTDIR'])
+
+FULL_TEST_DATA = sorted(itertools.chain(*[
+    [
+        (
+            tfile[0],
+            idx,
+            tfile[1].setenv,
+            test,
+        )
+        for idx, test in enumerate(tfile[1].tests)
+    ]
+    for tfile in TESTS.items()
+]))
+
+SKIP_ARGS = set(['check_only'])
+
+
+@ddt.ddt
+class TestStuff(unittest.TestCase):
+    # pylint: disable=no-self-use
+    """ Run the tests using the Python library. """
+
+    @ddt.data(
+        *FULL_TEST_DATA
+    )
+    @ddt.unpack
+    def test_run(self,      # type: TestStuff
+                 fname,     # type: str
+                 idx,       # type: int
+                 setenv,    # type: Dict[str, str]
+                 test,      # type: tdata.TestDef
+                 ):         # type: (...) -> None
+        """ Instantiate a confget object, load the data, check it. """
+        print('')
+
+        save_env = dict(os.environ)
+        try:
+            print('fname {fname} {idx:2} setenv {count} keys {kkk}'
+                  .format(fname=fname, idx=idx, count=len(setenv.keys()),
+                          kkk=test.keys))
+            if set(test.args.keys()) & SKIP_ARGS:
+                print('- skipping: {skip}'.format(
+                    skip=' '.join(sorted(set(test.args.keys()) & SKIP_ARGS))))
+                return
+
+            backend = test.get_backend()
+            print('- backend {back}'.format(back=backend))
+            config = test.get_config()
+            print('- config {cfg}'.format(cfg=config))
+            if test.setenv:
+                os.environ.update(setenv)
+
+            if test.stdin:
+                fname = os.environ['TESTDIR'] + '/' + test.stdin
+                print('- reopening {fname} as stdin'.format(fname=fname))
+                with open(fname, mode='r') as stdin:
+                    save_stdin = sys.stdin
+                    try:
+                        sys.stdin = stdin
+                        obj = backend(config)
+                        print('- obj {obj}'.format(obj=obj))
+                        data = obj.read_file()
+                    finally:
+                        sys.stdin = save_stdin
+            else:
+                obj = backend(config)
+                print('- obj {obj}'.format(obj=obj))
+                data = obj.read_file()
+
+            print('- sections: {sect}'.format(sect=sorted(data.keys())))
+            res = cformat.filter_vars(config, data)
+            print('- result: {res}'.format(res=res))
+            output = test.do_xform(res)
+            print('- transformed: {output}'.format(output=repr(output)))
+            test.output.check_result(output)
+        finally:
+            if test.setenv:
+                os.environ.clear()
+                os.environ.update(save_env)
diff --git a/t/01-get-values.t b/t/01-get-values.t
new file mode 100644
index 0000000..f338516
--- /dev/null
+++ b/t/01-get-values.t
@@ -0,0 +1,90 @@
+#!/bin/sh
+#
+# Copyright (c) 2008, 2011, 2019  Peter Pentchev
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+#    notice, this list of conditions and the following disclaimer.
+# 2. 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.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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.
+
+[ -z "$CONFGET" ] && CONFGET='./confget'
+[ -z "$TESTDIR" ] && TESTDIR='t'
+
+echo '1..18'
+
+
+if [ ! -f "$TESTDIR/t1.ini" ]; then
+        echo "Bail out!  No test file $TESTDIR/t1.ini"
+        exit 255
+fi
+v=`$CONFGET '-f' "$TESTDIR/t1.ini" '-s' 'a' 'key1' `
+res="$?"
+if [ "$v" = 'value1' ]; then echo 'ok 1'; else echo "not ok 1 v is '$v'"; fi
+v=`$CONFGET '-f' "$TESTDIR/t1.ini" '-s' 'a' '-N' 'key2' `
+res="$?"
+if [ "$v" = 'key2=value2' ]; then echo 'ok 2'; else echo "not ok 2 v is '$v'"; fi
+v=`$CONFGET '-f' "$TESTDIR/t1.ini" '-s' 'a' 'key3' `
+res="$?"
+if [ "$v" = '		 val'"'"'ue3' ]; then echo 'ok 3'; else echo "not ok 3 v is '$v'"; fi
+v=`$CONFGET '-f' "$TESTDIR/t1.ini" '-s' 'b sect' 'key4' `
+res="$?"
+if [ "$v" = 'v'"'"'alu'"'"'e4' ]; then echo 'ok 4'; else echo "not ok 4 v is '$v'"; fi
+v=`$CONFGET '-f' "$TESTDIR/t1.ini" '-s' 'c' 'key5' `
+res="$?"
+if [ "$v" = '		# value5' ]; then echo 'ok 5'; else echo "not ok 5 v is '$v'"; fi
+v=`$CONFGET '-f' "$TESTDIR/t1.ini" '-s' 'a' 'key1' 'key2' | tr "\n" " "`
+res="$?"
+if [ "$v" = 'key1=value1 key2=value2 ' ]; then echo 'ok 6'; else echo "not ok 6 v is '$v'"; fi
+v=`$CONFGET '-f' "$TESTDIR/t1.ini" '-s' 'a' '-n' 'key6' 'key2' | tr "\n" " "`
+res="$?"
+if [ "$v" = 'value2 value6 ' ]; then echo 'ok 7'; else echo "not ok 7 v is '$v'"; fi
+v=`$CONFGET '-f' "$TESTDIR/t1.ini" '-s' 'b sect' 'key7' `
+res="$?"
+if [ "$v" = 'value7' ]; then echo 'ok 8'; else echo "not ok 8 v is '$v'"; fi
+v=`$CONFGET '-s' 'b sect' -f - 'key7' < "$TESTDIR/t1.ini" `
+res="$?"
+if [ "$v" = 'value7' ]; then echo 'ok 9'; else echo "not ok 9 v is '$v'"; fi
+v=`$CONFGET '-s' 'b sect' -f - 'key77' < "$TESTDIR/t1.ini" `
+res="$?"
+if [ "$v" = '' ]; then echo 'ok 10'; else echo "not ok 10 v is '$v'"; fi
+v=`env Q1='key4&key5=%09%09%20val%27ue5&key6' Q2='' QUERY_STRING='key1=value1&key2=%3Dvalue2%26&key3=%09%09%20val%27ue3' $CONFGET -t http_get 'key1' `
+res="$?"
+if [ "$v" = 'value1' ]; then echo 'ok 11'; else echo "not ok 11 v is '$v'"; fi
+v=`env Q1='key4&key5=%09%09%20val%27ue5&key6' Q2='' QUERY_STRING='key1=value1&key2=%3Dvalue2%26&key3=%09%09%20val%27ue3' $CONFGET -t http_get '-N' 'key2' `
+res="$?"
+if [ "$v" = 'key2==value2&' ]; then echo 'ok 12'; else echo "not ok 12 v is '$v'"; fi
+v=`env Q1='key4&key5=%09%09%20val%27ue5&key6' Q2='' QUERY_STRING='key1=value1&key2=%3Dvalue2%26&key3=%09%09%20val%27ue3' $CONFGET -t http_get 'key3' `
+res="$?"
+if [ "$v" = '		 val'"'"'ue3' ]; then echo 'ok 13'; else echo "not ok 13 v is '$v'"; fi
+v=`env Q1='key4&key5=%09%09%20val%27ue5&key6' Q2='' QUERY_STRING='key1=value1&key2=%3Dvalue2%26&key3=%09%09%20val%27ue3' $CONFGET -t http_get '-s' 'Q1' 'key4' `
+res="$?"
+if [ "$v" = '' ]; then echo 'ok 14'; else echo "not ok 14 v is '$v'"; fi
+v=`env Q1='key4&key5=%09%09%20val%27ue5&key6' Q2='' QUERY_STRING='key1=value1&key2=%3Dvalue2%26&key3=%09%09%20val%27ue3' $CONFGET -t http_get '-s' 'Q1' 'key5' `
+res="$?"
+if [ "$v" = '		 val'"'"'ue5' ]; then echo 'ok 15'; else echo "not ok 15 v is '$v'"; fi
+v=`env Q1='key4&key5=%09%09%20val%27ue5&key6' Q2='' QUERY_STRING='key1=value1&key2=%3Dvalue2%26&key3=%09%09%20val%27ue3' $CONFGET -t http_get '-s' 'Q1' 'key6' `
+res="$?"
+if [ "$v" = '' ]; then echo 'ok 16'; else echo "not ok 16 v is '$v'"; fi
+v=`env Q1='key4&key5=%09%09%20val%27ue5&key6' Q2='' QUERY_STRING='key1=value1&key2=%3Dvalue2%26&key3=%09%09%20val%27ue3' $CONFGET -t http_get '-s' 'Q1' 'key66' `
+res="$?"
+if [ "$v" = '' ]; then echo 'ok 17'; else echo "not ok 17 v is '$v'"; fi
+v=`env Q1='key4&key5=%09%09%20val%27ue5&key6' Q2='' QUERY_STRING='key1=value1&key2=%3Dvalue2%26&key3=%09%09%20val%27ue3' $CONFGET -t http_get '-s' 'Q2' 'key66' `
+res="$?"
+if [ "$v" = '' ]; then echo 'ok 18'; else echo "not ok 18 v is '$v'"; fi
diff --git a/t/02-check-values.t b/t/02-check-values.t
new file mode 100644
index 0000000..f91fab3
--- /dev/null
+++ b/t/02-check-values.t
@@ -0,0 +1,81 @@
+#!/bin/sh
+#
+# Copyright (c) 2008, 2011, 2019  Peter Pentchev
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+#    notice, this list of conditions and the following disclaimer.
+# 2. 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.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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.
+
+[ -z "$CONFGET" ] && CONFGET='./confget'
+[ -z "$TESTDIR" ] && TESTDIR='t'
+
+echo '1..15'
+
+
+if [ ! -f "$TESTDIR/t1.ini" ]; then
+        echo "Bail out!  No test file $TESTDIR/t1.ini"
+        exit 255
+fi
+v=`$CONFGET '-c' '-f' "$TESTDIR/t1.ini" '-s' 'a' 'key1' `
+res="$?"
+if [ "$res" = 0 ]; then echo 'ok 1'; else echo "not ok 1 res is '$res'"; fi
+v=`$CONFGET '-c' '-f' "$TESTDIR/t1.ini" '-s' 'a' 'key2' `
+res="$?"
+if [ "$res" = 0 ]; then echo 'ok 2'; else echo "not ok 2 res is '$res'"; fi
+v=`$CONFGET '-c' '-f' "$TESTDIR/t1.ini" '-s' 'a' 'key3' `
+res="$?"
+if [ "$res" = 0 ]; then echo 'ok 3'; else echo "not ok 3 res is '$res'"; fi
+v=`$CONFGET '-c' '-f' "$TESTDIR/t1.ini" '-s' 'a' 'key4' `
+res="$?"
+if [ "$res" != 0 ]; then echo 'ok 4'; else echo "not ok 4 res is '$res'"; fi
+v=`$CONFGET '-c' '-f' "$TESTDIR/t1.ini" '-s' 'b sect' 'key5' `
+res="$?"
+if [ "$res" != 0 ]; then echo 'ok 5'; else echo "not ok 5 res is '$res'"; fi
+v=`$CONFGET '-c' '-f' "$TESTDIR/t1.ini" '-s' 'b sect' 'key4' `
+res="$?"
+if [ "$res" = 0 ]; then echo 'ok 6'; else echo "not ok 6 res is '$res'"; fi
+v=`$CONFGET '-c' '-f' "$TESTDIR/t1.ini" '-s' 'c' 'key5' `
+res="$?"
+if [ "$res" = 0 ]; then echo 'ok 7'; else echo "not ok 7 res is '$res'"; fi
+v=`env Q1='key4&key5=%09%09%20val%27ue5&key6' Q2='' QUERY_STRING='key1=value1&key2=%3Dvalue2%26&key3=%09%09%20val%27ue3' $CONFGET -t http_get '-c' 'key1' `
+res="$?"
+if [ "$res" = 0 ]; then echo 'ok 8'; else echo "not ok 8 res is '$res'"; fi
+v=`env Q1='key4&key5=%09%09%20val%27ue5&key6' Q2='' QUERY_STRING='key1=value1&key2=%3Dvalue2%26&key3=%09%09%20val%27ue3' $CONFGET -t http_get '-c' 'key2' `
+res="$?"
+if [ "$res" = 0 ]; then echo 'ok 9'; else echo "not ok 9 res is '$res'"; fi
+v=`env Q1='key4&key5=%09%09%20val%27ue5&key6' Q2='' QUERY_STRING='key1=value1&key2=%3Dvalue2%26&key3=%09%09%20val%27ue3' $CONFGET -t http_get '-c' 'key3' `
+res="$?"
+if [ "$res" = 0 ]; then echo 'ok 10'; else echo "not ok 10 res is '$res'"; fi
+v=`env Q1='key4&key5=%09%09%20val%27ue5&key6' Q2='' QUERY_STRING='key1=value1&key2=%3Dvalue2%26&key3=%09%09%20val%27ue3' $CONFGET -t http_get '-c' '-s' 'Q1' 'key4' `
+res="$?"
+if [ "$res" = 0 ]; then echo 'ok 11'; else echo "not ok 11 res is '$res'"; fi
+v=`env Q1='key4&key5=%09%09%20val%27ue5&key6' Q2='' QUERY_STRING='key1=value1&key2=%3Dvalue2%26&key3=%09%09%20val%27ue3' $CONFGET -t http_get '-c' '-s' 'Q1' 'key6' `
+res="$?"
+if [ "$res" = 0 ]; then echo 'ok 12'; else echo "not ok 12 res is '$res'"; fi
+v=`env Q1='key4&key5=%09%09%20val%27ue5&key6' Q2='' QUERY_STRING='key1=value1&key2=%3Dvalue2%26&key3=%09%09%20val%27ue3' $CONFGET -t http_get '-c' 'key6' `
+res="$?"
+if [ "$res" != 0 ]; then echo 'ok 13'; else echo "not ok 13 res is '$res'"; fi
+v=`env Q1='key4&key5=%09%09%20val%27ue5&key6' Q2='' QUERY_STRING='key1=value1&key2=%3Dvalue2%26&key3=%09%09%20val%27ue3' $CONFGET -t http_get '-c' '-s' 'Q1' 'key1' `
+res="$?"
+if [ "$res" != 0 ]; then echo 'ok 14'; else echo "not ok 14 res is '$res'"; fi
+v=`env Q1='key4&key5=%09%09%20val%27ue5&key6' Q2='' QUERY_STRING='key1=value1&key2=%3Dvalue2%26&key3=%09%09%20val%27ue3' $CONFGET -t http_get '-c' '-s' 'Q2' 'key1' `
+res="$?"
+if [ "$res" != 0 ]; then echo 'ok 15'; else echo "not ok 15 res is '$res'"; fi
diff --git a/t/03-list-all.t b/t/03-list-all.t
new file mode 100644
index 0000000..327cb2d
--- /dev/null
+++ b/t/03-list-all.t
@@ -0,0 +1,57 @@
+#!/bin/sh
+#
+# Copyright (c) 2008, 2011, 2019  Peter Pentchev
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+#    notice, this list of conditions and the following disclaimer.
+# 2. 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.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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.
+
+[ -z "$CONFGET" ] && CONFGET='./confget'
+[ -z "$TESTDIR" ] && TESTDIR='t'
+
+echo '1..7'
+
+
+if [ ! -f "$TESTDIR/t1.ini" ]; then
+        echo "Bail out!  No test file $TESTDIR/t1.ini"
+        exit 255
+fi
+v=`$CONFGET '-f' "$TESTDIR/t1.ini" '-s' 'a' '-l' | wc -l | tr -d ' '`
+res="$?"
+if [ "$v" = '4' ]; then echo 'ok 1'; else echo "not ok 1 v is '$v'"; fi
+v=`$CONFGET '-f' "$TESTDIR/t1.ini" '-s' 'b sect' '-l' '-N' | fgrep -e '=' | wc -l | tr -d ' '`
+res="$?"
+if [ "$v" = '2' ]; then echo 'ok 2'; else echo "not ok 2 v is '$v'"; fi
+v=`$CONFGET '-f' "$TESTDIR/t1.ini" '-s' 'c' '-l' '-n' | fgrep -ve '=' | wc -l | tr -d ' '`
+res="$?"
+if [ "$v" = '1' ]; then echo 'ok 3'; else echo "not ok 3 v is '$v'"; fi
+v=`$CONFGET '-f' "$TESTDIR/t1.ini" '-s' 'd' '-l' | wc -l | tr -d ' '`
+res="$?"
+if [ "$v" = '0' ]; then echo 'ok 4'; else echo "not ok 4 v is '$v'"; fi
+v=`env Q1='key4&key5=%09%09%20val%27ue5&key6' Q2='' QUERY_STRING='key1=value1&key2=%3Dvalue2%26&key3=%09%09%20val%27ue3' $CONFGET -t http_get '-l' | wc -l | tr -d ' '`
+res="$?"
+if [ "$v" = '3' ]; then echo 'ok 5'; else echo "not ok 5 v is '$v'"; fi
+v=`env Q1='key4&key5=%09%09%20val%27ue5&key6' Q2='' QUERY_STRING='key1=value1&key2=%3Dvalue2%26&key3=%09%09%20val%27ue3' $CONFGET -t http_get '-l' '-s' 'Q1' | wc -l | tr -d ' '`
+res="$?"
+if [ "$v" = '3' ]; then echo 'ok 6'; else echo "not ok 6 v is '$v'"; fi
+v=`env Q1='key4&key5=%09%09%20val%27ue5&key6' Q2='' QUERY_STRING='key1=value1&key2=%3Dvalue2%26&key3=%09%09%20val%27ue3' $CONFGET -t http_get '-l' '-s' 'Q2' | wc -l | tr -d ' '`
+res="$?"
+if [ "$v" = '0' ]; then echo 'ok 7'; else echo "not ok 7 v is '$v'"; fi
diff --git a/t/04-bespoke-test-manpage.t b/t/04-bespoke-test-manpage.t
new file mode 100644
index 0000000..ddc0607
--- /dev/null
+++ b/t/04-bespoke-test-manpage.t
@@ -0,0 +1,39 @@
+#!/bin/sh
+#
+# Copyright (c) 2008  Peter Pentchev
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+#    notice, this list of conditions and the following disclaimer.
+# 2. 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.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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.
+
+[ -z "$MANPAGE" ] && MANPAGE='confget.1'
+[ -z "$GROFF" ] && GROFF='groff'
+[ -z "$GROFF_ARGS" ] && GROFF_ARGS='-mdoc -z'
+[ -z "$ZCAT" ] && ZCAT='zcat'
+
+echo '1..1'
+
+if [ "${MANPAGE%.gz}" != "$MANPAGE" ]; then
+	OUT=`$ZCAT $MANPAGE | $GROFF $GROFF_ARGS 2>&1`
+else
+	OUT=`$GROFF $GROFF_ARGS $MANPAGE 2>&1`
+fi
+if [ -z "$OUT" ]; then echo 'ok 1'; else echo 'not ok 1'; fi
diff --git a/t/05-match-names.t b/t/05-match-names.t
new file mode 100644
index 0000000..cd529e0
--- /dev/null
+++ b/t/05-match-names.t
@@ -0,0 +1,48 @@
+#!/bin/sh
+#
+# Copyright (c) 2008, 2011, 2019  Peter Pentchev
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+#    notice, this list of conditions and the following disclaimer.
+# 2. 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.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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.
+
+[ -z "$CONFGET" ] && CONFGET='./confget'
+[ -z "$TESTDIR" ] && TESTDIR='t'
+
+echo '1..4'
+
+
+if [ ! -f "$TESTDIR/t1.ini" ]; then
+        echo "Bail out!  No test file $TESTDIR/t1.ini"
+        exit 255
+fi
+v=`$CONFGET '-f' "$TESTDIR/t1.ini" '-s' 'a' '-L' '*' | wc -l | tr -d ' '`
+res="$?"
+if [ "$v" = '4' ]; then echo 'ok 1'; else echo "not ok 1 v is '$v'"; fi
+v=`$CONFGET '-f' "$TESTDIR/t1.ini" '-s' 'b sect' '-L' '-N' '*' | fgrep -e '=' | wc -l | tr -d ' '`
+res="$?"
+if [ "$v" = '2' ]; then echo 'ok 2'; else echo "not ok 2 v is '$v'"; fi
+v=`$CONFGET '-f' "$TESTDIR/t1.ini" '-s' 'a' '-L' '-n' '*ey2' | fgrep -ve '=' | wc -l | tr -d ' '`
+res="$?"
+if [ "$v" = '1' ]; then echo 'ok 3'; else echo "not ok 3 v is '$v'"; fi
+v=`$CONFGET '-f' "$TESTDIR/t1.ini" '-s' 'd' '-L' '*ey2' | wc -l | tr -d ' '`
+res="$?"
+if [ "$v" = '0' ]; then echo 'ok 4'; else echo "not ok 4 v is '$v'"; fi
diff --git a/t/06-get-default.t b/t/06-get-default.t
new file mode 100644
index 0000000..052f08e
--- /dev/null
+++ b/t/06-get-default.t
@@ -0,0 +1,56 @@
+#!/bin/sh
+#
+# Copyright (c) 2008, 2011, 2019  Peter Pentchev
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+#    notice, this list of conditions and the following disclaimer.
+# 2. 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.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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.
+
+[ -z "$CONFGET" ] && CONFGET='./confget'
+[ -z "$TESTDIR" ] && TESTDIR='t'
+
+echo '1..5'
+
+
+if [ ! -f "$TESTDIR/t1.ini" ]; then
+        echo "Bail out!  No test file $TESTDIR/t1.ini"
+        exit 255
+fi
+
+if [ ! -f "$TESTDIR/t2.ini" ]; then
+        echo "Bail out!  No test file $TESTDIR/t2.ini"
+        exit 255
+fi
+v=`$CONFGET '-f' "$TESTDIR/t1.ini" 'key1' `
+res="$?"
+if [ "$v" = 'value1' ]; then echo 'ok 1'; else echo "not ok 1 v is '$v'"; fi
+v=`$CONFGET '-f' "$TESTDIR/t1.ini" 'key4' `
+res="$?"
+if [ "$v" = '' ]; then echo 'ok 2'; else echo "not ok 2 v is '$v'"; fi
+v=`$CONFGET '-f' "$TESTDIR/t1.ini" 'key6' `
+res="$?"
+if [ "$v" = 'value6' ]; then echo 'ok 3'; else echo "not ok 3 v is '$v'"; fi
+v=`$CONFGET '-f' "$TESTDIR/t2.ini" 'key1' `
+res="$?"
+if [ "$v" = '1' ]; then echo 'ok 4'; else echo "not ok 4 v is '$v'"; fi
+v=`$CONFGET '-f' "$TESTDIR/t2.ini" 'key1' 'key2' | tr "\n" " "`
+res="$?"
+if [ "$v" = 'key1=1 key2=2 ' ]; then echo 'ok 5'; else echo "not ok 5 v is '$v'"; fi
diff --git a/t/07-match-values.t b/t/07-match-values.t
new file mode 100644
index 0000000..b66a9a8
--- /dev/null
+++ b/t/07-match-values.t
@@ -0,0 +1,63 @@
+#!/bin/sh
+#
+# Copyright (c) 2008, 2011, 2019  Peter Pentchev
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+#    notice, this list of conditions and the following disclaimer.
+# 2. 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.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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.
+
+[ -z "$CONFGET" ] && CONFGET='./confget'
+[ -z "$TESTDIR" ] && TESTDIR='t'
+
+echo '1..9'
+
+
+if [ ! -f "$TESTDIR/t1.ini" ]; then
+        echo "Bail out!  No test file $TESTDIR/t1.ini"
+        exit 255
+fi
+v=`$CONFGET '-f' "$TESTDIR/t1.ini" '-s' 'a' '-m' 'value*' 'key1' `
+res="$?"
+if [ "$v" = 'value1' ]; then echo 'ok 1'; else echo "not ok 1 v is '$v'"; fi
+v=`$CONFGET '-f' "$TESTDIR/t1.ini" '-s' 'a' '-m' '*ue2' 'key2' `
+res="$?"
+if [ "$v" = 'value2' ]; then echo 'ok 2'; else echo "not ok 2 v is '$v'"; fi
+v=`$CONFGET '-f' "$TESTDIR/t1.ini" '-s' 'a' '-m' '*val'"'"'ue*' 'key3' `
+res="$?"
+if [ "$v" = '		 val'"'"'ue3' ]; then echo 'ok 3'; else echo "not ok 3 v is '$v'"; fi
+v=`$CONFGET '-f' "$TESTDIR/t1.ini" '-s' 'b sect' '-m' '*alu*' 'key4' `
+res="$?"
+if [ "$v" = 'v'"'"'alu'"'"'e4' ]; then echo 'ok 4'; else echo "not ok 4 v is '$v'"; fi
+v=`$CONFGET '-f' "$TESTDIR/t1.ini" '-s' 'c' 'key5' `
+res="$?"
+if [ "$v" = '		# value5' ]; then echo 'ok 5'; else echo "not ok 5 v is '$v'"; fi
+v=`$CONFGET '-f' "$TESTDIR/t1.ini" '-s' 'a' '-m' '*alu*' 'key1' 'key2' | tr "\n" " "`
+res="$?"
+if [ "$v" = 'key1=value1 key2=value2 ' ]; then echo 'ok 6'; else echo "not ok 6 v is '$v'"; fi
+v=`$CONFGET '-f' "$TESTDIR/t1.ini" '-s' 'a' '-m' '*7' 'key6' `
+res="$?"
+if [ "$v" = '' ]; then echo 'ok 7'; else echo "not ok 7 v is '$v'"; fi
+v=`$CONFGET '-f' "$TESTDIR/t1.ini" '-s' 'b sect' '-c' '-m' '*7' 'key7' `
+res="$?"
+if [ "$res" = 0 ]; then echo 'ok 8'; else echo "not ok 8 res is '$res'"; fi
+v=`$CONFGET '-f' "$TESTDIR/t1.ini" '-s' 'b sect' '-c' '-m' '*7' 'key6' `
+res="$?"
+if [ "$res" != 0 ]; then echo 'ok 9'; else echo "not ok 9 res is '$res'"; fi
diff --git a/t/08-bespoke-shell-quote.t b/t/08-bespoke-shell-quote.t
new file mode 100644
index 0000000..49264ad
--- /dev/null
+++ b/t/08-bespoke-shell-quote.t
@@ -0,0 +1,49 @@
+#!/bin/sh
+#
+# Copyright (c) 2008  Peter Pentchev
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+#    notice, this list of conditions and the following disclaimer.
+# 2. 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.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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.
+
+[ -z "$CONFGET" ] && CONFGET='./confget'
+[ -z "$TESTDIR" ] && TESTDIR='t'
+
+T1="$TESTDIR/t1.ini"
+
+echo '1..6'
+
+if [ ! -f "$T1" ]; then
+	echo "Bail out!  No test file $T1"
+	exit 255
+fi
+
+v=`$CONFGET -f "$T1" -Ss a key1`
+if [ "$v" = "'value1'" ]; then echo 'ok 1'; else echo "not ok 1 v is '$v'"; fi
+v=`$CONFGET -f "$T1" -Ss a key3`
+if [ "$v" = "'		 val'\"'\"'ue3'" ]; then echo 'ok 2'; else echo "not ok 2 v is '$v'"; fi
+
+unset cfg_key1 cfg_key2 cfg_key3 cfg_key6
+eval `$CONFGET -f "$T1" -S -p cfg_ -s a -l`
+if [ "$cfg_key1" = 'value1' ]; then echo 'ok 3'; else echo "not ok 3 cfg_key1 is '$cfg_key1'"; fi
+if [ "$cfg_key2" = 'value2' ]; then echo 'ok 4'; else echo "not ok 4 cfg_key2 is '$cfg_key2'"; fi
+if [ "$cfg_key3" = " val'ue3" ]; then echo 'ok 5'; else echo "not ok 5 cfg_key3 is '$cfg_key3'"; fi
+if [ "$cfg_key6" = 'value6' ]; then echo 'ok 6'; else echo "not ok 6 cfg_key6 is '$cfg_key6'"; fi
diff --git a/t/09-bespoke-regexp.t b/t/09-bespoke-regexp.t
new file mode 100644
index 0000000..684de13
--- /dev/null
+++ b/t/09-bespoke-regexp.t
@@ -0,0 +1,59 @@
+#!/bin/sh
+#
+# Copyright (c) 2008, 2009  Peter Pentchev
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+#    notice, this list of conditions and the following disclaimer.
+# 2. 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.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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.
+
+[ -z "$CONFGET" ] && CONFGET='./confget'
+[ -z "$TESTDIR" ] && TESTDIR='t'
+
+T1="$TESTDIR/t1.ini"
+
+echo '1..8'
+
+if [ ! -f "$T1" ]; then
+	echo "Bail out!  No test file $T1"
+	exit 255
+fi
+
+v=`$CONFGET -f "$T1" -s a -x key1 2>&1`
+if [ "$v" = 'confget: No regular expression support in this confget build' ]; then
+	for i in 1 2 3 4 5 6 7 8; do
+		echo "ok $i # skip No regular expression support in confget"
+	done
+	exit 0
+fi
+
+unset cfg_key1 cfg_key2 cfg_key3 cfg_key6
+eval `$CONFGET -f "$T1" -S -p cfg_ -s a -L -x 'key[23]'`
+if [ "$cfg_key1" = '' ]; then echo 'ok 1'; else echo "not ok 1 cfg_key1 is '$cfg_key1'"; fi
+if [ "$cfg_key2" = 'value2' ]; then echo 'ok 2'; else echo "not ok 2 cfg_key2 is '$cfg_key2'"; fi
+if [ "$cfg_key3" = " val'ue3" ]; then echo 'ok 3'; else echo "not ok 3 cfg_key3 is '$cfg_key3'"; fi
+if [ "$cfg_key6" = '' ]; then echo 'ok 4'; else echo "not ok 4 cfg_key6 is '$cfg_key6'"; fi
+
+unset cfg_key1 cfg_key2 cfg_key3 cfg_key6
+eval `$CONFGET -f "$T1" -S -p cfg_ -s a -L -m "'" -x 'key[23]'`
+if [ "$cfg_key1" = '' ]; then echo 'ok 5'; else echo "not ok 5 cfg_key1 is '$cfg_key1'"; fi
+if [ "$cfg_key2" = '' ]; then echo 'ok 6'; else echo "not ok 6 cfg_key2 is '$cfg_key2'"; fi
+if [ "$cfg_key3" = " val'ue3" ]; then echo 'ok 7'; else echo "not ok 7 cfg_key3 is '$cfg_key3'"; fi
+if [ "$cfg_key6" = '' ]; then echo 'ok 8'; else echo "not ok 8 cfg_key6 is '$cfg_key6'"; fi
diff --git a/t/10-bespoke-qsections.t b/t/10-bespoke-qsections.t
new file mode 100644
index 0000000..7b7804a
--- /dev/null
+++ b/t/10-bespoke-qsections.t
@@ -0,0 +1,74 @@
+#!/bin/sh
+#
+# Copyright (c) 2012  Peter Pentchev
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+#    notice, this list of conditions and the following disclaimer.
+# 2. 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.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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.
+
+[ -z "$CONFGET" ] && CONFGET='./confget'
+[ -z "$TESTDIR" ] && TESTDIR='t'
+
+T1="$TESTDIR/t1.ini"
+T2="$TESTDIR/t2.ini"
+
+echo '1..3'
+
+if [ ! -f "$T1" ]; then
+	echo "Bail out!  No test file $T1"
+	exit 255
+fi
+if [ ! -f "$T2" ]; then
+	echo "Bail out!  No test file $T2"
+	exit 255
+fi
+
+# OK, this is so ugly it hurts...
+$CONFGET -f "$T1" -q sections 2>&1 | (
+	read v
+	if [ "$v" != 'a' ]; then
+		echo "nok 1 expected a got $v"
+	else
+		read v
+		if [ "$v" != 'b sect' ]; then
+			echo "nok 1 expected b sect got $v"
+		else
+			read v
+			if [ "$v" != 'c' ]; then
+				echo "nok 1 expected c got $v"
+			else
+				read v
+				if [ -n "$v" ]; then
+					echo "nok 1 expected empty got $v"
+				else
+					echo 'ok 1'
+				fi
+			fi
+		fi
+	fi
+)
+#if [ "$v" = "a\nb sect\nc" ]; then echo 'ok 1'; else echo "not ok 1 v is $v"; fi
+
+v=`$CONFGET -f "$T2" -q sections 2>&1`
+if [ "$v" = 'sec1' ]; then echo 'ok 2'; else echo "not ok 2 v is $v"; fi
+
+v=`$CONFGET -q sections -t http 2>&1`
+if expr "x$v" : 'x.* is only supported ' > /dev/null; then echo 'ok 3'; else echo "not ok 3 v is $v"; fi
diff --git a/t/11-no-default.t b/t/11-no-default.t
new file mode 100644
index 0000000..b531334
--- /dev/null
+++ b/t/11-no-default.t
@@ -0,0 +1,71 @@
+#!/bin/sh
+#
+# Copyright (c) 2008, 2011, 2019  Peter Pentchev
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+#    notice, this list of conditions and the following disclaimer.
+# 2. 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.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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.
+
+[ -z "$CONFGET" ] && CONFGET='./confget'
+[ -z "$TESTDIR" ] && TESTDIR='t'
+
+echo '1..10'
+
+
+if [ ! -f "$TESTDIR/t1.ini" ]; then
+        echo "Bail out!  No test file $TESTDIR/t1.ini"
+        exit 255
+fi
+
+if [ ! -f "$TESTDIR/t2.ini" ]; then
+        echo "Bail out!  No test file $TESTDIR/t2.ini"
+        exit 255
+fi
+v=`$CONFGET '-f' "$TESTDIR/t1.ini" 'key1' `
+res="$?"
+if [ "$v" = 'value1' ]; then echo 'ok 1'; else echo "not ok 1 v is '$v'"; fi
+v=`$CONFGET '-f' "$TESTDIR/t1.ini" 'key4' `
+res="$?"
+if [ "$v" = '' ]; then echo 'ok 2'; else echo "not ok 2 v is '$v'"; fi
+v=`$CONFGET '-f' "$TESTDIR/t1.ini" 'key6' `
+res="$?"
+if [ "$v" = 'value6' ]; then echo 'ok 3'; else echo "not ok 3 v is '$v'"; fi
+v=`$CONFGET '-f' "$TESTDIR/t1.ini" '-s' '' 'key1' `
+res="$?"
+if [ "$v" = '' ]; then echo 'ok 4'; else echo "not ok 4 v is '$v'"; fi
+v=`$CONFGET '-f' "$TESTDIR/t1.ini" '-s' '' 'key4' `
+res="$?"
+if [ "$v" = '' ]; then echo 'ok 5'; else echo "not ok 5 v is '$v'"; fi
+v=`$CONFGET '-f' "$TESTDIR/t1.ini" '-s' '' 'key6' `
+res="$?"
+if [ "$v" = '' ]; then echo 'ok 6'; else echo "not ok 6 v is '$v'"; fi
+v=`$CONFGET '-f' "$TESTDIR/t2.ini" 'key1' `
+res="$?"
+if [ "$v" = '1' ]; then echo 'ok 7'; else echo "not ok 7 v is '$v'"; fi
+v=`$CONFGET '-f' "$TESTDIR/t2.ini" 'key1' 'key2' | tr "\n" " "`
+res="$?"
+if [ "$v" = 'key1=1 key2=2 ' ]; then echo 'ok 8'; else echo "not ok 8 v is '$v'"; fi
+v=`$CONFGET '-f' "$TESTDIR/t2.ini" '-s' '' 'key1' `
+res="$?"
+if [ "$v" = '1' ]; then echo 'ok 9'; else echo "not ok 9 v is '$v'"; fi
+v=`$CONFGET '-f' "$TESTDIR/t2.ini" '-s' '' 'key1' 'key2' | tr "\n" " "`
+res="$?"
+if [ "$v" = 'key1=1 key2=2 ' ]; then echo 'ok 10'; else echo "not ok 10 v is '$v'"; fi
diff --git a/t/12-last-value.t b/t/12-last-value.t
new file mode 100644
index 0000000..baa8122
--- /dev/null
+++ b/t/12-last-value.t
@@ -0,0 +1,78 @@
+#!/bin/sh
+#
+# Copyright (c) 2008, 2011, 2019  Peter Pentchev
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+#    notice, this list of conditions and the following disclaimer.
+# 2. 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.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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.
+
+[ -z "$CONFGET" ] && CONFGET='./confget'
+[ -z "$TESTDIR" ] && TESTDIR='t'
+
+echo '1..14'
+
+
+if [ ! -f "$TESTDIR/t3.ini" ]; then
+        echo "Bail out!  No test file $TESTDIR/t3.ini"
+        exit 255
+fi
+v=`$CONFGET '-f' "$TESTDIR/t3.ini" 'both' `
+res="$?"
+if [ "$v" = 'default' ]; then echo 'ok 1'; else echo "not ok 1 v is '$v'"; fi
+v=`$CONFGET '-f' "$TESTDIR/t3.ini" 'defonly' `
+res="$?"
+if [ "$v" = 'default' ]; then echo 'ok 2'; else echo "not ok 2 v is '$v'"; fi
+v=`$CONFGET '-f' "$TESTDIR/t3.ini" 'aonly' `
+res="$?"
+if [ "$v" = '' ]; then echo 'ok 3'; else echo "not ok 3 v is '$v'"; fi
+v=`$CONFGET '-f' "$TESTDIR/t3.ini" '-s' '' 'both' `
+res="$?"
+if [ "$v" = 'default' ]; then echo 'ok 4'; else echo "not ok 4 v is '$v'"; fi
+v=`$CONFGET '-f' "$TESTDIR/t3.ini" '-s' '' 'defonly' `
+res="$?"
+if [ "$v" = 'default' ]; then echo 'ok 5'; else echo "not ok 5 v is '$v'"; fi
+v=`$CONFGET '-f' "$TESTDIR/t3.ini" '-s' '' 'aonly' `
+res="$?"
+if [ "$v" = '' ]; then echo 'ok 6'; else echo "not ok 6 v is '$v'"; fi
+v=`$CONFGET '-f' "$TESTDIR/t3.ini" '-s' 'a' 'both' `
+res="$?"
+if [ "$v" = 'a' ]; then echo 'ok 7'; else echo "not ok 7 v is '$v'"; fi
+v=`$CONFGET '-f' "$TESTDIR/t3.ini" '-s' 'a' 'defonly' `
+res="$?"
+if [ "$v" = '' ]; then echo 'ok 8'; else echo "not ok 8 v is '$v'"; fi
+v=`$CONFGET '-f' "$TESTDIR/t3.ini" '-s' 'a' 'aonly' `
+res="$?"
+if [ "$v" = 'a' ]; then echo 'ok 9'; else echo "not ok 9 v is '$v'"; fi
+v=`$CONFGET '-f' "$TESTDIR/t3.ini" '-s' 'a' '-O' 'both' `
+res="$?"
+if [ "$v" = 'a' ]; then echo 'ok 10'; else echo "not ok 10 v is '$v'"; fi
+v=`$CONFGET '-f' "$TESTDIR/t3.ini" '-s' 'a' '-O' 'defonly' `
+res="$?"
+if [ "$v" = 'default' ]; then echo 'ok 11'; else echo "not ok 11 v is '$v'"; fi
+v=`$CONFGET '-f' "$TESTDIR/t3.ini" '-s' 'a' '-O' 'aonly' `
+res="$?"
+if [ "$v" = 'a' ]; then echo 'ok 12'; else echo "not ok 12 v is '$v'"; fi
+v=`$CONFGET '-f' "$TESTDIR/t3.ini" '-m' 'def*' '-s' 'a' '-O' 'both' `
+res="$?"
+if [ "$v" = '' ]; then echo 'ok 13'; else echo "not ok 13 v is '$v'"; fi
+v=`$CONFGET '-f' "$TESTDIR/t3.ini" '-m' 'a*' '-s' 'a' '-O' 'both' `
+res="$?"
+if [ "$v" = 'a' ]; then echo 'ok 14'; else echo "not ok 14 v is '$v'"; fi
diff --git a/t/13-bespoke-features.t b/t/13-bespoke-features.t
new file mode 100644
index 0000000..4ed1088
--- /dev/null
+++ b/t/13-bespoke-features.t
@@ -0,0 +1,110 @@
+#!/bin/sh
+#
+# Copyright (c) 2008, 2011, 2017  Peter Pentchev
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+#    notice, this list of conditions and the following disclaimer.
+# 2. 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.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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.
+
+[ -z "$CONFGET" ] && CONFGET='./confget'
+
+echo '1..11'
+
+# First, make sure '-q features' reports 'BASE=...'
+all="$($CONFGET -q features)"
+if [ -n "$all" ]; then
+	echo 'ok 1'
+else
+	echo 'not ok 1 -q features did not output anything'
+fi
+
+if [ "${all%%=*}" = 'BASE' ]; then
+	base_and_all="${all#BASE=}"
+else
+	base_and_all="${all#* BASE=}"
+fi
+if [ "$base_and_all" != "$all" ]; then
+	echo 'ok 2'
+else
+	echo "not ok 2 -q features did not contain 'BASE=': $all"
+fi
+
+just_base="${base_and_all%% *}"
+if [ -n "$just_base" ]; then
+	echo 'ok 3'
+else
+	echo "not ok 3 -q features contained an empty 'BASE=': $all"
+fi
+
+second_base="${base_and_all#* BASE=}"
+if [ "$base_and_all" = "$second_base" ]; then
+	echo 'ok 4'
+else
+	echo "not ok 4 -q features contained more than one 'BASE=': $all"
+fi
+
+# Now check that '-q feature BASE' outputs BASE
+base="$($CONFGET -q feature BASE)"
+if [ "$base" = "$just_base" ]; then
+	echo 'ok 5'
+else
+	echo "not ok 5 -q feature BASE did not return the same string as in -q features: '$just_base' vs '$base'"
+fi
+
+# Now for some failure cases...
+res=0
+out="$($CONFGET -q features something 2>/dev/null)" || res="$?"
+if [ "$res" -ne 0 ]; then
+	echo 'ok 6'
+else
+	echo "not ok 6 '-q features' with an argument succeeded"
+fi
+if [ "$out" = '' ]; then
+	echo 'ok 7'
+else
+	echo "not ok 7 '-q features' with an argument output something: $out"
+fi
+
+res=0
+out="$($CONFGET -q feature 2>/dev/null)" || res="$?"
+if [ "$res" -ne 0 ]; then
+	echo 'ok 8'
+else
+	echo "not ok 8 '-q feature' with no argument succeeded"
+fi
+if [ "$out" = '' ]; then
+	echo 'ok 9'
+else
+	echo "not ok 9 '-q feature' with no argument output something: $out"
+fi
+
+res=0
+out="$($CONFGET -q feature BASE something 2>/dev/null)" || res="$?"
+if [ "$res" -ne 0 ]; then
+	echo 'ok 10'
+else
+	echo "not ok 10 '-q feature' with two arguments succeeded"
+fi
+if [ "$out" = '' ]; then
+	echo 'ok 11'
+else
+	echo "not ok 11 '-q feature' with two arguments output something: $out"
+fi
diff --git a/t/14-bespoke-too-many.t b/t/14-bespoke-too-many.t
new file mode 100644
index 0000000..5c1be63
--- /dev/null
+++ b/t/14-bespoke-too-many.t
@@ -0,0 +1,69 @@
+#!/bin/sh
+#
+# Copyright (c) 2018  Peter Pentchev
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+#    notice, this list of conditions and the following disclaimer.
+# 2. 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.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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.
+
+[ -z "$CONFGET" ] && CONFGET='./confget'
+[ -z "$TESTDIR" ] && TESTDIR='t'
+
+T1="$TESTDIR/t1.ini"
+T2="$TESTDIR/t2.ini"
+
+echo '1..13'
+
+if [ ! -f "$T1" ]; then
+	echo "Bail out!  No test file $T1"
+	exit 255
+fi
+if [ ! -f "$T2" ]; then
+	echo "Bail out!  No test file $T2"
+	exit 255
+fi
+
+idx=1
+for args in \
+    '-l -L k' \
+    '-l -q sections' \
+    '-l -q features' \
+    '-l -q feature BASE' \
+    '-l key1' \
+    '-L -q sections k' \
+    '-L -q features k' \
+    '-L -q feature BASE k' \
+    '-q sections -q features' \
+    '-q sections -q feature BASE' \
+    '-q sections key1' \
+    '-q features -q feature BASE' \
+    '-q features key1'; do
+	if [ "${args#-q sections -q}" != "$args" ] || [ "${args#-q features -q}" != "$args" ]; then
+		if [ "${CONFGET#*python}" != "$CONFGET" ]; then
+			echo "ok $idx $args - skipped, Python argparse"
+			idx="$((idx + 1))"
+			continue
+		fi
+	fi
+	v=`$CONFGET -f "$T2" $args 2>&1`
+	if expr "x$v" : 'x.*Only a single ' > /dev/null; then echo "ok $idx $args"; else echo "not ok $idx args $args v is $v"; fi
+	idx="$((idx + 1))"
+done
diff --git a/t/defs/schema/test-1.0.json b/t/defs/schema/test-1.0.json
new file mode 100644
index 0000000..065fe1f
--- /dev/null
+++ b/t/defs/schema/test-1.0.json
@@ -0,0 +1,176 @@
+{
+	"$schema": "http://json-schema.org/draft-07/schema#",
+	"$id": "https://devel.ringlet.net/textproc/confget/schema/test-1.0.json",
+	"title": "confget test definition",
+	"description": "A definition of a test file for the confget test suite",
+	"definitions": {
+		"format": {
+			"type": "object",
+			"properties": {
+				"version": {
+					"type": "object",
+					"properties": {
+						"major": {
+							"type": "integer",
+							"const": 1
+						},
+						"minor": {
+							"type": "integer",
+							"const": 0
+						}
+					},
+					"required": ["major", "minor"],
+					"additionalProperties": false
+				}
+			},
+			"required": ["version"],
+			"additionalProperties": false
+		},
+
+		"setenv": {
+			"type": "object",
+			"additionalProperties": {
+				"type": "string"
+			}
+		},
+
+		"test_def": {
+			"type": "object",
+			"properties": {
+				"args": {
+					"$ref": "#/definitions/test_def_args"
+				},
+				"keys": {
+					"type": "array",
+					"items": {
+						"type": "string"
+					}
+				},
+				"xform": {
+					"type": "string",
+					"enum": [
+						"",
+						"count-lines",
+						"count-lines-eq",
+						"count-lines-non-eq",
+						"newline-to-space"
+					]
+				},
+				"backend": {
+					"type": "string",
+					"enum": ["http_get", "ini"]
+				},
+				"setenv": {
+					"type": "boolean"
+				},
+				"stdin": {
+					"oneOf": [
+						{
+							"type": "null"
+						},
+						{
+							"type": "string",
+							"pattern": "^[0-9a-z_-]+\\.ini$"
+						}
+					]
+				},
+				"output": {
+					"oneOf": [
+						{
+							"$ref": "#/definitions/output_exact"
+						},
+						{
+							"$ref": "#/definitions/output_exit_ok"
+						}
+					]
+				}
+			},
+			"required": ["args", "keys", "output"],
+			"additionalProperties": false
+		},
+
+		"test_def_args": {
+			"type": "object",
+			"properties": {
+				"check_only": {
+					"type": "string",
+					"maxLength": 0
+				},
+				"filename": {
+					"type": "string"
+				},
+				"hide_var_name": {
+					"type": "string",
+					"maxLength": 0
+				},
+				"list_all": {
+					"type": "string",
+					"maxLength": 0
+				},
+				"match_var_names": {
+					"type": "string",
+					"maxLength": 0
+				},
+				"match_var_values": {
+					"type": "string"
+				},
+				"section": {
+					"type": "string"
+				},
+				"section_override": {
+					"type": "string",
+					"maxLength": 0
+				},
+				"section_specified": {
+					"type": "string",
+					"maxLength": 0
+				},
+				"show_var_name": {
+					"type": "string",
+					"maxLength": 0
+				}
+			},
+			"additionalProperties": false
+		},
+
+		"output_exact": {
+			"type": "object",
+			"properties": {
+				"exact": {
+					"type": "string"
+				}
+			},
+			"required": ["exact"],
+			"additionalProperties": false
+		},
+
+		"output_exit_ok": {
+			"type": "object",
+			"properties": {
+				"exit": {
+					"type": "boolean"
+				}
+			},
+			"required": ["exit"],
+			"additionalProperties": false
+		}
+	},
+
+	"type": "object",
+	"properties": {
+		"format": {
+			"$ref": "#/definitions/format"
+		},
+		"setenv": {
+			"$ref": "#/definitions/setenv"
+		},
+		"tests": {
+			"type": "array",
+			"items": {
+				"$ref": "#/definitions/test_def"
+			}
+		}
+	},
+	"required": ["format", "tests"],
+	"additionalProperties": false
+}
diff --git a/t/defs/tests/01-get-values.json b/t/defs/tests/01-get-values.json
new file mode 100644
index 0000000..eb89340
--- /dev/null
+++ b/t/defs/tests/01-get-values.json
@@ -0,0 +1,293 @@
+{
+  "format": {
+    "version": {
+      "major": 1,
+      "minor": 0
+    }
+  },
+  "setenv": {
+    "QUERY_STRING": "key1=value1&key2=%3Dvalue2%26&key3=%09%09%20val%27ue3",
+    "Q1": "key4&key5=%09%09%20val%27ue5&key6",
+    "Q2": ""
+  },
+  "tests": [
+    {
+      "args": {
+        "filename": "t1.ini",
+        "section": "a"
+      },
+      "keys": [
+        "key1"
+      ],
+      "xform": "",
+      "backend": "ini",
+      "output": {
+        "exact": "value1"
+      },
+      "setenv": false,
+      "stdin": null
+    },
+    {
+      "args": {
+        "filename": "t1.ini",
+        "section": "a",
+        "show_var_name": ""
+      },
+      "keys": [
+        "key2"
+      ],
+      "xform": "",
+      "backend": "ini",
+      "output": {
+        "exact": "key2=value2"
+      },
+      "setenv": false,
+      "stdin": null
+    },
+    {
+      "args": {
+        "filename": "t1.ini",
+        "section": "a"
+      },
+      "keys": [
+        "key3"
+      ],
+      "xform": "",
+      "backend": "ini",
+      "output": {
+        "exact": "\t\t val'ue3"
+      },
+      "setenv": false,
+      "stdin": null
+    },
+    {
+      "args": {
+        "filename": "t1.ini",
+        "section": "b sect"
+      },
+      "keys": [
+        "key4"
+      ],
+      "xform": "",
+      "backend": "ini",
+      "output": {
+        "exact": "v'alu'e4"
+      },
+      "setenv": false,
+      "stdin": null
+    },
+    {
+      "args": {
+        "filename": "t1.ini",
+        "section": "c"
+      },
+      "keys": [
+        "key5"
+      ],
+      "xform": "",
+      "backend": "ini",
+      "output": {
+        "exact": "\t\t# value5"
+      },
+      "setenv": false,
+      "stdin": null
+    },
+    {
+      "args": {
+        "filename": "t1.ini",
+        "section": "a"
+      },
+      "keys": [
+        "key1",
+        "key2"
+      ],
+      "xform": "newline-to-space",
+      "backend": "ini",
+      "output": {
+        "exact": "key1=value1 key2=value2 "
+      },
+      "setenv": false,
+      "stdin": null
+    },
+    {
+      "args": {
+        "filename": "t1.ini",
+        "section": "a",
+        "hide_var_name": ""
+      },
+      "keys": [
+        "key6",
+        "key2"
+      ],
+      "xform": "newline-to-space",
+      "backend": "ini",
+      "output": {
+        "exact": "value2 value6 "
+      },
+      "setenv": false,
+      "stdin": null
+    },
+    {
+      "args": {
+        "filename": "t1.ini",
+        "section": "b sect"
+      },
+      "keys": [
+        "key7"
+      ],
+      "xform": "",
+      "backend": "ini",
+      "output": {
+        "exact": "value7"
+      },
+      "setenv": false,
+      "stdin": null
+    },
+    {
+      "args": {
+        "section": "b sect"
+      },
+      "keys": [
+        "key7"
+      ],
+      "xform": "",
+      "backend": "ini",
+      "output": {
+        "exact": "value7"
+      },
+      "setenv": false,
+      "stdin": "t1.ini"
+    },
+    {
+      "args": {
+        "section": "b sect"
+      },
+      "keys": [
+        "key77"
+      ],
+      "xform": "",
+      "backend": "ini",
+      "output": {
+        "exact": ""
+      },
+      "setenv": false,
+      "stdin": "t1.ini"
+    },
+    {
+      "args": {},
+      "keys": [
+        "key1"
+      ],
+      "xform": "",
+      "backend": "http_get",
+      "output": {
+        "exact": "value1"
+      },
+      "setenv": true,
+      "stdin": null
+    },
+    {
+      "args": {
+        "show_var_name": ""
+      },
+      "keys": [
+        "key2"
+      ],
+      "xform": "",
+      "backend": "http_get",
+      "output": {
+        "exact": "key2==value2&"
+      },
+      "setenv": true,
+      "stdin": null
+    },
+    {
+      "args": {},
+      "keys": [
+        "key3"
+      ],
+      "xform": "",
+      "backend": "http_get",
+      "output": {
+        "exact": "\t\t val'ue3"
+      },
+      "setenv": true,
+      "stdin": null
+    },
+    {
+      "args": {
+        "section": "Q1"
+      },
+      "keys": [
+        "key4"
+      ],
+      "xform": "",
+      "backend": "http_get",
+      "output": {
+        "exact": ""
+      },
+      "setenv": true,
+      "stdin": null
+    },
+    {
+      "args": {
+        "section": "Q1"
+      },
+      "keys": [
+        "key5"
+      ],
+      "xform": "",
+      "backend": "http_get",
+      "output": {
+        "exact": "\t\t val'ue5"
+      },
+      "setenv": true,
+      "stdin": null
+    },
+    {
+      "args": {
+        "section": "Q1"
+      },
+      "keys": [
+        "key6"
+      ],
+      "xform": "",
+      "backend": "http_get",
+      "output": {
+        "exact": ""
+      },
+      "setenv": true,
+      "stdin": null
+    },
+    {
+      "args": {
+        "section": "Q1"
+      },
+      "keys": [
+        "key66"
+      ],
+      "xform": "",
+      "backend": "http_get",
+      "output": {
+        "exact": ""
+      },
+      "setenv": true,
+      "stdin": null
+    },
+    {
+      "args": {
+        "section": "Q2"
+      },
+      "keys": [
+        "key66"
+      ],
+      "xform": "",
+      "backend": "http_get",
+      "output": {
+        "exact": ""
+      },
+      "setenv": true,
+      "stdin": null
+    }
+  ]
+}
diff --git a/t/defs/tests/02-check-values.json b/t/defs/tests/02-check-values.json
new file mode 100644
index 0000000..1772d80
--- /dev/null
+++ b/t/defs/tests/02-check-values.json
@@ -0,0 +1,258 @@
+{
+  "format": {
+    "version": {
+      "major": 1,
+      "minor": 0
+    }
+  },
+  "setenv": {
+    "QUERY_STRING": "key1=value1&key2=%3Dvalue2%26&key3=%09%09%20val%27ue3",
+    "Q1": "key4&key5=%09%09%20val%27ue5&key6",
+    "Q2": ""
+  },
+  "tests": [
+    {
+      "args": {
+        "check_only": "",
+        "filename": "t1.ini",
+        "section": "a"
+      },
+      "keys": [
+        "key1"
+      ],
+      "xform": "",
+      "backend": "ini",
+      "output": {
+        "exit": true
+      },
+      "setenv": false,
+      "stdin": null
+    },
+    {
+      "args": {
+        "check_only": "",
+        "filename": "t1.ini",
+        "section": "a"
+      },
+      "keys": [
+        "key2"
+      ],
+      "xform": "",
+      "backend": "ini",
+      "output": {
+        "exit": true
+      },
+      "setenv": false,
+      "stdin": null
+    },
+    {
+      "args": {
+        "check_only": "",
+        "filename": "t1.ini",
+        "section": "a"
+      },
+      "keys": [
+        "key3"
+      ],
+      "xform": "",
+      "backend": "ini",
+      "output": {
+        "exit": true
+      },
+      "setenv": false,
+      "stdin": null
+    },
+    {
+      "args": {
+        "check_only": "",
+        "filename": "t1.ini",
+        "section": "a"
+      },
+      "keys": [
+        "key4"
+      ],
+      "xform": "",
+      "backend": "ini",
+      "output": {
+        "exit": false
+      },
+      "setenv": false,
+      "stdin": null
+    },
+    {
+      "args": {
+        "check_only": "",
+        "filename": "t1.ini",
+        "section": "b sect"
+      },
+      "keys": [
+        "key5"
+      ],
+      "xform": "",
+      "backend": "ini",
+      "output": {
+        "exit": false
+      },
+      "setenv": false,
+      "stdin": null
+    },
+    {
+      "args": {
+        "check_only": "",
+        "filename": "t1.ini",
+        "section": "b sect"
+      },
+      "keys": [
+        "key4"
+      ],
+      "xform": "",
+      "backend": "ini",
+      "output": {
+        "exit": true
+      },
+      "setenv": false,
+      "stdin": null
+    },
+    {
+      "args": {
+        "check_only": "",
+        "filename": "t1.ini",
+        "section": "c"
+      },
+      "keys": [
+        "key5"
+      ],
+      "xform": "",
+      "backend": "ini",
+      "output": {
+        "exit": true
+      },
+      "setenv": false,
+      "stdin": null
+    },
+    {
+      "args": {
+        "check_only": ""
+      },
+      "keys": [
+        "key1"
+      ],
+      "xform": "",
+      "backend": "http_get",
+      "output": {
+        "exit": true
+      },
+      "setenv": true,
+      "stdin": null
+    },
+    {
+      "args": {
+        "check_only": ""
+      },
+      "keys": [
+        "key2"
+      ],
+      "xform": "",
+      "backend": "http_get",
+      "output": {
+        "exit": true
+      },
+      "setenv": true,
+      "stdin": null
+    },
+    {
+      "args": {
+        "check_only": ""
+      },
+      "keys": [
+        "key3"
+      ],
+      "xform": "",
+      "backend": "http_get",
+      "output": {
+        "exit": true
+      },
+      "setenv": true,
+      "stdin": null
+    },
+    {
+      "args": {
+        "check_only": "",
+        "section": "Q1"
+      },
+      "keys": [
+        "key4"
+      ],
+      "xform": "",
+      "backend": "http_get",
+      "output": {
+        "exit": true
+      },
+      "setenv": true,
+      "stdin": null
+    },
+    {
+      "args": {
+        "check_only": "",
+        "section": "Q1"
+      },
+      "keys": [
+        "key6"
+      ],
+      "xform": "",
+      "backend": "http_get",
+      "output": {
+        "exit": true
+      },
+      "setenv": true,
+      "stdin": null
+    },
+    {
+      "args": {
+        "check_only": ""
+      },
+      "keys": [
+        "key6"
+      ],
+      "xform": "",
+      "backend": "http_get",
+      "output": {
+        "exit": false
+      },
+      "setenv": true,
+      "stdin": null
+    },
+    {
+      "args": {
+        "check_only": "",
+        "section": "Q1"
+      },
+      "keys": [
+        "key1"
+      ],
+      "xform": "",
+      "backend": "http_get",
+      "output": {
+        "exit": false
+      },
+      "setenv": true,
+      "stdin": null
+    },
+    {
+      "args": {
+        "check_only": "",
+        "section": "Q2"
+      },
+      "keys": [
+        "key1"
+      ],
+      "xform": "",
+      "backend": "http_get",
+      "output": {
+        "exit": false
+      },
+      "setenv": true,
+      "stdin": null
+    }
+  ]
+}
diff --git a/t/defs/tests/03-list-all.json b/t/defs/tests/03-list-all.json
new file mode 100644
index 0000000..b85b867
--- /dev/null
+++ b/t/defs/tests/03-list-all.json
@@ -0,0 +1,118 @@
+{
+  "format": {
+    "version": {
+      "major": 1,
+      "minor": 0
+    }
+  },
+  "setenv": {
+    "QUERY_STRING": "key1=value1&key2=%3Dvalue2%26&key3=%09%09%20val%27ue3",
+    "Q1": "key4&key5=%09%09%20val%27ue5&key6",
+    "Q2": ""
+  },
+  "tests": [
+    {
+      "args": {
+        "filename": "t1.ini",
+        "section": "a",
+        "list_all": ""
+      },
+      "keys": [],
+      "xform": "count-lines",
+      "backend": "ini",
+      "output": {
+        "exact": "4"
+      },
+      "setenv": false,
+      "stdin": null
+    },
+    {
+      "args": {
+        "filename": "t1.ini",
+        "section": "b sect",
+        "list_all": "",
+        "show_var_name": ""
+      },
+      "keys": [],
+      "xform": "count-lines-eq",
+      "backend": "ini",
+      "output": {
+        "exact": "2"
+      },
+      "setenv": false,
+      "stdin": null
+    },
+    {
+      "args": {
+        "filename": "t1.ini",
+        "section": "c",
+        "list_all": "",
+        "hide_var_name": ""
+      },
+      "keys": [],
+      "xform": "count-lines-non-eq",
+      "backend": "ini",
+      "output": {
+        "exact": "1"
+      },
+      "setenv": false,
+      "stdin": null
+    },
+    {
+      "args": {
+        "filename": "t1.ini",
+        "section": "d",
+        "list_all": ""
+      },
+      "keys": [],
+      "xform": "count-lines",
+      "backend": "ini",
+      "output": {
+        "exact": "0"
+      },
+      "setenv": false,
+      "stdin": null
+    },
+    {
+      "args": {
+        "list_all": ""
+      },
+      "keys": [],
+      "xform": "count-lines",
+      "backend": "http_get",
+      "output": {
+        "exact": "3"
+      },
+      "setenv": true,
+      "stdin": null
+    },
+    {
+      "args": {
+        "list_all": "",
+        "section": "Q1"
+      },
+      "keys": [],
+      "xform": "count-lines",
+      "backend": "http_get",
+      "output": {
+        "exact": "3"
+      },
+      "setenv": true,
+      "stdin": null
+    },
+    {
+      "args": {
+        "list_all": "",
+        "section": "Q2"
+      },
+      "keys": [],
+      "xform": "count-lines",
+      "backend": "http_get",
+      "output": {
+        "exact": "0"
+      },
+      "setenv": true,
+      "stdin": null
+    }
+  ]
+}
diff --git a/t/defs/tests/05-match-names.json b/t/defs/tests/05-match-names.json
new file mode 100644
index 0000000..7dd76c2
--- /dev/null
+++ b/t/defs/tests/05-match-names.json
@@ -0,0 +1,81 @@
+{
+  "format": {
+    "version": {
+      "major": 1,
+      "minor": 0
+    }
+  },
+  "setenv": {},
+  "tests": [
+    {
+      "args": {
+        "filename": "t1.ini",
+        "section": "a",
+        "match_var_names": ""
+      },
+      "keys": [
+        "*"
+      ],
+      "xform": "count-lines",
+      "backend": "ini",
+      "output": {
+        "exact": "4"
+      },
+      "setenv": false,
+      "stdin": null
+    },
+    {
+      "args": {
+        "filename": "t1.ini",
+        "section": "b sect",
+        "match_var_names": "",
+        "show_var_name": ""
+      },
+      "keys": [
+        "*"
+      ],
+      "xform": "count-lines-eq",
+      "backend": "ini",
+      "output": {
+        "exact": "2"
+      },
+      "setenv": false,
+      "stdin": null
+    },
+    {
+      "args": {
+        "filename": "t1.ini",
+        "section": "a",
+        "match_var_names": "",
+        "hide_var_name": ""
+      },
+      "keys": [
+        "*ey2"
+      ],
+      "xform": "count-lines-non-eq",
+      "backend": "ini",
+      "output": {
+        "exact": "1"
+      },
+      "setenv": false,
+      "stdin": null
+    },
+    {
+      "args": {
+        "filename": "t1.ini",
+        "section": "d",
+        "match_var_names": ""
+      },
+      "keys": [
+        "*ey2"
+      ],
+      "xform": "count-lines",
+      "backend": "ini",
+      "output": {
+        "exact": "0"
+      },
+      "setenv": false,
+      "stdin": null
+    }
+  ]
+}
diff --git a/t/defs/tests/06-get-default.json b/t/defs/tests/06-get-default.json
new file mode 100644
index 0000000..d20f812
--- /dev/null
+++ b/t/defs/tests/06-get-default.json
@@ -0,0 +1,87 @@
+{
+  "format": {
+    "version": {
+      "major": 1,
+      "minor": 0
+    }
+  },
+  "setenv": {},
+  "tests": [
+    {
+      "args": {
+        "filename": "t1.ini"
+      },
+      "keys": [
+        "key1"
+      ],
+      "xform": "",
+      "backend": "ini",
+      "output": {
+        "exact": "value1"
+      },
+      "setenv": false,
+      "stdin": null
+    },
+    {
+      "args": {
+        "filename": "t1.ini"
+      },
+      "keys": [
+        "key4"
+      ],
+      "xform": "",
+      "backend": "ini",
+      "output": {
+        "exact": ""
+      },
+      "setenv": false,
+      "stdin": null
+    },
+    {
+      "args": {
+        "filename": "t1.ini"
+      },
+      "keys": [
+        "key6"
+      ],
+      "xform": "",
+      "backend": "ini",
+      "output": {
+        "exact": "value6"
+      },
+      "setenv": false,
+      "stdin": null
+    },
+    {
+      "args": {
+        "filename": "t2.ini"
+      },
+      "keys": [
+        "key1"
+      ],
+      "xform": "",
+      "backend": "ini",
+      "output": {
+        "exact": "1"
+      },
+      "setenv": false,
+      "stdin": null
+    },
+    {
+      "args": {
+        "filename": "t2.ini"
+      },
+      "keys": [
+        "key1",
+        "key2"
+      ],
+      "xform": "newline-to-space",
+      "backend": "ini",
+      "output": {
+        "exact": "key1=1 key2=2 "
+      },
+      "setenv": false,
+      "stdin": null
+    }
+  ]
+}
diff --git a/t/defs/tests/07-match-values.json b/t/defs/tests/07-match-values.json
new file mode 100644
index 0000000..96c6e2a
--- /dev/null
+++ b/t/defs/tests/07-match-values.json
@@ -0,0 +1,166 @@
+{
+  "format": {
+    "version": {
+      "major": 1,
+      "minor": 0
+    }
+  },
+  "setenv": {},
+  "tests": [
+    {
+      "args": {
+        "filename": "t1.ini",
+        "section": "a",
+        "match_var_values": "value*"
+      },
+      "keys": [
+        "key1"
+      ],
+      "xform": "",
+      "backend": "ini",
+      "output": {
+        "exact": "value1"
+      },
+      "setenv": false,
+      "stdin": null
+    },
+    {
+      "args": {
+        "filename": "t1.ini",
+        "section": "a",
+        "match_var_values": "*ue2"
+      },
+      "keys": [
+        "key2"
+      ],
+      "xform": "",
+      "backend": "ini",
+      "output": {
+        "exact": "value2"
+      },
+      "setenv": false,
+      "stdin": null
+    },
+    {
+      "args": {
+        "filename": "t1.ini",
+        "section": "a",
+        "match_var_values": "*val'ue*"
+      },
+      "keys": [
+        "key3"
+      ],
+      "xform": "",
+      "backend": "ini",
+      "output": {
+        "exact": "\t\t val'ue3"
+      },
+      "setenv": false,
+      "stdin": null
+    },
+    {
+      "args": {
+        "filename": "t1.ini",
+        "section": "b sect",
+        "match_var_values": "*alu*"
+      },
+      "keys": [
+        "key4"
+      ],
+      "xform": "",
+      "backend": "ini",
+      "output": {
+        "exact": "v'alu'e4"
+      },
+      "setenv": false,
+      "stdin": null
+    },
+    {
+      "args": {
+        "filename": "t1.ini",
+        "section": "c"
+      },
+      "keys": [
+        "key5"
+      ],
+      "xform": "",
+      "backend": "ini",
+      "output": {
+        "exact": "\t\t# value5"
+      },
+      "setenv": false,
+      "stdin": null
+    },
+    {
+      "args": {
+        "filename": "t1.ini",
+        "section": "a",
+        "match_var_values": "*alu*"
+      },
+      "keys": [
+        "key1",
+        "key2"
+      ],
+      "xform": "newline-to-space",
+      "backend": "ini",
+      "output": {
+        "exact": "key1=value1 key2=value2 "
+      },
+      "setenv": false,
+      "stdin": null
+    },
+    {
+      "args": {
+        "filename": "t1.ini",
+        "section": "a",
+        "match_var_values": "*7"
+      },
+      "keys": [
+        "key6"
+      ],
+      "xform": "",
+      "backend": "ini",
+      "output": {
+        "exact": ""
+      },
+      "setenv": false,
+      "stdin": null
+    },
+    {
+      "args": {
+        "filename": "t1.ini",
+        "section": "b sect",
+        "check_only": "",
+        "match_var_values": "*7"
+      },
+      "keys": [
+        "key7"
+      ],
+      "xform": "",
+      "backend": "ini",
+      "output": {
+        "exit": true
+      },
+      "setenv": false,
+      "stdin": null
+    },
+    {
+      "args": {
+        "filename": "t1.ini",
+        "section": "b sect",
+        "check_only": "",
+        "match_var_values": "*7"
+      },
+      "keys": [
+        "key6"
+      ],
+      "xform": "",
+      "backend": "ini",
+      "output": {
+        "exit": false
+      },
+      "setenv": false,
+      "stdin": null
+    }
+  ]
+}
diff --git a/t/defs/tests/11-no-default.json b/t/defs/tests/11-no-default.json
new file mode 100644
index 0000000..492043f
--- /dev/null
+++ b/t/defs/tests/11-no-default.json
@@ -0,0 +1,173 @@
+{
+  "format": {
+    "version": {
+      "major": 1,
+      "minor": 0
+    }
+  },
+  "setenv": {},
+  "tests": [
+    {
+      "args": {
+        "filename": "t1.ini"
+      },
+      "keys": [
+        "key1"
+      ],
+      "xform": "",
+      "backend": "ini",
+      "output": {
+        "exact": "value1"
+      },
+      "setenv": false,
+      "stdin": null
+    },
+    {
+      "args": {
+        "filename": "t1.ini"
+      },
+      "keys": [
+        "key4"
+      ],
+      "xform": "",
+      "backend": "ini",
+      "output": {
+        "exact": ""
+      },
+      "setenv": false,
+      "stdin": null
+    },
+    {
+      "args": {
+        "filename": "t1.ini"
+      },
+      "keys": [
+        "key6"
+      ],
+      "xform": "",
+      "backend": "ini",
+      "output": {
+        "exact": "value6"
+      },
+      "setenv": false,
+      "stdin": null
+    },
+    {
+      "args": {
+        "filename": "t1.ini",
+        "section": "",
+        "section_specified": ""
+      },
+      "keys": [
+        "key1"
+      ],
+      "xform": "",
+      "backend": "ini",
+      "output": {
+        "exact": ""
+      },
+      "setenv": false,
+      "stdin": null
+    },
+    {
+      "args": {
+        "filename": "t1.ini",
+        "section": "",
+        "section_specified": ""
+      },
+      "keys": [
+        "key4"
+      ],
+      "xform": "",
+      "backend": "ini",
+      "output": {
+        "exact": ""
+      },
+      "setenv": false,
+      "stdin": null
+    },
+    {
+      "args": {
+        "filename": "t1.ini",
+        "section": "",
+        "section_specified": ""
+      },
+      "keys": [
+        "key6"
+      ],
+      "xform": "",
+      "backend": "ini",
+      "output": {
+        "exact": ""
+      },
+      "setenv": false,
+      "stdin": null
+    },
+    {
+      "args": {
+        "filename": "t2.ini"
+      },
+      "keys": [
+        "key1"
+      ],
+      "xform": "",
+      "backend": "ini",
+      "output": {
+        "exact": "1"
+      },
+      "setenv": false,
+      "stdin": null
+    },
+    {
+      "args": {
+        "filename": "t2.ini"
+      },
+      "keys": [
+        "key1",
+        "key2"
+      ],
+      "xform": "newline-to-space",
+      "backend": "ini",
+      "output": {
+        "exact": "key1=1 key2=2 "
+      },
+      "setenv": false,
+      "stdin": null
+    },
+    {
+      "args": {
+        "filename": "t2.ini",
+        "section": "",
+        "section_specified": ""
+      },
+      "keys": [
+        "key1"
+      ],
+      "xform": "",
+      "backend": "ini",
+      "output": {
+        "exact": "1"
+      },
+      "setenv": false,
+      "stdin": null
+    },
+    {
+      "args": {
+        "filename": "t2.ini",
+        "section": "",
+        "section_specified": ""
+      },
+      "keys": [
+        "key1",
+        "key2"
+      ],
+      "xform": "newline-to-space",
+      "backend": "ini",
+      "output": {
+        "exact": "key1=1 key2=2 "
+      },
+      "setenv": false,
+      "stdin": null
+    }
+  ]
+}
diff --git a/t/defs/tests/12-last-value.json b/t/defs/tests/12-last-value.json
new file mode 100644
index 0000000..3005780
--- /dev/null
+++ b/t/defs/tests/12-last-value.json
@@ -0,0 +1,239 @@
+{
+  "format": {
+    "version": {
+      "major": 1,
+      "minor": 0
+    }
+  },
+  "setenv": {},
+  "tests": [
+    {
+      "args": {
+        "filename": "t3.ini"
+      },
+      "keys": [
+        "both"
+      ],
+      "xform": "",
+      "backend": "ini",
+      "output": {
+        "exact": "default"
+      },
+      "setenv": false,
+      "stdin": null
+    },
+    {
+      "args": {
+        "filename": "t3.ini"
+      },
+      "keys": [
+        "defonly"
+      ],
+      "xform": "",
+      "backend": "ini",
+      "output": {
+        "exact": "default"
+      },
+      "setenv": false,
+      "stdin": null
+    },
+    {
+      "args": {
+        "filename": "t3.ini"
+      },
+      "keys": [
+        "aonly"
+      ],
+      "xform": "",
+      "backend": "ini",
+      "output": {
+        "exact": ""
+      },
+      "setenv": false,
+      "stdin": null
+    },
+    {
+      "args": {
+        "filename": "t3.ini",
+        "section": ""
+      },
+      "keys": [
+        "both"
+      ],
+      "xform": "",
+      "backend": "ini",
+      "output": {
+        "exact": "default"
+      },
+      "setenv": false,
+      "stdin": null
+    },
+    {
+      "args": {
+        "filename": "t3.ini",
+        "section": ""
+      },
+      "keys": [
+        "defonly"
+      ],
+      "xform": "",
+      "backend": "ini",
+      "output": {
+        "exact": "default"
+      },
+      "setenv": false,
+      "stdin": null
+    },
+    {
+      "args": {
+        "filename": "t3.ini",
+        "section": ""
+      },
+      "keys": [
+        "aonly"
+      ],
+      "xform": "",
+      "backend": "ini",
+      "output": {
+        "exact": ""
+      },
+      "setenv": false,
+      "stdin": null
+    },
+    {
+      "args": {
+        "filename": "t3.ini",
+        "section": "a"
+      },
+      "keys": [
+        "both"
+      ],
+      "xform": "",
+      "backend": "ini",
+      "output": {
+        "exact": "a"
+      },
+      "setenv": false,
+      "stdin": null
+    },
+    {
+      "args": {
+        "filename": "t3.ini",
+        "section": "a"
+      },
+      "keys": [
+        "defonly"
+      ],
+      "xform": "",
+      "backend": "ini",
+      "output": {
+        "exact": ""
+      },
+      "setenv": false,
+      "stdin": null
+    },
+    {
+      "args": {
+        "filename": "t3.ini",
+        "section": "a"
+      },
+      "keys": [
+        "aonly"
+      ],
+      "xform": "",
+      "backend": "ini",
+      "output": {
+        "exact": "a"
+      },
+      "setenv": false,
+      "stdin": null
+    },
+    {
+      "args": {
+        "filename": "t3.ini",
+        "section": "a",
+        "section_override": ""
+      },
+      "keys": [
+        "both"
+      ],
+      "xform": "",
+      "backend": "ini",
+      "output": {
+        "exact": "a"
+      },
+      "setenv": false,
+      "stdin": null
+    },
+    {
+      "args": {
+        "filename": "t3.ini",
+        "section": "a",
+        "section_override": ""
+      },
+      "keys": [
+        "defonly"
+      ],
+      "xform": "",
+      "backend": "ini",
+      "output": {
+        "exact": "default"
+      },
+      "setenv": false,
+      "stdin": null
+    },
+    {
+      "args": {
+        "filename": "t3.ini",
+        "section": "a",
+        "section_override": ""
+      },
+      "keys": [
+        "aonly"
+      ],
+      "xform": "",
+      "backend": "ini",
+      "output": {
+        "exact": "a"
+      },
+      "setenv": false,
+      "stdin": null
+    },
+    {
+      "args": {
+        "filename": "t3.ini",
+        "match_var_values": "def*",
+        "section": "a",
+        "section_override": ""
+      },
+      "keys": [
+        "both"
+      ],
+      "xform": "",
+      "backend": "ini",
+      "output": {
+        "exact": ""
+      },
+      "setenv": false,
+      "stdin": null
+    },
+    {
+      "args": {
+        "filename": "t3.ini",
+        "match_var_values": "a*",
+        "section": "a",
+        "section_override": ""
+      },
+      "keys": [
+        "both"
+      ],
+      "xform": "",
+      "backend": "ini",
+      "output": {
+        "exact": "a"
+      },
+      "setenv": false,
+      "stdin": null
+    }
+  ]
+}
diff --git a/t/defs/tools/encode.py b/t/defs/tools/encode.py
new file mode 100644
index 0000000..32e961a
--- /dev/null
+++ b/t/defs/tools/encode.py
@@ -0,0 +1,105 @@
+#!/usr/bin/python3
+#
+# Copyright (c) 2019  Peter Pentchev 
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+#    notice, this list of conditions and the following disclaimer.
+# 2. 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.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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.
+
+"""
+Encode a Python confget test data structure into a JSON file.
+"""
+
+import json
+
+from typing import Any, Dict
+
+from unit_tests.data import data as t_data
+from unit_tests.data import defs as t_defs
+
+
+class TestEncoder(json.JSONEncoder):
+    """ Encode the confget test data into serializable objects. """
+
+    def encode_test_file_def(self, obj: t_defs.TestFileDef) -> Dict[str, Any]:
+        """ Encode a full TestFileDef object. """
+        return {
+            'format': {
+                'version': {
+                    'major': 1,
+                    'minor': 0,
+                },
+            },
+            'setenv': obj.setenv,
+            'tests': [self.default(test) for test in obj.tests],
+        }
+
+    def encode_test_def(self, obj: t_defs.TestDef) -> Dict[str, Any]:
+        """ Encode a single test definition. """
+        return {
+            'args': obj.args,
+            'keys': obj.keys,
+            'xform': obj.xform,
+            'backend': obj.backend,
+            'output': self.default(obj.output),
+            'setenv': obj.setenv,
+            'stdin': obj.stdin,
+        }
+
+    def encode_exact_output_def(self, obj: t_defs.TestExactOutputDef
+                                ) -> Dict[str, str]:
+        """ Encode an exact output requirement. """
+        return {
+            'exact': obj.exact,
+        }
+
+    def encode_exit_ok_output_def(self, obj: t_defs.TestExitOKOutputDef
+                                  ) -> Dict[str, bool]:
+        """ Encode an exit code requirement. """
+        return {
+            'exit': obj.success,
+        }
+
+    SERIALIZERS = {
+        t_defs.TestFileDef: encode_test_file_def,
+        t_defs.TestDef: encode_test_def,
+        t_defs.TestExactOutputDef: encode_exact_output_def,
+        t_defs.TestExitOKOutputDef: encode_exit_ok_output_def,
+    }
+
+    def default(self, obj: Any) -> Any:
+        """ Meow. """
+        method = self.SERIALIZERS.get(type(obj), None)
+        if method is not None:
+            return method(self, obj)
+        return super(TestEncoder, self).default(obj)
+
+
+def main() -> None:
+    """ Main function: encode, output. """
+    for name, tdef in sorted(t_data.TESTS.items()):
+        print(f'--- {name} ---')
+        with open(f'output/{name}.json', mode='w') as outf:
+            print(json.dumps(tdef, indent=2, cls=TestEncoder), file=outf)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/t/defs/tools/generate.py b/t/defs/tools/generate.py
new file mode 100755
index 0000000..45dd99a
--- /dev/null
+++ b/t/defs/tools/generate.py
@@ -0,0 +1,146 @@
+#!/usr/bin/python3
+#
+# Copyright (c) 2018, 2019  Peter Pentchev 
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+#    notice, this list of conditions and the following disclaimer.
+# 2. 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.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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.
+
+"""
+Generate shell scripts with TAP output for the confget tests.
+"""
+
+from typing import List
+
+from unit_tests import data as t_data
+
+
+PRELUDE = r'''#!/bin/sh
+#
+# Copyright (c) 2008, 2011, 2019  Peter Pentchev
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+#    notice, this list of conditions and the following disclaimer.
+# 2. 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.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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.
+
+[ -z "$CONFGET" ] && CONFGET='./confget'
+[ -z "$TESTDIR" ] && TESTDIR='t'
+
+echo '1..{count}'
+'''
+
+CHECK_TESTFILE = r'''
+if [ ! -f "$TESTDIR/{fname}" ]; then
+        echo "Bail out!  No test file $TESTDIR/{fname}"
+        exit 255
+fi'''
+
+
+def add_cmdline_options(cmdline: List[str], test: t_data.defs.TestDef
+                        ) -> None:
+    """ Add the options from test.args into cmdline. """
+    for name, value in test.args.items():
+        option = t_data.CMDLINE_OPTIONS[name]
+        if option[0] == '':
+            continue
+        cmdline.append(f"'{option[0]}'")
+        if option[1]:
+            if name == 'filename':
+                cmdline.append(f'"$TESTDIR/{value}"')
+            else:
+                cmdline.append(t_data.shell_escape(value))
+
+
+def main() -> None:
+    """ Main function: generate the test files. """
+    tests = t_data.load_all_tests('.')
+    for fname, data in sorted(tests.items()):
+        print(f'Generating {fname}.t with {len(data.tests)} tests')
+        with open(f'{fname}.t', mode='w') as testf:
+            print(PRELUDE.format(count=len(data.tests)), file=testf)
+
+            filenames = sorted({
+                test.args['filename'] for test in data.tests
+                if 'filename' in test.args
+            })
+            for filename in filenames:
+                print(CHECK_TESTFILE.format(fname=filename), file=testf)
+
+            for idx, test in enumerate(data.tests):
+                tap_idx = idx + 1
+
+                cmdline: List[str] = []
+                if test.setenv:
+                    cmdline.append('env')
+                    for name, value in sorted(data.setenv.items()):
+                        cmdline.append(f'{name}={t_data.shell_escape(value)}')
+
+                cmdline.append('$CONFGET')
+                if test.backend != 'ini':
+                    cmdline.extend(['-t', test.backend])
+
+                add_cmdline_options(cmdline, test)
+
+                if test.stdin is not None:
+                    cmdline.extend([
+                        t_data.CMDLINE_OPTIONS['filename'][0],
+                        '-'
+                    ])
+
+                cmdline.extend([f"'{key}'" for key in test.keys])
+
+                if test.stdin is not None:
+                    cmdline.extend(['<', f'"$TESTDIR/{test.stdin}"'])
+
+                xform = t_data.XFORM[test.xform].command
+                print(f'- {tap_idx}: {" ".join(cmdline)} {xform}')
+
+                print(f'v=`{" ".join(cmdline)} {xform}`', file=testf)
+                print('res="$?"', file=testf)
+
+                check = test.output.get_check()
+                print(f'  - {check}')
+                print(f"if {check}; then echo 'ok {tap_idx}'; else "
+                      f'echo "not ok {tap_idx} {test.output.var_name} is '
+                      f"'${test.output.var_name}'\"; fi",
+                      file=testf)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/t/t1.ini b/t/t1.ini
new file mode 100644
index 0000000..3b58b6e
--- /dev/null
+++ b/t/t1.ini
@@ -0,0 +1,44 @@
+# This is just a sample config file
+# It is hereby placed into the public domain.
+
+	# A sample INI file
+
+# An INI file is separated into sections.
+# Each section starts with its name in square brackets on a line by itself.
+# Each section contains key=value pairs, with the whitespace before
+# the key, around the equal sign, or at the end of the line simply ignored.
+# A value may span multiple lines - a line continuation is denoted by
+# a backslash (\) as the last character on the line.
+
+; Empty lines, lines consisting entirely of whitespace, or lines with
+; optional whitespace at the start, followed by a semicolon (;) or
+; a pound sign (#) and any other characters, are ignored.
+
+[a]
+# This is section a.
+key1=value1
+	key2 = value2   
+key3		=\
+		 val'ue3
+
+[ b sect  ]
+key4=		v'alu'e4
+
+	[   c  	   ]
+
+	key5		= \
+	\
+	# value5
+	# And nothing more.
+
+# Okay, let's see what happens now...
+# A section may be split within the file - another section header with
+# the same name continues the definition of variables in the same section.
+
+[a]
+key6=value6
+
+[b sect]
+key7=value7
+
+# And that's all, folks!
diff --git a/t/t2.ini b/t/t2.ini
new file mode 100644
index 0000000..7646698
--- /dev/null
+++ b/t/t2.ini
@@ -0,0 +1,25 @@
+# Just a test file.  Or something.
+# This file is hereby placed into the public domain.
+
+# Some configuration files are so simple, they do not even need sections.
+# The confget utility uses the concept of a "default" section - the one
+# that is used if the -s option is not specified.
+# If any key/value pairs is found in the INI file before the first section
+# header, they - and only they - are in the default section.
+# If there are no key/value pairs before the section header, then the first
+# section defined is considered to be the default.
+
+# In this file, the following two pairs will be in the default section,
+# and they may be obtained by "confget -f t2.ini key1 key2".
+key1=1
+key2=2
+
+# In the t1.ini file, there are no key/value pairs before the first
+# section header, so the first section - "a" - will be the default,
+# and "confget -f t1.ini key1" would return "value1".
+
+# The following values of key1 and key2 may only be obtained by specifying
+# the section name: "confget -f t2.ini -s sec1 key1 key2".
+[sec1]
+key1=3
+key2=4
diff --git a/t/t3.ini b/t/t3.ini
new file mode 100644
index 0000000..482d172
--- /dev/null
+++ b/t/t3.ini
@@ -0,0 +1,13 @@
+# Just a test file.  Or something.
+# This file is hereby placed into the public domain.
+#
+# Test if confget can properly override "default" values with
+# ones specified in a named section.
+
+both=default
+defonly=default
+
+[a]
+aonly=b
+both=a
+aonly=a
diff --git a/test-fgetln.c b/test-fgetln.c
new file mode 100644
index 0000000..3b14bfc
--- /dev/null
+++ b/test-fgetln.c
@@ -0,0 +1,50 @@
+/*-
+ * Copyright (c) 2012, 2013  Peter Pentchev
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. 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.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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.
+ */
+
+#include 
+#include 
+
+int
+main(void)
+{
+	char *line;
+	size_t n;
+
+	line = fgetln(stdin, &n);
+	if (line == NULL) {
+		if (ferror(stdin))
+			perror("Trying to read a line");
+		else
+			puts("No data read");
+	} else {
+		size_t i;
+
+		printf("%zu 0 ", n);
+		for (i = 0; i < n; i++)
+			putchar(*line++);
+	}
+	return (0);
+}
diff --git a/test-getline.c b/test-getline.c
new file mode 100644
index 0000000..85a3152
--- /dev/null
+++ b/test-getline.c
@@ -0,0 +1,50 @@
+/*-
+ * Copyright (c) 2012, 2013  Peter Pentchev
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. 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.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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.
+ */
+
+#include 
+#include 
+
+int
+main(void)
+{
+	char *line;
+	size_t n;
+	ssize_t res;
+
+	line = NULL;
+	n = 0;
+	res = getline(&line, &n, stdin);
+	if (res == -1) {
+		if (ferror(stdin))
+			perror("Trying to read a line");
+		else
+			puts("No data read");
+	} else {
+		printf("%zu %zd %s", n, res, line);
+		free(line);
+	}
+	return (0);
+}
-- 
cgit v1.2.3


From fd268181119f970d82f931193c6214cef1ca2e34 Mon Sep 17 00:00:00 2001
From: Peter Pentchev 
Date: Wed, 27 Feb 2019 00:44:26 +0200
Subject: Import confget_2.2.0-4.debian.tar.xz

[dgit import tarball confget 2.2.0-4 confget_2.2.0-4.debian.tar.xz]
---
 changelog                          | 225 +++++++++++++++++++++++++++++++++++++
 control                            | 113 +++++++++++++++++++
 copyright                          |  40 +++++++
 gbp.conf                           |   5 +
 patches/python-no-executable.patch |  19 ++++
 patches/series                     |   2 +
 patches/test-too-many-pypy.patch   |  16 +++
 rules                              | 126 +++++++++++++++++++++
 source/format                      |   1 +
 tests/check-doc.sh                 |  12 ++
 tests/control                      |  15 +++
 tests/tap-python.sh                |  32 ++++++
 upstream/metadata                  |   6 +
 upstream/signing-key.asc           |  51 +++++++++
 watch                              |   3 +
 15 files changed, 666 insertions(+)
 create mode 100644 changelog
 create mode 100644 control
 create mode 100644 copyright
 create mode 100644 gbp.conf
 create mode 100644 patches/python-no-executable.patch
 create mode 100644 patches/series
 create mode 100644 patches/test-too-many-pypy.patch
 create mode 100755 rules
 create mode 100644 source/format
 create mode 100755 tests/check-doc.sh
 create mode 100644 tests/control
 create mode 100755 tests/tap-python.sh
 create mode 100644 upstream/metadata
 create mode 100644 upstream/signing-key.asc
 create mode 100644 watch

diff --git a/changelog b/changelog
new file mode 100644
index 0000000..2305af2
--- /dev/null
+++ b/changelog
@@ -0,0 +1,225 @@
+confget (2.2.0-4) unstable; urgency=medium
+
+  * Use the test-name autopkgtest feature.
+
+ -- Peter Pentchev   Wed, 27 Feb 2019 00:44:26 +0200
+
+confget (2.2.0-3) unstable; urgency=medium
+
+  * Switch to a DEP-14 debian/master branch.
+
+ -- Peter Pentchev   Thu, 14 Feb 2019 18:04:50 +0200
+
+confget (2.2.0-2) unstable; urgency=medium
+
+  * Move the Python build dependencies to Build-Depends-Indep and
+    only use the Python buildsystem when available, thus allowing
+    confget to actually build on architectures where PyPy is not
+    available yet.
+
+ -- Peter Pentchev   Tue, 15 Jan 2019 14:45:18 +0200
+
+confget (2.2.0-1) unstable; urgency=medium
+
+  * New upstream version:
+    - update the upstream copyright years
+    - build the Python 2.x, 3.x, and PyPy modules
+    - run the TAP tests (both at build time and via autopkgtest) with
+      the Python modules, too, not only with the C executable
+  * Add the year 2019 to my debian/* copyright notice.
+  * Declare compliance with Debian Policy 4.3.0 with no changes.
+  * Bump the debhelper compatibility level to 12 with no changes.
+  * Add a trivial git-buildpackage config file.
+  * Conditionally run some commands in the rules file depending on which
+    packages are being built.
+  * Change DEB_NODOC to BUILD_DOC for consistency.
+  * Check for "nocheck" in DEB_BUILD_OPTIONS before running tests.
+  * Break out the autopkgtests's "is the documentation installed" check
+    into a separate tool.
+  * Run `dh_missing --fail-missing`.
+
+ -- Peter Pentchev   Mon, 14 Jan 2019 12:38:52 +0200
+
+confget (2.1.1-1) unstable; urgency=medium
+
+  * Build-depend on debhelper 11 and use the B-D: debhelper-compat (= 11)
+    mechanism.
+  * Bump the year on my debian/* copyright notice.
+  * Use my Debian e-mail address.
+  * Bring up to compliance with Debian Policy 4.2.1: install the upstream
+    release notes (CHANGES) as NEWS and not changelog.
+  * Add trivial autopkgtests running adequate and feature-check.
+  * Minimize the upstream signing key, only keep the roam@ringlet.net UID.
+  * New upstream version.
+
+ -- Peter Pentchev   Tue, 27 Nov 2018 11:12:44 +0200
+
+confget (2.1.0-1) unstable; urgency=medium
+
+  * Declare compliance with Debian Policy 4.1.1 with no changes.
+  * Let dpkg-buildflags take care of the LFS compiler and linker flags.
+  * New upstream version; update the copyright years.
+  * Add "Rules-Requires-Root: no" to the source control stanza and
+    override the upstream Makefile's install(1) invocations.
+
+ -- Peter Pentchev   Sat, 11 Nov 2017 23:21:21 +0200
+
+confget (2.0.0-3) unstable; urgency=medium
+
+  * Update the package for compliance with Debian Policy 4.1.0:
+    - do not install the example files and the manual page if "nodoc" is
+      specified in DEB_BUILD_OPTIONS or the "nodoc" build profile is active
+    - use the source t/ directory in the autopkgtest suite since
+      /usr/share/doc/confget/examples/tests/ will not exist with "nodoc"
+    - drop the implied "Testsuite: autopkgtest" source control header
+  * Bump the debhelper compatibility level to 11 and the build dependency
+    to 10.8~.
+
+ -- Peter Pentchev   Mon, 25 Sep 2017 12:24:14 +0300
+
+confget (2.0.0-2) unstable; urgency=medium
+
+  * Depend on debhelper 10 now that it is in unstable, testing, and
+    jessie-backports.  Drop the Lintian override about the version.
+  * Switch to the HTTPS scheme for various Debian and upstream URLs.
+  * Use the v4 substitution variables in the watch file.
+
+ -- Peter Pentchev   Thu, 17 Nov 2016 13:53:02 +0200
+
+confget (2.0.0-1) unstable; urgency=medium
+
+  * Use export-minimal for upstream's PGP public key.
+  * Use "BSD-2-clause" as the short license name in the copyright file.
+  * Point Vcs-Git and Vcs-Browser to the GitLab repository after
+    the migration from Gitorious and change both to HTTPS URLs.
+  * Declare compliance with Debian Policy 3.9.8 with no changes.
+  * Add an upstream metadata file.
+  * Bump the year on my debian/* copyright notice.
+  * Bump the debhelper compatibility level to 10:
+    - bump the version of the debhelper B-D to 9.20160403~
+    - drop the --parallel option from the dh invocation (enabled by default)
+    - override the "experimental debhelper version" Lintian warning
+  * New upstream release:
+    - fix a FTBFS with new versions of glibc by replacing the _GNU_SOURCE and
+      _BSD_SOURCE feature macros with _POSIX_C_SOURCE and _XOPEN_SOURCE
+    - install all the tests as examples, not just the *.ini files used by them
+    - teach the manpage test about compressed manual pages so that it may be
+      run on a system where confget has been installed, e.g. via autopkgtest
+    - allow passing linker flags for Large File Support, too
+  * Update the upstream copyright years in the copyright file.
+  * Add an autopkgtest suite running the installed tests.
+  * Look for a *.tar.bz2 tarball in the watch file and bump the version to 4.
+  * Fold the upstream source into a single Files section in the copyright file.
+
+ -- Peter Pentchev   Mon, 11 Apr 2016 12:47:31 +0300
+
+confget (1.05-1) unstable; urgency=medium
+
+  * Update the copyright file:
+    - convert it to the 1.0 format
+    - separate lists of copyrighted files with whitespace, not commas
+    - bump the year of my copyright notice
+    - fix the upstream homepage location
+  * Bump Standards-Version to 3.9.5 with no changes.
+  * Bump the debhelper compatibility level to 9 with no changes.
+  * Get the hardening options directly from debhelper:
+    - remove the build dependency on hardening-includes
+    - no longer include the hardening Makefile snippet into the rules file
+    - explicitly enable all the hardening features; they may be disabled
+      in the future if confget should fail to build anywhere
+    - use DEB_CFLAGS_MAINT_APPEND to, well, append to CFLAGS
+  * Enable parallel building - not that it matters a lot in this case :)
+  * New upstream release:
+    - honors CPPFLAGS now, so the hardening flags may be passed as-is
+    - autodetects getline(3) support, so no need for CFLAGS_CONF
+    - renames PCRE_CFLAGS to PCRE_CPPFLAGS, so follow suit
+    - update the copyright years in the copyright file
+    - follow upstream and switch from -ansi to -std=c99
+  * Remove the obsolete DM-Upload-Allowed field.
+  * Drop the source compression options; dpkg-dev's defaults are good enough.
+  * Update the watch file after the devel.ringlet.net website change.
+  * Let uscan verify upstream's signature against my own key.
+
+ -- Peter Pentchev   Sat, 23 Aug 2014 12:07:22 +0300
+
+confget (1.03-1) unstable; urgency=low
+
+  * New upstream release:
+    - allow spaces in INI file section names.  Closes: #632400
+    - update the copyright years in debian/copyright
+  * Bump Standards-Version to 3.9.2 with no changes.
+  * Update the copyright file to the latest DEP 5 candidate format and
+    fix the DEP 5 URL after the Alioth migration.
+  * Add Multi-Arch: foreign to the binary package.
+
+ -- Peter Pentchev   Wed, 06 Jul 2011 19:52:10 +0300
+
+confget (1.02-5) unstable; urgency=low
+
+  * Update the copyright file to the latest DEP 5 candidate format and
+    bump the year of my copyright notice.
+  * Upload to unstable.
+
+ -- Peter Pentchev   Tue, 08 Feb 2011 17:30:54 +0200
+
+confget (1.02-4) experimental; urgency=low
+
+  * Switch to Git and point the Vcs-* fields to Gitorious.
+  * Bump Standards-Version to 3.9.1 with no changes.
+  * Switch to bzip2 compression for the Debian tarball.
+  * Fix the formatting of the copyright file - Maintainer at the top,
+    no Author in the file stanzas.
+  * Wrap the Build-Depends line.
+  * Turn on the build hardening by default.
+  * Use the hardening-includes package instead of hardening-wrapper so that
+    the hardening flags are visible in CFLAGS and LDFLAGS.
+  * Bump the debhelper compatibility level to 8 with no changes.
+
+ -- Peter Pentchev   Fri, 24 Dec 2010 16:26:38 +0200
+
+confget (1.02-3) unstable; urgency=low
+
+  * Simplify the rules file by removing the useless diffsrc target;
+    it was intended for my own use, but its time has long passed.
+  * Simplify the rules file even more by using debhelper override rules.
+  * Bump Standards-Version to 3.8.4 with no changes.
+  * Switch to the 3.0 (quilt) source format - we don't have any patches,
+    but I still like its debian.tar.gz distribution mode.
+  * Bump the copyright years on the Debian packaging.
+  * Drop the groff dependency - europs.tmac was moved into
+    groff-base-1.20.1-6 and our test suite works again.
+  * Update the copyright file to DEP 5 rev. 135:
+    - declare the public domain status in the Copyright clause, too
+    - use BSD instead of BSD-2
+  * Use dpkg-buildflags from dpkg-dev 1.15.7 to obtain CFLAGS, CPPFLAGS,
+    and LDFLAGS instead of depending on dpkg-buildpackage to provide them.
+  * Shorten the Vcs-Browser URL.
+  * Bump Standards-Version to 3.9.0 with no changes.
+
+ -- Peter Pentchev   Mon, 28 Jun 2010 17:38:07 +0300
+
+confget (1.02-2) unstable; urgency=low
+
+  * Only enable the hardening wrapper conditionally.
+  * Bump Standards-Version to 3.8.2 with no changes.
+  * Remove the useless "configure" target from the rules file.
+  * In the copyright file, break out the license in a separate section and
+    declare compatibility with the current revision of DEP 5.
+  * Add groff as a build dependency for the test suite; groff-base seems
+    to not be enough any longer.
+
+ -- Peter Pentchev   Tue, 11 Aug 2009 15:42:47 +0300
+
+confget (1.02-1) unstable; urgency=low
+
+  * New upstream version.
+  * Specify the examples directory location at install time.
+  * Update the copyright file.
+
+ -- Peter Pentchev   Sat, 21 Mar 2009 19:28:39 +0200
+
+confget (1.01-1) unstable; urgency=low
+
+  * Initial release. Closes: #502543.
+
+ -- Peter Pentchev   Tue, 17 Mar 2009 18:10:35 +0200
diff --git a/control b/control
new file mode 100644
index 0000000..0dbd8e6
--- /dev/null
+++ b/control
@@ -0,0 +1,113 @@
+Source: confget
+Section: text
+Priority: optional
+Maintainer: Peter Pentchev 
+Build-Depends:
+ debhelper-compat (= 12),
+ dh-python,
+ libpcre3-dev,
+Build-Depends-Indep:
+ pypy,
+ pypy-setuptools,
+ pypy-six,
+ python-all,
+ python-ddt,
+ python-pytest,
+ python-six,
+ python-setuptools,
+ python3-all,
+ python3-ddt,
+ python3-pytest,
+ python3-six,
+ python3-setuptools,
+Standards-Version: 4.3.0
+Homepage: https://devel.ringlet.net/textproc/confget/
+Vcs-Git: https://gitlab.com/confget/confget.git -b debian/master
+Vcs-Browser: https://gitlab.com/confget/confget/tree/debian/master
+Rules-Requires-Root: no
+
+Package: confget
+Architecture: any
+Depends: ${shlibs:Depends}, ${misc:Depends}
+Multi-Arch: foreign
+Description: read variables from INI-style configuration files
+ The confget utility examines a INI-style configuration file and retrieves
+ the value of the specified variables from the specified section.
+ Its intended use is to let shell scripts use the same INI-style
+ configuration files as other programs, to avoid duplication of data.
+ .
+ The confget utility may retrieve the values of one or more variables,
+ list all the variables in a specified section, list only those whose names
+ or values match a specified pattern (shell glob or regular expression), or
+ check if a variable is present in the file at all.  It has a "shell-quoting"
+ output mode that quotes the variable values in a way suitable for passing
+ them directly to a Bourne-style shell.
+
+Package: pypy-confget
+Section: python
+Architecture: all
+Multi-Arch: foreign
+Depends: ${misc:Depends}, ${pypy:Depends}
+Recommends: ${pypy:Recommends}
+Suggests: ${pypy:Suggests}
+Provides: ${pypy:Provides}
+Description: read variables from INI-style configuration files - PyPy library
+ The confget utility examines a INI-style configuration file and retrieves
+ the value of the specified variables from the specified section.
+ Its intended use is to let shell scripts use the same INI-style
+ configuration files as other programs, to avoid duplication of data.
+ .
+ The confget utility may retrieve the values of one or more variables,
+ list all the variables in a specified section, list only those whose names
+ or values match a specified pattern (shell glob or regular expression), or
+ check if a variable is present in the file at all.  It has a "shell-quoting"
+ output mode that quotes the variable values in a way suitable for passing
+ them directly to a Bourne-style shell.
+ .
+ This package contains the PyPy library.
+
+Package: python-confget
+Section: python
+Architecture: all
+Multi-Arch: foreign
+Depends: ${misc:Depends}, ${python:Depends}
+Recommends: ${python:Recommends}
+Suggests: ${python:Suggests}
+Provides: ${python:Provides}
+Description: read variables from INI-style configuration files - Python 2.x library
+ The confget utility examines a INI-style configuration file and retrieves
+ the value of the specified variables from the specified section.
+ Its intended use is to let shell scripts use the same INI-style
+ configuration files as other programs, to avoid duplication of data.
+ .
+ The confget utility may retrieve the values of one or more variables,
+ list all the variables in a specified section, list only those whose names
+ or values match a specified pattern (shell glob or regular expression), or
+ check if a variable is present in the file at all.  It has a "shell-quoting"
+ output mode that quotes the variable values in a way suitable for passing
+ them directly to a Bourne-style shell.
+ .
+ This package contains the Python 2.x library.
+
+Package: python3-confget
+Section: python
+Architecture: all
+Multi-Arch: foreign
+Depends: ${misc:Depends}, ${python3:Depends}
+Recommends: ${python3:Recommends}
+Suggests: ${python3:Suggests}
+Provides: ${python3:Provides}
+Description: read variables from INI-style configuration files - Python 3.x library
+ The confget utility examines a INI-style configuration file and retrieves
+ the value of the specified variables from the specified section.
+ Its intended use is to let shell scripts use the same INI-style
+ configuration files as other programs, to avoid duplication of data.
+ .
+ The confget utility may retrieve the values of one or more variables,
+ list all the variables in a specified section, list only those whose names
+ or values match a specified pattern (shell glob or regular expression), or
+ check if a variable is present in the file at all.  It has a "shell-quoting"
+ output mode that quotes the variable values in a way suitable for passing
+ them directly to a Bourne-style shell.
+ .
+ This package contains the Python 3.x library.
diff --git a/copyright b/copyright
new file mode 100644
index 0000000..4c30eb8
--- /dev/null
+++ b/copyright
@@ -0,0 +1,40 @@
+Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+Upstream-Name: confget
+Upstream-Contact: Peter Pentchev 
+Source: https://devel.ringlet.net/textproc/confget/
+License: BSD-2-clause
+
+Files: *
+Copyright: Copyright (c) 2008 - 2019  Peter Pentchev
+License: BSD-2-clause
+
+Files: makedep.sh t/t1.ini t/t2.ini t/t3.ini
+Copyright: This file is hereby placed in the public domain.
+License: public-domain
+  This file is hereby placed in the public domain.
+
+Files: debian/*
+Copyright: Copyright (c) 2008 - 2014, 2016 - 2019  Peter Pentchev
+License: BSD-2-clause
+
+License: BSD-2-clause
+  Redistribution and use in source and binary forms, with or without
+  modification, are permitted provided that the following conditions
+  are met:
+  1. Redistributions of source code must retain the above copyright
+     notice, this list of conditions and the following disclaimer.
+  2. 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.
+  .
+  THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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.
diff --git a/gbp.conf b/gbp.conf
new file mode 100644
index 0000000..0dcab64
--- /dev/null
+++ b/gbp.conf
@@ -0,0 +1,5 @@
+[DEFAULT]
+pristine-tar = True
+sign-tags = True
+debian-branch = debian/master
+upstream-branch = master
diff --git a/patches/python-no-executable.patch b/patches/python-no-executable.patch
new file mode 100644
index 0000000..4ba1624
--- /dev/null
+++ b/patches/python-no-executable.patch
@@ -0,0 +1,19 @@
+Description: Do not install the Python command-line script.
+Forwarded: not-needed
+Author: Peter Pentchev 
+Last-Update: 2019-01-13
+
+--- a/python/setup.py
++++ b/python/setup.py
+@@ -122,11 +122,5 @@
+         'Topic :: Utilities',
+     ],
+ 
+-    entry_points={
+-        'console_scripts': [
+-            'confget=confget.__main__:main',
+-        ],
+-    },
+-
+     zip_safe=True,
+ )
diff --git a/patches/series b/patches/series
new file mode 100644
index 0000000..7cff224
--- /dev/null
+++ b/patches/series
@@ -0,0 +1,2 @@
+python-no-executable.patch
+test-too-many-pypy.patch
diff --git a/patches/test-too-many-pypy.patch b/patches/test-too-many-pypy.patch
new file mode 100644
index 0000000..e851b56
--- /dev/null
+++ b/patches/test-too-many-pypy.patch
@@ -0,0 +1,16 @@
+Description: Skip some of the "too many options" tests on PyPy, too.
+Forwarded: yes
+Author: Peter Pentchev 
+Last-Update: 2019-01-13
+
+--- a/t/14-bespoke-too-many.t
++++ b/t/14-bespoke-too-many.t
+@@ -57,7 +57,7 @@
+     '-q features -q feature BASE' \
+     '-q features key1'; do
+ 	if [ "${args#-q sections -q}" != "$args" ] || [ "${args#-q features -q}" != "$args" ]; then
+-		if [ "${CONFGET#*python}" != "$CONFGET" ]; then
++		if [ "${CONFGET#*python}" != "$CONFGET" ] || [ "${CONFGET#*pypy}" != "$CONFGET" ]; then
+ 			echo "ok $idx $args - skipped, Python argparse"
+ 			idx="$((idx + 1))"
+ 			continue
diff --git a/rules b/rules
new file mode 100755
index 0000000..8f89201
--- /dev/null
+++ b/rules
@@ -0,0 +1,126 @@
+#!/usr/bin/make -f
+# -*- makefile -*-
+# Debian build rules for confget, the configuration variable extractor
+
+export PYBUILD_NAME=confget
+export PYBUILD_DISABLE=test/pypy
+
+# Aim for the top, adapt if anything should break on the buildds.
+DEB_BUILD_MAINT_OPTIONS=	hardening=+all future=+lfs
+export DEB_BUILD_MAINT_OPTIONS
+
+DEB_CFLAGS_MAINT_APPEND=	-pipe -Wall -W -std=c99 -pedantic -Wbad-function-cast \
+		-Wcast-align -Wcast-qual -Wchar-subscripts -Winline \
+		-Wmissing-prototypes -Wnested-externs -Wpointer-arith \
+		-Wredundant-decls -Wshadow -Wstrict-prototypes -Wwrite-strings
+ifneq (,$(filter werror,$(DEB_BUILD_OPTIONS)))
+	DEB_CFLAGS_MAINT_APPEND+=	-Werror
+endif
+export DEB_CFLAGS_MAINT_APPEND
+
+export PCRE_CPPFLAGS=-DHAVE_PCRE
+export PCRE_LIBS=-lpcre
+
+ifneq (,$(filter confget,$(shell dh_listpackages)))
+BUILD_IMPL_C=yes
+else
+BUILD_IMPL_C=no
+endif
+ifneq (,$(filter pypy-confget,$(shell dh_listpackages)))
+BUILD_IMPL_PYPY=yes
+else
+BUILD_IMPL_PYPY=no
+endif
+ifneq (,$(filter python-confget,$(shell dh_listpackages)))
+BUILD_IMPL_PY2=yes
+else
+BUILD_IMPL_PY2=no
+endif
+ifneq (,$(filter python3-confget,$(shell dh_listpackages)))
+BUILD_IMPL_PY3=yes
+else
+BUILD_IMPL_PY3=no
+endif
+ifneq (no no no,${BUILD_IMPL_PYPY} ${BUILD_IMPL_PY2} ${BUILD_IMPL_PY3})
+BUILD_IMPL_PY=yes
+else
+BUILD_IMPL_PY=no
+endif
+
+ifeq (,$(filter nodoc,$(DEB_BUILD_OPTIONS) $(DEB_BUILD_PROFILES)))
+BUILD_DOC=yes
+else
+BUILD_DOC=no
+endif
+
+D=	${CURDIR}/debian
+DTMP=	$D/tmp
+PY=	${CURDIR}/python
+TESTD=	${CURDIR}/t
+
+override_dh_auto_build:
+	@echo 'building C: ${BUILD_IMPL_C}, py2: ${BUILD_IMPL_PY2}, py3: ${BUILD_IMPL_PY3}, python: ${BUILD_IMPL_PY}, doc: ${BUILD_DOC}'
+ifeq (yes,${BUILD_IMPL_C})
+	dh_auto_build -- LFS_CPPFLAGS= LFS_LDFLAGS=
+endif
+ifeq (yes,${BUILD_IMPL_PY})
+	dh_auto_build -D '${PY}' --buildsystem pybuild
+endif
+
+override_dh_auto_install:
+ifeq (yes,${BUILD_IMPL_C})
+ifeq (no,${BUILD_DOC})
+	mv -f Makefile Makefile.bak
+	sed -e 's/^install:.*/install: all install-bin/' Makefile.bak > Makefile
+endif
+	dh_auto_install -- DESTDIR=${CURDIR}/debian/confget PREFIX=/usr \
+		MANDIR=/usr/share/man/man BINGRP=root MANGRP=root \
+		EXAMPLESDIR=/usr/share/doc/confget/examples \
+		INSTALL_PROGRAM='install -m 755' \
+		INSTALL_SCRIPT='install -m 755' INSTALL_DATA='install -m 644'
+ifeq (no,${BUILD_DOC})
+	mv -f Makefile.bak Makefile
+endif
+endif
+ifeq (yes,${BUILD_IMPL_PY})
+	dh_auto_install -D '${PY}' --buildsystem pybuild
+endif
+
+ifeq (,$(filter nocheck,${DEB_BUILD_OPTIONS}))
+override_dh_auto_test:
+ifeq (yes,${BUILD_IMPL_C})
+	dh_auto_test
+endif
+ifeq (yes,${BUILD_IMPL_PY})
+	env PYTHONPATH='${PY}' '$D/tests/tap-python.sh'
+	env TESTDIR='${TESTD}' dh_auto_test -D '${PY}' --buildsystem pybuild -- --test-pytest --test-args '${PY}/unit_tests'
+endif
+endif
+
+override_dh_installchangelogs:
+	dh_installchangelogs -X CHANGES
+	set -e; for pkg in $$(dh_listpackages); do \
+		install -m 644 CHANGES "debian/$$pkg/usr/share/doc/$$pkg/NEWS"; \
+	done
+
+override_dh_auto_clean:
+	dh_auto_clean
+	if dpkg-query -W -f '$${Package}\n' | fgrep -qxe python-setuptools -e python3-setuptools -e pypy-setuptools; then \
+		dh_auto_clean -D '${PY}' --buildsystem pybuild; \
+	fi
+	rm -rf -- '${PY}/.pytest_cache' '${PY}/confget.egg-info'
+
+override_dh_missing:
+	dh_missing --fail-missing
+
+%:
+	set -e; \
+	unset with; \
+	for pkg in python2 python3 pypy; do \
+		if dpkg-query -W "$${pkg%2}-setuptools" > /dev/null 2>&1; then \
+			with="$$with,$$pkg"; \
+		fi; \
+	done; \
+	with="$${with:+--with $${with#,}}"; \
+	echo "Running 'dh $@ $$with'"; \
+	dh $@ $$with
diff --git a/source/format b/source/format
new file mode 100644
index 0000000..163aaf8
--- /dev/null
+++ b/source/format
@@ -0,0 +1 @@
+3.0 (quilt)
diff --git a/tests/check-doc.sh b/tests/check-doc.sh
new file mode 100755
index 0000000..a01445b
--- /dev/null
+++ b/tests/check-doc.sh
@@ -0,0 +1,12 @@
+#!/bin/sh
+
+set -e
+
+if [ -d /usr/share/doc/confget/examples ]; then
+	export MANPAGE='/usr/share/man/man1/confget.1.gz'
+	export TESTDIR='/usr/share/doc/confget/examples/tests'
+else
+	export TESTDIR='t'
+fi
+
+exec "$@"
diff --git a/tests/control b/tests/control
new file mode 100644
index 0000000..02439e6
--- /dev/null
+++ b/tests/control
@@ -0,0 +1,15 @@
+Test-Command: debian/tests/check-doc.sh env CONFGET='/usr/bin/confget' sh -c 'exec prove -v "$TESTDIR"'
+Depends: @, perl, groff-base
+Features: test-name=tap-perl
+
+Test-Command: debian/tests/check-doc.sh debian/tests/tap-python.sh
+Depends: @, perl, groff-base
+Features: test-name=tap-python
+
+Test-Command: adequate confget
+Depends: @, adequate
+Features: test-name=adequate
+
+Test-Command: feature-check -O -qfeatures -P '' confget BASE
+Depends: @, feature-check
+Features: test-name=feature-check
diff --git a/tests/tap-python.sh b/tests/tap-python.sh
new file mode 100755
index 0000000..06cbfee
--- /dev/null
+++ b/tests/tap-python.sh
@@ -0,0 +1,32 @@
+#!/bin/sh
+
+set -e
+
+# Find the available Python 2.x and 3.x versions.
+
+interpreters=''
+if [ -n "$(command -v pyversions 2>/dev/null)" ]; then
+	for ver in $(pyversions -i -v); do
+		interpreters="$interpreters python$ver"
+	done
+fi
+if [ -n "$(command -v py3versions 2>/dev/null)" ]; then
+	for ver in $(py3versions -i -v); do
+		interpreters="$interpreters python$ver"
+	done
+fi
+
+# Add PyPy into the mix.
+
+if [ -n "$(command -v pypy 2>/dev/null)" ]; then
+	interpreters="$interpreters pypy"
+fi
+
+# Finally run the tests.
+
+for python in $interpreters; do
+	printf -- '\n\n============ Testing %s\n\n' "$python"
+	env CONFGET="$python -m confget" prove t
+done
+
+printf -- '\n\n============ The TAP tests passed for all Python versions\n\n'
diff --git a/upstream/metadata b/upstream/metadata
new file mode 100644
index 0000000..bd233e4
--- /dev/null
+++ b/upstream/metadata
@@ -0,0 +1,6 @@
+Name: confget
+Bug-Submit: mailto:roam@ringlet.net
+Contact: Peter Pentchev 
+Security-Contact: Peter Pentchev 
+Repository: https://gitlab.com/confget/confget.git
+Repository-Browse: https://gitlab.com/confget/confget
diff --git a/upstream/signing-key.asc b/upstream/signing-key.asc
new file mode 100644
index 0000000..85224fe
--- /dev/null
+++ b/upstream/signing-key.asc
@@ -0,0 +1,51 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mQINBErYV2wBEADRxUMup42dh99ugC5+Yh4scQ5h00Kw5sPxqAPVftGCI7VBLQpE
+31egmF0ksdTnApPW7uTiU05nhQKNmXGChwbCZ5r97dcrO/bKPKm5CraYGhPBcqZ4
+iY7qtndkWb7VhEOHw2y1ALYzBxjs54t8V0zzXMV9QVbYHmD/eSY4/JCw1LO9Tt/I
+NoJgZzfUrreuIMAotVgsIjJ9fkxLlxdqfkPql+mSxjn2x9UqBU2QY5hnL4GwX/EB
+NC4e6uWwj5YMZmSyuajqbRsSWLL6OMCpTDN+JKhsVPu1tiglFnQFS0Wo2A9JvEwk
+jJMcBJeI05+6xy/3IA3cBEUlaKSaWqY27MCzGlvp0n3uaIu7yGxVPZ8OPJC7TOaR
+cQQxhGVsBecbv9RpeLt6wYNB8U1I9IDmPWl+T9g4ocJdz3ckz/Y8wDvV9tNZTtJG
+nPHT3TL+ULBDhzkNMAL7GdRsinJq2vrhh00i2GJAEp7aMgaVNDwfASzdGUgJWXs5
+ylKm5Mx5Bk+wj8fSQR9cL2enosa1POSSPU1jumJaeni50fAXixXbrxhjBvJDYrjo
+Rgs5vWih4Z4wMQ7GWplDjX7OheqBd+jwbW3os+MMPgNU56kCqAtka4fbm2qcvwMn
+H8yMCydKW7wf1hojbTIAzlRW1KC6qGuOImgf9n8kMZ7yn918COUw6galWwARAQAB
+tCFQZXRlciBQZW50Y2hldiA8cm9hbUByaW5nbGV0Lm5ldD6JAjoEEwEKACQCGwMF
+CwkIBwMFFQoJCAsFFgIDAQACHgECF4AFAkrYXb4CGQEACgkQZR7vsCUn3xMAPxAA
+mf1vcF+g58Nc/bGwTFtxWQ+lnjHMX6S0xGw/BX5N7bY/EZte9JdXsBoVREX4zH6+
+GMuza9F9d4swid62TiUmrqBUJ/Ractoh/kogvrTmVax+CNiIRPzm3mOxkI7mSdKy
+dXERl0MMYWXqLjDpt99d05x+9R22YpAocFnbL0Za7cm4z6dWALlrUqBwNLoeKmEN
+IK34uJOF3W+iqNpT9CQv/ahK8Hrpe/5J7qjlJwvJQTXYLtVxAN6L3oH+5JCeXU14
+4nMg1X9u64QXJYmgBDRbBh+x1rnK+VNC8iF087JmUoZCgXU5M4jG2MYxv+JCIuH1
+OUYyO3TSlkgh0JavcJ62/5WNN/BoEYpQMBRKXKewFpImHQXio5jyXa6+4VlY21Xz
+Xdqya/ubtKj/Q3xbjCqaDi0MkjRQhXkUhE/z1DsEIPK+VZtTQ2/4CUgTGYBif5sO
+ghkFtKIMTkXiZ39mI0+/I/6Dqcgzf6pgWqfio9SMYJ22tOM8xUB17vR48s7nORr7
+yYcl0APQsrNwTHZyjc9vPYY/dcLm0nyrjtFm0yZwqKbhZsBrAN6h9Uq3uRBsj363
+fWG3QqCS58EgH5si//shH1zcNuSDkBp/IC0kG84FLA+v0S4Qo8MY6rnvYei/FLeW
+Zp2xCfJ1Z6HwSqUq365tjEjw+R0BYRg4GaSBwIA9QZq5Ag0ESthX1QEQAKmLebh5
+ENoh2+DKupEs1n3u1vusCCJMs6IJ7pO2bcs/TMpcC944ILurEofWHXGtcKWHFgKw
+vQEiMqV5xal5bdwovQwY/i4bx4HV3/WIcdbIGMetojpUQXt4h9/dFY9W5crrW8SL
+0E9XF1Bz3rPbA0tEa3FBIsEE6T0oXpwZ8rGcAE3roE8JIwk0uVC1heCIwJllxtwh
+6isP9Q9qz1EmvbOxiA5T9H0KKgiAo7FGOyZh3ebH9hNlyKYJOOE9zi46dj/fqY/8
+28qcEZcQPcCpJSjqTJf/fuLg+ic4DWfeAklH1NJTjjrjFHZIERHkNvFZ94qfKojZ
+9Ood3fbBkGxN6975oFmi9jYE0dPz77Mna3WrExFocv6LFnuxBuGotcCYAZCTPrfk
+wdl1VvaXNuXov7L+UJvH9fRHnWpGR0d+hkeJHZcyQ/bCBwusyLQO06x9ss7kxLjV
+I5JNOJk3xTZT1oA26DoA8md0r1Lo4F5XSFYDPFIXheT3QrmfibxiZjyzk48mXHWS
+78bT5JEmgmyJ9CrEkFOAuN72gAatwBCKRR0mNe+cAmir00pWJUsd1Yv9+Q4bOEhx
+qhVKaN6RkRtssHGJ6a6uPqKB59GDBy0iDq3kxW9eSl/QVMRuiwj7kwvphPSuoCh3
+Rpb2tjH5jn38yWWZ7l1U722d8W35xKubNuqJABEBAAGJAh8EGAEKAAkFAkrYV9UC
+GwwACgkQZR7vsCUn3xO4FA//Z1t/cgjP6fdiPwJ70yCIpwPww9YfDJprdxyVMivc
+nNw/+yo21L5M4IYadwZp8APhKFfQsQZtW0ibNVPI12+tFyIKE/IEr+/ki+jgUqMz
+uGtlgrgm0+j72/kwcz1LzLTEMeA1w+c2W/CqnzJk6gJswSMqzYZ3U9EmcWkYDehx
+lNVMKjEAkGhtpqtkcB1Fy4PQ7XediVb8T7LYvx+9eJ8i/0qLbbLcFB1r/P5+qWHo
+CPHGxKNtRpbDaQsyoqIz+/sx8XSvwNktHBN5AldlySAPePPqLExw7FVpwTGgfENN
+/dRHJEf7dHflZkE5RHqJMCecJC6BgJhJtjm64uN9Xk7p0gOPhBswryknEXQhP7La
+UL4Uc4+YTTKlspjLCVJ3K9uHleCCJx9LPpe0KROz552jBZ9Wqj/iPcM04xPzmL9t
+Sqo/rguHAK24ET8Z1pNu4brc1VxPL29gupzMSwljF3m1Re8D2dc8RTRYbCKcYviv
+bwTQZw/5DgsxB8qxXEyfmAE+SWQX1hFLXwamVVVtijyeYrbsx4gjAxe7Bun7qOeT
+if7gp1JN4siRLp6zU6nPaU7M4E5cl+WhY5LmHvln/dj2xByniY39aeRNr9Wf/4yh
+hviqJaAYTGnr9xhyI+m0arzGvK3GRfADsTjumJ3pCmoRaQuhao4wKm2HyUT43sqk
+zlA=
+=bKsR
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/watch b/watch
new file mode 100644
index 0000000..dfceaf0
--- /dev/null
+++ b/watch
@@ -0,0 +1,3 @@
+version=4
+opts=pgpsigurlmangle=s/$/.asc/ \
+https://devel.ringlet.net/textproc/confget/ https://devel.ringlet.net/(?:files/)?textproc/confget/confget-@ANY_VERSION@@ARCHIVE_EXT@
-- 
cgit v1.2.3


From ac5f1c4e4d02db08638dd599ffe2669203bae933 Mon Sep 17 00:00:00 2001
From: Peter Pentchev 
Date: Wed, 27 Feb 2019 00:44:26 +0200
Subject: Do not install the Python command-line script.

Forwarded: not-needed
Last-Update: 2019-01-13


Gbp-Pq: Name python-no-executable.patch
---
 python/setup.py | 6 ------
 1 file changed, 6 deletions(-)

diff --git a/python/setup.py b/python/setup.py
index 2d9f620..18c2610 100644
--- a/python/setup.py
+++ b/python/setup.py
@@ -122,11 +122,5 @@ setuptools.setup(
         'Topic :: Utilities',
     ],
 
-    entry_points={
-        'console_scripts': [
-            'confget=confget.__main__:main',
-        ],
-    },
-
     zip_safe=True,
 )
-- 
cgit v1.2.3


From 574911359530da61bf72bd24fd42bff71b2b760c Mon Sep 17 00:00:00 2001
From: Peter Pentchev 
Date: Wed, 27 Feb 2019 00:44:26 +0200
Subject: Skip some of the "too many options" tests on PyPy, too.

Forwarded: yes
Last-Update: 2019-01-13


Gbp-Pq: Name test-too-many-pypy.patch
---
 t/14-bespoke-too-many.t | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/t/14-bespoke-too-many.t b/t/14-bespoke-too-many.t
index 5c1be63..2856b94 100644
--- a/t/14-bespoke-too-many.t
+++ b/t/14-bespoke-too-many.t
@@ -57,7 +57,7 @@ for args in \
     '-q features -q feature BASE' \
     '-q features key1'; do
 	if [ "${args#-q sections -q}" != "$args" ] || [ "${args#-q features -q}" != "$args" ]; then
-		if [ "${CONFGET#*python}" != "$CONFGET" ]; then
+		if [ "${CONFGET#*python}" != "$CONFGET" ] || [ "${CONFGET#*pypy}" != "$CONFGET" ]; then
 			echo "ok $idx $args - skipped, Python argparse"
 			idx="$((idx + 1))"
 			continue
-- 
cgit v1.2.3


From 475c25266aab1a8479dcc8dc2a46d544298e65ae Mon Sep 17 00:00:00 2001
From: Peter Pentchev 
Date: Mon, 9 Sep 2019 16:15:03 +0300
Subject: Do not install the Python command-line script.

Forwarded: not-needed
Last-Update: 2019-09-09


Gbp-Pq: Name python-no-executable.patch
---
 python/setup.py | 1 -
 1 file changed, 1 deletion(-)

diff --git a/python/setup.py b/python/setup.py
index 6c9f436..36d018c 100644
--- a/python/setup.py
+++ b/python/setup.py
@@ -108,6 +108,5 @@ setuptools.setup(
         "Topic :: Software Development :: Libraries :: Python Modules",
         "Topic :: Utilities",
     ],
-    entry_points={"console_scripts": ["confget=confget.__main__:main"]},
     zip_safe=True,
 )
-- 
cgit v1.2.3