/* commands.c : irssi Copyright (C) 1999-2000 Timo Sirainen 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 2 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, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "module.h" #include "signals.h" #include "commands.h" #include "misc.h" #include "special-vars.h" #include "window-item-def.h" #include "servers.h" #include "channels.h" #include "lib-config/iconfig.h" #include "settings.h" GSList *commands; char *current_command; static int signal_default_command; static GSList *alias_runstack; COMMAND_REC *command_find(const char *cmd) { GSList *tmp; g_return_val_if_fail(cmd != NULL, NULL); for (tmp = commands; tmp != NULL; tmp = tmp->next) { COMMAND_REC *rec = tmp->data; if (g_strcasecmp(rec->cmd, cmd) == 0) return rec; } return NULL; } static COMMAND_MODULE_REC *command_module_find(COMMAND_REC *rec, const char *module) { GSList *tmp; g_return_val_if_fail(rec != NULL, NULL); g_return_val_if_fail(module != NULL, NULL); for (tmp = rec->modules; tmp != NULL; tmp = tmp->next) { COMMAND_MODULE_REC *rec = tmp->data; if (g_strcasecmp(rec->name, module) == 0) return rec; } return NULL; } static COMMAND_MODULE_REC * command_module_find_and_remove(COMMAND_REC *rec, SIGNAL_FUNC func) { GSList *tmp, *tmp2; g_return_val_if_fail(rec != NULL, NULL); g_return_val_if_fail(func != NULL, NULL); for (tmp = rec->modules; tmp != NULL; tmp = tmp->next) { COMMAND_MODULE_REC *rec = tmp->data; for (tmp2 = rec->callbacks; tmp2 != NULL; tmp2 = tmp2->next) { COMMAND_CALLBACK_REC *cb = tmp2->data; if (cb->func == func) { rec->callbacks = g_slist_remove(rec->callbacks, cb); return rec; } } } return NULL; } int command_have_sub(const char *command) { GSList *tmp; int len; g_return_val_if_fail(command != NULL, FALSE); /* find "command "s */ len = strlen(command); for (tmp = commands; tmp != NULL; tmp = tmp->next) { COMMAND_REC *rec = tmp->data; if (g_strncasecmp(rec->cmd, command, len) == 0 && rec->cmd[len] == ' ') return TRUE; } return FALSE; } static COMMAND_MODULE_REC * command_module_get(COMMAND_REC *rec, const char *module, int protocol) { COMMAND_MODULE_REC *modrec; g_return_val_if_fail(rec != NULL, NULL); modrec = command_module_find(rec, module); if (modrec == NULL) { modrec = g_new0(COMMAND_MODULE_REC, 1); modrec->name = g_strdup(module); modrec->protocol = -1; rec->modules = g_slist_append(rec->modules, modrec); } if (protocol != -1) modrec->protocol = protocol; return modrec; } void command_bind_full(const char *module, int priority, const char *cmd, int protocol, const char *category, SIGNAL_FUNC func, void *user_data) { COMMAND_REC *rec; COMMAND_MODULE_REC *modrec; COMMAND_CALLBACK_REC *cb; char *str; g_return_if_fail(module != NULL); g_return_if_fail(cmd != NULL); rec = command_find(cmd); if (rec == NULL) { rec = g_new0(COMMAND_REC, 1); rec->cmd = g_strdup(cmd); rec->category = category == NULL ? NULL : g_strdup(category); commands = g_slist_append(commands, rec); } modrec = command_module_get(rec, module, protocol); cb = g_new0(COMMAND_CALLBACK_REC, 1); cb->func = func; cb->user_data = user_data; modrec->callbacks = g_slist_append(modrec->callbacks, cb); if (func != NULL) { str = g_strconcat("command ", cmd, NULL); signal_add_full(module, priority, str, func, user_data); g_free(str); } signal_emit("commandlist new", 1, rec); } static void command_free(COMMAND_REC *rec) { commands = g_slist_remove(commands, rec); signal_emit("commandlist remove", 1, rec); g_free_not_null(rec->category); g_strfreev(rec->options); g_free(rec->cmd); g_free(rec); } static void command_module_free(COMMAND_MODULE_REC *modrec, COMMAND_REC *rec) { rec->modules = g_slist_remove(rec->modules, modrec); g_slist_foreach(modrec->callbacks, (GFunc) g_free, NULL); g_slist_free(modrec->callbacks); g_free(modrec->name); g_free_not_null(modrec->options); g_free(modrec); } static void command_module_destroy(COMMAND_REC *rec, COMMAND_MODULE_REC *modrec) { GSList *tmp, *freelist; command_module_free(modrec, rec); /* command_set_options() might have added module declaration of it's own without any signals .. check if they're the only ones left and if so, destroy them. */ freelist = NULL; for (tmp = rec->modules; tmp != NULL; tmp = tmp->next) { COMMAND_MODULE_REC *rec = tmp->data; if (rec->callbacks == NULL) freelist = g_slist_append(freelist, rec); else { g_slist_free(freelist); freelist = NULL; break; } } g_slist_foreach(freelist, (GFunc) command_module_free, rec); g_slist_free(freelist); if (rec->modules == NULL) command_free(rec); } void command_unbind_full(const char *cmd, SIGNAL_FUNC func, void *user_data) { COMMAND_REC *rec; COMMAND_MODULE_REC *modrec; char *str; g_return_if_fail(cmd != NULL); g_return_if_fail(func != NULL); rec = command_find(cmd); if (rec != NULL) { modrec = command_module_find_and_remove(rec, func); g_return_if_fail(modrec != NULL); if (modrec->callbacks == NULL) command_module_destroy(rec, modrec); } str = g_strconcat("command ", cmd, NULL); signal_remove_data(str, func, user_data); g_free(str); } /* Expand `cmd' - returns `cmd' if not found, NULL if more than one match is found */ static const char *command_expand(char *cmd) { GSList *tmp; const char *match; int len, multiple; g_return_val_if_fail(cmd != NULL, NULL); multiple = FALSE; match = NULL; len = strlen(cmd); for (tmp = commands; tmp != NULL; tmp = tmp->next) { COMMAND_REC *rec = tmp->data; if (g_strncasecmp(rec->cmd, cmd, len) == 0 && strchr(rec->cmd+len, ' ') == NULL) { if (rec->cmd[len] == '\0') { /* full match */ return rec->cmd; } if (match != NULL) { /* multiple matches, we still need to check if there's some command left that is a full match.. */ multiple = TRUE; } /* check that this is the only match */ match = rec->cmd; } } if (multiple) { signal_emit("error command", 2, GINT_TO_POINTER(CMDERR_AMBIGUOUS), cmd); return NULL; } return match != NULL ? match : cmd; } void command_runsub(const char *cmd, const char *data, void *server, void *item) { const char *newcmd; char *orig, *subcmd, *defcmd, *args; g_return_if_fail(data != NULL); while (*data == ' ') data++; if (*data == '\0') { /* no subcommand given - list the subcommands */ signal_emit("list subcommands", 2, cmd); return; } /* get command.. */ orig = subcmd = g_strdup_printf("command %s %s", cmd, data); args = strchr(subcmd+8 + strlen(cmd)+1, ' '); if (args != NULL) *args++ = '\0'; else args = ""; while (*args == ' ') args++; /* check if this command can be expanded */ newcmd = command_expand(subcmd+8); if (newcmd == NULL) { /* ambiguous command */ g_free(orig); return; } subcmd = g_strconcat("command ", newcmd, NULL); g_strdown(subcmd); if (!signal_emit(subcmd, 3, args, server, item)) { defcmd = g_strdup_printf("default command %s", cmd); if (!signal_emit(defcmd, 3, data, server, item)) { signal_emit("error command", 2, GINT_TO_POINTER(CMDERR_UNKNOWN), subcmd+8); } g_free(defcmd); } g_free(subcmd); g_free(orig); } static GSList *optlist_find(GSList *optlist, const char *option) { while (optlist != NULL) { char *name = optlist->data; if (iscmdtype(*name)) name++; if (g_strcasecmp(name, option) == 0) return optlist; optlist = optlist->next; } return NULL; } int command_have_option(const char *cmd, const char *option) { COMMAND_REC *rec; char **tmp; g_return_val_if_fail(cmd != NULL, FALSE); g_return_val_if_fail(option != NULL, FALSE); rec = command_find(cmd); g_return_val_if_fail(rec != NULL, FALSE); if (rec->options == NULL) return FALSE; for (tmp = rec->options; *tmp != NULL; tmp++) { char *name = iscmdtype(**tmp) ? (*tmp)+1 : *tmp; if (g_strcasecmp(name, option) == 0) return TRUE; } return FALSE; } static void command_calc_options(COMMAND_REC *rec, const char *options) { char **optlist, **tmp, *name, *str; GSList *list, *oldopt; optlist = g_strsplit(options, " ", -1); if (rec->options == NULL) { /* first call - use specified args directly */ rec->options = optlist; return; } /* save old options to linked list */ list = NULL; for (tmp = rec->options; *tmp != NULL; tmp++) list = g_slist_append(list, g_strdup(*tmp)); g_strfreev(rec->options); /* merge the options */ for (tmp = optlist; *tmp != NULL; tmp++) { name = iscmdtype(**tmp) ? (*tmp)+1 : *tmp; oldopt = optlist_find(list, name); if (oldopt != NULL) { /* already specified - overwrite old defination */ g_free(oldopt->data); oldopt->data = g_strdup(*tmp); } else { /* new option, append to list */ list = g_slist_append(list, g_strdup(*tmp)); } } g_strfreev(optlist); /* linked list -> string[] */ str = gslist_to_string(list, " "); rec->options = g_strsplit(str, " ", -1); g_free(str); g_slist_foreach(list, (GFunc) g_free, NULL); g_slist_free(list); } /* recalculate options to command from options in all modules */ static void command_update_options(COMMAND_REC *rec) { GSList *tmp; g_strfreev(rec->options); rec->options = NULL; for (tmp = rec->modules; tmp != NULL; tmp = tmp->next) { COMMAND_MODULE_REC *modrec = tmp->data; if (modrec->options != NULL) command_calc_options(rec, modrec->options); } } void command_set_options_module(const char *module, const char *cmd, const char *options) { COMMAND_REC *rec; COMMAND_MODULE_REC *modrec; int reload; g_return_if_fail(module != NULL); g_return_if_fail(cmd != NULL); g_return_if_fail(options != NULL); rec = command_find(cmd); g_return_if_fail(rec != NULL); modrec = command_module_get(rec, module, -1); reload = modrec->options != NULL; if (reload) { /* options already set for the module .. we need to recalculate everything */ g_free(modrec->options); } modrec->options = g_strdup(options); if (reload) command_update_options(rec); else command_calc_options(rec, options); } char *cmd_get_param(char **data) { char *pos; g_return_val_if_fail(data != NULL, NULL); g_return_val_if_fail(*data != NULL, NULL); while (**data == ' ') (*data)++; pos = *data; while (**data != '\0' && **data != ' ') (*data)++; if (**data == ' ') *(*data)++ = '\0'; return pos; } char *cmd_get_quoted_param(char **data) { char *pos, quote; g_return_val_if_fail(data != NULL, NULL); g_return_val_if_fail(*data != NULL, NULL); while (**data == ' ') (*data)++; if (**data != '\'' && **data != '"') return cmd_get_param(data); quote = **data; (*data)++; pos = *data; while (**data != '\0' && (**data != quote || ((*data)[1] != ' ' && (*data)[1] != '\0'))) { if (**data == '\\' && (*data)[1] != '\0') g_memmove(*data, (*data)+1, strlen(*data)); (*data)++; } if (**data == quote) { *(*data)++ = '\0'; if (**data == ' ') (*data)++; } return pos; } /* Find specified option from list of options - the `option' might be shortened version of the full command. Returns index where the option was found, -1 if not found or -2 if there was multiple matches. */ static int option_find(char **array, const char *option) { char **tmp; int index, found, len, multiple; g_return_val_if_fail(array != NULL, -1); g_return_val_if_fail(option != NULL, -1); len = strlen(option); found = -1; index = 0; multiple = FALSE; for (tmp = array; *tmp != NULL; tmp++, index++) { const char *text = *tmp + iscmdtype(**tmp); if (g_strncasecmp(text, option, len) == 0) { if (text[len] == '\0') { /* full match */ return index; } if (found != -1) { /* multiple matches - we still need to check if there's a full match left.. */ multiple = TRUE; } /* partial match, check that it's the only one */ found = index; } } if (multiple) return -2; return found; } static int get_cmd_options(char **data, int ignore_unknown, const char *cmd, GHashTable *options) { COMMAND_REC *rec; char *option, *arg, **optlist; int pos; /* get option definations */ rec = cmd == NULL ? NULL : command_find(cmd); optlist = rec == NULL ? NULL : rec->options; option = NULL; pos = -1; for (;;) { if (**data == '-') { if (option != NULL && *optlist[pos] == '+') { /* required argument missing! */ *data = optlist[pos] + 1; return CMDERR_OPTION_ARG_MISSING; } (*data)++; if (**data == '-' && i_isspace((*data)[1])) { /* -- option means end of options even if next word starts with - */ (*data)++; while (i_isspace(**data)) (*data)++; break; } if (**data == '\0') option = "-"; else if (!i_isspace(**data)) option = cmd_get_param(data); else { option = "-"; (*data)++; } /* check if this option can have argument */ pos = optlist == NULL ? -1 : option_find(optlist, option); if (pos == -1 && optlist != NULL && is_numeric(option, '\0')) { /* check if we want - option */ pos = option_find(optlist, "#"); if (pos != -1) { g_hash_table_insert(options, "#", option); pos = -3; } } if (pos == -1 && !ignore_unknown) { /* unknown option! */ *data = option; return CMDERR_OPTION_UNKNOWN; } if (pos == -2 && !ignore_unknown) { /* multiple matches */ *data = option; return CMDERR_OPTION_AMBIGUOUS; } if (pos >= 0) { /* if we used a shortcut of parameter, put the whole parameter name in options table */ option = optlist[pos] + iscmdtype(*optlist[pos]); } if (options != NULL && pos != -3) g_hash_table_insert(options, option, ""); if (pos < 0 || !iscmdtype(*optlist[pos]) || *optlist[pos] == '!') option = NULL; while (i_isspace(**data)) (*data)++; continue; } if (option == NULL) break; if (*optlist[pos] == '@' && !i_isdigit(**data)) break; /* expected a numeric argument */ /* save the argument */ arg = cmd_get_quoted_param(data); if (options != NULL) { g_hash_table_remove(options, option); g_hash_table_insert(options, option, arg); } option = NULL; while (i_isspace(**data)) (*data)++; } return 0; } typedef struct { char *data; GHashTable *options; } CMD_TEMP_REC; static const char * get_optional_channel(WI_ITEM_REC *active_item, char **data, int require_name) { CHANNEL_REC *chanrec; const char *ret; char *tmp, *origtmp, *channel; if (active_item == NULL) { /* no active channel in window, channel required */ return cmd_get_param(data); } origtmp = tmp = g_strdup(*data); channel = cmd_get_param(&tmp); if (strcmp(channel, "*") == 0 && !require_name) { /* "*" means active channel */ cmd_get_param(data); ret = window_item_get_target(active_item); } else if (!server_ischannel(active_item->server, channel)) { /* we don't have channel parameter - use active channel */ ret = window_item_get_target(active_item); } else { /* Find the channel first and use it's name if found. This allows automatic !channel -> !XXXXXchannel replaces. */ channel = cmd_get_param(data); chanrec = channel_find(active_item->server, channel); ret = chanrec == NULL ? channel : chanrec->name; } g_free(origtmp); return ret; } int cmd_get_params(const char *data, gpointer *free_me, int count, ...) { WI_ITEM_REC *item; CMD_TEMP_REC *rec; GHashTable **opthash; char **str, *arg, *datad; va_list args; int cnt, error, ignore_unknown, require_name; g_return_val_if_fail(data != NULL, FALSE); va_start(args, count); rec = g_new0(CMD_TEMP_REC, 1); rec->data = g_strdup(data); *free_me = rec; datad = rec->data; error = FALSE; item = (count & PARAM_FLAG_OPTCHAN) == 0 ? NULL: (WI_ITEM_REC *) va_arg(args, WI_ITEM_REC *); if (count & PARAM_FLAG_OPTIONS) { arg = (char *) va_arg(args, char *); opthash = (GHashTable **) va_arg(args, GHashTable **); rec->options = *opthash = g_hash_table_new((GHashFunc) g_istr_hash, (GCompareFunc) g_istr_equal); ignore_unknown = count & PARAM_FLAG_UNKNOWN_OPTIONS; error = get_cmd_options(&datad, ignore_unknown, arg, *opthash); } if (!error) { /* and now handle the string */ cnt = PARAM_WITHOUT_FLAGS(count); if (count & PARAM_FLAG_OPTCHAN) { /* optional channel as first parameter */ require_name = (count & PARAM_FLAG_OPTCHAN_NAME) == PARAM_FLAG_OPTCHAN_NAME; arg = (char *) get_optional_channel(item, &datad, require_name); str = (char **) va_arg(args, char **); if (str != NULL) *str = arg; cnt--; } while (cnt-- > 0) { if (cnt == 0 && count & PARAM_FLAG_GETREST) { /* get rest */ arg = datad; } else { arg = (count & PARAM_FLAG_NOQUOTES) ? cmd_get_param(&datad) : cmd_get_quoted_param(&datad); } str = (char **) va_arg(args, char **); if (str != NULL) *str = arg; } } va_end(args); if (error) { signal_emit("error command", 2, GINT_TO_POINTER(error), datad); signal_stop(); cmd_params_free(rec); *free_me = NULL; } return !error; } void cmd_params_free(void *free_me) { CMD_TEMP_REC *rec = free_me; if (rec->options != NULL) g_hash_table_destroy(rec->options); g_free(rec->data); g_free(rec); } static void command_module_unbind_all(COMMAND_REC *rec, COMMAND_MODULE_REC *modrec) { GSList *tmp, *next; for (tmp = modrec->callbacks; tmp != NULL; tmp = next) { COMMAND_CALLBACK_REC *cb = tmp->data; next = tmp->next; command_unbind_full(rec->cmd, cb->func, cb->user_data); } if (g_slist_find(commands, rec) != NULL) { /* this module might have removed some options from command, update them. */ command_update_options(rec); } } void commands_remove_module(const char *module) { GSList *tmp, *next, *modlist; g_return_if_fail(module != NULL); for (tmp = commands; tmp != NULL; tmp = next) { COMMAND_REC *rec = tmp->data; next = tmp->next; modlist = gslist_find_string(rec->modules, module); if (modlist != NULL) command_module_unbind_all(rec, modlist->data); } } static int cmd_protocol_match(COMMAND_REC *cmd, SERVER_REC *server) { GSList *tmp; for (tmp = cmd->modules; tmp != NULL; tmp = tmp->next) { COMMAND_MODULE_REC *rec = tmp->data; if (rec->protocol == -1) { /* at least one module accepts the command without specific protocol */ return 1; } if (server != NULL && rec->protocol == server->chat_type) { /* matching protocol found */ return 1; } } return 0; } #define alias_runstack_push(alias) \ alias_runstack = g_slist_append(alias_runstack, alias) #define alias_runstack_pop(alias) \ alias_runstack = g_slist_remove(alias_runstack, alias) #define alias_runstack_find(alias) \ (gslist_find_icase_string(alias_runstack, alias) != NULL) static void parse_command(const char *command, int expand_aliases, SERVER_REC *server, void *item) { COMMAND_REC *rec; const char *alias, *newcmd; char *cmd, *orig, *args, *oldcmd; g_return_if_fail(command != NULL); cmd = orig = g_strconcat("command ", command, NULL); args = strchr(cmd+8, ' '); if (args != NULL) *args++ = '\0'; else args = ""; /* check if there's an alias for command. Don't allow recursive aliases */ alias = !expand_aliases || alias_runstack_find(cmd+8) ? NULL : alias_find(cmd+8); if (alias != NULL) { alias_runstack_push(cmd+8); eval_special_string(alias, args, server, item); alias_runstack_pop(cmd+8); g_free(orig); return; } /* check if this command can be expanded */ newcmd = command_expand(cmd+8); if (newcmd == NULL) { /* ambiguous command */ g_free(orig); return; } rec = command_find(newcmd); if (rec != NULL && !cmd_protocol_match(rec, server)) { g_free(orig); signal_emit("error command", 2, GINT_TO_POINTER(server == NULL ? CMDERR_NOT_CONNECTED : CMDERR_ILLEGAL_PROTO)); return; } cmd = g_strconcat("command ", newcmd, NULL); g_strdown(cmd); oldcmd = current_command; current_command = cmd+8; if (server != NULL) server_ref(server); if (!signal_emit(cmd, 3, args, server, item)) { signal_emit_id(signal_default_command, 3, command, server, item); } if (server != NULL) { if (server->connection_lost) server_disconnect(server); server_unref(server); } current_command = oldcmd; g_free(cmd); g_free(orig); } static void event_command(const char *line, SERVER_REC *server, void *item) { char *cmdchar; int expand_aliases = TRUE; g_return_if_fail(line != NULL); cmdchar = *line == '\0' ? NULL : strchr(settings_get_str("cmdchars"), *line); if (cmdchar != NULL && line[1] == ' ') { /* "/ text" = same as sending "text" to active channel. */ line += 2; cmdchar = NULL; } if (cmdchar == NULL) { /* non-command - let someone else handle this */ signal_emit("send text", 3, line, server, item); return; } /* same cmdchar twice ignores aliases ignores aliases */ line++; if (*line == *cmdchar) { line++; expand_aliases = FALSE; } /* ^command hides the output - we'll do this at fe-common but we have to skip the ^ char here.. */ if (*line == '^') line++; parse_command(line, expand_aliases, server, item); } /* SYNTAX: EVAL */ static void cmd_eval(const char *data, SERVER_REC *server, void *item) { g_return_if_fail(data != NULL); eval_special_string(data, "", server, item); } /* SYNTAX: CD */ static void cmd_cd(const char *data) { char *str; g_return_if_fail(data != NULL); if (*data == '\0') return; str = convert_home(data); chdir(str); g_free(str); } void commands_init(void) { commands = NULL; current_command = NULL; alias_runstack = NULL; signal_default_command = signal_get_uniq_id("default command"); settings_add_str("misc", "cmdchars", "/"); signal_add("send command", (SIGNAL_FUNC) event_command); command_bind("eval", NULL, (SIGNAL_FUNC) cmd_eval); command_bind("cd", NULL, (SIGNAL_FUNC) cmd_cd); } void commands_deinit(void) { g_free_not_null(current_command); signal_remove("send command", (SIGNAL_FUNC) event_command); command_unbind("eval", (SIGNAL_FUNC) cmd_eval); command_unbind("cd", (SIGNAL_FUNC) cmd_cd); }