├── .gitignore ├── Makefile ├── README ├── arguments.c ├── requests.c ├── stackoverflow-cli.c ├── stackoverflow-cli.h └── stackoverflow.c /.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | stackoverflow-cli -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | UNAME := $(shell uname) 2 | 3 | ifeq ($(UNAME), Darwin) 4 | CFLAGS ?=-Wall -std=c99 -ggdb -I/opt/local/include 5 | CCLINK ?=-ljson -lcurl -L/opt/local/lib 6 | else 7 | CFLAGS ?=-Wall -std=c99 -ggdb 8 | CCLINK ?=-ljson -lcurl 9 | endif 10 | 11 | all: stackoverflow-cli 12 | 13 | OBJ = stackoverflow.o requests.o arguments.o stackoverflow-cli.o 14 | 15 | stackoverflow.o: stackoverflow.c stackoverflow-cli.h 16 | requests.o: requests.c stackoverflow-cli.h 17 | arguments.o: arguments.c stackoverflow-cli.h 18 | stackoverflow-cli.o: stackoverflow-cli.c stackoverflow-cli.h 19 | 20 | CCOPT=$(CFLAGS) 21 | 22 | %.o: %.c 23 | $(CC) $(CCOPT) -c $< -o $@ 24 | 25 | stackoverflow-cli: $(OBJ) 26 | $(CC) -o $@ $(CCLINK) $(OBJ) 27 | 28 | clean: 29 | rm -rf *.o stackoverflow-cli 30 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | *stackoverflow-cli* is a command line interface to the stackoverflow API designed to be run in text environments in combination with other GNU tools to extract useful information. 2 | 3 | To compile this thing you need libcurl and json-c libraries, downnload the code and run the Makefile. 4 | 5 | No configure script yet. 6 | 7 | Usage: ./stackoverflow-cli [options...] 8 | 9 | Operations has to be one of the following: 10 | 11 | Search: 12 | --search Searches the questions database. 13 | --intitle A string that must appear verbatim in the title. 14 | --tagged List of tags delimited by semicolons at least one has to be on the questions. 15 | --nottagged List of tags delimited by semicolons that must not be in the question. 16 | 17 | Users: 18 | --users Searches the users database. 19 | --id List of user ids delimited by a semicolon. 20 | --filter A string that must appear in the users name. 21 | 22 | Questions: 23 | --questions Searches the questions database with answers. 24 | --id List of question ids delimited by a semicolon. 25 | 26 | Generic parameters: 27 | 28 | --pagesize How many items per page? 100 maximum. 29 | --page What page do you want to see? 30 | 31 | -------------------------------------------------------------------------------- /arguments.c: -------------------------------------------------------------------------------- 1 | #include "stackoverflow-cli.h" 2 | 3 | #include 4 | 5 | void printUsage (const char *name, const char *msg) { 6 | if (msg != NULL) 7 | fprintf(stderr, "%s\n", msg); 8 | 9 | printf("Usage: %s [options...]\n\n", name); 10 | printf("Operations has to be one of the following:\n\n"); 11 | printf("Search:\n"); 12 | printf("\t--search\t\tSearches the questions database.\n"); 13 | printf("\t--intitle \tA string that must appear verbatim in the title.\n"); 14 | printf("\t--tagged \tList of tags delimited by semicolons at least one has to be on the questions.\n"); 15 | printf("\t--nottagged \tList of tags delimited by semicolons that must not be in the question.\n"); 16 | printf("\n"); 17 | printf("Users:\n"); 18 | printf("\t--users\t\t\tSearches the users database.\n"); 19 | printf("\t--id \t\tList of user ids delimited by a semicolon.\n"); 20 | printf("\t--filter \tA string that must appear in the users name.\n"); 21 | printf("\n"); 22 | printf("Questions:\n"); 23 | printf("\t--questions\t\tSearches the questions database with answers.\n"); 24 | printf("\t--id \t\tList of question ids delimited by a semicolon.\n"); 25 | printf("\n"); 26 | printf("Generic parameters:\n\n"); 27 | printf("\t--pagesize \tHow many items per page? 100 maximum.\n"); 28 | printf("\t--page \t\tWhat page do you want to see?\n"); 29 | printf("\n"); 30 | } 31 | 32 | int processArguments (int argc, char **argv, stackoverflow_cli_opts *opts) { 33 | static const struct option getopt_long_opts[] = { 34 | { "search", no_argument, NULL, 'S' }, 35 | { "nottagged", required_argument, NULL, 'n' }, 36 | { "tagged", required_argument, NULL, 'g' }, 37 | { "intitle", required_argument, NULL, 't' }, 38 | { "pagesize", required_argument, NULL, 's' }, 39 | { "page", required_argument, NULL, 'p' }, 40 | { "users", no_argument, NULL, 'U' }, 41 | { "filter", required_argument, NULL, 'f' }, 42 | { "id", required_argument, NULL, 'i' }, 43 | { "questions", no_argument, NULL, 'Q' }, 44 | { NULL, no_argument, NULL, 0 } 45 | }; 46 | 47 | char *optstring = "SUn:g:t:s:p:f:h?"; 48 | 49 | memset(opts, 0, sizeof(stackoverflow_cli_opts)); 50 | 51 | /* Some sane defaults */ 52 | opts->operation = none; 53 | 54 | for (;;) { 55 | int opt_index = 0; 56 | int opt = getopt_long(argc, argv, optstring, getopt_long_opts, &opt_index); 57 | 58 | if (opt == -1) 59 | break; 60 | 61 | switch (opt) { 62 | case 'S': 63 | opts->operation = search; 64 | break; 65 | case 'U': 66 | opts->operation = users; 67 | break; 68 | case 'Q': 69 | opts->operation = questions; 70 | break; 71 | case 'n': 72 | opts->nottagged = optarg; 73 | break; 74 | case 'g': 75 | opts->tagged = optarg; 76 | break; 77 | case 't': 78 | opts->intitle = optarg; 79 | break; 80 | case 's': 81 | opts->pagesize = optarg; 82 | break; 83 | case 'p': 84 | opts->page = optarg; 85 | break; 86 | case 'f': 87 | opts->filter = optarg; 88 | break; 89 | case 'i': 90 | opts->id = optarg; 91 | break; 92 | case 'h': 93 | case '?': 94 | default: 95 | break; 96 | } 97 | } 98 | 99 | if (opts->operation == none) { 100 | printUsage(argv[0], NULL); 101 | return 0; 102 | } 103 | 104 | if (opts->operation == questions && opts->id == NULL) { 105 | printUsage(argv[0], "This operation needs --id to be specified.\n"); 106 | return 0; 107 | } 108 | 109 | return 1; 110 | } 111 | -------------------------------------------------------------------------------- /requests.c: -------------------------------------------------------------------------------- 1 | #include "stackoverflow-cli.h" 2 | 3 | #include 4 | 5 | #define URL_BUILDER_ALLOCATION_BLOCK 512 6 | 7 | static size_t requestCallback (char *ptr, size_t size, size_t nmemb, void *usr) { 8 | size_t realsize = size * nmemb; 9 | responseObject *response = (responseObject *)usr; 10 | 11 | if (realsize != 0) { 12 | response->data = realloc(response->data, response->size + realsize + 1); 13 | 14 | if (response->data == NULL) { 15 | printf("Not enough memory!"); 16 | exit(EXIT_FAILURE); 17 | } 18 | 19 | memcpy(&response->data[response->size], ptr, realsize); 20 | response->size += realsize; 21 | response->data[response->size] = 0; 22 | } else { 23 | response->data = NULL; 24 | response->size = 0; 25 | } 26 | 27 | return realsize; 28 | } 29 | 30 | /* 31 | * This is not the most beautiful piece of code ever but I really tried to keep it general enough for 32 | * this little app url building needs. --fms 33 | * 34 | * - baseUrl : base string in which to append everything else, must NOT end with '/' !! 35 | * - numPath : number of path "pieces" passed in the vararg list. 36 | * - numKeyValues : number of key and values pairs passed in the vararg list (after path pieces). 37 | * 38 | * If the num* parameters do not match exactly the number of char * passed, it will crash, 39 | * there's no way to be sure the numbers are actually correct inside the function (that I know of at least). 40 | */ 41 | char *buildUrl (const char *baseUrl, int numPath, int numKeyValues, ...) { 42 | char *result = malloc(URL_BUILDER_ALLOCATION_BLOCK); 43 | int cur_len = 0, cur_alloc = URL_BUILDER_ALLOCATION_BLOCK; 44 | va_list ap; 45 | char *key = NULL, *escaped_key = NULL; 46 | int key_len, i; 47 | CURL *curlHandle = NULL; 48 | char *eos = NULL; 49 | 50 | curl_global_init(CURL_GLOBAL_ALL); 51 | 52 | curlHandle = curl_easy_init(); 53 | 54 | if (result == NULL) { 55 | fprintf(stderr, "Not enough memory!\n"); 56 | return NULL; 57 | } 58 | 59 | if (curlHandle == NULL) { 60 | free(result); 61 | fprintf(stderr, "Could not create libcurl handle.\n"); 62 | return NULL; 63 | } 64 | 65 | memset(result, 0, URL_BUILDER_ALLOCATION_BLOCK); 66 | strncat(result, baseUrl, URL_BUILDER_ALLOCATION_BLOCK - 1); 67 | cur_len = strlen(result); 68 | 69 | va_start(ap, numKeyValues); 70 | for (i = 0; i < numPath + numKeyValues; i++) { 71 | key = va_arg(ap, char *); 72 | 73 | if (key != NULL) { 74 | key_len = strlen(key); 75 | 76 | escaped_key = curl_easy_escape(curlHandle, key, key_len); 77 | 78 | if (escaped_key == NULL) { 79 | fprintf(stderr, "Not enough memory!\n"); 80 | curl_easy_cleanup(curlHandle); 81 | free(result); 82 | return NULL; 83 | } 84 | 85 | key_len = strlen(escaped_key); 86 | } else { 87 | key_len = 0; 88 | } 89 | 90 | if (cur_len + key_len + 1 > cur_alloc) { 91 | if (!realloc(result, cur_alloc + URL_BUILDER_ALLOCATION_BLOCK)) { 92 | fprintf(stderr, "Not enough memory!\n"); 93 | curl_easy_cleanup(curlHandle); 94 | free(result); 95 | return NULL; 96 | } else { 97 | memset(result + cur_alloc, 0, URL_BUILDER_ALLOCATION_BLOCK); 98 | cur_alloc += URL_BUILDER_ALLOCATION_BLOCK; 99 | } 100 | } 101 | 102 | if (i < numPath) 103 | eos = "/"; 104 | else if (i == numPath) 105 | eos = "?"; 106 | else if (eos[0] == '=') 107 | eos = "&"; 108 | else 109 | eos = "="; 110 | 111 | strncat(result, eos, 1); 112 | cur_len++; 113 | 114 | if (key != NULL) { 115 | strncat(result, escaped_key, cur_alloc - cur_len - 1); 116 | cur_len += key_len; 117 | 118 | curl_free(escaped_key); 119 | } 120 | } 121 | va_end(ap); 122 | 123 | curl_easy_cleanup(curlHandle); 124 | 125 | return result; 126 | } 127 | 128 | void makeWebRequest (const char *url, responseObject *response) { 129 | CURL *curlHandle = NULL; 130 | 131 | memset(response, 0, sizeof(responseObject)); 132 | 133 | curl_global_init(CURL_GLOBAL_ALL); 134 | 135 | curlHandle = curl_easy_init(); 136 | 137 | if (curlHandle == NULL) { 138 | fprintf(stderr, "Could not create libcurl handle.\n"); 139 | return; 140 | } 141 | 142 | curl_easy_setopt(curlHandle, CURLOPT_URL, url); 143 | curl_easy_setopt(curlHandle, CURLOPT_ENCODING, "\0"); 144 | curl_easy_setopt(curlHandle, CURLOPT_WRITEFUNCTION, requestCallback); 145 | curl_easy_setopt(curlHandle, CURLOPT_WRITEDATA, (void *)response); 146 | curl_easy_perform(curlHandle); 147 | curl_easy_cleanup(curlHandle); 148 | } 149 | 150 | void freeWebResponse (responseObject *response) { 151 | if (response == NULL) 152 | return; 153 | 154 | if (response->data != NULL) 155 | free(response->data); 156 | 157 | response->size = 0; 158 | } 159 | -------------------------------------------------------------------------------- /stackoverflow-cli.c: -------------------------------------------------------------------------------- 1 | #include "stackoverflow-cli.h" 2 | 3 | int main (int argc, char **argv) { 4 | stackoverflow_cli_opts global_opts; 5 | int result = EXIT_FAILURE; 6 | 7 | if (!processArguments(argc, argv, &global_opts)) 8 | return result; 9 | 10 | if (global_opts.operation == none) 11 | result = EXIT_FAILURE; 12 | else if (global_opts.operation == search) 13 | result = searchCommand(&global_opts) ? EXIT_SUCCESS : EXIT_FAILURE; 14 | else if (global_opts.operation == users) 15 | result = usersCommand(&global_opts) ? EXIT_SUCCESS : EXIT_FAILURE; 16 | else if (global_opts.operation == questions) 17 | result = questionsCommand(&global_opts) ? EXIT_SUCCESS : EXIT_FAILURE; 18 | 19 | return result; 20 | } 21 | -------------------------------------------------------------------------------- /stackoverflow-cli.h: -------------------------------------------------------------------------------- 1 | #ifndef __STACKOVERFLOW_H__ 2 | #define __STACKOVERFLOW_H__ 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | /** Helpers from requests.c *****************************/ 11 | typedef struct responseObject { 12 | size_t size; 13 | char *data; 14 | } responseObject; 15 | 16 | void makeWebRequest (const char *url, responseObject *response); 17 | void freeWebResponse (responseObject *response); 18 | /********************************************************/ 19 | 20 | /** Helpers from arguments.c ****************************/ 21 | enum stackoverflow_operation { none = 0, search, users, questions, user }; 22 | 23 | typedef struct stackoverflow_cli_opts { 24 | enum stackoverflow_operation operation; 25 | char *nottagged; 26 | char *tagged; 27 | char *intitle; 28 | char *filter; 29 | char *id; 30 | char *pagesize; 31 | char *page; 32 | } stackoverflow_cli_opts; 33 | 34 | int processArguments (int argc, char **argv, stackoverflow_cli_opts *opts); 35 | void printUsage (const char *name, const char *msg); 36 | /********************************************************/ 37 | 38 | /*** Helpers from stackoverflow.c ***********************/ 39 | char *buildUrl (const char *baseUrl, int numPath, int numKeyValues, ...); 40 | int searchCommand (stackoverflow_cli_opts *opts); 41 | int usersCommand (stackoverflow_cli_opts *opts); 42 | int questionsCommand (stackoverflow_cli_opts *opts); 43 | /********************************************************/ 44 | 45 | #define STACKOVERFLOW_API_URL "http://api.stackoverflow.com/1.1" 46 | 47 | #endif 48 | -------------------------------------------------------------------------------- /stackoverflow.c: -------------------------------------------------------------------------------- 1 | #include "stackoverflow-cli.h" 2 | 3 | #include 4 | 5 | int searchCommand (stackoverflow_cli_opts *opts) { 6 | responseObject response; 7 | char *url = buildUrl(STACKOVERFLOW_API_URL, 8 | 1, 10, "search", 9 | "nottagged", opts->nottagged, 10 | "tagged", opts->tagged, 11 | "intitle", opts->intitle, 12 | "pagesize", opts->pagesize, 13 | "page", opts->page); 14 | 15 | if (url == NULL) 16 | return 0; 17 | 18 | makeWebRequest(url, &response); 19 | 20 | if (response.size == 0) 21 | return 0; 22 | 23 | json_object *jsonDoc = json_tokener_parse(response.data); 24 | 25 | if (jsonDoc == NULL || is_error(jsonDoc)) { 26 | fprintf(stderr, "Not a valid JSON response?!\n"); 27 | freeWebResponse(&response); 28 | free(url); 29 | return 0; 30 | } 31 | 32 | json_object *jerror = json_object_object_get(jsonDoc, "error"); 33 | 34 | if (jerror != NULL) { 35 | fprintf(stderr, "%s\n", json_object_get_string(json_object_object_get(jerror, "message"))); 36 | freeWebResponse(&response); 37 | free(url); 38 | return 0; 39 | } 40 | 41 | json_object *questions = json_object_object_get(jsonDoc, "questions"); 42 | 43 | if (json_object_get_type(questions) != json_type_array) { 44 | fprintf(stderr, "Questions is expected to be an array!"); 45 | freeWebResponse(&response); 46 | free(url); 47 | return 0; 48 | } 49 | 50 | for (int i = 0; i < json_object_array_length(questions); i++) { 51 | json_object *question = json_object_array_get_idx(questions, i); 52 | 53 | time_t created_stamp = json_object_get_int(json_object_object_get(question, "creation_date")); 54 | struct tm *created = gmtime(&created_stamp); 55 | 56 | printf("id: %i || ", json_object_get_int(json_object_object_get(question, "question_id"))); 57 | printf("title: %s || ", json_object_get_string(json_object_object_get(question, "title"))); 58 | printf("answers: %i || ", json_object_get_int(json_object_object_get(question, "answer_count"))); 59 | printf("author: %s || ", json_object_get_string( 60 | json_object_object_get(json_object_object_get(question, "owner"), "display_name"))); 61 | printf("up_votes: %i || ", json_object_get_int(json_object_object_get(question, "up_vote_count"))); 62 | printf("down_votes: %i || ", json_object_get_int(json_object_object_get(question, "down_vote_count"))); 63 | printf("created: %i/%i/%i %i:%i:%i || ", 64 | created->tm_mday, 65 | created->tm_mon + 1, 66 | created->tm_year + 1900, 67 | created->tm_hour + 1, 68 | created->tm_min + 1, 69 | created->tm_sec + 1); 70 | printf("view_count: %i", json_object_get_int(json_object_object_get(question, "view_count"))); 71 | printf("\n"); 72 | } 73 | 74 | freeWebResponse(&response); 75 | free(url); 76 | 77 | return 1; 78 | } 79 | 80 | int usersCommand (stackoverflow_cli_opts *opts) { 81 | responseObject response; 82 | char *url = buildUrl(STACKOVERFLOW_API_URL, 83 | 2, 6, 84 | "users", opts->id, 85 | "filter", opts->filter, 86 | "pagesize", opts->pagesize, 87 | "page", opts->page); 88 | 89 | if (url == NULL) 90 | return 0; 91 | 92 | makeWebRequest(url, &response); 93 | 94 | if (response.size == 0) 95 | return 0; 96 | 97 | json_object *jsonDoc = json_tokener_parse(response.data); 98 | 99 | if (jsonDoc == NULL || is_error(jsonDoc)) { 100 | fprintf(stderr, "Not a valid JSON response?!\n"); 101 | freeWebResponse(&response); 102 | free(url); 103 | return 0; 104 | } 105 | 106 | json_object *jerror = json_object_object_get(jsonDoc, "error"); 107 | 108 | if (jerror != NULL) { 109 | fprintf(stderr, "%s\n", json_object_get_string(json_object_object_get(jerror, "message"))); 110 | freeWebResponse(&response); 111 | free(url); 112 | return 0; 113 | } 114 | 115 | json_object *users = json_object_object_get(jsonDoc, "users"); 116 | 117 | if (json_object_get_type(users) != json_type_array) { 118 | fprintf(stderr, "Users is expected to be an array!"); 119 | freeWebResponse(&response); 120 | free(url); 121 | return 0; 122 | } 123 | 124 | for (int i = 0; i < json_object_array_length(users); i++) { 125 | json_object *user = json_object_array_get_idx(users, i); 126 | 127 | time_t created_stamp = json_object_get_int(json_object_object_get(user, "creation_date")); 128 | struct tm *created = gmtime(&created_stamp); 129 | 130 | printf("id: %i || ", json_object_get_int(json_object_object_get(user, "user_id"))); 131 | printf("name: %s || ", json_object_get_string(json_object_object_get(user, "display_name"))); 132 | printf("type: %s || ", json_object_get_string(json_object_object_get(user, "user_type"))); 133 | printf("reputation: %i || ", json_object_get_int(json_object_object_get(user, "reputation"))); 134 | printf("questions: %i || ", json_object_get_int(json_object_object_get(user, "question_count"))); 135 | printf("answers: %i || ", json_object_get_int(json_object_object_get(user, "answer_count"))); 136 | printf("up_votes: %i || ", json_object_get_int(json_object_object_get(user, "up_vote_count"))); 137 | printf("down_votes: %i || ", json_object_get_int(json_object_object_get(user, "down_vote_count"))); 138 | printf("created: %i/%i/%i %i:%i:%i", 139 | created->tm_mday, 140 | created->tm_mon + 1, 141 | created->tm_year + 1900, 142 | created->tm_hour + 1, 143 | created->tm_min + 1, 144 | created->tm_sec + 1); 145 | printf("\n"); 146 | } 147 | 148 | freeWebResponse(&response); 149 | free(url); 150 | 151 | return 1; 152 | } 153 | 154 | int questionsCommand (stackoverflow_cli_opts *opts) { 155 | responseObject response; 156 | char *url = buildUrl(STACKOVERFLOW_API_URL, 157 | 3, 6, 158 | "questions", opts->id, "answers", 159 | "filter", opts->filter, 160 | "pagesize", opts->pagesize, 161 | "page", opts->page); 162 | 163 | if (url == NULL) 164 | return 0; 165 | 166 | makeWebRequest(url, &response); 167 | 168 | if (response.size == 0) 169 | return 0; 170 | 171 | json_object *jsonDoc = json_tokener_parse(response.data); 172 | 173 | if (jsonDoc == NULL || is_error(jsonDoc)) { 174 | fprintf(stderr, "Not a valid JSON response?!\n"); 175 | freeWebResponse(&response); 176 | free(url); 177 | return 0; 178 | } 179 | 180 | json_object *jerror = json_object_object_get(jsonDoc, "error"); 181 | 182 | if (jerror != NULL) { 183 | fprintf(stderr, "%s\n", json_object_get_string(json_object_object_get(jerror, "message"))); 184 | freeWebResponse(&response); 185 | free(url); 186 | return 0; 187 | } 188 | 189 | json_object *answers = json_object_object_get(jsonDoc, "answers"); 190 | 191 | if (json_object_get_type(answers) != json_type_array) { 192 | fprintf(stderr, "Answers is expected to be an array!"); 193 | freeWebResponse(&response); 194 | free(url); 195 | return 0; 196 | } 197 | 198 | for (int i = 0; i < json_object_array_length(answers); i++) { 199 | json_object *answer = json_object_array_get_idx(answers, i); 200 | 201 | time_t created_stamp = json_object_get_int(json_object_object_get(answer, "creation_date")); 202 | struct tm *created = gmtime(&created_stamp); 203 | 204 | printf("id: %i || ", json_object_get_int(json_object_object_get(answer, "answer_id"))); 205 | printf("question_title: %s || ", json_object_get_string(json_object_object_get(answer, "title"))); 206 | printf("accepted: %i || ", json_object_get_boolean(json_object_object_get(answer, "accepted"))); 207 | printf("up_votes: %i || ", json_object_get_int(json_object_object_get(answer, "up_vote_count"))); 208 | printf("down_votes: %i || ", json_object_get_int(json_object_object_get(answer, "down_vote_count"))); 209 | printf("author: %s || ", json_object_get_string( 210 | json_object_object_get(json_object_object_get(answer, "owner"), "display_name"))); 211 | printf("created: %i/%i/%i %i:%i:%i", 212 | created->tm_mday, 213 | created->tm_mon + 1, 214 | created->tm_year + 1900, 215 | created->tm_hour + 1, 216 | created->tm_min + 1, 217 | created->tm_sec + 1); 218 | printf("\n"); 219 | } 220 | 221 | freeWebResponse(&response); 222 | free(url); 223 | 224 | return 1; 225 | } 226 | --------------------------------------------------------------------------------