View Javadoc
1   /*
2    * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> and others
3    *
4    * This program and the accompanying materials are made available under the
5    * terms of the Eclipse Distribution License v. 1.0 which is available at
6    * https://www.eclipse.org/org/documents/edl-v10.php.
7    *
8    * SPDX-License-Identifier: BSD-3-Clause
9    */
10  
11  package org.eclipse.jgit.pgm.opt;
12  
13  import java.io.IOException;
14  import java.io.Writer;
15  import java.lang.reflect.Field;
16  import java.util.ArrayList;
17  import java.util.Iterator;
18  import java.util.List;
19  import java.util.ResourceBundle;
20  
21  import org.eclipse.jgit.lib.ObjectId;
22  import org.eclipse.jgit.lib.Repository;
23  import org.eclipse.jgit.pgm.Die;
24  import org.eclipse.jgit.pgm.TextBuiltin;
25  import org.eclipse.jgit.pgm.internal.CLIText;
26  import org.eclipse.jgit.revwalk.RevCommit;
27  import org.eclipse.jgit.revwalk.RevTree;
28  import org.eclipse.jgit.revwalk.RevWalk;
29  import org.eclipse.jgit.transport.RefSpec;
30  import org.eclipse.jgit.treewalk.AbstractTreeIterator;
31  import org.kohsuke.args4j.CmdLineException;
32  import org.kohsuke.args4j.IllegalAnnotationError;
33  import org.kohsuke.args4j.NamedOptionDef;
34  import org.kohsuke.args4j.OptionDef;
35  import org.kohsuke.args4j.OptionHandlerRegistry;
36  import org.kohsuke.args4j.spi.OptionHandler;
37  import org.kohsuke.args4j.spi.RestOfArgumentsHandler;
38  import org.kohsuke.args4j.spi.Setter;
39  
40  /**
41   * Extended command line parser which handles --foo=value arguments.
42   * <p>
43   * The args4j package does not natively handle --foo=value and instead prefers
44   * to see --foo value on the command line. Many users are used to the GNU style
45   * --foo=value long option, so we convert from the GNU style format to the
46   * args4j style format prior to invoking args4j for parsing.
47   */
48  public class CmdLineParser extends org.kohsuke.args4j.CmdLineParser {
49  	static {
50  		OptionHandlerRegistry registry = OptionHandlerRegistry.getRegistry();
51  		registry.registerHandler(AbstractTreeIterator.class,
52  				AbstractTreeIteratorHandler.class);
53  		registry.registerHandler(ObjectId.class, ObjectIdHandler.class);
54  		registry.registerHandler(RefSpec.class, RefSpecHandler.class);
55  		registry.registerHandler(RevCommit.class, RevCommitHandler.class);
56  		registry.registerHandler(RevTree.class, RevTreeHandler.class);
57  		registry.registerHandler(List.class, OptionWithValuesListHandler.class);
58  	}
59  
60  	private final Repository db;
61  
62  	private RevWalk walk;
63  
64  	private boolean seenHelp;
65  
66  	private TextBuiltin cmd;
67  
68  	/**
69  	 * Creates a new command line owner that parses arguments/options and set
70  	 * them into the given object.
71  	 *
72  	 * @param bean
73  	 *            instance of a class annotated by
74  	 *            {@link org.kohsuke.args4j.Option} and
75  	 *            {@link org.kohsuke.args4j.Argument}. this object will receive
76  	 *            values.
77  	 * @throws IllegalAnnotationError
78  	 *             if the option bean class is using args4j annotations
79  	 *             incorrectly.
80  	 */
81  	public CmdLineParser(Object bean) {
82  		this(bean, null);
83  	}
84  
85  	/**
86  	 * Creates a new command line owner that parses arguments/options and set
87  	 * them into the given object.
88  	 *
89  	 * @param bean
90  	 *            instance of a class annotated by
91  	 *            {@link org.kohsuke.args4j.Option} and
92  	 *            {@link org.kohsuke.args4j.Argument}. this object will receive
93  	 *            values.
94  	 * @param repo
95  	 *            repository this parser can translate options through.
96  	 * @throws IllegalAnnotationError
97  	 *             if the option bean class is using args4j annotations
98  	 *             incorrectly.
99  	 */
100 	public CmdLineParser(Object bean, Repository repo) {
101 		super(bean);
102 		if (bean instanceof TextBuiltin) {
103 			cmd = (TextBuiltin) bean;
104 		}
105 		if (repo == null && cmd != null) {
106 			repo = cmd.getRepository();
107 		}
108 		this.db = repo;
109 	}
110 
111 	/** {@inheritDoc} */
112 	@Override
113 	public void parseArgument(String... args) throws CmdLineException {
114 		final ArrayList<String> tmp = new ArrayList<>(args.length);
115 		for (int argi = 0; argi < args.length; argi++) {
116 			final String str = args[argi];
117 			if (str.equals("--")) { //$NON-NLS-1$
118 				while (argi < args.length)
119 					tmp.add(args[argi++]);
120 				break;
121 			}
122 
123 			if (str.startsWith("--")) { //$NON-NLS-1$
124 				final int eq = str.indexOf('=');
125 				if (eq > 0) {
126 					tmp.add(str.substring(0, eq));
127 					tmp.add(str.substring(eq + 1));
128 					continue;
129 				}
130 			}
131 
132 			tmp.add(str);
133 
134 			if (containsHelp(args)) {
135 				// suppress exceptions on required parameters if help is present
136 				seenHelp = true;
137 				// stop argument parsing here
138 				break;
139 			}
140 		}
141 		List<OptionHandler> backup = null;
142 		if (seenHelp) {
143 			backup = unsetRequiredOptions();
144 		}
145 
146 		try {
147 			super.parseArgument(tmp.toArray(new String[0]));
148 		} catch (Die e) {
149 			if (!seenHelp) {
150 				throw e;
151 			}
152 			printToErrorWriter(CLIText.fatalError(e.getMessage()));
153 		} finally {
154 			// reset "required" options to defaults for correct command printout
155 			if (backup != null && !backup.isEmpty()) {
156 				restoreRequiredOptions(backup);
157 			}
158 			seenHelp = false;
159 		}
160 	}
161 
162 	private void printToErrorWriter(String error) {
163 		if (cmd == null) {
164 			System.err.println(error);
165 		} else {
166 			try {
167 				cmd.getErrorWriter().println(error);
168 			} catch (IOException e1) {
169 				System.err.println(error);
170 			}
171 		}
172 	}
173 
174 	private List<OptionHandler> unsetRequiredOptions() {
175 		List<OptionHandler> options = getOptions();
176 		List<OptionHandler> backup = new ArrayList<>(options);
177 		for (Iterator<OptionHandler> iterator = options.iterator(); iterator
178 				.hasNext();) {
179 			OptionHandler handler = iterator.next();
180 			if (handler.option instanceof NamedOptionDef
181 					&& handler.option.required()) {
182 				iterator.remove();
183 			}
184 		}
185 		return backup;
186 	}
187 
188 	private void restoreRequiredOptions(List<OptionHandler> backup) {
189 		List<OptionHandler> options = getOptions();
190 		options.clear();
191 		options.addAll(backup);
192 	}
193 
194 	/**
195 	 * Check if array contains help option
196 	 *
197 	 * @param args
198 	 *            non null
199 	 * @return true if the given array contains help option
200 	 * @since 4.2
201 	 */
202 	protected boolean containsHelp(String... args) {
203 		return TextBuiltin.containsHelp(args);
204 	}
205 
206 	/**
207 	 * Get the repository this parser translates values through.
208 	 *
209 	 * @return the repository, if specified during construction.
210 	 */
211 	public Repository getRepository() {
212 		if (db == null)
213 			throw new IllegalStateException(CLIText.get().noGitRepositoryConfigured);
214 		return db;
215 	}
216 
217 	/**
218 	 * Get the revision walker used to support option parsing.
219 	 *
220 	 * @return the revision walk used by this option parser.
221 	 */
222 	public RevWalk getRevWalk() {
223 		if (walk == null)
224 			walk = new RevWalk(getRepository());
225 		return walk;
226 	}
227 
228 	/**
229 	 * Get the revision walker used to support option parsing.
230 	 * <p>
231 	 * This method does not initialize the RevWalk and may return null.
232 	 *
233 	 * @return the revision walk used by this option parser, or null.
234 	 */
235 	public RevWalk getRevWalkGently() {
236 		return walk;
237 	}
238 
239 	class MyOptionDef extends OptionDef {
240 
241 		public MyOptionDef(OptionDef o) {
242 			super(o.usage(), o.metaVar(), o.required(), o.help(), o.hidden(),
243 					o.handler(), o.isMultiValued());
244 		}
245 
246 		@Override
247 		public String toString() {
248 			if (metaVar() == null)
249 				return "ARG"; //$NON-NLS-1$
250 			try {
251 				Field field = CLIText.class.getField(metaVar());
252 				String ret = field.get(CLIText.get()).toString();
253 				return ret;
254 			} catch (Exception e) {
255 				e.printStackTrace(System.err);
256 				return metaVar();
257 			}
258 		}
259 
260 		@Override
261 		public boolean required() {
262 			return seenHelp ? false : super.required();
263 		}
264 	}
265 
266 	/** {@inheritDoc} */
267 	@Override
268 	protected OptionHandler createOptionHandler(OptionDef o, Setter setter) {
269 		if (o instanceof NamedOptionDef) {
270 			return super.createOptionHandler(o, setter);
271 		}
272 		return super.createOptionHandler(new MyOptionDef(o), setter);
273 
274 	}
275 
276 	/** {@inheritDoc} */
277 	@Override
278 	public void printSingleLineUsage(Writer w, ResourceBundle rb) {
279 		List<OptionHandler> options = getOptions();
280 		if (options.isEmpty()) {
281 			super.printSingleLineUsage(w, rb);
282 			return;
283 		}
284 		List<OptionHandler> backup = new ArrayList<>(options);
285 		boolean changed = sortRestOfArgumentsHandlerToTheEnd(options);
286 		try {
287 			super.printSingleLineUsage(w, rb);
288 		} finally {
289 			if (changed) {
290 				options.clear();
291 				options.addAll(backup);
292 			}
293 		}
294 	}
295 
296 	private boolean sortRestOfArgumentsHandlerToTheEnd(
297 			List<OptionHandler> options) {
298 		for (int i = 0; i < options.size(); i++) {
299 			OptionHandler handler = options.get(i);
300 			if (handler instanceof RestOfArgumentsHandler
301 					|| handler instanceof PathTreeFilterHandler) {
302 				options.remove(i);
303 				options.add(handler);
304 				return true;
305 			}
306 		}
307 		return false;
308 	}
309 }