ACC SHELL

Path : /usr/share/YaST2/modules/
File Upload :
Current File : //usr/share/YaST2/modules/String.ycp

/**
 * File:	modules/String.ycp
 * Package:	yast2
 * Summary:	String manipulation routines
 * Authors:	Michal Svec <msvec@suse.cz>
 *
 * $Id: String.ycp 57542 2009-06-11 16:48:30Z lslezak $
 */

{

module "String";
textdomain "base";

/**
 * Quote a string with 's
 *
 * More precisely it protects single quotes inside the string but does not
 * prepend or append single quotes.
 *
 * @param var unquoted string
 * @return quoted string
 * @example quote("a'b") -> "a'\''b"
 */
global define string Quote(string var) ``{
    if(var == nil || var == "") return "";
    return mergestring(splitstring(var, "'"), "'\\''");
}

/**
 * Unquote a string with 's (quoted with quote)
 * @param var quoted string
 * @return unquoted string
 * @see quote
 */
global define string UnQuote(string var) ``{
    if(var == nil || var == "") return "";
    y2debug("var=%1", var);
    while(regexpmatch(var, "'\\\\''")) {
	var = regexpsub(var, "(.*)'\\\\''(.*)", "\\1'\\2");
	y2debug("var=%1", var);
    }
    return var;
}

/**
 * Optional formatted text
 * @return sformat (f, s) if s is neither empty or nil, else ""
 */
global string OptFormat (string f, string s) {
    return (s == "" || s == nil)? "": sformat (f, s);
}

/**
 * Optional parenthesized text
 * @return " (Foo)" if Foo is neither empty or nil, else ""
 */
global string OptParens (string s) {
    return OptFormat (" (%1)", s);
}

/**
 * @param l a list of strings
 * @return only non-"" items
 */
global list<string> NonEmpty (list<string> l) {
    return filter (string i, l, ``( i != "" ));
}

/**
 * @param s \n-terminated items
 * @return the items as a list, with empty lines removed
 */
global list<string> NewlineItems (string s) {
    return NonEmpty (splitstring (s, "\n"));
}

/**
 * Return a pretty description of a byte count
 *
 * Return a pretty description of a byte count with required precision
 * and using B, kB, MB, GB or TB as unit as appropriate.
 *
 * Uses the current locale defined decimal separator
 * (i.e. the result is language dependant).
 *
 * @param bytes	size (e.g. free diskspace, memory size) in Bytes
 * @param precision number of fraction digits in output
 * @param omit_zeroes if true then do not add zeroes
 *	(useful for memory size - 128 MB RAM looks better than 128.00 MB RAM)
 * @return formatted string
 *
 * @example FormatSizeWithPrecision(128, 2, true) -> "128 B"
 * @example FormatSizeWithPrecision(4096, 2, true) -> "4 kB"
 * @example FormatSizeWithPrecision(4096, 2, false) -> "4.00 kB"
 * @example FormatSizeWithPrecision(1024*1024, 2, true) -> "1 MB"
 */
global define string FormatSizeWithPrecision(integer bytes, integer precision, boolean omit_zeroes) ``{

    if(bytes == nil) return "";

    const list units = [
	/* Byte abbreviated */
	_("B"),
	/* KiloByte abbreviated */
	_("kB"),
	/* MegaByte abbreviated */
	_("MB"),
	/* GigaByte abbreviated */
	_("GB"),
	/* TeraByte abbreviated */
	_("TB"),
    ];

    integer index = 0;
    float whole = tofloat(bytes);

    while((whole >= 1024.0 || whole <= -1024.0) && index + 1 < size(units)) {
	whole = whole / 1024.0;
	index = index + 1;
    }

    if (precision == nil || precision < 0 || (omit_zeroes == true && (whole - tointeger(whole) == 0.0)))
    {
	precision = 0;
    }

    return float::tolstring(whole, precision) + " " + units[index]:"";
}

/**
 * Return a pretty description of a byte count
 *
 * Return a pretty description of a byte count, with two fraction digits
 * and using B, kB, MB, GB or TB as unit as appropriate.
 *
 * Uses the current locale defined decimal separator
 * (i.e. the result is language dependant).
 *
 * @param bytes	size (e.g. free diskspace) in Bytes
 * @return formatted string
 *
 * @example FormatSize(23456767890) -> "223.70 MB"
 */
global define string FormatSize(integer bytes) ``{
    return FormatSizeWithPrecision(bytes, 2, false);
}

/**
 * Return a pretty description of a download rate
 *
 * Return a pretty description of a download rate, with two fraction digits
 * and using B/s, kB/s, MB/s, GB/s or TB/s as unit as appropriate.
 *
 * @param bytes_per_second download rate (in B/s)
 * @return formatted string
 *
 * @example FormatRate(6780) -> ""
 * @example FormatRate(0) -> ""
 * @example FormatRate(895321) -> ""
 */
global string FormatRate(integer bytes_per_second)
{
    // covert a number to download rate string
    // %1 is string - size in bytes, B, kB, MB, GB or TB
    return sformat(_("%1/s"), FormatSize(bytes_per_second));
}

/**
 * Add a download rate status to a message.
 *
 * Add the current and the average download rate to the message.
 *
 * @param text the message with %1 placeholder for the download rate string
 * @param avg_bps average download rate (in B/s)
 * @param curr_bps current download rate (in B/s)
 *
 * @return string formatted message
 */
global string FormatRateMessage(string text, integer avg_bps, integer curr_bps)
{
    string rate = "";

    if (curr_bps > 0)
    {
	rate = FormatRate(curr_bps);

	if (avg_bps > 0)
	{
	    // format download rate message: %1 = the current download rate (e.g. "242.6kB/s")
	    // %2 is the average download rate (e.g. "228.3kB/s")
	    // to translators: keep translation of "on average" as short as possible
	    rate = sformat(_("%1 (on average %2)"), rate, String::FormatRate(avg_bps));
	}
    }

    // add download rate to the downloading message
    // %1 is URL, %2 is formatted download rate, e.g. "242.6kB/s (avg. 228.3kB/s)"
    // in ncurses UI the strings are exchanged (%1 is the rate, %2 is URL)
    // due to limited space on the screen
    return sformat(text, rate);
}

/**
 * Format an integer number as (at least) two digits; use leading zeroes if
 * necessary.
 * @param x input
 * @return string number as two-digit string
 **/
string FormatTwoDigits( integer x )
{
    return x < 10 && x >= 0 ?
	sformat( "0%1", x ) :
	sformat(  "%1", x );
}


/**
 * Format an integer seconds value with min:sec or hours:min:sec
 * @param seconds time (in seconds)
 * @return string formatted string (empty for negative values)
 **/
global string FormatTime( integer seconds )
{
    if ( seconds < 0 )
	return "";

    if ( seconds < 3600 )	// Less than one hour
    {
	return sformat( "%1:%2", FormatTwoDigits( seconds / 60 ), FormatTwoDigits( seconds % 60 ) );
    }
    else	// More than one hour - we don't hope this will ever happen, but who knows?
    {
	integer hours = seconds / 3600;
	seconds = seconds % 3600;
	return sformat( "%1:%2:%3", hours, FormatTwoDigits( seconds / 60 ), FormatTwoDigits( seconds % 60 ) );
    }
}

/**
 * Remove blanks at begin and end of input string.
 * @param input string to be stripped
 * @return stripped string
 * @example CutBlanks("  any  input     ") -> "any  input"
 */
global define string CutBlanks(string input) ``{

    if(input == nil || size(input) < 1) return "";

    integer pos1 = findfirstnotof(input, " \t");
    if(pos1 == nil) return "";

    integer pos2 = findlastnotof(input, " \t");

    return substring(input, pos1, pos2 - pos1 + 1);
}

/**
 * Remove any leading zeros
 *
 * Remove any leading zeros that make tointeger inadvertently
 * assume an octal number (e.g. "09" -> "9", "0001" -> "1",
 * but "0" -> "0")
 *
 * @param input number that might contain leadig zero
 * @return string that has leading zeros removed
 */
global define string CutZeros(string input) ``{
    if(input == nil || size(input) < 1) return "";
    if(!regexpmatch(input, "^0.*")) return input;
    string output = regexpsub(input, "^0+(.*)$", "\\1");
    if(size(output) < 1) return "0";
    return output;
}

/**
 * Add spaces after the text to make it long enough
 *
 * Add spaces after the text to make it long enough. If the text is longer
 * than requested, no changes are made.
 *
 * @param text text to be padded
 * @param length requested length
 * @return padded text
 */
global define string Pad(string text, integer length) {
    if(text == nil) text = "";

    integer rest = length - size(text);
    string pad = "";
    while(rest > 0) {
	pad = pad + " ";
	rest = rest - 1;
    }

    return text + pad;
}

/**
 * Add zeros before the text to make it long enough.
 *
 * Add zeros before the text to make it long enough. If the text is longer
 * than requested, no changes are made.
 *
 * @param text text to be padded
 * @param length requested length
 * @return padded text
 */
global define string PadZeros(string text, integer length) {
    if(text == nil) text = "";

    integer rest = length - size(text);
    string pad = "";
    while(rest > 0) {
	pad = pad + "0";
	rest = rest - 1;
    }

    return pad + text;
}

/**
 * Parse string of values
 *
 * Parse string of values - split string to values, quoting and backslash sequences are supported
 * @param options Input string
 * @param parameters Parmeter used at parsing - map with keys:
 *"separator":<string> - value separator (default: " \t"),
 *"unique":<boolean> - result will not contain any duplicates, first occurance of the string is stored into output (default: false),
 *"interpret_backslash":<boolean> - convert backslash sequence into one character (e.g. "\\n" => "\n") (default: true)
 *"remove_whitespace":<boolean> - remove white spaces around values (default: true),
 * @return list<string> List of strings
 */
global define list<string> ParseOptions(string options, map parameters) ``{
	list<string> ret = [];

        // parsing options
	string separator = parameters["separator"]:" \t";
        boolean unique = parameters["unique"]:false;
	boolean interpret_backslash = parameters["interpret_backslash"]:true;
        boolean remove_whitespace = parameters["remove_whitespace"]:true;

	y2debug("Input: string: '%1', parameters: %2", options, parameters);
        y2debug("Used values: separator: '%1', unique: %2, remove_whitespace: %3",
	    separator, unique, remove_whitespace);

        if (options == nil)
	{
	    return [];
        }

        // two algorithms are used:
	// first is much faster, but only usable if string
        // doesn't contain any double qoute characters
	// and backslash sequences are not interpreted
        // second is more general, but of course slower

        if (findfirstof(options, "\"") == nil && interpret_backslash == false)
	{
	    // easy case - no qouting, don't interpres backslash sequences => use splitstring
	    list<string> values = splitstring(options, separator);

	    foreach (string v, values, ``{
		if (remove_whitespace == true)
		{
		    v = CutBlanks(v);
		}

		if (unique == true)
		{
		    if (!contains(ret, v)) ret = add(ret, v);
		}
		else
		{
		    ret = add(ret, v);
		}
	    });
	}
	else
	{
	    // quoting is used or backslash interpretation is enabled
	    // so it' not possible to split input
	    // parsing each character is needed - use finite automaton

	    // state
	    symbol state = `out_of_string;
	    // position in the input string
	    integer index = 0;
	    // parsed value - buffer
	    string str = "";

	    while(index < size(options))
	    {
		string character = substring(options, index, 1);

		y2debug("character: %1 state: %2 index: %3", character, state, index);

		// interpret backslash sequence
		if (character == "\\" && interpret_backslash == true)
		{
		    if (index + 1 < size(options))
		    {
			string nextcharacter = substring(options, index + 1, 1);
			index = index + 1;

			// backslah sequences
			map backslash_seq = $[
			    "a"	: "\a",	// alert
			    "b"	: "\b", // backspace
			    "e"	: "\e", // escape
			    "f"	: "\f", // FF
			    "n"	: "\n", // NL
			    "r"	: "\r", // CR
			    "t"	: "\t", // tab
			    "v"	: "\v", // vertical tab
			    "\\": "\\", // backslash
			];

			if (haskey(backslash_seq, nextcharacter) == true)
			{
			    character = backslash_seq[nextcharacter]:"DUMMY";
			}
			else
			{
			    if (nextcharacter != "\"")
			    {
				// ignore backslash in invalid backslash sequence
				character = nextcharacter;
			    }
			    else
			    {
				// backslash will be removed later,
				// double quote and escaped double quote have to different yet
				character = "\\\"";
			    }
			}

			y2debug("backslash sequence: '%1'", character);
		    }
		    else
		    {
			y2warning("Missing character after backslash (\\) at the end of string");
		    }
		}

		if (state == `out_of_string)
		{
		    // ignore separator or white space at the beginning of the string
		    if (issubstring(separator, character) == true || (remove_whitespace == true && (character == " " || character == "\t")) )
		    {
			index = index + 1;
			continue;
		    }
		    // start of a quoted string
		    else if (character == "\"")
		    {
			state = `in_quoted_string;
		    }
		    else
		    {
			// start of a string
			state = `in_string;

			if (character == "\\\"")
			{
			    str = "\"";
			}
			else
			{
			    str = character;
			}
		    }
		}

		// after double quoted string - handle non-separator chars after double quote
		else if (state == `in_quoted_string_after_dblqt)
		{
		    if (issubstring(separator, character) == true)
		    {
			if (unique == true)
			{
			    if (!contains(ret, str)) ret = add(ret, str);
			}
			else
			{
			    ret = add(ret, str);
			}

			str = "";
			state = `out_of_string;
		    }
		    else if (character == "\\\"")
		    {
			str = str + "\"";
		    }
		    else
		    {
			str = str + character;
		    }
		}
		else if (state == `in_quoted_string)
		{
		    if (character == "\"")
		    {
			// end of quoted string
			state = `in_quoted_string_after_dblqt;
		    }
		    else if (character == "\\\"")
		    {
			str = str + "\"";
		    }
		    else
		    {
			str = str + character;
		    }
		}
		else if (state == `in_string)
		{
		    if (issubstring(separator, character) == true)
		    {
			state = `out_of_string;

			if (remove_whitespace == true)
			{
			    str = CutBlanks(str);
			}

			if (unique == true)
			{
			    if (!contains(ret, str)) ret = add(ret, str);
			}
			else
			{
			    ret = add(ret, str);
			}

			str = "";
		    }
		    else if (character == "\\\"")
		    {
			str = str + "\"";
		    }
		    else
		    {
			str = str + character;
		    }
		}

		index = index + 1;
	    }

	    // error - still in quoted string
	    if (state == `in_quoted_string || state == `in_quoted_string_after_dblqt)
	    {
		if (state == `in_quoted_string)
		{
		    y2warning("Missing trainling double quote character(\") in input: '%1'", options);
		}

		if (unique == true)
		{
		    if (!contains(ret, str)) ret = add(ret, str);
		}
		else
		{
		    ret = add(ret, str);
		}
	    }

	    // process last string in the buffer
	    if (state == `in_string)
	    {
		if (remove_whitespace)
		{
		    str = CutBlanks(str);
		}

		if (unique == true)
		{
		    if (!contains(ret, str)) ret = add(ret, str);
		}
		else
		{
		    ret = add(ret, str);
		}
	    }
	}


	y2debug("Parsed values: %1", ret);

	return ret;
}

