| | |
| | | try (BufferedReader bReader = batchCommandsReader()) { |
| | | List<String> initialArgs = removeBatchArgs(args); |
| | | |
| | | // Split the CLI string into arguments array |
| | | String command = ""; |
| | | String line; |
| | | while ((line = bReader.readLine()) != null) { |
| | | if (line.isEmpty() || line.startsWith("#")) { |
| | | // Empty line or comment |
| | | continue; |
| | | } |
| | | // command split in several line support |
| | | if (line.endsWith("\\")) { |
| | | // command is split into several lines |
| | | command += line.substring(0, line.length() - 1); |
| | | continue; |
| | | } |
| | | String command; |
| | | while ((command = nextBatchCommand(bReader)) != null) { |
| | | // Only the echo is trimmed: an escaped blank at the end of the command is kept. |
| | | printlnNoWrap(LocalizableMessage.raw(command.trim())); |
| | | |
| | | command += line; |
| | | command = command.trim(); |
| | | printlnNoWrap(LocalizableMessage.raw(command)); |
| | | |
| | | // Append initial arguments to the file line |
| | | final String[] allArgsArray = buildCommandArgs(initialArgs, command); |
| | | int exitCode = main(allArgsArray, getOutputStream(), getErrorStream()); |
| | | final int exitCode = runBatchCommand(initialArgs, command, getOutputStream(), getErrorStream()); |
| | | if (exitCode != ReturnCode.SUCCESS.get()) { |
| | | System.exit(filterExitCode(exitCode)); |
| | | } |
| | | println(); |
| | | // reset command |
| | | command = ""; |
| | | } |
| | | } catch (IOException ex) { |
| | | errPrintln(ERR_DSCFG_ERROR_READING_BATCH_FILE.get(ex)); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Runs one command of a batch with the initial arguments appended, and returns its exit code. |
| | | * A command that cannot be split into arguments is reported on the error stream: a batch |
| | | * always runs with --no-prompt, so that is where dsconfig writes its errors. |
| | | */ |
| | | static int runBatchCommand(final List<String> initialArgs, final String command, final PrintStream out, |
| | | final PrintStream err) { |
| | | try { |
| | | // Append initial arguments to the file line |
| | | return main(buildCommandArgs(initialArgs, command), out, err); |
| | | } catch (final ArgumentException e) { |
| | | err.println(wrapText(e.getMessageObject(), MAX_LINE_WIDTH)); |
| | | return ReturnCode.ERROR_USER_DATA.get(); |
| | | } |
| | | } |
| | | |
| | | private BufferedReader batchCommandsReader() throws FileNotFoundException { |
| | | if (batchArgument.isPresent()) { |
| | | return new BufferedReader(new InputStreamReader(System.in)); |
| | |
| | | } |
| | | } |
| | | |
| | | private String[] buildCommandArgs(List<String> initialArgs, String batchCommand) { |
| | | /** |
| | | * Reads the next command of a batch, or returns {@code null} at its end. Lines that are empty |
| | | * or hold only blanks, and comments, are skipped, also inside a command that continues over |
| | | * several lines, and a line that ends in an unescaped backslash continues on the next line. A |
| | | * command whose last line continues still runs at the end of the batch. |
| | | */ |
| | | static String nextBatchCommand(final BufferedReader reader) throws IOException { |
| | | final StringBuilder command = new StringBuilder(); |
| | | String line; |
| | | while ((line = reader.readLine()) != null) { |
| | | if (line.trim().isEmpty() || line.startsWith("#")) { |
| | | // Empty or blank line, or comment |
| | | continue; |
| | | } |
| | | if (!continuesOnNextLine(line)) { |
| | | return command.append(line).toString(); |
| | | } |
| | | command.append(line, 0, line.length() - 1); |
| | | } |
| | | return command.toString().trim().isEmpty() ? null : command.toString(); |
| | | } |
| | | |
| | | /** Whether a line ends in a backslash that is not itself escaped by a backslash. */ |
| | | private static boolean continuesOnNextLine(final String line) { |
| | | int backslashes = 0; |
| | | for (int i = line.length() - 1; i >= 0 && line.charAt(i) == '\\'; i--) { |
| | | backslashes++; |
| | | } |
| | | return backslashes % 2 == 1; |
| | | } |
| | | |
| | | private static String[] buildCommandArgs(List<String> initialArgs, String batchCommand) throws ArgumentException { |
| | | final Collection<String> commandArgs = toCommandArgs(batchCommand); |
| | | final int length = commandArgs.size() + initialArgs.size(); |
| | | final List<String> allArguments = new ArrayList<>(length); |
| | |
| | | return allArguments.toArray(new String[length]); |
| | | } |
| | | |
| | | static Collection<String> toCommandArgs(String command) { |
| | | Collection<String> commandArgs = new ArrayList<>(); |
| | | StringBuilder builder = new StringBuilder(); |
| | | boolean inQuotes = false; |
| | | /** |
| | | * Splits a batch line into arguments the way a POSIX shell does, without any expansion: a |
| | | * backslash outside quotes escapes the next char, single quotes keep everything literally, and |
| | | * inside double quotes a backslash escapes only {@code "}, {@code \}, {@code $} and {@code `}. |
| | | * |
| | | * @throws ArgumentException |
| | | * If a quote is not closed. |
| | | */ |
| | | static Collection<String> toCommandArgs(String command) throws ArgumentException { |
| | | final Collection<String> commandArgs = new ArrayList<>(); |
| | | final StringBuilder builder = new StringBuilder(); |
| | | // A word that holds only quotes is still an (empty) argument. |
| | | boolean inWord = false; |
| | | for (int i = 0; i < command.length(); i++) { |
| | | final char c = command.charAt(i); |
| | | switch (c) { |
| | | case ' ': |
| | | case '\t': |
| | | if (inWord) { |
| | | commandArgs.add(builder.toString()); |
| | | builder.setLength(0); |
| | | inWord = false; |
| | | } |
| | | continue; |
| | | case '\\': |
| | | // A trailing backslash has nothing to escape and is kept. |
| | | builder.append(i + 1 < command.length() ? command.charAt(++i) : c); |
| | | break; |
| | | case '\'': |
| | | final int end = command.indexOf('\'', i + 1); |
| | | if (end < 0) { |
| | | throw new ArgumentException(ERR_DSCFG_ERROR_BATCH_UNCLOSED_QUOTE.get(command.trim())); |
| | | } |
| | | builder.append(command, i + 1, end); |
| | | i = end; |
| | | break; |
| | | case '"': |
| | | for (i++; i < command.length() && command.charAt(i) != '"'; i++) { |
| | | if (command.charAt(i) == '\\' && i + 1 < command.length() |
| | | && "\"\\$`".indexOf(command.charAt(i + 1)) >= 0) { |
| | | i++; |
| | | } |
| | | builder.append(command.charAt(i)); |
| | | } |
| | | if (i == command.length()) { |
| | | throw new ArgumentException(ERR_DSCFG_ERROR_BATCH_UNCLOSED_QUOTE.get(command.trim())); |
| | | } |
| | | break; |
| | | default: |
| | | builder.append(c); |
| | | break; |
| | | case '\\': |
| | | builder.append(command.charAt(++i)); |
| | | break; |
| | | case '"': |
| | | if (inQuotes) { |
| | | builder = newArgumentString(commandArgs, builder); |
| | | inQuotes = false; |
| | | } else { |
| | | inQuotes = true; |
| | | } |
| | | break; |
| | | case ' ': |
| | | if (inQuotes) { |
| | | builder.append(c); |
| | | } else { |
| | | builder = newArgumentString(commandArgs, builder); |
| | | } |
| | | break; |
| | | } |
| | | inWord = true; |
| | | } |
| | | newArgumentString(commandArgs, builder); |
| | | if (inWord) { |
| | | commandArgs.add(builder.toString()); |
| | | } |
| | | return commandArgs; |
| | | } |
| | | |
| | | private static StringBuilder newArgumentString(Collection<String> commandArgs, StringBuilder stringBuilder) { |
| | | if (stringBuilder.length() > 0) { |
| | | commandArgs.add(stringBuilder.toString()); |
| | | stringBuilder = new StringBuilder(); |
| | | } |
| | | return stringBuilder; |
| | | } |
| | | |
| | | private List<String> removeBatchArgs(String[] args) { |
| | | // Build a list of initial arguments, |
| | | // removing the batch file option + its value |