]> mj.ucw.cz Git - libucw.git/commitdiff
Moved some utils from utils/ to ucw/
authorMartin Mares <mj@ucw.cz>
Tue, 9 Sep 2008 10:53:03 +0000 (12:53 +0200)
committerMartin Mares <mj@ucw.cz>
Tue, 9 Sep 2008 10:53:03 +0000 (12:53 +0200)
Some of the utilities do not need sherlock (and are not anyhow related
to it), so they were moved to libucw:

    * b224 (base224 encoder/decoder)
    * daemon-helper (replacement for start-stop-daemon)
    * hex (hex dumper)
    * rotate-log (log rotator)
    * urltool (url manipulating tool)

Introduced a CONFIG_UCW_UTILS config switch which controls compilation
of these utilities.

Originally by Michal, with minor changes by MJ:

    * basecode.test is added to TESTS only if CONFIG_UCW_UTILS is set.
    * Changed the order of Makefile sections and Set statements to
      a more logical one.

free/libs/default.cfg
ucw/Makefile
ucw/b224.c [new file with mode: 0644]
ucw/daemon-helper.c [new file with mode: 0644]
ucw/default.cfg
ucw/hex.c [new file with mode: 0644]
ucw/rotate-log.pl [new file with mode: 0644]
ucw/urltool.c [new file with mode: 0644]

index 6a1bdc216e3e6a6e3f948b374fc775061d5fd2e5..eb60cd3e21a7adbf3e10af892443a1213be03713 100644 (file)
@@ -14,6 +14,7 @@ Set("CONFIG_UCW_THREADS" => 1);
 Set("CONFIG_UCW_PERL" => 1);
 Set("CONFIG_UCW_PERL_MODULES" => 1);
 Set("CONFIG_UCW_SHELL_UTILS" => 1);
+Set("CONFIG_UCW_UTILS" => 1);
 
 # Libsh settings
 Set("CONFIG_BUCKET_SHIFT" => 6);
index 5d2ce0c2d748f2cf9279037a7740ec3a44d973df..0f2dc39eace705474d9c2a7babe9c22e72ec4ff3 100644 (file)
@@ -3,7 +3,11 @@
 DIRS+=ucw
 LIBUCW=$(o)/ucw/libucw.pc
 
-PROGS+=$(o)/ucw/basecode
+ifdef CONFIG_UCW_UTILS
+UCW_UTILS=basecode b224 rotate-log urltool daemon-helper hex
+
+PROGS+=$(addprefix $(o)/ucw/,$(UCW_UTILS))
+endif
 
 LIBUCW_MODS= \
        threads \
@@ -94,7 +98,7 @@ $(o)/ucw/basecode: $(o)/ucw/basecode.o $(LIBUCW)
 TESTS+=$(addprefix $(o)/ucw/,regex.test unicode.test hash-test.test mempool.test stkstring.test \
     slists.test kmp-test.test bbuf.test getopt.test ff-unicode.test eltpool.test \
     fb-socket.test trie-test.test string.test sha1.test asort-test.test binheap-test.test \
-    redblack-test.test basecode.test fb-file.test fb-grow.test fb-pool.test fb-atomic.test \
+    redblack-test.test fb-file.test fb-grow.test fb-pool.test fb-atomic.test \
     fb-limfd.test fb-temp.test fb-mem.test fb-buffer.test fb-mmap.test url.test)
 
 $(o)/ucw/regex.test: $(o)/ucw/regex-t
@@ -115,7 +119,6 @@ $(o)/ucw/trie-test.test: $(o)/ucw/trie-test
 $(o)/ucw/asort-test.test: $(o)/ucw/asort-test
 $(o)/ucw/binheap-test.test: $(o)/ucw/binheap-test
 $(o)/ucw/redblack-test.test: $(o)/ucw/redblack-test