/**
 * Remove first or every match of given regular expression from a string
 *
 * (e.g. CutRegexMatch( "abcdef12ef34gh000", "[0-9]+", true ) -> "abcdefefgh",
 * CutRegexMatch( "abcdef12ef34gh000", "[0-9]+", false ) -> "abcdefef34gh000")
 *
 * @param input string that might occur regex
 * @param regex regular expression to search for, must not contain brackets
 * @param glob flag if only first or every occuring match should be removed
 * @return string that has matches removed
 */
global define string CutRegexMatch(string input, string regex, boolean glob) ``{
    if(input == nil || size(input) < 1) return "";
    string output = input;
    if( regexpmatch( output, regex ) )
	{
	list p = regexppos( output, regex );
	do
	    {
	    output = substring( output, 0, p[0]:0 ) +
	             substring( output, p[0]:0+p[1]:0 );
	    p = regexppos( output, regex );
	    }
	while( glob && size(p)>0 );
	}
    return output;
}

/**
 * Function for escaping (replacing) (HTML|XML...) tags with their
 * (HTML|XML...) meaning.
 *
 * Usable to present text "as is" in RichText.
 *
 * @param string	text to escape
 * @return string	escaped text
 */
global define string EscapeTags (string text) ``{
    text = mergestring(splitstring(text, "&"), "&amp;");
    text = mergestring(splitstring(text, "<"), "&lt;");
    text = mergestring(splitstring(text, ">"), "&gt;");

    return text;
}

