1+ package dev .starfix ;
2+
3+ import com .fasterxml .jackson .databind .ObjectMapper ;
4+ import com .fasterxml .jackson .dataformat .yaml .YAMLFactory ;
5+ import picocli .CommandLine ;
6+ import picocli .CommandLine .IDefaultValueProvider ;
7+ import picocli .CommandLine .Model .ArgSpec ;
8+ import picocli .CommandLine .Model .CommandSpec ;
9+ import picocli .CommandLine .Model .OptionSpec ;
10+ import picocli .CommandLine .Model .PositionalParamSpec ;
11+
12+ import java .io .File ;
13+ import java .io .FileReader ;
14+ import java .io .IOException ;
15+ import java .io .Reader ;
16+ import java .util .Properties ;
17+
18+ /**
19+ * {@link IDefaultValueProvider IDefaultValueProvider} implementation that loads default values for command line
20+ * options and positional parameters from a properties file or {@code Properties} object.
21+ * <h2>Location</h2>
22+ * By default, this implementation tries to find a properties file named
23+ * {@code ".<YOURCOMMAND>.properties"} in the user home directory, where {@code "<YOURCOMMAND>"} is the {@linkplain CommandLine.Command#name() name} of the command.
24+ * If a command has {@linkplain CommandLine.Command#aliases() aliases} in addition to its {@linkplain CommandLine.Command#name() name},
25+ * these aliases are also used to try to find the properties file. For example:
26+ * <pre>{@code
27+ * @Command(name = "git", defaultValueProvider = YAMLDefaultProvider.class)
28+ * class Git { }
29+ * }</pre>
30+ * <p>The above will try to load default values from {@code new File(System.getProperty("user.home"), ".git.properties")}.
31+ * </p>
32+ * <p>
33+ * The location of the properties file can also be controlled with system property {@code "picocli.defaults.<YOURCOMMAND>.path"},
34+ * in which case the value of the property must be the path to the file containing the default values.
35+ * </p>
36+ * <p>
37+ * The location of the properties file may also be specified programmatically. For example:
38+ * </p>
39+ * <pre>
40+ * CommandLine cmd = new CommandLine(new MyCommand());
41+ * File defaultsFile = new File("path/to/config/mycommand.properties");
42+ * cmd.setDefaultValueProvider(new YAMLDefaultProvider(defaultsFile));
43+ * cmd.execute(args);
44+ * </pre>
45+ * <h2>Format</h2>
46+ * <p>
47+ * For options, the key is either the {@linkplain CommandLine.Option#descriptionKey() descriptionKey},
48+ * or the option's {@linkplain OptionSpec#longestName() longest name}.
49+ * </p><p>
50+ * For positional parameters, the key is either the
51+ * {@linkplain CommandLine.Parameters#descriptionKey() descriptionKey},
52+ * or the positional parameter's {@linkplain PositionalParamSpec#paramLabel() param label}.
53+ * </p><p>
54+ * End users may not know what the {@code descriptionKey} of your options and positional parameters are, so be sure
55+ * to document that with your application.
56+ * </p>
57+ * <h2>Subcommands</h2>
58+ * <p>
59+ * The default values for options and positional parameters of subcommands can be included in the
60+ * properties file for the top-level command, so that end users need to maintain only a single file.
61+ * This can be achieved by prefixing the key with the command's qualified name.
62+ * For example, to give the {@code `git commit`} command's {@code --cleanup} option a
63+ * default value of {@code strip}, define a key of {@code git.commit.cleanup} and assign
64+ * it a default value.
65+ * </p><pre>
66+ * # /home/remko/.git.properties
67+ * git.commit.cleanup = strip
68+ * </pre>
69+ */
70+ public class YAMLDefaultProvider implements IDefaultValueProvider {
71+
72+ private Properties properties ;
73+
74+ /**
75+ * Default constructor, used when this default value provider is specified in
76+ * the annotations:
77+ * <pre>
78+ * {@code
79+ * @Command(name = "mycmd",
80+ * defaultValueProvider = YAMLDefaultProvider.class)
81+ * class MyCommand // ...
82+ * }
83+ * </pre>
84+ * <p>
85+ * This loads default values from a properties file named
86+ * {@code ".mycmd.properties"} in the user home directory.
87+ * </p><p>
88+ * The location of the properties file can also be controlled with system property {@code "picocli.defaults.<YOURCOMMAND>.path"},
89+ * in which case the value of the property must be the path to the file containing the default values.
90+ * </p>
91+ * @see YAMLDefaultProvider the YAMLDefaultProvider class description
92+ */
93+ public YAMLDefaultProvider () {}
94+
95+ /**
96+ * This constructor loads default values from the specified properties object.
97+ * This may be used programmatically. For example:
98+ * <pre>
99+ * CommandLine cmd = new CommandLine(new MyCommand());
100+ * Properties defaults = getProperties();
101+ * cmd.setDefaultValueProvider(new YAMLDefaultProvider(defaults));
102+ * cmd.execute(args);
103+ * </pre>
104+ * @param properties the properties containing the default values
105+ * @see YAMLDefaultProvider the YAMLDefaultProvider class description
106+ */
107+ public YAMLDefaultProvider (Properties properties ) {
108+ this .properties = properties ;
109+ }
110+
111+ /**
112+ * This constructor loads default values from the specified properties file.
113+ * This may be used programmatically. For example:
114+ * <pre>
115+ * CommandLine cmd = new CommandLine(new MyCommand());
116+ * File defaultsFile = new File("path/to/config/file.properties");
117+ * cmd.setDefaultValueProvider(new YAMLDefaultProvider(defaultsFile));
118+ * cmd.execute(args);
119+ * </pre>
120+ * @param file the file to load default values from. Must be non-{@code null} and
121+ * must contain default values in the standard java {@link Properties} format.
122+ * @see YAMLDefaultProvider the YAMLDefaultProvider class description
123+ */
124+ public YAMLDefaultProvider (File file ) {
125+ this (createProperties (file ));
126+ }
127+
128+ private static Properties createProperties (File file ) {
129+ if (file == null ) {
130+ throw new NullPointerException ("file is null" );
131+ }
132+
133+ Properties result = new Properties ();
134+ if (file .exists () && file .canRead ()) {
135+
136+ try {
137+ ObjectMapper mapper = new ObjectMapper (new YAMLFactory ());
138+ Properties config = mapper .readValue (file , Properties .class );
139+ result .putAll (config );
140+ } catch (IOException ioe ) {
141+ System .err .println ("WARN - could not read defaults from " + file .getAbsolutePath () + ": " + ioe );
142+ }
143+ } else {
144+ System .err .println ("WARN - defaults configuration file " + file .getAbsolutePath () + " does not exist or is not readable" );
145+ }
146+ return result ;
147+ }
148+
149+ private static Properties loadProperties (CommandSpec commandSpec ) {
150+ if (commandSpec == null ) { return null ; }
151+ Properties p = System .getProperties ();
152+ for (String name : commandSpec .names ()) {
153+ String path = p .getProperty ("picocli.defaults." + name + ".path" );
154+ File defaultPath = new File (p .getProperty ("user.home" ) + File .separator + "/.config" , name + ".yaml" );
155+ File file = path == null ? defaultPath : new File (path );
156+ if (file .canRead ()) {
157+ return createProperties (file );
158+ }
159+ }
160+ return loadProperties (commandSpec .parent ());
161+ }
162+
163+ @ Override
164+ public String defaultValue (ArgSpec argSpec ) throws Exception {
165+ if (properties == null ) {
166+ properties = loadProperties (argSpec .command ());
167+ }
168+ if (properties == null || properties .isEmpty ()) {
169+ return null ;
170+ }
171+ return argSpec .isOption ()
172+ ? optionDefaultValue ((OptionSpec ) argSpec )
173+ : positionalDefaultValue ((PositionalParamSpec ) argSpec );
174+ }
175+
176+ private String optionDefaultValue (OptionSpec option ) {
177+ String result = getValue (option .descriptionKey (), option .command ());
178+ result = result != null ? result : getValue (stripPrefix (option .longestName ()), option .command ());
179+ return result ;
180+ }
181+ private static String stripPrefix (String prefixed ) {
182+ for (int i = 0 ; i < prefixed .length (); i ++) {
183+ if (Character .isJavaIdentifierPart (prefixed .charAt (i ))) {
184+ return prefixed .substring (i );
185+ }
186+ }
187+ return prefixed ;
188+ }
189+
190+ private String positionalDefaultValue (PositionalParamSpec positional ) {
191+ String result = getValue (positional .descriptionKey (), positional .command ());
192+ result = result != null ? result : getValue (positional .paramLabel (), positional .command ());
193+ return result ;
194+ }
195+
196+ private String getValue (String key , CommandSpec spec ) {
197+ String result = null ;
198+ if (spec != null ) {
199+ String cmd = spec .qualifiedName ("." );
200+ result = properties .getProperty (cmd + "." + key );
201+ }
202+ return result != null ? result : properties .getProperty (key );
203+ }
204+ }
0 commit comments