Implemented process expansion on OS X

Also fixed issue where process expansion would always fail for processes with spaces
Fixes https://github.com/fish-shell/fish-shell/issues/56
This commit is contained in:
ridiculousfish 2012-07-16 12:05:36 -07:00
parent 548ea1e54a
commit 33c6410809
5 changed files with 367 additions and 209 deletions

View file

@ -136,6 +136,40 @@ wcstring_list_t completions_to_wcstring_list( const std::vector<completion_t> &l
return strings;
}
int fgetws2(wcstring *s, FILE *f)
{
int i=0;
wint_t c;
while( 1 )
{
errno=0;
c = getwc( f );
if( errno == EILSEQ )
{
continue;
}
switch( c )
{
/* End of line */
case WEOF:
case L'\n':
case L'\0':
return i;
/* Ignore carriage returns */
case L'\r':
break;
default:
i++;
s->push_back((wchar_t)c);
break;
}
}
}
int fgetws2( wchar_t **b, int *len, FILE *f )
{

View file

@ -206,6 +206,9 @@ wcstring_list_t completions_to_wcstring_list( const std::vector<completion_t> &c
*/
int fgetws2( wchar_t **buff, int *len, FILE *f );
/** Like fgetws2, but reads into a string */
int fgetws2(wcstring *s, FILE *f);
void sort_strings( std::vector<wcstring> &strings);
void sort_completions( std::vector<completion_t> &strings);

View file

@ -16,11 +16,13 @@ parameter expansion.
#include <pwd.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/sysctl.h>
#include <termios.h>
#include <dirent.h>
#include <sys/stat.h>
#include <unistd.h>
#include <signal.h>
#include <algorithm>
#include <assert.h>
#include <vector>
@ -121,6 +123,8 @@ parameter expansion.
*/
#define UNCLEAN L"$*?\\\"'({})"
static void remove_internal_separator( wcstring &s, bool conv );
int expand_is_clean( const wchar_t *in )
{
@ -259,59 +263,288 @@ static int iswnumeric( const wchar_t *n )
See if the process described by \c proc matches the commandline \c
cmd
*/
static int match_pid( const wchar_t *cmd,
const wchar_t *proc,
int flags,
int *offset)
static bool match_pid( const wchar_t *cmd,
const wchar_t *proc,
int flags,
int *offset)
{
/* Test for direct match */
if( wcsncmp( cmd, proc, wcslen( proc ) ) == 0 )
/* Test for a direct match. If the proc string is empty (e.g. the user tries to complete against %), then return an offset pointing at the base command. That ensures that you don't see a bunch of dumb paths when completing against all processes. */
if( proc[0] != L'\0' && wcsncmp( cmd, proc, wcslen( proc ) ) == 0 )
{
if( offset )
*offset = 0;
return 1;
return true;
}
/*
Test if the commandline is a path to the command, if so we try
to match against only the command part
*/
wchar_t *first_token = tok_first( cmd );
if( first_token == 0 )
return 0;
/* Get the command to match against. We're only interested in the last path component. */
const wcstring base_cmd = wbasename(cmd);
wchar_t *start=0;
wchar_t prev=0;
wchar_t *p;
bool result = string_prefixes_string(proc, base_cmd);
if (result)
{
/* It's a match. Return the offset within the full command. */
if (offset)
*offset = wcslen(cmd) - base_cmd.size();
}
return result;
}
/*
This should be done by basename(), if it wasn't for the fact
that is does not accept wide strings
*/
for( p=first_token; *p; p++ )
{
if( *p == L'/' && prev != L'\\' )
start = p;
prev = *p;
}
if( start )
{
/** Helper class for iterating over processes. The names returned have been unescaped (e.g. may include spaces) */
#ifdef KERN_PROCARGS2
if( wcsncmp( start+1, proc, wcslen( proc ) ) == 0 )
{
if( offset )
*offset = start+1-first_token;
/* BSD / OS X process completions */
free( first_token );
class process_iterator_t {
std::vector<pid_t> pids;
size_t idx;
return 1;
}
}
free( first_token );
wcstring name_for_pid(pid_t pid);
return 0;
public:
process_iterator_t();
bool next_process(wcstring *str, pid_t *pid);
};
wcstring process_iterator_t::name_for_pid(pid_t pid)
{
wcstring result;
int mib[4], maxarg = 0, numArgs = 0;
size_t size = 0;
char *args = NULL, *stringPtr = NULL;
mib[0] = CTL_KERN;
mib[1] = KERN_ARGMAX;
size = sizeof(maxarg);
if (sysctl(mib, 2, &maxarg, &size, NULL, 0) == -1) {
return result;
}
args = (char *)malloc( maxarg );
if ( args == NULL ) {
return result;
}
mib[0] = CTL_KERN;
mib[1] = KERN_PROCARGS2;
mib[2] = pid;
size = (size_t)maxarg;
if ( sysctl(mib, 3, args, &size, NULL, 0) == -1 ) {
free( args );
return result;;
}
memcpy( &numArgs, args, sizeof(numArgs) );
stringPtr = args + sizeof(numArgs);
result = str2wcstring(stringPtr);
free(args);
return result;
}
bool process_iterator_t::next_process(wcstring *out_str, pid_t *out_pid)
{
wcstring name;
pid_t pid = 0;
bool result = false;
while (idx < pids.size())
{
pid = pids.at(idx++);
name = name_for_pid(pid);
if (! name.empty())
{
result = true;
break;
}
}
if (result)
{
*out_str = name;
*out_pid = pid;
}
return result;
}
process_iterator_t::process_iterator_t() : idx(0)
{
int err;
struct kinfo_proc * result;
bool done;
static const int name[] = { CTL_KERN, KERN_PROC, KERN_PROC_ALL, 0 };
// Declaring name as const requires us to cast it when passing it to
// sysctl because the prototype doesn't include the const modifier.
size_t length;
// We start by calling sysctl with result == NULL and length == 0.
// That will succeed, and set length to the appropriate length.
// We then allocate a buffer of that size and call sysctl again
// with that buffer. If that succeeds, we're done. If that fails
// with ENOMEM, we have to throw away our buffer and loop. Note
// that the loop causes use to call sysctl with NULL again; this
// is necessary because the ENOMEM failure case sets length to
// the amount of data returned, not the amount of data that
// could have been returned.
result = NULL;
done = false;
do {
assert(result == NULL);
// Call sysctl with a NULL buffer.
length = 0;
err = sysctl( (int *) name, (sizeof(name) / sizeof(*name)) - 1,
NULL, &length,
NULL, 0);
if (err == -1) {
err = errno;
}
// Allocate an appropriately sized buffer based on the results
// from the previous call.
if (err == 0) {
result = (struct kinfo_proc *)malloc(length);
if (result == NULL) {
err = ENOMEM;
}
}
// Call sysctl again with the new buffer. If we get an ENOMEM
// error, toss away our buffer and start again.
if (err == 0) {
err = sysctl( (int *) name, (sizeof(name) / sizeof(*name)) - 1,
result, &length,
NULL, 0);
if (err == -1) {
err = errno;
}
if (err == 0) {
done = true;
} else if (err == ENOMEM) {
assert(result != NULL);
free(result);
result = NULL;
err = 0;
}
}
} while (err == 0 && ! done);
// Clean up and establish post conditions.
if (err == 0 && result != NULL)
{
for (size_t idx = 0; idx < length / sizeof(struct kinfo_proc); idx++)
pids.push_back(result[idx].kp_proc.p_pid);
}
if (result)
free(result);
}
#else
/* /proc style process completions */
class process_iterator_t {
DIR *dir;
public:
process_iterator_t();
~process_iterator_t();
bool next_process(wcstring *out_str, pid_t *out_pid);
};
process_iterator_t::process_iterator_t(void)
{
dir = opendir( "/proc" );
}
process_iterator_t::~process_iterator_t(void)
{
if (dir)
closedir(dir);
}
bool process_iterator_t::next_process(wcstring *out_str, pid_t *out_pid)
{
wcstring cmd;
pid_t pid = 0;
while (cmd.empty())
{
wcstring name;
if (! dir || ! wreaddir(dir, name))
break;
if (!iswnumeric(name.c_str()))
continue;
wcstring path = wcstring(L"/proc/") + name;
struct stat buf;
if (wstat(path, &buf))
continue;
if( buf.st_uid != getuid() )
continue;
/* remember the pid */
pid = (long)wcstol(name.c_str(), NULL, 10);
/* the 'cmdline' file exists, it should contain the commandline */
FILE *cmdfile;
if ((cmdfile=wfopen(path + L"/cmdline", "r"))==0)
{
wcstring full_command_line;
signal_block();
fgetws2(&full_command_line, cmdfile);
signal_unblock();
/* The command line needs to be escaped */
wchar_t *first_arg = tok_first( full_command_line.c_str() );
if (first_arg)
{
cmd = first_arg;
free(first_arg);
}
}
#ifdef SunOS
else if ((cmdfile=wfopen(path + L"/psinfo", "r"))==0)
{
psinfo_t info;
if (fread(&info, sizeof(info), 1, cmdfile))
{
/* The filename is unescaped */
cmd = str2wcstring(info.pr_fname);
}
}
#endif
if (cmdfile)
fclose(cmdfile);
}
bool result = ! cmd.empty();
if (result)
{
*out_str = cmd;
*out_pid = pid;
}
return result;
}
#endif
std::vector<wcstring> expand_get_all_process_names(void)
{
wcstring name;
pid_t pid;
process_iterator_t iterator;
std::vector<wcstring> result;
while (iterator.next_process(&name, &pid))
{
result.push_back(name);
}
return result;
}
/**
@ -332,11 +565,6 @@ static int find_process( const wchar_t *proc,
int flags,
std::vector<completion_t> &out )
{
DIR *dir;
wchar_t *pdir_name;
wchar_t *pfile_name;
wchar_t *cmd=0;
int sz=0;
int found = 0;
const job_t *j;
@ -400,7 +628,7 @@ static int find_process( const wchar_t *proc,
{
int offset;
if( j->command_wcstr() == 0 )
if( j->command_is_empty() )
continue;
if( match_pid( j->command_wcstr(), proc, flags, &offset ) )
@ -463,122 +691,29 @@ static int find_process( const wchar_t *proc,
return 1;
}
if( !(dir = opendir( "/proc" )))
{
/*
This system does not have a /proc filesystem.
*/
return 1;
}
/* Iterate over all processes */
wcstring process_name;
pid_t process_pid;
process_iterator_t iterator;
while (iterator.next_process(&process_name, &process_pid))
{
int offset;
pdir_name = (wchar_t *)malloc( sizeof(wchar_t)*256 );
pfile_name = (wchar_t *)malloc( sizeof(wchar_t)*64 );
wcscpy( pdir_name, L"/proc/" );
wcstring nameStr;
while (wreaddir(dir, nameStr))
{
const wchar_t *name = nameStr.c_str();
struct stat buf;
if( !iswnumeric( name ) )
continue;
wcscpy( pdir_name + 6, name );
if( wstat( pdir_name, &buf ) )
{
continue;
}
if( buf.st_uid != getuid() )
{
continue;
}
wcscpy( pfile_name, pdir_name );
wcscat( pfile_name, L"/cmdline" );
if( !wstat( pfile_name, &buf ) )
{
/*
the 'cmdline' file exists, it should contain the commandline
*/
FILE *cmdfile;
if((cmdfile=wfopen( pfile_name, "r" ))==0)
{
wperror( L"fopen" );
continue;
}
signal_block();
fgetws2( &cmd, &sz, cmdfile );
signal_unblock();
fclose( cmdfile );
}
else
{
#ifdef SunOS
wcscpy( pfile_name, pdir_name );
wcscat( pfile_name, L"/psinfo" );
if( !wstat( pfile_name, &buf ) )
{
psinfo_t info;
FILE *psfile;
if((psfile=wfopen( pfile_name, "r" ))==0)
{
wperror( L"fopen" );
continue;
}
if( fread( &info, sizeof(info), 1, psfile ) )
{
if( cmd != 0 )
free( cmd );
cmd = str2wcs( info.pr_fname );
}
fclose( psfile );
}
else
#endif
{
if( cmd != 0 )
{
*cmd = 0;
}
}
}
if( cmd != 0 )
{
int offset;
if( match_pid( cmd, proc, flags, &offset ) )
{
if( flags & ACCEPT_INCOMPLETE )
{
completion_allocate( out,
cmd + offset + wcslen(proc),
COMPLETE_PROCESS_DESC,
0 );
}
else
{
if (name)
out.push_back(completion_t(name));
}
}
}
}
if( cmd != 0 )
free( cmd );
free( pdir_name );
free( pfile_name );
closedir( dir );
if( match_pid( process_name.c_str(), proc, flags, &offset ) )
{
if( flags & ACCEPT_INCOMPLETE )
{
completion_allocate( out,
process_name.c_str() + offset + wcslen(proc),
COMPLETE_PROCESS_DESC,
0 );
}
else
{
out.push_back(completion_t(to_string((long)process_pid)));
}
}
}
return 1;
}
@ -586,11 +721,15 @@ static int find_process( const wchar_t *proc,
/**
Process id expansion
*/
static int expand_pid( const wcstring &instr,
static int expand_pid( const wcstring &instr_with_sep,
int flags,
std::vector<completion_t> &out )
{
/* expand_string calls us with internal separators in instr...sigh */
wcstring instr = instr_with_sep;
remove_internal_separator(instr, false);
if( instr.empty() || instr.at(0) != PROCESS_EXPAND )
{
out.push_back(completion_t(instr));
@ -1366,49 +1505,28 @@ void expand_tilde( wcstring &input)
Remove any internal separators. Also optionally convert wildcard characters to
regular equivalents. This is done to support EXPAND_SKIP_WILDCARDS.
*/
static void remove_internal_separator( const void *s, int conv )
static void remove_internal_separator( wcstring &str, bool conv )
{
wchar_t *in = (wchar_t *)s;
wchar_t *out=in;
/* Remove all instances of INTERNAL_SEPARATOR */
str.erase(std::remove(str.begin(), str.end(), (wchar_t)INTERNAL_SEPARATOR), str.end());
CHECK( s, );
while( *in )
{
switch( *in )
{
case INTERNAL_SEPARATOR:
in++;
break;
case ANY_CHAR:
in++;
*out++ = conv?L'?':ANY_CHAR;
break;
case ANY_STRING:
in++;
*out++ = conv?L'*':ANY_STRING;
break;
case ANY_STRING_RECURSIVE:
in++;
*out++ = conv?L'*':ANY_STRING_RECURSIVE;
break;
default:
*out++ = *in++;
}
}
*out=0;
}
static void remove_internal_separator2( wcstring &s, int conv )
{
wchar_t *tmp = wcsdup(s.c_str());
remove_internal_separator(tmp, conv);
s = tmp;
free(tmp);
/* If conv is true, replace all instances of ANY_CHAR with '?', ANY_STRING with '*', ANY_STRING_RECURSIVE with '*' */
if (conv)
{
for (size_t idx = 0; idx < str.size(); idx++)
{
switch (str.at(idx))
{
case ANY_CHAR:
str.at(idx) = L'?';
break;
case ANY_STRING:
case ANY_STRING_RECURSIVE:
str.at(idx) = L'*';
break;
}
}
}
}
@ -1542,7 +1660,7 @@ int expand_string( const wcstring &input, std::vector<completion_t> &output, exp
wcstring next_str = in->at(i).completion;
int wc_res;
remove_internal_separator2( next_str, EXPAND_SKIP_WILDCARDS & flags );
remove_internal_separator( next_str, (EXPAND_SKIP_WILDCARDS & flags) ? true : false );
const wchar_t *next = next_str.c_str();
if( ((flags & ACCEPT_INCOMPLETE) && (!(flags & EXPAND_SKIP_WILDCARDS))) ||

View file

@ -192,5 +192,9 @@ int expand_is_clean( const wchar_t *in );
*/
void expand_variable_error( parser_t &parser, const wchar_t *token, int token_pos, int error_pos );
/**
Testing function for getting all process names.
*/
std::vector<wcstring> expand_get_all_process_names(void);
#endif

View file

@ -696,7 +696,6 @@ wchar_t *tok_first( const wchar_t *str )
return res;
}
int tok_get_pos( tokenizer *tok )
{
CHECK( tok, 0 );