/**
 * Shorthand for select (splitstring (s, separators), 0, "")
 * Useful now that the above produces a deprecation warning.
 * @param s string to be split
 * @param separators characters which delimit components
 * @return first component or ""
 */
global string FirstChunk (string s, string separators) {
    list <string> l = splitstring (s, separators);
    return l[0]:"";
}

// character sets, suitable for ValidChars

string cupper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
string clower = "abcdefghijklmnopqrstuvwxyz";
string calpha = cupper + clower;
string cdigit = "0123456789";
string cxdigit = cdigit + "ABCDEFabcdef";
string calnum = calpha + cdigit;
string cpunct = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";
string cgraph = calnum + cpunct;
string cspace = " \f\r\n\t\013";
string cprint = cspace + cgraph;

/**
 * The 26 uppercase ASCII letters
 */
global string CUpper () {
    return cupper;
}

/**
 * The 26 lowercase ASCII letters
 */
global string CLower () {
    return clower;
}

/**
 * The 52 upper and lowercase ASCII letters
 */
global string CAlpha () {
    return calpha;
}

/**
 * Digits: 0123456789
 */
global string CDigit () {
    return cdigit;
}

/**
 * Hexadecimal digits: 0123456789ABCDEFabcdef
 */
global string CXdigit () {
    return cxdigit;
}

