#!/usr/bin/perl # $Id$ # -*- perl -*- vim: ft=perl sw=4 sts=4 # For documentation, run perldoc blogcl use strict; use Net::MovableType; use Getopt::Long; use File::Temp (); use Term::Complete; use vars qw(%opts $mt); if (-r $ENV{HOME} . "/.blogclrc") { open CONFIG, $ENV{HOME} . "/.blogclrc"; while() { s/\#.*//; next unless m/^([^=]*)=(.*)/; $opts{$1}=$2; } close CONFIG; } Getopt::Long::Configure ("bundling", "auto_help"); die "Invalid arguments\n" unless GetOptions (\%opts, 'username|l=s', 'password|p=s', 'url|u=s', 'blogid|b=i', 'title|t=s', 'category|c=s', 'file|f=s'); sub init_mt { die "No url given.\n" unless(defined($opts{url})); die "No username given.\n" unless(defined($opts{username})); die "No passowrd given.\n" unless(defined($opts{password})); $mt = new Net::MovableType($opts{'url'}) || die "$!"; $mt->username($opts{'username'}); $mt->password($opts{'password'}); my $blogid = $opts{'blogid'} || $mt->resolveBlogId($opts{'username'}); unless($blogid) { my $userblogs=$mt->getUsersBlogs(); if($userblogs && $userblogs->[0]->{blogid}) { $blogid=$userblogs->[0]->{'blogid'}; } else { warn "Blog ID unknown.\n"; } } $mt->blogId($blogid); } sub print_recent { my $entries = $mt->getRecentPosts(shift || 10); while ( my $entry = shift @$entries ) { printf("[%02d] - %s\n", $entry->{postid}, $entry->{title} ); } # postid userid content title description dateCreated } sub print_categories { print join("\n",map {$_->{categoryName}} @{$mt->getCategoryList()}), "\n"; } sub delete_post { my $postid=shift; my $post = $mt->getPost($postid) or die "$!"; my $categories = $mt->getPostCategories($postid) or die "$!"; if(defined($opts{file})) { if($opts{file} eq "-") { print STDOUT $post->{description}, "\n"; } else { open FH, ">".$opts{file} or die "$!"; print $opts{file}, "\n"; print FH $post->{description}, "\n"; close FH; } } $mt->deletePost($postid,1); } sub show_post { my $postid=shift; my $post = $mt->getPost($postid) or die "$!"; my $categories = $mt->getPostCategories($postid) or die "$!"; if(defined($opts{file})) { if($opts{file} eq "-") { print STDOUT $post->{description}, "\n"; } else { open FH, ">".$opts{file} or die "$!"; print $opts{file}, "\n"; print FH $post->{description}, "\n"; close FH; } } else { printf("Title: %s (%d)\n", $post->{title}, $post->{postid}); if(@$categories>1) { print "Categories:"; } elsif (@$categories==1) { print "Category:"; } foreach(@$categories) { print " ", $_->{'categoryName'}; } print "\n" if(@$categories); print "Body:\n"; print $post->{description}, "\n"; } } sub edit_string { my $string = shift; if(defined($opts{file})) { open FILE, $opts{file} or die "$!"; chop($string = join("", )); close FILE; return $string; } my $editor = $ENV{VISUAL} || $ENV{EDITOR}; $editor ||= "/usr/bin/sensible-editor" if (-x "/usr/bin/sensible-editor"); $editor ||= "/bin/vi" if (-x "/bin/vi"); $editor ||= "/usr/bin/nano" if (-x "/usr/bin/nano"); my $tmp = new File::Temp(SUFFIX => '.html'); print $tmp $string, "\n"; close $tmp; my $mtime = (stat($tmp->filename))[9]; system(split (/ /, $editor), $tmp->filename); if($mtime == (stat($tmp->filename))[9]) { return undef; } else { open $tmp; chop($string = join("", <$tmp>)); close $tmp; return $string; } } sub new_post { my $title = $opts{title} || Complete('Title: '); my $category = $opts{category} || Complete('Categories: ', map {$_->{categoryName}} @{$mt->getCategoryList()}); die "No title given.\n" unless($title); my $description = edit_string(""); if(defined($description)) { my $postid = $mt->newPost({ 'title' => $title, 'description' => $description }); $mt->setPostCategories($postid, $category ? [split(/[, ]+/, $category)] : $category) || warn "$!" if(defined($category)); $mt->publishPost($postid); } else { die "No change. Aborting.\n"; } } sub edit_post { my $postid=shift; my $post = $mt->getPost($postid); my $categories = $mt->getPostCategories($postid); my $title = $opts{title} || Complete('Title: ', $post->{title}) || $post->{title}; my $description = edit_string($post->{description}); if(defined($description)) { $mt->editPost($postid, { 'title' => $title, 'description' => $description }); $mt->setPostCategories($postid, $opts{category} ? [split(/[, ]/, $opts{category})] : $opts{category}) || warn "$!" if(defined($opts{'category'})); $mt->publishPost($postid); } else { die "No change. Aborting.\n"; } } if ($ARGV[0] eq "help") { Getopt::Long::HelpMessage(); } elsif ($ARGV[0] eq "categories") { init_mt(); print_categories(); } elsif ($ARGV[0] eq "list" && $ARGV[1] =~ /^\d*$/) { init_mt(); print_recent($ARGV[1]); } elsif ($ARGV[0] eq "new") { init_mt(); new_post(); } elsif ($ARGV[0] eq "edit" && $ARGV[1] =~ /^\d+$/) { init_mt(); edit_post($ARGV[1]) } elsif ($ARGV[0] eq "show" && $ARGV[1] =~ /^\d+$/) { init_mt(); show_post($ARGV[1]) } elsif ($ARGV[0] eq "delete" && $ARGV[1] =~ /^\d+$/) { init_mt(); delete_post($ARGV[1]) } else { Getopt::Long::HelpMessage(); exit(1); } __END__ =head1 NAME blogcl - Blog from the command-line =head1 SYNOPSIS B S<[ B<--title>|B<-t> "Title" ]> S<[ B<--category>|B<-c> category[,...] ]> S<[ B<--file>|B<-f> F ]> S<[ B<--username>|B<-l> username ]> S<[ B<--password>|B<-p> password ]> S<[ B<--url>|B<-u> url ]> S<[ B<--blogid>|B<-b> blogid ]> { B | B postid | B postid | B postid | B [count] | B } =head1 DESCRIPTION This is a simple script used to blog from the command-line. It was designed for use with Drupal, but may work with any blog featuring a Movable Type compatible interface. One of the commands below must be present. =over 4 =item B Creates a new blog entry. =item B Edit an existing blog entry. An existing postid must be specified. To find a post's postid, use B. =item B Shows the contents of the post specified by the postid given on the command-line. =item B Deletes a post. Specify a B<--file> option to back up the contents (but not the title or category) of the post. =item B List recents posts and their postids. An optional integer specifies the number of posts to list. =item B Gives categories that are valid parameters to the B<--category> option. =item B Show usage information. =back =head1 OPTIONS =over 4 =item B<-b>, B<--blogid> Specify a blog ID. Defaults to the first blog found. =item B<-c>, B<--category> Specify a category for a new or existing entry. For multiple categories, seperate them with a comma. Ignored except when used with either B or B. =item B<-f>, B<--file> When used with B or B, specifies a file from which to read the post's body. When used with B, specifies a file to which to write the post's body. =item B<-l>, B<--username> Specify a username. =item B<-p>, B<--password> Specify a password. =item B<-t>, B<--title> Specify a title for a new or existing entry. Ignored except when used with either B or B. =item B<-u>, B<--url> Specify the URL of the XMLRPC interface. =back =head1 FILES =over 4 =item F<~/.blogclrc> Contains options of format parameter=value. Only the long form of options may be used. Omit leading hyphens. =back =head1 SEE ALSO L =head1 BUGS Error checking is minimal. =head1 COPYRIGHT Copyright by Tim Pope. All rights reserved. This program is free software; you may redistribute it and/or modify it under the same terms as perl itself. =head1 AUTHORS Tim Pope, Eperl@relongto.usE. L =cut