-$(o)/ucw/basecode.test: $(o)/ucw/basecode
 $(addprefix $(o)/ucw/fb-,file.test grow.test pool.test socket.test atomic.test \
        limfd.test temp.test mem.test buffer.test mmap.test): %.test: %-t
 $(o)/ucw/fb-atomic-tt.o: CFLAGS += -DFB_ATOMIC_TRACE
@@ -141,3 +144,13 @@ endif
 ifdef CONFIG_UCW_SHELL_UTILS
 include $(s)/ucw/shell/Makefile
 endif
+
+ifdef CONFIG_UCW_UTILS
+$(o)/ucw/b224: $(o)/ucw/b224.o $(LIBUCW)
+$(o)/ucw/daemon-helper: $(o)/ucw/daemon-helper.o $(LIBUCW)
+$(o)/ucw/urltool: $(o)/ucw/urltool.o $(LIBUCW)
+$(o)/ucw/hex: $(o)/ucw/hex.o $(LIBUCW)
+
+TESTS+=$(o)/ucw/basecode.test
+$(o)/ucw/basecode.test: $(o)/ucw/basecode
+endif
diff --git a/ucw/b224.c b/ucw/b224.c
new file mode 100644 (file)
index 0000000..501bd14
--- /dev/null
@@ -0,0 +1,66 @@
+/*
+ *     A Program For Manipulation With Base224 Encoded Files
+ *
+ *     (c) 2002 Martin Mares <mj@ucw.cz>
+ */
+
+#include "ucw/lib.h"
+#include "ucw/fastbuf.h"
+#include "ucw/base224.h"
+
+#include <stdio.h>
+
+int main(int argc, char **argv)
+{
+  struct fastbuf *in = bfdopen_shared(0, 4096);
+  struct fastbuf *out = bfdopen_shared(1, 4096);
+  byte ib[BASE224_IN_CHUNK*10], ob[BASE224_OUT_CHUNK*10], *b;
+  uns il, ol;
+
+  if (argc != 2 || argv[1][0] != '-')
+    goto usage;
+
+  switch (argv[1][1])
+    {
+    case 'e':                          /* Plain encoding */
+      while (il = bread(in, ib, sizeof(ib)))
+       {
+         ol = base224_encode(ob, ib, il);
+         bwrite(out, ob, ol);
+       }
+      break;
+    case 'E':                          /* Line block encoding */
+      while (il = bread(in, ib, BASE224_IN_CHUNK*6))
+       {
+         ol = base224_encode(ob, ib, il);
+         bputc(out, 'N');
+         bwrite(out, ob, ol);
+         bputc(out, '\n');
+       }
+      break;
+    case 'd':                          /* Plain decoding */
+      while (ol = bread(in, ob, sizeof(ob)))
+       {
+         il = base224_decode(ib, ob, ol);
+         bwrite(out, ib, il);
+       }
+      break;
+    case 'D':                          /* Line block decoding */
+      while (b = bgets(in, ob, sizeof(ob)))
+       {
+         if (!ob[0])
+           die("Invalid line syntax");
+         il = base224_decode(ib, ob+1, b-ob-1);
+         bwrite(out, ib, il);
+       }
+      break;
+    default:
+    usage:
+      fputs("Usage: b224 (-e|-E|-d|-D)\n", stderr);
+      return 1;
+    }
+
+  bclose(in);
+  bclose(out);
+  return 0;
+}
diff --git a/ucw/daemon-helper.c b/ucw/daemon-helper.c
new file mode 100644 (file)
index 0000000..6146aab
--- /dev/null
@@ -0,0 +1,185 @@
+/*
+ *     A Simple Wrapper for Starting and Stopping of Daemons
+ *
+ *     (c) 2003 Martin Mares <mj@ucw.cz>
+ *
+ *     It would seem that we are reinventing the wheel and the
+ *     start-stop-daemon command present in most Linux distributions
+ *     is just what we need, but the usual "does the process already
+ *     exist?" strategies fail in presence of multiple running daemons.
+ *
+ *     Return codes:
+ *     101     already running
+ *     102     not running
+ */
+
+#include "ucw/lib.h"
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <signal.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <getopt.h>
+#include <errno.h>
+#include <alloca.h>
+
+enum action {
+  ACTION_NONE,
+  ACTION_START,
+  ACTION_STOP,
+  ACTION_FORCE_STOP,
+  ACTION_CHECK,
+  ACTION_RELOAD
+};
+
+static int action;
+
+static struct option options[] = {
+  { "pid-file",                required_argument,      NULL, 'p' },
+  { "status-file",     required_argument,      NULL, 's' },
+  { "start",           no_argument,            &action, ACTION_START },
+  { "stop",            no_argument,            &action, ACTION_STOP },
+  { "force-stop",      no_argument,            &action, ACTION_FORCE_STOP },
+  { "check",           no_argument,            &action, ACTION_CHECK },
+  { "reload",          no_argument,            &action, ACTION_RELOAD },
+  { NULL,              no_argument,            NULL, 0 }
+};
+
+static void NONRET
+usage(void)
+{
+  fputs("\n\
+Usage: daemon-helper --start <options> -- <daemon> <args>\n\
+   or: daemon-helper --stop <options>\n\
+   or: daemon-helper --force-stop <options>\n\
+   or: daemon-helper --reload <options>\n\
+   or: daemon-helper --check <options>\n\
+\n\
+Options:\n\
+--pid-file <name>      Name of PID file for this daemon (mandatory)\n\
+--status-file <name>   Status file used by the daemon (deleted just before starting)\n\
+", stderr);
+  exit(1);
+}
+
+int
+main(int argc, char **argv)
+{
+  int c, fd;
+  char *pidfile = NULL;
+  char *statfile = NULL;
+  struct flock fl;
+  char buf[64];
+
+  while ((c = getopt_long(argc, argv, "", options, NULL)) >= 0)
+    switch (c)
+      {
+      case 0:
+       break;
+      case 'p':
+       pidfile = optarg;
+       break;
+      case 's':
+       statfile = optarg;
+       break;
+      default:
+       usage();
+      }
+  if (!pidfile)
+    usage();
+
+  bzero(&fl, sizeof(fl));
+  fl.l_type = F_WRLCK;
+  fl.l_whence = SEEK_SET;
+
+  switch (action)
+    {
+    case ACTION_START:
+      if (optind >= argc)
+       usage();
+      fd = open(pidfile, O_RDWR | O_CREAT, 0666);
+      if (fd < 0)
+       die("Unable to create %s: %m", pidfile);
+      if ((c = fcntl(fd, F_SETLK, &fl)) < 0)
+       {
+         if (errno == EAGAIN || errno == EACCES)
+           return 101;
+         else
+           die("fcntl lock on %s failed: %m", pidfile);
+       }
+      c = sprintf(buf, "%d\n", getpid());
+      if (write(fd, buf, c) != c)
+       die("write on %s failed: %m", pidfile);
+      if (ftruncate(fd, c) < 0)
+       die("truncate on %s failed: %m", pidfile);
+      if (statfile && unlink(statfile) < 0 && errno != ENOENT)
+       die("unlink(%s) failed: %m", statfile);
+      setsid();
+      /* Disconnect from stdin and stdout, leave stderr to the daemon. */
+      close(0);
+      open("/dev/null", O_RDWR, 0);
+      dup2(0, 1);
+      argv += optind;
+      argc -= optind;
+      char **a = alloca(sizeof(char *) * (argc+1));
+      memcpy(a, argv, sizeof(char *) * argc);
+      a[argc] = NULL;
+      execv(a[0], a);
+      die("Cannot execute %s: %m", a[0]);
+    case ACTION_STOP:
+    case ACTION_FORCE_STOP:
+    case ACTION_CHECK:
+    case ACTION_RELOAD:
+      if (optind < argc)
+       usage();
+      fd = open(pidfile, O_RDWR);
+      if (fd < 0)
+       {
+         if (errno == ENOENT)
+           return 102;
+         else
+           die("Unable to open %s: %m", pidfile);
+       }
+      if ((c = fcntl(fd, F_SETLK, &fl)) >= 0)
+       {
+       nopid:
+         unlink(pidfile);
+         return 102;
+       }
+      if (errno != EAGAIN && errno != EACCES)
+       die("fcntl lock on %s failed: %m", pidfile);
+      if ((c = read(fd, buf, sizeof(buf))) < 0)
+       die("read on %s failed: %m", pidfile);
+      if (!c)
+       goto nopid;
+      if (c >= (int) sizeof(buf) || sscanf(buf, "%d", &c) != 1)
+       die("PID file syntax error");
+      int sig = 0;
+      if (action == ACTION_CHECK || action == ACTION_RELOAD)
+       {
+         if (action == ACTION_RELOAD)
+           sig = SIGHUP;
+         if (kill(c, sig) < 0 && errno == ESRCH)
+           goto nopid;
+         return 0;
+       }
+      sig = (action == ACTION_STOP) ? SIGTERM : SIGQUIT;
+      if (kill(c, sig) < 0)
+       {
+         if (errno == ESRCH)
+           goto nopid;
+         die("Cannot kill process %d: %m", c);
+       }
+      if ((c = fcntl(fd, F_SETLKW, &fl)) < 0)
+       die("Cannot lock %s: %m", pidfile);
+      if (statfile)
+       unlink(statfile);
+      if (unlink(pidfile) < 0)
+       die("Cannot unlink %s: %m", pidfile);
+      return 0;
+    default:
+      usage();
+    }
+}
index 8c2b21ba853371e36c8eceef526a579db552a378..cb6e346171b80164f5da157093a33fbaf8f3f1c2 100644 (file)
@@ -35,6 +35,9 @@ UnSet("CONFIG_UCW_PERL_MODULES");
 # Include support utilities for shell scripts
 Set("CONFIG_UCW_SHELL_UTILS" => 1);
 
+# Include utilities
+Set("CONFIG_UCW_UTILS" => 1);
+
 # Default configuration file
 UnSet("DEFAULT_CONFIG");
 
diff --git a/ucw/hex.c b/ucw/hex.c
new file mode 100644 (file)
index 0000000..4ddb96a
--- /dev/null
+++ b/ucw/hex.c
@@ -0,0 +1,263 @@
+/*
+ *  Hexadecimal dumper (CP/M style format)
+ *
+ *  Original version (c) Eric S. Raymond <esr@snark.thyrsus.com>
+ *  Heavily modified by Martin Mares <mj@ucw.cz>
+ *
+ *  Can be freely distributed and used under the terms of the GNU GPL.
+ */
+
+#include "ucw/lib.h"
+#include "ucw/lfs.h"
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <ctype.h>
+#include <string.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+#define DEFWIDTH 16            /* Default # chars to show per line */
+#define MAXWIDTH 32            /* Maximum # of bytes per line  */
+
+typedef int bool;
+#define TRUE   1
+#define FALSE  0
+
+static long    linesize = DEFWIDTH;    /* # of bytes to print per line */
+static bool    cflag = FALSE;          /* show printables as ASCII if true */
+static bool    gflag = FALSE;          /* suppress mid-page gutter if true */
+static ucw_off_t       start = 0;              /* file offset to start dumping at */
+static ucw_off_t       length = 0;             /* if nz, how many chars to dump */
+
+static void dumpfile(FILE *f)
+     /* dump a single, specified file -- stdin if filename is NULL */
+{
+  int     ch = '\0';           /* current character            */
+  char    ascii[MAXWIDTH+3];   /* printable ascii data         */
+  int     i = 0;               /* counter: # bytes processed   */
+  int     ai = 0;              /* index into ascii[]           */
+  ucw_off_t offset = start;    /* byte offset of line in file  */
+  int     hpos = 0;            /* horizontal position counter  */
+  ucw_off_t fstart = start;
+  ucw_off_t flength = length;
+  char    *specials = "\b\f\n\r\t";
+  char    *escapes = "bfnrt";
+  char    *cp;
+
+  if (fstart && ucw_seek(fileno(f), fstart, SEEK_SET) >= 0)
+    fstart = 0;
+
+  do {
+    ch = getc(f);
+
+    if (ch != EOF)
+      {
+       if (length && flength-- <= 0)
+         ch = EOF;
+      }
+
+    if (ch != EOF)
+      {
+       if (i++ % linesize == 0)
+         {
+           (void) printf("%04Lx ", (long long) offset);
+           offset += linesize;
+           hpos = 5;
+         }
+
+       /* output one space for the mid-page gutter */
+       if (!gflag)
+         if ((i - 1) % (linesize / 2) == 0)
+           {
+             (void) putchar(' ');
+             hpos++;
+             ascii[ai++] = ' ';
+           }
+
+       /* dump the indicated representation of a character */
+       ascii[ai] = (isprint (ch) || ch == ' ') ? ch : '.';
+
+       if (cflag && (isprint(ch) || ch == ' '))
+         (void) printf("%c  ", ch);
+       else if (cflag && ch && (cp = strchr(specials, ch)))
+         (void) printf("\\%c ", escapes[cp - specials]);
+       else
+         (void) printf("%02x ", ch);
+
+       /* update counters and things */
+       ai++;
+       hpos += 3;
+      }
+
+    /* At end-of-line or EOF, show ASCII version of data. */
+    if (i && (ch == EOF || (i % linesize == 0)))
+      {
+       if (!cflag)
+         {
+           while (hpos < linesize * 3 + 7)
+             {
+               hpos++;
+               (void) putchar(' ');
+             }
+
+           ascii[ai] = '\0';
+           (void) printf("%s", ascii);
+         }
+
+       if (ch != EOF || (i % linesize != 0))
+         (void) putchar('\n');
+       ai = 0;         /* reset counters */
+      }
+  } while
+    (ch != EOF);
+}
+
+static ucw_off_t getoffs(char *cp)
+     /* fetch decimal or hex integer to be used as file start or offset */
+{
+  ucw_off_t value = 0;
+  char *hexdigits = "0123456789abcdefABCDEF";
+
+#if 0
+  bool foundzero = FALSE;
+  int base = 0;
+
+  for (; *cp; cp++)
+    if (*cp == '0')
+      foundzero = TRUE;
+    else if (isdigit(*cp))
+      {
+       base = 10;
+       break;
+      }
+    else if (*cp = 'x' || *cp == 'X' || *cp == 'h' || *cp == 'H')
+      {
+       base = 16;
+       cp++;
+       break;
+      }
+    else
+      return(-1L);
+
+  if (base == 0)
+    if (foundzero)
+      base = 10;
+    else
+      return(-1L);
+
+  if (base == 10)
+    {
+      for (; *cp; cp++)
+       if (isdigit(*cp))
+         value = value * 10 + (*cp - '0');
+       else
+         return(-1L);
+    }
+  else
+#endif
+    {
+      for (; *cp; cp++)
+       if (strchr(hexdigits, *cp))
+         value = value*16 + (strchr(hexdigits, tolower(*cp))-hexdigits);
+       else
+         return -1;
+    }
+
+  return(value);
+}
+
+int main(int argc, char **argv)
+{
+  FILE    *infile;         /* file pointer input file */
+  int      dumpcount = 0;  /* count of files dumped so far */
+  char    *cp;
+  int    fd;
+
+  for (argv++, argc--; argc > 0; argv++, argc--)
+    {
+      char s = **argv;
+
+      if (s == '-' || s == '+')
+       {
+         int   c = *++*argv;
+
+         switch (c)
+           {
+           case 'c': cflag = (s == '-'); continue;
+           case 'g': gflag = (s == '-'); continue;
+
+           case 's':
+             if ((*argv)[1])
+               (*argv)++;
+             else
+               argc--, argv++;
+             if (s == '-' && argc >= 0)
+               {
+                 if (cp = strchr(*argv, ','))
+                   *cp++ = '\0';
+                 if ((start = getoffs(*argv)) < 0)
+                   {
+                     (void) fputs("hex: start offset no good\n", stderr);
+                     exit(1);
+                   }
+
+                 if (cp)
+                   if ((length = getoffs(cp)) < 0)
+                     {
+                       (void) fputs("hex: length no good\n", stderr);
+                       exit(1);
+                     }
+               }
+             else
+               start = length = 0L;
+             continue;
+
+           case '\0':
+             infile = stdin;
+             break;
+
+           case 'w':
+             if ((*argv)[1])
+               (*argv)++;
+             else
+               argc--, argv++;
+             if ((linesize = getoffs(*argv)) == -1L || linesize > MAXWIDTH)
+               {
+                 (void) fputs("hex: line width no good\n", stderr);
+                 exit(1);
+               }
+             if (linesize % 2)
+               gflag = TRUE;
+             continue;
+
+           default:
+             (void) fprintf(stderr, "hex: no such option as %s\n", *argv);
+             exit(1);
+           }
+       }
+      else
+       {
+         fd = ucw_open(*argv, O_RDONLY, 0);
+         if (fd < 0 || !(infile = fdopen(fd, "r")))
+           {
+             (void) fprintf(stderr, "hex: cannot open %s: %m\n", *argv);
+             exit(1);
+           }
+       }
+
+      if (dumpcount > 0 || argc > 1)
+       if (infile == stdin)
+         (void) printf("---- <Standard input> ----\n");
+       else
+         (void) printf("---- %s ----\n", *argv);
+      dumpfile(infile);
+      dumpcount++;
+      if (infile != stdin)
+       (void) fclose(infile);
+    }
+
+  if (dumpcount == 0)
+    dumpfile(stdin);
+  return(0);
+}
diff --git a/ucw/rotate-log.pl b/ucw/rotate-log.pl
new file mode 100644 (file)
index 0000000..e4c9b10
--- /dev/null
@@ -0,0 +1,27 @@
+#!/usr/bin/perl
+
+#      Rotate Sherlock logs
+#      (c) 2001--2002 Martin Mares <mj@ucw.cz>
+
+use File::stat;
+
+@ARGV >= 3 or die "Usage: rotate-log <days-to-compress> <date-to-delete> <logs...>";
+
+$now = time;
+$cps = shift @ARGV;
+$del = shift @ARGV;
+
+$compress_thr = $now - 86400 * $cps;
+$delete_thr = $now - 86400 * $del;
+foreach $f (@ARGV) {
+       -f $f or next;
+       $st = stat $f or next;
+       if ($del > 0 && $st->mtime < $delete_thr) {
+               print "Deleting $f\n";
+               unlink $f || die "Delete FAILED: $!";
+       } elsif ($cps > 0 && $st->mtime < $compress_thr && $f !~ /\.(gz|bz2)$/) {
+               print "Compressing $f\n";
+               `gzip -f $f`;
+               $? && die "Compression FAILED: $!";
+       }
+}
diff --git a/ucw/urltool.c b/ucw/urltool.c
new file mode 100644 (file)
index 0000000..5bcb33c
--- /dev/null
@@ -0,0 +1,145 @@
+/*
+ *     Sherlock Utilities -- URL Handling Tool
+ *
+ *     (c) 2004 Martin Mares <mj@ucw.cz>
+ */
+
+#include "ucw/lib.h"
+#include "ucw/getopt.h"
+#include "ucw/url.h"
+#include "ucw/fastbuf.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+static byte *base_url;
+static struct url base;
+static uns opt_split = 0, opt_normalize = 0, opt_forgive = 0;
+static struct fastbuf *fout;
+static uns err_count;
+
+static void
+process_url(byte *url)
+{
+  byte buf1[MAX_URL_SIZE], buf2[MAX_URL_SIZE], buf3[MAX_URL_SIZE], buf4[MAX_URL_SIZE];
+  int e;
+  struct url ur;
+
+  if ((e = url_deescape(url, buf1)) || (e = url_split(buf1, &ur, buf2)))
+    goto error;
+  if ((base_url || opt_normalize) && (e = url_normalize(&ur, &base)))
+    goto error;
+  if (opt_normalize && (e = url_canonicalize(&ur)))
+    goto error;
+  if (opt_split)
+    {
+      if (ur.protocol)
+       bprintf(fout, "protocol=%s\n", ur.protocol);
+      if (ur.user)
+       bprintf(fout, "user=%s\n", ur.user);
+      if (ur.pass)
+       bprintf(fout, "pass=%s\n", ur.pass);
+      if (ur.host)
+       bprintf(fout, "host=%s\n", ur.host);
+      if (ur.port != ~0U)
+       bprintf(fout, "port=%d\n", ur.port);
+      if (ur.rest)
+       bprintf(fout, "rest=%s\n", ur.rest);
+      bputc(fout, '\n');
+    }
+  else
+    {
+      if ((e = url_pack(&ur, buf3)) || (e = url_enescape(buf3, buf4)))
+       goto error;
+      bprintf(fout, "%s\n", buf4);
+    }
+  return;
+
+ error:
+  msg(L_ERROR, "%s: %s", url, url_error(e));
+  err_count++;
+}
+
+static char *shortopts = CF_SHORT_OPTS "b:fns";
+static struct option longopts[] =
+{
+  CF_LONG_OPTS
+  { "base",            1, 0, 'b' },
+  { "forgive",         0, 0, 'f' },
+  { "normalize",       0, 0, 'n' },
+  { "split",           0, 0, 's' },
+  { NULL,              0, 0, 0 }
+};
+
+static char *help = "\
+Usage: urltool [<options>] <operations> [<URL's>]\n\
+\n\
+Options:\n"
+CF_USAGE "\
+-b, --base <URL>\tInput URL's are relative to this base\n\
+-f, --forgive\t\tReturn exit status 0 even if there were errors\n\
+\n\
+Operations:\n\
+-s, --split\t\tSplit a given URL to components\n\
+-n, --normalize\t\tNormalize given URL\n\
+";
+
+static void NONRET
+usage(byte *msg)
+{
+  if (msg)
+    {
+      fputs(msg, stderr);
+      fputc('\n', stderr);
+    }
+  fputs(help, stderr);
+  exit(1);
+}
+
+int
+main(int argc, char **argv)
+{
+  int opt, err;
+  byte *base_url = NULL;
+  byte basebuf1[MAX_URL_SIZE], basebuf2[MAX_URL_SIZE];
+
+  log_init(argv[0]);
+  while ((opt = cf_getopt(argc, argv, shortopts, longopts, NULL)) >= 0)
+    switch (opt)
+      {
+      case 'b':
+       base_url = optarg;
+       err = url_canon_split(base_url, basebuf1, basebuf2, &base);
+       if (err)
+         die("Invalid base URL: %s", url_error(err));
+       break;
+      case 's':
+       opt_split = 1;
+       break;
+      case 'n':
+       opt_normalize = 1;
+       break;
+      case 'f':
+       opt_forgive = 1;
+       break;
+      default:
+       usage("Invalid option");
+      }
+
+  fout = bfdopen_shared(1, 4096);
+  if (optind >= argc)
+    {
+      struct fastbuf *fin = bfdopen_shared(0, 4096);
+      byte url[MAX_URL_SIZE];
+      while (bgets(fin, url, sizeof(url)))
+       process_url(url);
+      bclose(fin);
+    }
+  else
+    while (optind < argc)
+      process_url(argv[optind++]);
+  bclose(fout);
+
+  return (err_count && !opt_forgive);
+}