/**
 * The 62 upper and lowercase ASCII letters and digits
 */
global string CAlnum () {
    return calnum;
}

/**
 * The ASCII printable non-blank non-alphanumeric characters
 */
global string CPunct () {
    return cpunct;
}

/**
 * Printable ASCII charcters except whitespace, 33-126
 */
global string CGraph () {
    return cgraph;
}

/**
 * ASCII whitespace: SPACE CR LF HT VT FF
 */
global string CSpace () {
    return cspace;
}

/**
 * Printable ASCII characters including whitespace
 */
global string CPrint () {
    return cprint;
}

/**
 * Characters valid in a filename (not pathname).
 * Naturally "/" is disallowed. Otherwise, the graphical ASCII
 * characters are allowed.
 * @return string for ValidChars
 */
global string ValidCharsFilename () {
    return deletechars (CGraph (), "/");
}


    // 64 characters is the base undeline length
    string base_underline = "----------------------------------------------------------------";

    /* - hidden for documentation -
     *
     * Local function for finding longest records in the table.
     *
     * @param	list <list <string> > table items
     * @return	list <integer> longest records by columns
     */
    list <integer> FindLongestRecords (list <list <string> > items) {
	list <integer> longest = [];

	// searching all rows
	foreach (list <string> row, items, {
	    // starting with column 0
	    integer col_counter = 0;
	    // testing all columns on the row
	    foreach (string col, row, {
		integer col_size = size(col);
		// found longer record for this column
		if (col_size>longest[col_counter]:-1) longest[col_counter] = col_size;
		// next column
		col_counter = col_counter + 1;
	    });
	});

	return longest;
    }

    /* - hidden for documentation -
     *
     * Local function creates table row.
     *
     * @param	list <string> row items
     * @param	list <integer> columns lengths
     * @param	integer record horizontal padding
     * @return	string padded table row
     */
    string CreateTableRow (list <string> row_items, list <integer> cols_lenghts, integer horizontal_padding) {
	string row = "";

	integer col_counter = 0;
	integer records_count = size(row_items) - 1;

	foreach (string record, row_items, {
	    integer padding = cols_lenghts[col_counter]:0;
	    if (col_counter<records_count) padding = padding + horizontal_padding;

	    row = row + String::Pad(record, padding);
	    col_counter = col_counter + 1;
	});

	return row;
    }

    /* - hidden for documentation -
     *
     * Local function returns underline string /length/ long.
     *
     * @param	integer length of underline
     * @return	string /length/ long underline
     */
    string CreateUnderline (integer length) {
	string underline = base_underline;
	while (size(underline)<length) {
	    underline = underline + base_underline;
	}
	underline = substring(underline, 0, length);

	return underline;
    }

    /* - hidden for documentation -
     *
     * Local function for creating header underline for table.
     * It uses maximal lengths of records defined in cols_lenghts.
     *
     * @param	list <integer> maximal lengths of records in columns
     * @param	integer horizontal padding of records
     * @return	string table header underline
     */
    string CreateTableHeaderUnderline (list <integer> cols_lenghts, integer horizontal_padding) {
	integer col_counter = 0;
	// count of added paddings
	integer records_count = size(cols_lenghts) - 1;
	// total length of underline
	integer total_size = 0;

	foreach (integer col_size, cols_lenghts, {
	    total_size = total_size + col_size;
	    // adding padding where necessary
	    if (col_counter<records_count) {
		total_size = total_size + horizontal_padding;
	    }
	    col_counter = col_counter + 1;
	});

	return CreateUnderline(total_size);
    }

    /**
     * Function creates text table without using HTML tags.
     * (Useful for commandline)
     * Undefined option uses the default one.
     *
     * @param	list <string> header
     * @param	list <list <string> > items
     * @param	map <string, string> options
     * @return	string table
     *
     * Header: [ "Id", "Configuration", "Device" ]
     * Items: [ [ "1", "aaa", "Samsung Calex" ], [ "2", "bbb", "Trivial Trinitron" ] ]
     * Possible Options: horizontal_padding (for columns), table_left_padding (for table)
     */
    global define string TextTable (list<string> header, list <list <string> > items, map <string, any> options) {

	integer current_horizontal_padding	= (integer) options["horizontal_padding"]:2;
	integer current_table_left_padding	= (integer) options["table_left_padding"]:4;

	list <integer> cols_lenghts = FindLongestRecords(add(items, header));

	// whole table is left-padded
	string table_left_padding = String::Pad("", current_table_left_padding);
	// the last row has no newline
	integer rows_count = size(items);
	string table = "";

	table = table + table_left_padding
	    + CreateTableRow(header, cols_lenghts, current_horizontal_padding) + "\n";
	table = table + table_left_padding
	    + CreateTableHeaderUnderline(cols_lenghts, current_horizontal_padding) + "\n";
	integer rows_counter = 1;
	foreach (list <string> row, items, {
	    table = table + table_left_padding
		+ CreateTableRow(row, cols_lenghts, current_horizontal_padding) +
		(rows_counter<rows_count ? "\n":"");
	    rows_counter = rows_counter + 1;
	});
	return table;
    }

    /**
     * Function returns underlined text header without using HTML tags.
     * (Useful for commandline)
     *
     * @param	string header line
     * @param	integer left padding
     * @return	string underlined header line
     */
    global define string UnderlinedHeader (string header_line, integer left_padding) {

	return
	    Pad("", left_padding) + header_line + "\n" +
	    Pad("", left_padding) + CreateUnderline(size(header_line));
    }



