robots.txt Generator — C source
Build a standards-compliant robots.txt with per-user-agent allow/disallow rules, crawl-delay, and sitemap entries.
This is the C implementation — the same logic the interactive tool runs, in a shareable, citable form.
/* robots-txt-generator — C port: standards-compliant robots.txt generator + parser.
*
* Self-contained display port of the CosmoDev Robots.txt Generator tool — same
* contract as cli/robots-txt-generator/robots-txt-generator.go (the live Go
* twin) and src/lib/robotsTxt.ts (canonical TypeScript), matching the
* dependency-free Rust port in this directory. A group carries a LIST of
* user-agents (the REP spec's stacked User-agent lines); parsing aggregates
* repeated blocks per agent, then re-merges agents whose accumulated rules
* are identical into one stacked group. Fixed-capacity demo storage: 8 groups,
* 12 agents, 16 rules/group, 8 KB body. The parser's scratch is a
* function-local static (single-threaded demo, keeps the stack shallow).
*/
#include <ctype.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_AGENTS 12
#define MAX_RULES 16
#define MAX_GROUPS 8
#define UA_LEN 64
#define PATH_LEN 128
#define URL_LEN 256
typedef struct {
char userAgents[MAX_AGENTS][UA_LEN]; int nUserAgents; /* {"*"} or stacked agents */
char disallow[MAX_RULES][PATH_LEN]; int nDisallow; /* "" renders as a bare "Disallow:" */
char allow[MAX_RULES][PATH_LEN]; int nAllow;
double crawlDelay; int hasCrawlDelay; /* NaN = unset, never emitted */
} RuleGroup;
typedef struct {
RuleGroup groups[MAX_GROUPS]; int nGroups;
char sitemaps[MAX_RULES][URL_LEN]; int nSitemaps;
} RobotsConfig;
static void rg_init(RuleGroup *g) { memset(g, 0, sizeof *g); }
static void rg_agent(RuleGroup *g, const char *ua) {
if (g->nUserAgents < MAX_AGENTS) snprintf(g->userAgents[g->nUserAgents++], UA_LEN, "%s", ua);
}
static void rg_disallow(RuleGroup *g, const char *d) {
if (g->nDisallow < MAX_RULES) snprintf(g->disallow[g->nDisallow++], PATH_LEN, "%s", d);
}
static void rg_allow(RuleGroup *g, const char *a) {
if (g->nAllow < MAX_RULES) snprintf(g->allow[g->nAllow++], PATH_LEN, "%s", a);
}
static int visible(const char *s) { /* any non-whitespace char? */
for (; *s; s++)
if (!isspace((unsigned char)*s)) return 1;
return 0;
}
static char *trim_in_place(char *s) {
while (isspace((unsigned char)*s)) s++;
char *end = s + strlen(s);
while (end > s && isspace((unsigned char)end[-1])) end--;
*end = '\0';
return s;
}
/* Append prefix + trim(v) + '\n' to out; a NULL v emits the bare prefix. */
static void append_trimmed(char *out, size_t cap, const char *prefix, const char *v) {
size_t used = strnlen(out, cap);
if (used + 1 >= cap) return;
int n = snprintf(out + used, cap - used, "%s", prefix);
if (n < 0) return;
used += (size_t)n;
if (used >= cap) return;
if (v) {
while (isspace((unsigned char)*v)) v++;
const char *end = v + strlen(v);
while (end > v && isspace((unsigned char)end[-1])) end--;
n = snprintf(out + used, cap - used, "%.*s", (int)(end - v), v);
if (n < 0) return;
used += (size_t)n;
if (used >= cap) return;
}
snprintf(out + used, cap - used, "\n");
}
/* Fold 3+ consecutive newlines to two, strip trailing whitespace, guarantee a
* single terminating newline (mirrors TS /\n{3,}/g + trimEnd). */
static void finish(char *out) {
size_t w = 0, r = 0, len = strlen(out);
int run = 0;
for (; r < len; r++) {
if (out[r] == '\n') { if (++run <= 2) out[w++] = '\n'; }
else { run = 0; out[w++] = out[r]; }
}
while (w > 0 && isspace((unsigned char)out[w - 1])) w--;
out[w] = '\n';
out[w + 1] = '\0';
}
void generate_robots(const RobotsConfig *cfg, char *out, size_t cap) {
out[0] = '\0';
for (int i = 0; i < cfg->nGroups; i++) {
const RuleGroup *g = &cfg->groups[i];
/* cleanAgents in the TS lib: trim + drop blanks. A non-empty list that
* trims away entirely degrades to "*"; an empty list means "no group". */
int emitted = 0;
for (int a = 0; a < g->nUserAgents; a++) {
if (!visible(g->userAgents[a])) continue;
append_trimmed(out, cap, "User-agent: ", g->userAgents[a]);
emitted = 1;
}
if (!emitted) {
if (g->nUserAgents > 0) append_trimmed(out, cap, "User-agent: ", "*");
else continue;
}
for (int a = 0; a < g->nAllow; a++) /* blanks carry no meaning */
if (visible(g->allow[a])) append_trimmed(out, cap, "Allow: ", g->allow[a]);
if (g->nDisallow == 0) {
append_trimmed(out, cap, "Disallow:", NULL); /* the allow-all marker */
} else {
for (int d = 0; d < g->nDisallow; d++)
append_trimmed(out, cap, "Disallow: ", g->disallow[d]); /* "" kept verbatim */
}
if (g->hasCrawlDelay && isfinite(g->crawlDelay)) { /* JS-style number: 10.0 -> "10" */
char line[64];
if (g->crawlDelay == floor(g->crawlDelay))
snprintf(line, sizeof line, "Crawl-delay: %lld", (long long)g->crawlDelay);
else
snprintf(line, sizeof line, "Crawl-delay: %g", g->crawlDelay);
append_trimmed(out, cap, line, NULL);
}
append_trimmed(out, cap, "", NULL); /* blank line separates groups */
}
for (int s = 0; s < cfg->nSitemaps; s++)
if (visible(cfg->sitemaps[s])) append_trimmed(out, cap, "Sitemap: ", cfg->sitemaps[s]);
finish(out);
}
/* Move the pending User-agent stack into per-agent rule buckets (Google's
* merge semantics: a rule line applies to every agent of the immediately
* preceding consecutive User-agent stack). */
static void flush_stack(char stack[][UA_LEN], int *nStack, RuleGroup *perUA, int *nUA,
int *curIdx, int *nCur) {
if (*nStack == 0) return;
*nCur = 0;
for (int s = 0; s < *nStack; s++) {
int i = 0;
for (; i < *nUA; i++)
if (strcmp(perUA[i].userAgents[0], stack[s]) == 0) break;
if (i == *nUA) {
rg_init(&perUA[*nUA]);
rg_agent(&perUA[*nUA], stack[s]);
(*nUA)++;
}
curIdx[(*nCur)++] = i;
}
*nStack = 0;
}
static int paths_equal(int nA, const char a[][PATH_LEN], int nB, const char b[][PATH_LEN]) {
if (nA != nB) return 0;
for (int i = 0; i < nA; i++)
if (strcmp(a[i], b[i]) != 0) return 0;
return 1;
}
static int cd_equal(const RuleGroup *a, const RuleGroup *b) { /* non-finite merges as unset, like TS */
int ha = a->hasCrawlDelay && isfinite(a->crawlDelay);
int hb = b->hasCrawlDelay && isfinite(b->crawlDelay);
return ha && hb ? a->crawlDelay == b->crawlDelay : ha == hb;
}
/* ParseRobots in the Go twin: strip '#'-comments, split on the FIRST ':'
* (values may contain colons), unknown directives ignored. Garbage
* Crawl-delay values coerce to NaN like JS Number() — never re-emitted. */
void parse_robots(const char *text, RobotsConfig *cfg) {
static RuleGroup perUA[MAX_AGENTS]; /* fixed-capacity demo scratch */
memset(perUA, 0, sizeof perUA);
memset(cfg, 0, sizeof *cfg);
char stack[MAX_AGENTS][UA_LEN];
int nStack = 0, nUA = 0, nCur = 0;
int curIdx[MAX_AGENTS];
char line[512];
const char *p = text;
while (p && *p) {
const char *nl = strchr(p, '\n');
size_t len = nl ? (size_t)(nl - p) : strlen(p);
if (len >= sizeof line) len = sizeof line - 1;
memcpy(line, p, len);
line[len] = '\0';
p = nl ? nl + 1 : NULL;
char *hash = strchr(line, '#');
if (hash) *hash = '\0';
char *t = trim_in_place(line);
if (!*t) continue;
char *colon = strchr(t, ':');
if (!colon) continue;
*colon = '\0';
char *field = trim_in_place(t);
char *value = trim_in_place(colon + 1);
for (char *f = field; *f; f++) *f = (char)tolower((unsigned char)*f);
if (strcmp(field, "user-agent") == 0) {
const char *ua = *value ? value : "*";
int dup = 0;
for (int i = 0; i < nStack; i++)
if (strcmp(stack[i], ua) == 0) { dup = 1; break; }
if (!dup && nStack < MAX_AGENTS) snprintf(stack[nStack++], UA_LEN, "%s", ua);
} else if (strcmp(field, "sitemap") == 0) {
flush_stack(stack, &nStack, perUA, &nUA, curIdx, &nCur);
if (cfg->nSitemaps < MAX_RULES) snprintf(cfg->sitemaps[cfg->nSitemaps++], URL_LEN, "%s", value);
} else if (strcmp(field, "disallow") == 0 || strcmp(field, "allow") == 0 ||
strcmp(field, "crawl-delay") == 0) {
flush_stack(stack, &nStack, perUA, &nUA, curIdx, &nCur);
for (int i = 0; i < nCur; i++) {
RuleGroup *acc = &perUA[curIdx[i]];
if (strcmp(field, "disallow") == 0) rg_disallow(acc, value);
else if (strcmp(field, "allow") == 0) rg_allow(acc, value);
else {
char *endp = NULL;
double v = strtod(value, &endp);
acc->hasCrawlDelay = 1;
acc->crawlDelay = (endp && *endp == '\0') ? v : NAN;
}
}
}
}
/* Merge agents with identical rule sets into one stacked group. */
for (int i = 0; i < nUA; i++) {
RuleGroup *acc = &perUA[i];
int k = -1;
for (int j = 0; j < cfg->nGroups; j++) {
RuleGroup *g = &cfg->groups[j];
if (paths_equal(g->nDisallow, g->disallow, acc->nDisallow, acc->disallow) &&
paths_equal(g->nAllow, g->allow, acc->nAllow, acc->allow) && cd_equal(g, acc)) { k = j; break; }
}
if (k >= 0) {
rg_agent(&cfg->groups[k], acc->userAgents[0]);
} else if (cfg->nGroups < MAX_GROUPS) {
RuleGroup *g = &cfg->groups[cfg->nGroups++];
rg_agent(g, acc->userAgents[0]);
for (int d = 0; d < acc->nDisallow; d++) rg_disallow(g, acc->disallow[d]);
for (int a = 0; a < acc->nAllow; a++) rg_allow(g, acc->allow[a]);
g->hasCrawlDelay = acc->hasCrawlDelay;
g->crawlDelay = acc->crawlDelay;
}
}
}
int main(void) {
/* Sample config -> generated body -> parse round-trip check (tool page behavior). */
static RobotsConfig cfg, parsed;
static char body[8192], again[8192];
RuleGroup *g1 = &cfg.groups[cfg.nGroups++];
rg_agent(g1, "*");
rg_disallow(g1, "/private/");
rg_disallow(g1, "/admin/");
rg_allow(g1, "/admin/public/");
RuleGroup *g2 = &cfg.groups[cfg.nGroups++];
rg_agent(g2, "GPTBot");
rg_agent(g2, "CCBot");
rg_disallow(g2, "/");
g2->hasCrawlDelay = 1;
g2->crawlDelay = 10.0;
snprintf(cfg.sitemaps[cfg.nSitemaps++], URL_LEN, "%s", "https://example.com/sitemap.xml");
generate_robots(&cfg, body, sizeof body);
fputs(body, stdout);
parse_robots(body, &parsed);
generate_robots(&parsed, again, sizeof again);
printf("round-trip stable: %s\n", strcmp(again, body) == 0 ? "true" : "false");
}
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →