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

1import os 

2import subprocess 

3import sys 

4from argparse import SUPPRESS, ArgumentParser, RawTextHelpFormatter 

5from typing import Any, List 

6 

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 

13 

14 

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 ) 

62 

63 

64def read_config(config_files: List[str]) -> Any: 

65 reader = ConfigReader(config_files) 

66 return reader.get_config() 

67 

68 

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) 

92 

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()) 

102 

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) 

108 

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) 

142 

143 

144if __name__ == "__main__": 

145 main()