read_line(char *buf, size_t size)
{
	size_t len ;
	bool eof = false;
	size_t config_id_len;
	char *buf_start;
	bool rev_cmp;
	size_t ofs;
	bool recheck;
	static def_t *def = NULL;
	static char *next_ptr = NULL;
	bool multiline_param_def = false;
	char *end;
	static char *line_residue = NULL;
	size_t skip;
	char *p;

	config_id_len = config_id ? strlen(config_id) : 0;
	do {
		if (line_residue) {
			strcpy(buf, line_residue);
			FREE(line_residue);
			line_residue = NULL;
		}
		else if (next_ptr) {
			/* We are expanding a multiline parameter, so copy next line */
			end = strchr(next_ptr, DEF_LINE_END[0]);
			if (!end) {
				strcpy(buf, next_ptr);
				if (!LIST_ISEMPTY(multiline_stack)) {
					next_ptr = LIST_TAIL_DATA(multiline_stack);
					list_remove(multiline_stack, multiline_stack->tail);
				}
				else
					next_ptr = NULL;
			} else {
				strncpy(buf, next_ptr, (size_t)(end - next_ptr));
				buf[end - next_ptr] = '\0';
				next_ptr = end + 1;
			}
		}
		else if (!LIST_ISEMPTY(seq_list)) {
			seq_t *seq = LIST_TAIL_DATA(seq_list);
			char val[12];
			snprintf(val, sizeof(val), "%d", seq->next);
#ifdef PARSER_DEBUG
			log_message(LOG_INFO, "Processing seq %d of %s for '%s'",  seq->next, seq->var, seq->text);
#endif
			set_definition(seq->var, val);
			strcpy(buf, seq->text);
			seq->next += seq->step;
			if ((seq->step > 0 && seq->next > seq->last) ||
			    (seq->step < 0 && seq->next < seq->last)) {
#ifdef PARSER_DEBUG
				log_message(LOG_INFO, "Removing seq %s for '%s'", seq->var, seq->text);
#endif
				list_remove(seq_list, seq_list->tail);
			}
		}
		else {
retry:
			if (!fgets(buf, (int)size, current_stream))
			{
				eof = true;
				buf[0] = '\0';
				break;
			}

			/* Check if we have read the end of a line */
			len = strlen(buf);
			if (buf[0] && buf[len-1] == '\n')
				current_file_line_no++;

			/* Remove end of line chars */
			while (len && (buf[len-1] == '\n' || buf[len-1] == '\r'))
				len--;

			/* Skip blank lines */
			if (!len)
				goto retry;

			buf[len] = '\0';

			decomment(buf);
		}

		len = strlen(buf);

		/* Handle multi-line definitions */
		if (multiline_param_def) {
			/* Remove trailing whitespace */
			if (len && buf[len-1] == '\\') {
				len--;
				while (len >= 1 && isblank(buf[len - 1]))
					len--;
				buf[len++] = DEF_LINE_END[0];
			} else {
				multiline_param_def = false;
				if (!def->value_len)
					def->multiline = false;
			}

			/* Don't add blank lines */
			if (len >= 2 ||
			    (len && !multiline_param_def)) {
				/* Add the line to the definition */
				def->value = REALLOC(def->value, def->value_len + len + 1);
				strncpy(def->value + def->value_len, buf, len);
				def->value_len += len;
				def->value[def->value_len] = '\0';
			}

			buf[0] = '\0';
			continue;
		}

		if (len == 0)
			continue;

		recheck = false;
		do {
			if (buf[0] == '@') {
				/* If the line starts '@', check the following word matches the system id.
				   @^ reverses the sense of the match */
				if (buf[1] == '^') {
					rev_cmp = true;
					ofs = 2;
				} else {
					rev_cmp = false;
					ofs = 1;
				}

				/* We need something after the system_id */
				if (!(buf_start = strpbrk(buf + ofs, " \t"))) {
					buf[0] = '\0';
					break;
				}

				/* Check if config_id matches/doesn't match as appropriate */
				if ((!config_id ||
				     (size_t)(buf_start - (buf + ofs)) != config_id_len ||
				     strncmp(buf + ofs, config_id, config_id_len)) != rev_cmp) {
					buf[0] = '\0';
					break;
				}

				/* Remove the @config_id from start of line */
				buf_start += strspn(buf_start, " \t");
				len -= (buf_start - buf);
				memmove(buf, buf_start, len + 1);
			}

			if (buf[0] == '$' && (def = check_definition(buf))) {
				/* check_definition() saves the definition */
				if (def->multiline)
					multiline_param_def = true;
				buf[0] = '\0';
				break;
			}

			if (buf[0] == '~')
				break;

			if (!LIST_ISEMPTY(defs) && (p = strchr(buf, '$'))) {
				if (!replace_param(buf, size, &next_ptr)) {
					/* If nothing has changed, we don't need to do any more processing */
					break;
				}

				if (buf[0] == '@')
					recheck = true;
				if (strchr(buf, '$'))
					recheck = true;
			}
		} while (recheck);
	} while (buf[0] == '\0' || check_include(buf));

	/* Search for BOB[0] or EOB[0] not in "" */
	if (buf[0]) {
		p = buf;
		if (p[0] != BOB[0] && p[0] != EOB[0]) {
			while ((p = strpbrk(p, BOB EOB "\""))) {
				if (*p != '"')
					break;

				/* Skip over anything in ""s */
				if (!(p = strchr(p + 1, '"')))
					break;

				p++;
			}
		}

		if (p && (p[0] == BOB[0] || p[0] == EOB[0])) {
			if (p == buf)
				skip = strspn(p + 1, " \t") + 1;
			else
				skip = 0;

			if (p[skip]) {
				/* Skip trailing whitespace */
				len = strlen(p + skip);
				while (len && (p[skip+len-1] == ' ' || p[skip+len-1] == '\t'))
					len--;
				line_residue = MALLOC(len + 1);
				p[skip+len] = '\0';
				strcpy(line_residue, p + skip);
				p[skip] = '\0';
			}
		}

		/* Skip trailing whitespace */
		len = strlen(buf);
		while (len && (buf[len-1] == ' ' || buf[len-1] == '\t'))
			len--;
		buf[len] = '\0';

		/* Check that we haven't got too many '}'s */
		if (!strcmp(buf, BOB))
			block_depth++;
		else if (!strcmp(buf, EOB)) {
			if (--block_depth < 0) {
				report_config_error(CONFIG_UNEXPECTED_EOB, "Extra '}' found");
				block_depth = 0;
			}
		}
	}

#ifdef PARSER_DEBUG
	log_message(LOG_INFO, "read_line(%d): '%s'", block_depth, buf);
#endif

	return !eof;
}