Considerable redesign to incorporate UI enhancements from chiark
authorsimon <simon@cda61777-01e9-0310-a592-d414129be87e>
Wed, 23 Nov 2011 19:48:31 +0000 (19:48 +0000)
committersimon <simon@cda61777-01e9-0310-a592-d414129be87e>
Wed, 23 Nov 2011 19:48:31 +0000 (19:48 +0000)
users. Now articles may be specified as a message-id, as a group and
an article number separated by whitespace, or as a group and an
article number separated by a colon. Also, with no arguments, nntpid
will read from standard input, parse each line into an article
description, and display that article.

git-svn-id: svn://svn.tartarus.org/sgt/utils@9341 cda61777-01e9-0310-a592-d414129be87e

nntpid/nntpid
nntpid/nntpid.but

index f1544bb..ffdde0d 100755 (executable)
 # a 480 response from the news server; if your news server isn't paranoid
 # then the script will never need to look at NNTPAUTH.
 
-# Copyright 2000,2004 Simon Tatham. All rights reserved.
+# Copyright 2000,2004,2011 Simon Tatham. All rights reserved.
 
 require 5.002;
 use Socket;
 use FileHandle;
 
 $usage =
-  "usage: nntpid [ -v ] [ -d ] <message-id>\n" .
-  "   or: nntpid [ -v ] [ -d ] <newsgroup> <article-number>\n" .
-  "   or: nntpid [ -v ] -a <newsgroup>\n" .
-  "where: -v                  verbose (print interaction with news server)\n" .
+  "usage: nntpid [ -v ] [ -d ] <article> [<article>...]           display articles\n" .
+  "   or: nntpid [ -v ] [ -d ]           display articles read from standard input\n" .
+  "   or: nntpid [ -v ] -a <newsgroup>             dump a newsgroup in mbox format\n" .
+  "where: <article>           a news article specified in one of several ways:\n" .
+  "                            - Message-Id in angle brackets\n" .
+  "                            - bare Message-Id without angle brackets\n" .
+  "                            - newsgroup and article number in separate words\n" .
+  "                            - newsgroup and article number separated by :\n" .
+  "       -v                  verbose (print interaction with news server)\n" .
   "       -d                  direct output (don't consider using PAGER)\n" .
   "       -a                  dump all articles in group to stdout as mbox\n" .
   " also: nntpid --version    report version number\n" .
@@ -34,7 +39,7 @@ $usage =
   "       nntpid --licence    display (MIT) licence text\n";
 
 $licence =
-  "nntpid is copyright 2000,2004 Simon Tatham.\n" .
+  "nntpid is copyright 2000,2004,2011 Simon Tatham.\n" .
   "\n" .
   "Permission is hereby granted, free of charge, to any person\n" .
   "obtaining a copy of this software and associated documentation files\n" .
@@ -58,13 +63,13 @@ $licence =
 
 $pager = 1;
 $verbose = 0;
-$all = 0;
+$mode = 'list';
 
 while ($ARGV[0] =~ /^-(.+)$/) {
   shift @ARGV;
   $verbose = 1, next if $1 eq "v";
   $pager = 0, next if $1 eq "d";
-  $all = 1, next if $1 eq "a";
+  $mode = 'all', next if $1 eq "a";
   if ($1 eq "-help") {
     print STDERR $usage;
     exit 0;
@@ -81,19 +86,37 @@ while ($ARGV[0] =~ /^-(.+)$/) {
   }
 }
 
-die $usage if !defined $ARGV[0];
-
-if ($all) {
-  $group = $ARGV[0];
-} elsif (defined $ARGV[1]) {
+if ($mode eq 'all') {
+  # -a uses completely different command-line semantics from the
+  # normal ones..
+  die "nntpid: -a expected exactly one argument\n" if @ARGV != 1;
   $group = $ARGV[0];
-  $mid = $ARGV[1];
+} elsif (!@ARGV) {
+  # We will read article ids from standard input once we've connected
+  # to the NNTP server.
+  $mode = 'stdin';
 } else {
-  $group = "misc.misc";
-  $mid = $ARGV[0];
-  $mid =~ s/^<//;
-  $mid =~ s/>$//;
-  $mid = "<$mid>";
+  @list = ();
+  while (defined ($arg = shift @ARGV)) {
+    # See if this argument makes sense on its own.
+    ($group, $mid) = &parsearticle($arg);
+    if (defined $mid) {
+      push @list, $arg;
+    } else {
+      # If it doesn't, try concatenating it with a space to the next
+      # argument (so you can provide a group and article number in two
+      # successive command-line arguments).
+      $args = $arg . " " . $ARGV[0];
+      ($group, $mid) = &parsearticle($args);
+      if (defined $mid) {
+        push @list, $args;
+        shift @ARGV; # and eat the second argument
+      } else {
+        # If all else fails, die in panic.
+        die "nntpid: argument '$arg': unable to parse\n";
+      }
+    }
+  }
 }
 
 $ns=$ENV{'NNTPSERVER'};
@@ -106,47 +129,96 @@ $ns = inet_aton($ns);
 $proto = getprotobyname("tcp");
 $paddr = sockaddr_in($port, $ns);
 
-socket(S,PF_INET,SOCK_STREAM,$proto) or die "socket: $!";
-connect(S,$paddr) or die "connect: $!";
+&connect;
+if ($mode eq 'all') {
+  # Write out the entire contents of a newsgroup in mbox format.
+  $numbers = &docmd("GROUP $group");
+  @numbers = split / /, $numbers;
+  $fatal = 0; # ignore failure to retrieve any given article
+  for ($mid = $numbers[1]; $mid <= $numbers[2]; $mid++) {
+    $art = &getart("$group:$mid");
+    $art =~ s/\n(>*From )/\n>$1/gs;
+    print "From nntpid ".(localtime)."\n".$art."\n";
+  }
+} elsif ($mode eq 'stdin') {
+  while (<>) {
+    chomp;
+    s/^\s+//; s/\s+$//; # trim whitespace
+    &displayarticle($_);
+  }
+} elsif ($mode eq 'list') {
+  for $item (@list) {
+    &displayarticle($item);
+  }
+}
+
+sub parsearticle {
+  # Article identifiers used as input to this program can be in a
+  # variety of formats. This function untangles one into a standard
+  # format, which is either (undef, message-id) or (group, article
+  # number). In case of parse failure, it returns (undef, undef).
+  my $art = shift @_;
+  if ($art =~ /^(.*<)?([^<>]*\@[^<>]*)(>.*)?$/) {
+    # Anything with an @ sign is treated as a Message-ID. We trim
+    # angle brackets and anything outside them.
+    return (undef, $2);
+  } elsif ($art =~ /^(\S+)(\s+|:)(\d+)$/) {
+    # A group name and article number separated by whitespace or a
+    # colon.
+    return ($1, $3);
+  } else {
+    # Unable to parse.
+    return (undef, undef);
+  }
+}
 
-S->autoflush(1);
+sub displayarticle {
+  my $mid = shift @_; 
 
-$fatal = 1; # most errors need to be fatal
+  &connect;
 
-&getline;
-$code =~ /^2\d\d/ or die "no initial greeting from server\n";
+  my $art = &getart($mid);
 
-&docmd("MODE READER");
-# some servers require a GROUP before an ARTICLE command
-$numbers = &docmd("GROUP $group");
-if ($all) {
-    @numbers = split / /, $numbers;
-    $fatal = 0; # ignore failure to retrieve any given article
-    for ($mid = $numbers[1]; $mid <= $numbers[2]; $mid++) {
-       $art = &getart($mid);
-       $art =~ s/\n(>*From )/\n>$1/gs;
-       print "From nntpid ".(localtime)."\n".$art."\n";
-    }
-} else {
-    $art = &getart($mid);
-    &docmd("QUIT");
-    close S;
-
-    if ($pager and -t STDOUT) {
-       $pagername = $ENV{"PAGER"};
-       $pagername = "more" unless defined $pagername;
-       open PAGER, "| $pagername";
-       print PAGER $art;
-       close PAGER;
-    } else {
-       print $art;
-    }
+  if ($pager and -t STDOUT) {
+    # Close the NNTP connection before invoking the pager, in case the
+    # user spends so long looking at the article that the server times
+    # us out.
+    &disconnect;
+
+    $pagername = $ENV{"PAGER"};
+    $pagername = "more" unless defined $pagername;
+    open PAGER, "| $pagername";
+    print PAGER $art;
+    close PAGER;
+  } else {
+    print $art;
+  }
 }
 
 sub getart {
-  my ($mid) = @_;
-  $ret = &docmd("ARTICLE $mid");
+  my $art = shift @_;
+  my $group;
+  my $mid;
+
+  ($group, $mid) = &parsearticle($art);
+  if (!defined $mid) {
+    warn "unable to parse '$art'\n";
+    return undef;
+  } elsif (defined $group) {
+    # This is a (group, article number) pair.
+    &docmd("GROUP $group");
+    $ret = &docmd("ARTICLE $mid");
+  } else {
+    # This is a Message-Id. Some NNTP servers will insist on having
+    # seen a GROUP command before 'ARTICLE <some.random@message.id>',
+    # so ensure we've sent one.
+    &docmd("GROUP misc.misc") unless $in_a_group;
+    $ret = &docmd("ARTICLE <$mid>");
+  }
+
   return undef if !defined $ret;
+  $in_a_group = 1;
+
   $art = "";
   while (1) {
     &getline;
@@ -172,6 +244,30 @@ sub getline {
   return substr($_,4);
 }
 
+sub connect {
+  return if $connected;
+  socket(S,PF_INET,SOCK_STREAM,$proto) or die "socket: $!";
+  connect(S,$paddr) or die "connect: $!";
+
+  S->autoflush(1);
+
+  $fatal = 1; # most errors need to be fatal
+
+  &getline;
+  $code =~ /^2\d\d/ or die "no initial greeting from server\n";
+
+  &docmd("MODE READER");
+
+  $connected = 1;
+  $in_a_group = 0;
+}
+
+sub disconnect {
+  &docmd("QUIT");
+  close S;
+  $connected = 0;
+}
+
 sub docmd {
   my ($cmd) = @_;
   # We go at most twice round the following loop. If the first attempt
index 934068d..ee6f026 100644 (file)
@@ -8,17 +8,17 @@
 
 \U SYNOPSIS
 
-\c nntpid [ -v ] [ -d ] message-id
-\e bbbbbb   bb     bb   iiiiiiiiii
-\c nntpid [ -v ] [ -d ] newsgroup-name article-number
-\e bbbbbb   bb     bb   iiiiiiiiiiiiii iiiiiiiiiiiiii
+\c nntpid [ -v ] [ -d ] article [ article... ]
+\e bbbbbb   bb     bb   iiiiiii   iiiiiii
+\c nntpid [ -v ] [ -d ]
+\e bbbbbb   bb     bb
 \c nntpid [ -v ] -a newsgroup-name
 \e bbbbbb   bb   bb iiiiiiiiiiiiii
 
 \U DESCRIPTION
 
 \cw{nntpid} makes a connection to a news server, retrieves one or
-more articles, and displays it.
+more articles, and displays them.
 
 You can specify the article you want by either:
 
@@ -40,9 +40,9 @@ that cause unexpected behaviour in your terminal. If \cw{nntpid}
 detects that its standard output is not a terminal, however, it will
 bypass the pager and just write out the article directly.
 
-There is a third mode of operation, enabled by the \cw{-a} option,
-in which \cw{nntpid} retrieves \e{all} available articles in the
-group and writes them to standard output in \cw{mbox} format.
+There is an alternative mode of operation, enabled by the \cw{-a}
+option, in which \cw{nntpid} retrieves \e{all} available articles in
+the group and writes them to standard output in \cw{mbox} format.
 
 The location of the news server is obtained by reading the
 environment variable \cw{NNTPSERVER}, or failing that the file
@@ -50,13 +50,40 @@ environment variable \cw{NNTPSERVER}, or failing that the file
 
 \U ARGUMENTS
 
-If you specify one argument, \cw{nntpid} assumes it is a Message-ID.
-The angle brackets that usually delimit Message-IDs are optional;
-\cw{nntpid} will strip them off if it sees them, and will not
-complain if it does not.
+\cw{nntpid} will attempt to interpret its argument list as specifying
+a series of news articles, as follows:
 
-If you specify two arguments, \cw{nntpid} will interpret the first
-as a newsgroup name, and the second as an article number.
+\b An argument containing an @ sign will be parsed as a Message-ID.
+The angle brackets that usually delimit Message-IDs are optional;
+\cw{nntpid} will strip them off if it sees them, and will not complain
+if it does not. If the angle brackets are present, anything outside
+them will also be discarded.
+
+\b Otherwise, an argument containing whitespace or a colon will be
+parsed as a group name and an article number.
+
+\b Otherwise, two successive arguments will be treated as a group name
+and an article number.
+
+For example, the following invocations should all behave identically.
+(Single quotes are intended to represent POSIX shell quoting, not part
+of the command line as it reaches \cw{nntpid}.)
+
+\c $ nntpid '<foo.bar@baz.quux>' misc.test 1234
+\e   bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
+\c $ nntpid 'foo.bar@baz.quux' misc.test:1234
+\e   bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
+\c $ nntpid 'wibble <foo.bar@baz.quux> blah' 'misc.test 1234'
+\e   bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
+
+If \cw{nntpid} is given no arguments at all, it will read from
+standard input. Every line it reads will be interpreted as described
+above, except that whitespace will also be trimmed from the start and
+end of the line first.
+
+If you provide the \cw{-a} option (see below), none of the above
+applies. Instead, \cw{nntpid} will expect exactly one command-line
+argument, which it will treat as a newsgroup name.
 
 \U OPTIONS