//////////////////////////////////////////
// sysconfig metadata related functions //
//////////////////////////////////////////

/**
 * Get metadata lines from input string
 * @param input Input string - complete comment of a sysconfig variable
 * @return list<string> Metadata lines in list
 */
global define list<string> GetMetaDataLines(string input) ``{
    if (input == nil || input == "")
    {
	return [];
    }

    list<string> lines = splitstring(input, "\n");
    return (filter(string line, lines, ``(regexpmatch(line, "^##.*"))));
}

/**
 * Get comment without metadata
 * @param input Input string - complete comment of a sysconfig variable
 * @return string Comment used as variable description
 */
global define string GetCommentLines(string input) ``{
    if (input == nil || input == "")
    {
	return "";
    }

    list<string> lines = splitstring(input, "\n");

    string ret = "";

    foreach(string line, lines, ``{
	    string com_line = regexpsub(line, "^#([^#].*)", "\\1");

	    if (com_line == nil)
	    {
		// add empty lines
		if (regexpmatch(line, "^#[ \t]*$") == true)
		{
		    ret = ret + "\n";
		}
	    }
	    else
	    {
		ret = ret + com_line + "\n";
	    }
	}
    );

    return ret;
}

/**
 * Parse metadata from a sysconfig comment
 * @param comment comment of a sysconfig variable (single line or multiline string)
 * @return map parsed metadata
 */
