X-Git-Url: https://git.sur5r.net/?a=blobdiff_plain;f=src%2Fconfig_parser.c;h=a9baef2710bc386f98fe4c0004e4649ff82db16d;hb=66a1fa7d4643981bcc8f78b03e170e35bf00bfc7;hp=80d436b1d44a522fd97d38b353fe682e91b09113;hpb=2bf7793d4d241efca25c452b5c8a5897b2a29da3;p=i3%2Fi3 diff --git a/src/config_parser.c b/src/config_parser.c index 80d436b1..a9baef27 100644 --- a/src/config_parser.c +++ b/src/config_parser.c @@ -4,7 +4,7 @@ * vim:ts=4:sw=4:expandtab * * i3 - an improved dynamic tiling window manager - * © 2009-2012 Michael Stapelberg and contributors (see also: LICENSE) + * © 2009 Michael Stapelberg and contributors (see also: LICENSE) * * config_parser.c: hand-written parser to parse configuration directives. * @@ -31,12 +31,21 @@ #include #include #include +#include +#include +#include +#include #include "all.h" // Macros to make the YAJL API a bit easier to use. -#define y(x, ...) yajl_gen_ ## x (command_output.json_gen, ##__VA_ARGS__) -#define ystr(str) yajl_gen_string(command_output.json_gen, (unsigned char*)str, strlen(str)) +#define y(x, ...) yajl_gen_##x(command_output.json_gen, ##__VA_ARGS__) +#define ystr(str) yajl_gen_string(command_output.json_gen, (unsigned char *)str, strlen(str)) + +#ifndef TEST_PARSER +pid_t config_error_nagbar_pid = -1; +static struct context *context; +#endif /******************************************************************************* * The data structures used for parsing. Essentially the current state and a @@ -137,7 +146,6 @@ static void push_long(const char *identifier, long num) { "in the code, or a new command which contains more than " "10 identified tokens.\n"); exit(1); - } static const char *get_string(const char *identifier) { @@ -150,7 +158,7 @@ static const char *get_string(const char *identifier) { return NULL; } -static const long get_long(const char *identifier) { +static long get_long(const char *identifier) { for (int c = 0; c < 10; c++) { if (stack[c].identifier == NULL) break; @@ -223,26 +231,25 @@ static void clear_criteria(void *unused_criteria) { static cmdp_state state; static Match current_match; -static struct ConfigResult subcommand_output; -static struct ConfigResult command_output; +static struct ConfigResultIR subcommand_output; +static struct ConfigResultIR command_output; /* A list which contains the states that lead to the current state, e.g. * INITIAL, WORKSPACE_LAYOUT. * When jumping back to INITIAL, statelist_idx will simply be set to 1 * (likewise for other states, e.g. MODE or BAR). * This list is used to process the nearest error token. */ -static cmdp_state statelist[10] = { INITIAL }; +static cmdp_state statelist[10] = {INITIAL}; /* NB: statelist_idx points to where the next entry will be inserted */ static int statelist_idx = 1; #include "GENERATED_config_call.h" - static void next_state(const cmdp_token *token) { cmdp_state _next_state = token->next_state; - //printf("token = name %s identifier %s\n", token->name, token->identifier); - //printf("next_state = %d\n", token->next_state); + //printf("token = name %s identifier %s\n", token->name, token->identifier); + //printf("next_state = %d\n", token->next_state); if (token->next_state == __CALL) { subcommand_output.json_gen = command_output.json_gen; GENERATED_call(token->extra.call_identifier, &subcommand_output); @@ -260,7 +267,7 @@ static void next_state(const cmdp_token *token) { for (int i = 0; i < statelist_idx; i++) { if (statelist[i] != _next_state) continue; - statelist_idx = i+1; + statelist_idx = i + 1; return; } @@ -295,7 +302,7 @@ static char *single_line(const char *start) { return result; } -struct ConfigResult *parse_config(const char *input, struct context *context) { +struct ConfigResultIR *parse_config(const char *input, struct context *context) { /* Dump the entire config file into the debug log. We cannot just use * DLOG("%s", input); because one log message must not exceed 4 KiB. */ const char *dumpwalk = input; @@ -314,12 +321,8 @@ struct ConfigResult *parse_config(const char *input, struct context *context) { state = INITIAL; statelist_idx = 1; -/* A YAJL JSON generator used for formatting replies. */ -#if YAJL_MAJOR >= 2 + /* A YAJL JSON generator used for formatting replies. */ command_output.json_gen = yajl_gen_alloc(NULL); -#else - command_output.json_gen = yajl_gen_alloc(NULL, NULL); -#endif y(array_open); @@ -330,20 +333,20 @@ struct ConfigResult *parse_config(const char *input, struct context *context) { bool token_handled; linecnt = 1; - // TODO: make this testable +// TODO: make this testable #ifndef TEST_PARSER cfg_criteria_init(¤t_match, &subcommand_output, INITIAL); #endif /* The "<=" operator is intentional: We also handle the terminating 0-byte * explicitly by looking for an 'end' token. */ - while ((walk - input) <= len) { + while ((size_t)(walk - input) <= len) { /* Skip whitespace before every token, newlines are relevant since they * separate configuration directives. */ while ((*walk == ' ' || *walk == '\t') && *walk != '\0') walk++; - //printf("remaining input: %s\n", walk); + //printf("remaining input: %s\n", walk); cmdp_token_ptr *ptr = &(tokens[state]); token_handled = false; @@ -393,7 +396,7 @@ struct ConfigResult *parse_config(const char *input, struct context *context) { if (*walk == '"') { beginning++; walk++; - while (*walk != '\0' && (*walk != '"' || *(walk-1) == '\\')) + while (*walk != '\0' && (*walk != '"' || *(walk - 1) == '\\')) walk++; } else { if (token->name[0] == 's') { @@ -405,22 +408,22 @@ struct ConfigResult *parse_config(const char *input, struct context *context) { * semicolon (;). */ while (*walk != ' ' && *walk != '\t' && *walk != ']' && *walk != ',' && - *walk != ';' && *walk != '\r' && + *walk != ';' && *walk != '\r' && *walk != '\n' && *walk != '\0') walk++; } } if (walk != beginning) { - char *str = scalloc(walk-beginning + 1); + char *str = scalloc(walk - beginning + 1); /* We copy manually to handle escaping of characters. */ int inpos, outpos; for (inpos = 0, outpos = 0; - inpos < (walk-beginning); + inpos < (walk - beginning); inpos++, outpos++) { /* We only handle escaped double quotes to not break * backwards compatibility with people using \w in * regular expressions etc. */ - if (beginning[inpos] == '\\' && beginning[inpos+1] == '"') + if (beginning[inpos] == '\\' && beginning[inpos + 1] == '"') inpos++; str[outpos] = beginning[inpos]; } @@ -437,24 +440,34 @@ struct ConfigResult *parse_config(const char *input, struct context *context) { } } + if (strcmp(token->name, "line") == 0) { + while (*walk != '\0' && *walk != '\n' && *walk != '\r') + walk++; + next_state(token); + token_handled = true; + linecnt++; + walk++; + break; + } + if (strcmp(token->name, "end") == 0) { //printf("checking for end: *%s*\n", walk); if (*walk == '\0' || *walk == '\n' || *walk == '\r') { next_state(token); token_handled = true; - /* To make sure we start with an appropriate matching +/* To make sure we start with an appropriate matching * datastructure for commands which do *not* specify any * criteria, we re-initialize the criteria system after * every command. */ - // TODO: make this testable +// TODO: make this testable #ifndef TEST_PARSER cfg_criteria_init(¤t_match, &subcommand_output, INITIAL); #endif linecnt++; walk++; break; - } - } + } + } } if (!token_handled) { @@ -501,7 +514,6 @@ struct ConfigResult *parse_config(const char *input, struct context *context) { possible_tokens); free(possible_tokens); - /* Go back to the beginning of the line */ const char *error_line = start_of_line(walk, input); @@ -521,10 +533,10 @@ struct ConfigResult *parse_config(const char *input, struct context *context) { /* Print context lines *before* the error, if any. */ if (linecnt > 1) { - const char *context_p1_start = start_of_line(error_line-2, input); + const char *context_p1_start = start_of_line(error_line - 2, input); char *context_p1_line = single_line(context_p1_start); if (linecnt > 2) { - const char *context_p2_start = start_of_line(context_p1_start-2, input); + const char *context_p2_start = start_of_line(context_p1_start - 2, input); char *context_p2_line = single_line(context_p2_start); ELOG("CONFIG: Line %3d: %s\n", linecnt - 2, context_p2_line); free(context_p2_line); @@ -566,7 +578,7 @@ struct ConfigResult *parse_config(const char *input, struct context *context) { y(map_close); /* Skip the rest of this line, but continue parsing. */ - while ((walk - input) <= len && *walk != '\n') + while ((size_t)(walk - input) <= len && *walk != '\n') walk++; free(position); @@ -577,7 +589,7 @@ struct ConfigResult *parse_config(const char *input, struct context *context) { * we find the nearest state which contains an token * and follow that one. */ bool error_token_found = false; - for (int i = statelist_idx-1; (i >= 0) && !error_token_found; i--) { + for (int i = statelist_idx - 1; (i >= 0) && !error_token_found; i--) { cmdp_token_ptr *errptr = &(tokens[statelist[i]]); for (int j = 0; j < errptr->n; j++) { if (strcmp(errptr->array[j].name, "error") != 0) @@ -649,4 +661,386 @@ int main(int argc, char *argv[]) { context.filename = ""; parse_config(argv[1], &context); } + +#else + +/* + * Goes through each line of buf (separated by \n) and checks for statements / + * commands which only occur in i3 v4 configuration files. If it finds any, it + * returns version 4, otherwise it returns version 3. + * + */ +static int detect_version(char *buf) { + char *walk = buf; + char *line = buf; + while (*walk != '\0') { + if (*walk != '\n') { + walk++; + continue; + } + + /* check for some v4-only statements */ + if (strncasecmp(line, "bindcode", strlen("bindcode")) == 0 || + strncasecmp(line, "force_focus_wrapping", strlen("force_focus_wrapping")) == 0 || + strncasecmp(line, "# i3 config file (v4)", strlen("# i3 config file (v4)")) == 0 || + strncasecmp(line, "workspace_layout", strlen("workspace_layout")) == 0) { + LOG("deciding for version 4 due to this line: %.*s\n", (int)(walk - line), line); + return 4; + } + + /* if this is a bind statement, we can check the command */ + if (strncasecmp(line, "bind", strlen("bind")) == 0) { + char *bind = strchr(line, ' '); + if (bind == NULL) + goto next; + while ((*bind == ' ' || *bind == '\t') && *bind != '\0') + bind++; + if (*bind == '\0') + goto next; + if ((bind = strchr(bind, ' ')) == NULL) + goto next; + while ((*bind == ' ' || *bind == '\t') && *bind != '\0') + bind++; + if (*bind == '\0') + goto next; + if (strncasecmp(bind, "layout", strlen("layout")) == 0 || + strncasecmp(bind, "floating", strlen("floating")) == 0 || + strncasecmp(bind, "workspace", strlen("workspace")) == 0 || + strncasecmp(bind, "focus left", strlen("focus left")) == 0 || + strncasecmp(bind, "focus right", strlen("focus right")) == 0 || + strncasecmp(bind, "focus up", strlen("focus up")) == 0 || + strncasecmp(bind, "focus down", strlen("focus down")) == 0 || + strncasecmp(bind, "border normal", strlen("border normal")) == 0 || + strncasecmp(bind, "border 1pixel", strlen("border 1pixel")) == 0 || + strncasecmp(bind, "border pixel", strlen("border pixel")) == 0 || + strncasecmp(bind, "border borderless", strlen("border borderless")) == 0 || + strncasecmp(bind, "--no-startup-id", strlen("--no-startup-id")) == 0 || + strncasecmp(bind, "bar", strlen("bar")) == 0) { + LOG("deciding for version 4 due to this line: %.*s\n", (int)(walk - line), line); + return 4; + } + } + + next: + /* advance to the next line */ + walk++; + line = walk; + } + + return 3; +} + +/* + * Calls i3-migrate-config-to-v4 to migrate a configuration file (input + * buffer). + * + * Returns the converted config file or NULL if there was an error (for + * example the script could not be found in $PATH or the i3 executable’s + * directory). + * + */ +static char *migrate_config(char *input, off_t size) { + int writepipe[2]; + int readpipe[2]; + + if (pipe(writepipe) != 0 || + pipe(readpipe) != 0) { + warn("migrate_config: Could not create pipes"); + return NULL; + } + + pid_t pid = fork(); + if (pid == -1) { + warn("Could not fork()"); + return NULL; + } + + /* child */ + if (pid == 0) { + /* close writing end of writepipe, connect reading side to stdin */ + close(writepipe[1]); + dup2(writepipe[0], 0); + + /* close reading end of readpipe, connect writing side to stdout */ + close(readpipe[0]); + dup2(readpipe[1], 1); + + static char *argv[] = { + NULL, /* will be replaced by the executable path */ + NULL}; + exec_i3_utility("i3-migrate-config-to-v4", argv); + } + + /* parent */ + + /* close reading end of the writepipe (connected to the script’s stdin) */ + close(writepipe[0]); + + /* write the whole config file to the pipe, the script will read everything + * immediately */ + if (writeall(writepipe[1], input, size) == -1) { + warn("Could not write to pipe"); + return NULL; + } + close(writepipe[1]); + + /* close writing end of the readpipe (connected to the script’s stdout) */ + close(readpipe[1]); + + /* read the script’s output */ + int conv_size = 65535; + char *converted = malloc(conv_size); + int read_bytes = 0, ret; + do { + if (read_bytes == conv_size) { + conv_size += 65535; + converted = realloc(converted, conv_size); + } + ret = read(readpipe[0], converted + read_bytes, conv_size - read_bytes); + if (ret == -1) { + warn("Cannot read from pipe"); + FREE(converted); + return NULL; + } + read_bytes += ret; + } while (ret > 0); + + /* get the returncode */ + int status; + wait(&status); + if (!WIFEXITED(status)) { + fprintf(stderr, "Child did not terminate normally, using old config file (will lead to broken behaviour)\n"); + return NULL; + } + + int returncode = WEXITSTATUS(status); + if (returncode != 0) { + fprintf(stderr, "Migration process exit code was != 0\n"); + if (returncode == 2) { + fprintf(stderr, "could not start the migration script\n"); + /* TODO: script was not found. tell the user to fix their system or create a v4 config */ + } else if (returncode == 1) { + fprintf(stderr, "This already was a v4 config. Please add the following line to your config file:\n"); + fprintf(stderr, "# i3 config file (v4)\n"); + /* TODO: nag the user with a message to include a hint for i3 in their config file */ + } + return NULL; + } + + return converted; +} + +/* + * Parses the given file by first replacing the variables, then calling + * parse_config and possibly launching i3-nagbar. + * + */ +bool parse_file(const char *f, bool use_nagbar) { + SLIST_HEAD(variables_head, Variable) variables = SLIST_HEAD_INITIALIZER(&variables); + int fd, ret, read_bytes = 0; + struct stat stbuf; + char *buf; + FILE *fstr; + char buffer[1026], key[512], value[512]; + + if ((fd = open(f, O_RDONLY)) == -1) + die("Could not open configuration file: %s\n", strerror(errno)); + + if (fstat(fd, &stbuf) == -1) + die("Could not fstat file: %s\n", strerror(errno)); + + buf = scalloc((stbuf.st_size + 1) * sizeof(char)); + while (read_bytes < stbuf.st_size) { + if ((ret = read(fd, buf + read_bytes, (stbuf.st_size - read_bytes))) < 0) + die("Could not read(): %s\n", strerror(errno)); + read_bytes += ret; + } + + if (lseek(fd, 0, SEEK_SET) == (off_t)-1) + die("Could not lseek: %s\n", strerror(errno)); + + if ((fstr = fdopen(fd, "r")) == NULL) + die("Could not fdopen: %s\n", strerror(errno)); + + while (!feof(fstr)) { + if (fgets(buffer, 1024, fstr) == NULL) { + if (feof(fstr)) + break; + die("Could not read configuration file\n"); + } + + /* sscanf implicitly strips whitespace. Also, we skip comments and empty lines. */ + if (sscanf(buffer, "%s %[^\n]", key, value) < 1 || + key[0] == '#' || strlen(key) < 3) + continue; + + if (strcasecmp(key, "set") == 0) { + if (value[0] != '$') { + ELOG("Malformed variable assignment, name has to start with $\n"); + continue; + } + + /* get key/value for this variable */ + char *v_key = value, *v_value; + if (strstr(value, " ") == NULL && strstr(value, "\t") == NULL) { + ELOG("Malformed variable assignment, need a value\n"); + continue; + } + + if (!(v_value = strstr(value, " "))) + v_value = strstr(value, "\t"); + + *(v_value++) = '\0'; + while (*v_value == '\t' || *v_value == ' ') + v_value++; + + struct Variable *new = scalloc(sizeof(struct Variable)); + new->key = sstrdup(v_key); + new->value = sstrdup(v_value); + SLIST_INSERT_HEAD(&variables, new, variables); + DLOG("Got new variable %s = %s\n", v_key, v_value); + continue; + } + } + fclose(fstr); + + /* For every custom variable, see how often it occurs in the file and + * how much extra bytes it requires when replaced. */ + struct Variable *current, *nearest; + int extra_bytes = 0; + /* We need to copy the buffer because we need to invalidate the + * variables (otherwise we will count them twice, which is bad when + * 'extra' is negative) */ + char *bufcopy = sstrdup(buf); + SLIST_FOREACH(current, &variables, variables) { + int extra = (strlen(current->value) - strlen(current->key)); + char *next; + for (next = bufcopy; + next < (bufcopy + stbuf.st_size) && + (next = strcasestr(next, current->key)) != NULL; + next += strlen(current->key)) { + *next = '_'; + extra_bytes += extra; + } + } + FREE(bufcopy); + + /* Then, allocate a new buffer and copy the file over to the new one, + * but replace occurences of our variables */ + char *walk = buf, *destwalk; + char *new = smalloc((stbuf.st_size + extra_bytes + 1) * sizeof(char)); + destwalk = new; + while (walk < (buf + stbuf.st_size)) { + /* Find the next variable */ + SLIST_FOREACH(current, &variables, variables) + current->next_match = strcasestr(walk, current->key); + nearest = NULL; + int distance = stbuf.st_size; + SLIST_FOREACH(current, &variables, variables) { + if (current->next_match == NULL) + continue; + if ((current->next_match - walk) < distance) { + distance = (current->next_match - walk); + nearest = current; + } + } + if (nearest == NULL) { + /* If there are no more variables, we just copy the rest */ + strncpy(destwalk, walk, (buf + stbuf.st_size) - walk); + destwalk += (buf + stbuf.st_size) - walk; + *destwalk = '\0'; + break; + } else { + /* Copy until the next variable, then copy its value */ + strncpy(destwalk, walk, distance); + strncpy(destwalk + distance, nearest->value, strlen(nearest->value)); + walk += distance + strlen(nearest->key); + destwalk += distance + strlen(nearest->value); + } + } + + /* analyze the string to find out whether this is an old config file (3.x) + * or a new config file (4.x). If it’s old, we run the converter script. */ + int version = detect_version(buf); + if (version == 3) { + /* We need to convert this v3 configuration */ + char *converted = migrate_config(new, stbuf.st_size); + if (converted != NULL) { + ELOG("\n"); + ELOG("****************************************************************\n"); + ELOG("NOTE: Automatically converted configuration file from v3 to v4.\n"); + ELOG("\n"); + ELOG("Please convert your config file to v4. You can use this command:\n"); + ELOG(" mv %s %s.O\n", f, f); + ELOG(" i3-migrate-config-to-v4 %s.O > %s\n", f, f); + ELOG("****************************************************************\n"); + ELOG("\n"); + free(new); + new = converted; + } else { + LOG("\n"); + LOG("**********************************************************************\n"); + LOG("ERROR: Could not convert config file. Maybe i3-migrate-config-to-v4\n"); + LOG("was not correctly installed on your system?\n"); + LOG("**********************************************************************\n"); + LOG("\n"); + } + } + + context = scalloc(sizeof(struct context)); + context->filename = f; + + struct ConfigResultIR *config_output = parse_config(new, context); + yajl_gen_free(config_output->json_gen); + + check_for_duplicate_bindings(context); + + if (use_nagbar && (context->has_errors || context->has_warnings)) { + ELOG("FYI: You are using i3 version %s\n", i3_version); + if (version == 3) + ELOG("Please convert your configfile first, then fix any remaining errors (see above).\n"); + + char *editaction, + *pageraction; + sasprintf(&editaction, "i3-sensible-editor \"%s\" && i3-msg reload\n", f); + sasprintf(&pageraction, "i3-sensible-pager \"%s\"\n", errorfilename); + char *argv[] = { + NULL, /* will be replaced by the executable path */ + "-f", + (config.font.pattern ? config.font.pattern : "fixed"), + "-t", + (context->has_errors ? "error" : "warning"), + "-m", + (context->has_errors ? "You have an error in your i3 config file!" : "Your config is outdated. Please fix the warnings to make sure everything works."), + "-b", + "edit config", + editaction, + (errorfilename ? "-b" : NULL), + (context->has_errors ? "show errors" : "show warnings"), + pageraction, + NULL}; + + start_nagbar(&config_error_nagbar_pid, argv); + free(editaction); + free(pageraction); + } + + bool has_errors = context->has_errors; + + FREE(context->line_copy); + free(context); + free(new); + free(buf); + + while (!SLIST_EMPTY(&variables)) { + current = SLIST_FIRST(&variables); + FREE(current->key); + FREE(current->value); + SLIST_REMOVE_HEAD(&variables, variables); + FREE(current); + } + + return !has_errors; +} + #endif