Coverage for src / dotbot / cli.py: 77%
87 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-29 10:55 -0800
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-29 10:55 -0800
1import os
2import subprocess
3import sys
4from argparse import SUPPRESS, ArgumentParser, RawTextHelpFormatter
5from typing import Any, List
7import dotbot
8from dotbot.config import ConfigReader, ReadingError
9from dotbot.dispatcher import Dispatcher, DispatchError
10from dotbot.messenger import Level, Messenger
11from dotbot.plugins import Clean, Create, Link, Shell
12from dotbot.util import module
15def add_options(parser: ArgumentParser) -> None:
16 parser.add_argument("-Q", "--super-quiet", action="store_true", help=SUPPRESS) # deprecated
17 parser.add_argument("-q", "--quiet", action="store_true", help="suppress most output")
18 parser.add_argument(
19 "-v",
20 "--verbose",
21 action="count",
22 default=0,
23 help="enable verbose output\n"
24 "-v: show informational messages\n"
25 "-vv: also, set shell commands stderr/stdout to true",
26 )
27 parser.add_argument("-d", "--base-directory", help="execute commands from within BASE_DIR", metavar="BASE_DIR")
28 parser.add_argument(
29 "-c", "--config-file", help="run commands given in CONFIG_FILE", metavar="CONFIG_FILE", nargs="+"
30 )
31 parser.add_argument(
32 "-p",
33 "--plugin",
34 action="append",
35 dest="plugins",
36 default=[],
37 help="load PLUGIN as a plugin",
38 metavar="PLUGIN",
39 )
40 parser.add_argument("--disable-built-in-plugins", action="store_true", help="disable built-in plugins")
41 parser.add_argument(
42 "--plugin-dir",
43 action="append",
44 dest="plugin_dirs",
45 default=[],
46 metavar="PLUGIN_DIR",
47 help=SUPPRESS, # deprecated
48 )
49 parser.add_argument("--only", nargs="+", help="only run specified directives", metavar="DIRECTIVE")
50 parser.add_argument("--except", nargs="+", dest="skip", help="skip specified directives", metavar="DIRECTIVE")
51 parser.add_argument("-n", "--dry-run", action="store_true", help="print what would be done, without doing it")
52 parser.add_argument("--force-color", dest="force_color", action="store_true", help="force color output")
53 parser.add_argument("--no-color", dest="no_color", action="store_true", help="disable color output")
54 parser.add_argument("--version", action="store_true", help="show program's version number and exit")
55 parser.add_argument(
56 "-x",
57 "--exit-on-failure",
58 dest="exit_on_failure",
59 action="store_true",
60 help="exit after first failed directive",
61 )
64def read_config(config_files: List[str]) -> Any:
65 reader = ConfigReader(config_files)
66 return reader.get_config()
69def main() -> None:
70 log = Messenger()
71 try:
72 parser = ArgumentParser(formatter_class=RawTextHelpFormatter)
73 add_options(parser)
74 options = parser.parse_args()
75 if options.version:
76 try:
77 with open(os.devnull) as devnull:
78 git_hash = subprocess.check_output(
79 ["git", "rev-parse", "HEAD"], # noqa: S607
80 cwd=os.path.dirname(os.path.abspath(__file__)),
81 stderr=devnull,
82 ).decode("ascii")
83 hash_msg = f" (git {git_hash[:10]})"
84 except (OSError, subprocess.CalledProcessError):
85 hash_msg = ""
86 print(f"Dotbot version {dotbot.__version__}{hash_msg}") # noqa: T201
87 sys.exit(0)
88 if options.super_quiet or options.quiet:
89 log.set_level(Level.WARNING)
90 if options.verbose > 0:
91 log.set_level(Level.INFO if options.verbose == 1 else Level.DEBUG)
93 if options.force_color and options.no_color:
94 log.error("`--force-color` and `--no-color` cannot both be provided")
95 sys.exit(1)
96 elif options.force_color:
97 log.use_color(True)
98 elif options.no_color:
99 log.use_color(False)
100 else:
101 log.use_color(sys.stdout.isatty())
103 plugins = []
104 if not options.disable_built_in_plugins:
105 plugins.extend([Clean, Create, Link, Shell])
106 module.load_plugins(options.plugin_dirs, plugins) # note, plugin_dirs is deprecated
107 module.load_plugins(options.plugins, plugins)
109 if not options.config_file:
110 log.error("No configuration file specified")
111 sys.exit(1)
112 tasks = read_config(options.config_file)
113 if not tasks:
114 log.warning("No tasks given in configuration, no work to do")
115 if options.base_directory:
116 base_directory = os.path.abspath(options.base_directory)
117 else:
118 # default to directory of first config file
119 base_directory = os.path.dirname(os.path.abspath(options.config_file[0]))
120 os.chdir(base_directory)
121 dotbot.dispatcher._all_plugins = plugins # for backwards compatibility, see dispatcher.py # noqa: SLF001
122 dispatcher = Dispatcher(
123 base_directory,
124 only=options.only,
125 skip=options.skip,
126 exit_on_failure=options.exit_on_failure,
127 options=options,
128 plugins=plugins,
129 )
130 success = dispatcher.dispatch(tasks)
131 if success:
132 log.info("All tasks executed successfully")
133 else:
134 msg = "Some tasks were not executed successfully"
135 raise DispatchError(msg) # noqa: TRY301
136 except (ReadingError, DispatchError) as e:
137 log.error(str(e)) # noqa: TRY400
138 sys.exit(1)
139 except KeyboardInterrupt:
140 log.error("Operation aborted") # noqa: TRY400
141 sys.exit(1)
144if __name__ == "__main__":
145 main()