global define map<string, string> ParseSysconfigComment(string comment) ``{
    map<string, string> ret = $[];

    // get metadata part of comment
    list<string> metalines = GetMetaDataLines(comment);
    list<string> joined_multilines = [];
    string multiline = "";

    y2debug("metadata: %1", metalines);

    // join multi line metadata lines
    foreach(string metaline, metalines, ``{
	    if (substring(metaline, size(metaline) - 1, 1) != "\\")
	    {
		if (multiline != "")
		{
		    // this not first multiline so remove comment mark
		    string without_comment = regexpsub(metaline, "^##(.*)", "\\1");

		    if (without_comment != nil)
		    {
			metaline = without_comment;
		    }
		}
		joined_multilines = add(joined_multilines, multiline + metaline);
		multiline = "";
	    }
	    else
	    {
		string part = substring(metaline, 0, size(metaline) - 1);

		if (multiline != "")
		{
		    // this not first multiline so remove comment mark
		    string without_comment = regexpsub(part, "^##(.*)", "\\1");

		    if (without_comment != nil)
		    {
			part = without_comment;
		    }
		}

		// add line to the previous lines
		multiline = multiline + part;
	    }
	}
    );

    y2debug("metadata after multiline joining: %1", joined_multilines);

    // parse each metadata line
    foreach(string metaline, joined_multilines, ``{

	    /* Ignore lines with ### -- general comments */
	    if (regexpmatch(metaline, "^###"))
	    {
		return;
	    }

	    string meta = regexpsub(metaline, "^##[ \t]*(.*)", "\\1");

	    // split sting to the tag and value part
	    integer colon_pos = findfirstof(meta, ":");
	    string tag = "";
	    string val = "";

	    if (colon_pos == nil)
	    {
		// colon is missing
		tag = meta;
	    }
	    else
	    {
		tag = substring(meta, 0, colon_pos);

		if (size(meta) > colon_pos + 1)
		{
		    val = substring(meta, colon_pos + 1);
		}
	    }

	    // remove whitespaces from parts
	    tag = CutBlanks(tag);
	    val = CutBlanks(val);

	    y2debug("tag: %1 val: '%2'", tag, val);

	    // add tag and value to map if they are present in comment
	    if (tag != "")
	    {
		ret = (map<string, string>) add(ret, tag, val);
	    }
	    else
	    {
		// ignore separator lines
		if (!regexpmatch(metaline, "^#*$"))
		{
		    y2warning("Unknown metadata line: %1", metaline);
		}
	    }
	}
    );

    y2debug("parsed sysconfig comment: %1", ret);

    return ret;
}

