find_help_tags(
    char_u	*arg,
    int		*num_matches,
    char_u	***matches,
    int		keep_lang)
{
    char_u	*s, *d;
    int		i;
    static char *(mtable[]) = {"*", "g*", "[*", "]*", ":*",
			       "/*", "/\\*", "\"*", "**",
			       "cpo-*", "/\\(\\)", "/\\%(\\)",
			       "?", ":?", "?<CR>", "g?", "g?g?", "g??",
			       "-?", "q?", "v_g?",
			       "/\\?", "/\\z(\\)", "\\=", ":s\\=",
			       "[count]", "[quotex]",
			       "[range]", ":[range]",
			       "[pattern]", "\\|", "\\%$",
			       "s/\\~", "s/\\U", "s/\\L",
			       "s/\\1", "s/\\2", "s/\\3", "s/\\9"};
    static char *(rtable[]) = {"star", "gstar", "[star", "]star", ":star",
			       "/star", "/\\\\star", "quotestar", "starstar",
			       "cpo-star", "/\\\\(\\\\)", "/\\\\%(\\\\)",
			       "?", ":?", "?<CR>", "g?", "g?g?", "g??",
			       "-?", "q?", "v_g?",
			       "/\\\\?", "/\\\\z(\\\\)", "\\\\=", ":s\\\\=",
			       "\\[count]", "\\[quotex]",
			       "\\[range]", ":\\[range]",
			       "\\[pattern]", "\\\\bar", "/\\\\%\\$",
			       "s/\\\\\\~", "s/\\\\U", "s/\\\\L",
			       "s/\\\\1", "s/\\\\2", "s/\\\\3", "s/\\\\9"};
    static char *(expr_table[]) = {"!=?", "!~?", "<=?", "<?", "==?", "=~?",
				">=?", ">?", "is?", "isnot?"};
    int flags;

    d = IObuff;		    /* assume IObuff is long enough! */

    if (STRNICMP(arg, "expr-", 5) == 0)
    {
	// When the string starting with "expr-" and containing '?' and matches
	// the table, it is taken literally.  Otherwise '?' is recognized as a
	// wildcard.
	for (i = (int)(sizeof(expr_table) / sizeof(char *)); --i >= 0; )
	    if (STRCMP(arg + 5, expr_table[i]) == 0)
	    {
		STRCPY(d, arg);
		break;
	    }
    }
    else
    {
	// Recognize a few exceptions to the rule.  Some strings that contain
	// '*' with "star".  Otherwise '*' is recognized as a wildcard.
	for (i = (int)(sizeof(mtable) / sizeof(char *)); --i >= 0; )
	    if (STRCMP(arg, mtable[i]) == 0)
	    {
		STRCPY(d, rtable[i]);
		break;
	    }
    }

    if (i < 0)	/* no match in table */
    {
	/* Replace "\S" with "/\\S", etc.  Otherwise every tag is matched.
	 * Also replace "\%^" and "\%(", they match every tag too.
	 * Also "\zs", "\z1", etc.
	 * Also "\@<", "\@=", "\@<=", etc.
	 * And also "\_$" and "\_^". */
	if (arg[0] == '\\'
		&& ((arg[1] != NUL && arg[2] == NUL)
		    || (vim_strchr((char_u *)"%_z@", arg[1]) != NULL
							   && arg[2] != NUL)))
	{
	    STRCPY(d, "/\\\\");
	    STRCPY(d + 3, arg + 1);
	    /* Check for "/\\_$", should be "/\\_\$" */
	    if (d[3] == '_' && d[4] == '$')
		STRCPY(d + 4, "\\$");
	}
	else
	{
	  /* Replace:
	   * "[:...:]" with "\[:...:]"
	   * "[++...]" with "\[++...]"
	   * "\{" with "\\{"		   -- matching "} \}"
	   */
	    if ((arg[0] == '[' && (arg[1] == ':'
			 || (arg[1] == '+' && arg[2] == '+')))
		    || (arg[0] == '\\' && arg[1] == '{'))
	      *d++ = '\\';

	  /*
	   * If tag starts with "('", skip the "(". Fixes CTRL-] on ('option'.
	   */
	  if (*arg == '(' && arg[1] == '\'')
	      arg++;
	  for (s = arg; *s; ++s)
	  {
	    /*
	     * Replace "|" with "bar" and '"' with "quote" to match the name of
	     * the tags for these commands.
	     * Replace "*" with ".*" and "?" with "." to match command line
	     * completion.
	     * Insert a backslash before '~', '$' and '.' to avoid their
	     * special meaning.
	     */
	    if (d - IObuff > IOSIZE - 10)	/* getting too long!? */
		break;
	    switch (*s)
	    {
		case '|':   STRCPY(d, "bar");
			    d += 3;
			    continue;
		case '"':   STRCPY(d, "quote");
			    d += 5;
			    continue;
		case '*':   *d++ = '.';
			    break;
		case '?':   *d++ = '.';
			    continue;
		case '$':
		case '.':
		case '~':   *d++ = '\\';
			    break;
	    }

	    /*
	     * Replace "^x" by "CTRL-X". Don't do this for "^_" to make
	     * ":help i_^_CTRL-D" work.
	     * Insert '-' before and after "CTRL-X" when applicable.
	     */
	    if (*s < ' ' || (*s == '^' && s[1] && (ASCII_ISALPHA(s[1])
			   || vim_strchr((char_u *)"?@[\\]^", s[1]) != NULL)))
	    {
		if (d > IObuff && d[-1] != '_' && d[-1] != '\\')
		    *d++ = '_';		/* prepend a '_' to make x_CTRL-x */
		STRCPY(d, "CTRL-");
		d += 5;
		if (*s < ' ')
		{
#ifdef EBCDIC
		    *d++ = CtrlChar(*s);
#else
		    *d++ = *s + '@';
#endif
		    if (d[-1] == '\\')
			*d++ = '\\';	/* double a backslash */
		}
		else
		    *d++ = *++s;
		if (s[1] != NUL && s[1] != '_')
		    *d++ = '_';		/* append a '_' */
		continue;
	    }
	    else if (*s == '^')		/* "^" or "CTRL-^" or "^_" */
		*d++ = '\\';

	    /*
	     * Insert a backslash before a backslash after a slash, for search
	     * pattern tags: "/\|" --> "/\\|".
	     */
	    else if (s[0] == '\\' && s[1] != '\\'
					       && *arg == '/' && s == arg + 1)
		*d++ = '\\';

	    /* "CTRL-\_" -> "CTRL-\\_" to avoid the special meaning of "\_" in
	     * "CTRL-\_CTRL-N" */
	    if (STRNICMP(s, "CTRL-\\_", 7) == 0)
	    {
		STRCPY(d, "CTRL-\\\\");
		d += 7;
		s += 6;
	    }

	    *d++ = *s;

	    /*
	     * If tag contains "({" or "([", tag terminates at the "(".
	     * This is for help on functions, e.g.: abs({expr}).
	     */
	    if (*s == '(' && (s[1] == '{' || s[1] =='['))
		break;

	    /*
	     * If tag starts with ', toss everything after a second '. Fixes
	     * CTRL-] on 'option'. (would include the trailing '.').
	     */
	    if (*s == '\'' && s > arg && *arg == '\'')
		break;
	    /* Also '{' and '}'. */
	    if (*s == '}' && s > arg && *arg == '{')
		break;
	  }
	  *d = NUL;

	  if (*IObuff == '`')
	  {
	      if (d > IObuff + 2 && d[-1] == '`')
	      {
		  /* remove the backticks from `command` */
		  mch_memmove(IObuff, IObuff + 1, STRLEN(IObuff));
		  d[-2] = NUL;
	      }
	      else if (d > IObuff + 3 && d[-2] == '`' && d[-1] == ',')
	      {
		  /* remove the backticks and comma from `command`, */
		  mch_memmove(IObuff, IObuff + 1, STRLEN(IObuff));
		  d[-3] = NUL;
	      }
	      else if (d > IObuff + 4 && d[-3] == '`'
					     && d[-2] == '\\' && d[-1] == '.')
	      {
		  /* remove the backticks and dot from `command`\. */
		  mch_memmove(IObuff, IObuff + 1, STRLEN(IObuff));
		  d[-4] = NUL;
	      }
	  }
	}
    }

    *matches = (char_u **)"";
    *num_matches = 0;
    flags = TAG_HELP | TAG_REGEXP | TAG_NAMES | TAG_VERBOSE;
    if (keep_lang)
	flags |= TAG_KEEP_LANG;
    if (find_tags(IObuff, num_matches, matches, flags, (int)MAXCOL, NULL) == OK
	    && *num_matches > 0)
    {
	/* Sort the matches found on the heuristic number that is after the
	 * tag name. */
	qsort((void *)*matches, (size_t)*num_matches,
					      sizeof(char_u *), help_compare);
	/* Delete more than TAG_MANY to reduce the size of the listing. */
	while (*num_matches > TAG_MANY)
	    vim_free((*matches)[--*num_matches]);
    }
    return OK;
}