Skip to content

Instantly share code, notes, and snippets.

@jpramosi
Created October 27, 2022 17:00
Show Gist options
  • Save jpramosi/7f81e9dbe3fe54f3645ca35d2192a07d to your computer and use it in GitHub Desktop.
Save jpramosi/7f81e9dbe3fe54f3645ca35d2192a07d to your computer and use it in GitHub Desktop.
Helper utility for shell scripts to read or modify configuration files like .ini, .toml or .conf
#!/bin/python3
"""
Config-Util - Helper utility for shell scripts to read or modify
configuration files like .ini, .toml or .conf
Copyright (C) 2022 Jimmy Pramosi <[email protected]>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import io
import sys
from argparse import ArgumentParser, RawTextHelpFormatter
from configparser import ConfigParser, _UNSET
from pathlib import Path
DESCRIPTION = """
Config-Util Copyright (C) 2022 Jimmy Pramosi <[email protected]>
This program comes with ABSOLUTELY NO WARRANTY.
This is free software, and you are welcome to redistribute it under certain
conditions.
You should have received a copy of the GNU General Public License along with
this program. If not, see <http://www.gnu.org/licenses/>.
"""
USAGE = """
Get operations:
cfg [OPTIONS] <FILE> <VAR-NAME>
cfg [OPTIONS] <FILE> <[SECTION-NAME]> <VAR-NAME>
Set operations:
cfg [OPTIONS] <FILE> <VAR-NAME> <NEW-VALUE>
cfg [OPTIONS] <FILE> <[SECTION-NAME]> <VAR-NAME> <NEW-VALUE>
"""
EPILOG = """
Examples:
get variable 'timeout_secs' without section
cfg vpn.conf timeout_secs
set variable 'timeout_secs' without section
cfg vpn.conf timeout_secs myprofile.ovpn
get variable 'tun-device' with section
cfg vpn.conf [OpenVpn] tun-device
set variable 'tun-device' with section
cfg vpn.conf [OpenVpn] tun-device tun0
get variable 'select_profile' with nested section
cfg vpn.conf [OpenVpn.profile] select_profile
set variable 'select_profile' with nested section
cfg vpn.conf [OpenVpn.profile] select_profile myprofile.ovpn\n
"""
def eprint(*args, **kwargs):
print(*args, file=sys.stderr, **kwargs)
def main():
commands = set()
def add_argument(parser: ArgumentParser, *args, **kwargs):
nonlocal commands
commands.add(args[0])
commands.add(args[1])
parser.add_argument(*args, **kwargs)
# parse arguments
parser = ArgumentParser(prog="cfg", description=DESCRIPTION,
usage=USAGE, epilog=EPILOG, formatter_class=RawTextHelpFormatter)
add_argument(parser, "-d", "--dump", action="store_true",
help="dump variable and value to stdout")
add_argument(parser, "-q", "--quote", action="store_true",
help="wrap values with quotes if contains whitespace")
add_argument(parser, "-a", "--auto-format", action="store_true",
help="format values automatically (quotes)")
add_argument(parser, "-n", "--new", action="store_true",
help="allow creating new variables with set operation")
add_argument(parser, "-p", "--print", action="store_true",
help="print file content after successful operation")
args, _ = parser.parse_known_args()
# clean and check args
sys.argv[:] = [x for x in sys.argv if x not in commands]
argn = len(sys.argv)
if argn < 3:
eprint(f"Invalid arguments:\n\n\t{' '.join(sys.argv)}\n\n")
parser.print_help()
return 1
filepath = Path(sys.argv[1]).resolve()
def write_string(config: ConfigParser):
output = ""
with io.StringIO() as ss:
config.write(ss)
ss.seek(0)
output = ss.read().replace("[_]\n", "")
return output
def format_value(value, force_quote=False):
if isinstance(value, str) and args.quote or force_quote or (args.auto_format and " " in value):
value = value.replace("\"", "\\\"")
value = f"\"{value}\""
return value
def dump_value(config: ConfigParser, name: str, value):
if args.dump:
print(f"{name}={value}")
elif args.print:
print(write_config(config))
else:
print(value)
def read_config():
with open(filepath, "rt", encoding="utf-8") as f:
config_string = "[_]\n" + f.read()
config = ConfigParser(comment_prefixes="/", allow_no_value=True)
config.read_string(config_string)
return config
def write_config(config: ConfigParser):
output = write_string(config)
with open(filepath, "wt", encoding="utf-8") as f:
f.write(output)
# get value without section
if argn == 3:
try:
config = read_config()
value = format_value(config.get("_", sys.argv[2]))
dump_value(config, sys.argv[2], value)
except Exception as ex:
eprint(ex)
return 1
return
# get value with section
if argn == 4 and sys.argv[2].startswith("[") and sys.argv[2].endswith("]"):
try:
config = read_config()
section = sys.argv[2].removeprefix("[").removesuffix("]")
value = format_value(config.get(
section, sys.argv[3]))
dump_value(config, sys.argv[2], value)
except Exception as ex:
eprint(ex)
return 1
return
# set value without section
if argn == 4:
try:
config = read_config()
fallback = type(sys.argv[3])() if args.new else _UNSET
rvalue = format_value(config.get(
"_", sys.argv[2], fallback=fallback))
value = format_value(
sys.argv[3], isinstance(rvalue, str) and rvalue.startswith("\""))
config.set("_", sys.argv[2], value)
write_config(config)
dump_value(config, sys.argv[2], value)
except Exception as ex:
eprint(ex)
return 1
return 0
# set value with section
if argn == 5:
try:
config = read_config()
section = sys.argv[2].removeprefix("[").removesuffix("]")
fallback = type(sys.argv[4])() if args.new else _UNSET
rvalue = format_value(config.get(
section, sys.argv[3], fallback=fallback))
value = format_value(
sys.argv[4], isinstance(rvalue, str) and rvalue.startswith("\""))
config.set(section,
sys.argv[3], value)
write_config(config)
dump_value(config, sys.argv[3], value)
except Exception as ex:
eprint(ex)
return 1
return 0
eprint(f"Too many arguments:\n\n\t{' '.join(sys.argv)}\n\n")
parser.print_help()
return 1
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment