version 1.115, 2009/12/20 07:28:36 |
version 1.116, 2010/01/04 02:03:57 |
|
|
/* I wish qsort() took a separate ctx for the comparison function...*/ |
/* I wish qsort() took a separate ctx for the comparison function...*/ |
int sort_flag; |
int sort_flag; |
|
|
|
/* Context used for commandline completion */ |
|
struct complete_ctx { |
|
struct sftp_conn *conn; |
|
char **remote_pathp; |
|
}; |
|
|
int remote_glob(struct sftp_conn *, const char *, int, |
int remote_glob(struct sftp_conn *, const char *, int, |
int (*)(const char *, int), glob_t *); /* proto for sftp-glob.c */ |
int (*)(const char *, int), glob_t *); /* proto for sftp-glob.c */ |
|
|
|
|
struct CMD { |
struct CMD { |
const char *c; |
const char *c; |
const int n; |
const int n; |
|
const int t; |
}; |
}; |
|
|
|
/* Type of completion */ |
|
#define NOARGS 0 |
|
#define REMOTE 1 |
|
#define LOCAL 2 |
|
|
static const struct CMD cmds[] = { |
static const struct CMD cmds[] = { |
{ "bye", I_QUIT }, |
{ "bye", I_QUIT, NOARGS }, |
{ "cd", I_CHDIR }, |
{ "cd", I_CHDIR, REMOTE }, |
{ "chdir", I_CHDIR }, |
{ "chdir", I_CHDIR, REMOTE }, |
{ "chgrp", I_CHGRP }, |
{ "chgrp", I_CHGRP, REMOTE }, |
{ "chmod", I_CHMOD }, |
{ "chmod", I_CHMOD, REMOTE }, |
{ "chown", I_CHOWN }, |
{ "chown", I_CHOWN, REMOTE }, |
{ "df", I_DF }, |
{ "df", I_DF, REMOTE }, |
{ "dir", I_LS }, |
{ "dir", I_LS, REMOTE }, |
{ "exit", I_QUIT }, |
{ "exit", I_QUIT, NOARGS }, |
{ "get", I_GET }, |
{ "get", I_GET, REMOTE }, |
{ "mget", I_GET }, |
{ "help", I_HELP, NOARGS }, |
{ "help", I_HELP }, |
{ "lcd", I_LCHDIR, LOCAL }, |
{ "lcd", I_LCHDIR }, |
{ "lchdir", I_LCHDIR, LOCAL }, |
{ "lchdir", I_LCHDIR }, |
{ "lls", I_LLS, LOCAL }, |
{ "lls", I_LLS }, |
{ "lmkdir", I_LMKDIR, LOCAL }, |
{ "lmkdir", I_LMKDIR }, |
{ "ln", I_SYMLINK, REMOTE }, |
{ "ln", I_SYMLINK }, |
{ "lpwd", I_LPWD, LOCAL }, |
{ "lpwd", I_LPWD }, |
{ "ls", I_LS, REMOTE }, |
{ "ls", I_LS }, |
{ "lumask", I_LUMASK, NOARGS }, |
{ "lumask", I_LUMASK }, |
{ "mkdir", I_MKDIR, REMOTE }, |
{ "mkdir", I_MKDIR }, |
{ "progress", I_PROGRESS, NOARGS }, |
{ "progress", I_PROGRESS }, |
{ "put", I_PUT, LOCAL }, |
{ "put", I_PUT }, |
{ "pwd", I_PWD, REMOTE }, |
{ "mput", I_PUT }, |
{ "quit", I_QUIT, NOARGS }, |
{ "pwd", I_PWD }, |
{ "rename", I_RENAME, REMOTE }, |
{ "quit", I_QUIT }, |
{ "rm", I_RM, REMOTE }, |
{ "rename", I_RENAME }, |
{ "rmdir", I_RMDIR, REMOTE }, |
{ "rm", I_RM }, |
{ "symlink", I_SYMLINK, REMOTE }, |
{ "rmdir", I_RMDIR }, |
{ "version", I_VERSION, NOARGS }, |
{ "symlink", I_SYMLINK }, |
{ "!", I_SHELL, NOARGS }, |
{ "version", I_VERSION }, |
{ "?", I_HELP, NOARGS }, |
{ "!", I_SHELL }, |
{ NULL, -1, -1 } |
{ "?", I_HELP }, |
|
{ NULL, -1} |
|
}; |
}; |
|
|
int interactive_loop(struct sftp_conn *, char *file1, char *file2); |
int interactive_loop(struct sftp_conn *, char *file1, char *file2); |
|
|
* Split a string into an argument vector using sh(1)-style quoting, |
* Split a string into an argument vector using sh(1)-style quoting, |
* comment and escaping rules, but with some tweaks to handle glob(3) |
* comment and escaping rules, but with some tweaks to handle glob(3) |
* wildcards. |
* wildcards. |
|
* The "sloppy" flag allows for recovery from missing terminating quote, for |
|
* use in parsing incomplete commandlines during tab autocompletion. |
|
* |
* Returns NULL on error or a NULL-terminated array of arguments. |
* Returns NULL on error or a NULL-terminated array of arguments. |
|
* |
|
* If "lastquote" is not NULL, the quoting character used for the last |
|
* argument is placed in *lastquote ("\0", "'" or "\""). |
|
* |
|
* If "terminated" is not NULL, *terminated will be set to 1 when the |
|
* last argument's quote has been properly terminated or 0 otherwise. |
|
* This parameter is only of use if "sloppy" is set. |
*/ |
*/ |
#define MAXARGS 128 |
#define MAXARGS 128 |
#define MAXARGLEN 8192 |
#define MAXARGLEN 8192 |
static char ** |
static char ** |
makeargv(const char *arg, int *argcp) |
makeargv(const char *arg, int *argcp, int sloppy, char *lastquote, |
|
u_int *terminated) |
{ |
{ |
int argc, quot; |
int argc, quot; |
size_t i, j; |
size_t i, j; |
|
|
error("string too long"); |
error("string too long"); |
return NULL; |
return NULL; |
} |
} |
|
if (terminated != NULL) |
|
*terminated = 1; |
|
if (lastquote != NULL) |
|
*lastquote = '\0'; |
state = MA_START; |
state = MA_START; |
i = j = 0; |
i = j = 0; |
for (;;) { |
for (;;) { |
|
|
if (state == MA_START) { |
if (state == MA_START) { |
argv[argc] = argvs + j; |
argv[argc] = argvs + j; |
state = q; |
state = q; |
|
if (lastquote != NULL) |
|
*lastquote = arg[i]; |
} else if (state == MA_UNQUOTED) |
} else if (state == MA_UNQUOTED) |
state = q; |
state = q; |
else if (state == q) |
else if (state == q) |
|
|
if (state == MA_START) { |
if (state == MA_START) { |
argv[argc] = argvs + j; |
argv[argc] = argvs + j; |
state = MA_UNQUOTED; |
state = MA_UNQUOTED; |
|
if (lastquote != NULL) |
|
*lastquote = '\0'; |
} |
} |
if (arg[i + 1] == '?' || arg[i + 1] == '[' || |
if (arg[i + 1] == '?' || arg[i + 1] == '[' || |
arg[i + 1] == '*' || arg[i + 1] == '\\') { |
arg[i + 1] == '*' || arg[i + 1] == '\\') { |
|
|
goto string_done; |
goto string_done; |
} else if (arg[i] == '\0') { |
} else if (arg[i] == '\0') { |
if (state == MA_SQUOTE || state == MA_DQUOTE) { |
if (state == MA_SQUOTE || state == MA_DQUOTE) { |
|
if (sloppy) { |
|
state = MA_UNQUOTED; |
|
if (terminated != NULL) |
|
*terminated = 0; |
|
goto string_done; |
|
} |
error("Unterminated quoted argument"); |
error("Unterminated quoted argument"); |
return NULL; |
return NULL; |
} |
} |
|
|
if (state == MA_START) { |
if (state == MA_START) { |
argv[argc] = argvs + j; |
argv[argc] = argvs + j; |
state = MA_UNQUOTED; |
state = MA_UNQUOTED; |
|
if (lastquote != NULL) |
|
*lastquote = '\0'; |
} |
} |
if ((state == MA_SQUOTE || state == MA_DQUOTE) && |
if ((state == MA_SQUOTE || state == MA_DQUOTE) && |
(arg[i] == '?' || arg[i] == '[' || arg[i] == '*')) { |
(arg[i] == '?' || arg[i] == '[' || arg[i] == '*')) { |
|
|
} |
} |
|
|
static int |
static int |
parse_args(const char **cpp, int *pflag, int *rflag, int *lflag, int *iflag, int *hflag, |
parse_args(const char **cpp, int *pflag, int *rflag, int *lflag, int *iflag, |
unsigned long *n_arg, char **path1, char **path2) |
int *hflag, unsigned long *n_arg, char **path1, char **path2) |
{ |
{ |
const char *cmd, *cp = *cpp; |
const char *cmd, *cp = *cpp; |
char *cp2, **argv; |
char *cp2, **argv; |
|
|
cp++; |
cp++; |
} |
} |
|
|
if ((argv = makeargv(cp, &argc)) == NULL) |
if ((argv = makeargv(cp, &argc, 0, NULL, NULL)) == NULL) |
return -1; |
return -1; |
|
|
/* Figure out which command we have */ |
/* Figure out which command we have */ |
|
|
return ("sftp> "); |
return ("sftp> "); |
} |
} |
|
|
|
/* Display entries in 'list' after skipping the first 'len' chars */ |
|
static void |
|
complete_display(char **list, u_int len) |
|
{ |
|
u_int y, m = 0, width = 80, columns = 1, colspace = 0, llen; |
|
struct winsize ws; |
|
char *tmp; |
|
|
|
/* Count entries for sort and find longest */ |
|
for (y = 0; list[y]; y++) |
|
m = MAX(m, strlen(list[y])); |
|
|
|
if (ioctl(fileno(stdin), TIOCGWINSZ, &ws) != -1) |
|
width = ws.ws_col; |
|
|
|
m = m > len ? m - len : 0; |
|
columns = width / (m + 2); |
|
columns = MAX(columns, 1); |
|
colspace = width / columns; |
|
colspace = MIN(colspace, width); |
|
|
|
printf("\n"); |
|
m = 1; |
|
for (y = 0; list[y]; y++) { |
|
llen = strlen(list[y]); |
|
tmp = llen > len ? list[y] + len : ""; |
|
printf("%-*s", colspace, tmp); |
|
if (m >= columns) { |
|
printf("\n"); |
|
m = 1; |
|
} else |
|
m++; |
|
} |
|
printf("\n"); |
|
} |
|
|
|
/* |
|
* Given a "list" of words that begin with a common prefix of "word", |
|
* attempt to find an autocompletion to extends "word" by the next |
|
* characters common to all entries in "list". |
|
*/ |
|
static char * |
|
complete_ambiguous(const char *word, char **list, size_t count) |
|
{ |
|
if (word == NULL) |
|
return NULL; |
|
|
|
if (count > 0) { |
|
u_int y, matchlen = strlen(list[0]); |
|
|
|
/* Find length of common stem */ |
|
for (y = 1; list[y]; y++) { |
|
u_int x; |
|
|
|
for (x = 0; x < matchlen; x++) |
|
if (list[0][x] != list[y][x]) |
|
break; |
|
|
|
matchlen = x; |
|
} |
|
|
|
if (matchlen > strlen(word)) { |
|
char *tmp = xstrdup(list[0]); |
|
|
|
tmp[matchlen] = NULL; |
|
return tmp; |
|
} |
|
} |
|
|
|
return xstrdup(word); |
|
} |
|
|
|
/* Autocomplete a sftp command */ |
|
static int |
|
complete_cmd_parse(EditLine *el, char *cmd, int lastarg, char quote, |
|
int terminated) |
|
{ |
|
u_int y, count = 0, cmdlen, tmplen; |
|
char *tmp, **list, argterm[3]; |
|
const LineInfo *lf; |
|
|
|
list = xcalloc((sizeof(cmds) / sizeof(*cmds)) + 1, sizeof(char *)); |
|
|
|
/* No command specified: display all available commands */ |
|
if (cmd == NULL) { |
|
for (y = 0; cmds[y].c; y++) |
|
list[count++] = xstrdup(cmds[y].c); |
|
|
|
list[count] = NULL; |
|
complete_display(list, 0); |
|
|
|
for (y = 0; list[y] != NULL; y++) |
|
xfree(list[y]); |
|
xfree(list); |
|
return count; |
|
} |
|
|
|
/* Prepare subset of commands that start with "cmd" */ |
|
cmdlen = strlen(cmd); |
|
for (y = 0; cmds[y].c; y++) { |
|
if (!strncasecmp(cmd, cmds[y].c, cmdlen)) |
|
list[count++] = xstrdup(cmds[y].c); |
|
} |
|
list[count] = NULL; |
|
|
|
if (count == 0) |
|
return 0; |
|
|
|
/* Complete ambigious command */ |
|
tmp = complete_ambiguous(cmd, list, count); |
|
if (count > 1) |
|
complete_display(list, 0); |
|
|
|
for (y = 0; list[y]; y++) |
|
xfree(list[y]); |
|
xfree(list); |
|
|
|
if (tmp != NULL) { |
|
tmplen = strlen(tmp); |
|
cmdlen = strlen(cmd); |
|
/* If cmd may be extended then do so */ |
|
if (tmplen > cmdlen) |
|
if (el_insertstr(el, tmp + cmdlen) == -1) |
|
fatal("el_insertstr failed."); |
|
lf = el_line(el); |
|
/* Terminate argument cleanly */ |
|
if (count == 1) { |
|
y = 0; |
|
if (!terminated) |
|
argterm[y++] = quote; |
|
if (lastarg || *(lf->cursor) != ' ') |
|
argterm[y++] = ' '; |
|
argterm[y] = '\0'; |
|
if (y > 0 && el_insertstr(el, argterm) == -1) |
|
fatal("el_insertstr failed."); |
|
} |
|
xfree(tmp); |
|
} |
|
|
|
return count; |
|
} |
|
|
|
/* |
|
* Determine whether a particular sftp command's arguments (if any) |
|
* represent local or remote files. |
|
*/ |
|
static int |
|
complete_is_remote(char *cmd) { |
|
int i; |
|
|
|
if (cmd == NULL) |
|
return -1; |
|
|
|
for (i = 0; cmds[i].c; i++) { |
|
if (!strncasecmp(cmd, cmds[i].c, strlen(cmds[i].c))) |
|
return cmds[i].t; |
|
} |
|
|
|
return -1; |
|
} |
|
|
|
/* Autocomplete a filename "file" */ |
|
static int |
|
complete_match(EditLine *el, struct sftp_conn *conn, char *remote_path, |
|
char *file, int remote, int lastarg, char quote, int terminated) |
|
{ |
|
glob_t g; |
|
char *tmp, *tmp2, ins[3]; |
|
u_int i, hadglob, pwdlen, len, tmplen, filelen; |
|
const LineInfo *lf; |
|
|
|
/* Glob from "file" location */ |
|
if (file == NULL) |
|
tmp = xstrdup("*"); |
|
else |
|
xasprintf(&tmp, "%s*", file); |
|
|
|
memset(&g, 0, sizeof(g)); |
|
if (remote != LOCAL) { |
|
tmp = make_absolute(tmp, remote_path); |
|
remote_glob(conn, tmp, GLOB_DOOFFS|GLOB_MARK, NULL, &g); |
|
} else |
|
glob(tmp, GLOB_DOOFFS|GLOB_MARK, NULL, &g); |
|
|
|
/* Determine length of pwd so we can trim completion display */ |
|
for (hadglob = tmplen = pwdlen = 0; tmp[tmplen] != 0; tmplen++) { |
|
/* Terminate counting on first unescaped glob metacharacter */ |
|
if (tmp[tmplen] == '*' || tmp[tmplen] == '?') { |
|
if (tmp[tmplen] != '*' || tmp[tmplen + 1] != '\0') |
|
hadglob = 1; |
|
break; |
|
} |
|
if (tmp[tmplen] == '\\' && tmp[tmplen + 1] != '\0') |
|
tmplen++; |
|
if (tmp[tmplen] == '/') |
|
pwdlen = tmplen + 1; /* track last seen '/' */ |
|
} |
|
xfree(tmp); |
|
|
|
if (g.gl_matchc == 0) |
|
goto out; |
|
|
|
if (g.gl_matchc > 1) |
|
complete_display(g.gl_pathv, pwdlen); |
|
|
|
tmp = NULL; |
|
/* Don't try to extend globs */ |
|
if (file == NULL || hadglob) |
|
goto out; |
|
|
|
tmp2 = complete_ambiguous(file, g.gl_pathv, g.gl_matchc); |
|
tmp = path_strip(tmp2, remote_path); |
|
xfree(tmp2); |
|
|
|
if (tmp == NULL) |
|
goto out; |
|
|
|
tmplen = strlen(tmp); |
|
filelen = strlen(file); |
|
|
|
if (tmplen > filelen) { |
|
tmp2 = tmp + filelen; |
|
len = strlen(tmp2); |
|
/* quote argument on way out */ |
|
for (i = 0; i < len; i++) { |
|
ins[0] = '\\'; |
|
ins[1] = tmp2[i]; |
|
ins[2] = '\0'; |
|
switch (tmp2[i]) { |
|
case '\'': |
|
case '"': |
|
case '\\': |
|
case '\t': |
|
case ' ': |
|
if (quote == '\0' || tmp2[i] == quote) { |
|
if (el_insertstr(el, ins) == -1) |
|
fatal("el_insertstr " |
|
"failed."); |
|
break; |
|
} |
|
/* FALLTHROUGH */ |
|
default: |
|
if (el_insertstr(el, ins + 1) == -1) |
|
fatal("el_insertstr failed."); |
|
break; |
|
} |
|
} |
|
} |
|
|
|
lf = el_line(el); |
|
/* |
|
* XXX should we really extend here? the user may not be done if |
|
* the filename is a directory. |
|
*/ |
|
if (g.gl_matchc == 1) { |
|
i = 0; |
|
if (!terminated) |
|
ins[i++] = quote; |
|
if (lastarg || *(lf->cursor) != ' ') |
|
ins[i++] = ' '; |
|
ins[i] = '\0'; |
|
if (i > 0 && el_insertstr(el, ins) == -1) |
|
fatal("el_insertstr failed."); |
|
} |
|
xfree(tmp); |
|
|
|
out: |
|
globfree(&g); |
|
return g.gl_matchc; |
|
} |
|
|
|
/* tab-completion hook function, called via libedit */ |
|
static unsigned char |
|
complete(EditLine *el, int ch) |
|
{ |
|
char **argv, *line, quote; |
|
u_int argc, carg, cursor, len, terminated, ret = CC_ERROR; |
|
const LineInfo *lf; |
|
struct complete_ctx *complete_ctx; |
|
|
|
lf = el_line(el); |
|
if (el_get(el, EL_CLIENTDATA, (void**)&complete_ctx) != 0) |
|
fatal("%s: el_get failed", __func__); |
|
|
|
/* Figure out which argument the cursor points to */ |
|
cursor = lf->cursor - lf->buffer; |
|
line = (char *)xmalloc(cursor + 1); |
|
memcpy(line, lf->buffer, cursor); |
|
line[cursor] = '\0'; |
|
argv = makeargv(line, &carg, 1, "e, &terminated); |
|
xfree(line); |
|
|
|
/* Get all the arguments on the line */ |
|
len = lf->lastchar - lf->buffer; |
|
line = (char *)xmalloc(len + 1); |
|
memcpy(line, lf->buffer, len); |
|
line[len] = '\0'; |
|
argv = makeargv(line, &argc, 1, NULL, NULL); |
|
|
|
/* Ensure cursor is at EOL or a argument boundary */ |
|
if (line[cursor] != ' ' && line[cursor] != '\0' && |
|
line[cursor] != '\n') { |
|
xfree(line); |
|
return ret; |
|
} |
|
|
|
if (carg == 0) { |
|
/* Show all available commands */ |
|
complete_cmd_parse(el, NULL, argc == carg, '\0', 1); |
|
ret = CC_REDISPLAY; |
|
} else if (carg == 1 && cursor > 0 && line[cursor - 1] != ' ') { |
|
/* Handle the command parsing */ |
|
if (complete_cmd_parse(el, argv[0], argc == carg, |
|
quote, terminated) != 0) |
|
ret = CC_REDISPLAY; |
|
} else if (carg >= 1) { |
|
/* Handle file parsing */ |
|
int remote = complete_is_remote(argv[0]); |
|
char *filematch = NULL; |
|
|
|
if (carg > 1 && line[cursor-1] != ' ') |
|
filematch = argv[carg - 1]; |
|
|
|
if (remote != 0 && |
|
complete_match(el, complete_ctx->conn, |
|
*complete_ctx->remote_pathp, filematch, |
|
remote, carg == argc, quote, terminated) != 0) |
|
ret = CC_REDISPLAY; |
|
} |
|
|
|
xfree(line); |
|
return ret; |
|
} |
|
|
int |
int |
interactive_loop(struct sftp_conn *conn, char *file1, char *file2) |
interactive_loop(struct sftp_conn *conn, char *file1, char *file2) |
{ |
{ |
char *pwd; |
char *remote_path; |
char *dir = NULL; |
char *dir = NULL; |
char cmd[2048]; |
char cmd[2048]; |
int err, interactive; |
int err, interactive; |
|
|
History *hl = NULL; |
History *hl = NULL; |
HistEvent hev; |
HistEvent hev; |
extern char *__progname; |
extern char *__progname; |
|
struct complete_ctx complete_ctx; |
|
|
if (!batchmode && isatty(STDIN_FILENO)) { |
if (!batchmode && isatty(STDIN_FILENO)) { |
if ((el = el_init(__progname, stdin, stdout, stderr)) == NULL) |
if ((el = el_init(__progname, stdin, stdout, stderr)) == NULL) |
|
|
el_set(el, EL_TERMINAL, NULL); |
el_set(el, EL_TERMINAL, NULL); |
el_set(el, EL_SIGNAL, 1); |
el_set(el, EL_SIGNAL, 1); |
el_source(el, NULL); |
el_source(el, NULL); |
|
|
|
/* Tab Completion */ |
|
el_set(el, EL_ADDFN, "ftp-complete", |
|
"Context senstive argument completion", complete); |
|
complete_ctx.conn = conn; |
|
complete_ctx.remote_pathp = &remote_path; |
|
el_set(el, EL_CLIENTDATA, (void*)&complete_ctx); |
|
el_set(el, EL_BIND, "^I", "ftp-complete", NULL); |
} |
} |
|
|
pwd = do_realpath(conn, "."); |
remote_path = do_realpath(conn, "."); |
if (pwd == NULL) |
if (remote_path == NULL) |
fatal("Need cwd"); |
fatal("Need cwd"); |
|
|
if (file1 != NULL) { |
if (file1 != NULL) { |
dir = xstrdup(file1); |
dir = xstrdup(file1); |
dir = make_absolute(dir, pwd); |
dir = make_absolute(dir, remote_path); |
|
|
if (remote_is_dir(conn, dir) && file2 == NULL) { |
if (remote_is_dir(conn, dir) && file2 == NULL) { |
printf("Changing to: %s\n", dir); |
printf("Changing to: %s\n", dir); |
snprintf(cmd, sizeof cmd, "cd \"%s\"", dir); |
snprintf(cmd, sizeof cmd, "cd \"%s\"", dir); |
if (parse_dispatch_command(conn, cmd, &pwd, 1) != 0) { |
if (parse_dispatch_command(conn, cmd, |
|
&remote_path, 1) != 0) { |
xfree(dir); |
xfree(dir); |
xfree(pwd); |
xfree(remote_path); |
xfree(conn); |
xfree(conn); |
return (-1); |
return (-1); |
} |
} |
|
|
snprintf(cmd, sizeof cmd, "get %s %s", dir, |
snprintf(cmd, sizeof cmd, "get %s %s", dir, |
file2); |
file2); |
|
|
err = parse_dispatch_command(conn, cmd, &pwd, 1); |
err = parse_dispatch_command(conn, cmd, |
|
&remote_path, 1); |
xfree(dir); |
xfree(dir); |
xfree(pwd); |
xfree(remote_path); |
xfree(conn); |
xfree(conn); |
return (err); |
return (err); |
} |
} |
|
|
printf("\n"); |
printf("\n"); |
} |
} |
} else { |
} else { |
if ((line = el_gets(el, &count)) == NULL || count <= 0) { |
if ((line = el_gets(el, &count)) == NULL || |
|
count <= 0) { |
printf("\n"); |
printf("\n"); |
break; |
break; |
} |
} |
|
|
interrupted = 0; |
interrupted = 0; |
signal(SIGINT, cmd_interrupt); |
signal(SIGINT, cmd_interrupt); |
|
|
err = parse_dispatch_command(conn, cmd, &pwd, batchmode); |
err = parse_dispatch_command(conn, cmd, &remote_path, |
|
batchmode); |
if (err != 0) |
if (err != 0) |
break; |
break; |
} |
} |
xfree(pwd); |
xfree(remote_path); |
xfree(conn); |
xfree(conn); |
|
|
if (el != NULL) |
if (el != NULL) |