/**
 * Replace substring in a string. All substrings source are replaced by string target.
 * @param s input string
 * @param source the string which will be replaced
 * @param target the new string which is used instead of source
 * @return string result
 */
global string Replace(string s, string source, string target) {
    if (s == nil)
    {
	return nil;
    }

    if (source == nil || source == "")
    {
	y2warning("invalid parameter source: %1", source);
	return s;
    }

    if (target == nil)
    {
	y2warning("invalid parameter target: %1", target);
	return s;
    }

    integer pos = find(s, source);
    while (pos >= 0) {
	string tmp = substring(s, 0, pos) + target;
	if (size(s) > pos + size(source))
	{
	    tmp = tmp + substring(s, pos + size(source));
	}

	s = tmp;

	pos = find(s, source);
    }

    return s;
}

/**
 * Returns text wrapped at defined margin. Very useful for translated strings
 * used for pop-up windows or dialogs where you can't know the width. It
 * controls the maximum width of the string so the text should allways fit into
 * the minimal ncurses window. If you expect some long words, such us URLs or
 * words with a hyphen inside, you can also set the additional split-characters
 * to "/-". Then the function can wrap the word also after these characters.
 * This function description was wrapped using the function String::WrapAt().
 *
 * @example String::WrapAt("Some very long text",30,"/-");
 *
 * @param string text to be wrapped
 * @param integer maximum width of the wrapped text
 * @param string additional split-characters such as "-" or "/"
 * @return string wrapped string
 */
