/*******************************************************************************
* Copyright (c) 2012 Arapiki Solutions Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* "Peter Smith <psmith@arapiki.com>" - initial API and
* implementation and/or initial documentation
*******************************************************************************/
package com.buildml.main;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import org.apache.commons.io.IOUtils;
import com.buildml.model.IActionMgr;
import com.buildml.model.IActionMgr.OperationType;
import com.buildml.model.IBuildStore;
import com.buildml.model.IFileMgr;
import com.buildml.model.IPackageMemberMgr;
import com.buildml.model.IPackageMemberMgr.PackageDesc;
import com.buildml.model.IPackageMgr;
import com.buildml.model.IPackageRootMgr;
import com.buildml.model.types.FileSet;
import com.buildml.model.types.ActionSet;
import com.buildml.utils.errors.ErrorCode;
import com.buildml.utils.print.PrintUtils;
import com.buildml.utils.string.ShellCommandUtils;
/**
* A collection of utility methods that can be used by any CLI Command code. This
* includes error reporting, command argument parsing, printing a FileSet and printing
* a ActionSet. These methods are all static, so no object is required for them to be
* invoked.
*
* Note: A number of these methods are used for command-line validation, and could
* potentially abort the program without returning. They should therefore only be
* used for command-line-based applications.
*
* @author "Peter Smith <psmith@arapiki.com>"
*/
public class CliUtils {
/*=====================================================================================*
* FIELDS/TYPES
*=====================================================================================*/
/** Enumeration for specifying how a action's command string should be displayed. */
public enum DisplayWidth {
/**
* As much as possible of the action's command line should be displayed on one line
* (truncate the remainder of the line if it's too long).
*/
ONE_LINE,
/**
* If the command line is too long, wrap it onto multiple lines, using our custom-set
* line width when splitting lines. Try to be intelligent about breaking lines at spaces,
* rather than in the middle of words.
*/
WRAPPED,
/**
* Don't do any command-line wrapping, and just let the terminal wrap the line
* if it's too long. The whole string will be shown, across multiple lines.
*/
NOT_WRAPPED
}
/**
* The number of columns (characters) per output line. This is used when wrapping text.
*/
private static int columnWidth = 80;
/**
* When validating command line arguments, this value is used to represent an unlimited
* number of arguments.
*/
public static final int ARGS_INFINITE = -1;
/**
* The number of characters to allow when printing the package details.
*/
public static final int PACKAGE_NAME_WIDTH = 25;
/*=====================================================================================*
* Public methods
*=====================================================================================*/
/**
* Given a user-specified string (from the command line), parse the specification into
* a FileSet data structure containing all the relevant files. The string specification
* is a colon-separated list of:
* <ol>
* <li> An absolute path name (starting with /), either a directory name or a file name.
* If the path is a directory, add all files and directories below that point in the tree.</li>
* <li>A path name starting with a "@root" - the same rules apply as for #1.</li>
* <li>A single file name, with one or more wildcard (*) characters. All files that match
* the name are added, no matter what their directory.</li>
* <li>A package spec, starting with %pkg, or the complement of a package, starting
* with %not-pkg.</li>
* </ol>
*
* @param fileMgr The FileMgr object that manages the files.
* @param pathSpecs A String of ":"-separated path specs (files, directories, or regular expressions).
* @return A FileSet containing all the files that were selected by the command-line arguments.
*/
public static FileSet getCmdLineFileSet(IFileMgr fileMgr, String pathSpecs) {
String pathSpecList[] = pathSpecs.split(":");
/* else populate a new FileSet */
FileSet result = new FileSet(fileMgr);
if (result.populateWithPaths(pathSpecList) != ErrorCode.OK) {
CliUtils.reportErrorAndExit("Invalid path filter provided");
}
return result;
}
/*-------------------------------------------------------------------------------------*/
/**
* Given a user-supplied set of command line arguments, parse those arguments and create
* a suitable ActionSet containing all the relevant actions that match the specification. The
* specification string is a colon-separated list of:
* <ol>
* <li>A specific action number, which will be added to the ActionSet.</li>
* <li>The action number followed by [/depth] to indicate that all actions in the sub tree,
* starting at the specified action and moving down the action tree "depth" level, should
* be added.</li>
* <li>If 'depth' is omitted (only the '/' is provided), all actions is the subtree are added
* (regardless of their depth).</li>
* <li>If the action number is prefixed by '-', the actions are removed from the ActionSet, rather
* than being added. The "/depth" and "/" suffix can be used to remove subactions as well.
* <li>The special syntax "%pkg/foo" means all actions in the package "foo".</li>
* <li>The special syntax "%not-pkg/foo" means all actions outside the package "foo".</li>
* </ol>
* @param actionMgr The ActionMgr object to query.
* @param actionSpecs The command line argument providing the action specification string.
* @return The ActionSet, as described by the input action specification.
*/
public static ActionSet getCmdLineActionSet(IActionMgr actionMgr, String actionSpecs) {
String actionSpecList[] = actionSpecs.split(":");
ActionSet result = new ActionSet(actionMgr);
if (result.populateWithActions(actionSpecList) != ErrorCode.OK) {
System.err.println("Error: Invalid action filter provided.");
System.exit(1);
}
return result;
}
/*-------------------------------------------------------------------------------------*/
/**
* Given a FileSet, display the files in that set in a pretty-printed format. This is
* used primarily for displaying the result of reports.
*
* @param outStream The PrintStream on which the output should be displayed.
* @param buildStore The BuildStore object containing the files to be listed.
* @param resultFileSet The set of files to be displayed (if null, show them all).
* @param filterFileSet If not-null, used to filter which paths from resultFileSet should be
* displayed (set to null to display everything).
* @param showRoots Indicates whether path roots should be displayed.
* @param showPkgs Indicates whether the package names should be displayed.
*/
public static void printFileSet(
PrintStream outStream, IBuildStore buildStore, FileSet resultFileSet,
FileSet filterFileSet, boolean showRoots, boolean showPkgs) {
IPackageRootMgr pkgRootMgr = buildStore.getPackageRootMgr();
/*
* This method uses recursion to traverse the VFS from the root to the leaves
* of the tree. It maintains a StringBuilder with the path encountered so far.
* That is, if the StringBuilder contains "/a/b/c" and the path "d" is encountered,
* then append "/d" to the StringBuilder to get "/a/b/c/d". Once we've finished
* traversing directory "d", we pop it off the StringBuilder and return to
* "/a/b/c/". This allows us to do a depth-first traversal of the VFS tree
* without doing more database access than we need to.
*
* The resultFileSet and filterFileSet work together to determine which paths are
* to be displayed. resultFileSet contains all the files from the relevant database
* query. On the other hand, filterFileSet is the list of files that have been
* selected by the user's command line argument (e.g. selecting a subdirectory, or
* selecting files that match a pattern, such as *.c).
*/
StringBuffer sb = new StringBuffer();
/* call the helper function to display each of our children */
printFileSetHelper(outStream, sb, buildStore, pkgRootMgr.getRootPath("root"),
resultFileSet, filterFileSet, showRoots, showPkgs);
}
/*-------------------------------------------------------------------------------------*/
/**
* Given a ActionSet, display the actions in that set in a pretty-printed format. To ensure
* that all actions are displayed, you should first call ActionSet.populateWithParents().
*
* @param outStream The PrintStream on which to display the output.
* @param buildStore The database containing the action information.
* @param resultActionSet The set of actions to be displayed (the results of some previous query).
* @param filterActionSet The set of actions to actually be displayed (for post-filtering the query results).
* @param outputFormat Mode for formatting the command strings.
* @param showPkgs Set to true if the package names should be shown.
*/
public static void printActionSet(
PrintStream outStream, IBuildStore buildStore,
ActionSet resultActionSet, ActionSet filterActionSet, DisplayWidth outputFormat,
boolean showPkgs) {
IActionMgr actionMgr = buildStore.getActionMgr();
/*
* We always start at the top root, even though we may only display a subset
* of the paths underneath that root.
*/
int topRoot = actionMgr.getRootAction("");
/* call the helper function to display each of our children */
Integer children[] = actionMgr.getChildren(topRoot);
for (int i = 0; i < children.length; i++) {
printActionSetHelper(outStream, buildStore, children[i],
resultActionSet, filterActionSet, outputFormat, showPkgs, 1);
}
}
/*-------------------------------------------------------------------------------------*/
/**
* Return the number of columns (characters) of output to be printed per report line.
* @return The number of columns.
*/
public static int getColumnWidth() {
return columnWidth;
}
/*-------------------------------------------------------------------------------------*/
/**
* Set the number of columns (characters) of output to be printed per report line. The
* minimum width is 40 characters. Any attempt to set a narrower width will revert to 40.
* @param newWidth The new column width to set.
*/
public static void setColumnWidth(int newWidth) {
if (newWidth < 40) {
newWidth = 40;
}
columnWidth = newWidth;
}
/*-------------------------------------------------------------------------------------*/
/**
* Parse the user-supplied package/scope string, and return the corresponding
* package ID and scope ID. The string should be in the format "package" or
* "package/scope". If "scope" is not provided (and scopeAllowed is true),
* "private" is assumed. If the input is invalid, display a meaningful error message
* and exit the program.
*
* This method may abort the whole program (never returning) if the input string
* is invalid.
*
* @param buildStore The IBuildStore containing the package information.
* @param pkgString The user-supplied input string (could be anything).
* @param scopeAllowed True if the input string is allowed to provide a scope name.
* @return An array of two integers. The first is the package's ID number,
* and the second is the scope's ID number.
*/
public static int[] parsePackageAndScope(
IBuildStore buildStore,
String pkgString,
boolean scopeAllowed) {
IPackageMgr pkgMgr = buildStore.getPackageMgr();
IPackageMemberMgr pkgMemberMgr = buildStore.getPackageMemberMgr();
String pkgName = null;
String scopeName = null;
/* check if there's a '/' in the string, to separate "package" from "scope" */
int slashIndex = pkgString.indexOf('/');
if (slashIndex != -1) {
pkgName = pkgString.substring(0, slashIndex);
scopeName = pkgString.substring(slashIndex + 1);
if (!scopeAllowed) {
CliUtils.reportErrorAndExit("Invalid syntax - '/" + scopeName + "' not allowed.");
}
}
/* else, there's no /, assume 'private' for the scope */
else {
pkgName = pkgString;
scopeName = "private";
}
/* compute the IDs */
int pkgId = pkgMgr.getId(pkgName);
int scopeId = pkgMemberMgr.getScopeId(scopeName);
if (pkgId == ErrorCode.NOT_FOUND) {
CliUtils.reportErrorAndExit("Unknown package: " + pkgName);
}
if (scopeId == ErrorCode.NOT_FOUND) {
CliUtils.reportErrorAndExit("Unknown scope name: " + scopeName);
}
return new int[]{ pkgId, scopeId };
}
/*-------------------------------------------------------------------------------------*/
/**
* Validation function to ensure that the number of arguments provided to a command
* is in range between minArgs and maxArgs.
*
* This method may abort the whole program (never returning) if the number of input
* arguments is invalid.
*
* @param cmdName The name of the command being executed.
* @param cmdArgs The array of input arguments.
* @param minArgs The minimum number of arguments required (0 or higher).
* @param maxArgs The maximum number of arguments required (0 or higher - possibly ARGS_INFINITE).
* @param message An error message to display if an invalid number of arguments is included.
*/
public static void validateArgs(String cmdName, String[] cmdArgs, int minArgs, int maxArgs,
String message) {
int actualArgs = cmdArgs.length;
/* too few arguments? */
if (actualArgs < minArgs) {
reportErrorAndExit("Too few arguments to " + cmdName + " - " + message);
}
/* too many arguments? */
else if ((maxArgs != ARGS_INFINITE) && (actualArgs > maxArgs)){
reportErrorAndExit("Too many arguments to " + cmdName + " - " + message);
}
}
/*-------------------------------------------------------------------------------------*/
/**
* Display an error message in a standard format, then exit the program with a non-zero
* error code. This method call never returns.
*
* @param message The message to be display. If null, just exit without displaying.
*/
public static void reportErrorAndExit(String message) {
if (message != null){
System.err.println("Error: " + message);
System.err.println(" Use bml -h for more help.");
}
System.exit(1);
}
/*-------------------------------------------------------------------------------------*/
/**
* Given that a command-line user may have specified the --read and --write command line
* options, return the appropriate OperationType value that can be used for querying the database.
*
* @param optionRead Set if the user provided the --read flag.
* @param optionWrite Set if the user provided the --write flag.
* @param optionModify Set if the user provided the --modify flag.
* @param optionDelete Set if the user provided the --delete flag.
* @return Either OP_UNSPECIFIED (search for either), OP_READ, or OP_WRITE
*/
public static OperationType getOperationType(boolean optionRead,
boolean optionWrite, boolean optionModify, boolean optionDelete) {
OperationType opType = OperationType.OP_UNSPECIFIED;
int optionsProvided = 0;
if (optionRead) {
opType = OperationType.OP_READ;
optionsProvided++;
}
if (optionWrite) {
opType = OperationType.OP_WRITE;
optionsProvided++;
}
if (optionModify) {
opType = OperationType.OP_MODIFIED;
optionsProvided++;
}
if (optionDelete) {
opType = OperationType.OP_DELETE;
optionsProvided++;
}
/* can't have more than one option provided at one time. */
if (optionsProvided > 1) {
System.err.println("Error: can't specify more than one of --read, --write, --modify or --delete.");
System.exit(-1);
}
return opType;
}
/*-------------------------------------------------------------------------------------*/
/**
* Generate a localized help message, for use when displaying online help.
* The provided message string will most likely contain a single line with
* an "#include" directive, which has the effect of pulling in another text
* file containing the main body of the message. This text file may also pull
* in other text files for inclusion.
*
* When searching for an included text file, the "messages/<lang%gt;/" directory
* is searched, where %lt;lang%gt; is a language specifier, such as "en".
*
* @param message Message to display, which most likely contains a #include directive.
* @return The full message string, which may be hundreds of lines long.
*/
public static String genLocalizedMessage(String message) {
/*
* We use recursion to pull all the messages (and possibly nested files)
* into the final string.
*/
StringBuffer sb = new StringBuffer();
genLocalizedMessageHelper("en", message, sb);
return sb.toString();
}
/*=====================================================================================*
* Private methods
*=====================================================================================*/
/**
* The CliUtils class can not be instantiated. Use the static methods only.
*/
private CliUtils() {
/* empty */
}
/*-------------------------------------------------------------------------------------*/
/**
* Helper method for displaying a path and all it's children, called exclusively by
* printFileSet().
*
* @param outStream The PrintStream on which to display paths.
* @param pathSoFar This path's parent path as a string, complete with trailing "/".
* @param buildStore The BuildStore in which these paths belong.
* @param thisPathId The path to display (assuming it's in the filesToShow FileSet).
* @param resultFileSet The set of files to be displayed (if null, show them all).
* @param filterFileSet If not-null, used to filter which paths from resultFileSet
* should be displayed (set to null to display everything).
* @param showRoots Whether to show path roots.
* @param showPkgs Whether to show the package names.
*/
private static void printFileSetHelper(
PrintStream outStream, StringBuffer pathSoFar, IBuildStore buildStore,
int thisPathId, FileSet resultFileSet, FileSet filterFileSet, boolean showRoots,
boolean showPkgs) {
IFileMgr fileMgr = buildStore.getFileMgr();
IPackageMgr pkgMgr = buildStore.getPackageMgr();
IPackageMemberMgr pkgMemberMgr = buildStore.getPackageMemberMgr();
IPackageRootMgr pkgRootMgr = buildStore.getPackageRootMgr();
/* StringBuilders for forming the package name and the root names */
StringBuilder pkgString = null;
StringBuilder rootString = null;
/* should this path be displayed? */
if (!shouldBeDisplayed(thisPathId, resultFileSet, filterFileSet)){
return;
}
/* fetch this path's name */
String baseName = fileMgr.getBaseName(thisPathId);
/* get this path's list of children */
Integer children[] = fileMgr.getChildPaths(thisPathId);
/*
* Figure out whether this path has attached roots.
*/
String rootNames[] = null;
if (showRoots) {
rootNames = pkgRootMgr.getRootsAtPath(thisPathId);
}
/*
* If we've been asked to display file packages, prepare the string to be printed.
*/
if (showPkgs) {
pkgString = new StringBuilder();
/* fetch the file's package and scope */
PackageDesc pkgAndScopeId = pkgMemberMgr.getPackageOfMember(IPackageMemberMgr.TYPE_FILE, thisPathId);
if (pkgAndScopeId == null) {
pkgString.append("Invalid file");
}
/* if valid, fetch the human-readable names */
else {
String pkgName = pkgMgr.getName(pkgAndScopeId.pkgId);
String scopeName = pkgMemberMgr.getScopeName(pkgAndScopeId.pkgScopeId);
/* if we can't fetch the text name of the package or scope... */
if (pkgName == null || scopeName == null) {
pkgString.append("Invalid package");
}
/* else, both names are valid, append them to the string */
else {
pkgString.append(pkgName);
pkgString.append(" - ");
pkgString.append(scopeName);
}
}
}
/*
* Does this path have a root (and we were asked to show roots)?
* If so, prepare the string to be printed.
*/
if ((rootNames != null) && (rootNames.length > 0)) {
rootString = new StringBuilder();
/* display a root name, or comma-separated root names */
rootString.append(" (");
for (int i = 0; i < rootNames.length; i++) {
if (i != 0) {
rootString.append(' ');
}
rootString.append('@');
rootString.append(rootNames[i]);
}
rootString.append(')');
}
/* show packages, if requested. Truncate to a fixed column width. */
if (pkgString != null) {
if (pkgString.length() > PACKAGE_NAME_WIDTH - 1) {
pkgString.setLength(PACKAGE_NAME_WIDTH - 1);
}
outStream.print(pkgString);
PrintUtils.indent(outStream, PACKAGE_NAME_WIDTH - pkgString.length());
}
/* Display this path, prefixed by the absolute pathSoFar */
outStream.print(pathSoFar);
outStream.print(baseName);
/* show roots, if requested */
if (rootString != null) {
outStream.print(rootString);
}
outStream.println();
/* if there are children, call ourselves recursively to display them */
if (children.length != 0) {
/* append this path onto the pathSoFar, since it'll become the pathSoFar for each child */
int pathSoFarLen = pathSoFar.length();
pathSoFar.append(baseName);
if (baseName.charAt(0) != '/') {
pathSoFar.append('/');
}
/* display each of the children */
for (int i = 0; i < children.length; i++) {
printFileSetHelper(outStream, pathSoFar, buildStore, children[i],
resultFileSet, filterFileSet, showRoots, showPkgs);
}
/* remove our base name from the pathSoFar, so our caller sees the correct value again */
pathSoFar.setLength(pathSoFarLen);
}
}
/*-------------------------------------------------------------------------------------*/
/**
* A helper method, called exclusively by printActionSet(). This method calls itself recursively
* as it traverses the ActionSet's tree structure.
*
* @param outStream The PrintStream on which to display the output.
* @param buildStore The database containing file, action and package information.
* @param actionId The ID of the action we're currently displaying (at this level of recursion).
* @param resultActionSet The full set of actions to be displayed (the result of some previous query).
* @param filterActionSet The set of actions to actually be displayed (for post-filtering the query results).
* @param outputFormat The way in which the actions should be formatted.
* @param showPkgs Set to true if we should display package names.
* @param indentLevel The number of spaces to indent this action by (at this recursion level).
*/
private static void printActionSetHelper(PrintStream outStream, IBuildStore buildStore,
int actionId, ActionSet resultActionSet, ActionSet filterActionSet,
DisplayWidth outputFormat, boolean showPkgs, int indentLevel) {
IActionMgr actionMgr = buildStore.getActionMgr();
IFileMgr fileMgr = buildStore.getFileMgr();
IPackageMgr pkgMgr = buildStore.getPackageMgr();
IPackageMemberMgr pkgMemberMgr = buildStore.getPackageMemberMgr();
/*
* Display the current action, at the appropriate indentation level. The format is:
*
* - Action 1 (/home/psmith/t/cvs-1.11.23)
* if test ! -f config.h; then rm -f stamp-h1; emake stamp-h1; else :;
*
* -- Action 2 (/home/psmith/t/cvs-1.11.23)
* failcom='exit 1'; for f in x $MAKEFLAGS; do case $f in *=* | --[!k]*);; \
*
* Where Action 1 is the parent of Action 2.
*/
/* is this action in the ActionSet to be printed? If not, terminate recursion */
if (! (((resultActionSet == null) || (resultActionSet.isMember(actionId))) &&
((filterActionSet == null) || (filterActionSet.isMember(actionId))))) {
return;
}
/*
* Fetch the action's command string (if there is one). It can either be
* in short format (on a single line), or a full string (possibly multiple lines)
*/
String command = (String) actionMgr.getSlotValue(actionId, IActionMgr.COMMAND_SLOT_ID);
if (command == null) {
command = "<unknown command>";
}
else if (outputFormat == DisplayWidth.ONE_LINE) {
command = ShellCommandUtils.getCommandSummary(command, getColumnWidth() - indentLevel - 3);
}
/* fetch the name of the directory the action was executed in */
int actionDirId = (Integer) actionMgr.getSlotValue(actionId, IActionMgr.DIRECTORY_SLOT_ID);
String actionDirName = fileMgr.getPathName(actionDirId);
/* display the correct number of "-" characters */
for (int i = 0; i != indentLevel; i++) {
outStream.append('-');
}
outStream.print(" Action " + actionId + " (" + actionDirName);
/* if requested, display the action's package name */
if (showPkgs) {
PackageDesc pkg = pkgMemberMgr.getPackageOfMember(IPackageMemberMgr.TYPE_ACTION, actionId);
if (pkg == null) {
outStream.print(" - Invalid action");
} else {
String pkgName = pkgMgr.getName(pkg.pkgId);
if (pkgName == null) {
outStream.print(" - Invalid package");
} else {
outStream.print(" - " + pkgName);
}
}
}
outStream.println(")");
/* display the action's command string. Each line must be indented appropriately */
if (outputFormat != DisplayWidth.NOT_WRAPPED) {
PrintUtils.indentAndWrap(outStream, command, indentLevel + 3, getColumnWidth());
outStream.println();
} else {
outStream.println(command);
}
/* recursively call ourselves to display each of our children */
Integer children[] = actionMgr.getChildren(actionId);
for (int i = 0; i < children.length; i++) {
printActionSetHelper(outStream, buildStore, children[i],
resultActionSet, filterActionSet, outputFormat, showPkgs, indentLevel + 1);
}
}
/*-------------------------------------------------------------------------------------*/
/**
* Determine whether this path is in the set of paths to be displayed. That is, it's
* in the resultFileSet as well as being part of filterFileSet.
*
* @param thisPathId The ID of the path we might want to display.
* @param resultFileSet The set of paths in the result set.
* @param filterFileSet The set of paths in the filter set.
* @return Whether or not the path should be displayed.
*/
private static boolean shouldBeDisplayed(int thisPathId,
FileSet resultFileSet, FileSet filterFileSet) {
return ((resultFileSet == null) || (resultFileSet.isMember(thisPathId))) &&
((filterFileSet == null) || (filterFileSet.isMember(thisPathId)));
}
/*-------------------------------------------------------------------------------------*/
/**
* A helper function for genLocalizedMessage, used for recursion.
*
* @param lang The language to localize into (e.g. "en" or "fr").
* @param message The message to be displayed (possibly including #include).
* @param sb The StringBuffer we'll use to build up the final string.
*/
private static void genLocalizedMessageHelper(
String lang,
String message,
StringBuffer sb) {
/* tokenize the string, and handle each line separately */
String [] lines = message.split("\n");
for (int i = 0; i < lines.length; i++) {
String thisLine = lines[i];
/*
* If this line contains an #include directive, read the file content, then
* call ourselves recursively to process the content.
*/
if (thisLine.matches("#include .*")){
String includeLine[] = thisLine.split(" ");
/* try to open the file (resource) as a stream */
String fileName = "messages/" + lang + "/" + includeLine[1];
InputStream inStream = ClassLoader.getSystemResourceAsStream(fileName);
if (inStream == null) {
sb.append("<missing include: " + fileName + ">\n");
}
/* read the stream into a string, then recursively process it */
else {
try {
String content = IOUtils.toString(inStream, "UTF-8");
genLocalizedMessageHelper(lang, content, sb);
} catch (IOException e1) {
sb.append("<invalid include: " + fileName + ">\n");
}
try {
inStream.close();
} catch (IOException e) {
/* nothing */
}
}
}
/* no #include directive, so just include the line verbatim */
else {
sb.append(lines[i]);
sb.append('\n');
}
}
}
/*-------------------------------------------------------------------------------------*/
}