From 8e3a3eef63c3c298c93fa8f8e827b10dd1b6327c Mon Sep 17 00:00:00 2001 From: Havoc Pennington Date: Mon, 9 Oct 2000 16:24:57 +0000 Subject: [PATCH] Add new files. 2000-10-09 Havoc Pennington * Makefile.am, tests/Makefile.am: Add new files. * tests/spawn-test.c, tests/shell-test.c: new tests for the shell/spawn stuff * gutils.c (g_find_program_in_path): convert a relative program name into an absolute pathname to an existing executable * gspawn.h, gspawn.c: New fork/exec API * gshell.h, gshell.c: Shell-related utilities, at the moment simply routines to parse argv and quote/unquote strings * guniprop.c (g_unichar_isspace): Return TRUE for the ASCII space characters isspace() returns TRUE for. * gfileutils.c (g_file_get_contents): Convenience function to slurp entire file into a string and return it. Partially written by Joel Becker. (g_file_test): file test function --- ChangeLog | 24 + ChangeLog.pre-2-0 | 24 + ChangeLog.pre-2-10 | 24 + ChangeLog.pre-2-12 | 24 + ChangeLog.pre-2-2 | 24 + ChangeLog.pre-2-4 | 24 + ChangeLog.pre-2-6 | 24 + ChangeLog.pre-2-8 | 24 + Makefile.am | 6 + gfileutils.c | 470 +++++++++++++++ gfileutils.h | 91 +++ glib.h | 6 + glib/Makefile.am | 6 + glib/gfileutils.c | 470 +++++++++++++++ glib/gfileutils.h | 91 +++ glib/glib.h | 6 + glib/gshell.c | 651 +++++++++++++++++++++ glib/gshell.h | 59 ++ glib/gspawn.c | 1392 ++++++++++++++++++++++++++++++++++++++++++++ glib/gspawn.h | 132 +++++ glib/guniprop.c | 13 +- glib/gutils.c | 100 ++++ gshell.c | 651 +++++++++++++++++++++ gshell.h | 59 ++ gspawn.c | 1392 ++++++++++++++++++++++++++++++++++++++++++++ gspawn.h | 132 +++++ guniprop.c | 13 +- gutils.c | 100 ++++ tests/Makefile.am | 8 +- tests/shell-test.c | 190 ++++++ tests/spawn-test.c | 96 +++ 31 files changed, 6319 insertions(+), 7 deletions(-) create mode 100644 gfileutils.c create mode 100644 gfileutils.h create mode 100644 glib/gfileutils.c create mode 100644 glib/gfileutils.h create mode 100644 glib/gshell.c create mode 100644 glib/gshell.h create mode 100644 glib/gspawn.c create mode 100644 glib/gspawn.h create mode 100644 gshell.c create mode 100644 gshell.h create mode 100644 gspawn.c create mode 100644 gspawn.h create mode 100644 tests/shell-test.c create mode 100644 tests/spawn-test.c diff --git a/ChangeLog b/ChangeLog index 88f87d7a0..b8738da28 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,27 @@ +2000-10-09 Havoc Pennington + + * Makefile.am, tests/Makefile.am: Add new files. + + * tests/spawn-test.c, tests/shell-test.c: new tests for + the shell/spawn stuff + + * gutils.c (g_find_program_in_path): convert a relative + program name into an absolute pathname to an existing + executable + + * gspawn.h, gspawn.c: New fork/exec API + + * gshell.h, gshell.c: Shell-related utilities, at the moment + simply routines to parse argv and quote/unquote strings + + * guniprop.c (g_unichar_isspace): Return TRUE for the + ASCII space characters isspace() returns TRUE for. + + * gfileutils.c (g_file_get_contents): Convenience function + to slurp entire file into a string and return it. Partially + written by Joel Becker. + (g_file_test): file test function + 2000-10-06 Tor Lillqvist * makefile.msc.in: Revamp to be like makefile.mingw.in, make diff --git a/ChangeLog.pre-2-0 b/ChangeLog.pre-2-0 index 88f87d7a0..b8738da28 100644 --- a/ChangeLog.pre-2-0 +++ b/ChangeLog.pre-2-0 @@ -1,3 +1,27 @@ +2000-10-09 Havoc Pennington + + * Makefile.am, tests/Makefile.am: Add new files. + + * tests/spawn-test.c, tests/shell-test.c: new tests for + the shell/spawn stuff + + * gutils.c (g_find_program_in_path): convert a relative + program name into an absolute pathname to an existing + executable + + * gspawn.h, gspawn.c: New fork/exec API + + * gshell.h, gshell.c: Shell-related utilities, at the moment + simply routines to parse argv and quote/unquote strings + + * guniprop.c (g_unichar_isspace): Return TRUE for the + ASCII space characters isspace() returns TRUE for. + + * gfileutils.c (g_file_get_contents): Convenience function + to slurp entire file into a string and return it. Partially + written by Joel Becker. + (g_file_test): file test function + 2000-10-06 Tor Lillqvist * makefile.msc.in: Revamp to be like makefile.mingw.in, make diff --git a/ChangeLog.pre-2-10 b/ChangeLog.pre-2-10 index 88f87d7a0..b8738da28 100644 --- a/ChangeLog.pre-2-10 +++ b/ChangeLog.pre-2-10 @@ -1,3 +1,27 @@ +2000-10-09 Havoc Pennington + + * Makefile.am, tests/Makefile.am: Add new files. + + * tests/spawn-test.c, tests/shell-test.c: new tests for + the shell/spawn stuff + + * gutils.c (g_find_program_in_path): convert a relative + program name into an absolute pathname to an existing + executable + + * gspawn.h, gspawn.c: New fork/exec API + + * gshell.h, gshell.c: Shell-related utilities, at the moment + simply routines to parse argv and quote/unquote strings + + * guniprop.c (g_unichar_isspace): Return TRUE for the + ASCII space characters isspace() returns TRUE for. + + * gfileutils.c (g_file_get_contents): Convenience function + to slurp entire file into a string and return it. Partially + written by Joel Becker. + (g_file_test): file test function + 2000-10-06 Tor Lillqvist * makefile.msc.in: Revamp to be like makefile.mingw.in, make diff --git a/ChangeLog.pre-2-12 b/ChangeLog.pre-2-12 index 88f87d7a0..b8738da28 100644 --- a/ChangeLog.pre-2-12 +++ b/ChangeLog.pre-2-12 @@ -1,3 +1,27 @@ +2000-10-09 Havoc Pennington + + * Makefile.am, tests/Makefile.am: Add new files. + + * tests/spawn-test.c, tests/shell-test.c: new tests for + the shell/spawn stuff + + * gutils.c (g_find_program_in_path): convert a relative + program name into an absolute pathname to an existing + executable + + * gspawn.h, gspawn.c: New fork/exec API + + * gshell.h, gshell.c: Shell-related utilities, at the moment + simply routines to parse argv and quote/unquote strings + + * guniprop.c (g_unichar_isspace): Return TRUE for the + ASCII space characters isspace() returns TRUE for. + + * gfileutils.c (g_file_get_contents): Convenience function + to slurp entire file into a string and return it. Partially + written by Joel Becker. + (g_file_test): file test function + 2000-10-06 Tor Lillqvist * makefile.msc.in: Revamp to be like makefile.mingw.in, make diff --git a/ChangeLog.pre-2-2 b/ChangeLog.pre-2-2 index 88f87d7a0..b8738da28 100644 --- a/ChangeLog.pre-2-2 +++ b/ChangeLog.pre-2-2 @@ -1,3 +1,27 @@ +2000-10-09 Havoc Pennington + + * Makefile.am, tests/Makefile.am: Add new files. + + * tests/spawn-test.c, tests/shell-test.c: new tests for + the shell/spawn stuff + + * gutils.c (g_find_program_in_path): convert a relative + program name into an absolute pathname to an existing + executable + + * gspawn.h, gspawn.c: New fork/exec API + + * gshell.h, gshell.c: Shell-related utilities, at the moment + simply routines to parse argv and quote/unquote strings + + * guniprop.c (g_unichar_isspace): Return TRUE for the + ASCII space characters isspace() returns TRUE for. + + * gfileutils.c (g_file_get_contents): Convenience function + to slurp entire file into a string and return it. Partially + written by Joel Becker. + (g_file_test): file test function + 2000-10-06 Tor Lillqvist * makefile.msc.in: Revamp to be like makefile.mingw.in, make diff --git a/ChangeLog.pre-2-4 b/ChangeLog.pre-2-4 index 88f87d7a0..b8738da28 100644 --- a/ChangeLog.pre-2-4 +++ b/ChangeLog.pre-2-4 @@ -1,3 +1,27 @@ +2000-10-09 Havoc Pennington + + * Makefile.am, tests/Makefile.am: Add new files. + + * tests/spawn-test.c, tests/shell-test.c: new tests for + the shell/spawn stuff + + * gutils.c (g_find_program_in_path): convert a relative + program name into an absolute pathname to an existing + executable + + * gspawn.h, gspawn.c: New fork/exec API + + * gshell.h, gshell.c: Shell-related utilities, at the moment + simply routines to parse argv and quote/unquote strings + + * guniprop.c (g_unichar_isspace): Return TRUE for the + ASCII space characters isspace() returns TRUE for. + + * gfileutils.c (g_file_get_contents): Convenience function + to slurp entire file into a string and return it. Partially + written by Joel Becker. + (g_file_test): file test function + 2000-10-06 Tor Lillqvist * makefile.msc.in: Revamp to be like makefile.mingw.in, make diff --git a/ChangeLog.pre-2-6 b/ChangeLog.pre-2-6 index 88f87d7a0..b8738da28 100644 --- a/ChangeLog.pre-2-6 +++ b/ChangeLog.pre-2-6 @@ -1,3 +1,27 @@ +2000-10-09 Havoc Pennington + + * Makefile.am, tests/Makefile.am: Add new files. + + * tests/spawn-test.c, tests/shell-test.c: new tests for + the shell/spawn stuff + + * gutils.c (g_find_program_in_path): convert a relative + program name into an absolute pathname to an existing + executable + + * gspawn.h, gspawn.c: New fork/exec API + + * gshell.h, gshell.c: Shell-related utilities, at the moment + simply routines to parse argv and quote/unquote strings + + * guniprop.c (g_unichar_isspace): Return TRUE for the + ASCII space characters isspace() returns TRUE for. + + * gfileutils.c (g_file_get_contents): Convenience function + to slurp entire file into a string and return it. Partially + written by Joel Becker. + (g_file_test): file test function + 2000-10-06 Tor Lillqvist * makefile.msc.in: Revamp to be like makefile.mingw.in, make diff --git a/ChangeLog.pre-2-8 b/ChangeLog.pre-2-8 index 88f87d7a0..b8738da28 100644 --- a/ChangeLog.pre-2-8 +++ b/ChangeLog.pre-2-8 @@ -1,3 +1,27 @@ +2000-10-09 Havoc Pennington + + * Makefile.am, tests/Makefile.am: Add new files. + + * tests/spawn-test.c, tests/shell-test.c: new tests for + the shell/spawn stuff + + * gutils.c (g_find_program_in_path): convert a relative + program name into an absolute pathname to an existing + executable + + * gspawn.h, gspawn.c: New fork/exec API + + * gshell.h, gshell.c: Shell-related utilities, at the moment + simply routines to parse argv and quote/unquote strings + + * guniprop.c (g_unichar_isspace): Return TRUE for the + ASCII space characters isspace() returns TRUE for. + + * gfileutils.c (g_file_get_contents): Convenience function + to slurp entire file into a string and return it. Partially + written by Joel Becker. + (g_file_test): file test function + 2000-10-06 Tor Lillqvist * makefile.msc.in: Revamp to be like makefile.mingw.in, make diff --git a/Makefile.am b/Makefile.am index 104d783b1..127d248a1 100644 --- a/Makefile.am +++ b/Makefile.am @@ -49,6 +49,7 @@ libglib_1_3_la_SOURCES = \ gdataset.c \ gdate.c \ gerror.c \ + gfileutils.c \ ghash.c \ ghook.c \ giochannel.c \ @@ -63,7 +64,9 @@ libglib_1_3_la_SOURCES = \ grel.c \ grand.c \ gscanner.c \ + gshell.c \ gslist.c \ + gspawn.c \ gstrfuncs.c \ gstring.c \ gthread.c \ @@ -81,8 +84,11 @@ libglib_1_3_la_SOURCES = \ glibincludedir=$(includedir)/glib-2.0 glibinclude_HEADERS = \ gerror.h \ + gfileutils.h \ glib.h \ glib-object.h \ + gshell.h \ + gspawn.h \ gunicode.h configexecincludedir = $(libdir)/glib-2.0/include diff --git a/gfileutils.c b/gfileutils.c new file mode 100644 index 000000000..11768526d --- /dev/null +++ b/gfileutils.c @@ -0,0 +1,470 @@ +/* gfileutils.c - File utility functions + * + * Copyright 2000 Red Hat, Inc. + * + * GLib is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * GLib is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with GLib; see the file COPYING.LIB. If not, + * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "glib.h" + +#include +#include +#include +#include +#include +#include +#include +#include + + +#define _(x) x + +/** + * g_file_test: + * @filename: a filename to test + * @test: bitfield of #GFileTest flags + * + * Returns TRUE if any of the tests in the bitfield @test are + * TRUE. For example, (G_FILE_TEST_EXISTS | G_FILE_TEST_IS_DIR) + * will return TRUE if the file exists; the check whether it's + * a directory doesn't matter since the existence test is TRUE. + * With the current set of available tests, there's no point + * passing in more than one test at a time. + * + * Return value: whether a test was TRUE + **/ +gboolean +g_file_test (const gchar *filename, + GFileTest test) +{ + if (test & G_FILE_TEST_EXISTS) + return (access (filename, F_OK) == 0); + else if (test & G_FILE_TEST_IS_EXECUTABLE) + return (access (filename, X_OK) == 0); + else + { + struct stat s; + + if (stat (filename, &s) < 0) + return FALSE; + + if ((test & G_FILE_TEST_IS_REGULAR) && + S_ISREG (s.st_mode)) + return TRUE; + else if ((test & G_FILE_TEST_IS_DIR) && + S_ISDIR (s.st_mode)) + return TRUE; + else if ((test & G_FILE_TEST_IS_SYMLINK) && + S_ISLNK (s.st_mode)) + return TRUE; + else + return FALSE; + } +} + +GQuark +g_file_error_quark (void) +{ + static GQuark q = 0; + if (q == 0) + q = g_quark_from_static_string ("g-file-error-quark"); + + return q; +} + +GFileError +g_file_error_from_errno (gint en) +{ + switch (en) + { +#ifdef EEXIST + case EEXIST: + return G_FILE_ERROR_EXIST; + break; +#endif + +#ifdef EISDIR + case EISDIR: + return G_FILE_ERROR_ISDIR; + break; +#endif + +#ifdef EACCES + case EACCES: + return G_FILE_ERROR_ACCES; + break; +#endif + +#ifdef ENAMETOOLONG + case ENAMETOOLONG: + return G_FILE_ERROR_NAMETOOLONG; + break; +#endif + +#ifdef ENOENT + case ENOENT: + return G_FILE_ERROR_NOENT; + break; +#endif + +#ifdef ENOTDIR + case ENOTDIR: + return G_FILE_ERROR_NOTDIR; + break; +#endif + +#ifdef ENXIO + case ENXIO: + return G_FILE_ERROR_NXIO; + break; +#endif + +#ifdef ENODEV + case ENODEV: + return G_FILE_ERROR_NODEV; + break; +#endif + +#ifdef EROFS + case EROFS: + return G_FILE_ERROR_ROFS; + break; +#endif + +#ifdef ETXTBSY + case ETXTBSY: + return G_FILE_ERROR_TXTBSY; + break; +#endif + +#ifdef EFAULT + case EFAULT: + return G_FILE_ERROR_FAULT; + break; +#endif + +#ifdef ELOOP + case ELOOP: + return G_FILE_ERROR_LOOP; + break; +#endif + +#ifdef ENOSPC + case ENOSPC: + return G_FILE_ERROR_NOSPC; + break; +#endif + +#ifdef ENOMEM + case ENOMEM: + return G_FILE_ERROR_NOMEM; + break; +#endif + +#ifdef EMFILE + case EMFILE: + return G_FILE_ERROR_MFILE; + break; +#endif + +#ifdef ENFILE + case ENFILE: + return G_FILE_ERROR_NFILE; + break; +#endif + +#ifdef EBADF + case EBADF: + return G_FILE_ERROR_BADF; + break; +#endif + +#ifdef EINVAL + case EINVAL: + return G_FILE_ERROR_INVAL; + break; +#endif + +#ifdef EPIPE + case EPIPE: + return G_FILE_ERROR_PIPE; + break; +#endif + +#ifdef EAGAIN + case EAGAIN: + return G_FILE_ERROR_AGAIN; + break; +#endif + +#ifdef EINTR + case EINTR: + return G_FILE_ERROR_INTR; + break; +#endif + +#ifdef EIO + case EIO: + return G_FILE_ERROR_IO; + break; +#endif + +#ifdef EPERM + case EPERM: + return G_FILE_ERROR_PERM; + break; +#endif + + default: + return G_FILE_ERROR_FAILED; + break; + } +} + +static gboolean +get_contents_stdio (const gchar *filename, + FILE *f, + gchar **contents, + guint *length, + GError **error) +{ + gchar buf[2048]; + size_t bytes; + GString *str; + + g_assert (f != NULL); + + str = g_string_new (""); + + while (!feof (f)) + { + bytes = fread (buf, 1, 2048, f); + + if (ferror (f)) + { + g_set_error (error, + G_FILE_ERROR, + g_file_error_from_errno (errno), + _("Error reading file '%s': %s"), + filename, strerror (errno)); + + g_string_free (str, TRUE); + + return FALSE; + } + + g_string_append_len (str, buf, bytes); + } + + fclose (f); + + if (length) + *length = str->len; + + *contents = g_string_free (str, FALSE); + + return TRUE; +} + +static gboolean +get_contents_regfile (const gchar *filename, + struct stat *stat_buf, + gint fd, + gchar **contents, + guint *length, + GError **error) +{ + gchar *buf; + size_t bytes_read; + size_t size; + + size = stat_buf->st_size; + + buf = g_new (gchar, size + 1); + + bytes_read = 0; + while (bytes_read < size) + { + gint rc; + + rc = read (fd, buf + bytes_read, size - bytes_read); + + if (rc < 0) + { + if (errno != EINTR) + { + close (fd); + + g_free (buf); + + g_set_error (error, + G_FILE_ERROR, + g_file_error_from_errno (errno), + _("Failed to read from file '%s': %s"), + filename, strerror (errno)); + + return FALSE; + } + } + else if (rc == 0) + break; + else + bytes_read += rc; + } + + buf[bytes_read] = '\0'; + + if (length) + *length = bytes_read; + + *contents = buf; + + return TRUE; +} + +static gboolean +get_contents_posix (const gchar *filename, + gchar **contents, + guint *length, + GError **error) +{ + struct stat stat_buf; + gint fd; + + fd = open (filename, O_RDONLY); + + if (fd < 0) + { + g_set_error (error, + G_FILE_ERROR, + g_file_error_from_errno (errno), + _("Failed to open file '%s': %s"), + filename, strerror (errno)); + + return FALSE; + } + + /* I don't think this will ever fail, aside from ENOMEM, but. */ + if (fstat (fd, &stat_buf) < 0) + { + close (fd); + + g_set_error (error, + G_FILE_ERROR, + g_file_error_from_errno (errno), + _("Failed to get attributes of file '%s': fstat() failed: %s"), + filename, strerror (errno)); + + return FALSE; + } + + if (stat_buf.st_size > 0 && S_ISREG (stat_buf.st_mode)) + { + return get_contents_regfile (filename, + &stat_buf, + fd, + contents, + length, + error); + } + else + { + FILE *f; + + f = fdopen (fd, "r"); + + if (f == NULL) + { + g_set_error (error, + G_FILE_ERROR, + g_file_error_from_errno (errno), + _("Failed to open file '%s': fdopen() failed: %s"), + filename, strerror (errno)); + + return FALSE; + } + + return get_contents_stdio (filename, f, contents, length, error); + } +} + +#ifdef G_OS_WIN32 +static gboolean +get_contents_win32 (const gchar *filename, + gchar **contents, + guint *length, + GError **error) +{ + FILE *f; + + /* I guess you want binary mode; maybe you want text sometimes? */ + f = fopen (filename, "rb"); + + if (f == NULL) + { + g_set_error (error, + G_FILE_ERROR, + g_file_error_from_errno (errno), + _("Failed to open file '%s': %s"), + filename, strerror (errno)); + + return FALSE; + } + + return get_contents_stdio (filename, f, contents, length, error); +} +#endif + +/** + * g_file_get_contents: + * @filename: a file to read contents from + * @contents: location to store an allocated string + * @length: location to store length in bytes of the contents + * @error: return location for a #GError + * + * Reads an entire file into allocated memory, with good error + * checking. If @error is set, FALSE is returned, and @contents is set + * to NULL. If TRUE is returned, @error will not be set, and @contents + * will be set to the file contents. The string stored in @contents + * will be nul-terminated, so for text files you can pass NULL for the + * @length argument. The error domain is #G_FILE_ERROR. Possible + * error codes are those in the #GFileError enumeration. + * + * FIXME currently crashes if the file is too big to fit in memory; + * should probably use g_try_malloc() when we have that function. + * + * Return value: TRUE on success, FALSE if error is set + **/ +gboolean +g_file_get_contents (const gchar *filename, + gchar **contents, + guint *length, + GError **error) +{ + g_return_val_if_fail (filename != NULL, FALSE); + g_return_val_if_fail (contents != NULL, FALSE); + + *contents = NULL; + if (length) + *length = 0; + +#ifdef G_OS_WIN32 + return get_contents_win32 (filename, contents, length, error); +#else + return get_contents_posix (filename, contents, length, error); +#endif +} + diff --git a/gfileutils.h b/gfileutils.h new file mode 100644 index 000000000..f6edd431b --- /dev/null +++ b/gfileutils.h @@ -0,0 +1,91 @@ +/* gfileutils.h - File utility functions + * + * Copyright 2000 Red Hat, Inc. + * + * GLib is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * GLib is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with GLib; see the file COPYING.LIB. If not, + * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#ifndef __GFILEUTILS_H__ +#define __GFILEUTILS_H__ + +#ifdef __cplusplus +extern "C" +{ +#endif + +#define G_FILE_ERROR g_file_error_quark () + +typedef enum +{ + G_FILE_ERROR_EXIST, + G_FILE_ERROR_ISDIR, + G_FILE_ERROR_ACCES, + G_FILE_ERROR_NAMETOOLONG, + G_FILE_ERROR_NOENT, + G_FILE_ERROR_NOTDIR, + G_FILE_ERROR_NXIO, + G_FILE_ERROR_NODEV, + G_FILE_ERROR_ROFS, + G_FILE_ERROR_TXTBSY, + G_FILE_ERROR_FAULT, + G_FILE_ERROR_LOOP, + G_FILE_ERROR_NOSPC, + G_FILE_ERROR_NOMEM, + G_FILE_ERROR_MFILE, + G_FILE_ERROR_NFILE, + G_FILE_ERROR_BADF, + G_FILE_ERROR_INVAL, + G_FILE_ERROR_PIPE, + G_FILE_ERROR_AGAIN, + G_FILE_ERROR_INTR, + G_FILE_ERROR_IO, + G_FILE_ERROR_PERM, + G_FILE_ERROR_FAILED +} GFileError; + +/* For backward-compat reasons, these are synced to an old + * anonymous enum in libgnome. But don't use that enum + * in new code. + */ +typedef enum +{ + G_FILE_TEST_IS_REGULAR = 1 << 0, + G_FILE_TEST_IS_SYMLINK = 1 << 1, + G_FILE_TEST_IS_DIR = 1 << 2, + G_FILE_TEST_IS_EXECUTABLE = 1 << 3, + G_FILE_TEST_EXISTS = 1 << 4, +} GFileTest; + +GQuark g_file_error_quark (void); +/* So other code can generate a GFileError */ +GFileError g_file_error_from_errno (gint err_no); + +gboolean g_file_test (const gchar *filename, + GFileTest test); +gboolean g_file_get_contents (const gchar *filename, + gchar **contents, + guint *length, + GError **error); + + + +#ifdef __cplusplus +} +#endif + +#endif /* __GFILEUTILS_H__ */ + + diff --git a/glib.h b/glib.h index 42e9acae7..d6d53ae4f 100644 --- a/glib.h +++ b/glib.h @@ -1831,6 +1831,8 @@ gchar* g_getenv (const gchar *variable); */ void g_atexit (GVoidFunc func); +/* Look for an executable in PATH, following execvp() rules */ +gchar* g_find_program_in_path (const gchar *program); /* Bit tests */ @@ -3506,5 +3508,9 @@ gchar* g_convert_with_fallback (const gchar *str, #endif /* __cplusplus */ #include +#include +#include +#include +#include #endif /* __G_LIB_H__ */ diff --git a/glib/Makefile.am b/glib/Makefile.am index 104d783b1..127d248a1 100644 --- a/glib/Makefile.am +++ b/glib/Makefile.am @@ -49,6 +49,7 @@ libglib_1_3_la_SOURCES = \ gdataset.c \ gdate.c \ gerror.c \ + gfileutils.c \ ghash.c \ ghook.c \ giochannel.c \ @@ -63,7 +64,9 @@ libglib_1_3_la_SOURCES = \ grel.c \ grand.c \ gscanner.c \ + gshell.c \ gslist.c \ + gspawn.c \ gstrfuncs.c \ gstring.c \ gthread.c \ @@ -81,8 +84,11 @@ libglib_1_3_la_SOURCES = \ glibincludedir=$(includedir)/glib-2.0 glibinclude_HEADERS = \ gerror.h \ + gfileutils.h \ glib.h \ glib-object.h \ + gshell.h \ + gspawn.h \ gunicode.h configexecincludedir = $(libdir)/glib-2.0/include diff --git a/glib/gfileutils.c b/glib/gfileutils.c new file mode 100644 index 000000000..11768526d --- /dev/null +++ b/glib/gfileutils.c @@ -0,0 +1,470 @@ +/* gfileutils.c - File utility functions + * + * Copyright 2000 Red Hat, Inc. + * + * GLib is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * GLib is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with GLib; see the file COPYING.LIB. If not, + * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "glib.h" + +#include +#include +#include +#include +#include +#include +#include +#include + + +#define _(x) x + +/** + * g_file_test: + * @filename: a filename to test + * @test: bitfield of #GFileTest flags + * + * Returns TRUE if any of the tests in the bitfield @test are + * TRUE. For example, (G_FILE_TEST_EXISTS | G_FILE_TEST_IS_DIR) + * will return TRUE if the file exists; the check whether it's + * a directory doesn't matter since the existence test is TRUE. + * With the current set of available tests, there's no point + * passing in more than one test at a time. + * + * Return value: whether a test was TRUE + **/ +gboolean +g_file_test (const gchar *filename, + GFileTest test) +{ + if (test & G_FILE_TEST_EXISTS) + return (access (filename, F_OK) == 0); + else if (test & G_FILE_TEST_IS_EXECUTABLE) + return (access (filename, X_OK) == 0); + else + { + struct stat s; + + if (stat (filename, &s) < 0) + return FALSE; + + if ((test & G_FILE_TEST_IS_REGULAR) && + S_ISREG (s.st_mode)) + return TRUE; + else if ((test & G_FILE_TEST_IS_DIR) && + S_ISDIR (s.st_mode)) + return TRUE; + else if ((test & G_FILE_TEST_IS_SYMLINK) && + S_ISLNK (s.st_mode)) + return TRUE; + else + return FALSE; + } +} + +GQuark +g_file_error_quark (void) +{ + static GQuark q = 0; + if (q == 0) + q = g_quark_from_static_string ("g-file-error-quark"); + + return q; +} + +GFileError +g_file_error_from_errno (gint en) +{ + switch (en) + { +#ifdef EEXIST + case EEXIST: + return G_FILE_ERROR_EXIST; + break; +#endif + +#ifdef EISDIR + case EISDIR: + return G_FILE_ERROR_ISDIR; + break; +#endif + +#ifdef EACCES + case EACCES: + return G_FILE_ERROR_ACCES; + break; +#endif + +#ifdef ENAMETOOLONG + case ENAMETOOLONG: + return G_FILE_ERROR_NAMETOOLONG; + break; +#endif + +#ifdef ENOENT + case ENOENT: + return G_FILE_ERROR_NOENT; + break; +#endif + +#ifdef ENOTDIR + case ENOTDIR: + return G_FILE_ERROR_NOTDIR; + break; +#endif + +#ifdef ENXIO + case ENXIO: + return G_FILE_ERROR_NXIO; + break; +#endif + +#ifdef ENODEV + case ENODEV: + return G_FILE_ERROR_NODEV; + break; +#endif + +#ifdef EROFS + case EROFS: + return G_FILE_ERROR_ROFS; + break; +#endif + +#ifdef ETXTBSY + case ETXTBSY: + return G_FILE_ERROR_TXTBSY; + break; +#endif + +#ifdef EFAULT + case EFAULT: + return G_FILE_ERROR_FAULT; + break; +#endif + +#ifdef ELOOP + case ELOOP: + return G_FILE_ERROR_LOOP; + break; +#endif + +#ifdef ENOSPC + case ENOSPC: + return G_FILE_ERROR_NOSPC; + break; +#endif + +#ifdef ENOMEM + case ENOMEM: + return G_FILE_ERROR_NOMEM; + break; +#endif + +#ifdef EMFILE + case EMFILE: + return G_FILE_ERROR_MFILE; + break; +#endif + +#ifdef ENFILE + case ENFILE: + return G_FILE_ERROR_NFILE; + break; +#endif + +#ifdef EBADF + case EBADF: + return G_FILE_ERROR_BADF; + break; +#endif + +#ifdef EINVAL + case EINVAL: + return G_FILE_ERROR_INVAL; + break; +#endif + +#ifdef EPIPE + case EPIPE: + return G_FILE_ERROR_PIPE; + break; +#endif + +#ifdef EAGAIN + case EAGAIN: + return G_FILE_ERROR_AGAIN; + break; +#endif + +#ifdef EINTR + case EINTR: + return G_FILE_ERROR_INTR; + break; +#endif + +#ifdef EIO + case EIO: + return G_FILE_ERROR_IO; + break; +#endif + +#ifdef EPERM + case EPERM: + return G_FILE_ERROR_PERM; + break; +#endif + + default: + return G_FILE_ERROR_FAILED; + break; + } +} + +static gboolean +get_contents_stdio (const gchar *filename, + FILE *f, + gchar **contents, + guint *length, + GError **error) +{ + gchar buf[2048]; + size_t bytes; + GString *str; + + g_assert (f != NULL); + + str = g_string_new (""); + + while (!feof (f)) + { + bytes = fread (buf, 1, 2048, f); + + if (ferror (f)) + { + g_set_error (error, + G_FILE_ERROR, + g_file_error_from_errno (errno), + _("Error reading file '%s': %s"), + filename, strerror (errno)); + + g_string_free (str, TRUE); + + return FALSE; + } + + g_string_append_len (str, buf, bytes); + } + + fclose (f); + + if (length) + *length = str->len; + + *contents = g_string_free (str, FALSE); + + return TRUE; +} + +static gboolean +get_contents_regfile (const gchar *filename, + struct stat *stat_buf, + gint fd, + gchar **contents, + guint *length, + GError **error) +{ + gchar *buf; + size_t bytes_read; + size_t size; + + size = stat_buf->st_size; + + buf = g_new (gchar, size + 1); + + bytes_read = 0; + while (bytes_read < size) + { + gint rc; + + rc = read (fd, buf + bytes_read, size - bytes_read); + + if (rc < 0) + { + if (errno != EINTR) + { + close (fd); + + g_free (buf); + + g_set_error (error, + G_FILE_ERROR, + g_file_error_from_errno (errno), + _("Failed to read from file '%s': %s"), + filename, strerror (errno)); + + return FALSE; + } + } + else if (rc == 0) + break; + else + bytes_read += rc; + } + + buf[bytes_read] = '\0'; + + if (length) + *length = bytes_read; + + *contents = buf; + + return TRUE; +} + +static gboolean +get_contents_posix (const gchar *filename, + gchar **contents, + guint *length, + GError **error) +{ + struct stat stat_buf; + gint fd; + + fd = open (filename, O_RDONLY); + + if (fd < 0) + { + g_set_error (error, + G_FILE_ERROR, + g_file_error_from_errno (errno), + _("Failed to open file '%s': %s"), + filename, strerror (errno)); + + return FALSE; + } + + /* I don't think this will ever fail, aside from ENOMEM, but. */ + if (fstat (fd, &stat_buf) < 0) + { + close (fd); + + g_set_error (error, + G_FILE_ERROR, + g_file_error_from_errno (errno), + _("Failed to get attributes of file '%s': fstat() failed: %s"), + filename, strerror (errno)); + + return FALSE; + } + + if (stat_buf.st_size > 0 && S_ISREG (stat_buf.st_mode)) + { + return get_contents_regfile (filename, + &stat_buf, + fd, + contents, + length, + error); + } + else + { + FILE *f; + + f = fdopen (fd, "r"); + + if (f == NULL) + { + g_set_error (error, + G_FILE_ERROR, + g_file_error_from_errno (errno), + _("Failed to open file '%s': fdopen() failed: %s"), + filename, strerror (errno)); + + return FALSE; + } + + return get_contents_stdio (filename, f, contents, length, error); + } +} + +#ifdef G_OS_WIN32 +static gboolean +get_contents_win32 (const gchar *filename, + gchar **contents, + guint *length, + GError **error) +{ + FILE *f; + + /* I guess you want binary mode; maybe you want text sometimes? */ + f = fopen (filename, "rb"); + + if (f == NULL) + { + g_set_error (error, + G_FILE_ERROR, + g_file_error_from_errno (errno), + _("Failed to open file '%s': %s"), + filename, strerror (errno)); + + return FALSE; + } + + return get_contents_stdio (filename, f, contents, length, error); +} +#endif + +/** + * g_file_get_contents: + * @filename: a file to read contents from + * @contents: location to store an allocated string + * @length: location to store length in bytes of the contents + * @error: return location for a #GError + * + * Reads an entire file into allocated memory, with good error + * checking. If @error is set, FALSE is returned, and @contents is set + * to NULL. If TRUE is returned, @error will not be set, and @contents + * will be set to the file contents. The string stored in @contents + * will be nul-terminated, so for text files you can pass NULL for the + * @length argument. The error domain is #G_FILE_ERROR. Possible + * error codes are those in the #GFileError enumeration. + * + * FIXME currently crashes if the file is too big to fit in memory; + * should probably use g_try_malloc() when we have that function. + * + * Return value: TRUE on success, FALSE if error is set + **/ +gboolean +g_file_get_contents (const gchar *filename, + gchar **contents, + guint *length, + GError **error) +{ + g_return_val_if_fail (filename != NULL, FALSE); + g_return_val_if_fail (contents != NULL, FALSE); + + *contents = NULL; + if (length) + *length = 0; + +#ifdef G_OS_WIN32 + return get_contents_win32 (filename, contents, length, error); +#else + return get_contents_posix (filename, contents, length, error); +#endif +} + diff --git a/glib/gfileutils.h b/glib/gfileutils.h new file mode 100644 index 000000000..f6edd431b --- /dev/null +++ b/glib/gfileutils.h @@ -0,0 +1,91 @@ +/* gfileutils.h - File utility functions + * + * Copyright 2000 Red Hat, Inc. + * + * GLib is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * GLib is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with GLib; see the file COPYING.LIB. If not, + * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#ifndef __GFILEUTILS_H__ +#define __GFILEUTILS_H__ + +#ifdef __cplusplus +extern "C" +{ +#endif + +#define G_FILE_ERROR g_file_error_quark () + +typedef enum +{ + G_FILE_ERROR_EXIST, + G_FILE_ERROR_ISDIR, + G_FILE_ERROR_ACCES, + G_FILE_ERROR_NAMETOOLONG, + G_FILE_ERROR_NOENT, + G_FILE_ERROR_NOTDIR, + G_FILE_ERROR_NXIO, + G_FILE_ERROR_NODEV, + G_FILE_ERROR_ROFS, + G_FILE_ERROR_TXTBSY, + G_FILE_ERROR_FAULT, + G_FILE_ERROR_LOOP, + G_FILE_ERROR_NOSPC, + G_FILE_ERROR_NOMEM, + G_FILE_ERROR_MFILE, + G_FILE_ERROR_NFILE, + G_FILE_ERROR_BADF, + G_FILE_ERROR_INVAL, + G_FILE_ERROR_PIPE, + G_FILE_ERROR_AGAIN, + G_FILE_ERROR_INTR, + G_FILE_ERROR_IO, + G_FILE_ERROR_PERM, + G_FILE_ERROR_FAILED +} GFileError; + +/* For backward-compat reasons, these are synced to an old + * anonymous enum in libgnome. But don't use that enum + * in new code. + */ +typedef enum +{ + G_FILE_TEST_IS_REGULAR = 1 << 0, + G_FILE_TEST_IS_SYMLINK = 1 << 1, + G_FILE_TEST_IS_DIR = 1 << 2, + G_FILE_TEST_IS_EXECUTABLE = 1 << 3, + G_FILE_TEST_EXISTS = 1 << 4, +} GFileTest; + +GQuark g_file_error_quark (void); +/* So other code can generate a GFileError */ +GFileError g_file_error_from_errno (gint err_no); + +gboolean g_file_test (const gchar *filename, + GFileTest test); +gboolean g_file_get_contents (const gchar *filename, + gchar **contents, + guint *length, + GError **error); + + + +#ifdef __cplusplus +} +#endif + +#endif /* __GFILEUTILS_H__ */ + + diff --git a/glib/glib.h b/glib/glib.h index 42e9acae7..d6d53ae4f 100644 --- a/glib/glib.h +++ b/glib/glib.h @@ -1831,6 +1831,8 @@ gchar* g_getenv (const gchar *variable); */ void g_atexit (GVoidFunc func); +/* Look for an executable in PATH, following execvp() rules */ +gchar* g_find_program_in_path (const gchar *program); /* Bit tests */ @@ -3506,5 +3508,9 @@ gchar* g_convert_with_fallback (const gchar *str, #endif /* __cplusplus */ #include +#include +#include +#include +#include #endif /* __G_LIB_H__ */ diff --git a/glib/gshell.c b/glib/gshell.c new file mode 100644 index 000000000..4bae260be --- /dev/null +++ b/glib/gshell.c @@ -0,0 +1,651 @@ +/* gshell.c - Shell-related utilities + * + * Copyright 2000 Red Hat, Inc. + * g_execvpe implementation based on GNU libc execvp: + * Copyright 1991, 92, 95, 96, 97, 98, 99 Free Software Foundation, Inc. + * + * GLib is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * GLib is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with GLib; see the file COPYING.LIB. If not, write + * to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "glib.h" +#include + +#ifdef _ +#warning "FIXME remove gettext hack" +#endif + +#define _(x) x + +GQuark +g_shell_error_quark (void) +{ + static GQuark quark = 0; + if (quark == 0) + quark = g_quark_from_static_string ("g-shell-error-quark"); + return quark; +} + +/* Single quotes preserve the literal string exactly. escape + * sequences are not allowed; not even \' - if you want a ' + * in the quoted text, you have to do something like 'foo'\''bar' + * + * Double quotes allow $ ` " \ and newline to be escaped with backslash. + * Otherwise double quotes preserve things literally. + */ + +gboolean +unquote_string_inplace (gchar* str, gchar** end, GError** err) +{ + gchar* dest; + gchar* s; + gchar quote_char; + + g_return_val_if_fail(end != NULL, FALSE); + g_return_val_if_fail(err == NULL || *err == NULL, FALSE); + g_return_val_if_fail(str != NULL, FALSE); + + dest = s = str; + + quote_char = *s; + + if (!(*s == '"' || *s == '\'')) + { + if (err) + *err = g_error_new(G_SHELL_ERROR, + G_SHELL_ERROR_BAD_QUOTING, + _("Quoted text doesn't begin with a quotation mark")); + *end = str; + return FALSE; + } + + /* Skip the initial quote mark */ + ++s; + + if (quote_char == '"') + { + while (*s) + { + g_assert(s > dest); /* loop invariant */ + + switch (*s) + { + case '"': + /* End of the string, return now */ + *dest = '\0'; + ++s; + *end = s; + return TRUE; + break; + + case '\\': + /* Possible escaped quote or \ */ + ++s; + switch (*s) + { + case '"': + case '\\': + case '`': + case '$': + case '\n': + *dest = *s; + ++s; + ++dest; + break; + + default: + /* not an escaped char */ + *dest = '\\'; + ++dest; + /* ++s already done. */ + break; + } + break; + + default: + *dest = *s; + ++dest; + ++s; + break; + } + + g_assert(s > dest); /* loop invariant */ + } + } + else + { + while (*s) + { + g_assert(s > dest); /* loop invariant */ + + if (*s == '\'') + { + /* End of the string, return now */ + *dest = '\0'; + ++s; + *end = s; + return TRUE; + } + else + { + *dest = *s; + ++dest; + ++s; + } + + g_assert(s > dest); /* loop invariant */ + } + } + + /* If we reach here this means the close quote was never encountered */ + + *dest = '\0'; + + if (err) + *err = g_error_new(G_SHELL_ERROR, + G_SHELL_ERROR_BAD_QUOTING, + _("Unmatched quotation mark in command line or other shell-quoted text")); + *end = s; + return FALSE; +} + +/** + * g_shell_quote: + * @unquoted_string: a literal string + * + * Quotes a string so that the shell (/bin/sh) will interpret the + * quoted string to mean @unquoted_string. If you pass a filename to + * the shell, for example, you should first quote it with this + * function. The return value must be freed with g_free(). The + * quoting style used is undefined (single or double quotes may be + * used). + * + * Return value: quoted string + **/ +gchar* +g_shell_quote (const gchar *unquoted_string) +{ + /* We always use single quotes, because the algorithm is cheesier. + * We could use double if we felt like it, that might be more + * human-readable. + */ + + const gchar *p; + GString *dest; + + g_return_val_if_fail (unquoted_string != NULL, NULL); + + dest = g_string_new ("'"); + + p = unquoted_string; + + /* could speed this up a lot by appending chunks of text at a + * time. + */ + while (*p) + { + /* Replace literal ' with a close ', a \', and a open ' */ + if (*p == '\'') + g_string_append (dest, "'\''"); + else + g_string_append_c (dest, *p); + + ++p; + } + + /* close the quote */ + g_string_append_c (dest, '\''); + + return g_string_free (dest, FALSE); +} + +/** + * g_shell_unquote: + * @quoted_string: shell-quoted string + * @error: error return location or NULL + * + * Unquotes a string as the shell (/bin/sh) would. Only handles + * quotes; if a string contains file globs, arithmetic operators, + * variables, backticks, redirections, or other special-to-the-shell + * features, the result will be different from the result a real shell + * would produce (the variables, backticks, etc. will be passed + * through literally instead of being expanded). This function is + * guaranteed to succeed if applied to the result of + * g_shell_quote(). If it fails, it returns NULL and sets the + * error. The @quoted_string need not actually contain quoted or + * escaped text; g_shell_unquote() simply goes through the string and + * unquotes/unescapes anything that the shell would. Both single and + * double quotes are handled, as are escapes including escaped + * newlines. The return value must be freed with g_free(). Possible + * errors are in the #G_SHELL_ERROR domain. + * + * Return value: an unquoted string + **/ +gchar* +g_shell_unquote (const gchar *quoted_string, + GError **error) +{ + gchar *unquoted; + gchar *end; + gchar *start; + GString *retval; + + g_return_val_if_fail (quoted_string != NULL, NULL); + + unquoted = g_strdup (quoted_string); + + start = unquoted; + end = unquoted; + retval = g_string_new (""); + + /* The loop allows cases such as + * "foo"blah blah'bar'woo foo"baz"la la la\'\''foo' + */ + while (*start) + { + /* Append all non-quoted chars, honoring backslash escape + */ + + while (*start && !(*start == '"' || *start == '\'')) + { + if (*start == '\\') + { + /* all characters can get escaped by backslash, + * except newline, which is removed if it follows + * a backslash outside of quotes + */ + + ++start; + if (*start) + { + if (*start != '\n') + g_string_append_c (retval, *start); + ++start; + } + } + else + { + g_string_append_c (retval, *start); + ++start; + } + } + + if (*start) + { + if (!unquote_string_inplace (start, &end, error)) + { + goto error; + } + else + { + g_string_append (retval, start); + start = end; + } + } + } + + return g_string_free (retval, FALSE); + + error: + g_assert (error == NULL || *error != NULL); + + g_free (unquoted); + g_string_free (retval, TRUE); + return NULL; +} + +/* g_parse_argv() does a semi-arbitrary weird subset of the way + * the shell parses a command line. We don't do variable expansion, + * don't understand that operators are tokens, don't do tilde expansion, + * don't do command substitution, no arithmetic expansion, IFS gets ignored, + * don't do filename globs, don't remove redirection stuff, etc. + * + * READ THE UNIX98 SPEC on "Shell Command Language" before changing + * the behavior of this code. + * + * Steps to parsing the argv string: + * + * - tokenize the string (but since we ignore operators, + * our tokenization may diverge from what the shell would do) + * note that tokenization ignores the internals of a quoted + * word and it always splits on spaces, not on IFS even + * if we used IFS. We also ignore "end of input indicator" + * (I guess this is control-D?) + * + * Tokenization steps, from UNIX98 with operator stuff removed, + * are: + * + * 1) "If the current character is backslash, single-quote or + * double-quote (\, ' or ") and it is not quoted, it will affect + * quoting for subsequent characters up to the end of the quoted + * text. The rules for quoting are as described in Quoting + * . During token recognition no substitutions will be actually + * performed, and the result token will contain exactly the + * characters that appear in the input (except for newline + * character joining), unmodified, including any embedded or + * enclosing quotes or substitution operators, between the quote + * mark and the end of the quoted text. The token will not be + * delimited by the end of the quoted field." + * + * 2) "If the current character is an unquoted newline character, + * the current token will be delimited." + * + * 3) "If the current character is an unquoted blank character, any + * token containing the previous character is delimited and the + * current character will be discarded." + * + * 4) "If the previous character was part of a word, the current + * character will be appended to that word." + * + * 5) "If the current character is a "#", it and all subsequent + * characters up to, but excluding, the next newline character + * will be discarded as a comment. The newline character that + * ends the line is not considered part of the comment. The + * "#" starts a comment only when it is at the beginning of a + * token. Since the search for the end-of-comment does not + * consider an escaped newline character specially, a comment + * cannot be continued to the next line." + * + * 6) "The current character will be used as the start of a new word." + * + * + * - for each token (word), perform portions of word expansion, namely + * field splitting (using default whitespace IFS) and quote + * removal. Field splitting may increase the number of words. + * Quote removal does not increase the number of words. + * + * "If the complete expansion appropriate for a word results in an + * empty field, that empty field will be deleted from the list of + * fields that form the completely expanded command, unless the + * original word contained single-quote or double-quote characters." + * - UNIX98 spec + * + * + */ + +static inline void +ensure_token (GString **token) +{ + if (*token == NULL) + *token = g_string_new (""); +} + +static void +delimit_token (GString **token, + GSList **retval) +{ + if (*token == NULL) + return; + + *retval = g_slist_prepend (*retval, g_string_free (*token, FALSE)); + + *token = NULL; +} + +static GSList* +tokenize_command_line (const gchar *command_line, + GError **error) +{ + gchar current_quote; + const gchar *p; + GString *current_token = NULL; + GSList *retval = NULL; + + current_quote = '\0'; + p = command_line; + + while (*p) + { + if (current_quote == '\\') + { + if (*p == '\n') + { + /* we append nothing; backslash-newline become nothing */ + } + else + { + /* we append the backslash and the current char, + * to be interpreted later after tokenization + */ + ensure_token (¤t_token); + g_string_append_c (current_token, '\\'); + g_string_append_c (current_token, *p); + } + + current_quote = '\0'; + } + else if (current_quote == '#') + { + /* Discard up to and including next newline */ + while (*p && *p != '\n') + ++p; + + current_quote = '\0'; + + if (*p == '\0') + break; + } + else if (current_quote) + { + if (*p == current_quote && + /* check that it isn't an escaped double quote */ + !(current_quote == '"' && p != command_line && *(p - 1) == '\\')) + { + /* close the quote */ + current_quote = '\0'; + } + + /* Everything inside quotes, and the close quote, + * gets appended literally. + */ + + ensure_token (¤t_token); + g_string_append_c (current_token, *p); + } + else + { + switch (*p) + { + case '\n': + delimit_token (¤t_token, &retval); + break; + + case ' ': + case '\t': + /* If the current token contains the previous char, delimit + * the current token. A nonzero length + * token should always contain the previous char. + */ + if (current_token && + current_token->len > 0) + { + delimit_token (¤t_token, &retval); + } + + /* discard all unquoted blanks (don't add them to a token) */ + break; + + + /* single/double quotes are appended to the token, + * escapes are maybe appended next time through the loop, + * comment chars are never appended. + */ + + case '\'': + case '"': + ensure_token (¤t_token); + g_string_append_c (current_token, *p); + + /* FALL THRU */ + + case '#': + case '\\': + current_quote = *p; + break; + + default: + /* Combines rules 4) and 6) - if we have a token, append to it, + * otherwise create a new token. + */ + ensure_token (¤t_token); + g_string_append_c (current_token, *p); + break; + } + } + + ++p; + } + + delimit_token (¤t_token, &retval); + + if (current_quote) + { + if (current_quote == '\\') + g_set_error (error, + G_SHELL_ERROR, + G_SHELL_ERROR_BAD_QUOTING, + _("Text ended just after a '\' character." + " (The text was '%s')"), + command_line); + else + g_set_error (error, + G_SHELL_ERROR, + G_SHELL_ERROR_BAD_QUOTING, + _("Text ended before matching quote was found for %c." + " (The text was '%s')"), + current_quote, command_line); + + goto error; + } + + if (retval == NULL) + { + g_set_error (error, + G_SHELL_ERROR, + G_SHELL_ERROR_EMPTY_STRING, + _("Text was empty (or contained only whitespace)")); + + goto error; + } + + /* we appended backward */ + retval = g_slist_reverse (retval); + + return retval; + + error: + g_assert (error == NULL || *error != NULL); + + if (retval) + { + g_slist_foreach (retval, (GFunc)g_free, NULL); + g_slist_free (retval); + } + + return NULL; +} + +/** + * g_shell_parse_argv: + * @command_line: command line to parse + * @argcp: return location for number of args + * @argvp: return location for array of args + * @error: return location for error + * + * Parses a command line into an argument vector, in much the same way + * the shell would, but without many of the expansions the shell would + * perform (variable expansion, globs, operators, filename expansion, + * etc. are not supported). The results are defined to be the same as + * those you would get from a UNIX98 /bin/sh, as long as the input + * contains none of the unsupported shell expansions. If the input + * does contain such expansions, they are passed through + * literally. Possible errors are those from the #G_SHELL_ERROR + * domain. + * + * Return value: TRUE on success, FALSE if error set + **/ +gboolean +g_shell_parse_argv (const gchar *command_line, + gint *argcp, + gchar ***argvp, + GError **error) +{ + /* Code based on poptParseArgvString() from libpopt */ + gint argc = 0; + gchar **argv = NULL; + GSList *tokens = NULL; + gint i; + GSList *tmp_list; + + g_return_val_if_fail (command_line != NULL, FALSE); + + tokens = tokenize_command_line (command_line, error); + if (tokens == NULL) + return FALSE; + + /* Because we can't have introduced any new blank space into the + * tokens (we didn't do any new expansions), we don't need to + * perform field splitting. If we were going to honor IFS or do any + * expansions, we would have to do field splitting on each word + * here. Also, if we were going to do any expansion we would need to + * remove any zero-length words that didn't contain quotes + * originally; but since there's no expansion we know all words have + * nonzero length, unless they contain quotes. + * + * So, we simply remove quotes, and don't do any field splitting or + * empty word removal, since we know there was no way to introduce + * such things. + */ + + argc = g_slist_length (tokens); + argv = g_new0 (gchar*, argc + 1); + i = 0; + tmp_list = tokens; + while (tmp_list) + { + argv[i] = g_shell_unquote (tmp_list->data, error); + + /* Since we already checked that quotes matched up in the + * tokenizer, this shouldn't be possible to reach I guess. + */ + if (argv[i] == NULL) + goto failed; + + tmp_list = g_slist_next (tmp_list); + ++i; + } + + g_slist_foreach (tokens, (GFunc)g_free, NULL); + g_slist_free (tokens); + + if (argcp) + *argcp = argc; + + if (argvp) + *argvp = argv; + else + g_strfreev (argv); + + return TRUE; + + failed: + + g_assert (error == NULL || *error != NULL); + g_strfreev (argv); + g_slist_foreach (tokens, (GFunc) g_free, NULL); + g_slist_free (tokens); + + return FALSE; +} diff --git a/glib/gshell.h b/glib/gshell.h new file mode 100644 index 000000000..0f7fd1f3b --- /dev/null +++ b/glib/gshell.h @@ -0,0 +1,59 @@ +/* gshell.h - Shell-related utilities + * + * Copyright 2000 Red Hat, Inc. + * + * GLib is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * GLib is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with GLib; see the file COPYING.LIB. If not, write + * to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#ifndef __GSHELL_H__ +#define __GSHELL_H__ + +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + +#define G_SHELL_ERROR g_shell_error_quark () + +typedef enum +{ + /* mismatched or otherwise mangled quoting */ + G_SHELL_ERROR_BAD_QUOTING, + /* string to be parsed was empty */ + G_SHELL_ERROR_EMPTY_STRING, + G_SHELL_ERROR_FAILED +} GShellError; + +GQuark g_shell_error_quark (void); + +gchar* g_shell_quote (const gchar *unquoted_string); +gchar* g_shell_unquote (const gchar *quoted_string, + GError **error); +gboolean g_shell_parse_argv (const gchar *command_line, + gint *argc, + gchar ***argv, + GError **error); + + +#ifdef __cplusplus +} +#endif + +#endif /* __GSHELL_H__ */ + + diff --git a/glib/gspawn.c b/glib/gspawn.c new file mode 100644 index 000000000..b6af58de7 --- /dev/null +++ b/glib/gspawn.c @@ -0,0 +1,1392 @@ +/* gspawn.c - Process launching + * + * Copyright 2000 Red Hat, Inc. + * g_execvpe implementation based on GNU libc execvp: + * Copyright 1991, 92, 95, 96, 97, 98, 99 Free Software Foundation, Inc. + * + * GLib is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * GLib is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with GLib; see the file COPYING.LIB. If not, write + * to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "glib.h" +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _ +#warning "FIXME remove gettext hack" +#endif + +#define _(x) x + +static gint g_execute (const gchar *file, + gchar **argv, + gchar **envp, + gboolean search_path); + +static gboolean make_pipe (gint p[2], + GError **error); +static gboolean fork_exec_with_pipes (gboolean intermediate_child, + const gchar *working_directory, + gchar **argv, + gchar **envp, + gboolean close_descriptors, + gboolean search_path, + gboolean stdout_to_null, + gboolean stderr_to_null, + gboolean child_inherits_stdin, + GSpawnChildSetupFunc child_setup, + gpointer user_data, + gint *child_pid, + gint *standard_input, + gint *standard_output, + gint *standard_error, + GError **error); + +GQuark +g_spawn_error_quark (void) +{ + static GQuark quark = 0; + if (quark == 0) + quark = g_quark_from_static_string ("g-exec-error-quark"); + return quark; +} + +/** + * g_spawn_async: + * @working_directory: child's current working directory, or NULL to inherit parent's + * @argv: child's argument vector + * @envp: child's environment, or NULL to inherit parent's + * @flags: flags from #GSpawnFlags + * @child_setup: function to run in the child just before exec() + * @user_data: user data for @child_setup + * @child_pid: return location for child process ID, or NULL + * @error: return location for error + * + * See g_spawn_async_with_pipes() for a full description; this function + * simply calls the g_spawn_async_with_pipes() without any pipes. + * + * Return value: TRUE on success, FALSE if error is set + **/ +gboolean +g_spawn_async (const gchar *working_directory, + gchar **argv, + gchar **envp, + GSpawnFlags flags, + GSpawnChildSetupFunc child_setup, + gpointer user_data, + gint *child_pid, + GError **error) +{ + g_return_val_if_fail (argv != NULL, FALSE); + + return g_spawn_async_with_pipes (working_directory, + argv, envp, + flags, + child_setup, + user_data, + child_pid, + NULL, NULL, NULL, + error); +} + +/* Avoids a danger in threaded situations (calling close() + * on a file descriptor twice, and another thread has + * re-opened it since the first close) + */ +static gint +close_and_invalidate (gint *fd) +{ + gint ret; + + ret = close (*fd); + *fd = -1; + + return ret; +} + +typedef enum +{ + READ_FAILED = 0, /* FALSE */ + READ_OK, + READ_EOF +} ReadResult; + +static ReadResult +read_data (GString *str, + gint fd, + GError **error) +{ + gint bytes; + gchar buf[4096]; + + again: + + bytes = read (fd, &buf, 4096); + + if (bytes == 0) + return READ_EOF; + else if (bytes > 0) + { + g_string_append_len (str, buf, bytes); + return READ_OK; + } + else if (bytes < 0 && errno == EINTR) + goto again; + else if (bytes < 0) + { + g_set_error (error, + G_SPAWN_ERROR, + G_SPAWN_ERROR_READ, + _("Failed to read data from child process (%s)"), + g_strerror (errno)); + + return READ_FAILED; + } + else + return READ_OK; +} + +/** + * g_spawn_sync: + * @working_directory: child's current working directory, or NULL to inherit parent's + * @argv: child's argument vector + * @envp: child's environment, or NULL to inherit parent's + * @flags: flags from #GSpawnFlags + * @child_setup: function to run in the child just before exec() + * @user_data: user data for @child_setup + * @standard_output: return location for child output + * @standard_error: return location for child error messages + * @exit_status: child exit status, as returned by waitpid() + * @error: return location for error + * + * Executes a child synchronously (waits for the child to exit before returning). + * All output from the child is stored in @standard_output and @standard_error, + * if those parameters are non-NULL. If @exit_status is non-NULL, the exit status + * of the child is stored there as it would be by waitpid(); standard UNIX + * macros such as WIFEXITED() and WEXITSTATUS() must be used to evaluate the + * exit status. If an error occurs, no data is returned in @standard_output, + * @standard_error, or @exit_status. + * + * This function calls g_spawn_async_with_pipes() internally; see that function + * for full details on the other parameters. + * + * Return value: TRUE on success, FALSE if an error was set. + **/ +gboolean +g_spawn_sync (const gchar *working_directory, + gchar **argv, + gchar **envp, + GSpawnFlags flags, + GSpawnChildSetupFunc child_setup, + gpointer user_data, + gchar **standard_output, + gchar **standard_error, + gint *exit_status, + GError **error) +{ + gint outpipe = -1; + gint errpipe = -1; + gint pid; + fd_set fds; + gint ret; + GString *outstr = NULL; + GString *errstr = NULL; + gboolean failed; + gint status; + + g_return_val_if_fail (argv != NULL, FALSE); + g_return_val_if_fail (!(flags & G_SPAWN_DO_NOT_REAP_CHILD), FALSE); + g_return_val_if_fail (standard_output == NULL || + !(flags & G_SPAWN_STDOUT_TO_DEV_NULL), FALSE); + g_return_val_if_fail (standard_error == NULL || + !(flags & G_SPAWN_STDERR_TO_DEV_NULL), FALSE); + + /* Just to ensure segfaults if callers try to use + * these when an error is reported. + */ + if (standard_output) + *standard_output = NULL; + + if (standard_error) + *standard_error = NULL; + + if (!fork_exec_with_pipes (FALSE, + working_directory, + argv, + envp, + !(flags & G_SPAWN_LEAVE_DESCRIPTORS_OPEN), + (flags & G_SPAWN_SEARCH_PATH) != 0, + (flags & G_SPAWN_STDOUT_TO_DEV_NULL) != 0, + (flags & G_SPAWN_STDERR_TO_DEV_NULL) != 0, + (flags & G_SPAWN_CHILD_INHERITS_STDIN) != 0, + child_setup, + user_data, + &pid, + NULL, + standard_output ? &outpipe : NULL, + standard_error ? &errpipe : NULL, + error)) + return FALSE; + + /* Read data from child. */ + + failed = FALSE; + + if (outpipe >= 0) + { + outstr = g_string_new (""); + } + + if (errpipe >= 0) + { + errstr = g_string_new (""); + } + + /* Read data until we get EOF on both pipes. */ + while (!failed && + (outpipe >= 0 || + errpipe >= 0)) + { + ret = 0; + + FD_ZERO (&fds); + if (outpipe >= 0) + FD_SET (outpipe, &fds); + if (errpipe >= 0) + FD_SET (errpipe, &fds); + + ret = select (MAX (outpipe, errpipe) + 1, + &fds, + NULL, NULL, + NULL /* no timeout */); + + if (ret < 0 && errno != EINTR) + { + failed = TRUE; + + g_set_error (error, + G_SPAWN_ERROR, + G_SPAWN_ERROR_READ, + _("Unexpected error in select() reading data from a child process (%s)"), + g_strerror (errno)); + + break; + } + + if (outpipe >= 0 && FD_ISSET (outpipe, &fds)) + { + switch (read_data (outstr, outpipe, error)) + { + case READ_FAILED: + failed = TRUE; + break; + case READ_EOF: + close_and_invalidate (&outpipe); + outpipe = -1; + break; + default: + break; + } + + if (failed) + break; + } + + if (errpipe >= 0 && FD_ISSET (errpipe, &fds)) + { + switch (read_data (errstr, errpipe, error)) + { + case READ_FAILED: + failed = TRUE; + break; + case READ_EOF: + close_and_invalidate (&errpipe); + errpipe = -1; + break; + default: + break; + } + + if (failed) + break; + } + } + + /* These should only be open still if we had an error. */ + + if (outpipe >= 0) + close_and_invalidate (&outpipe); + if (errpipe >= 0) + close_and_invalidate (&errpipe); + + /* Wait for child to exit, even if we have + * an error pending. + */ + again: + + ret = waitpid (pid, &status, 0); + + if (ret < 0) + { + if (errno == EINTR) + goto again; + else if (errno == ECHILD) + { + if (exit_status) + { + g_warning ("In call to g_spawn_sync(), exit status of a child process was requested but SIGCHLD action was set to SIG_IGN and ECHILD was received by waitpid(), so exit status can't be returned. This is a bug in the program calling g_spawn_sync(); either don't request the exit status, or don't set the SIGCHLD action."); + } + else + { + /* We don't need the exit status. */ + } + } + else + { + if (!failed) /* avoid error pileups */ + { + failed = TRUE; + + g_set_error (error, + G_SPAWN_ERROR, + G_SPAWN_ERROR_READ, + _("Unexpected error in waitpid() (%s)"), + g_strerror (errno)); + } + } + } + + if (failed) + { + if (outstr) + g_string_free (outstr, TRUE); + if (errstr) + g_string_free (errstr, TRUE); + + return FALSE; + } + else + { + if (exit_status) + *exit_status = status; + + if (standard_output) + *standard_output = g_string_free (outstr, FALSE); + + if (standard_error) + *standard_error = g_string_free (errstr, FALSE); + + return TRUE; + } +} + +/** + * g_spawn_async_with_pipes: + * @working_directory: child's current working directory, or NULL to inherit parent's + * @argv: child's argument vector + * @envp: child's environment, or NULL to inherit parent's + * @flags: flags from #GSpawnFlags + * @child_setup: function to run in the child just before exec() + * @user_data: user data for @child_setup + * @child_pid: return location for child process ID, or NULL + * @standard_input: return location for file descriptor to write to child's stdin, or NULL + * @standard_output: return location for file descriptor to read child's stdout, or NULL + * @standard_error: return location for file descriptor to read child's stderr, or NULL + * @error: return location for error + * + * Executes a child program asynchronously (your program will not + * block waiting for the child to exit). The child program is + * specified by the only argument that must be provided, @argv. @argv + * should be a NULL-terminated array of strings, to be passed as the + * argument vector for the child. The first string in @argv is of + * course the name of the program to execute. By default, the name of + * the program must be a full path; the PATH shell variable will only + * be searched if you pass the %G_SPAWN_SEARCH_PATH flag. + * + * @envp is a NULL-terminated array of strings, where each string + * has the form KEY=VALUE. This will become + * the child's environment. If @envp is NULL, the child inherits its + * parent's environment. + * + * @flags should be the bitwise OR of any flags you want to affect the + * function's behavior. The %G_SPAWN_DO_NOT_REAP_CHILD means that the + * child will not be automatically reaped; you must call waitpid() or + * handle SIGCHLD yourself, or the child will become a zombie. + * %G_SPAWN_LEAVE_DESCRIPTORS_OPEN means that the parent's open file + * descriptors will be inherited by the child; otherwise all + * descriptors except stdin/stdout/stderr will be closed before + * calling exec() in the child. %G_SPAWN_SEARCH_PATH means that + * argv[0] need not be an absolute path, it + * will be looked for in the user's PATH. %G_SPAWN_STDOUT_TO_DEV_NULL + * means that the child's standad output will be discarded, instead + * of going to the same location as the parent's standard output. + * %G_SPAWN_STDERR_TO_DEV_NULL means that the child's standard error + * will be discarded. %G_SPAWN_CHILD_INHERITS_STDIN means that + * the child will inherit the parent's standard input (by default, + * the child's standard input is attached to /dev/null). + * + * @child_setup and @user_data are a function and user data to be + * called in the child after GLib has performed all the setup it plans + * to perform (including creating pipes, closing file descriptors, + * etc.) but before calling exec(). That is, @child_setup is called + * just before calling exec() in the child. Obviously actions taken in + * this function will only affect the child, not the parent. + * + * If non-NULL, @child_pid will be filled with the child's process + * ID. You can use the process ID to send signals to the child, or + * to waitpid() if you specified the %G_SPAWN_DO_NOT_REAP_CHILD flag. + * + * If non-NULL, the @standard_input, @standard_output, @standard_error + * locations will be filled with file descriptors for writing to the child's + * standard input or reading from its standard output or standard error. + * The caller of g_spawn_async_with_pipes() must close these file descriptors + * when they are no longer in use. If these parameters are NULL, the + * corresponding pipe won't be created. + * + * @error can be NULL to ignore errors, or non-NULL to report errors. + * If an error is set, the function returns FALSE. Errors + * are reported even if they occur in the child (for example if the + * executable in argv[0] is not found). Typically + * the message field of returned errors should be displayed + * to users. Possible errors are those from the #G_SPAWN_ERROR domain. + * + * If an error occurs, @child_pid, @standard_input, @standard_output, + * and @standard_error will not be filled with valid values. + * + * Return value: TRUE on success, FALSE if an error was set + **/ +gboolean +g_spawn_async_with_pipes (const gchar *working_directory, + gchar **argv, + gchar **envp, + GSpawnFlags flags, + GSpawnChildSetupFunc child_setup, + gpointer user_data, + gint *child_pid, + gint *standard_input, + gint *standard_output, + gint *standard_error, + GError **error) +{ + g_return_val_if_fail (argv != NULL, FALSE); + g_return_val_if_fail (standard_output == NULL || + !(flags & G_SPAWN_STDOUT_TO_DEV_NULL), FALSE); + g_return_val_if_fail (standard_error == NULL || + !(flags & G_SPAWN_STDERR_TO_DEV_NULL), FALSE); + /* can't inherit stdin if we have an input pipe. */ + g_return_val_if_fail (standard_input == NULL || + !(flags & G_SPAWN_CHILD_INHERITS_STDIN), FALSE); + + return fork_exec_with_pipes (!(flags & G_SPAWN_DO_NOT_REAP_CHILD), + working_directory, + argv, + envp, + !(flags & G_SPAWN_LEAVE_DESCRIPTORS_OPEN), + (flags & G_SPAWN_SEARCH_PATH) != 0, + (flags & G_SPAWN_STDOUT_TO_DEV_NULL) != 0, + (flags & G_SPAWN_STDERR_TO_DEV_NULL) != 0, + (flags & G_SPAWN_CHILD_INHERITS_STDIN) != 0, + child_setup, + user_data, + child_pid, + standard_input, + standard_output, + standard_error, + error); +} + +/** + * g_spawn_command_line_sync: + * @command_line: a command line + * @standard_output: return location for child output + * @standard_error: return location for child errors + * @exit_status: return location for child exit status + * @error: return location for errors + * + * A simple version of g_spawn_sync() with little-used parameters + * removed, taking a command line instead of an argument vector. See + * g_spawn_sync() for full details. @command_line will be parsed by + * g_shell_parse_argv(). Unlike g_spawn_sync(), the %G_SPAWN_SEARCH_PATH flag + * is enabled. Note that %G_SPAWN_SEARCH_PATH can have security + * implications, so consider using g_spawn_sync() directly if + * appropriate. Possible errors are those from g_spawn_sync() and those + * from g_shell_parse_argv(). + * + * Return value: TRUE on success, FALSE if an error was set + **/ +gboolean +g_spawn_command_line_sync (const gchar *command_line, + gchar **standard_output, + gchar **standard_error, + gint *exit_status, + GError **error) +{ + gboolean retval; + gchar **argv = 0; + + g_return_val_if_fail (command_line != NULL, FALSE); + + if (!g_shell_parse_argv (command_line, + NULL, &argv, + error)) + return FALSE; + + retval = g_spawn_sync (NULL, + argv, + NULL, + G_SPAWN_SEARCH_PATH, + NULL, + NULL, + standard_output, + standard_error, + exit_status, + error); + g_strfreev (argv); + + return retval; +} + +/** + * g_spawn_command_line_async: + * @command_line: a command line + * @error: return location for errors + * + * A simple version of g_spawn_async() that parses a command line with + * g_shell_parse_argv() and passes it to g_spawn_async(). Runs a + * command line in the background. Unlike g_spawn_async(), the + * %G_SPAWN_SEARCH_PATH flag is enabled, other flags are not. Note + * that %G_SPAWN_SEARCH_PATH can have security implications, so + * consider using g_spawn_async() directly if appropriate. Possible + * errors are those from g_shell_parse_argv() and g_spawn_async(). + * + * Return value: TRUE on success, FALSE if error is set. + **/ +gboolean +g_spawn_command_line_async (const gchar *command_line, + GError **error) +{ + gboolean retval; + gchar **argv = 0; + + g_return_val_if_fail (command_line != NULL, FALSE); + + if (!g_shell_parse_argv (command_line, + NULL, &argv, + error)) + return FALSE; + + retval = g_spawn_async (NULL, + argv, + NULL, + G_SPAWN_SEARCH_PATH, + NULL, + NULL, + NULL, + error); + g_strfreev (argv); + + return retval; +} + +static gint +exec_err_to_g_error (gint en) +{ + switch (en) + { +#ifdef EACCES + case EACCES: + return G_SPAWN_ERROR_ACCES; + break; +#endif + +#ifdef EPERM + case EPERM: + return G_SPAWN_ERROR_PERM; + break; +#endif + +#ifdef E2BIG + case E2BIG: + return G_SPAWN_ERROR_2BIG; + break; +#endif + +#ifdef ENOEXEC + case ENOEXEC: + return G_SPAWN_ERROR_NOEXEC; + break; +#endif + +#ifdef ENAMETOOLONG + case ENAMETOOLONG: + return G_SPAWN_ERROR_NAMETOOLONG; + break; +#endif + +#ifdef ENOENT + case ENOENT: + return G_SPAWN_ERROR_NOENT; + break; +#endif + +#ifdef ENOMEM + case ENOMEM: + return G_SPAWN_ERROR_NOMEM; + break; +#endif + +#ifdef ENOTDIR + case ENOTDIR: + return G_SPAWN_ERROR_NOTDIR; + break; +#endif + +#ifdef ELOOP + case ELOOP: + return G_SPAWN_ERROR_LOOP; + break; +#endif + +#ifdef ETXTBUSY + case ETXTBUSY: + return G_SPAWN_ERROR_TXTBUSY; + break; +#endif + +#ifdef EIO + case EIO: + return G_SPAWN_ERROR_IO; + break; +#endif + +#ifdef ENFILE + case ENFILE: + return G_SPAWN_ERROR_NFILE; + break; +#endif + +#ifdef EMFILE + case EMFILE: + return G_SPAWN_ERROR_MFILE; + break; +#endif + +#ifdef EINVAL + case EINVAL: + return G_SPAWN_ERROR_INVAL; + break; +#endif + +#ifdef EISDIR + case EISDIR: + return G_SPAWN_ERROR_ISDIR; + break; +#endif + +#ifdef ELIBBAD + case ELIBBAD: + return G_SPAWN_ERROR_LIBBAD; + break; +#endif + + default: + return G_SPAWN_ERROR_FAILED; + break; + } +} + +static void +write_err_and_exit (gint fd, gint msg) +{ + gint en = errno; + + write (fd, &msg, sizeof(msg)); + write (fd, &en, sizeof(en)); + + _exit (1); +} + +static void +set_cloexec (gint fd) +{ + fcntl (fd, F_SETFD, FD_CLOEXEC); +} + +static gint +sane_dup2 (gint fd1, gint fd2) +{ + gint ret; + + retry: + ret = dup2 (fd1, fd2); + if (ret < 0 && errno == EINTR) + goto retry; + + return ret; +} + +enum +{ + CHILD_CHDIR_FAILED, + CHILD_EXEC_FAILED, + CHILD_DUP2_FAILED, + CHILD_FORK_FAILED +}; + +static void +do_exec (gint child_err_report_fd, + gint stdin_fd, + gint stdout_fd, + gint stderr_fd, + const gchar *working_directory, + gchar **argv, + gchar **envp, + gboolean close_descriptors, + gboolean search_path, + gboolean stdout_to_null, + gboolean stderr_to_null, + gboolean child_inherits_stdin, + GSpawnChildSetupFunc child_setup, + gpointer user_data) +{ + if (working_directory && chdir (working_directory) < 0) + write_err_and_exit (child_err_report_fd, + CHILD_CHDIR_FAILED); + + /* Close all file descriptors but stdin stdout and stderr as + * soon as we exec. Note that this includes + * child_err_report_fd, which keeps the parent from blocking + * forever on the other end of that pipe. + */ + if (close_descriptors) + { + gint open_max; + gint i; + + open_max = sysconf (_SC_OPEN_MAX); + for (i = 3; i < open_max; i++) + set_cloexec (i); + } + else + { + /* We need to do child_err_report_fd anyway */ + set_cloexec (child_err_report_fd); + } + + /* Redirect pipes as required */ + + if (stdin_fd >= 0) + { + /* dup2 can't actually fail here I don't think */ + + if (sane_dup2 (stdin_fd, 0) < 0) + write_err_and_exit (child_err_report_fd, + CHILD_DUP2_FAILED); + + /* ignore this if it doesn't work */ + close_and_invalidate (&stdin_fd); + } + else if (!child_inherits_stdin) + { + /* Keep process from blocking on a read of stdin */ + gint read_null = open ("/dev/null", O_RDONLY); + sane_dup2 (read_null, 0); + close_and_invalidate (&read_null); + } + + if (stdout_fd >= 0) + { + /* dup2 can't actually fail here I don't think */ + + if (sane_dup2 (stdout_fd, 1) < 0) + write_err_and_exit (child_err_report_fd, + CHILD_DUP2_FAILED); + + /* ignore this if it doesn't work */ + close_and_invalidate (&stdout_fd); + } + else if (stdout_to_null) + { + gint write_null = open ("/dev/null", O_WRONLY); + sane_dup2 (write_null, 1); + close_and_invalidate (&write_null); + } + + if (stderr_fd >= 0) + { + /* dup2 can't actually fail here I don't think */ + + if (sane_dup2 (stderr_fd, 2) < 0) + write_err_and_exit (child_err_report_fd, + CHILD_DUP2_FAILED); + + /* ignore this if it doesn't work */ + close_and_invalidate (&stderr_fd); + } + else if (stderr_to_null) + { + gint write_null = open ("/dev/null", O_WRONLY); + sane_dup2 (write_null, 2); + close_and_invalidate (&write_null); + } + + /* Call user function just before we exec */ + if (child_setup) + { + (* child_setup) (user_data); + } + + g_execute (argv[0], argv, envp, search_path); + + /* Exec failed */ + write_err_and_exit (child_err_report_fd, + CHILD_EXEC_FAILED); +} + +static gboolean +read_ints (int fd, + gint* buf, + gint n_ints_in_buf, + gint *n_ints_read, + GError **error) +{ + gint bytes = 0; + + while (TRUE) + { + gint chunk; + + if (bytes >= sizeof(gint)*2) + break; /* give up, who knows what happened, should not be + * possible. + */ + + again: + chunk = read (fd, + ((gchar*)buf) + bytes, + sizeof(gint)*n_ints_in_buf - bytes); + if (chunk < 0 && errno == EINTR) + goto again; + + if (chunk < 0) + { + /* Some weird shit happened, bail out */ + + g_set_error (error, + G_SPAWN_ERROR, + G_SPAWN_ERROR_FAILED, + _("Failed to read from child pipe (%s)"), + g_strerror (errno)); + + return FALSE; + } + else if (chunk == 0) + break; /* EOF */ + else + { + g_assert (chunk > 0); + + bytes += chunk; + } + } + + *n_ints_read = bytes/4; + + return TRUE; +} + +static gboolean +fork_exec_with_pipes (gboolean intermediate_child, + const gchar *working_directory, + gchar **argv, + gchar **envp, + gboolean close_descriptors, + gboolean search_path, + gboolean stdout_to_null, + gboolean stderr_to_null, + gboolean child_inherits_stdin, + GSpawnChildSetupFunc child_setup, + gpointer user_data, + gint *child_pid, + gint *standard_input, + gint *standard_output, + gint *standard_error, + GError **error) +{ + gint pid; + gint stdin_pipe[2] = { -1, -1 }; + gint stdout_pipe[2] = { -1, -1 }; + gint stderr_pipe[2] = { -1, -1 }; + gint child_err_report_pipe[2] = { -1, -1 }; + gint child_pid_report_pipe[2] = { -1, -1 }; + gint status; + + if (!make_pipe (child_err_report_pipe, error)) + return FALSE; + + if (intermediate_child && !make_pipe (child_pid_report_pipe, error)) + goto cleanup_and_fail; + + if (standard_input && !make_pipe (stdin_pipe, error)) + goto cleanup_and_fail; + + if (standard_output && !make_pipe (stdout_pipe, error)) + goto cleanup_and_fail; + + if (standard_error && !make_pipe (stderr_pipe, error)) + goto cleanup_and_fail; + + pid = fork (); + + if (pid < 0) + { + g_set_error (error, + G_SPAWN_ERROR, + G_SPAWN_ERROR_FORK, + _("Failed to fork (%s)"), + g_strerror (errno)); + + goto cleanup_and_fail; + } + else if (pid == 0) + { + /* Immediate child. This may or may not be the child that + * actually execs the new process. + */ + + /* Be sure we crash if the parent exits + * and we write to the err_report_pipe + */ + signal (SIGPIPE, SIG_DFL); + + /* Close the parent's end of the pipes; + * not needed in the close_descriptors case, + * though + */ + close_and_invalidate (&child_err_report_pipe[0]); + close_and_invalidate (&child_pid_report_pipe[0]); + close_and_invalidate (&stdin_pipe[1]); + close_and_invalidate (&stdout_pipe[0]); + close_and_invalidate (&stderr_pipe[0]); + + if (intermediate_child) + { + /* We need to fork an intermediate child that launches the + * final child. The purpose of the intermediate child + * is to exit, so we can waitpid() it immediately. + * Then the grandchild will not become a zombie. + */ + gint grandchild_pid; + + grandchild_pid = fork (); + + if (grandchild_pid < 0) + { + /* report -1 as child PID */ + write (child_pid_report_pipe[1], &grandchild_pid, + sizeof(grandchild_pid)); + + write_err_and_exit (child_err_report_pipe[1], + CHILD_FORK_FAILED); + } + else if (grandchild_pid == 0) + { + do_exec (child_err_report_pipe[1], + stdin_pipe[0], + stdout_pipe[1], + stderr_pipe[1], + working_directory, + argv, + envp, + close_descriptors, + search_path, + stdout_to_null, + stderr_to_null, + child_inherits_stdin, + child_setup, + user_data); + } + else + { + write (child_pid_report_pipe[1], &grandchild_pid, sizeof(grandchild_pid)); + close_and_invalidate (&child_pid_report_pipe[1]); + + _exit (0); + } + } + else + { + /* Just run the child. + */ + + do_exec (child_err_report_pipe[1], + stdin_pipe[0], + stdout_pipe[1], + stderr_pipe[1], + working_directory, + argv, + envp, + close_descriptors, + search_path, + stdout_to_null, + stderr_to_null, + child_inherits_stdin, + child_setup, + user_data); + } + } + else + { + /* Parent */ + + gint buf[2]; + gint n_ints = 0; + + /* Close the uncared-about ends of the pipes */ + close_and_invalidate (&child_err_report_pipe[1]); + close_and_invalidate (&child_pid_report_pipe[1]); + close_and_invalidate (&stdin_pipe[0]); + close_and_invalidate (&stdout_pipe[1]); + close_and_invalidate (&stderr_pipe[1]); + + /* If we had an intermediate child, reap it */ + if (intermediate_child) + { + wait_again: + if (waitpid (pid, &status, 0) < 0) + { + if (errno == EINTR) + goto wait_again; + else if (errno == ECHILD) + ; /* do nothing, child already reaped */ + else + g_warning ("waitpid() should not fail in %s", __FUNCTION__); + } + } + + + if (!read_ints (child_err_report_pipe[0], + buf, 2, &n_ints, + error)) + goto cleanup_and_fail; + + if (n_ints >= 2) + { + /* Error from the child. */ + + switch (buf[0]) + { + case CHILD_CHDIR_FAILED: + g_set_error (error, + G_SPAWN_ERROR, + G_SPAWN_ERROR_CHDIR, + _("Failed to change to directory '%s' (%s)"), + working_directory, + g_strerror (buf[1])); + + break; + + case CHILD_EXEC_FAILED: + g_set_error (error, + G_SPAWN_ERROR, + exec_err_to_g_error (buf[1]), + _("Failed to execute child process (%s)"), + g_strerror (buf[1])); + + break; + + case CHILD_DUP2_FAILED: + g_set_error (error, + G_SPAWN_ERROR, + G_SPAWN_ERROR_FAILED, + _("Failed to redirect output or input of child process (%s)"), + g_strerror (buf[1])); + + break; + + case CHILD_FORK_FAILED: + g_set_error (error, + G_SPAWN_ERROR, + G_SPAWN_ERROR_FORK, + _("Failed to fork child process (%s)"), + g_strerror (buf[1])); + break; + + default: + g_set_error (error, + G_SPAWN_ERROR, + G_SPAWN_ERROR_FAILED, + _("Unknown error executing child process")); + break; + } + + goto cleanup_and_fail; + } + + /* Get child pid from intermediate child pipe. */ + if (intermediate_child) + { + n_ints = 0; + + if (!read_ints (child_pid_report_pipe[0], + buf, 1, &n_ints, error)) + goto cleanup_and_fail; + + if (n_ints < 1) + { + g_set_error (error, + G_SPAWN_ERROR, + G_SPAWN_ERROR_FAILED, + _("Failed to read enough data from child pid pipe (%s)"), + g_strerror (errno)); + goto cleanup_and_fail; + } + else + { + /* we have the child pid */ + pid = buf[0]; + } + } + + /* Success against all odds! return the information */ + + if (child_pid) + *child_pid = pid; + + if (standard_input) + *standard_input = stdin_pipe[1]; + if (standard_output) + *standard_output = stdout_pipe[0]; + if (standard_error) + *standard_error = stderr_pipe[0]; + + return TRUE; + } + + cleanup_and_fail: + close_and_invalidate (&child_err_report_pipe[0]); + close_and_invalidate (&child_err_report_pipe[1]); + close_and_invalidate (&child_pid_report_pipe[0]); + close_and_invalidate (&child_pid_report_pipe[1]); + close_and_invalidate (&stdin_pipe[0]); + close_and_invalidate (&stdin_pipe[1]); + close_and_invalidate (&stdout_pipe[0]); + close_and_invalidate (&stdout_pipe[1]); + close_and_invalidate (&stderr_pipe[0]); + close_and_invalidate (&stderr_pipe[1]); + + return FALSE; +} + +static gboolean +make_pipe (gint p[2], + GError **error) +{ + if (pipe (p) < 0) + { + g_set_error (error, + G_SPAWN_ERROR, + G_SPAWN_ERROR_FAILED, + _("Failed to create pipe for communicating with child process (%s)"), + g_strerror (errno)); + return FALSE; + } + else + return TRUE; +} + +/* Based on execvp from GNU C Library */ + +static void +script_execute (const gchar *file, + gchar **argv, + gchar **envp, + gboolean search_path) +{ + /* Count the arguments. */ + int argc = 0; + while (argv[argc]) + ++argc; + + /* Construct an argument list for the shell. */ + { + gchar **new_argv; + + new_argv = g_new0 (gchar*, argc + 1); + + new_argv[0] = (char *) "/bin/sh"; + new_argv[1] = (char *) file; + while (argc > 1) + { + new_argv[argc] = argv[argc - 1]; + --argc; + } + + /* Execute the shell. */ + if (envp) + execve (new_argv[0], new_argv, envp); + else + execv (new_argv[0], new_argv); + + g_free (new_argv); + } +} + +static gchar* +my_strchrnul (const gchar *str, gchar c) +{ + gchar *p = (gchar*) str; + while (*p && (*p != c)) + ++p; + + return p; +} + +static gint +g_execute (const gchar *file, + gchar **argv, + gchar **envp, + gboolean search_path) +{ + if (*file == '\0') + { + /* We check the simple case first. */ + errno = ENOENT; + return -1; + } + + if (!search_path || strchr (file, '/') != NULL) + { + /* Don't search when it contains a slash. */ + if (envp) + execve (file, argv, envp); + else + execv (file, argv); + + if (errno == ENOEXEC) + script_execute (file, argv, envp, FALSE); + } + else + { + gboolean got_eacces = 0; + char *path, *p, *name, *freeme; + size_t len; + size_t pathlen; + + path = g_getenv ("PATH"); + if (path == NULL) + { + /* There is no `PATH' in the environment. The default + * search path in libc is the current directory followed by + * the path `confstr' returns for `_CS_PATH'. + */ + + /* In GLib we put . last, for security, and don't use the + * unportable confstr(); UNIX98 does not actually specify + * what to search if PATH is unset. POSIX may, dunno. + */ + + path = "/bin:/usr/bin:."; + } + + len = strlen (file) + 1; + pathlen = strlen (path); + freeme = name = g_malloc (pathlen + len + 1); + + /* Copy the file name at the top, including '\0' */ + memcpy (name + pathlen + 1, file, len); + name = name + pathlen; + /* And add the slash before the filename */ + *name = '/'; + + p = path; + do + { + char *startp; + + path = p; + p = my_strchrnul (path, ':'); + + if (p == path) + /* Two adjacent colons, or a colon at the beginning or the end + * of `PATH' means to search the current directory. + */ + startp = name + 1; + else + startp = memcpy (name - (p - path), path, p - path); + + /* Try to execute this name. If it works, execv will not return. */ + if (envp) + execve (startp, argv, envp); + else + execv (startp, argv); + + if (errno == ENOEXEC) + script_execute (startp, argv, envp, search_path); + + switch (errno) + { + case EACCES: + /* Record the we got a `Permission denied' error. If we end + * up finding no executable we can use, we want to diagnose + * that we did find one but were denied access. + */ + got_eacces = TRUE; + + /* FALL THRU */ + + case ENOENT: +#ifdef ESTALE + case ESTALE: +#endif +#ifdef ENOTDIR + case ENOTDIR: +#endif + /* Those errors indicate the file is missing or not executable + * by us, in which case we want to just try the next path + * directory. + */ + break; + + default: + /* Some other error means we found an executable file, but + * something went wrong executing it; return the error to our + * caller. + */ + g_free (freeme); + return -1; + } + } + while (*p++ != '\0'); + + /* We tried every element and none of them worked. */ + if (got_eacces) + /* At least one failure was due to permissions, so report that + * error. + */ + errno = EACCES; + + g_free (freeme); + } + + /* Return the error from the last attempt (probably ENOENT). */ + return -1; +} diff --git a/glib/gspawn.h b/glib/gspawn.h new file mode 100644 index 000000000..4ae0536b6 --- /dev/null +++ b/glib/gspawn.h @@ -0,0 +1,132 @@ +/* gspawn.h - Process launching + * + * Copyright 2000 Red Hat, Inc. + * + * GLib is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * GLib is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with GLib; see the file COPYING.LIB. If not, write + * to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#ifndef __GSPAWN_H__ +#define __GSPAWN_H__ + +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + +/* I'm not sure I remember our proposed naming convention here. */ +#define G_SPAWN_ERROR g_spawn_error_quark () + +typedef enum +{ + G_SPAWN_ERROR_FORK, /* fork failed due to lack of memory */ + G_SPAWN_ERROR_READ, /* read or select on pipes failed */ + G_SPAWN_ERROR_CHDIR, /* changing to working dir failed */ + G_SPAWN_ERROR_ACCES, /* execv() returned EACCES */ + G_SPAWN_ERROR_PERM, /* execv() returned EPERM */ + G_SPAWN_ERROR_2BIG, /* execv() returned E2BIG */ + G_SPAWN_ERROR_NOEXEC, /* execv() returned ENOEXEC */ + G_SPAWN_ERROR_NAMETOOLONG, /* "" "" ENAMETOOLONG */ + G_SPAWN_ERROR_NOENT, /* "" "" ENOENT */ + G_SPAWN_ERROR_NOMEM, /* "" "" ENOMEM */ + G_SPAWN_ERROR_NOTDIR, /* "" "" ENOTDIR */ + G_SPAWN_ERROR_LOOP, /* "" "" ELOOP */ + G_SPAWN_ERROR_TXTBUSY, /* "" "" ETXTBUSY */ + G_SPAWN_ERROR_IO, /* "" "" EIO */ + G_SPAWN_ERROR_NFILE, /* "" "" ENFILE */ + G_SPAWN_ERROR_MFILE, /* "" "" EMFLE */ + G_SPAWN_ERROR_INVAL, /* "" "" EINVAL */ + G_SPAWN_ERROR_ISDIR, /* "" "" EISDIR */ + G_SPAWN_ERROR_LIBBAD, /* "" "" ELIBBAD */ + G_SPAWN_ERROR_FAILED /* other fatal failure, error->message + * should explain + */ +} GSpawnError; + +typedef void (* GSpawnChildSetupFunc) (gpointer user_data); + +typedef enum +{ + G_SPAWN_LEAVE_DESCRIPTORS_OPEN = 1 << 0, + G_SPAWN_DO_NOT_REAP_CHILD = 1 << 1, + /* look for argv[0] in the path i.e. use execvp() */ + G_SPAWN_SEARCH_PATH = 1 << 2, + /* Dump output to /dev/null */ + G_SPAWN_STDOUT_TO_DEV_NULL = 1 << 3, + G_SPAWN_STDERR_TO_DEV_NULL = 1 << 4, + G_SPAWN_CHILD_INHERITS_STDIN = 1 << 5 +} GSpawnFlags; + +GQuark g_spawn_error_quark (void); + +gboolean g_spawn_async (const gchar *working_directory, + gchar **argv, + gchar **envp, + GSpawnFlags flags, + GSpawnChildSetupFunc child_setup, + gpointer user_data, + gint *child_pid, + GError **error); + + +/* Opens pipes for non-NULL standard_output, standard_input, standard_error, + * and returns the parent's end of the pipes. + */ +gboolean g_spawn_async_with_pipes (const gchar *working_directory, + gchar **argv, + gchar **envp, + GSpawnFlags flags, + GSpawnChildSetupFunc child_setup, + gpointer user_data, + gint *child_pid, + gint *standard_input, + gint *standard_output, + gint *standard_error, + GError **error); + + +/* If standard_output or standard_error are non-NULL, the full + * standard output or error of the command will be placed there. + */ + +gboolean g_spawn_sync (const gchar *working_directory, + gchar **argv, + gchar **envp, + GSpawnFlags flags, + GSpawnChildSetupFunc child_setup, + gpointer user_data, + gchar **standard_output, + gchar **standard_error, + gint *exit_status, + GError **error); + +gboolean g_spawn_command_line_sync (const gchar *command_line, + gchar **standard_output, + gchar **standard_error, + gint *exit_status, + GError **error); +gboolean g_spawn_command_line_async (const gchar *command_line, + GError **error); + + +#ifdef __cplusplus +} +#endif + +#endif /* __GSPAWN_H__ */ + + diff --git a/glib/guniprop.c b/glib/guniprop.c index 3c2c29e6a..c45fc7ab8 100644 --- a/glib/guniprop.c +++ b/glib/guniprop.c @@ -118,9 +118,16 @@ g_unichar_ispunct (gunichar c) gboolean g_unichar_isspace (gunichar c) { - int t = TYPE (c); - return (t == G_UNICODE_SPACE_SEPARATOR || t == G_UNICODE_LINE_SEPARATOR - || t == G_UNICODE_PARAGRAPH_SEPARATOR); + /* special-case these since Unicode thinks they are not spaces */ + if (c == ' ' || c == '\t' || c == '\n' || c == '\r' || + c == '\f' || c == '\v') /* "the mythical vertical tab" */ + return TRUE; + else + { + int t = TYPE (c); + return (t == G_UNICODE_SPACE_SEPARATOR || t == G_UNICODE_LINE_SEPARATOR + || t == G_UNICODE_PARAGRAPH_SEPARATOR); + } } /** diff --git a/glib/gutils.c b/glib/gutils.c index c7284ca29..4910beb0c 100644 --- a/glib/gutils.c +++ b/glib/gutils.c @@ -143,6 +143,106 @@ g_atexit (GVoidFunc func) g_error ("Could not register atexit() function: %s", error); } +/* Based on execvp() from GNU Libc. + * Some of this code is cut-and-pasted into gspawn.c + */ + +static gchar* +my_strchrnul (const gchar *str, gchar c) +{ + gchar *p = (gchar*)str; + while (*p && (*p != c)) + ++p; + + return p; +} + +/** + * g_find_program_in_path: + * @file: a program name + * + * Locates the first executable named @file in the user's path, in the + * same way that execvp() would locate it. Returns an allocated string + * with the absolute path name, or NULL if the program is not found in + * the path. If @file is already an absolute path, returns a copy of + * @file if @file exists and is executable, and NULL otherwise. + * + * Return value: absolute path, or NULL + **/ +gchar* +g_find_program_in_path (const gchar *file) +{ + gchar *path, *p, *name, *freeme; + size_t len; + size_t pathlen; + + g_return_val_if_fail (file != NULL, NULL); + + if (*file == '/') + { + if (g_file_test (file, G_FILE_TEST_IS_EXECUTABLE)) + return g_strdup (file); + else + return NULL; + } + + path = g_getenv ("PATH"); + if (path == NULL) + { + /* There is no `PATH' in the environment. The default + * search path in libc is the current directory followed by + * the path `confstr' returns for `_CS_PATH'. + */ + + /* In GLib we put . last, for security, and don't use the + * unportable confstr(); UNIX98 does not actually specify + * what to search if PATH is unset. POSIX may, dunno. + */ + + path = "/bin:/usr/bin:."; + } + + len = strlen (file) + 1; + pathlen = strlen (path); + freeme = name = g_malloc (pathlen + len + 1); + + /* Copy the file name at the top, including '\0' */ + memcpy (name + pathlen + 1, file, len); + name = name + pathlen; + /* And add the slash before the filename */ + *name = '/'; + + p = path; + do + { + char *startp; + + path = p; + p = my_strchrnul (path, ':'); + + if (p == path) + /* Two adjacent colons, or a colon at the beginning or the end + * of `PATH' means to search the current directory. + */ + startp = name + 1; + else + startp = memcpy (name - (p - path), path, p - path); + + if (g_file_test (startp, G_FILE_TEST_IS_EXECUTABLE)) + { + gchar *ret; + ret = g_strdup (startp); + g_free (freeme); + return ret; + } + } + while (*p++ != '\0'); + + g_free (freeme); + + return NULL; +} + gint g_snprintf (gchar *str, gulong n, diff --git a/gshell.c b/gshell.c new file mode 100644 index 000000000..4bae260be --- /dev/null +++ b/gshell.c @@ -0,0 +1,651 @@ +/* gshell.c - Shell-related utilities + * + * Copyright 2000 Red Hat, Inc. + * g_execvpe implementation based on GNU libc execvp: + * Copyright 1991, 92, 95, 96, 97, 98, 99 Free Software Foundation, Inc. + * + * GLib is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * GLib is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with GLib; see the file COPYING.LIB. If not, write + * to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "glib.h" +#include + +#ifdef _ +#warning "FIXME remove gettext hack" +#endif + +#define _(x) x + +GQuark +g_shell_error_quark (void) +{ + static GQuark quark = 0; + if (quark == 0) + quark = g_quark_from_static_string ("g-shell-error-quark"); + return quark; +} + +/* Single quotes preserve the literal string exactly. escape + * sequences are not allowed; not even \' - if you want a ' + * in the quoted text, you have to do something like 'foo'\''bar' + * + * Double quotes allow $ ` " \ and newline to be escaped with backslash. + * Otherwise double quotes preserve things literally. + */ + +gboolean +unquote_string_inplace (gchar* str, gchar** end, GError** err) +{ + gchar* dest; + gchar* s; + gchar quote_char; + + g_return_val_if_fail(end != NULL, FALSE); + g_return_val_if_fail(err == NULL || *err == NULL, FALSE); + g_return_val_if_fail(str != NULL, FALSE); + + dest = s = str; + + quote_char = *s; + + if (!(*s == '"' || *s == '\'')) + { + if (err) + *err = g_error_new(G_SHELL_ERROR, + G_SHELL_ERROR_BAD_QUOTING, + _("Quoted text doesn't begin with a quotation mark")); + *end = str; + return FALSE; + } + + /* Skip the initial quote mark */ + ++s; + + if (quote_char == '"') + { + while (*s) + { + g_assert(s > dest); /* loop invariant */ + + switch (*s) + { + case '"': + /* End of the string, return now */ + *dest = '\0'; + ++s; + *end = s; + return TRUE; + break; + + case '\\': + /* Possible escaped quote or \ */ + ++s; + switch (*s) + { + case '"': + case '\\': + case '`': + case '$': + case '\n': + *dest = *s; + ++s; + ++dest; + break; + + default: + /* not an escaped char */ + *dest = '\\'; + ++dest; + /* ++s already done. */ + break; + } + break; + + default: + *dest = *s; + ++dest; + ++s; + break; + } + + g_assert(s > dest); /* loop invariant */ + } + } + else + { + while (*s) + { + g_assert(s > dest); /* loop invariant */ + + if (*s == '\'') + { + /* End of the string, return now */ + *dest = '\0'; + ++s; + *end = s; + return TRUE; + } + else + { + *dest = *s; + ++dest; + ++s; + } + + g_assert(s > dest); /* loop invariant */ + } + } + + /* If we reach here this means the close quote was never encountered */ + + *dest = '\0'; + + if (err) + *err = g_error_new(G_SHELL_ERROR, + G_SHELL_ERROR_BAD_QUOTING, + _("Unmatched quotation mark in command line or other shell-quoted text")); + *end = s; + return FALSE; +} + +/** + * g_shell_quote: + * @unquoted_string: a literal string + * + * Quotes a string so that the shell (/bin/sh) will interpret the + * quoted string to mean @unquoted_string. If you pass a filename to + * the shell, for example, you should first quote it with this + * function. The return value must be freed with g_free(). The + * quoting style used is undefined (single or double quotes may be + * used). + * + * Return value: quoted string + **/ +gchar* +g_shell_quote (const gchar *unquoted_string) +{ + /* We always use single quotes, because the algorithm is cheesier. + * We could use double if we felt like it, that might be more + * human-readable. + */ + + const gchar *p; + GString *dest; + + g_return_val_if_fail (unquoted_string != NULL, NULL); + + dest = g_string_new ("'"); + + p = unquoted_string; + + /* could speed this up a lot by appending chunks of text at a + * time. + */ + while (*p) + { + /* Replace literal ' with a close ', a \', and a open ' */ + if (*p == '\'') + g_string_append (dest, "'\''"); + else + g_string_append_c (dest, *p); + + ++p; + } + + /* close the quote */ + g_string_append_c (dest, '\''); + + return g_string_free (dest, FALSE); +} + +/** + * g_shell_unquote: + * @quoted_string: shell-quoted string + * @error: error return location or NULL + * + * Unquotes a string as the shell (/bin/sh) would. Only handles + * quotes; if a string contains file globs, arithmetic operators, + * variables, backticks, redirections, or other special-to-the-shell + * features, the result will be different from the result a real shell + * would produce (the variables, backticks, etc. will be passed + * through literally instead of being expanded). This function is + * guaranteed to succeed if applied to the result of + * g_shell_quote(). If it fails, it returns NULL and sets the + * error. The @quoted_string need not actually contain quoted or + * escaped text; g_shell_unquote() simply goes through the string and + * unquotes/unescapes anything that the shell would. Both single and + * double quotes are handled, as are escapes including escaped + * newlines. The return value must be freed with g_free(). Possible + * errors are in the #G_SHELL_ERROR domain. + * + * Return value: an unquoted string + **/ +gchar* +g_shell_unquote (const gchar *quoted_string, + GError **error) +{ + gchar *unquoted; + gchar *end; + gchar *start; + GString *retval; + + g_return_val_if_fail (quoted_string != NULL, NULL); + + unquoted = g_strdup (quoted_string); + + start = unquoted; + end = unquoted; + retval = g_string_new (""); + + /* The loop allows cases such as + * "foo"blah blah'bar'woo foo"baz"la la la\'\''foo' + */ + while (*start) + { + /* Append all non-quoted chars, honoring backslash escape + */ + + while (*start && !(*start == '"' || *start == '\'')) + { + if (*start == '\\') + { + /* all characters can get escaped by backslash, + * except newline, which is removed if it follows + * a backslash outside of quotes + */ + + ++start; + if (*start) + { + if (*start != '\n') + g_string_append_c (retval, *start); + ++start; + } + } + else + { + g_string_append_c (retval, *start); + ++start; + } + } + + if (*start) + { + if (!unquote_string_inplace (start, &end, error)) + { + goto error; + } + else + { + g_string_append (retval, start); + start = end; + } + } + } + + return g_string_free (retval, FALSE); + + error: + g_assert (error == NULL || *error != NULL); + + g_free (unquoted); + g_string_free (retval, TRUE); + return NULL; +} + +/* g_parse_argv() does a semi-arbitrary weird subset of the way + * the shell parses a command line. We don't do variable expansion, + * don't understand that operators are tokens, don't do tilde expansion, + * don't do command substitution, no arithmetic expansion, IFS gets ignored, + * don't do filename globs, don't remove redirection stuff, etc. + * + * READ THE UNIX98 SPEC on "Shell Command Language" before changing + * the behavior of this code. + * + * Steps to parsing the argv string: + * + * - tokenize the string (but since we ignore operators, + * our tokenization may diverge from what the shell would do) + * note that tokenization ignores the internals of a quoted + * word and it always splits on spaces, not on IFS even + * if we used IFS. We also ignore "end of input indicator" + * (I guess this is control-D?) + * + * Tokenization steps, from UNIX98 with operator stuff removed, + * are: + * + * 1) "If the current character is backslash, single-quote or + * double-quote (\, ' or ") and it is not quoted, it will affect + * quoting for subsequent characters up to the end of the quoted + * text. The rules for quoting are as described in Quoting + * . During token recognition no substitutions will be actually + * performed, and the result token will contain exactly the + * characters that appear in the input (except for newline + * character joining), unmodified, including any embedded or + * enclosing quotes or substitution operators, between the quote + * mark and the end of the quoted text. The token will not be + * delimited by the end of the quoted field." + * + * 2) "If the current character is an unquoted newline character, + * the current token will be delimited." + * + * 3) "If the current character is an unquoted blank character, any + * token containing the previous character is delimited and the + * current character will be discarded." + * + * 4) "If the previous character was part of a word, the current + * character will be appended to that word." + * + * 5) "If the current character is a "#", it and all subsequent + * characters up to, but excluding, the next newline character + * will be discarded as a comment. The newline character that + * ends the line is not considered part of the comment. The + * "#" starts a comment only when it is at the beginning of a + * token. Since the search for the end-of-comment does not + * consider an escaped newline character specially, a comment + * cannot be continued to the next line." + * + * 6) "The current character will be used as the start of a new word." + * + * + * - for each token (word), perform portions of word expansion, namely + * field splitting (using default whitespace IFS) and quote + * removal. Field splitting may increase the number of words. + * Quote removal does not increase the number of words. + * + * "If the complete expansion appropriate for a word results in an + * empty field, that empty field will be deleted from the list of + * fields that form the completely expanded command, unless the + * original word contained single-quote or double-quote characters." + * - UNIX98 spec + * + * + */ + +static inline void +ensure_token (GString **token) +{ + if (*token == NULL) + *token = g_string_new (""); +} + +static void +delimit_token (GString **token, + GSList **retval) +{ + if (*token == NULL) + return; + + *retval = g_slist_prepend (*retval, g_string_free (*token, FALSE)); + + *token = NULL; +} + +static GSList* +tokenize_command_line (const gchar *command_line, + GError **error) +{ + gchar current_quote; + const gchar *p; + GString *current_token = NULL; + GSList *retval = NULL; + + current_quote = '\0'; + p = command_line; + + while (*p) + { + if (current_quote == '\\') + { + if (*p == '\n') + { + /* we append nothing; backslash-newline become nothing */ + } + else + { + /* we append the backslash and the current char, + * to be interpreted later after tokenization + */ + ensure_token (¤t_token); + g_string_append_c (current_token, '\\'); + g_string_append_c (current_token, *p); + } + + current_quote = '\0'; + } + else if (current_quote == '#') + { + /* Discard up to and including next newline */ + while (*p && *p != '\n') + ++p; + + current_quote = '\0'; + + if (*p == '\0') + break; + } + else if (current_quote) + { + if (*p == current_quote && + /* check that it isn't an escaped double quote */ + !(current_quote == '"' && p != command_line && *(p - 1) == '\\')) + { + /* close the quote */ + current_quote = '\0'; + } + + /* Everything inside quotes, and the close quote, + * gets appended literally. + */ + + ensure_token (¤t_token); + g_string_append_c (current_token, *p); + } + else + { + switch (*p) + { + case '\n': + delimit_token (¤t_token, &retval); + break; + + case ' ': + case '\t': + /* If the current token contains the previous char, delimit + * the current token. A nonzero length + * token should always contain the previous char. + */ + if (current_token && + current_token->len > 0) + { + delimit_token (¤t_token, &retval); + } + + /* discard all unquoted blanks (don't add them to a token) */ + break; + + + /* single/double quotes are appended to the token, + * escapes are maybe appended next time through the loop, + * comment chars are never appended. + */ + + case '\'': + case '"': + ensure_token (¤t_token); + g_string_append_c (current_token, *p); + + /* FALL THRU */ + + case '#': + case '\\': + current_quote = *p; + break; + + default: + /* Combines rules 4) and 6) - if we have a token, append to it, + * otherwise create a new token. + */ + ensure_token (¤t_token); + g_string_append_c (current_token, *p); + break; + } + } + + ++p; + } + + delimit_token (¤t_token, &retval); + + if (current_quote) + { + if (current_quote == '\\') + g_set_error (error, + G_SHELL_ERROR, + G_SHELL_ERROR_BAD_QUOTING, + _("Text ended just after a '\' character." + " (The text was '%s')"), + command_line); + else + g_set_error (error, + G_SHELL_ERROR, + G_SHELL_ERROR_BAD_QUOTING, + _("Text ended before matching quote was found for %c." + " (The text was '%s')"), + current_quote, command_line); + + goto error; + } + + if (retval == NULL) + { + g_set_error (error, + G_SHELL_ERROR, + G_SHELL_ERROR_EMPTY_STRING, + _("Text was empty (or contained only whitespace)")); + + goto error; + } + + /* we appended backward */ + retval = g_slist_reverse (retval); + + return retval; + + error: + g_assert (error == NULL || *error != NULL); + + if (retval) + { + g_slist_foreach (retval, (GFunc)g_free, NULL); + g_slist_free (retval); + } + + return NULL; +} + +/** + * g_shell_parse_argv: + * @command_line: command line to parse + * @argcp: return location for number of args + * @argvp: return location for array of args + * @error: return location for error + * + * Parses a command line into an argument vector, in much the same way + * the shell would, but without many of the expansions the shell would + * perform (variable expansion, globs, operators, filename expansion, + * etc. are not supported). The results are defined to be the same as + * those you would get from a UNIX98 /bin/sh, as long as the input + * contains none of the unsupported shell expansions. If the input + * does contain such expansions, they are passed through + * literally. Possible errors are those from the #G_SHELL_ERROR + * domain. + * + * Return value: TRUE on success, FALSE if error set + **/ +gboolean +g_shell_parse_argv (const gchar *command_line, + gint *argcp, + gchar ***argvp, + GError **error) +{ + /* Code based on poptParseArgvString() from libpopt */ + gint argc = 0; + gchar **argv = NULL; + GSList *tokens = NULL; + gint i; + GSList *tmp_list; + + g_return_val_if_fail (command_line != NULL, FALSE); + + tokens = tokenize_command_line (command_line, error); + if (tokens == NULL) + return FALSE; + + /* Because we can't have introduced any new blank space into the + * tokens (we didn't do any new expansions), we don't need to + * perform field splitting. If we were going to honor IFS or do any + * expansions, we would have to do field splitting on each word + * here. Also, if we were going to do any expansion we would need to + * remove any zero-length words that didn't contain quotes + * originally; but since there's no expansion we know all words have + * nonzero length, unless they contain quotes. + * + * So, we simply remove quotes, and don't do any field splitting or + * empty word removal, since we know there was no way to introduce + * such things. + */ + + argc = g_slist_length (tokens); + argv = g_new0 (gchar*, argc + 1); + i = 0; + tmp_list = tokens; + while (tmp_list) + { + argv[i] = g_shell_unquote (tmp_list->data, error); + + /* Since we already checked that quotes matched up in the + * tokenizer, this shouldn't be possible to reach I guess. + */ + if (argv[i] == NULL) + goto failed; + + tmp_list = g_slist_next (tmp_list); + ++i; + } + + g_slist_foreach (tokens, (GFunc)g_free, NULL); + g_slist_free (tokens); + + if (argcp) + *argcp = argc; + + if (argvp) + *argvp = argv; + else + g_strfreev (argv); + + return TRUE; + + failed: + + g_assert (error == NULL || *error != NULL); + g_strfreev (argv); + g_slist_foreach (tokens, (GFunc) g_free, NULL); + g_slist_free (tokens); + + return FALSE; +} diff --git a/gshell.h b/gshell.h new file mode 100644 index 000000000..0f7fd1f3b --- /dev/null +++ b/gshell.h @@ -0,0 +1,59 @@ +/* gshell.h - Shell-related utilities + * + * Copyright 2000 Red Hat, Inc. + * + * GLib is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * GLib is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with GLib; see the file COPYING.LIB. If not, write + * to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#ifndef __GSHELL_H__ +#define __GSHELL_H__ + +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + +#define G_SHELL_ERROR g_shell_error_quark () + +typedef enum +{ + /* mismatched or otherwise mangled quoting */ + G_SHELL_ERROR_BAD_QUOTING, + /* string to be parsed was empty */ + G_SHELL_ERROR_EMPTY_STRING, + G_SHELL_ERROR_FAILED +} GShellError; + +GQuark g_shell_error_quark (void); + +gchar* g_shell_quote (const gchar *unquoted_string); +gchar* g_shell_unquote (const gchar *quoted_string, + GError **error); +gboolean g_shell_parse_argv (const gchar *command_line, + gint *argc, + gchar ***argv, + GError **error); + + +#ifdef __cplusplus +} +#endif + +#endif /* __GSHELL_H__ */ + + diff --git a/gspawn.c b/gspawn.c new file mode 100644 index 000000000..b6af58de7 --- /dev/null +++ b/gspawn.c @@ -0,0 +1,1392 @@ +/* gspawn.c - Process launching + * + * Copyright 2000 Red Hat, Inc. + * g_execvpe implementation based on GNU libc execvp: + * Copyright 1991, 92, 95, 96, 97, 98, 99 Free Software Foundation, Inc. + * + * GLib is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * GLib is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with GLib; see the file COPYING.LIB. If not, write + * to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "glib.h" +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _ +#warning "FIXME remove gettext hack" +#endif + +#define _(x) x + +static gint g_execute (const gchar *file, + gchar **argv, + gchar **envp, + gboolean search_path); + +static gboolean make_pipe (gint p[2], + GError **error); +static gboolean fork_exec_with_pipes (gboolean intermediate_child, + const gchar *working_directory, + gchar **argv, + gchar **envp, + gboolean close_descriptors, + gboolean search_path, + gboolean stdout_to_null, + gboolean stderr_to_null, + gboolean child_inherits_stdin, + GSpawnChildSetupFunc child_setup, + gpointer user_data, + gint *child_pid, + gint *standard_input, + gint *standard_output, + gint *standard_error, + GError **error); + +GQuark +g_spawn_error_quark (void) +{ + static GQuark quark = 0; + if (quark == 0) + quark = g_quark_from_static_string ("g-exec-error-quark"); + return quark; +} + +/** + * g_spawn_async: + * @working_directory: child's current working directory, or NULL to inherit parent's + * @argv: child's argument vector + * @envp: child's environment, or NULL to inherit parent's + * @flags: flags from #GSpawnFlags + * @child_setup: function to run in the child just before exec() + * @user_data: user data for @child_setup + * @child_pid: return location for child process ID, or NULL + * @error: return location for error + * + * See g_spawn_async_with_pipes() for a full description; this function + * simply calls the g_spawn_async_with_pipes() without any pipes. + * + * Return value: TRUE on success, FALSE if error is set + **/ +gboolean +g_spawn_async (const gchar *working_directory, + gchar **argv, + gchar **envp, + GSpawnFlags flags, + GSpawnChildSetupFunc child_setup, + gpointer user_data, + gint *child_pid, + GError **error) +{ + g_return_val_if_fail (argv != NULL, FALSE); + + return g_spawn_async_with_pipes (working_directory, + argv, envp, + flags, + child_setup, + user_data, + child_pid, + NULL, NULL, NULL, + error); +} + +/* Avoids a danger in threaded situations (calling close() + * on a file descriptor twice, and another thread has + * re-opened it since the first close) + */ +static gint +close_and_invalidate (gint *fd) +{ + gint ret; + + ret = close (*fd); + *fd = -1; + + return ret; +} + +typedef enum +{ + READ_FAILED = 0, /* FALSE */ + READ_OK, + READ_EOF +} ReadResult; + +static ReadResult +read_data (GString *str, + gint fd, + GError **error) +{ + gint bytes; + gchar buf[4096]; + + again: + + bytes = read (fd, &buf, 4096); + + if (bytes == 0) + return READ_EOF; + else if (bytes > 0) + { + g_string_append_len (str, buf, bytes); + return READ_OK; + } + else if (bytes < 0 && errno == EINTR) + goto again; + else if (bytes < 0) + { + g_set_error (error, + G_SPAWN_ERROR, + G_SPAWN_ERROR_READ, + _("Failed to read data from child process (%s)"), + g_strerror (errno)); + + return READ_FAILED; + } + else + return READ_OK; +} + +/** + * g_spawn_sync: + * @working_directory: child's current working directory, or NULL to inherit parent's + * @argv: child's argument vector + * @envp: child's environment, or NULL to inherit parent's + * @flags: flags from #GSpawnFlags + * @child_setup: function to run in the child just before exec() + * @user_data: user data for @child_setup + * @standard_output: return location for child output + * @standard_error: return location for child error messages + * @exit_status: child exit status, as returned by waitpid() + * @error: return location for error + * + * Executes a child synchronously (waits for the child to exit before returning). + * All output from the child is stored in @standard_output and @standard_error, + * if those parameters are non-NULL. If @exit_status is non-NULL, the exit status + * of the child is stored there as it would be by waitpid(); standard UNIX + * macros such as WIFEXITED() and WEXITSTATUS() must be used to evaluate the + * exit status. If an error occurs, no data is returned in @standard_output, + * @standard_error, or @exit_status. + * + * This function calls g_spawn_async_with_pipes() internally; see that function + * for full details on the other parameters. + * + * Return value: TRUE on success, FALSE if an error was set. + **/ +gboolean +g_spawn_sync (const gchar *working_directory, + gchar **argv, + gchar **envp, + GSpawnFlags flags, + GSpawnChildSetupFunc child_setup, + gpointer user_data, + gchar **standard_output, + gchar **standard_error, + gint *exit_status, + GError **error) +{ + gint outpipe = -1; + gint errpipe = -1; + gint pid; + fd_set fds; + gint ret; + GString *outstr = NULL; + GString *errstr = NULL; + gboolean failed; + gint status; + + g_return_val_if_fail (argv != NULL, FALSE); + g_return_val_if_fail (!(flags & G_SPAWN_DO_NOT_REAP_CHILD), FALSE); + g_return_val_if_fail (standard_output == NULL || + !(flags & G_SPAWN_STDOUT_TO_DEV_NULL), FALSE); + g_return_val_if_fail (standard_error == NULL || + !(flags & G_SPAWN_STDERR_TO_DEV_NULL), FALSE); + + /* Just to ensure segfaults if callers try to use + * these when an error is reported. + */ + if (standard_output) + *standard_output = NULL; + + if (standard_error) + *standard_error = NULL; + + if (!fork_exec_with_pipes (FALSE, + working_directory, + argv, + envp, + !(flags & G_SPAWN_LEAVE_DESCRIPTORS_OPEN), + (flags & G_SPAWN_SEARCH_PATH) != 0, + (flags & G_SPAWN_STDOUT_TO_DEV_NULL) != 0, + (flags & G_SPAWN_STDERR_TO_DEV_NULL) != 0, + (flags & G_SPAWN_CHILD_INHERITS_STDIN) != 0, + child_setup, + user_data, + &pid, + NULL, + standard_output ? &outpipe : NULL, + standard_error ? &errpipe : NULL, + error)) + return FALSE; + + /* Read data from child. */ + + failed = FALSE; + + if (outpipe >= 0) + { + outstr = g_string_new (""); + } + + if (errpipe >= 0) + { + errstr = g_string_new (""); + } + + /* Read data until we get EOF on both pipes. */ + while (!failed && + (outpipe >= 0 || + errpipe >= 0)) + { + ret = 0; + + FD_ZERO (&fds); + if (outpipe >= 0) + FD_SET (outpipe, &fds); + if (errpipe >= 0) + FD_SET (errpipe, &fds); + + ret = select (MAX (outpipe, errpipe) + 1, + &fds, + NULL, NULL, + NULL /* no timeout */); + + if (ret < 0 && errno != EINTR) + { + failed = TRUE; + + g_set_error (error, + G_SPAWN_ERROR, + G_SPAWN_ERROR_READ, + _("Unexpected error in select() reading data from a child process (%s)"), + g_strerror (errno)); + + break; + } + + if (outpipe >= 0 && FD_ISSET (outpipe, &fds)) + { + switch (read_data (outstr, outpipe, error)) + { + case READ_FAILED: + failed = TRUE; + break; + case READ_EOF: + close_and_invalidate (&outpipe); + outpipe = -1; + break; + default: + break; + } + + if (failed) + break; + } + + if (errpipe >= 0 && FD_ISSET (errpipe, &fds)) + { + switch (read_data (errstr, errpipe, error)) + { + case READ_FAILED: + failed = TRUE; + break; + case READ_EOF: + close_and_invalidate (&errpipe); + errpipe = -1; + break; + default: + break; + } + + if (failed) + break; + } + } + + /* These should only be open still if we had an error. */ + + if (outpipe >= 0) + close_and_invalidate (&outpipe); + if (errpipe >= 0) + close_and_invalidate (&errpipe); + + /* Wait for child to exit, even if we have + * an error pending. + */ + again: + + ret = waitpid (pid, &status, 0); + + if (ret < 0) + { + if (errno == EINTR) + goto again; + else if (errno == ECHILD) + { + if (exit_status) + { + g_warning ("In call to g_spawn_sync(), exit status of a child process was requested but SIGCHLD action was set to SIG_IGN and ECHILD was received by waitpid(), so exit status can't be returned. This is a bug in the program calling g_spawn_sync(); either don't request the exit status, or don't set the SIGCHLD action."); + } + else + { + /* We don't need the exit status. */ + } + } + else + { + if (!failed) /* avoid error pileups */ + { + failed = TRUE; + + g_set_error (error, + G_SPAWN_ERROR, + G_SPAWN_ERROR_READ, + _("Unexpected error in waitpid() (%s)"), + g_strerror (errno)); + } + } + } + + if (failed) + { + if (outstr) + g_string_free (outstr, TRUE); + if (errstr) + g_string_free (errstr, TRUE); + + return FALSE; + } + else + { + if (exit_status) + *exit_status = status; + + if (standard_output) + *standard_output = g_string_free (outstr, FALSE); + + if (standard_error) + *standard_error = g_string_free (errstr, FALSE); + + return TRUE; + } +} + +/** + * g_spawn_async_with_pipes: + * @working_directory: child's current working directory, or NULL to inherit parent's + * @argv: child's argument vector + * @envp: child's environment, or NULL to inherit parent's + * @flags: flags from #GSpawnFlags + * @child_setup: function to run in the child just before exec() + * @user_data: user data for @child_setup + * @child_pid: return location for child process ID, or NULL + * @standard_input: return location for file descriptor to write to child's stdin, or NULL + * @standard_output: return location for file descriptor to read child's stdout, or NULL + * @standard_error: return location for file descriptor to read child's stderr, or NULL + * @error: return location for error + * + * Executes a child program asynchronously (your program will not + * block waiting for the child to exit). The child program is + * specified by the only argument that must be provided, @argv. @argv + * should be a NULL-terminated array of strings, to be passed as the + * argument vector for the child. The first string in @argv is of + * course the name of the program to execute. By default, the name of + * the program must be a full path; the PATH shell variable will only + * be searched if you pass the %G_SPAWN_SEARCH_PATH flag. + * + * @envp is a NULL-terminated array of strings, where each string + * has the form KEY=VALUE. This will become + * the child's environment. If @envp is NULL, the child inherits its + * parent's environment. + * + * @flags should be the bitwise OR of any flags you want to affect the + * function's behavior. The %G_SPAWN_DO_NOT_REAP_CHILD means that the + * child will not be automatically reaped; you must call waitpid() or + * handle SIGCHLD yourself, or the child will become a zombie. + * %G_SPAWN_LEAVE_DESCRIPTORS_OPEN means that the parent's open file + * descriptors will be inherited by the child; otherwise all + * descriptors except stdin/stdout/stderr will be closed before + * calling exec() in the child. %G_SPAWN_SEARCH_PATH means that + * argv[0] need not be an absolute path, it + * will be looked for in the user's PATH. %G_SPAWN_STDOUT_TO_DEV_NULL + * means that the child's standad output will be discarded, instead + * of going to the same location as the parent's standard output. + * %G_SPAWN_STDERR_TO_DEV_NULL means that the child's standard error + * will be discarded. %G_SPAWN_CHILD_INHERITS_STDIN means that + * the child will inherit the parent's standard input (by default, + * the child's standard input is attached to /dev/null). + * + * @child_setup and @user_data are a function and user data to be + * called in the child after GLib has performed all the setup it plans + * to perform (including creating pipes, closing file descriptors, + * etc.) but before calling exec(). That is, @child_setup is called + * just before calling exec() in the child. Obviously actions taken in + * this function will only affect the child, not the parent. + * + * If non-NULL, @child_pid will be filled with the child's process + * ID. You can use the process ID to send signals to the child, or + * to waitpid() if you specified the %G_SPAWN_DO_NOT_REAP_CHILD flag. + * + * If non-NULL, the @standard_input, @standard_output, @standard_error + * locations will be filled with file descriptors for writing to the child's + * standard input or reading from its standard output or standard error. + * The caller of g_spawn_async_with_pipes() must close these file descriptors + * when they are no longer in use. If these parameters are NULL, the + * corresponding pipe won't be created. + * + * @error can be NULL to ignore errors, or non-NULL to report errors. + * If an error is set, the function returns FALSE. Errors + * are reported even if they occur in the child (for example if the + * executable in argv[0] is not found). Typically + * the message field of returned errors should be displayed + * to users. Possible errors are those from the #G_SPAWN_ERROR domain. + * + * If an error occurs, @child_pid, @standard_input, @standard_output, + * and @standard_error will not be filled with valid values. + * + * Return value: TRUE on success, FALSE if an error was set + **/ +gboolean +g_spawn_async_with_pipes (const gchar *working_directory, + gchar **argv, + gchar **envp, + GSpawnFlags flags, + GSpawnChildSetupFunc child_setup, + gpointer user_data, + gint *child_pid, + gint *standard_input, + gint *standard_output, + gint *standard_error, + GError **error) +{ + g_return_val_if_fail (argv != NULL, FALSE); + g_return_val_if_fail (standard_output == NULL || + !(flags & G_SPAWN_STDOUT_TO_DEV_NULL), FALSE); + g_return_val_if_fail (standard_error == NULL || + !(flags & G_SPAWN_STDERR_TO_DEV_NULL), FALSE); + /* can't inherit stdin if we have an input pipe. */ + g_return_val_if_fail (standard_input == NULL || + !(flags & G_SPAWN_CHILD_INHERITS_STDIN), FALSE); + + return fork_exec_with_pipes (!(flags & G_SPAWN_DO_NOT_REAP_CHILD), + working_directory, + argv, + envp, + !(flags & G_SPAWN_LEAVE_DESCRIPTORS_OPEN), + (flags & G_SPAWN_SEARCH_PATH) != 0, + (flags & G_SPAWN_STDOUT_TO_DEV_NULL) != 0, + (flags & G_SPAWN_STDERR_TO_DEV_NULL) != 0, + (flags & G_SPAWN_CHILD_INHERITS_STDIN) != 0, + child_setup, + user_data, + child_pid, + standard_input, + standard_output, + standard_error, + error); +} + +/** + * g_spawn_command_line_sync: + * @command_line: a command line + * @standard_output: return location for child output + * @standard_error: return location for child errors + * @exit_status: return location for child exit status + * @error: return location for errors + * + * A simple version of g_spawn_sync() with little-used parameters + * removed, taking a command line instead of an argument vector. See + * g_spawn_sync() for full details. @command_line will be parsed by + * g_shell_parse_argv(). Unlike g_spawn_sync(), the %G_SPAWN_SEARCH_PATH flag + * is enabled. Note that %G_SPAWN_SEARCH_PATH can have security + * implications, so consider using g_spawn_sync() directly if + * appropriate. Possible errors are those from g_spawn_sync() and those + * from g_shell_parse_argv(). + * + * Return value: TRUE on success, FALSE if an error was set + **/ +gboolean +g_spawn_command_line_sync (const gchar *command_line, + gchar **standard_output, + gchar **standard_error, + gint *exit_status, + GError **error) +{ + gboolean retval; + gchar **argv = 0; + + g_return_val_if_fail (command_line != NULL, FALSE); + + if (!g_shell_parse_argv (command_line, + NULL, &argv, + error)) + return FALSE; + + retval = g_spawn_sync (NULL, + argv, + NULL, + G_SPAWN_SEARCH_PATH, + NULL, + NULL, + standard_output, + standard_error, + exit_status, + error); + g_strfreev (argv); + + return retval; +} + +/** + * g_spawn_command_line_async: + * @command_line: a command line + * @error: return location for errors + * + * A simple version of g_spawn_async() that parses a command line with + * g_shell_parse_argv() and passes it to g_spawn_async(). Runs a + * command line in the background. Unlike g_spawn_async(), the + * %G_SPAWN_SEARCH_PATH flag is enabled, other flags are not. Note + * that %G_SPAWN_SEARCH_PATH can have security implications, so + * consider using g_spawn_async() directly if appropriate. Possible + * errors are those from g_shell_parse_argv() and g_spawn_async(). + * + * Return value: TRUE on success, FALSE if error is set. + **/ +gboolean +g_spawn_command_line_async (const gchar *command_line, + GError **error) +{ + gboolean retval; + gchar **argv = 0; + + g_return_val_if_fail (command_line != NULL, FALSE); + + if (!g_shell_parse_argv (command_line, + NULL, &argv, + error)) + return FALSE; + + retval = g_spawn_async (NULL, + argv, + NULL, + G_SPAWN_SEARCH_PATH, + NULL, + NULL, + NULL, + error); + g_strfreev (argv); + + return retval; +} + +static gint +exec_err_to_g_error (gint en) +{ + switch (en) + { +#ifdef EACCES + case EACCES: + return G_SPAWN_ERROR_ACCES; + break; +#endif + +#ifdef EPERM + case EPERM: + return G_SPAWN_ERROR_PERM; + break; +#endif + +#ifdef E2BIG + case E2BIG: + return G_SPAWN_ERROR_2BIG; + break; +#endif + +#ifdef ENOEXEC + case ENOEXEC: + return G_SPAWN_ERROR_NOEXEC; + break; +#endif + +#ifdef ENAMETOOLONG + case ENAMETOOLONG: + return G_SPAWN_ERROR_NAMETOOLONG; + break; +#endif + +#ifdef ENOENT + case ENOENT: + return G_SPAWN_ERROR_NOENT; + break; +#endif + +#ifdef ENOMEM + case ENOMEM: + return G_SPAWN_ERROR_NOMEM; + break; +#endif + +#ifdef ENOTDIR + case ENOTDIR: + return G_SPAWN_ERROR_NOTDIR; + break; +#endif + +#ifdef ELOOP + case ELOOP: + return G_SPAWN_ERROR_LOOP; + break; +#endif + +#ifdef ETXTBUSY + case ETXTBUSY: + return G_SPAWN_ERROR_TXTBUSY; + break; +#endif + +#ifdef EIO + case EIO: + return G_SPAWN_ERROR_IO; + break; +#endif + +#ifdef ENFILE + case ENFILE: + return G_SPAWN_ERROR_NFILE; + break; +#endif + +#ifdef EMFILE + case EMFILE: + return G_SPAWN_ERROR_MFILE; + break; +#endif + +#ifdef EINVAL + case EINVAL: + return G_SPAWN_ERROR_INVAL; + break; +#endif + +#ifdef EISDIR + case EISDIR: + return G_SPAWN_ERROR_ISDIR; + break; +#endif + +#ifdef ELIBBAD + case ELIBBAD: + return G_SPAWN_ERROR_LIBBAD; + break; +#endif + + default: + return G_SPAWN_ERROR_FAILED; + break; + } +} + +static void +write_err_and_exit (gint fd, gint msg) +{ + gint en = errno; + + write (fd, &msg, sizeof(msg)); + write (fd, &en, sizeof(en)); + + _exit (1); +} + +static void +set_cloexec (gint fd) +{ + fcntl (fd, F_SETFD, FD_CLOEXEC); +} + +static gint +sane_dup2 (gint fd1, gint fd2) +{ + gint ret; + + retry: + ret = dup2 (fd1, fd2); + if (ret < 0 && errno == EINTR) + goto retry; + + return ret; +} + +enum +{ + CHILD_CHDIR_FAILED, + CHILD_EXEC_FAILED, + CHILD_DUP2_FAILED, + CHILD_FORK_FAILED +}; + +static void +do_exec (gint child_err_report_fd, + gint stdin_fd, + gint stdout_fd, + gint stderr_fd, + const gchar *working_directory, + gchar **argv, + gchar **envp, + gboolean close_descriptors, + gboolean search_path, + gboolean stdout_to_null, + gboolean stderr_to_null, + gboolean child_inherits_stdin, + GSpawnChildSetupFunc child_setup, + gpointer user_data) +{ + if (working_directory && chdir (working_directory) < 0) + write_err_and_exit (child_err_report_fd, + CHILD_CHDIR_FAILED); + + /* Close all file descriptors but stdin stdout and stderr as + * soon as we exec. Note that this includes + * child_err_report_fd, which keeps the parent from blocking + * forever on the other end of that pipe. + */ + if (close_descriptors) + { + gint open_max; + gint i; + + open_max = sysconf (_SC_OPEN_MAX); + for (i = 3; i < open_max; i++) + set_cloexec (i); + } + else + { + /* We need to do child_err_report_fd anyway */ + set_cloexec (child_err_report_fd); + } + + /* Redirect pipes as required */ + + if (stdin_fd >= 0) + { + /* dup2 can't actually fail here I don't think */ + + if (sane_dup2 (stdin_fd, 0) < 0) + write_err_and_exit (child_err_report_fd, + CHILD_DUP2_FAILED); + + /* ignore this if it doesn't work */ + close_and_invalidate (&stdin_fd); + } + else if (!child_inherits_stdin) + { + /* Keep process from blocking on a read of stdin */ + gint read_null = open ("/dev/null", O_RDONLY); + sane_dup2 (read_null, 0); + close_and_invalidate (&read_null); + } + + if (stdout_fd >= 0) + { + /* dup2 can't actually fail here I don't think */ + + if (sane_dup2 (stdout_fd, 1) < 0) + write_err_and_exit (child_err_report_fd, + CHILD_DUP2_FAILED); + + /* ignore this if it doesn't work */ + close_and_invalidate (&stdout_fd); + } + else if (stdout_to_null) + { + gint write_null = open ("/dev/null", O_WRONLY); + sane_dup2 (write_null, 1); + close_and_invalidate (&write_null); + } + + if (stderr_fd >= 0) + { + /* dup2 can't actually fail here I don't think */ + + if (sane_dup2 (stderr_fd, 2) < 0) + write_err_and_exit (child_err_report_fd, + CHILD_DUP2_FAILED); + + /* ignore this if it doesn't work */ + close_and_invalidate (&stderr_fd); + } + else if (stderr_to_null) + { + gint write_null = open ("/dev/null", O_WRONLY); + sane_dup2 (write_null, 2); + close_and_invalidate (&write_null); + } + + /* Call user function just before we exec */ + if (child_setup) + { + (* child_setup) (user_data); + } + + g_execute (argv[0], argv, envp, search_path); + + /* Exec failed */ + write_err_and_exit (child_err_report_fd, + CHILD_EXEC_FAILED); +} + +static gboolean +read_ints (int fd, + gint* buf, + gint n_ints_in_buf, + gint *n_ints_read, + GError **error) +{ + gint bytes = 0; + + while (TRUE) + { + gint chunk; + + if (bytes >= sizeof(gint)*2) + break; /* give up, who knows what happened, should not be + * possible. + */ + + again: + chunk = read (fd, + ((gchar*)buf) + bytes, + sizeof(gint)*n_ints_in_buf - bytes); + if (chunk < 0 && errno == EINTR) + goto again; + + if (chunk < 0) + { + /* Some weird shit happened, bail out */ + + g_set_error (error, + G_SPAWN_ERROR, + G_SPAWN_ERROR_FAILED, + _("Failed to read from child pipe (%s)"), + g_strerror (errno)); + + return FALSE; + } + else if (chunk == 0) + break; /* EOF */ + else + { + g_assert (chunk > 0); + + bytes += chunk; + } + } + + *n_ints_read = bytes/4; + + return TRUE; +} + +static gboolean +fork_exec_with_pipes (gboolean intermediate_child, + const gchar *working_directory, + gchar **argv, + gchar **envp, + gboolean close_descriptors, + gboolean search_path, + gboolean stdout_to_null, + gboolean stderr_to_null, + gboolean child_inherits_stdin, + GSpawnChildSetupFunc child_setup, + gpointer user_data, + gint *child_pid, + gint *standard_input, + gint *standard_output, + gint *standard_error, + GError **error) +{ + gint pid; + gint stdin_pipe[2] = { -1, -1 }; + gint stdout_pipe[2] = { -1, -1 }; + gint stderr_pipe[2] = { -1, -1 }; + gint child_err_report_pipe[2] = { -1, -1 }; + gint child_pid_report_pipe[2] = { -1, -1 }; + gint status; + + if (!make_pipe (child_err_report_pipe, error)) + return FALSE; + + if (intermediate_child && !make_pipe (child_pid_report_pipe, error)) + goto cleanup_and_fail; + + if (standard_input && !make_pipe (stdin_pipe, error)) + goto cleanup_and_fail; + + if (standard_output && !make_pipe (stdout_pipe, error)) + goto cleanup_and_fail; + + if (standard_error && !make_pipe (stderr_pipe, error)) + goto cleanup_and_fail; + + pid = fork (); + + if (pid < 0) + { + g_set_error (error, + G_SPAWN_ERROR, + G_SPAWN_ERROR_FORK, + _("Failed to fork (%s)"), + g_strerror (errno)); + + goto cleanup_and_fail; + } + else if (pid == 0) + { + /* Immediate child. This may or may not be the child that + * actually execs the new process. + */ + + /* Be sure we crash if the parent exits + * and we write to the err_report_pipe + */ + signal (SIGPIPE, SIG_DFL); + + /* Close the parent's end of the pipes; + * not needed in the close_descriptors case, + * though + */ + close_and_invalidate (&child_err_report_pipe[0]); + close_and_invalidate (&child_pid_report_pipe[0]); + close_and_invalidate (&stdin_pipe[1]); + close_and_invalidate (&stdout_pipe[0]); + close_and_invalidate (&stderr_pipe[0]); + + if (intermediate_child) + { + /* We need to fork an intermediate child that launches the + * final child. The purpose of the intermediate child + * is to exit, so we can waitpid() it immediately. + * Then the grandchild will not become a zombie. + */ + gint grandchild_pid; + + grandchild_pid = fork (); + + if (grandchild_pid < 0) + { + /* report -1 as child PID */ + write (child_pid_report_pipe[1], &grandchild_pid, + sizeof(grandchild_pid)); + + write_err_and_exit (child_err_report_pipe[1], + CHILD_FORK_FAILED); + } + else if (grandchild_pid == 0) + { + do_exec (child_err_report_pipe[1], + stdin_pipe[0], + stdout_pipe[1], + stderr_pipe[1], + working_directory, + argv, + envp, + close_descriptors, + search_path, + stdout_to_null, + stderr_to_null, + child_inherits_stdin, + child_setup, + user_data); + } + else + { + write (child_pid_report_pipe[1], &grandchild_pid, sizeof(grandchild_pid)); + close_and_invalidate (&child_pid_report_pipe[1]); + + _exit (0); + } + } + else + { + /* Just run the child. + */ + + do_exec (child_err_report_pipe[1], + stdin_pipe[0], + stdout_pipe[1], + stderr_pipe[1], + working_directory, + argv, + envp, + close_descriptors, + search_path, + stdout_to_null, + stderr_to_null, + child_inherits_stdin, + child_setup, + user_data); + } + } + else + { + /* Parent */ + + gint buf[2]; + gint n_ints = 0; + + /* Close the uncared-about ends of the pipes */ + close_and_invalidate (&child_err_report_pipe[1]); + close_and_invalidate (&child_pid_report_pipe[1]); + close_and_invalidate (&stdin_pipe[0]); + close_and_invalidate (&stdout_pipe[1]); + close_and_invalidate (&stderr_pipe[1]); + + /* If we had an intermediate child, reap it */ + if (intermediate_child) + { + wait_again: + if (waitpid (pid, &status, 0) < 0) + { + if (errno == EINTR) + goto wait_again; + else if (errno == ECHILD) + ; /* do nothing, child already reaped */ + else + g_warning ("waitpid() should not fail in %s", __FUNCTION__); + } + } + + + if (!read_ints (child_err_report_pipe[0], + buf, 2, &n_ints, + error)) + goto cleanup_and_fail; + + if (n_ints >= 2) + { + /* Error from the child. */ + + switch (buf[0]) + { + case CHILD_CHDIR_FAILED: + g_set_error (error, + G_SPAWN_ERROR, + G_SPAWN_ERROR_CHDIR, + _("Failed to change to directory '%s' (%s)"), + working_directory, + g_strerror (buf[1])); + + break; + + case CHILD_EXEC_FAILED: + g_set_error (error, + G_SPAWN_ERROR, + exec_err_to_g_error (buf[1]), + _("Failed to execute child process (%s)"), + g_strerror (buf[1])); + + break; + + case CHILD_DUP2_FAILED: + g_set_error (error, + G_SPAWN_ERROR, + G_SPAWN_ERROR_FAILED, + _("Failed to redirect output or input of child process (%s)"), + g_strerror (buf[1])); + + break; + + case CHILD_FORK_FAILED: + g_set_error (error, + G_SPAWN_ERROR, + G_SPAWN_ERROR_FORK, + _("Failed to fork child process (%s)"), + g_strerror (buf[1])); + break; + + default: + g_set_error (error, + G_SPAWN_ERROR, + G_SPAWN_ERROR_FAILED, + _("Unknown error executing child process")); + break; + } + + goto cleanup_and_fail; + } + + /* Get child pid from intermediate child pipe. */ + if (intermediate_child) + { + n_ints = 0; + + if (!read_ints (child_pid_report_pipe[0], + buf, 1, &n_ints, error)) + goto cleanup_and_fail; + + if (n_ints < 1) + { + g_set_error (error, + G_SPAWN_ERROR, + G_SPAWN_ERROR_FAILED, + _("Failed to read enough data from child pid pipe (%s)"), + g_strerror (errno)); + goto cleanup_and_fail; + } + else + { + /* we have the child pid */ + pid = buf[0]; + } + } + + /* Success against all odds! return the information */ + + if (child_pid) + *child_pid = pid; + + if (standard_input) + *standard_input = stdin_pipe[1]; + if (standard_output) + *standard_output = stdout_pipe[0]; + if (standard_error) + *standard_error = stderr_pipe[0]; + + return TRUE; + } + + cleanup_and_fail: + close_and_invalidate (&child_err_report_pipe[0]); + close_and_invalidate (&child_err_report_pipe[1]); + close_and_invalidate (&child_pid_report_pipe[0]); + close_and_invalidate (&child_pid_report_pipe[1]); + close_and_invalidate (&stdin_pipe[0]); + close_and_invalidate (&stdin_pipe[1]); + close_and_invalidate (&stdout_pipe[0]); + close_and_invalidate (&stdout_pipe[1]); + close_and_invalidate (&stderr_pipe[0]); + close_and_invalidate (&stderr_pipe[1]); + + return FALSE; +} + +static gboolean +make_pipe (gint p[2], + GError **error) +{ + if (pipe (p) < 0) + { + g_set_error (error, + G_SPAWN_ERROR, + G_SPAWN_ERROR_FAILED, + _("Failed to create pipe for communicating with child process (%s)"), + g_strerror (errno)); + return FALSE; + } + else + return TRUE; +} + +/* Based on execvp from GNU C Library */ + +static void +script_execute (const gchar *file, + gchar **argv, + gchar **envp, + gboolean search_path) +{ + /* Count the arguments. */ + int argc = 0; + while (argv[argc]) + ++argc; + + /* Construct an argument list for the shell. */ + { + gchar **new_argv; + + new_argv = g_new0 (gchar*, argc + 1); + + new_argv[0] = (char *) "/bin/sh"; + new_argv[1] = (char *) file; + while (argc > 1) + { + new_argv[argc] = argv[argc - 1]; + --argc; + } + + /* Execute the shell. */ + if (envp) + execve (new_argv[0], new_argv, envp); + else + execv (new_argv[0], new_argv); + + g_free (new_argv); + } +} + +static gchar* +my_strchrnul (const gchar *str, gchar c) +{ + gchar *p = (gchar*) str; + while (*p && (*p != c)) + ++p; + + return p; +} + +static gint +g_execute (const gchar *file, + gchar **argv, + gchar **envp, + gboolean search_path) +{ + if (*file == '\0') + { + /* We check the simple case first. */ + errno = ENOENT; + return -1; + } + + if (!search_path || strchr (file, '/') != NULL) + { + /* Don't search when it contains a slash. */ + if (envp) + execve (file, argv, envp); + else + execv (file, argv); + + if (errno == ENOEXEC) + script_execute (file, argv, envp, FALSE); + } + else + { + gboolean got_eacces = 0; + char *path, *p, *name, *freeme; + size_t len; + size_t pathlen; + + path = g_getenv ("PATH"); + if (path == NULL) + { + /* There is no `PATH' in the environment. The default + * search path in libc is the current directory followed by + * the path `confstr' returns for `_CS_PATH'. + */ + + /* In GLib we put . last, for security, and don't use the + * unportable confstr(); UNIX98 does not actually specify + * what to search if PATH is unset. POSIX may, dunno. + */ + + path = "/bin:/usr/bin:."; + } + + len = strlen (file) + 1; + pathlen = strlen (path); + freeme = name = g_malloc (pathlen + len + 1); + + /* Copy the file name at the top, including '\0' */ + memcpy (name + pathlen + 1, file, len); + name = name + pathlen; + /* And add the slash before the filename */ + *name = '/'; + + p = path; + do + { + char *startp; + + path = p; + p = my_strchrnul (path, ':'); + + if (p == path) + /* Two adjacent colons, or a colon at the beginning or the end + * of `PATH' means to search the current directory. + */ + startp = name + 1; + else + startp = memcpy (name - (p - path), path, p - path); + + /* Try to execute this name. If it works, execv will not return. */ + if (envp) + execve (startp, argv, envp); + else + execv (startp, argv); + + if (errno == ENOEXEC) + script_execute (startp, argv, envp, search_path); + + switch (errno) + { + case EACCES: + /* Record the we got a `Permission denied' error. If we end + * up finding no executable we can use, we want to diagnose + * that we did find one but were denied access. + */ + got_eacces = TRUE; + + /* FALL THRU */ + + case ENOENT: +#ifdef ESTALE + case ESTALE: +#endif +#ifdef ENOTDIR + case ENOTDIR: +#endif + /* Those errors indicate the file is missing or not executable + * by us, in which case we want to just try the next path + * directory. + */ + break; + + default: + /* Some other error means we found an executable file, but + * something went wrong executing it; return the error to our + * caller. + */ + g_free (freeme); + return -1; + } + } + while (*p++ != '\0'); + + /* We tried every element and none of them worked. */ + if (got_eacces) + /* At least one failure was due to permissions, so report that + * error. + */ + errno = EACCES; + + g_free (freeme); + } + + /* Return the error from the last attempt (probably ENOENT). */ + return -1; +} diff --git a/gspawn.h b/gspawn.h new file mode 100644 index 000000000..4ae0536b6 --- /dev/null +++ b/gspawn.h @@ -0,0 +1,132 @@ +/* gspawn.h - Process launching + * + * Copyright 2000 Red Hat, Inc. + * + * GLib is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * GLib is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with GLib; see the file COPYING.LIB. If not, write + * to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#ifndef __GSPAWN_H__ +#define __GSPAWN_H__ + +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + +/* I'm not sure I remember our proposed naming convention here. */ +#define G_SPAWN_ERROR g_spawn_error_quark () + +typedef enum +{ + G_SPAWN_ERROR_FORK, /* fork failed due to lack of memory */ + G_SPAWN_ERROR_READ, /* read or select on pipes failed */ + G_SPAWN_ERROR_CHDIR, /* changing to working dir failed */ + G_SPAWN_ERROR_ACCES, /* execv() returned EACCES */ + G_SPAWN_ERROR_PERM, /* execv() returned EPERM */ + G_SPAWN_ERROR_2BIG, /* execv() returned E2BIG */ + G_SPAWN_ERROR_NOEXEC, /* execv() returned ENOEXEC */ + G_SPAWN_ERROR_NAMETOOLONG, /* "" "" ENAMETOOLONG */ + G_SPAWN_ERROR_NOENT, /* "" "" ENOENT */ + G_SPAWN_ERROR_NOMEM, /* "" "" ENOMEM */ + G_SPAWN_ERROR_NOTDIR, /* "" "" ENOTDIR */ + G_SPAWN_ERROR_LOOP, /* "" "" ELOOP */ + G_SPAWN_ERROR_TXTBUSY, /* "" "" ETXTBUSY */ + G_SPAWN_ERROR_IO, /* "" "" EIO */ + G_SPAWN_ERROR_NFILE, /* "" "" ENFILE */ + G_SPAWN_ERROR_MFILE, /* "" "" EMFLE */ + G_SPAWN_ERROR_INVAL, /* "" "" EINVAL */ + G_SPAWN_ERROR_ISDIR, /* "" "" EISDIR */ + G_SPAWN_ERROR_LIBBAD, /* "" "" ELIBBAD */ + G_SPAWN_ERROR_FAILED /* other fatal failure, error->message + * should explain + */ +} GSpawnError; + +typedef void (* GSpawnChildSetupFunc) (gpointer user_data); + +typedef enum +{ + G_SPAWN_LEAVE_DESCRIPTORS_OPEN = 1 << 0, + G_SPAWN_DO_NOT_REAP_CHILD = 1 << 1, + /* look for argv[0] in the path i.e. use execvp() */ + G_SPAWN_SEARCH_PATH = 1 << 2, + /* Dump output to /dev/null */ + G_SPAWN_STDOUT_TO_DEV_NULL = 1 << 3, + G_SPAWN_STDERR_TO_DEV_NULL = 1 << 4, + G_SPAWN_CHILD_INHERITS_STDIN = 1 << 5 +} GSpawnFlags; + +GQuark g_spawn_error_quark (void); + +gboolean g_spawn_async (const gchar *working_directory, + gchar **argv, + gchar **envp, + GSpawnFlags flags, + GSpawnChildSetupFunc child_setup, + gpointer user_data, + gint *child_pid, + GError **error); + + +/* Opens pipes for non-NULL standard_output, standard_input, standard_error, + * and returns the parent's end of the pipes. + */ +gboolean g_spawn_async_with_pipes (const gchar *working_directory, + gchar **argv, + gchar **envp, + GSpawnFlags flags, + GSpawnChildSetupFunc child_setup, + gpointer user_data, + gint *child_pid, + gint *standard_input, + gint *standard_output, + gint *standard_error, + GError **error); + + +/* If standard_output or standard_error are non-NULL, the full + * standard output or error of the command will be placed there. + */ + +gboolean g_spawn_sync (const gchar *working_directory, + gchar **argv, + gchar **envp, + GSpawnFlags flags, + GSpawnChildSetupFunc child_setup, + gpointer user_data, + gchar **standard_output, + gchar **standard_error, + gint *exit_status, + GError **error); + +gboolean g_spawn_command_line_sync (const gchar *command_line, + gchar **standard_output, + gchar **standard_error, + gint *exit_status, + GError **error); +gboolean g_spawn_command_line_async (const gchar *command_line, + GError **error); + + +#ifdef __cplusplus +} +#endif + +#endif /* __GSPAWN_H__ */ + + diff --git a/guniprop.c b/guniprop.c index 3c2c29e6a..c45fc7ab8 100644 --- a/guniprop.c +++ b/guniprop.c @@ -118,9 +118,16 @@ g_unichar_ispunct (gunichar c) gboolean g_unichar_isspace (gunichar c) { - int t = TYPE (c); - return (t == G_UNICODE_SPACE_SEPARATOR || t == G_UNICODE_LINE_SEPARATOR - || t == G_UNICODE_PARAGRAPH_SEPARATOR); + /* special-case these since Unicode thinks they are not spaces */ + if (c == ' ' || c == '\t' || c == '\n' || c == '\r' || + c == '\f' || c == '\v') /* "the mythical vertical tab" */ + return TRUE; + else + { + int t = TYPE (c); + return (t == G_UNICODE_SPACE_SEPARATOR || t == G_UNICODE_LINE_SEPARATOR + || t == G_UNICODE_PARAGRAPH_SEPARATOR); + } } /** diff --git a/gutils.c b/gutils.c index c7284ca29..4910beb0c 100644 --- a/gutils.c +++ b/gutils.c @@ -143,6 +143,106 @@ g_atexit (GVoidFunc func) g_error ("Could not register atexit() function: %s", error); } +/* Based on execvp() from GNU Libc. + * Some of this code is cut-and-pasted into gspawn.c + */ + +static gchar* +my_strchrnul (const gchar *str, gchar c) +{ + gchar *p = (gchar*)str; + while (*p && (*p != c)) + ++p; + + return p; +} + +/** + * g_find_program_in_path: + * @file: a program name + * + * Locates the first executable named @file in the user's path, in the + * same way that execvp() would locate it. Returns an allocated string + * with the absolute path name, or NULL if the program is not found in + * the path. If @file is already an absolute path, returns a copy of + * @file if @file exists and is executable, and NULL otherwise. + * + * Return value: absolute path, or NULL + **/ +gchar* +g_find_program_in_path (const gchar *file) +{ + gchar *path, *p, *name, *freeme; + size_t len; + size_t pathlen; + + g_return_val_if_fail (file != NULL, NULL); + + if (*file == '/') + { + if (g_file_test (file, G_FILE_TEST_IS_EXECUTABLE)) + return g_strdup (file); + else + return NULL; + } + + path = g_getenv ("PATH"); + if (path == NULL) + { + /* There is no `PATH' in the environment. The default + * search path in libc is the current directory followed by + * the path `confstr' returns for `_CS_PATH'. + */ + + /* In GLib we put . last, for security, and don't use the + * unportable confstr(); UNIX98 does not actually specify + * what to search if PATH is unset. POSIX may, dunno. + */ + + path = "/bin:/usr/bin:."; + } + + len = strlen (file) + 1; + pathlen = strlen (path); + freeme = name = g_malloc (pathlen + len + 1); + + /* Copy the file name at the top, including '\0' */ + memcpy (name + pathlen + 1, file, len); + name = name + pathlen; + /* And add the slash before the filename */ + *name = '/'; + + p = path; + do + { + char *startp; + + path = p; + p = my_strchrnul (path, ':'); + + if (p == path) + /* Two adjacent colons, or a colon at the beginning or the end + * of `PATH' means to search the current directory. + */ + startp = name + 1; + else + startp = memcpy (name - (p - path), path, p - path); + + if (g_file_test (startp, G_FILE_TEST_IS_EXECUTABLE)) + { + gchar *ret; + ret = g_strdup (startp); + g_free (freeme); + return ret; + } + } + while (*p++ != '\0'); + + g_free (freeme); + + return NULL; +} + gint g_snprintf (gchar *str, gulong n, diff --git a/tests/Makefile.am b/tests/Makefile.am index 4e3d0fbf1..f2846e91b 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -1,6 +1,8 @@ INCLUDES = -I$(top_srcdir) @GLIB_DEBUG_FLAGS@ +EFENCE= + EXTRA_DIST = \ makefile.msc \ makefile.msc.in \ @@ -18,7 +20,9 @@ TESTS = \ queue-test \ rand-test \ relation-test \ + shell-test \ slist-test \ + spawn-test \ strfunc-test \ string-test \ thread-test \ @@ -28,7 +32,7 @@ TESTS = \ noinst_PROGRAMS = $(TESTS) -progs_LDADD = $(top_builddir)/libglib-1.3.la +progs_LDADD = $(EFENCE) $(top_builddir)/libglib-1.3.la $(EFENCE) thread_LDADD = $(progs_LDADD) $(top_builddir)/gthread/libgthread-1.3.la @G_THREAD_LIBS@ array_test_LDADD = $(progs_LDADD) @@ -41,7 +45,9 @@ node_test_LDADD = $(progs_LDADD) queue_test_LDADD = $(progs_LDADD) rand_test_LDADD = $(progs_LDADD) relation_test_LDADD = $(progs_LDADD) +shell_test_LDADD = $(progs_LDADD) slist_test_LDADD = $(progs_LDADD) +spawn_test_LDADD = $(progs_LDADD) strfunc_test_LDADD = $(progs_LDADD) string_test_LDADD = $(progs_LDADD) thread_test_LDADD = $(thread_LDADD) diff --git a/tests/shell-test.c b/tests/shell-test.c new file mode 100644 index 000000000..aa5c8c655 --- /dev/null +++ b/tests/shell-test.c @@ -0,0 +1,190 @@ +/* GLIB - Library of useful routines for C programming + * Copyright (C) 1995-1997 Peter Mattis, Spencer Kimball and Josh MacDonald + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +/* + * Modified by the GLib Team and others 1997-2000. See the AUTHORS + * file for a list of people on the GLib Team. See the ChangeLog + * files for a list of changes. These files are distributed with + * GLib at ftp://ftp.gtk.org/pub/gtk/. + */ + +#undef G_LOG_DOMAIN + +#include +#include +#include +#include + + +typedef struct _TestResult TestResult; + +struct _TestResult +{ + gint argc; + const gchar **argv; +}; + +static const gchar * +test_command_lines[] = +{ + /* 0 */ "foo bar", + /* 1 */ "foo 'bar'", + /* 2 */ "foo \"bar\"", + /* 3 */ "foo '' 'bar'", + /* 4 */ "foo \"bar\"'baz'blah'foo'\\''blah'\"boo\"", + /* 5 */ "foo \t \tblah\tfoo\t\tbar baz", + /* 6 */ "foo ' spaces more spaces lots of spaces in this ' \t", + /* 7 */ "foo \\\nbar", + /* 8 */ "foo '' ''", + /* 9 */ "foo \\\" la la la", + /* 10 */ "foo \\ foo woo woo\\ ", + /* 11 */ "foo \"yada yada \\$\\\"\"", + NULL +}; + +static const gchar *result0[] = { "foo", "bar", NULL }; +static const gchar *result1[] = { "foo", "bar", NULL }; +static const gchar *result2[] = { "foo", "bar", NULL }; +static const gchar *result3[] = { "foo", "", "bar", NULL }; +static const gchar *result4[] = { "foo", "barbazblahfoo'blahboo", NULL }; +static const gchar *result5[] = { "foo", "blah", "foo", "bar", "baz", NULL }; +static const gchar *result6[] = { "foo", " spaces more spaces lots of spaces in this ", NULL }; +static const gchar *result7[] = { "foo", "bar", NULL }; +static const gchar *result8[] = { "foo", "", "", NULL }; +static const gchar *result9[] = { "foo", "\"", "la", "la", "la", NULL }; +static const gchar *result10[] = { "foo", " foo", "woo", "woo ", NULL }; +static const gchar *result11[] = { "foo", "yada yada $\"", NULL }; + +static const TestResult +correct_results[] = +{ + { G_N_ELEMENTS (result0) - 1, result0 }, + { G_N_ELEMENTS (result1) - 1, result1 }, + { G_N_ELEMENTS (result2) - 1, result2 }, + { G_N_ELEMENTS (result3) - 1, result3 }, + { G_N_ELEMENTS (result4) - 1, result4 }, + { G_N_ELEMENTS (result5) - 1, result5 }, + { G_N_ELEMENTS (result6) - 1, result6 }, + { G_N_ELEMENTS (result7) - 1, result7 }, + { G_N_ELEMENTS (result8) - 1, result8 }, + { G_N_ELEMENTS (result9) - 1, result9 }, + { G_N_ELEMENTS (result10) - 1, result10 }, + { G_N_ELEMENTS (result11) - 1, result11 } +}; + +static void +print_test (const gchar *cmdline, gint argc, gchar **argv, + const TestResult *result) +{ + gint i; + + printf ("\nCommand line was: '%s'\n", cmdline); + + printf ("Expected result (%d args):\n", result->argc); + + i = 0; + while (result->argv[i]) + { + printf (" %3d '%s'\n", i, result->argv[i]); + + ++i; + } + + printf ("Actual result (%d args):\n", argc); + + i = 0; + while (argv[i]) + { + printf (" %3d '%s'\n", i, argv[i]); + + ++i; + } +} + +static void +do_argv_test (const gchar *cmdline, const TestResult *result) +{ + gint argc; + gchar **argv; + GError *err; + gint i; + + err = NULL; + if (!g_shell_parse_argv (cmdline, &argc, &argv, &err)) + { + fprintf (stderr, "Error parsing command line that should work fine: %s\n", + err->message); + + exit (1); + } + + if (argc != result->argc) + { + fprintf (stderr, "Expected and actual argc don't match\n"); + print_test (cmdline, argc, argv, result); + exit (1); + } + + i = 0; + while (argv[i]) + { + if (strcmp (argv[i], result->argv[i]) != 0) + { + fprintf (stderr, "Expected and actual arg %d do not match\n", i); + print_test (cmdline, argc, argv, result); + exit (1); + } + + ++i; + } + + if (argv[i] != NULL) + { + fprintf (stderr, "argv didn't get NULL-terminated\n"); + exit (1); + } +} + +static void +run_tests (void) +{ + GError *err; + gint i; + + i = 0; + while (test_command_lines[i]) + { + printf ("g_shell_parse_argv() test %d - ", i); + do_argv_test (test_command_lines[i], &correct_results[i]); + printf ("ok (%s)\n", test_command_lines[i]); + + ++i; + } +} + +int +main (int argc, + char *argv[]) +{ + run_tests (); + + return 0; +} + + diff --git a/tests/spawn-test.c b/tests/spawn-test.c new file mode 100644 index 000000000..b4091faff --- /dev/null +++ b/tests/spawn-test.c @@ -0,0 +1,96 @@ +/* GLIB - Library of useful routines for C programming + * Copyright (C) 1995-1997 Peter Mattis, Spencer Kimball and Josh MacDonald + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +/* + * Modified by the GLib Team and others 1997-2000. See the AUTHORS + * file for a list of people on the GLib Team. See the ChangeLog + * files for a list of changes. These files are distributed with + * GLib at ftp://ftp.gtk.org/pub/gtk/. + */ + +#undef G_LOG_DOMAIN + +#include +#include +#include +#include + + +static void +run_tests (void) +{ + GError *err; + gchar *output = NULL; + + printf ("The following errors are supposed to occur:\n"); + + err = NULL; + if (!g_spawn_command_line_sync ("nonexistent_application foo 'bar baz' blah blah", + NULL, NULL, NULL, + &err)) + { + fprintf (stderr, "Error (normal, supposed to happen): %s\n", err->message); + g_error_free (err); + } + + err = NULL; + if (!g_spawn_command_line_async ("nonexistent_application foo bar baz \"blah blah\"", + &err)) + { + fprintf (stderr, "Error (normal, supposed to happen): %s\n", err->message); + g_error_free (err); + } + + printf ("Errors after this are not supposed to happen:\n"); + + err = NULL; + if (!g_spawn_command_line_sync ("/bin/sh -c 'echo hello'", + &output, NULL, NULL, + &err)) + { + fprintf (stderr, "Error: %s\n", err->message); + g_error_free (err); + exit (1); + } + else + { + g_assert (output != NULL); + + if (strcmp (output, "hello\n") != 0) + { + printf ("output was '%s', should have been 'hello'\n", + output); + + exit (1); + } + + g_free (output); + } +} + +int +main (int argc, + char *argv[]) +{ + run_tests (); + + return 0; +} + +