global string WrapAt(string text, integer width, string split_string) {
    string new_string = "";
    integer avail = width;  // characters available in this line
    string lsep = "";  // set to "\n" when at the beginning of a new line
    string wsep = "";  // set to " " after words, unless at the beginning

    foreach (string word, splitstring(text, " \n"), {
	while (size(word) > 0) {
	// decide where to split the current word
	integer split_at = 0;
	if (size(word) <= width) {
	    split_at = size(word);
	} else {
	    split_at = findlastof(substring(word, 0, avail - size(wsep)),
					  " \n" + split_string);
	    if (split_at != nil) {
		split_at = split_at + 1;
	    } else {
		split_at = findlastof(substring(word, 0, width),
					      " \n" + split_string);
		if (split_at != nil) {
		    split_at = split_at + 1;
		} else {
		    split_at = avail - size(wsep);
		}
	    }
	}

	// decide whether it fits into the same line or must go on
	// a separate line
	if (size(wsep) + split_at > avail) {
	    if (size(new_string) > 0)
		new_string = new_string + "\n";
	    avail = width;
	    wsep = "";
	    lsep = "";
	}

	// add the next word or partial word
	new_string = new_string + lsep + wsep +
		     substring(word, 0, split_at);
	avail = avail - size(wsep) - split_at;
	wsep = ""; lsep = "";
	if (avail == 0) {
	    avail = width;
	    lsep = "\n";
	} else if (split_at == size(word)) {
	    wsep = " ";
	}
	word = substring(word, split_at);
	}
    });

    return new_string;
}

/**
 * Make a random base-36 number.
 * srandom should be called beforehand.
 * @param len string length
 * @return random string of 0-9 and a-z
 */
global string Random (integer len) {
    if (len <= 0)
    {
	return "";
    }
    string digits = cdigit + clower; // uses the character classes from above
    integer base = size (digits);
    integer max = 1;
    integer i = len;
    while (i > 0)
    {
	max = max * base;
	i = i - 1;
    }
    integer rnum = random (max);
    string ret = "";
    i = len;
    while (i > 0)
    {
	integer digit = rnum % base;
	rnum = rnum / base;
	ret = ret + substring (digits, digit, 1);
	i = i -1;
    }
    return ret;
}

/**
 * Format file name - truncate the middle part of the directory to fit to the reqested lenght.
 * Path elements in the middle of the string are replaced by ellipsis (...).
 * The result migth be longer that requested size if size of the last element
 * (with ellipsis) is longer than the requested size. If the requested size is greater than
 * size of the input then the string is not modified. The last part (file name) is never removed.
 *
 * @example FormatFilename("/really/very/long/file/name", 15) -> "/.../file/name"
 * @example FormatFilename("/really/very/long/file/name", 5) -> ".../name"
 * @example FormatFilename("/really/very/long/file/name", 100) -> "/really/very/long/file/name"
 *
 * @param file_path file name
 * @param len requested maximum lenght of the output
 * @return string Truncated file name
 */
global string FormatFilename(string file_path, integer len)
{
    if (size(file_path) <= len)
    {
	return file_path;
    }

    list<string> dir = splitstring(file_path, "/");
    string file = dir[size(dir) - 1]:"";
    dir = remove(dir, size(dir) - 1);

    // there is a slash at the end, add the directory name
    if (file == "")
    {
	file = dir[size(dir) - 1]:"" + "/";
	dir = remove(dir, size(dir) - 1);
    }

    if (size(mergestring(dir, "/")) <= 3 || size(dir) == 0)
    {
	// the path is short, replacing by ... cannot help
	return file_path;
    }

    string ret = "";

    do
    {
	// put the ellipsis in the middle of the path
	integer ellipsis = size(dir) / 2;

	// ellipsis - used to replace part of text to make it shorter
	// example: "/really/very/long/file/name", "/.../file/name")
	dir[ellipsis] = _("...");

	ret = mergestring(add(dir, file), "/");

	if (size(ret) > len)
	{
	    // still too long, remove the ellipsis and start a new iteration
	    dir = remove(dir, ellipsis);
	}
	else
	{
	    // the size is OK
	    break;
	}
    }
    while (size(dir) > 0);

    return ret;
}

/**
 * Remove a shortcut from a label, so that it can be inserted into help
 * to avoid risk of different translation of the label
 * @param label string a label possibly including a shortcut
 * @return string the label without the shortcut mark
 */
global string RemoveShortcut (string label) {
    string ret = label;
    if (regexpmatch (label, "^(.*[^&])?(&&)*&[[:alnum:]].*$"))
	ret = regexpsub (label, "^((.*[^&])?(&&)*)&([[:alnum:]].*)$", "\\1\\4");
    return ret;
}

/**
 * Checks whether string str starts with test.
 */
global boolean StartsWith(string str, string test)
{
    return search(str, test) == 0;
}

/* EOF */
}

ACC SHELL 2018