mirror of
https://github.com/fish-shell/fish-shell
synced 2024-12-27 05:13:10 +00:00
Major update to the ahead-of-time syntax checker
darcs-hash:20060521192524-ac50b-48713f826558e66ef21046d1bb779623cc2fd97a.gz
This commit is contained in:
parent
bbf2a3836f
commit
1c2cbb00bc
4 changed files with 362 additions and 63 deletions
24
expand.c
24
expand.c
|
@ -76,27 +76,6 @@ parameter expansion.
|
||||||
*/
|
*/
|
||||||
#define COMPLETE_LAST_DESC _( L"Last background job")
|
#define COMPLETE_LAST_DESC _( L"Last background job")
|
||||||
|
|
||||||
/**
|
|
||||||
Error issued on invalid variable name
|
|
||||||
*/
|
|
||||||
#define COMPLETE_VAR_DESC _( L"The '$' character begins a variable name. The character '%lc', which directly followed a '$', is not allowed as a part of a variable name, and variable names may not be zero characters long. To learn more about variable expansion in fish, type 'help expand-variable'.")
|
|
||||||
|
|
||||||
/**
|
|
||||||
Error issued on invalid variable name
|
|
||||||
*/
|
|
||||||
#define COMPLETE_VAR_NULL_DESC _( L"The '$' begins a variable name. It was given at the end of an argument. Variable names may not be zero characters long. To learn more about variable expansion in fish, type 'help expand-variable'.")
|
|
||||||
|
|
||||||
/**
|
|
||||||
Error issued on invalid variable name
|
|
||||||
*/
|
|
||||||
#define COMPLETE_VAR_BRACKET_DESC _( L"Did you mean {$VARIABLE}? The '$' character begins a variable name. A bracket, which directly followed a '$', is not allowed as a part of a variable name, and variable names may not be zero characters long. To learn more about variable expansion in fish, type 'help expand-variable'." )
|
|
||||||
|
|
||||||
/**
|
|
||||||
Error issued on invalid variable name
|
|
||||||
*/
|
|
||||||
#define COMPLETE_VAR_PARAN_DESC _( L"Did you mean (COMMAND)? In fish, the '$' character is only used for accessing variables. To learn more about command substitution in fish, type 'help expand-command-substitution'.")
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
String in process expansion denoting ourself
|
String in process expansion denoting ourself
|
||||||
*/
|
*/
|
||||||
|
@ -804,8 +783,7 @@ static int expand_variables( wchar_t *in, array_list_t *out, int last_idx )
|
||||||
{
|
{
|
||||||
error( SYNTAX_ERROR,
|
error( SYNTAX_ERROR,
|
||||||
-1,
|
-1,
|
||||||
COMPLETE_VAR_NULL_DESC,
|
COMPLETE_VAR_NULL_DESC );
|
||||||
in[stop_pos] );
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
21
expand.h
21
expand.h
|
@ -112,6 +112,27 @@ enum
|
||||||
/** String containing the character for separating two array elements */
|
/** String containing the character for separating two array elements */
|
||||||
#define ARRAY_SEP_STR L"\x1e"
|
#define ARRAY_SEP_STR L"\x1e"
|
||||||
|
|
||||||
|
/**
|
||||||
|
Error issued on invalid variable name
|
||||||
|
*/
|
||||||
|
#define COMPLETE_VAR_DESC _( L"The '$' character begins a variable name. The character '%lc', which directly followed a '$', is not allowed as a part of a variable name, and variable names may not be zero characters long. To learn more about variable expansion in fish, type 'help expand-variable'.")
|
||||||
|
|
||||||
|
/**
|
||||||
|
Error issued on invalid variable name
|
||||||
|
*/
|
||||||
|
#define COMPLETE_VAR_NULL_DESC _( L"The '$' begins a variable name. It was given at the end of an argument. Variable names may not be zero characters long. To learn more about variable expansion in fish, type 'help expand-variable'.")
|
||||||
|
|
||||||
|
/**
|
||||||
|
Error issued on invalid variable name
|
||||||
|
*/
|
||||||
|
#define COMPLETE_VAR_BRACKET_DESC _( L"Did you mean {$VARIABLE}? The '$' character begins a variable name. A bracket, which directly followed a '$', is not allowed as a part of a variable name, and variable names may not be zero characters long. To learn more about variable expansion in fish, type 'help expand-variable'." )
|
||||||
|
|
||||||
|
/**
|
||||||
|
Error issued on invalid variable name
|
||||||
|
*/
|
||||||
|
#define COMPLETE_VAR_PARAN_DESC _( L"Did you mean (COMMAND)? In fish, the '$' character is only used for accessing variables. To learn more about command substitution in fish, type 'help expand-command-substitution'.")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
322
parser.c
322
parser.c
|
@ -76,7 +76,7 @@ The fish parser. Contains functions for parsing code.
|
||||||
/**
|
/**
|
||||||
Error message for short circuit command error.
|
Error message for short circuit command error.
|
||||||
*/
|
*/
|
||||||
#define COND_ERR_MSG _( L"Short circuit command requires additional command")
|
#define COND_ERR_MSG _( L"Pipe or short circuit command requires additional command")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Error message on reaching maximum recusrion depth
|
Error message on reaching maximum recusrion depth
|
||||||
|
@ -2542,10 +2542,237 @@ int eval( const wchar_t *cmd, io_data_t *io, int block_type )
|
||||||
return code;
|
return code;
|
||||||
}
|
}
|
||||||
|
|
||||||
int parser_test( wchar_t * buff,
|
static int parser_test_argument( const wchar_t *arg, int babble )
|
||||||
|
{
|
||||||
|
wchar_t *unesc;
|
||||||
|
wchar_t *pos;
|
||||||
|
int err=0;
|
||||||
|
|
||||||
|
const wchar_t *paran_begin, *paran_end;
|
||||||
|
wchar_t *arg_cpy = wcsdup( arg );
|
||||||
|
int do_loop = 1;
|
||||||
|
|
||||||
|
while( do_loop )
|
||||||
|
{
|
||||||
|
switch( parse_util_locate_cmdsubst(arg_cpy,
|
||||||
|
¶n_begin,
|
||||||
|
¶n_end,
|
||||||
|
0 ) )
|
||||||
|
{
|
||||||
|
case -1:
|
||||||
|
err=1;
|
||||||
|
if( babble )
|
||||||
|
{
|
||||||
|
error( SYNTAX_ERROR,
|
||||||
|
-1,
|
||||||
|
L"Mismatched parans" );
|
||||||
|
print_errors();
|
||||||
|
}
|
||||||
|
free( arg_cpy );
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
case 0:
|
||||||
|
do_loop = 0;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
{
|
||||||
|
|
||||||
|
wchar_t *subst = wcsndup( paran_begin+1, paran_end-paran_begin-1 );
|
||||||
|
string_buffer_t tmp;
|
||||||
|
sb_init( &tmp );
|
||||||
|
|
||||||
|
sb_append_substring( &tmp, arg_cpy, paran_begin - arg_cpy);
|
||||||
|
sb_append_char( &tmp, INTERNAL_SEPARATOR);
|
||||||
|
sb_append( &tmp, paran_end+1);
|
||||||
|
|
||||||
|
// debug( 1, L"%ls -> %ls %ls", arg_cpy, subst, tmp.buff );
|
||||||
|
|
||||||
|
err |= parser_test( subst, babble );
|
||||||
|
|
||||||
|
free( subst );
|
||||||
|
free( arg_cpy );
|
||||||
|
arg_cpy = (wchar_t *)tmp.buff;
|
||||||
|
|
||||||
|
/*
|
||||||
|
Do _not_ call sb_destroy on this stringbuffer - it's
|
||||||
|
buffer is used as the new 'arg_cpy'.
|
||||||
|
*/
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unesc = unescape( arg_cpy, 1 );
|
||||||
|
free( arg_cpy );
|
||||||
|
|
||||||
|
/*
|
||||||
|
Check for invalid variable expansions
|
||||||
|
*/
|
||||||
|
for( pos = unesc; *pos; pos++ )
|
||||||
|
{
|
||||||
|
switch( *pos )
|
||||||
|
{
|
||||||
|
case VARIABLE_EXPAND:
|
||||||
|
case VARIABLE_EXPAND_SINGLE:
|
||||||
|
{
|
||||||
|
switch( *(pos+1))
|
||||||
|
{
|
||||||
|
case BRACKET_BEGIN:
|
||||||
|
{
|
||||||
|
err=1;
|
||||||
|
if( babble )
|
||||||
|
{
|
||||||
|
error( SYNTAX_ERROR,
|
||||||
|
-1,
|
||||||
|
COMPLETE_VAR_BRACKET_DESC );
|
||||||
|
print_errors();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case INTERNAL_SEPARATOR:
|
||||||
|
{
|
||||||
|
err=1;
|
||||||
|
if( babble )
|
||||||
|
{
|
||||||
|
error( SYNTAX_ERROR,
|
||||||
|
-1,
|
||||||
|
COMPLETE_VAR_PARAN_DESC );
|
||||||
|
print_errors();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 0:
|
||||||
|
{
|
||||||
|
err=1;
|
||||||
|
if( babble )
|
||||||
|
{
|
||||||
|
error( SYNTAX_ERROR,
|
||||||
|
-1,
|
||||||
|
COMPLETE_VAR_NULL_DESC );
|
||||||
|
print_errors();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
if( !iswalnum(*(pos+1)) &&
|
||||||
|
*(pos+1)!=L'_' &&
|
||||||
|
*(pos+1)!=VARIABLE_EXPAND &&
|
||||||
|
*(pos+1)!=VARIABLE_EXPAND_SINGLE )
|
||||||
|
{
|
||||||
|
err=1;
|
||||||
|
if( babble )
|
||||||
|
{
|
||||||
|
error( SYNTAX_ERROR,
|
||||||
|
-1,
|
||||||
|
COMPLETE_VAR_DESC,
|
||||||
|
*(pos+1) );
|
||||||
|
print_errors();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
free( unesc );
|
||||||
|
return err;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
int parser_test_args(const wchar_t * buff,
|
||||||
int babble )
|
int babble )
|
||||||
{
|
{
|
||||||
tokenizer tok;
|
tokenizer tok;
|
||||||
|
tokenizer *previous_tokenizer = current_tokenizer;
|
||||||
|
int previous_pos = current_tokenizer_pos;
|
||||||
|
int do_loop = 1;
|
||||||
|
int err = 0;
|
||||||
|
|
||||||
|
current_tokenizer = &tok;
|
||||||
|
|
||||||
|
for( tok_init( &tok, buff, 0 );
|
||||||
|
do_loop && tok_has_next( &tok );
|
||||||
|
tok_next( &tok ) )
|
||||||
|
{
|
||||||
|
current_tokenizer_pos = tok_get_pos( &tok );
|
||||||
|
switch( tok_last_type( &tok ) )
|
||||||
|
{
|
||||||
|
|
||||||
|
case TOK_STRING:
|
||||||
|
{
|
||||||
|
err |= parser_test_argument( tok_last( &tok ), babble );
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case TOK_END:
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case TOK_ERROR:
|
||||||
|
{
|
||||||
|
if( babble )
|
||||||
|
{
|
||||||
|
error( SYNTAX_ERROR,
|
||||||
|
tok_get_pos( &tok ),
|
||||||
|
TOK_ERR_MSG,
|
||||||
|
tok_last(&tok) );
|
||||||
|
print_errors();
|
||||||
|
}
|
||||||
|
err=1;
|
||||||
|
do_loop=0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
if( babble )
|
||||||
|
{
|
||||||
|
error( SYNTAX_ERROR,
|
||||||
|
tok_get_pos( &tok ),
|
||||||
|
UNEXPECTED_TOKEN_ERR_MSG,
|
||||||
|
tok_get_desc( tok_last_type(&tok)) );
|
||||||
|
print_errors();
|
||||||
|
}
|
||||||
|
err=1;
|
||||||
|
do_loop=0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tok_destroy( &tok );
|
||||||
|
|
||||||
|
current_tokenizer=previous_tokenizer;
|
||||||
|
current_tokenizer_pos = previous_pos;
|
||||||
|
|
||||||
|
error_code=0;
|
||||||
|
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
int parser_test( const wchar_t * buff,
|
||||||
|
int babble )
|
||||||
|
{
|
||||||
|
tokenizer tok;
|
||||||
|
/*
|
||||||
|
Set to one if a command name has been given for the currently
|
||||||
|
parsed process specification
|
||||||
|
*/
|
||||||
int had_cmd=0;
|
int had_cmd=0;
|
||||||
int count = 0;
|
int count = 0;
|
||||||
int err=0;
|
int err=0;
|
||||||
|
@ -2554,9 +2781,15 @@ int parser_test( wchar_t * buff,
|
||||||
static int block_pos[BLOCK_MAX_COUNT];
|
static int block_pos[BLOCK_MAX_COUNT];
|
||||||
static int block_type[BLOCK_MAX_COUNT];
|
static int block_type[BLOCK_MAX_COUNT];
|
||||||
int is_pipeline = 0;
|
int is_pipeline = 0;
|
||||||
|
/*
|
||||||
|
Set to one if the currently specified process can not be used inside a pipeline
|
||||||
|
*/
|
||||||
int forbid_pipeline = 0;
|
int forbid_pipeline = 0;
|
||||||
|
/*
|
||||||
|
Set to one if an additional process specification is needed
|
||||||
|
*/
|
||||||
int needs_cmd=0;
|
int needs_cmd=0;
|
||||||
int require_additional_commands=0;
|
void *context = halloc( 0, 0 );
|
||||||
|
|
||||||
current_tokenizer = &tok;
|
current_tokenizer = &tok;
|
||||||
|
|
||||||
|
@ -2576,7 +2809,23 @@ int parser_test( wchar_t * buff,
|
||||||
int mark = tok_get_pos( &tok );
|
int mark = tok_get_pos( &tok );
|
||||||
had_cmd = 1;
|
had_cmd = 1;
|
||||||
|
|
||||||
if( require_additional_commands )
|
if( !expand_one( context,
|
||||||
|
wcsdup( tok_last( &tok ) ),
|
||||||
|
EXPAND_SKIP_SUBSHELL | EXPAND_SKIP_VARIABLES ) )
|
||||||
|
{
|
||||||
|
err=1;
|
||||||
|
if( babble )
|
||||||
|
{
|
||||||
|
error( SYNTAX_ERROR,
|
||||||
|
tok_get_pos( &tok ),
|
||||||
|
ILLEGAL_CMD_ERR_MSG,
|
||||||
|
tok_last( &tok ) );
|
||||||
|
|
||||||
|
print_errors();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if( needs_cmd )
|
||||||
{
|
{
|
||||||
if( contains_str( tok_last(&tok),
|
if( contains_str( tok_last(&tok),
|
||||||
L"end",
|
L"end",
|
||||||
|
@ -2593,7 +2842,7 @@ int parser_test( wchar_t * buff,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
require_additional_commands--;
|
needs_cmd=0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -2638,7 +2887,6 @@ int parser_test( wchar_t * buff,
|
||||||
|
|
||||||
// debug( 2, L"add block of type %d after cmd %ls\n", block_type[count], tok_last(&tok) );
|
// debug( 2, L"add block of type %d after cmd %ls\n", block_type[count], tok_last(&tok) );
|
||||||
|
|
||||||
|
|
||||||
block_pos[count] = current_tokenizer_pos;
|
block_pos[count] = current_tokenizer_pos;
|
||||||
tok_next( &tok );
|
tok_next( &tok );
|
||||||
count++;
|
count++;
|
||||||
|
@ -2658,9 +2906,6 @@ int parser_test( wchar_t * buff,
|
||||||
had_cmd = 0;
|
had_cmd = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
The short circuit commands requires _two_ additional commands.
|
|
||||||
*/
|
|
||||||
if( contains_str( tok_last( &tok ),
|
if( contains_str( tok_last( &tok ),
|
||||||
L"or",
|
L"or",
|
||||||
L"and",
|
L"and",
|
||||||
|
@ -2679,12 +2924,11 @@ int parser_test( wchar_t * buff,
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
require_additional_commands=1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
There are a lot of situations where pipelines
|
There are a lot of situations where pipelines
|
||||||
are forbidden, inclusing when using the exec
|
are forbidden, including when using the exec
|
||||||
builtin.
|
builtin.
|
||||||
*/
|
*/
|
||||||
if( parser_is_pipe_forbidden( tok_last( &tok ) ) )
|
if( parser_is_pipe_forbidden( tok_last( &tok ) ) )
|
||||||
|
@ -2795,6 +3039,11 @@ int parser_test( wchar_t * buff,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
err = parser_test_argument( tok_last( &tok ), babble );
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2840,7 +3089,31 @@ int parser_test( wchar_t * buff,
|
||||||
|
|
||||||
case TOK_PIPE:
|
case TOK_PIPE:
|
||||||
{
|
{
|
||||||
if( forbid_pipeline )
|
if( !had_cmd )
|
||||||
|
{
|
||||||
|
err=1;
|
||||||
|
if( babble )
|
||||||
|
{
|
||||||
|
if( tok_get_pos(&tok)>0 && buff[tok_get_pos(&tok)-1] == L'|' )
|
||||||
|
{
|
||||||
|
error( SYNTAX_ERROR,
|
||||||
|
tok_get_pos( &tok ),
|
||||||
|
CMD_OR_ERR_MSG,
|
||||||
|
tok_get_desc( tok_last_type(&tok) ) );
|
||||||
|
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
error( SYNTAX_ERROR,
|
||||||
|
tok_get_pos( &tok ),
|
||||||
|
CMD_ERR_MSG,
|
||||||
|
tok_get_desc( tok_last_type(&tok)));
|
||||||
|
}
|
||||||
|
|
||||||
|
print_errors();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if( forbid_pipeline )
|
||||||
{
|
{
|
||||||
err=1;
|
err=1;
|
||||||
if( babble )
|
if( babble )
|
||||||
|
@ -2852,22 +3125,37 @@ int parser_test( wchar_t * buff,
|
||||||
print_errors();
|
print_errors();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
needs_cmd=0;
|
else
|
||||||
|
{
|
||||||
|
needs_cmd=1;
|
||||||
is_pipeline=1;
|
is_pipeline=1;
|
||||||
|
had_cmd=0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
case TOK_BACKGROUND:
|
case TOK_BACKGROUND:
|
||||||
{
|
{
|
||||||
if( needs_cmd && !had_cmd )
|
if( !had_cmd )
|
||||||
{
|
{
|
||||||
err = 1;
|
err = 1;
|
||||||
if( babble )
|
if( babble )
|
||||||
|
{
|
||||||
|
if( tok_get_pos(&tok)>0 && buff[tok_get_pos(&tok)-1] == L'&' )
|
||||||
|
{
|
||||||
|
error( SYNTAX_ERROR,
|
||||||
|
tok_get_pos( &tok ),
|
||||||
|
CMD_AND_ERR_MSG,
|
||||||
|
tok_get_desc( tok_last_type(&tok) ) );
|
||||||
|
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
error( SYNTAX_ERROR,
|
error( SYNTAX_ERROR,
|
||||||
tok_get_pos( &tok ),
|
tok_get_pos( &tok ),
|
||||||
CMD_ERR_MSG,
|
CMD_ERR_MSG,
|
||||||
tok_get_desc( tok_last_type(&tok)));
|
tok_get_desc( tok_last_type(&tok)));
|
||||||
|
}
|
||||||
|
|
||||||
print_errors();
|
print_errors();
|
||||||
}
|
}
|
||||||
|
@ -2898,7 +3186,7 @@ int parser_test( wchar_t * buff,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if( require_additional_commands )
|
if( needs_cmd )
|
||||||
{
|
{
|
||||||
err=1;
|
err=1;
|
||||||
if( babble )
|
if( babble )
|
||||||
|
@ -2928,6 +3216,8 @@ int parser_test( wchar_t * buff,
|
||||||
|
|
||||||
error_code=0;
|
error_code=0;
|
||||||
|
|
||||||
|
halloc_free( context );
|
||||||
|
|
||||||
return err | ((count!=0)<<1);
|
return err | ((count!=0)<<1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
16
parser.h
16
parser.h
|
@ -315,10 +315,20 @@ const wchar_t *parser_get_block_desc( int block );
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Test if the specified string can be parsed, or if more bytes need to be read first.
|
Test if the specified string can be parsed, or if more bytes need
|
||||||
The result has the first bit set if the string contains errors, and the second bit is set if the string contains an unclosed block.
|
to be read first. The result has the first bit set if the string
|
||||||
|
contains errors, and the second bit is set if the string contains
|
||||||
|
an unclosed block.
|
||||||
*/
|
*/
|
||||||
int parser_test( wchar_t * buff, int babble );
|
int parser_test( const wchar_t * buff, int babble );
|
||||||
|
|
||||||
|
/**
|
||||||
|
Test if the specified string can be parsed as an argument list,
|
||||||
|
e.g. sent to eval_args. The result has the first bit set if the
|
||||||
|
string contains errors, and the second bit is set if the string
|
||||||
|
contains an unclosed block.
|
||||||
|
*/
|
||||||
|
int parser_test_args( const wchar_t * buff, int babble );
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Returns the full path of the specified directory. If the \c in is a
|
Returns the full path of the specified directory. If the \c in is a
|
||||||
|
|
Loading…
Reference in a new issue