var isupper = func(c) { c >= `A` and c <= `Z` }
var islower = func(c) { c >= `a` and c <= `z` }
var toupper = func(c) { islower(c) ? c + `A` - `a` : c }
var tolower = func(c) { isupper(c) ? c + `a` - `A` : c }

var isletter = func(c) { isupper(c) or islower(c) }
var isdigit = func(c) { c >= `0` and c <= `9` }
var isalnum = func(c) { isletter(c) or isdigit(c) }
var isspace = func(c) { c == ` ` or c == `\t` or c == `\n` or c == `\r` }


##
# trim spaces at the left (lr < 0), at the right (lr > 0), or both (lr = 0)
#
var trim = func(s, lr = 0) {
	var l = 0;
	if (lr <= 0)
		for (; l < size(s); l += 1)
			if (!isspace(s[l]))
				break;
	var r = size(s) - 1;
	if (lr >= 0)
		for (; r >= 0; r -= 1)
			if (!isspace(s[r]))
				break;
	return r < l ? "" : substr(s, l, r - l + 1);
}


##
# return string converted to lower case letters
#
var lc = func(str) {
	var s = "";
	for (var i = 0; i < size(str); i += 1)
		s ~= chr(tolower(str[i]));
	return s;
}


##
# return string converted to upper case letters
#
var uc = func(str) {
	var s = "";
	for (var i = 0; i < size(str); i += 1)
		s ~= chr(toupper(str[i]));
	return s;
}


##
# case insensitive string compare and match functions
# (not very effective -- converting the array to be sorted
# first is faster)
#
var icmp = func(a, b) cmp(lc(a), lc(b));
var imatch = func(a, b) match(lc(a), lc(b));


##
# check if string <str> matches shell style pattern <patt>
#
# Rules:
# ?   stands for any single character
# *   stands for any number (including zero) of arbitrary characters
# \   escapes the next character and makes it stand for itself; that is:
#     \? stands for a question mark (not the "any single character" placeholder)
# []  stands for a group of characters:
#     [abc]      stands for letters a, b or c
#     [^abc]     stands for any character but a, b, and c
#     [1-4]      stands for digits 1 to 4 (1, 2, 3, 4)
#     [1-4-]     stands for digits 1 to 4, and the minus
#     [-1-4]     same as above
#     [1-3-6]    stands for digits 1 to 3, minus, and 6
#     [1-3-6-9]  stands for digits 1 to 3, minus, and 6 to 9
#     [][]       stands for the closing and the opening bracket (']' must be first!)
#     [^^]       stands for all characters but the caret symbol
#
# Note that a minus can't be a range delimiter, as in [a--b]
#
# Example:
#     string.match(name, "*[0-9].xml"); ... true if 'name' ends with digit followed by ".xml"
#
var match = func(str, patt) {
	var s = 0;
	for (var p = 0; p < size(patt) and s < size(str); ) {
		if (patt[p] == `\\`) {
			if ((p += 1) >= size(patt))
				return 0;  # pattern ends with backslash

		} elsif (patt[p] == `?`) {
			s += 1;
			p += 1;
			continue;

		} elsif (patt[p] == `*`) {
			for (; p < size(patt); p += 1)
				if (patt[p] != `*`)
					break;
			if (p >= size(patt))
				return 1;

			for (; s < size(str); s += 1)
				if (match(substr(str, s), substr(patt, p)))
					return 1;
			continue;

		} elsif (patt[p] == `[`) {
			setsize(var x = [], 256);
			var invert = 0;
			if ((p += 1) < size(patt) and patt[p] == `^`) {
				p += 1;
				invert = 1;
			}
			for (var i = 0; p < size(patt); p += 1) {
				if (patt[p] == `]` and i)
					break;
				x[patt[p]] = 1;
				i += 1;

				if (p + 2 < patt[p] and patt[p] != `-` and patt[p + 1] == `-`
						and patt[p + 2] != `]` and patt[p + 2] != `-`) {
					var from = patt[p];
					var to = patt[p += 2];
					for (var c = from; c <= to; c += 1)
						x[c] = 1;
				}
			}
			if (invert ? !!x[str[s]] : !x[str[s]])
				return 0;
			s += 1;
			p += 1;
			continue;
		}

		if (str[s] != patt[p])
			return 0;
		s += 1;
		p += 1;
	}
	return s == size(str) and p == size(patt);
}


##
# Removes superfluous slashes, empty and "." elements, expands
# all ".." elements, and turns all backslashes into slashes.
# The result will start with a slash if it started with a slash
# or backslash, it will end without slash. Should be applied on
# absolute property or file paths, otherwise ".." elements might
# be resolved wrongly.
#
var fixpath = func(path) {
	var d = "";
	for (var i = 0; i < size(path); i += 1)
		d ~= path[i] == `\\` ? "/" : chr(path[i]);

	var prefix = d[0] == `/` ? "/" : "";
	var stack = [];
	foreach (var e; split("/", d)) {
		if (e == "." or e == "")
			continue;
		elsif (e == "..")
			pop(stack);
		else
			append(stack, e);
	}
	if (!size(stack))
		return "/";
	path = stack[0];
	foreach (var s; subvec(stack, 1))
		path ~= "/" ~ s;
	return prefix ~ path;
}