/* * 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 */