Separate out the filename completion functions from the readline() code.
Pass in loads of parameters instead of relying on shed-loads of global variables to modify the behaviour. The filename completion code can now be enabled by code that uses el_gets(). (eg /bin/sh)
This commit is contained in:
parent
9a563a9ab1
commit
41a59814ed
|
@ -1,4 +1,4 @@
|
|||
# $NetBSD: Makefile,v 1.29 2003/12/05 13:37:48 lukem Exp $
|
||||
# $NetBSD: Makefile,v 1.30 2005/05/07 16:01:25 dsl Exp $
|
||||
# @(#)Makefile 8.1 (Berkeley) 6/4/93
|
||||
|
||||
USE_SHLIBDIR= yes
|
||||
|
@ -6,7 +6,8 @@ USE_SHLIBDIR= yes
|
|||
WARNS= 3
|
||||
LIB= edit
|
||||
|
||||
OSRCS= chared.c common.c el.c emacs.c fcns.c help.c hist.c key.c map.c \
|
||||
OSRCS= chared.c common.c el.c emacs.c fcns.c filecomplete.c help.c hist.c \
|
||||
key.c map.c \
|
||||
parse.c prompt.c read.c refresh.c search.c sig.c term.c tty.c vi.c
|
||||
|
||||
MAN= editline.3 editrc.5
|
||||
|
|
|
@ -0,0 +1,529 @@
|
|||
/* $NetBSD: filecomplete.c,v 1.1 2005/05/07 16:01:25 dsl Exp $ */
|
||||
|
||||
/*-
|
||||
* Copyright (c) 1997 The NetBSD Foundation, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This code is derived from software contributed to The NetBSD Foundation
|
||||
* by Jaromir Dolecek.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions
|
||||
* are met:
|
||||
* 1. Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* 3. All advertising materials mentioning features or use of this software
|
||||
* must display the following acknowledgement:
|
||||
* This product includes software developed by the NetBSD
|
||||
* Foundation, Inc. and its contributors.
|
||||
* 4. Neither the name of The NetBSD Foundation nor the names of its
|
||||
* contributors may be used to endorse or promote products derived
|
||||
* from this software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
|
||||
* ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
|
||||
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
|
||||
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
* POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
#if !defined(lint) && !defined(SCCSID)
|
||||
__RCSID("$NetBSD: filecomplete.c,v 1.1 2005/05/07 16:01:25 dsl Exp $");
|
||||
#endif /* not lint && not SCCSID */
|
||||
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
#include <stdio.h>
|
||||
#include <dirent.h>
|
||||
#include <string.h>
|
||||
#include <pwd.h>
|
||||
#include <ctype.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#include <limits.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#ifdef HAVE_VIS_H
|
||||
#include <vis.h>
|
||||
#else
|
||||
#include "np/vis.h"
|
||||
#endif
|
||||
#ifdef HAVE_ALLOCA_H
|
||||
#include <alloca.h>
|
||||
#endif
|
||||
#include "el.h"
|
||||
#include "fcns.h" /* for EL_NUM_FCNS */
|
||||
#include "histedit.h"
|
||||
#include "filecomplete.h"
|
||||
|
||||
static char break_chars[] = { ' ', '\t', '\n', '"', '\\', '\'', '`', '@', '$',
|
||||
'>', '<', '=', ';', '|', '&', '{', '(', '\0' };
|
||||
|
||||
|
||||
/********************************/
|
||||
/* completion functions */
|
||||
|
||||
/*
|
||||
* does tilde expansion of strings of type ``~user/foo''
|
||||
* if ``user'' isn't valid user name or ``txt'' doesn't start
|
||||
* w/ '~', returns pointer to strdup()ed copy of ``txt''
|
||||
*
|
||||
* it's callers's responsibility to free() returned string
|
||||
*/
|
||||
char *
|
||||
tilde_expand(char *txt)
|
||||
{
|
||||
struct passwd pwres, *pass;
|
||||
char *temp;
|
||||
size_t len = 0;
|
||||
char pwbuf[1024];
|
||||
|
||||
if (txt[0] != '~')
|
||||
return (strdup(txt));
|
||||
|
||||
temp = strchr(txt + 1, '/');
|
||||
if (temp == NULL) {
|
||||
temp = strdup(txt + 1);
|
||||
if (temp == NULL)
|
||||
return NULL;
|
||||
} else {
|
||||
len = temp - txt + 1; /* text until string after slash */
|
||||
temp = malloc(len);
|
||||
if (temp == NULL)
|
||||
return NULL;
|
||||
(void)strncpy(temp, txt + 1, len - 2);
|
||||
temp[len - 2] = '\0';
|
||||
}
|
||||
if (getpwnam_r(temp, &pwres, pwbuf, sizeof(pwbuf), &pass) != 0)
|
||||
pass = NULL;
|
||||
free(temp); /* value no more needed */
|
||||
if (pass == NULL)
|
||||
return (strdup(txt));
|
||||
|
||||
/* update pointer txt to point at string immedially following */
|
||||
/* first slash */
|
||||
txt += len;
|
||||
|
||||
temp = malloc(strlen(pass->pw_dir) + 1 + strlen(txt) + 1);
|
||||
if (temp == NULL)
|
||||
return NULL;
|
||||
(void)sprintf(temp, "%s/%s", pass->pw_dir, txt);
|
||||
|
||||
return (temp);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* return first found file name starting by the ``text'' or NULL if no
|
||||
* such file can be found
|
||||
* value of ``state'' is ignored
|
||||
*
|
||||
* it's caller's responsibility to free returned string
|
||||
*/
|
||||
static char *
|
||||
filename_completion_function(const char *text, int state)
|
||||
{
|
||||
static DIR *dir = NULL;
|
||||
static char *filename = NULL, *dirname = NULL;
|
||||
static size_t filename_len = 0;
|
||||
struct dirent *entry;
|
||||
char *temp;
|
||||
size_t len;
|
||||
|
||||
if (state == 0 || dir == NULL) {
|
||||
temp = strrchr(text, '/');
|
||||
if (temp) {
|
||||
char *nptr;
|
||||
temp++;
|
||||
nptr = realloc(filename, strlen(temp) + 1);
|
||||
if (nptr == NULL) {
|
||||
free(filename);
|
||||
return NULL;
|
||||
}
|
||||
filename = nptr;
|
||||
(void)strcpy(filename, temp);
|
||||
len = temp - text; /* including last slash */
|
||||
nptr = realloc(dirname, len + 1);
|
||||
if (nptr == NULL) {
|
||||
free(filename);
|
||||
return NULL;
|
||||
}
|
||||
dirname = nptr;
|
||||
(void)strncpy(dirname, text, len);
|
||||
dirname[len] = '\0';
|
||||
} else {
|
||||
if (*text == 0)
|
||||
filename = NULL;
|
||||
else {
|
||||
filename = strdup(text);
|
||||
if (filename == NULL)
|
||||
return NULL;
|
||||
}
|
||||
dirname = NULL;
|
||||
}
|
||||
|
||||
/* support for ``~user'' syntax */
|
||||
if (dirname && *dirname == '~') {
|
||||
char *nptr;
|
||||
temp = tilde_expand(dirname);
|
||||
if (temp == NULL)
|
||||
return NULL;
|
||||
nptr = realloc(dirname, strlen(temp) + 1);
|
||||
if (nptr == NULL) {
|
||||
free(dirname);
|
||||
return NULL;
|
||||
}
|
||||
dirname = nptr;
|
||||
(void)strcpy(dirname, temp); /* safe */
|
||||
free(temp); /* no longer needed */
|
||||
}
|
||||
/* will be used in cycle */
|
||||
filename_len = filename ? strlen(filename) : 0;
|
||||
|
||||
if (dir != NULL) {
|
||||
(void)closedir(dir);
|
||||
dir = NULL;
|
||||
}
|
||||
dir = opendir(dirname ? dirname : ".");
|
||||
if (!dir)
|
||||
return (NULL); /* cannot open the directory */
|
||||
}
|
||||
|
||||
/* find the match */
|
||||
while ((entry = readdir(dir)) != NULL) {
|
||||
/* skip . and .. */
|
||||
if (entry->d_name[0] == '.' && (!entry->d_name[1]
|
||||
|| (entry->d_name[1] == '.' && !entry->d_name[2])))
|
||||
continue;
|
||||
if (filename_len == 0)
|
||||
break;
|
||||
/* otherwise, get first entry where first */
|
||||
/* filename_len characters are equal */
|
||||
if (entry->d_name[0] == filename[0]
|
||||
#if defined(__SVR4) || defined(__linux__)
|
||||
&& strlen(entry->d_name) >= filename_len
|
||||
#else
|
||||
&& entry->d_namlen >= filename_len
|
||||
#endif
|
||||
&& strncmp(entry->d_name, filename,
|
||||
filename_len) == 0)
|
||||
break;
|
||||
}
|
||||
|
||||
if (entry) { /* match found */
|
||||
|
||||
struct stat stbuf;
|
||||
#if defined(__SVR4) || defined(__linux__)
|
||||
len = strlen(entry->d_name) +
|
||||
#else
|
||||
len = entry->d_namlen +
|
||||
#endif
|
||||
((dirname) ? strlen(dirname) : 0) + 1 + 1;
|
||||
temp = malloc(len);
|
||||
if (temp == NULL)
|
||||
return NULL;
|
||||
(void)sprintf(temp, "%s%s",
|
||||
dirname ? dirname : "", entry->d_name); /* safe */
|
||||
|
||||
/* test, if it's directory */
|
||||
if (stat(temp, &stbuf) == 0 && S_ISDIR(stbuf.st_mode))
|
||||
strcat(temp, "/"); /* safe */
|
||||
} else {
|
||||
(void)closedir(dir);
|
||||
dir = NULL;
|
||||
temp = NULL;
|
||||
}
|
||||
|
||||
return (temp);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* returns list of completions for text given
|
||||
*/
|
||||
static char **
|
||||
completion_matches(const char *text, char *(*genfunc)(const char *, int))
|
||||
{
|
||||
char **match_list = NULL, *retstr, *prevstr;
|
||||
size_t match_list_len, max_equal, which, i;
|
||||
size_t matches;
|
||||
|
||||
matches = 0;
|
||||
match_list_len = 1;
|
||||
while ((retstr = (*genfunc) (text, (int)matches)) != NULL) {
|
||||
/* allow for list terminator here */
|
||||
if (matches + 3 >= match_list_len) {
|
||||
char **nmatch_list;
|
||||
while (matches + 3 >= match_list_len)
|
||||
match_list_len <<= 1;
|
||||
nmatch_list = realloc(match_list,
|
||||
match_list_len * sizeof(char *));
|
||||
if (nmatch_list == NULL) {
|
||||
free(match_list);
|
||||
return NULL;
|
||||
}
|
||||
match_list = nmatch_list;
|
||||
|
||||
}
|
||||
match_list[++matches] = retstr;
|
||||
}
|
||||
|
||||
if (!match_list)
|
||||
return NULL; /* nothing found */
|
||||
|
||||
/* find least denominator and insert it to match_list[0] */
|
||||
which = 2;
|
||||
prevstr = match_list[1];
|
||||
max_equal = strlen(prevstr);
|
||||
for (; which <= matches; which++) {
|
||||
for (i = 0; i < max_equal &&
|
||||
prevstr[i] == match_list[which][i]; i++)
|
||||
continue;
|
||||
max_equal = i;
|
||||
}
|
||||
|
||||
retstr = malloc(max_equal + 1);
|
||||
if (retstr == NULL) {
|
||||
free(match_list);
|
||||
return NULL;
|
||||
}
|
||||
(void)strncpy(retstr, match_list[1], max_equal);
|
||||
retstr[max_equal] = '\0';
|
||||
match_list[0] = retstr;
|
||||
|
||||
/* add NULL as last pointer to the array */
|
||||
match_list[matches + 1] = (char *) NULL;
|
||||
|
||||
return (match_list);
|
||||
}
|
||||
|
||||
/*
|
||||
* Sort function for qsort(). Just wrapper around strcasecmp().
|
||||
*/
|
||||
static int
|
||||
_fn_qsort_string_compare(const void *i1, const void *i2)
|
||||
{
|
||||
const char *s1 = ((const char * const *)i1)[0];
|
||||
const char *s2 = ((const char * const *)i2)[0];
|
||||
|
||||
return strcasecmp(s1, s2);
|
||||
}
|
||||
|
||||
/*
|
||||
* Display list of strings in columnar format on readline's output stream.
|
||||
* 'matches' is list of strings, 'len' is number of strings in 'matches',
|
||||
* 'max' is maximum length of string in 'matches'.
|
||||
*/
|
||||
void
|
||||
fn_display_match_list (EditLine *el, char **matches, int len, int max)
|
||||
{
|
||||
int i, idx, limit, count;
|
||||
int screenwidth = el->el_term.t_size.h;
|
||||
|
||||
/*
|
||||
* Find out how many entries can be put on one line, count
|
||||
* with two spaces between strings.
|
||||
*/
|
||||
limit = screenwidth / (max + 2);
|
||||
if (limit == 0)
|
||||
limit = 1;
|
||||
|
||||
/* how many lines of output */
|
||||
count = len / limit;
|
||||
if (count * limit < len)
|
||||
count++;
|
||||
|
||||
/* Sort the items if they are not already sorted. */
|
||||
qsort(&matches[1], (size_t)(len - 1), sizeof(char *),
|
||||
_fn_qsort_string_compare);
|
||||
|
||||
idx = 1;
|
||||
for(; count > 0; count--) {
|
||||
for(i = 0; i < limit && matches[idx]; i++, idx++)
|
||||
(void)fprintf(el->el_outfile, "%-*s ", max,
|
||||
matches[idx]);
|
||||
(void)fprintf(el->el_outfile, "\n");
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Complete the word at or before point,
|
||||
* 'what_to_do' says what to do with the completion.
|
||||
* \t means do standard completion.
|
||||
* `?' means list the possible completions.
|
||||
* `*' means insert all of the possible completions.
|
||||
* `!' means to do standard completion, and list all possible completions if
|
||||
* there is more than one.
|
||||
*
|
||||
* Note: '*' support is not implemented
|
||||
* '!' could never be invoked
|
||||
*/
|
||||
int
|
||||
fn_complete(EditLine *el,
|
||||
char *(*complet_func)(const char *, int),
|
||||
char **(*attempted_completion_function)(const char *, int, int),
|
||||
const char *word_break, const char *special_prefixes,
|
||||
char append_character, int query_items,
|
||||
int *completion_type, int *over, int *point, int *end)
|
||||
{
|
||||
const LineInfo *li;
|
||||
char *temp, **matches;
|
||||
const char *ctemp;
|
||||
size_t len;
|
||||
int what_to_do = '\t';
|
||||
|
||||
if (el->el_state.lastcmd == el->el_state.thiscmd)
|
||||
what_to_do = '?';
|
||||
|
||||
/* readline's rl_complete() has to be told what we did... */
|
||||
if (completion_type != NULL)
|
||||
*completion_type = what_to_do;
|
||||
|
||||
if (!complet_func)
|
||||
complet_func = filename_completion_function;
|
||||
|
||||
/* We now look backwards for the start of a filename/variable word */
|
||||
li = el_line(el);
|
||||
ctemp = (const char *) li->cursor;
|
||||
while (ctemp > li->buffer
|
||||
&& !strchr(word_break, ctemp[-1])
|
||||
&& (!special_prefixes || !strchr(special_prefixes, ctemp[-1]) ) )
|
||||
ctemp--;
|
||||
|
||||
len = li->cursor - ctemp;
|
||||
temp = alloca(len + 1);
|
||||
(void)strncpy(temp, ctemp, len);
|
||||
temp[len] = '\0';
|
||||
|
||||
/* these can be used by function called in completion_matches() */
|
||||
/* or (*attempted_completion_function)() */
|
||||
if (point != 0)
|
||||
*point = li->cursor - li->buffer;
|
||||
if (end != NULL)
|
||||
*end = li->lastchar - li->buffer;
|
||||
|
||||
if (attempted_completion_function) {
|
||||
int cur_off = li->cursor - li->buffer;
|
||||
matches = (*attempted_completion_function) (temp,
|
||||
(int)(cur_off - len), cur_off);
|
||||
} else
|
||||
matches = 0;
|
||||
if (!attempted_completion_function ||
|
||||
(over != NULL && *over && !matches))
|
||||
matches = completion_matches(temp, complet_func);
|
||||
|
||||
if (over != NULL)
|
||||
*over = 0;
|
||||
|
||||
if (matches) {
|
||||
int i, retval = CC_REFRESH;
|
||||
int matches_num, maxlen, match_len, match_display=1;
|
||||
|
||||
/*
|
||||
* Only replace the completed string with common part of
|
||||
* possible matches if there is possible completion.
|
||||
*/
|
||||
if (matches[0][0] != '\0') {
|
||||
el_deletestr(el, (int) len);
|
||||
el_insertstr(el, matches[0]);
|
||||
}
|
||||
|
||||
if (what_to_do == '?')
|
||||
goto display_matches;
|
||||
|
||||
if (matches[2] == NULL && strcmp(matches[0], matches[1]) == 0) {
|
||||
/*
|
||||
* We found exact match. Add a space after
|
||||
* it, unless we do filename completion and the
|
||||
* object is a directory.
|
||||
*/
|
||||
size_t alen = strlen(matches[0]);
|
||||
if ((complet_func != filename_completion_function
|
||||
|| (alen > 0 && (matches[0])[alen - 1] != '/'))
|
||||
&& append_character) {
|
||||
char buf[2];
|
||||
buf[0] = append_character;
|
||||
buf[1] = '\0';
|
||||
el_insertstr(el, buf);
|
||||
}
|
||||
} else if (what_to_do == '!') {
|
||||
display_matches:
|
||||
/*
|
||||
* More than one match and requested to list possible
|
||||
* matches.
|
||||
*/
|
||||
|
||||
for(i=1, maxlen=0; matches[i]; i++) {
|
||||
match_len = strlen(matches[i]);
|
||||
if (match_len > maxlen)
|
||||
maxlen = match_len;
|
||||
}
|
||||
matches_num = i - 1;
|
||||
|
||||
/* newline to get on next line from command line */
|
||||
(void)fprintf(el->el_outfile, "\n");
|
||||
|
||||
/*
|
||||
* If there are too many items, ask user for display
|
||||
* confirmation.
|
||||
*/
|
||||
if (matches_num > query_items) {
|
||||
(void)fprintf(el->el_outfile,
|
||||
"Display all %d possibilities? (y or n) ",
|
||||
matches_num);
|
||||
(void)fflush(el->el_outfile);
|
||||
if (getc(stdin) != 'y')
|
||||
match_display = 0;
|
||||
(void)fprintf(el->el_outfile, "\n");
|
||||
}
|
||||
|
||||
if (match_display)
|
||||
fn_display_match_list(el, matches, matches_num,
|
||||
maxlen);
|
||||
retval = CC_REDISPLAY;
|
||||
} else if (matches[0][0]) {
|
||||
/*
|
||||
* There was some common match, but the name was
|
||||
* not complete enough. Next tab will print possible
|
||||
* completions.
|
||||
*/
|
||||
el_beep(el);
|
||||
} else {
|
||||
/* lcd is not a valid object - further specification */
|
||||
/* is needed */
|
||||
el_beep(el);
|
||||
retval = CC_NORM;
|
||||
}
|
||||
|
||||
/* free elements of array and the array itself */
|
||||
for (i = 0; matches[i]; i++)
|
||||
free(matches[i]);
|
||||
free(matches), matches = NULL;
|
||||
|
||||
return (retval);
|
||||
}
|
||||
return (CC_NORM);
|
||||
}
|
||||
|
||||
/*
|
||||
* el-compatible wrapper around rl_complete; needed for key binding
|
||||
*/
|
||||
/* ARGSUSED */
|
||||
unsigned char
|
||||
_el_fn_complete(EditLine *el, int ch __attribute__((__unused__)))
|
||||
{
|
||||
return (unsigned char)fn_complete(el, NULL, NULL,
|
||||
break_chars, NULL, ' ', 100,
|
||||
NULL, NULL, NULL, NULL);
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
/* $NetBSD: filecomplete.h,v 1.1 2005/05/07 16:01:25 dsl Exp $ */
|
||||
|
||||
/*-
|
||||
* Copyright (c) 1997 The NetBSD Foundation, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This code is derived from software contributed to The NetBSD Foundation
|
||||
* by Jaromir Dolecek.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions
|
||||
* are met:
|
||||
* 1. Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* 3. All advertising materials mentioning features or use of this software
|
||||
* must display the following acknowledgement:
|
||||
* This product includes software developed by the NetBSD
|
||||
* Foundation, Inc. and its contributors.
|
||||
* 4. Neither the name of The NetBSD Foundation nor the names of its
|
||||
* contributors may be used to endorse or promote products derived
|
||||
* from this software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
|
||||
* ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
|
||||
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
|
||||
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
* POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
#ifndef _FILECOMPLETE_H_
|
||||
#define _FILECOMPLETE_H_
|
||||
|
||||
int fn_complete(EditLine *,
|
||||
char *(*)(const char *, int),
|
||||
char **(*)(const char *, int, int),
|
||||
const char *, const char *, char, int,
|
||||
int *, int *, int *, int *);
|
||||
|
||||
void fn_display_match_list(EditLine *, char **, int, int);
|
||||
char *tilde_expand(char *txt);
|
||||
|
||||
#endif
|
|
@ -1,4 +1,4 @@
|
|||
/* $NetBSD: histedit.h,v 1.25 2003/12/05 13:37:48 lukem Exp $ */
|
||||
/* $NetBSD: histedit.h,v 1.26 2005/05/07 16:01:25 dsl Exp $ */
|
||||
|
||||
/*-
|
||||
* Copyright (c) 1992, 1993
|
||||
|
@ -106,6 +106,7 @@ int el_parse(EditLine *, int, const char **);
|
|||
*/
|
||||
int el_set(EditLine *, int, ...);
|
||||
int el_get(EditLine *, int, void *);
|
||||
unsigned char _el_fn_complete(EditLine *, int);
|
||||
|
||||
/*
|
||||
* el_set/el_get parameters
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* $NetBSD: readline.c,v 1.52 2005/04/19 03:29:18 christos Exp $ */
|
||||
/* $NetBSD: readline.c,v 1.53 2005/05/07 16:01:25 dsl Exp $ */
|
||||
|
||||
/*-
|
||||
* Copyright (c) 1997 The NetBSD Foundation, Inc.
|
||||
|
@ -38,7 +38,7 @@
|
|||
|
||||
#include "config.h"
|
||||
#if !defined(lint) && !defined(SCCSID)
|
||||
__RCSID("$NetBSD: readline.c,v 1.52 2005/04/19 03:29:18 christos Exp $");
|
||||
__RCSID("$NetBSD: readline.c,v 1.53 2005/05/07 16:01:25 dsl Exp $");
|
||||
#endif /* not lint && not SCCSID */
|
||||
|
||||
#include <sys/types.h>
|
||||
|
@ -65,6 +65,7 @@ __RCSID("$NetBSD: readline.c,v 1.52 2005/04/19 03:29:18 christos Exp $");
|
|||
#include "fcns.h" /* for EL_NUM_FCNS */
|
||||
#include "histedit.h"
|
||||
#include "readline/readline.h"
|
||||
#include "filecomplete.h"
|
||||
|
||||
/* for rl_complete() */
|
||||
#define TAB '\r'
|
||||
|
@ -141,7 +142,7 @@ int rl_completion_query_items = 100;
|
|||
* in the parsed text when it is passed to the completion function.
|
||||
* Shell uses this to help determine what kind of completing to do.
|
||||
*/
|
||||
char *rl_special_prefixes = (char *)NULL;
|
||||
char *rl_special_prefixes = NULL;
|
||||
|
||||
/*
|
||||
* This is the character appended to the completed words if at the end of
|
||||
|
@ -151,13 +152,9 @@ int rl_completion_append_character = ' ';
|
|||
|
||||
/* stuff below is used internally by libedit for readline emulation */
|
||||
|
||||
/* if not zero, non-unique completions always show list of possible matches */
|
||||
static int _rl_complete_show_all = 0;
|
||||
|
||||
static History *h = NULL;
|
||||
static EditLine *e = NULL;
|
||||
static Function *map[256];
|
||||
static int el_rl_complete_cmdnum = 0;
|
||||
|
||||
/* internal functions */
|
||||
static unsigned char _el_rl_complete(EditLine *, int);
|
||||
|
@ -168,8 +165,6 @@ static int _history_expand_command(const char *, size_t, size_t,
|
|||
char **);
|
||||
static char *_rl_compat_sub(const char *, const char *,
|
||||
const char *, int);
|
||||
static int _rl_complete_internal(int);
|
||||
static int _rl_qsort_string_compare(const void *, const void *);
|
||||
static int _rl_event_read_char(EditLine *, char *);
|
||||
static void _rl_update_pos(void);
|
||||
|
||||
|
@ -214,7 +209,6 @@ rl_initialize(void)
|
|||
{
|
||||
HistEvent ev;
|
||||
const LineInfo *li;
|
||||
int i;
|
||||
int editmode = 1;
|
||||
struct termios t;
|
||||
|
||||
|
@ -282,17 +276,6 @@ rl_initialize(void)
|
|||
"ReadLine compatible suspend function",
|
||||
_el_rl_tstp);
|
||||
el_set(e, EL_BIND, "^Z", "rl_tstp", NULL);
|
||||
|
||||
/*
|
||||
* Find out where the rl_complete function was added; this is
|
||||
* used later to detect that lastcmd was also rl_complete.
|
||||
*/
|
||||
for(i=EL_NUM_FCNS; i < e->el_map.nfunc; i++) {
|
||||
if (e->el_map.func[i] == _el_rl_complete) {
|
||||
el_rl_complete_cmdnum = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* read settings from configuration file */
|
||||
el_source(e, NULL);
|
||||
|
@ -1368,180 +1351,6 @@ history_search_pos(const char *str,
|
|||
/********************************/
|
||||
/* completion functions */
|
||||
|
||||
/*
|
||||
* does tilde expansion of strings of type ``~user/foo''
|
||||
* if ``user'' isn't valid user name or ``txt'' doesn't start
|
||||
* w/ '~', returns pointer to strdup()ed copy of ``txt''
|
||||
*
|
||||
* it's callers's responsibility to free() returned string
|
||||
*/
|
||||
char *
|
||||
tilde_expand(char *txt)
|
||||
{
|
||||
struct passwd pwres, *pass;
|
||||
char *temp;
|
||||
size_t len = 0;
|
||||
char pwbuf[1024];
|
||||
|
||||
if (txt[0] != '~')
|
||||
return (strdup(txt));
|
||||
|
||||
temp = strchr(txt + 1, '/');
|
||||
if (temp == NULL) {
|
||||
temp = strdup(txt + 1);
|
||||
if (temp == NULL)
|
||||
return NULL;
|
||||
} else {
|
||||
len = temp - txt + 1; /* text until string after slash */
|
||||
temp = malloc(len);
|
||||
if (temp == NULL)
|
||||
return NULL;
|
||||
(void)strncpy(temp, txt + 1, len - 2);
|
||||
temp[len - 2] = '\0';
|
||||
}
|
||||
if (getpwnam_r(temp, &pwres, pwbuf, sizeof(pwbuf), &pass) != 0)
|
||||
pass = NULL;
|
||||
free(temp); /* value no more needed */
|
||||
if (pass == NULL)
|
||||
return (strdup(txt));
|
||||
|
||||
/* update pointer txt to point at string immedially following */
|
||||
/* first slash */
|
||||
txt += len;
|
||||
|
||||
temp = malloc(strlen(pass->pw_dir) + 1 + strlen(txt) + 1);
|
||||
if (temp == NULL)
|
||||
return NULL;
|
||||
(void)sprintf(temp, "%s/%s", pass->pw_dir, txt);
|
||||
|
||||
return (temp);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* return first found file name starting by the ``text'' or NULL if no
|
||||
* such file can be found
|
||||
* value of ``state'' is ignored
|
||||
*
|
||||
* it's caller's responsibility to free returned string
|
||||
*/
|
||||
char *
|
||||
filename_completion_function(const char *text, int state)
|
||||
{
|
||||
static DIR *dir = NULL;
|
||||
static char *filename = NULL, *dirname = NULL;
|
||||
static size_t filename_len = 0;
|
||||
struct dirent *entry;
|
||||
char *temp;
|
||||
size_t len;
|
||||
|
||||
if (state == 0 || dir == NULL) {
|
||||
temp = strrchr(text, '/');
|
||||
if (temp) {
|
||||
char *nptr;
|
||||
temp++;
|
||||
nptr = realloc(filename, strlen(temp) + 1);
|
||||
if (nptr == NULL) {
|
||||
free(filename);
|
||||
return NULL;
|
||||
}
|
||||
filename = nptr;
|
||||
(void)strcpy(filename, temp);
|
||||
len = temp - text; /* including last slash */
|
||||
nptr = realloc(dirname, len + 1);
|
||||
if (nptr == NULL) {
|
||||
free(filename);
|
||||
return NULL;
|
||||
}
|
||||
dirname = nptr;
|
||||
(void)strncpy(dirname, text, len);
|
||||
dirname[len] = '\0';
|
||||
} else {
|
||||
if (*text == 0)
|
||||
filename = NULL;
|
||||
else {
|
||||
filename = strdup(text);
|
||||
if (filename == NULL)
|
||||
return NULL;
|
||||
}
|
||||
dirname = NULL;
|
||||
}
|
||||
|
||||
/* support for ``~user'' syntax */
|
||||
if (dirname && *dirname == '~') {
|
||||
char *nptr;
|
||||
temp = tilde_expand(dirname);
|
||||
if (temp == NULL)
|
||||
return NULL;
|
||||
nptr = realloc(dirname, strlen(temp) + 1);
|
||||
if (nptr == NULL) {
|
||||
free(dirname);
|
||||
return NULL;
|
||||
}
|
||||
dirname = nptr;
|
||||
(void)strcpy(dirname, temp); /* safe */
|
||||
free(temp); /* no longer needed */
|
||||
}
|
||||
/* will be used in cycle */
|
||||
filename_len = filename ? strlen(filename) : 0;
|
||||
|
||||
if (dir != NULL) {
|
||||
(void)closedir(dir);
|
||||
dir = NULL;
|
||||
}
|
||||
dir = opendir(dirname ? dirname : ".");
|
||||
if (!dir)
|
||||
return (NULL); /* cannot open the directory */
|
||||
}
|
||||
/* find the match */
|
||||
while ((entry = readdir(dir)) != NULL) {
|
||||
/* skip . and .. */
|
||||
if (entry->d_name[0] == '.' && (!entry->d_name[1]
|
||||
|| (entry->d_name[1] == '.' && !entry->d_name[2])))
|
||||
continue;
|
||||
if (filename_len == 0)
|
||||
break;
|
||||
/* otherwise, get first entry where first */
|
||||
/* filename_len characters are equal */
|
||||
if (entry->d_name[0] == filename[0]
|
||||
#if defined(__SVR4) || defined(__linux__)
|
||||
&& strlen(entry->d_name) >= filename_len
|
||||
#else
|
||||
&& entry->d_namlen >= filename_len
|
||||
#endif
|
||||
&& strncmp(entry->d_name, filename,
|
||||
filename_len) == 0)
|
||||
break;
|
||||
}
|
||||
|
||||
if (entry) { /* match found */
|
||||
|
||||
struct stat stbuf;
|
||||
#if defined(__SVR4) || defined(__linux__)
|
||||
len = strlen(entry->d_name) +
|
||||
#else
|
||||
len = entry->d_namlen +
|
||||
#endif
|
||||
((dirname) ? strlen(dirname) : 0) + 1 + 1;
|
||||
temp = malloc(len);
|
||||
if (temp == NULL)
|
||||
return NULL;
|
||||
(void)sprintf(temp, "%s%s",
|
||||
dirname ? dirname : "", entry->d_name); /* safe */
|
||||
|
||||
/* test, if it's directory */
|
||||
if (stat(temp, &stbuf) == 0 && S_ISDIR(stbuf.st_mode))
|
||||
strcat(temp, "/"); /* safe */
|
||||
} else {
|
||||
(void)closedir(dir);
|
||||
dir = NULL;
|
||||
temp = NULL;
|
||||
}
|
||||
|
||||
return (temp);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* a completion generator for usernames; returns _first_ username
|
||||
* which starts with supplied text
|
||||
|
@ -1576,16 +1385,6 @@ username_completion_function(const char *text, int state)
|
|||
}
|
||||
|
||||
|
||||
/*
|
||||
* el-compatible wrapper around rl_complete; needed for key binding
|
||||
*/
|
||||
/* ARGSUSED */
|
||||
static unsigned char
|
||||
_el_rl_complete(EditLine *el __attribute__((__unused__)), int ch)
|
||||
{
|
||||
return (unsigned char) rl_complete(0, ch);
|
||||
}
|
||||
|
||||
/*
|
||||
* el-compatible wrapper to send TSTP on ^Z
|
||||
*/
|
||||
|
@ -1597,278 +1396,25 @@ _el_rl_tstp(EditLine *el __attribute__((__unused__)), int ch __attribute__((__un
|
|||
return CC_NORM;
|
||||
}
|
||||
|
||||
/*
|
||||
* returns list of completions for text given
|
||||
*/
|
||||
char **
|
||||
completion_matches(const char *text, CPFunction *genfunc)
|
||||
{
|
||||
char **match_list = NULL, *retstr, *prevstr;
|
||||
size_t match_list_len, max_equal, which, i;
|
||||
size_t matches;
|
||||
|
||||
if (h == NULL || e == NULL)
|
||||
rl_initialize();
|
||||
|
||||
matches = 0;
|
||||
match_list_len = 1;
|
||||
while ((retstr = (*genfunc) (text, (int)matches)) != NULL) {
|
||||
/* allow for list terminator here */
|
||||
if (matches + 3 >= match_list_len) {
|
||||
char **nmatch_list;
|
||||
while (matches + 3 >= match_list_len)
|
||||
match_list_len <<= 1;
|
||||
nmatch_list = realloc(match_list,
|
||||
match_list_len * sizeof(char *));
|
||||
if (nmatch_list == NULL) {
|
||||
free(match_list);
|
||||
return NULL;
|
||||
}
|
||||
match_list = nmatch_list;
|
||||
|
||||
}
|
||||
match_list[++matches] = retstr;
|
||||
}
|
||||
|
||||
if (!match_list)
|
||||
return NULL; /* nothing found */
|
||||
|
||||
/* find least denominator and insert it to match_list[0] */
|
||||
which = 2;
|
||||
prevstr = match_list[1];
|
||||
max_equal = strlen(prevstr);
|
||||
for (; which <= matches; which++) {
|
||||
for (i = 0; i < max_equal &&
|
||||
prevstr[i] == match_list[which][i]; i++)
|
||||
continue;
|
||||
max_equal = i;
|
||||
}
|
||||
|
||||
retstr = malloc(max_equal + 1);
|
||||
if (retstr == NULL) {
|
||||
free(match_list);
|
||||
return NULL;
|
||||
}
|
||||
(void)strncpy(retstr, match_list[1], max_equal);
|
||||
retstr[max_equal] = '\0';
|
||||
match_list[0] = retstr;
|
||||
|
||||
/* add NULL as last pointer to the array */
|
||||
match_list[matches + 1] = (char *) NULL;
|
||||
|
||||
return (match_list);
|
||||
}
|
||||
|
||||
/*
|
||||
* Sort function for qsort(). Just wrapper around strcasecmp().
|
||||
*/
|
||||
static int
|
||||
_rl_qsort_string_compare(i1, i2)
|
||||
const void *i1, *i2;
|
||||
{
|
||||
const char *s1 = ((const char * const *)i1)[0];
|
||||
const char *s2 = ((const char * const *)i2)[0];
|
||||
|
||||
return strcasecmp(s1, s2);
|
||||
}
|
||||
|
||||
/*
|
||||
* Display list of strings in columnar format on readline's output stream.
|
||||
* 'matches' is list of strings, 'len' is number of strings in 'matches',
|
||||
* 'max' is maximum length of string in 'matches'.
|
||||
*/
|
||||
void
|
||||
rl_display_match_list (matches, len, max)
|
||||
char **matches;
|
||||
int len, max;
|
||||
rl_display_match_list(char **matches, int len, int max)
|
||||
{
|
||||
int i, idx, limit, count;
|
||||
int screenwidth = e->el_term.t_size.h;
|
||||
|
||||
/*
|
||||
* Find out how many entries can be put on one line, count
|
||||
* with two spaces between strings.
|
||||
*/
|
||||
limit = screenwidth / (max + 2);
|
||||
if (limit == 0)
|
||||
limit = 1;
|
||||
|
||||
/* how many lines of output */
|
||||
count = len / limit;
|
||||
if (count * limit < len)
|
||||
count++;
|
||||
|
||||
/* Sort the items if they are not already sorted. */
|
||||
qsort(&matches[1], (size_t)(len - 1), sizeof(char *),
|
||||
_rl_qsort_string_compare);
|
||||
|
||||
idx = 1;
|
||||
for(; count > 0; count--) {
|
||||
for(i = 0; i < limit && matches[idx]; i++, idx++)
|
||||
(void)fprintf(e->el_outfile, "%-*s ", max,
|
||||
matches[idx]);
|
||||
(void)fprintf(e->el_outfile, "\n");
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Complete the word at or before point, called by rl_complete()
|
||||
* 'what_to_do' says what to do with the completion.
|
||||
* `?' means list the possible completions.
|
||||
* TAB means do standard completion.
|
||||
* `*' means insert all of the possible completions.
|
||||
* `!' means to do standard completion, and list all possible completions if
|
||||
* there is more than one.
|
||||
*
|
||||
* Note: '*' support is not implemented
|
||||
*/
|
||||
static int
|
||||
_rl_complete_internal(int what_to_do)
|
||||
{
|
||||
Function *complet_func;
|
||||
const LineInfo *li;
|
||||
char *temp, **matches;
|
||||
const char *ctemp;
|
||||
size_t len;
|
||||
|
||||
rl_completion_type = what_to_do;
|
||||
|
||||
if (h == NULL || e == NULL)
|
||||
rl_initialize();
|
||||
|
||||
complet_func = rl_completion_entry_function;
|
||||
if (!complet_func)
|
||||
complet_func = (Function *)(void *)filename_completion_function;
|
||||
|
||||
/* We now look backwards for the start of a filename/variable word */
|
||||
li = el_line(e);
|
||||
ctemp = (const char *) li->cursor;
|
||||
while (ctemp > li->buffer
|
||||
&& !strchr(rl_basic_word_break_characters, ctemp[-1])
|
||||
&& (!rl_special_prefixes
|
||||
|| !strchr(rl_special_prefixes, ctemp[-1]) ) )
|
||||
ctemp--;
|
||||
|
||||
len = li->cursor - ctemp;
|
||||
temp = alloca(len + 1);
|
||||
(void)strncpy(temp, ctemp, len);
|
||||
temp[len] = '\0';
|
||||
|
||||
/* these can be used by function called in completion_matches() */
|
||||
/* or (*rl_attempted_completion_function)() */
|
||||
_rl_update_pos();
|
||||
|
||||
if (rl_attempted_completion_function) {
|
||||
int end = li->cursor - li->buffer;
|
||||
matches = (*rl_attempted_completion_function) (temp, (int)
|
||||
(end - len), end);
|
||||
} else
|
||||
matches = 0;
|
||||
if (!rl_attempted_completion_function ||
|
||||
(!rl_attempted_completion_over && !matches))
|
||||
matches = completion_matches(temp, (CPFunction *)complet_func);
|
||||
|
||||
if (rl_attempted_completion_over)
|
||||
rl_attempted_completion_over = 0;
|
||||
|
||||
if (matches) {
|
||||
int i, retval = CC_REFRESH;
|
||||
int matches_num, maxlen, match_len, match_display=1;
|
||||
|
||||
/*
|
||||
* Only replace the completed string with common part of
|
||||
* possible matches if there is possible completion.
|
||||
*/
|
||||
if (matches[0][0] != '\0') {
|
||||
el_deletestr(e, (int) len);
|
||||
el_insertstr(e, matches[0]);
|
||||
}
|
||||
|
||||
if (what_to_do == '?')
|
||||
goto display_matches;
|
||||
|
||||
if (matches[2] == NULL && strcmp(matches[0], matches[1]) == 0) {
|
||||
/*
|
||||
* We found exact match. Add a space after
|
||||
* it, unless we do filename completion and the
|
||||
* object is a directory.
|
||||
*/
|
||||
size_t alen = strlen(matches[0]);
|
||||
if ((complet_func !=
|
||||
(Function *)filename_completion_function
|
||||
|| (alen > 0 && (matches[0])[alen - 1] != '/'))
|
||||
&& rl_completion_append_character) {
|
||||
char buf[2];
|
||||
buf[0] = rl_completion_append_character;
|
||||
buf[1] = '\0';
|
||||
el_insertstr(e, buf);
|
||||
}
|
||||
} else if (what_to_do == '!') {
|
||||
display_matches:
|
||||
/*
|
||||
* More than one match and requested to list possible
|
||||
* matches.
|
||||
*/
|
||||
|
||||
for(i=1, maxlen=0; matches[i]; i++) {
|
||||
match_len = strlen(matches[i]);
|
||||
if (match_len > maxlen)
|
||||
maxlen = match_len;
|
||||
}
|
||||
matches_num = i - 1;
|
||||
|
||||
/* newline to get on next line from command line */
|
||||
(void)fprintf(e->el_outfile, "\n");
|
||||
|
||||
/*
|
||||
* If there are too many items, ask user for display
|
||||
* confirmation.
|
||||
*/
|
||||
if (matches_num > rl_completion_query_items) {
|
||||
(void)fprintf(e->el_outfile,
|
||||
"Display all %d possibilities? (y or n) ",
|
||||
matches_num);
|
||||
(void)fflush(e->el_outfile);
|
||||
if (getc(stdin) != 'y')
|
||||
match_display = 0;
|
||||
(void)fprintf(e->el_outfile, "\n");
|
||||
}
|
||||
|
||||
if (match_display)
|
||||
rl_display_match_list(matches, matches_num,
|
||||
maxlen);
|
||||
retval = CC_REDISPLAY;
|
||||
} else if (matches[0][0]) {
|
||||
/*
|
||||
* There was some common match, but the name was
|
||||
* not complete enough. Next tab will print possible
|
||||
* completions.
|
||||
*/
|
||||
el_beep(e);
|
||||
} else {
|
||||
/* lcd is not a valid object - further specification */
|
||||
/* is needed */
|
||||
el_beep(e);
|
||||
retval = CC_NORM;
|
||||
}
|
||||
|
||||
/* free elements of array and the array itself */
|
||||
for (i = 0; matches[i]; i++)
|
||||
free(matches[i]);
|
||||
free(matches), matches = NULL;
|
||||
|
||||
return (retval);
|
||||
}
|
||||
return (CC_NORM);
|
||||
fn_display_match_list(e, matches, len, max);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* complete word at current point
|
||||
*/
|
||||
/* ARGSUSED */
|
||||
int
|
||||
/*ARGSUSED*/
|
||||
rl_complete(int ignore, int invoking_key)
|
||||
rl_complete(int ignore __attribute__((__unused__)), int invoking_key)
|
||||
{
|
||||
if (h == NULL || e == NULL)
|
||||
rl_initialize();
|
||||
|
@ -1879,15 +1425,26 @@ rl_complete(int ignore, int invoking_key)
|
|||
arr[1] = '\0';
|
||||
el_insertstr(e, arr);
|
||||
return (CC_REFRESH);
|
||||
} else if (e->el_state.lastcmd == el_rl_complete_cmdnum)
|
||||
return _rl_complete_internal('?');
|
||||
else if (_rl_complete_show_all)
|
||||
return _rl_complete_internal('!');
|
||||
else
|
||||
return _rl_complete_internal(TAB);
|
||||
}
|
||||
|
||||
/* Just look at how many global variables modify this operation! */
|
||||
return fn_complete(e,
|
||||
(CPFunction *)rl_completion_entry_function,
|
||||
rl_attempted_completion_function,
|
||||
rl_basic_word_break_characters, rl_special_prefixes,
|
||||
rl_completion_append_character, rl_completion_query_items,
|
||||
&rl_completion_type, &rl_attempted_completion_over,
|
||||
&rl_point, &rl_end);
|
||||
}
|
||||
|
||||
|
||||
/* ARGSUSED */
|
||||
static unsigned char
|
||||
_el_rl_complete(EditLine *el __attribute__((__unused__)), int ch)
|
||||
{
|
||||
return (unsigned char)rl_complete(0, ch);
|
||||
}
|
||||
|
||||
/*
|
||||
* misc other functions
|
||||
*/
|
||||
|
|
Loading…
Reference in New Issue