Skip to content

Instantly share code, notes, and snippets.

@fonic
Last active February 13, 2020 11:04
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save fonic/a01a7a45e09289635cf19d7878e85103 to your computer and use it in GitHub Desktop.
Save fonic/a01a7a45e09289635cf19d7878e85103 to your computer and use it in GitHub Desktop.
C Run Command Function and Wrappers (Stack Overflow: https://stackoverflow.com/q/59576159/1976617)
/**************************************************************************
* *
* C Run Command Function and Wrappers *
* *
* Related Stack Overflow question: *
* https://stackoverflow.com/q/59576159/1976617 *
* *
* Created by Fonic <https://github.com/fonic> *
* Date: 01/07/20 *
* *
**************************************************************************/
/***************************************
* *
* Includes *
* *
***************************************/
#include <stdarg.h> // va_start, va_end, va_list, ...
#include <stdio.h> // stdout, stderr, stdin, EOF, fdopen
#include <stdlib.h> // exit, free
#include <unistd.h> // exec*, pipe, fork, dup, close
#include <sys/wait.h> // wait, waitpid
#include <string.h> // strerror
#include <errno.h> // errno
#include <signal.h> // kill, SIGKILL
/***************************************
* *
* Functions *
* *
***************************************/
// Debug output
static FILE *dbg_dst = NULL;
#define debugf(format, ...) fprintf(dbg_dst, "[%s] [%s:%-3d:%s] "format"\n", "DEBUG", __FILE__, __LINE__, __func__, ##__VA_ARGS__)
#define infof(format, ...) fprintf(dbg_dst, "[%s] [%s:%-3d:%s] "format"\n", "INFO ", __FILE__, __LINE__, __func__, ##__VA_ARGS__)
#define warnf(format, ...) fprintf(dbg_dst, "[%s] [%s:%-3d:%s] "format"\n", "WARN ", __FILE__, __LINE__, __func__, ##__VA_ARGS__)
#define errorf(format, ...) fprintf(dbg_dst, "[%s] [%s:%-3d:%s] "format"\n", "ERROR", __FILE__, __LINE__, __func__, ##__VA_ARGS__)
// Runs command with arguments, returns output and return value (arguments provided as array)
// NOTE: array of arguments must end with NULL element
// NOTE: both stdout and stderr of executed command are captured and stored in output
// NOTE: if output != NULL, (*output) is allocated using malloc and has to be freed afterwards by caller
#define RUN_COMMAND_INITIAL_BUFFER_SIZE 4096
int run_command(char **output, int *retval, const char *command, const char* const args[])
{
int pipe_fds[2];
pid_t pid;
debugf("Running command '%s':", command);
// Create pipe for parent-child communication
debugf("Creating pipe...");
if (pipe(pipe_fds) == -1) {
errorf("Failed to create pipe: %s", strerror(errno));
return 1;
}
// Fork into parent and child
debugf("Forking into parent and child...");
pid = fork();
if (pid == -1) {
errorf("Failed to fork: %s", strerror(errno));
return 1;
}
// Child
if (pid == 0) {
pid_t cpid = getpid();
int dbg_dst_fd_backup = -1;
FILE *dbg_dst_file_backup = NULL;
debugf("Child: pid: %d", cpid);
// Backup current debug output destination, redirect debug output to backup
// NOTE: without this, debug output would be sent to parent due to redirection of stdout/stderr below
if (dbg_dst != NULL) {
debugf("Child: backing up debug output destination, redirecting debug output to backup...");
int dbg_dst_fd = fileno(dbg_dst);
if (dbg_dst_fd == -1) {
errorf("Child: failed to determine file descriptor of debug output destination: %s", strerror(errno));
goto child_error;
}
dbg_dst_fd_backup = dup(dbg_dst_fd);
if (dbg_dst_fd_backup == -1) {
errorf("Child: failed to duplicate file descriptor of debug output destination: %s", strerror(errno));
goto child_error;
}
dbg_dst_file_backup = fdopen(dbg_dst_fd_backup, "w");
if (dbg_dst_fd_backup == -1) {
errorf("Child: failed to open backup of debug output destination for writing: %s", strerror(errno));
goto child_error;
}
dbg_dst = dbg_dst_file_backup;
}
// Redirect stdout/stderr to pipe -> send output to parent
debugf("Child: redirecting stdout to pipe...");
if (dup2(pipe_fds[1], STDOUT_FILENO) == -1) {
errorf("Child: failed to redirect stdout: %s", strerror(errno));
goto child_error;
}
debugf("Child: redirecting stderr to pipe...");
if (dup2(pipe_fds[1], STDERR_FILENO) == -1) {
errorf("Child: failed to redirect stderr: %s", strerror(errno));
goto child_error;
}
// Close both pipe ends (pipe_fds[0] belongs to parent, pipe_fds[1] no longer needed after dup2)
debugf("Child: closing both pipe ends...");
close(pipe_fds[0]);
close(pipe_fds[1]);
pipe_fds[0] = pipe_fds[1] = -1;
// Set buffering for stdout/stderr
debugf("Child: setting buffering for stdout...");
if (setvbuf(stdout, NULL, _IONBF, 0) != 0) {
errorf("Child: failed to set buffering for stdout: %s", strerror(errno));
goto child_error;
}
debugf("Child: setting buffering for stderr...");
if (setvbuf(stderr, NULL, _IONBF, 0) != 0) {
errorf("Child: failed to set buffering for stderr: %s", strerror(errno));
goto child_error;
}
// Execute command -> replaces child process, execution ends on execvp line
// NOTE: execvp works like shell, i.e. PATH is searched if no absolute path to executable provided
debugf("Child: executing command...");
execvp(command, (char * const *)args);
errorf("Child: failed to execute command: %s", strerror(errno));
// Terminate child (will only be reached in case of an error)
child_error:
if (pipe_fds[0] != -1)
close(pipe_fds[0]);
if (pipe_fds[1] != -1)
close(pipe_fds[1]);
if (dbg_dst_fd_backup != -1)
close(dbg_dst_fd_backup);
if (dbg_dst_file_backup != NULL)
fclose(dbg_dst_file_backup);
exit(1);
}
// Parent
FILE *pipe_file = NULL;
char *buffer = NULL;
long unsigned int buf_size;
long unsigned int buf_data;
long unsigned int buf_free;
int wstatus;
debugf("Parent: child created (pid: %i)", pid);
// Close child's side of pipe
debugf("Parent: closing child's side of pipe...");
close(pipe_fds[1]);
// Open parent's side of pipe for reading
debugf("Parent: opening pipe for reading...");
pipe_file = fdopen(pipe_fds[0], "r");
if (pipe_file == NULL) {
errorf("Parent: failed to open pipe for reading: %s", strerror(errno));
goto parent_error;
}
// Allocate buffer to store child's output
debugf("Parent: allocating buffer...");
buf_size = RUN_COMMAND_INITIAL_BUFFER_SIZE;
buf_data = 0;
buf_free = buf_size;
buffer = (char *) malloc(buf_size);
if (buffer == NULL) {
errorf("Parent: failed to allocate buffer");
goto parent_error;
}
// Read from parent's side of pipe until EOF
debugf("Parent: reading from pipe...");
while (!feof(pipe_file)) {
debugf("Parent: buf_size: %lu, buf_data: %lu, buf_free: %lu", buf_size, buf_data, buf_free);
int n = fread(buffer + buf_data, 1, buf_free, pipe_file);
if (n <= 0 && ferror(pipe_file) != 0) {
errorf("Parent: failed to read from stdin");
goto parent_error;
}
buf_data += n;
buf_free -= n;
if (buf_free <= 0) {
buf_free += buf_size;
buf_size *= 2;
char *newbuf = (char *) realloc(buffer, buf_size);
if (newbuf == NULL) {
errorf("Parent: failed to reallocate buffer");
goto parent_error;
}
buffer = newbuf;
}
}
if (buf_data > 0 && buffer[buf_data-1] == '\n') // remove trailing newline
buf_data--;
buffer[buf_data++] = '\0';
debugf("Parent: buf_size: %lu, buf_data: %lu, buf_free: %lu", buf_size, buf_data, buf_free);
// Resize buffer to final size
debugf("Parent: resizing buffer to final size...");
buf_size = buf_data;
buf_free = 0;
char *newbuf = (char *) realloc(buffer, buf_size);
if (newbuf == NULL) {
errorf("Parent: failed to reallocate buffer");
goto parent_error;
}
buffer = newbuf;
debugf("Parent: buf_size: %lu, buf_data: %lu, buf_free: %lu", buf_size, buf_data, buf_free);
// Wait for child to finish
debugf("Parent: waiting for child to terminate...");
if (waitpid(pid, &wstatus, 0) == -1) {
errorf("Parent: child failed to terminate, killing");
kill(pid, SIGKILL);
}
debugf("Parent: child terminated (wstatus: %i, exit status: %i)", wstatus, WEXITSTATUS(wstatus));
// Close pipe file and pipe
debugf("Parent: closing pipe file and pipe...");
fclose(pipe_file);
close(pipe_fds[0]);
// Return results using provided references
if (output != NULL) {
(*output) = buffer;
} else {
debugf("Parent: discarding buffer contents");
free(buffer);
}
if (retval != NULL)
(*retval) = WEXITSTATUS(wstatus);
debugf("Finished running command");
return 0;
parent_error:
close(pipe_fds[0]);
kill(pid, SIGKILL);
if (pipe_file != NULL)
fclose(pipe_file);
if (buffer != NULL)
free(buffer);
return 1;
}
// Runs command with arguments, returns output and return value (variadic arguments; static maximum limit; string references)
// NOTE: this works, but documentation is unclear about exact va_* behavior regarding validity of retrieved arguments
#define RUN_COMMAND_MAX_ARGS 100
int run_command2(char **output, int *retval, const char *command, ...)
{
va_list val;
const char *args[RUN_COMMAND_MAX_ARGS];
// Put reference to command, references to variadic arguments and NULL in args
va_start(val, command);
args[0] = command;
int i = 0;
do {
debugf("Argument %i: %s", i, args[i]);
i++;
args[i] = va_arg(val, const char *);
} while (args[i] != NULL && i < RUN_COMMAND_MAX_ARGS-1);
args[i] = NULL;
va_end(val);
// Call 'run_command()', return result
return run_command(output, retval, command, args);
}
// Runs command with arguments, returns output and return value (variadic arguments; dynamic, no limit; string references)
// NOTE: this works, but documentation is unclear about exact va_* behavior regarding validity of retrieved arguments
// NOTE: this uses a variable length array (VLA) to store args; VLAs are controversial, e.g. https://lkml.org/lkml/2018/3/7/621
int run_command2_5(char **output, int *retval, const char *command, ...)
{
va_list val;
int argc;
// Determine number of arguments (minimum: command + NULL == 2)
va_start(val, command);
argc = 1;
do {
argc++;
} while (va_arg(val, const char *) != NULL);
va_end(val);
debugf("argc: %i", argc);
// Allocate args as VLA
const char *args[argc];
// Put reference to command, references to variadic arguments and NULL in args
args[0] = command;
va_start(val, command);
int i = 0;
do {
debugf("Argument %i: %s", i, args[i]);
i++;
args[i] = va_arg(val, const char *);
} while (i < argc && args[i] != NULL);
va_end(val);
// Call 'run_command()', return result
return run_command(output, retval, command, args);
}
// Runs command with arguments, returns output and return value (variadic arguments; dynamic, no limit; string references)
// NOTE: this works, but documentation is unclear about exact va_* behavior regarding validity of retrieved arguments
// NOTE: probably seconds best solution after run_command4()
int run_command3(char **output, int *retval, const char *command, ...)
{
va_list val;
const char **args;
int argc;
int result;
// Determine number of arguments (minimum: command + NULL == 2)
va_start(val, command);
argc = 1;
do {
argc++;
} while (va_arg(val, const char *) != NULL);
va_end(val);
debugf("argc: %i", argc);
// Allocate args (array of pointers to string)
args = (const char **) malloc(argc * sizeof(const char *));
if (args == NULL) {
errorf("Failed to allocate args");
return 1;
}
// Put reference to command, references to variadic arguments and NULL in args
args[0] = command;
va_start(val, command);
int i = 0;
do {
debugf("Argument %i: %s", i, args[i]);
i++;
args[i] = va_arg(val, const char *);
} while (i < argc && args[i] != NULL);
va_end(val);
// Call 'run_command()'
result = run_command(output, retval, command, args);
// Free args, return result
free(args);
return result;
}
// Runs command with arguments, returns output and return value (variadic arguments; dynamic, no limit; string copies)
// NOTE: safest variant - all argument strings are copied, thus independent of exact va_* behavior regarding validity of retrieved arguments
int run_command4(char **output, int *retval, const char *command, ...)
{
va_list val;
char **args;
int argc;
int result = 1; // assume error
// Determine number of arguments (minimum: command + NULL == 2)
va_start(val, command);
argc = 1;
do {
argc++;
} while (va_arg(val, const char *) != NULL);
va_end(val);
debugf("argc: %i", argc);
// Allocate args (array of pointers to string)
args = (char **) calloc(argc, sizeof(char *));
if (args == NULL) {
errorf("Failed to allocate args");
goto bail_out;
}
// Copy command to args[0]
args[0] = strdup(command);
if (args[0] == NULL) {
errorf("Failed to copy argument %i: %s", 0, strerror(errno));
goto bail_out;
}
debugf("Argument %i: %s", 0, args[0]);
// Copy variadic arguments to args[1..argc-2], i.e. spots in between command ... NULL
va_start(val, command);
for (int i = 1; i <= argc-2; i++) {
args[i] = strdup(va_arg(val, const char *));
if (args[i] == NULL) {
errorf("Failed to copy argument %i: %s", i, strerror(errno));
goto bail_out;
}
debugf("Argument %i: %s", i, args[i]);
}
va_end(val);
// Terminate list of arguments with NULL
args[argc-1] = NULL;
// Call 'run_command()'
result = run_command(output, retval, command, (const char * const *)args);
// Free args, return result
bail_out:
if (args != NULL) {
for (int i = 0; i <= argc-2; i++) {
debugf("Freeing argument %i: %s", i, args[i]);
free(args[i]);
}
}
free(args);
return result;
}
/***************************************
* *
* Main *
* *
***************************************/
int main(int argc, char *argv[])
{
dbg_dst = stderr;
char *output = NULL;
int retval = 0;
const char *command = "find";
const char* const args[] = { command, "/tmp", "-type", "f", NULL };
run_command(&output, &retval, command, args);
infof("Retval: %i, output: '%s'", retval, output);
free(output);
run_command2(&output, &retval, "/bin/echo", "hello", "how", "are", "you", "today", "?", NULL);
infof("Retval: %i, output: '%s'", retval, output);
free(output);
run_command2_5(&output, &retval, "/bin/echo", "hello", "how", "are", "you", "today", "?", NULL);
infof("Retval: %i, output: '%s'", retval, output);
free(output);
run_command3(&output, &retval, "/bin/echo", "hello", "how", "are", "you", "today", "?", NULL);
infof("Retval: %i, output: '%s'", retval, output);
free(output);
run_command4(&output, &retval, "/bin/echo", "hello", "how", "are", "you", "today", "?", NULL);
infof("Retval: %i, output: '%s'", retval, output);
free(output);
run_command4(NULL, NULL, "/bin/sleep", "3s", NULL);
return 0;
}
@fonic
Copy link
Author

fonic commented Jan 7, 2020

Usage:

  • Download & save as run_command.c
  • Compile: gcc run_command.c -o run_command -g -Wall -fsanitize=address -fno-omit-frame-pointer
  • Run: ./run_command

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment