/*
* Copyright (C) 2012-2015 Robin Haberkorn
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include
#include "sciteco.h"
#include "interface.h"
#include "undo.h"
#include "expressions.h"
#include "qregisters.h"
#include "ioview.h"
#include "ring.h"
#include "parser.h"
#include "error.h"
#include "spawn.h"
/*
* Debian 7 is still at libglib v2.33, so
* for the time being we support this UNIX-only
* implementation of g_spawn_check_exit_status()
* partially emulating libglib v2.34
*/
#ifndef G_SPAWN_EXIT_ERROR
#ifdef G_OS_UNIX
#warning "libglib v2.34 or later recommended."
#else
#error "libglib v2.34 or later required."
#endif
#include
#include
#define G_SPAWN_EXIT_ERROR \
g_quark_from_static_string("g-spawn-exit-error-quark")
static gboolean
g_spawn_check_exit_status(gint exit_status, GError **error)
{
if (!WIFEXITED(exit_status)) {
g_set_error(error, G_SPAWN_ERROR, G_SPAWN_ERROR_FAILED,
"Abnormal process termination (%d)",
exit_status);
return FALSE;
}
if (WEXITSTATUS(exit_status) != 0) {
g_set_error(error, G_SPAWN_EXIT_ERROR, WEXITSTATUS(exit_status),
"Unsuccessful exit status %d",
WEXITSTATUS(exit_status));
return FALSE;
}
return TRUE;
}
#endif
namespace SciTECO {
namespace States {
StateExecuteCommand executecommand;
StateEGCommand egcommand;
}
extern "C" {
static void child_watch_cb(GPid pid, gint status, gpointer data);
static gboolean stdin_watch_cb(GIOChannel *chan,
GIOCondition condition, gpointer data);
static gboolean stdout_watch_cb(GIOChannel *chan,
GIOCondition condition, gpointer data);
}
static QRegister *register_argument = NULL;
gchar **
parse_shell_command_line(const gchar *cmdline, GError **error)
{
gchar **argv;
#ifdef G_OS_WIN32
if (!(Flags::ed & Flags::ED_SHELLEMU)) {
const gchar *argv_win32[] = {
"cmd.exe", "/q", "/c", cmdline, NULL
};
return g_strdupv((gchar **)argv_win32);
}
#elif defined(G_OS_UNIX)
if (!(Flags::ed & Flags::ED_SHELLEMU)) {
const gchar *argv_unix[] = {
"/bin/sh", "-c", cmdline, NULL
};
return g_strdupv((gchar **)argv_unix);
}
#endif
if (!g_shell_parse_argv(cmdline, NULL, &argv, error))
return NULL;
return argv;
}
/*$
* EC[command]$ -- Execute operating system command and filter buffer contents
* linesEC[command]$
* -EC[command]$
* from,toEC[command]$
* :EC[command]$ -> Success|Failure
* lines:EC[command]$ -> Success|Failure
* -:EC[command]$ -> Success|Failure
* from,to:EC[command]$ -> Success|Failure
*
* The EC command allows you to interface with the operating
* system shell and external programs.
* The external program is spawned as a background process
* and its standard input stream is fed with data from the
* current document, i.e. text is piped into the external
* program.
* When automatic EOL translation is enabled, this will
* translate all end of line sequences according to the
* source document's EOL mode (see \fBEL\fP command).
* For instance when piping from a document with DOS
* line breaks, the receiving program will only be sent
* DOS line breaks.
* The process' standard output stream is also redirected
* and inserted into the current document.
* End of line sequences are normalized accordingly
* but the EOL mode guessed from the program's output is
* \fBnot\fP set on the document.
* The process' standard error stream is discarded.
* If data is piped into the external program, its output
* replaces that data in the buffer.
* Dot is always left at the end of the insertion.
*
* If invoked without parameters, no data is piped into
* the process (and no characters are removed) and its
* output is inserted at the current buffer position.
* This is equivalent to invoking \(lq.,.EC\(rq.
* If invoked with one parameter, the next or previous number
* of are piped from the buffer into the program and
* its output replaces these .
* This effectively runs as a filter over .
* \(lq-EC\(rq may be written as a short-cut for \(lq-1EC\(rq.
* When invoked with two parameters, the characters beginning
* at position up to the character at position
* are piped into the program and replaced with its output.
* This effectively runs as a filter over a buffer
* range.
*
* Errors are thrown not only for invalid buffer ranges
* but also for errors during process execution.
* If the external has an unsuccessful exit code,
* the EC command will also fail.
* If the EC command is colon-modified, it will instead return
* a TECO boolean signifying success or failure.
* In case of an unsuccessful exit code, a colon-modified EC
* will return the absolute value of the process exit
* code (which is also a TECO failure boolean) and 0 for all
* other failures.
* This feature may be used to take action depending on a
* specific process exit code.
*
* execution is by default platform-dependant.
* On Windows, is passed to the default command
* interpreter \(lqcmd.exe\(rq with the \(lq/c\(rq
* command-line argument.
* On Unix/Linux, is passed to the \(lq/bin/sh\(rq
* shell with the \(lq-c\(rq command-line argument.
* Therefore operating system restrictions on the maximum
* length of command-line arguments apply to and
* quoting of parameters within is somewhat platform
* dependant.
* On all other platforms, \*(ST will uniformly parse
* just as an UNIX98 \(lq/bin/sh\(rq would, but without
* performing any expansions.
* The program specified in is searched for in
* standard locations (according to the \(lqPATH\(rq environment
* variable).
* This mode of operation can also be enforced on Windows and
* Unix by enabling bit 7 in the ED flag, e.g. by executing
* \(lq0,128ED\(rq, and is recommended when writing cross-platform
* macros using the EC command.
*
* Note that when run interactively and subsequently rubbed
* out, \*(ST can easily undo all changes to the editor
* state.
* It \fBcannot\fP however undo any other side-effects that the
* execution of might have had on your system.
*
* Note also that the EC command blocks indefinitely until
* the completes, which may result in editor hangs.
* You may however interrupt the spawned process by sending
* the \fBSIGINT\fP signal to \*(ST, e.g. by pressing CTRL+C.
*
* In interactive mode, \*(ST performs TAB-completion
* of filenames in the string parameter but
* by doing so does not attempt any escaping of shell-relevant
* characters like whitespaces.
*/
StateExecuteCommand::StateExecuteCommand() : StateExpectString()
{
/*
* Context and loop can be reused between EC invocations.
* However we should not use the default context, since it
* may be used by GTK
*/
ctx.mainctx = g_main_context_new();
ctx.mainloop = g_main_loop_new(ctx.mainctx, FALSE);
}
StateExecuteCommand::~StateExecuteCommand()
{
g_main_loop_unref(ctx.mainloop);
g_main_context_unref(ctx.mainctx);
}
void
StateExecuteCommand::initial(void)
{
tecoBool rc = SUCCESS;
expressions.eval();
/*
* By evaluating arguments here, the command may fail
* before the string argument is typed
*/
switch (expressions.args()) {
case 0:
if (expressions.num_sign > 0) {
/* pipe nothing, insert at dot */
ctx.from = ctx.to = interface.ssm(SCI_GETCURRENTPOS);
break;
}
/* fall through if prefix sign is "-" */
case 1: {
/* pipe and replace line range */
sptr_t line;
ctx.from = interface.ssm(SCI_GETCURRENTPOS);
line = interface.ssm(SCI_LINEFROMPOSITION, ctx.from) +
expressions.pop_num_calc();
ctx.to = interface.ssm(SCI_POSITIONFROMLINE, line);
rc = TECO_BOOL(Validate::line(line));
if (ctx.to < ctx.from) {
tecoInt temp = ctx.from;
ctx.from = ctx.to;
ctx.to = temp;
}
break;
}
default:
/* pipe and replace character range */
ctx.to = expressions.pop_num_calc();
ctx.from = expressions.pop_num_calc();
rc = TECO_BOOL(ctx.from <= ctx.to &&
Validate::pos(ctx.from) &&
Validate::pos(ctx.to));
break;
}
if (IS_FAILURE(rc)) {
if (eval_colon()) {
expressions.push(rc);
ctx.from = ctx.to = -1;
/* done() will still be called */
} else {
throw RangeError("EC");
}
}
}
/*
* FIXME: `xclip -selection clipboard -in` hangs -- the
* stdout watcher is never activated!
* Workaround is to pipe to /dev/null
*/
State *
StateExecuteCommand::done(const gchar *str)
{
BEGIN_EXEC(&States::start);
if (ctx.from < 0)
/*
* initial() failed without throwing
* error (colon-modified)
*/
return &States::start;
gchar **argv;
static const gint flags = G_SPAWN_DO_NOT_REAP_CHILD |
G_SPAWN_SEARCH_PATH |
G_SPAWN_STDERR_TO_DEV_NULL;
GPid pid;
gint stdin_fd, stdout_fd;
GIOChannel *stdin_chan, *stdout_chan;
ctx.text_added = false;
/* opaque state for IOView::save() */
ctx.stdin_state = 0;
/* opaque state for IOView::channel_read_with_eol() */
ctx.stdout_state = 0;
/* eol style guessed from the stdout stream */
ctx.eol_style = -1;
ctx.error = NULL;
argv = parse_shell_command_line(str, &ctx.error);
if (!argv)
goto gerror;
g_spawn_async_with_pipes(NULL, argv, NULL, (GSpawnFlags)flags,
NULL, NULL, &pid,
&stdin_fd, &stdout_fd, NULL,
&ctx.error);
g_strfreev(argv);
if (ctx.error)
goto gerror;
ctx.child_src = g_child_watch_source_new(pid);
g_source_set_callback(ctx.child_src, (GSourceFunc)child_watch_cb,
&ctx, NULL);
g_source_attach(ctx.child_src, ctx.mainctx);
#ifdef G_OS_WIN32
stdin_chan = g_io_channel_win32_new_fd(stdin_fd);
stdout_chan = g_io_channel_win32_new_fd(stdout_fd);
#else /* the UNIX constructors should work everywhere else */
stdin_chan = g_io_channel_unix_new(stdin_fd);
stdout_chan = g_io_channel_unix_new(stdout_fd);
#endif
g_io_channel_set_flags(stdin_chan, G_IO_FLAG_NONBLOCK, NULL);
g_io_channel_set_encoding(stdin_chan, NULL, NULL);
g_io_channel_set_buffered(stdin_chan, FALSE);
g_io_channel_set_flags(stdout_chan, G_IO_FLAG_NONBLOCK, NULL);
g_io_channel_set_encoding(stdout_chan, NULL, NULL);
/*
* IOView::save() expects the channel to be buffered
* for performance reasons
*/
g_io_channel_set_buffered(stdout_chan, TRUE);
ctx.stdin_src = g_io_create_watch(stdin_chan,
(GIOCondition)(G_IO_OUT | G_IO_ERR | G_IO_HUP));
g_source_set_callback(ctx.stdin_src, (GSourceFunc)stdin_watch_cb,
&ctx, NULL);
g_source_attach(ctx.stdin_src, ctx.mainctx);
ctx.stdout_src = g_io_create_watch(stdout_chan,
(GIOCondition)(G_IO_IN | G_IO_ERR | G_IO_HUP));
g_source_set_callback(ctx.stdout_src, (GSourceFunc)stdout_watch_cb,
&ctx, NULL);
g_source_attach(ctx.stdout_src, ctx.mainctx);
if (!register_argument) {
if (current_doc_must_undo())
interface.undo_ssm(SCI_GOTOPOS, interface.ssm(SCI_GETCURRENTPOS));
interface.ssm(SCI_GOTOPOS, ctx.to);
}
interface.ssm(SCI_BEGINUNDOACTION);
ctx.start = ctx.from;
g_main_loop_run(ctx.mainloop);
if (!register_argument)
interface.ssm(SCI_DELETERANGE, ctx.from, ctx.to - ctx.from);
interface.ssm(SCI_ENDUNDOACTION);
if (register_argument) {
if (ctx.eol_style >= 0) {
register_argument->undo_set_eol_mode();
register_argument->set_eol_mode(ctx.eol_style);
}
} else if (ctx.from != ctx.to || ctx.text_added) {
/* undo action is only effective if it changed anything */
if (current_doc_must_undo())
interface.undo_ssm(SCI_UNDO);
interface.ssm(SCI_SCROLLCARET);
ring.dirtify();
}
if (!g_source_is_destroyed(ctx.stdin_src))
g_io_channel_shutdown(stdin_chan, TRUE, NULL);
g_io_channel_unref(stdin_chan);
g_source_unref(ctx.stdin_src);
g_io_channel_shutdown(stdout_chan, TRUE, NULL);
g_io_channel_unref(stdout_chan);
g_source_unref(ctx.stdout_src);
g_source_unref(ctx.child_src);
g_spawn_close_pid(pid);
if (ctx.error)
goto gerror;
if (interface.is_interrupted())
throw Error("Interrupted");
if (eval_colon())
expressions.push(SUCCESS);
undo.push_var(register_argument) = NULL;
return &States::start;
gerror:
if (!eval_colon())
throw GlibError(ctx.error);
/*
* If possible, encode process exit code
* in return boolean. It's guaranteed to be
* a failure since it's non-negative.
*/
if (ctx.error->domain == G_SPAWN_EXIT_ERROR)
expressions.push(ABS(ctx.error->code));
else
expressions.push(FAILURE);
undo.push_var(register_argument) = NULL;
g_error_free(ctx.error);
return &States::start;
}
/*$
* EGq[command]$ -- Set Q-Register to output of operating system command
* linesEGq[command]$
* -EGq[command]$
* from,toEGq[command]$
* :EGq[command]$ -> Success|Failure
* lines:EGq[command]$ -> Success|Failure
* -:EGq[command]$ -> Success|Failure
* from,to:EGq[command]$ -> Success|Failure
*
* Runs an operating system and set Q-Register
* to the data read from its standard output stream.
* Data may be fed to from the current buffer/document.
* The interpretation of the parameters and as well
* as the colon-modification is analoguous to the EC command.
*
* The EG command only differs from EC in not deleting any
* characters from the current buffer, not changing
* the current buffer position and writing process output
* to the Q-Register .
* In other words, the current buffer is not modified by EG.
* Also since EG replaces the string value of , the register's
* EOL mode is set to the mode guessed from the external program's
* output.
*
* The register is defined if it does not already exist.
*/
State *
StateEGCommand::got_register(QRegister ®)
{
BEGIN_EXEC(&States::executecommand);
undo.push_var(register_argument) = ®
return &States::executecommand;
}
/*
* Glib callbacks
*/
static void
child_watch_cb(GPid pid, gint status, gpointer data)
{
StateExecuteCommand::Context &ctx =
*(StateExecuteCommand::Context *)data;
/*
* Writing stdin or reading stdout might have already
* failed. We preserve the earliest GError.
*/
if (!ctx.error)
g_spawn_check_exit_status(status, &ctx.error);
if (g_source_is_destroyed(ctx.stdout_src))
g_main_loop_quit(ctx.mainloop);
}
static gboolean
stdin_watch_cb(GIOChannel *chan, GIOCondition condition, gpointer data)
{
StateExecuteCommand::Context &ctx =
*(StateExecuteCommand::Context *)data;
/* we always read from the current view */
IOView *view = (IOView *)interface.get_current_view();
gsize bytes_written;
/*
* IOView::save() cares about automatic EOL conversion
*/
switch (view->save(chan, ctx.from, ctx.to - ctx.start,
&bytes_written, ctx.stdin_state,
ctx.error ? NULL : &ctx.error)) {
case G_IO_STATUS_ERROR:
/* do not yet quit -- we still have to reap the child */
goto remove;
case G_IO_STATUS_NORMAL:
break;
case G_IO_STATUS_EOF:
/* process closed stdin preliminarily? */
goto remove;
case G_IO_STATUS_AGAIN:
return G_SOURCE_CONTINUE;
}
ctx.start += bytes_written;
if (ctx.start == ctx.to)
/* this will signal EOF to the process */
goto remove;
return G_SOURCE_CONTINUE;
remove:
/*
* Channel is always shut down here (fd is closed),
* so it's always shut down IF the GSource has been
* destroyed. It is not guaranteed to be destroyed
* during the main loop run however since it quits
* as soon as the child was reaped and stdout was read.
*/
g_io_channel_shutdown(chan, TRUE, NULL);
return G_SOURCE_REMOVE;
}
static gboolean
stdout_watch_cb(GIOChannel *chan, GIOCondition condition, gpointer data)
{
StateExecuteCommand::Context &ctx =
*(StateExecuteCommand::Context *)data;
GIOStatus status;
gchar buffer[1024];
gsize read_len = 0;
guint offset = 0;
gsize block_len = 0;
/* we're not really interested in that: */
gboolean eol_style_inconsistent = FALSE;
for (;;) {
status = IOView::channel_read_with_eol(
chan, buffer, sizeof(buffer),
read_len, offset, block_len,
ctx.stdout_state, ctx.eol_style,
eol_style_inconsistent,
ctx.error ? NULL : &ctx.error
);
switch (status) {
case G_IO_STATUS_NORMAL:
break;
case G_IO_STATUS_ERROR:
case G_IO_STATUS_EOF:
if (g_source_is_destroyed(ctx.child_src))
g_main_loop_quit(ctx.mainloop);
return G_SOURCE_REMOVE;
case G_IO_STATUS_AGAIN:
return G_SOURCE_CONTINUE;
}
if (!block_len)
continue;
if (register_argument) {
if (ctx.text_added) {
register_argument->undo_append_string();
register_argument->append_string(buffer+offset, block_len);
} else {
register_argument->undo_set_string();
register_argument->set_string(buffer+offset, block_len);
}
} else {
interface.ssm(SCI_ADDTEXT, block_len, (sptr_t)(buffer+offset));
}
ctx.text_added = true;
}
/* not reached */
return G_SOURCE_CONTINUE;
}
} /* namespace SciTECO */