1 /* 2 * CDDL HEADER START 3 * 4 * The contents of this file are subject to the terms of the 5 * Common Development and Distribution License (the "License"). 6 * You may not use this file except in compliance with the License. 7 * 8 * See LICENSE.txt included in this distribution for the specific 9 * language governing permissions and limitations under the License. 10 * 11 * When distributing Covered Code, include this CDDL HEADER in each 12 * file and include the License file at LICENSE.txt. 13 * If applicable, add the following below this CDDL HEADER, with the 14 * fields enclosed by brackets "[]" replaced with your own identifying 15 * information: Portions Copyright [yyyy] [name of copyright owner] 16 * 17 * CDDL HEADER END 18 */ 19 20 /* 21 * Copyright (c) 2005, 2022, Oracle and/or its affiliates. All rights reserved. 22 * Portions Copyright (c) 2011, Jens Elkner. 23 * Portions Copyright (c) 2017, 2020, Chris Fraire <cfraire@me.com>. 24 */ 25 package org.opengrok.indexer.index; 26 27 import java.io.BufferedReader; 28 import java.io.File; 29 import java.io.FileInputStream; 30 import java.io.IOException; 31 import java.io.InputStreamReader; 32 import java.io.PrintStream; 33 import java.io.UncheckedIOException; 34 import java.lang.reflect.Field; 35 import java.lang.reflect.InvocationTargetException; 36 import java.net.URI; 37 import java.net.URISyntaxException; 38 import java.nio.file.Files; 39 import java.nio.file.Path; 40 import java.nio.file.Paths; 41 import java.text.ParseException; 42 import java.util.ArrayList; 43 import java.util.Arrays; 44 import java.util.Collection; 45 import java.util.Collections; 46 import java.util.HashMap; 47 import java.util.HashSet; 48 import java.util.List; 49 import java.util.Locale; 50 import java.util.Map; 51 import java.util.Map.Entry; 52 import java.util.Scanner; 53 import java.util.Set; 54 import java.util.TreeSet; 55 import java.util.concurrent.CountDownLatch; 56 import java.util.logging.Level; 57 import java.util.logging.Logger; 58 import java.util.stream.Collectors; 59 60 import org.apache.commons.lang3.SystemUtils; 61 import org.opengrok.indexer.Info; 62 import org.opengrok.indexer.Metrics; 63 import org.opengrok.indexer.analysis.AnalyzerGuru; 64 import org.opengrok.indexer.analysis.AnalyzerGuruHelp; 65 import org.opengrok.indexer.analysis.Ctags; 66 import org.opengrok.indexer.configuration.CanonicalRootValidator; 67 import org.opengrok.indexer.configuration.CommandTimeoutType; 68 import org.opengrok.indexer.configuration.Configuration; 69 import org.opengrok.indexer.configuration.ConfigurationHelp; 70 import org.opengrok.indexer.configuration.LuceneLockName; 71 import org.opengrok.indexer.configuration.Project; 72 import org.opengrok.indexer.configuration.RuntimeEnvironment; 73 import org.opengrok.indexer.history.HistoryGuru; 74 import org.opengrok.indexer.history.RepositoriesHelp; 75 import org.opengrok.indexer.history.Repository; 76 import org.opengrok.indexer.history.RepositoryFactory; 77 import org.opengrok.indexer.history.RepositoryInfo; 78 import org.opengrok.indexer.logger.LoggerFactory; 79 import org.opengrok.indexer.logger.LoggerUtil; 80 import org.opengrok.indexer.util.CtagsUtil; 81 import org.opengrok.indexer.util.Executor; 82 import org.opengrok.indexer.util.HostUtil; 83 import org.opengrok.indexer.util.OptionParser; 84 import org.opengrok.indexer.util.Statistics; 85 86 /** 87 * Creates and updates an inverted source index as well as generates Xref, file 88 * stats etc., if specified in the options. 89 * 90 * We shall use / as path delimiter in whole opengrok for uuids and paths 91 * from Windows systems, the path shall be converted when entering the index or web 92 * and converted back if needed* to access original file 93 * 94 * *Windows already supports opening /var/opengrok as C:\var\opengrok 95 */ 96 @SuppressWarnings({"PMD.AvoidPrintStackTrace", "PMD.SystemPrintln"}) 97 public final class Indexer { 98 99 private static final Logger LOGGER = LoggerFactory.getLogger(Indexer.class); 100 101 /* tunables for -r (history for remote repositories) */ 102 private static final String ON = "on"; 103 private static final String OFF = "off"; 104 private static final String DIRBASED = "dirbased"; 105 private static final String UIONLY = "uionly"; 106 107 //whole app uses this separator 108 public static final char PATH_SEPARATOR = '/'; 109 public static final String PATH_SEPARATOR_STRING = Character.toString(PATH_SEPARATOR); 110 111 private static final String HELP_OPT_1 = "--help"; 112 private static final String HELP_OPT_2 = "-?"; 113 private static final String HELP_OPT_3 = "-h"; 114 115 private static final Indexer indexer = new Indexer(); 116 private static Configuration cfg = null; 117 private static boolean checkIndex = false; 118 private static boolean runIndex = true; 119 private static boolean optimizedChanged = false; 120 private static boolean addProjects = false; 121 private static boolean searchRepositories = false; 122 private static boolean bareConfig = false; 123 private static boolean awaitProfiler; 124 125 private static boolean help; 126 private static String helpUsage; 127 private static HelpMode helpMode = HelpMode.DEFAULT; 128 129 private static String configFilename = null; 130 private static int status = 0; 131 132 private static final Set<String> repositories = new HashSet<>(); 133 private static Set<String> searchPaths = new HashSet<>(); 134 private static final HashSet<String> allowedSymlinks = new HashSet<>(); 135 private static final HashSet<String> canonicalRoots = new HashSet<>(); 136 private static final Set<String> defaultProjects = new TreeSet<>(); 137 private static final HashSet<String> disabledRepositories = new HashSet<>(); 138 private static RuntimeEnvironment env = null; 139 private static String webappURI = null; 140 141 private static OptionParser optParser = null; 142 private static boolean verbose = false; 143 144 private static final String[] ON_OFF = {ON, OFF}; 145 private static final String[] REMOTE_REPO_CHOICES = {ON, OFF, DIRBASED, UIONLY}; 146 private static final String[] LUCENE_LOCKS = {ON, OFF, "simple", "native"}; 147 private static final String OPENGROK_JAR = "opengrok.jar"; 148 149 private static final int WEBAPP_CONNECT_TIMEOUT = 1000; // in milliseconds 150 getInstance()151 public static Indexer getInstance() { 152 return indexer; 153 } 154 155 /** 156 * Program entry point. 157 * 158 * @param argv argument vector 159 */ 160 @SuppressWarnings("PMD.UseStringBufferForStringAppends") main(String[] argv)161 public static void main(String[] argv) { 162 Statistics stats = new Statistics(); //this won't count JVM creation though 163 boolean update = true; 164 165 Executor.registerErrorHandler(); 166 List<String> subFiles = RuntimeEnvironment.getInstance().getSubFiles(); 167 Set<String> subFilesArgs = new HashSet<>(); 168 169 boolean createDict = false; 170 171 try { 172 argv = parseOptions(argv); 173 174 if (webappURI != null && !HostUtil.isReachable(webappURI, WEBAPP_CONNECT_TIMEOUT)) { 175 System.err.println(webappURI + " is not reachable."); 176 System.exit(1); 177 } 178 179 /* 180 * Attend to disabledRepositories here in case exitWithHelp() will 181 * need to report about repos. 182 */ 183 disabledRepositories.addAll(cfg.getDisabledRepositories()); 184 cfg.setDisabledRepositories(disabledRepositories); 185 for (String repoName : disabledRepositories) { 186 LOGGER.log(Level.FINEST, "Disabled {0}", repoName); 187 } 188 189 if (help) { 190 exitWithHelp(); 191 } 192 193 checkConfiguration(); 194 195 if (awaitProfiler) { 196 pauseToAwaitProfiler(); 197 } 198 199 env = RuntimeEnvironment.getInstance(); 200 env.setIndexer(true); 201 202 // Complete the configuration of repository types. 203 List<Class<? extends Repository>> repositoryClasses = RepositoryFactory.getRepositoryClasses(); 204 for (Class<? extends Repository> clazz : repositoryClasses) { 205 // Set external repository binaries from System properties. 206 try { 207 Field f = clazz.getDeclaredField("CMD_PROPERTY_KEY"); 208 Object key = f.get(null); 209 if (key != null) { 210 cfg.setRepoCmd(clazz.getCanonicalName(), 211 System.getProperty(key.toString())); 212 } 213 } catch (Exception e) { 214 // don't care 215 } 216 } 217 218 // Logging starts here. 219 if (verbose) { 220 String fn = LoggerUtil.getFileHandlerPattern(); 221 if (fn != null) { 222 System.out.println("Logging filehandler pattern: " + fn); 223 } 224 } 225 226 // automatically allow symlinks that are directly in source root 227 File sourceRootFile = new File(cfg.getSourceRoot()); 228 File[] projectDirs = sourceRootFile.listFiles(); 229 if (projectDirs != null) { 230 for (File projectDir : projectDirs) { 231 if (!projectDir.getCanonicalPath().equals(projectDir.getAbsolutePath())) { 232 allowedSymlinks.add(projectDir.getAbsolutePath()); 233 } 234 } 235 } 236 237 allowedSymlinks.addAll(cfg.getAllowedSymlinks()); 238 cfg.setAllowedSymlinks(allowedSymlinks); 239 240 canonicalRoots.addAll(cfg.getCanonicalRoots()); 241 cfg.setCanonicalRoots(canonicalRoots); 242 243 // Assemble the unprocessed command line arguments (possibly a list of paths). 244 // This will be used to perform more fine-grained checking in invalidateRepositories() 245 // called from the setConfiguration() below. 246 for (String arg : argv) { 247 String path = Paths.get(cfg.getSourceRoot(), arg).toString(); 248 subFilesArgs.add(path); 249 } 250 251 // If a user used customizations for projects he perhaps just 252 // used the key value for project without a name but the code 253 // expects a name for the project. Therefore, we fill the name 254 // according to the project key which is the same. 255 for (Entry<String, Project> entry : cfg.getProjects().entrySet()) { 256 if (entry.getValue().getName() == null) { 257 entry.getValue().setName(entry.getKey()); 258 } 259 } 260 261 // Check version of index(es) versus current Lucene version and exit 262 // with return code upon failure. 263 if (checkIndex) { 264 if (cfg.getDataRoot() == null || cfg.getDataRoot().isEmpty()) { 265 System.err.println("Need data root in configuration for index check (use -R)"); 266 System.exit(1); 267 } 268 269 if (!IndexCheck.check(cfg, subFilesArgs)) { 270 System.err.printf("Index check failed%n"); 271 System.err.print("You might want to remove " + 272 (!subFilesArgs.isEmpty() ? "data for projects " + String.join(",", subFilesArgs) : 273 "all data") + " under the data root and reindex\n"); 274 System.exit(1); 275 } 276 277 System.exit(0); 278 } 279 280 // Set updated configuration in RuntimeEnvironment. This is called so that the tunables set 281 // via command line options are available. 282 env.setConfiguration(cfg, subFilesArgs, CommandTimeoutType.INDEXER); 283 284 // Let repository types to add items to ignoredNames. 285 // This changes env so is called after the setConfiguration() 286 // call above. 287 RepositoryFactory.initializeIgnoredNames(env); 288 289 if (bareConfig) { 290 // Set updated configuration in RuntimeEnvironment. 291 env.setConfiguration(cfg, subFilesArgs, CommandTimeoutType.INDEXER); 292 293 getInstance().sendToConfigHost(env, webappURI); 294 writeConfigToFile(env, configFilename); 295 System.exit(0); 296 } 297 298 /* 299 * Add paths to directories under source root. If projects 300 * are enabled the path should correspond to a project because 301 * project path is necessary to correctly set index directory 302 * (otherwise the index files will end up in index data root 303 * directory and not per project data root directory). 304 * For the check we need to have 'env' already set. 305 */ 306 for (String path : subFilesArgs) { 307 String srcPath = env.getSourceRootPath(); 308 if (srcPath == null) { 309 System.err.println("Error getting source root from environment. Exiting."); 310 System.exit(1); 311 } 312 313 path = path.substring(srcPath.length()); 314 if (env.hasProjects()) { 315 // The paths need to correspond to a project. 316 Project project; 317 if ((project = Project.getProject(path)) != null) { 318 subFiles.add(path); 319 List<RepositoryInfo> repoList = env.getProjectRepositoriesMap().get(project); 320 if (repoList != null) { 321 repositories.addAll(repoList. 322 stream().map(RepositoryInfo::getDirectoryNameRelative).collect(Collectors.toSet())); 323 } 324 } else { 325 System.err.println("The path " + path 326 + " does not correspond to a project"); 327 } 328 } else { 329 subFiles.add(path); 330 } 331 } 332 333 if (!subFilesArgs.isEmpty() && subFiles.isEmpty()) { 334 System.err.println("None of the paths were added, exiting"); 335 System.exit(1); 336 } 337 338 if (!subFiles.isEmpty() && configFilename != null) { 339 LOGGER.log(Level.WARNING, "The collection of entries to process is non empty ({0}), seems like " + 340 "the intention is to perform per project reindex, however the -W option is used. " + 341 "This will likely not work.", subFiles); 342 } 343 344 Metrics.updateSubFiles(subFiles); 345 346 // If the webapp is running with a config that does not contain 347 // 'projectsEnabled' property (case of upgrade or transition 348 // from project-less config to one with projects), set the property 349 // so that the 'project/indexed' messages 350 // emitted during indexing do not cause validation error. 351 if (addProjects && webappURI != null) { 352 try { 353 IndexerUtil.enableProjects(webappURI); 354 } catch (Exception e) { 355 LOGGER.log(Level.SEVERE, String.format("Couldn't notify the webapp on %s.", webappURI), e); 356 System.err.printf("Couldn't notify the webapp on %s: %s.%n", webappURI, e.getLocalizedMessage()); 357 } 358 } 359 360 LOGGER.log(Level.INFO, "Indexer version {0} ({1}) running on Java {2}", 361 new Object[]{Info.getVersion(), Info.getRevision(), System.getProperty("java.version")}); 362 363 // Create history cache first. 364 if (searchRepositories) { 365 if (searchPaths.isEmpty()) { 366 String[] dirs = env.getSourceRootFile(). 367 list((f, name) -> f.isDirectory() && env.getPathAccepter().accept(f)); 368 if (dirs != null) { 369 searchPaths.addAll(Arrays.asList(dirs)); 370 } 371 } 372 373 searchPaths = searchPaths.stream(). 374 map(t -> Paths.get(env.getSourceRootPath(), t).toString()). 375 collect(Collectors.toSet()); 376 } 377 getInstance().prepareIndexer(env, searchPaths, addProjects, 378 createDict, runIndex, subFiles, new ArrayList<>(repositories)); 379 380 // Set updated configuration in RuntimeEnvironment. This is called so that repositories discovered 381 // in prepareIndexer() are stored in the Configuration used by RuntimeEnvironment. 382 env.setConfiguration(cfg, subFilesArgs, CommandTimeoutType.INDEXER); 383 384 // prepareIndexer() populated the list of projects so now default projects can be set. 385 env.setDefaultProjectsFromNames(defaultProjects); 386 387 // And now index it all. 388 if (runIndex || (optimizedChanged && env.isOptimizeDatabase())) { 389 IndexChangedListener progress = new DefaultIndexChangedListener(); 390 getInstance().doIndexerExecution(update, subFiles, progress); 391 } 392 393 writeConfigToFile(env, configFilename); 394 395 // Finally, send new configuration to the web application in the case of full reindex. 396 if (webappURI != null && subFiles.isEmpty()) { 397 getInstance().sendToConfigHost(env, webappURI); 398 } 399 400 env.getIndexerParallelizer().bounce(); 401 } catch (ParseException e) { 402 System.err.println("** " + e.getMessage()); 403 System.exit(1); 404 } catch (IndexerException ex) { 405 LOGGER.log(Level.SEVERE, "Exception running indexer", ex); 406 System.err.println("Exception: " + ex.getLocalizedMessage()); 407 System.err.println(optParser.getUsage()); 408 System.exit(1); 409 } catch (Throwable e) { 410 LOGGER.log(Level.SEVERE, "Unexpected Exception", e); 411 System.err.println("Exception: " + e.getLocalizedMessage()); 412 System.exit(1); 413 } finally { 414 stats.report(LOGGER, "Indexer finished", "indexer.total"); 415 } 416 } 417 checkConfiguration()418 private static void checkConfiguration() { 419 if (bareConfig && (env.getConfigURI() == null || env.getConfigURI().isEmpty())) { 420 die("Missing webappURI setting"); 421 } 422 423 if (!repositories.isEmpty() && !cfg.isHistoryEnabled()) { 424 die("Repositories were specified; history is off however"); 425 } 426 427 try { 428 cfg.checkConfiguration(); 429 } catch (Configuration.ConfigurationException e) { 430 die(e.getMessage()); 431 } 432 } 433 434 /** 435 * Parse OpenGrok Indexer options 436 * This method was created so that it would be easier to write unit 437 * tests against the Indexer option parsing mechanism. 438 * 439 * Limit usage lines to {@link org.opengrok.indexer.util.OptionParser.Option#MAX_DESCRIPTION_LINE_LENGTH} 440 * characters for concise formatting. 441 * 442 * @param argv the command line arguments 443 * @return array of remaining non option arguments 444 * @throws ParseException if parsing failed 445 */ parseOptions(String[] argv)446 public static String[] parseOptions(String[] argv) throws ParseException { 447 final String[] usage = {HELP_OPT_1}; 448 449 if (argv.length == 0) { 450 argv = usage; // will force usage output 451 status = 1; // with non-zero EXIT STATUS 452 } 453 454 /* 455 * Pre-match any of the --help options so that some possible exception-generating args handlers (e.g. -R) 456 * can be short-circuited. 457 */ 458 boolean preHelp = Arrays.stream(argv).anyMatch(s -> HELP_OPT_1.equals(s) || 459 HELP_OPT_2.equals(s) || HELP_OPT_3.equals(s)); 460 461 OptionParser configure = OptionParser.scan(parser -> 462 parser.on("-R configPath").execute(cfgFile -> { 463 try { 464 cfg = Configuration.read(new File((String) cfgFile)); 465 } catch (IOException e) { 466 if (!preHelp) { 467 die(e.getMessage()); 468 } else { 469 System.err.printf("Warning: failed to read -R %s%n", cfgFile); 470 } 471 } 472 })); 473 474 searchPaths.clear(); 475 476 optParser = OptionParser.execute(parser -> { 477 parser.setPrologue(String.format("%nUsage: java -jar %s [options] [subDir1 [...]]%n", OPENGROK_JAR)); 478 479 parser.on(HELP_OPT_3, HELP_OPT_2, HELP_OPT_1, "=[mode]", 480 "With no mode specified, display this usage summary. Or specify a mode:", 481 " config - display configuration.xml examples.", 482 " ctags - display ctags command-line.", 483 " guru - display AnalyzerGuru details.", 484 " repos - display enabled repositories.").execute(v -> { 485 help = true; 486 helpUsage = parser.getUsage(); 487 String mode = (String) v; 488 if (mode != null && !mode.isEmpty()) { 489 try { 490 helpMode = HelpMode.valueOf(((String) v).toUpperCase(Locale.ROOT)); 491 } catch (IllegalArgumentException ex) { 492 die("mode '" + v + "' is not valid."); 493 } 494 } 495 }); 496 497 parser.on("--apiTimeout", "=number", Integer.class, 498 "Set timeout for asynchronous API requests.").execute(v -> cfg.setApiTimeout((Integer) v)); 499 500 parser.on("--connectTimeout", "=number", Integer.class, 501 "Set connect timeout. Used for API requests.").execute(v -> cfg.setConnectTimeout((Integer) v)); 502 503 parser.on( 504 "-A (.ext|prefix.):(-|analyzer)", "--analyzer", 505 "/(\\.\\w+|\\w+\\.):(-|[a-zA-Z_0-9.]+)/", 506 "Associates files with the specified prefix or extension (case-", 507 "insensitive) to be analyzed with the given analyzer, where 'analyzer'", 508 "may be specified using a class name (case-sensitive e.g. RubyAnalyzer)", 509 "or analyzer language name (case-sensitive e.g. C). Option may be", 510 "repeated.", 511 " Ex: -A .foo:CAnalyzer", 512 " will use the C analyzer for all files ending with .FOO", 513 " Ex: -A bar.:Perl", 514 " will use the Perl analyzer for all files starting with", 515 " \"BAR\" (no full-stop)", 516 " Ex: -A .c:-", 517 " will disable specialized analyzers for all files ending with .c"). 518 execute(analyzerSpec -> { 519 String[] arg = ((String) analyzerSpec).split(":"); 520 String fileSpec = arg[0]; 521 String analyzer = arg[1]; 522 configureFileAnalyzer(fileSpec, analyzer); 523 } 524 ); 525 526 parser.on("-c", "--ctags", "=/path/to/ctags", 527 "Path to Universal Ctags. Default is ctags in environment PATH.").execute( 528 v -> cfg.setCtags((String) v)); 529 530 parser.on("--canonicalRoot", "=/path/", 531 "Allow symlinks to canonical targets starting with the specified root", 532 "without otherwise needing to specify -N,--symlink for such symlinks. A", 533 "canonical root must end with a file separator. For security, a canonical", 534 "root cannot be the root directory. Option may be repeated.").execute(v -> { 535 String root = (String) v; 536 String problem = CanonicalRootValidator.validate(root, "--canonicalRoot"); 537 if (problem != null) { 538 die(problem); 539 } 540 canonicalRoots.add(root); 541 }); 542 543 parser.on("--checkIndex", "Check index, exit with 0 on success,", 544 "with 1 on failure.").execute(v -> checkIndex = true); 545 546 parser.on("-d", "--dataRoot", "=/path/to/data/root", 547 "The directory where OpenGrok stores the generated data."). 548 execute(drPath -> { 549 File dataRoot = new File((String) drPath); 550 if (!dataRoot.exists() && !dataRoot.mkdirs()) { 551 die("Cannot create data root: " + dataRoot); 552 } 553 if (!dataRoot.isDirectory()) { 554 die("Data root must be a directory"); 555 } 556 try { 557 cfg.setDataRoot(dataRoot.getCanonicalPath()); 558 } catch (IOException e) { 559 die(e.getMessage()); 560 } 561 } 562 ); 563 564 parser.on("--depth", "=number", Integer.class, 565 "Scanning depth for repositories in directory structure relative to", 566 "source root. Default is " + Configuration.defaultScanningDepth + ".").execute(depth -> 567 cfg.setScanningDepth((Integer) depth)); 568 569 parser.on("--disableRepository", "=type_name", 570 "Disables operation of an OpenGrok-supported repository. See also", 571 "-h,--help repos. Option may be repeated.", 572 " Ex: --disableRepository git", 573 " will disable the GitRepository", 574 " Ex: --disableRepository MercurialRepository").execute(v -> { 575 String repoType = (String) v; 576 String repoSimpleType = RepositoryFactory.matchRepositoryByName(repoType); 577 if (repoSimpleType == null) { 578 System.err.printf("'--disableRepository %s' does not match a type and is ignored%n", v); 579 } else { 580 disabledRepositories.add(repoSimpleType); 581 } 582 }); 583 584 parser.on("-e", "--economical", 585 "To consume less disk space, OpenGrok will not generate and save", 586 "hypertext cross-reference files but will generate on demand, which could", 587 "be slightly slow.").execute(v -> cfg.setGenerateHtml(false)); 588 589 parser.on("-G", "--assignTags", 590 "Assign commit tags to all entries in history for all repositories.").execute(v -> 591 cfg.setTagsEnabled(true)); 592 593 // for backward compatibility 594 parser.on("-H", "Enable history.").execute(v -> cfg.setHistoryEnabled(true)); 595 596 parser.on("--historyBased", "=on|off", ON_OFF, Boolean.class, 597 "If history based reindex is in effect, the set of files ", 598 "changed/deleted since the last reindex is determined from history ", 599 "of the repositories. This needs history, history cache and ", 600 "projects to be enabled. This should be much faster than the ", 601 "classic way of traversing the directory structure. ", 602 "The default is on. If you need to e.g. index files untracked by ", 603 "SCM, set this to off. Currently works only for Git.", 604 "All repositories in a project need to support this in order ", 605 "to be indexed using history."). 606 execute(v -> cfg.setHistoryBasedReindex((Boolean) v)); 607 608 parser.on("--historyThreads", "=number", Integer.class, 609 "The number of threads to use for history cache generation on repository level. ", 610 "By default the number of threads will be set to the number of available CPUs.", 611 "Assumes -H/--history.").execute(threadCount -> 612 cfg.setHistoryParallelism((Integer) threadCount)); 613 614 parser.on("--historyFileThreads", "=number", Integer.class, 615 "The number of threads to use for history cache generation ", 616 "when dealing with individual files.", 617 "By default the number of threads will be set to the number of available CPUs.", 618 "Assumes -H/--history.").execute(threadCount -> 619 cfg.setHistoryFileParallelism((Integer) threadCount)); 620 621 parser.on("-I", "--include", "=pattern", 622 "Only files matching this pattern will be examined. Pattern supports", 623 "wildcards (example: -I '*.java' -I '*.c'). Option may be repeated.").execute( 624 pattern -> cfg.getIncludedNames().add((String) pattern)); 625 626 parser.on("-i", "--ignore", "=pattern", 627 "Ignore matching files (prefixed with 'f:' or no prefix) or directories", 628 "(prefixed with 'd:'). Pattern supports wildcards (example: -i '*.so'", 629 "-i d:'test*'). Option may be repeated.").execute(pattern -> 630 cfg.getIgnoredNames().add((String) pattern)); 631 632 parser.on("-l", "--lock", "=on|off|simple|native", LUCENE_LOCKS, 633 "Set OpenGrok/Lucene locking mode of the Lucene database during index", 634 "generation. \"on\" is an alias for \"simple\". Default is off.").execute(v -> { 635 try { 636 if (v != null) { 637 String vuc = v.toString().toUpperCase(Locale.ROOT); 638 cfg.setLuceneLocking(LuceneLockName.valueOf(vuc)); 639 } 640 } catch (IllegalArgumentException e) { 641 System.err.printf("`--lock %s' is invalid and ignored%n", v); 642 } 643 }); 644 645 parser.on("--leadingWildCards", "=on|off", ON_OFF, Boolean.class, 646 "Allow or disallow leading wildcards in a search. Default is on.").execute(v -> 647 cfg.setAllowLeadingWildcard((Boolean) v)); 648 649 parser.on("-m", "--memory", "=number", Double.class, 650 "Amount of memory (MB) that may be used for buffering added documents and", 651 "deletions before they are flushed to the directory (default " + 652 Configuration.defaultRamBufferSize + ").", 653 "Please increase JVM heap accordingly too.").execute(memSize -> 654 cfg.setRamBufferSize((Double) memSize)); 655 656 parser.on("--mandoc", "=/path/to/mandoc", "Path to mandoc(1) binary.") 657 .execute(mandocPath -> cfg.setMandoc((String) mandocPath)); 658 659 parser.on("-N", "--symlink", "=/path/to/symlink", 660 "Allow the symlink to be followed. Other symlinks targeting the same", 661 "canonical target or canonical children will be allowed too. Option may", 662 "be repeated. (By default only symlinks directly under the source root", 663 "directory are allowed. See also --canonicalRoot)").execute(v -> 664 allowedSymlinks.add((String) v)); 665 666 parser.on("-n", "--noIndex", 667 "Do not generate indexes and other data (such as history cache and xref", 668 "files), but process all other command line options.").execute(v -> 669 runIndex = false); 670 671 parser.on("--nestingMaximum", "=number", Integer.class, 672 "Maximum depth of nested repositories. Default is 1.").execute(v -> 673 cfg.setNestingMaximum((Integer) v)); 674 675 parser.on("-O", "--optimize", "=on|off", ON_OFF, Boolean.class, 676 "Turn on/off the optimization of the index database as part of the", 677 "indexing step. Default is on."). 678 execute(v -> { 679 boolean oldval = cfg.isOptimizeDatabase(); 680 cfg.setOptimizeDatabase((Boolean) v); 681 if (oldval != cfg.isOptimizeDatabase()) { 682 optimizedChanged = true; 683 } 684 } 685 ); 686 687 parser.on("-o", "--ctagOpts", "=path", 688 "File with extra command line options for ctags."). 689 execute(path -> { 690 String CTagsExtraOptionsFile = (String) path; 691 File CTagsFile = new File(CTagsExtraOptionsFile); 692 if (!(CTagsFile.isFile() && CTagsFile.canRead())) { 693 die("File '" + CTagsExtraOptionsFile + "' not found for the -o option"); 694 } 695 System.err.println("INFO: file with extra " 696 + "options for ctags: " + CTagsExtraOptionsFile); 697 cfg.setCTagsExtraOptionsFile(CTagsExtraOptionsFile); 698 } 699 ); 700 701 parser.on("-P", "--projects", 702 "Generate a project for each top-level directory in source root.").execute(v -> { 703 addProjects = true; 704 cfg.setProjectsEnabled(true); 705 }); 706 707 parser.on("-p", "--defaultProject", "=path/to/default/project", 708 "Path (relative to the source root) to a project that should be selected", 709 "by default in the web application (when no other project is set either", 710 "in a cookie or in parameter). Option may be repeated to specify several", 711 "projects. Use the special value __all__ to indicate all projects.").execute(v -> 712 defaultProjects.add((String) v)); 713 714 parser.on("--profiler", "Pause to await profiler or debugger."). 715 execute(v -> awaitProfiler = true); 716 717 parser.on("--progress", 718 "Print per-project percentage progress information.").execute(v -> 719 cfg.setPrintProgress(true)); 720 721 parser.on("-Q", "--quickScan", "=on|off", ON_OFF, Boolean.class, 722 "Turn on/off quick context scan. By default, only the first 1024KB of a", 723 "file is scanned, and a link ('[..all..]') is inserted when the file is", 724 "bigger. Activating this may slow the server down. (Note: this setting", 725 "only affects the web application.) Default is on.").execute(v -> 726 cfg.setQuickContextScan((Boolean) v)); 727 728 parser.on("-q", "--quiet", 729 "Run as quietly as possible. Sets logging level to WARNING.").execute(v -> 730 LoggerUtil.setBaseConsoleLogLevel(Level.WARNING)); 731 732 parser.on("-R /path/to/configuration", 733 "Read configuration from the specified file.").execute(v -> { 734 // Already handled above. This populates usage. 735 }); 736 737 parser.on("-r", "--remote", "=on|off|uionly|dirbased", 738 REMOTE_REPO_CHOICES, 739 "Specify support for remote SCM systems.", 740 " on - allow retrieval for remote SCM systems.", 741 " off - ignore SCM for remote systems.", 742 " uionly - support remote SCM for user interface only.", 743 "dirbased - allow retrieval during history index only for repositories", 744 " which allow getting history for directories."). 745 execute(v -> { 746 String option = (String) v; 747 if (option.equalsIgnoreCase(ON)) { 748 cfg.setRemoteScmSupported(Configuration.RemoteSCM.ON); 749 } else if (option.equalsIgnoreCase(OFF)) { 750 cfg.setRemoteScmSupported(Configuration.RemoteSCM.OFF); 751 } else if (option.equalsIgnoreCase(DIRBASED)) { 752 cfg.setRemoteScmSupported(Configuration.RemoteSCM.DIRBASED); 753 } else if (option.equalsIgnoreCase(UIONLY)) { 754 cfg.setRemoteScmSupported(Configuration.RemoteSCM.UIONLY); 755 } 756 } 757 ); 758 759 parser.on("--renamedHistory", "=on|off", ON_OFF, Boolean.class, 760 "Enable or disable generating history for renamed files.", 761 "If set to on, makes history indexing slower for repositories", 762 "with lots of renamed files. Default is off.").execute(v -> 763 cfg.setHandleHistoryOfRenamedFiles((Boolean) v)); 764 765 parser.on("--repository", "=[path/to/repository|@file_with_paths]", 766 "Path (relative to the source root) to a repository for generating", 767 "history (if -H,--history is on). By default all discovered repositories", 768 "are history-eligible; using --repository limits to only those specified.", 769 "File containing paths can be specified via @path syntax.", 770 "Option may be repeated.") 771 .execute(v -> handlePathParameter(repositories, ((String) v).trim())); 772 773 parser.on("-S", "--search", "=[path/to/repository|@file_with_paths]", 774 "Search for source repositories under -s,--source, and add them. Path", 775 "(relative to the source root) is optional. ", 776 "File containing paths can be specified via @path syntax.", 777 "Option may be repeated.") 778 .execute(v -> { 779 searchRepositories = true; 780 String value = ((String) v).trim(); 781 if (!value.isEmpty()) { 782 handlePathParameter(searchPaths, value); 783 } 784 }); 785 786 parser.on("-s", "--source", "=/path/to/source/root", 787 "The root directory of the source tree."). 788 execute(source -> { 789 File sourceRoot = new File((String) source); 790 if (!sourceRoot.isDirectory()) { 791 die("Source root " + sourceRoot + " must be a directory"); 792 } 793 try { 794 cfg.setSourceRoot(sourceRoot.getCanonicalPath()); 795 } catch (IOException e) { 796 die(e.getMessage()); 797 } 798 } 799 ); 800 801 parser.on("--style", "=path", 802 "Path to the subdirectory in the web application containing the requested", 803 "stylesheet. The factory-setting is: \"default\".").execute(stylePath -> 804 cfg.setWebappLAF((String) stylePath)); 805 806 parser.on("-T", "--threads", "=number", Integer.class, 807 "The number of threads to use for index generation, repository scan", 808 "and repository invalidation.", 809 "By default the number of threads will be set to the number of available", 810 "CPUs. This influences the number of spawned ctags processes as well."). 811 execute(threadCount -> cfg.setIndexingParallelism((Integer) threadCount)); 812 813 parser.on("-t", "--tabSize", "=number", Integer.class, 814 "Default tab size to use (number of spaces per tab character).") 815 .execute(tabSize -> cfg.setTabSize((Integer) tabSize)); 816 817 parser.on("--token", "=string|@file_with_string", 818 "Authorization bearer API token to use when making API calls", 819 "to the web application"). 820 execute(optarg -> { 821 String value = ((String) optarg).trim(); 822 if (value.startsWith("@")) { 823 try (BufferedReader in = new BufferedReader(new InputStreamReader( 824 new FileInputStream(Path.of(value).toString().substring(1))))) { 825 String token = in.readLine().trim(); 826 cfg.setIndexerAuthenticationToken(token); 827 } catch (IOException e) { 828 die("Failed to read from " + value); 829 } 830 } else { 831 cfg.setIndexerAuthenticationToken(value); 832 } 833 }); 834 835 parser.on("-U", "--uri", "=SCHEME://webappURI:port/contextPath", 836 "Send the current configuration to the specified web application.").execute(webAddr -> { 837 webappURI = (String) webAddr; 838 try { 839 URI uri = new URI(webappURI); 840 String scheme = uri.getScheme(); 841 if (!scheme.equals("http") && !scheme.equals("https")) { 842 die("webappURI '" + webappURI + "' does not have HTTP/HTTPS scheme"); 843 } 844 } catch (URISyntaxException e) { 845 die("URL '" + webappURI + "' is not valid."); 846 } 847 848 env = RuntimeEnvironment.getInstance(); 849 env.setConfigURI(webappURI); 850 } 851 ); 852 853 parser.on("---unitTest"); // For unit test only, will not appear in help 854 855 parser.on("--updateConfig", 856 "Populate the web application with a bare configuration, and exit.").execute(v -> 857 bareConfig = true); 858 859 parser.on("--userPage", "=URL", 860 "Base URL of the user Information provider.", 861 "Example: \"https://www.example.org/viewProfile.jspa?username=\".", 862 "Use \"none\" to disable link.").execute(v -> cfg.setUserPage((String) v)); 863 864 parser.on("--userPageSuffix", "=URL-suffix", 865 "URL Suffix for the user Information provider. Default: \"\".") 866 .execute(suffix -> cfg.setUserPageSuffix((String) suffix)); 867 868 parser.on("-V", "--version", "Print version, and quit.").execute(v -> { 869 System.out.println(Info.getFullVersion()); 870 System.exit(0); 871 }); 872 873 parser.on("-v", "--verbose", "Set logging level to INFO.").execute(v -> { 874 verbose = true; 875 LoggerUtil.setBaseConsoleLogLevel(Level.INFO); 876 }); 877 878 parser.on("-W", "--writeConfig", "=/path/to/configuration", 879 "Write the current configuration to the specified file (so that the web", 880 "application can use the same configuration).").execute(configFile -> 881 configFilename = (String) configFile); 882 883 parser.on("--webappCtags", "=on|off", ON_OFF, Boolean.class, 884 "Web application should run ctags when necessary. Default is off."). 885 execute(v -> cfg.setWebappCtags((Boolean) v)); 886 }); 887 888 // Need to read the configuration file first, so that options may be overwritten later. 889 configure.parse(argv); 890 891 LOGGER.log(Level.INFO, "Indexer options: {0}", Arrays.toString(argv)); 892 893 if (cfg == null) { 894 cfg = new Configuration(); 895 } 896 897 cfg.setHistoryEnabled(false); // force user to turn on history capture 898 899 argv = optParser.parse(argv); 900 901 return argv; 902 } 903 die(String message)904 private static void die(String message) { 905 System.err.println("ERROR: " + message); 906 System.exit(1); 907 } 908 configureFileAnalyzer(String fileSpec, String analyzer)909 private static void configureFileAnalyzer(String fileSpec, String analyzer) { 910 911 boolean prefix = false; 912 913 // removing '.' from file specification 914 // expecting either ".extensionName" or "prefixName." 915 if (fileSpec.endsWith(".")) { 916 fileSpec = fileSpec.substring(0, fileSpec.lastIndexOf('.')); 917 prefix = true; 918 } else { 919 fileSpec = fileSpec.substring(1); 920 } 921 fileSpec = fileSpec.toUpperCase(Locale.ROOT); 922 923 // Disable analyzer? 924 if (analyzer.equals("-")) { 925 if (prefix) { 926 AnalyzerGuru.addPrefix(fileSpec, null); 927 } else { 928 AnalyzerGuru.addExtension(fileSpec, null); 929 } 930 } else { 931 try { 932 if (prefix) { 933 AnalyzerGuru.addPrefix( 934 fileSpec, 935 AnalyzerGuru.findFactory(analyzer)); 936 } else { 937 AnalyzerGuru.addExtension( 938 fileSpec, 939 AnalyzerGuru.findFactory(analyzer)); 940 } 941 942 } catch (ClassNotFoundException | IllegalAccessException | InstantiationException | NoSuchMethodException 943 | InvocationTargetException e) { 944 LOGGER.log(Level.SEVERE, "Unable to locate FileAnalyzerFactory for {0}", analyzer); 945 LOGGER.log(Level.SEVERE, "Stack: ", e.fillInStackTrace()); 946 System.exit(1); 947 } 948 } 949 } 950 951 /** 952 * Write configuration to a file. 953 * @param env runtime environment 954 * @param filename file name to write the configuration to 955 * @throws IOException if I/O exception occurred 956 */ writeConfigToFile(RuntimeEnvironment env, String filename)957 public static void writeConfigToFile(RuntimeEnvironment env, String filename) throws IOException { 958 if (filename != null) { 959 LOGGER.log(Level.INFO, "Writing configuration to {0}", filename); 960 env.writeConfiguration(new File(filename)); 961 LOGGER.log(Level.INFO, "Done writing configuration to {0}", filename); 962 } 963 } 964 965 // Wrapper for prepareIndexer() that always generates history cache. prepareIndexer(RuntimeEnvironment env, boolean searchRepositories, boolean addProjects, boolean createDict, List<String> subFiles, List<String> repositories)966 public void prepareIndexer(RuntimeEnvironment env, 967 boolean searchRepositories, 968 boolean addProjects, 969 boolean createDict, 970 List<String> subFiles, 971 List<String> repositories) throws IndexerException, IOException { 972 973 prepareIndexer(env, 974 searchRepositories ? Collections.singleton(env.getSourceRootPath()) : Collections.emptySet(), 975 addProjects, createDict, true, subFiles, repositories); 976 } 977 978 /** 979 * Generate history cache and/or scan the repositories. 980 * 981 * This is the first phase of the indexing where history cache is being 982 * generated for repositories (at least for those which support getting 983 * history per directory). 984 * 985 * @param env runtime environment 986 * @param searchPaths list of paths in which to search for repositories 987 * @param addProjects if true, add projects 988 * @param createDict if true, create dictionary 989 * @param createHistoryCache create history cache flag 990 * @param subFiles list of directories 991 * @param repositories list of repositories 992 * @throws IndexerException indexer exception 993 * @throws IOException I/O exception 994 */ prepareIndexer(RuntimeEnvironment env, Set<String> searchPaths, boolean addProjects, boolean createDict, boolean createHistoryCache, List<String> subFiles, List<String> repositories)995 public void prepareIndexer(RuntimeEnvironment env, 996 Set<String> searchPaths, 997 boolean addProjects, 998 boolean createDict, 999 boolean createHistoryCache, 1000 List<String> subFiles, 1001 List<String> repositories) throws IndexerException, IOException { 1002 1003 if (!env.validateUniversalCtags()) { 1004 throw new IndexerException("Didn't find Universal Ctags"); 1005 } 1006 1007 // Projects need to be created first since when adding repositories below, 1008 // some project properties might be needed for that. 1009 if (addProjects) { 1010 File[] files = env.getSourceRootFile().listFiles(); 1011 Map<String, Project> projects = env.getProjects(); 1012 1013 addProjects(files, projects); 1014 } 1015 1016 if (!searchPaths.isEmpty()) { 1017 LOGGER.log(Level.INFO, "Scanning for repositories in {0} (down to {1} levels below source root)", 1018 new Object[]{searchPaths, env.getScanningDepth()}); 1019 Statistics stats = new Statistics(); 1020 env.setRepositories(searchPaths.toArray(new String[0])); 1021 stats.report(LOGGER, String.format("Done scanning for repositories, found %d repositories", 1022 env.getRepositories().size()), "indexer.repository.scan"); 1023 } 1024 1025 if (createHistoryCache) { 1026 // Even if history is disabled globally, it can be enabled for some repositories. 1027 if (repositories != null && !repositories.isEmpty()) { 1028 LOGGER.log(Level.INFO, "Generating history cache for repositories: {0}", 1029 String.join(",", repositories)); 1030 HistoryGuru.getInstance(). 1031 createCache(repositories); 1032 } else { 1033 LOGGER.log(Level.INFO, "Generating history cache for all repositories ..."); 1034 HistoryGuru.getInstance().createCache(); 1035 } 1036 LOGGER.info("Done generating history cache"); 1037 } 1038 1039 if (createDict) { 1040 IndexDatabase.listFrequentTokens(subFiles); 1041 } 1042 } 1043 addProjects(File[] files, Map<String, Project> projects)1044 private void addProjects(File[] files, Map<String, Project> projects) { 1045 // Keep a copy of the old project list so that we can preserve 1046 // the customization of existing projects. 1047 Map<String, Project> oldProjects = new HashMap<>(); 1048 for (Project p : projects.values()) { 1049 oldProjects.put(p.getName(), p); 1050 } 1051 1052 projects.clear(); 1053 1054 // Add a project for each top-level directory in source root. 1055 for (File file : files) { 1056 String name = file.getName(); 1057 String path = '/' + name; 1058 if (oldProjects.containsKey(name)) { 1059 // This is an existing object. Reuse the old project, 1060 // possibly with customizations, instead of creating a 1061 // new with default values. 1062 Project p = oldProjects.get(name); 1063 p.setPath(path); 1064 p.setName(name); 1065 p.completeWithDefaults(); 1066 projects.put(name, p); 1067 } else if (!name.startsWith(".") && file.isDirectory()) { 1068 // Found a new directory with no matching project, so 1069 // create a new project with default properties. 1070 projects.put(name, new Project(name, path)); 1071 } 1072 } 1073 } 1074 1075 /** 1076 * This is the second phase of the indexer which generates Lucene index 1077 * by passing source code files through ctags, generating xrefs 1078 * and storing data from the source files in the index (along with history, 1079 * if any). 1080 * 1081 * @param update if set to true, index database is updated, otherwise optimized 1082 * @param subFiles index just some subdirectories 1083 * @param progress object to receive notifications as indexer progress is made 1084 * @throws IOException if I/O exception occurred 1085 */ doIndexerExecution(final boolean update, List<String> subFiles, IndexChangedListener progress)1086 public void doIndexerExecution(final boolean update, List<String> subFiles, 1087 IndexChangedListener progress) 1088 throws IOException { 1089 Statistics elapsed = new Statistics(); 1090 LOGGER.info("Starting indexing"); 1091 1092 RuntimeEnvironment env = RuntimeEnvironment.getInstance(); 1093 IndexerParallelizer parallelizer = env.getIndexerParallelizer(); 1094 final CountDownLatch latch; 1095 if (subFiles == null || subFiles.isEmpty()) { 1096 if (update) { 1097 latch = IndexDatabase.updateAll(progress); 1098 } else if (env.isOptimizeDatabase()) { 1099 latch = IndexDatabase.optimizeAll(); 1100 } else { 1101 latch = new CountDownLatch(0); 1102 } 1103 } else { 1104 List<IndexDatabase> dbs = new ArrayList<>(); 1105 1106 for (String path : subFiles) { 1107 Project project = Project.getProject(path); 1108 if (project == null && env.hasProjects()) { 1109 LOGGER.log(Level.WARNING, "Could not find a project for \"{0}\"", path); 1110 } else { 1111 IndexDatabase db; 1112 if (project == null) { 1113 db = new IndexDatabase(); 1114 } else { 1115 db = new IndexDatabase(project); 1116 } 1117 int idx = dbs.indexOf(db); 1118 if (idx != -1) { 1119 db = dbs.get(idx); 1120 } 1121 1122 if (db.addDirectory(path)) { 1123 if (idx == -1) { 1124 dbs.add(db); 1125 } 1126 } else { 1127 LOGGER.log(Level.WARNING, "Directory does not exist \"{0}\"", path); 1128 } 1129 } 1130 } 1131 1132 latch = new CountDownLatch(dbs.size()); 1133 for (final IndexDatabase db : dbs) { 1134 final boolean optimize = env.isOptimizeDatabase(); 1135 db.addIndexChangedListener(progress); 1136 parallelizer.getFixedExecutor().submit(() -> { 1137 try { 1138 if (update) { 1139 db.update(); 1140 } else if (optimize) { 1141 db.optimize(); 1142 } 1143 } catch (Throwable e) { 1144 LOGGER.log(Level.SEVERE, "An error occurred while " 1145 + (update ? "updating" : "optimizing") 1146 + " index", e); 1147 } finally { 1148 latch.countDown(); 1149 } 1150 }); 1151 } 1152 } 1153 1154 // Wait forever for the executors to finish. 1155 try { 1156 LOGGER.info("Waiting for the executors to finish"); 1157 latch.await(); 1158 } catch (InterruptedException exp) { 1159 LOGGER.log(Level.WARNING, "Received interrupt while waiting" + 1160 " for executor to finish", exp); 1161 } 1162 elapsed.report(LOGGER, "Done indexing data of all repositories", "indexer.repository.indexing"); 1163 1164 CtagsUtil.deleteTempFiles(); 1165 } 1166 sendToConfigHost(RuntimeEnvironment env, String host)1167 public void sendToConfigHost(RuntimeEnvironment env, String host) { 1168 LOGGER.log(Level.INFO, "Sending configuration to: {0}", host); 1169 try { 1170 env.writeConfiguration(host); 1171 } catch (IOException | IllegalArgumentException ex) { 1172 LOGGER.log(Level.SEVERE, String.format( 1173 "Failed to send configuration to %s " 1174 + "(is web application server running with opengrok deployed?)", host), ex); 1175 } catch (InterruptedException e) { 1176 LOGGER.log(Level.WARNING, "interrupted while sending configuration"); 1177 } 1178 LOGGER.info("Configuration update routine done, check log output for errors."); 1179 } 1180 pauseToAwaitProfiler()1181 private static void pauseToAwaitProfiler() { 1182 Scanner scan = new Scanner(System.in); 1183 String in; 1184 do { 1185 System.out.print("Start profiler. Continue (Y/N)? "); 1186 in = scan.nextLine().toLowerCase(Locale.ROOT); 1187 } while (!in.equals("y") && !in.equals("n")); 1188 1189 if (in.equals("n")) { 1190 System.exit(1); 1191 } 1192 } 1193 1194 // Visible for testing handlePathParameter(Collection<String> paramValueStore, String pathValue)1195 static void handlePathParameter(Collection<String> paramValueStore, String pathValue) { 1196 if (pathValue.startsWith("@")) { 1197 paramValueStore.addAll(loadPathsFromFile(pathValue.substring(1))); 1198 } else { 1199 paramValueStore.add(pathValue); 1200 } 1201 } 1202 loadPathsFromFile(String filename)1203 private static List<String> loadPathsFromFile(String filename) { 1204 try { 1205 return Files.readAllLines(Path.of(filename)); 1206 } catch (IOException e) { 1207 LOGGER.log(Level.SEVERE, String.format("Could not load paths from %s", filename), e); 1208 throw new UncheckedIOException(e); 1209 } 1210 } 1211 exitWithHelp()1212 private static void exitWithHelp() { 1213 PrintStream helpStream = status != 0 ? System.err : System.out; 1214 switch (helpMode) { 1215 case CONFIG: 1216 helpStream.print(ConfigurationHelp.getSamples()); 1217 break; 1218 case CTAGS: 1219 /* 1220 * Force the environment's ctags, because this method is called 1221 * before main() does the heavyweight setConfiguration(). 1222 */ 1223 env.setCtags(cfg.getCtags()); 1224 helpStream.println("Ctags command-line:"); 1225 helpStream.println(); 1226 helpStream.println(getCtagsCommand()); 1227 helpStream.println(); 1228 break; 1229 case GURU: 1230 helpStream.println(AnalyzerGuruHelp.getUsage()); 1231 break; 1232 case REPOS: 1233 /* 1234 * Force the environment's disabledRepositories (as above). 1235 */ 1236 env.setDisabledRepositories(cfg.getDisabledRepositories()); 1237 helpStream.println(RepositoriesHelp.getText()); 1238 break; 1239 default: 1240 helpStream.println(helpUsage); 1241 break; 1242 } 1243 System.exit(status); 1244 } 1245 getCtagsCommand()1246 private static String getCtagsCommand() { 1247 Ctags ctags = CtagsUtil.newInstance(env); 1248 return Executor.escapeForShell(ctags.getArgv(), true, SystemUtils.IS_OS_WINDOWS); 1249 } 1250 1251 private enum HelpMode { 1252 CONFIG, CTAGS, DEFAULT, GURU, REPOS 1253 } 1254 Indexer()1255 private Indexer() { 1256 } 1257 } 1258