aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xsbodeps133
-rwxr-xr-xsbodl88
-rwxr-xr-xsbolint1095
-rwxr-xr-xsbosearch516
-rwxr-xr-xsbosubmit124
5 files changed, 1956 insertions, 0 deletions
diff --git a/sbodeps b/sbodeps
new file mode 100755
index 0000000..9eeb692
--- /dev/null
+++ b/sbodeps
@@ -0,0 +1,133 @@
+#!/bin/bash
+
+# This really is a bash script, please don't change to #!/bin/sh
+
+SBOROOT=${SBOROOT:-/home/urchlay/slackbuilds/sbo/14.1/}
+if [ ! -d $SBOROOT ]; then
+ SBOROOT=.
+fi
+
+SELF="$( echo "$0" | sed 's,.*/,,' )"
+
+usage() {
+ cat <<EOF
+$SELF: print dependency tree of SBo builds
+B. Watson, 20140911. Licensed under WTFPL: Do WTF you want with this.
+
+Usage: $SELF [-i] [-q] package [package ...]
+
+-i Ignore installed packages (treat everything as not installed).
+-q Output a sbopkg queue file (sqf). With -i, this will be a complete
+ queue, with all deps listed. Without -i, the queue file won't contain
+ packages that are already installed (which might mean no output at
+ all).
+
+A local copy of the slackbuilds.org repository is required, created
+with e.g. rsync. By default, this script assumes the current directory
+is a mirror of the Slackware version you're running, but you can set
+SBOROOT in the environment to use a different location. Example:
+
+export SBOROOT=/home/bob/sbomirror/14.1
+
+The SBOROOT (or current dir) needs to contain the category directories
+(academic, accessibility, audio, etc etc).
+EOF
+}
+
+print_installed_status() {
+ if [ "$IGNORE" = "yes" ]; then
+ echo
+ return
+ fi
+ pkg_is_installed $1 >/dev/null && echo " (installed)" || echo " (NOT installed)"
+}
+
+find_deps() {
+ local i
+ local req
+ local REQUIRES
+ local info=$(/bin/ls $SBOROOT/*/$2/$2.info 2>/dev/null | head -1)
+
+ if [ -z "$info" ]; then
+ echo "$SELF: $2: no such package" 1>&2
+ RETVAL=1
+ return;
+ fi
+
+ eval $(grep ^REQUIRES= $info)
+ if [ "$REQUIRES" = "" ]; then
+ return
+ fi
+ for req in $REQUIRES; do
+ for i in $(seq 1 $1 ); do
+ echo -n " "
+ done
+ echo -n $req
+ if [ "$req" = "%README%" ]; then
+ echo
+ else
+ print_installed_status $req
+ if [ "$QUEUE" = "yes" ]; then
+ if [ "$IGNORE" = "yes" ] || ! pkg_is_installed $req ; then
+ echo $1 $req >> $QTMPFILE
+ fi
+ fi
+ find_deps $(( $1 + 1 )) $req
+ fi
+ done
+}
+
+while [ "$argsdone" != "yes" ]; do
+ case "$1" in
+ ''|-h|-help|--help) usage; exit 0 ;;
+ -iq|-qi) QUEUE=yes ; IGNORE=yes ; shift ;;
+ -q) QUEUE=yes ; shift ;;
+ -i) IGNORE=yes ; shift ;;
+ -*) echo "Unknown option $1"; usage; exit 1 ;;
+ *) argsdone=yes ;;
+ esac
+done
+
+if [ "$QUEUE" = "yes" ]; then
+ QTMPFILE=${TMP:-/tmp}/sbodeps.$$.$RANDOM
+ rm -f $QTMPFILE
+
+ # save old stdout to fd 3, suppress stdout since we don't print our tree
+ exec 3>&1 1>/dev/null
+fi
+
+if [ ! -e "$SBOROOT/system" ]; then
+ echo "$SELF: path \"$SBOROOT\" doesn't look like an SBo repo, set SBOROOT in environment" 1>&2
+ exit 1
+fi
+
+RETVAL=0
+
+for arg; do
+ echo -n $arg
+ print_installed_status $arg
+ [ "$QUEUE" = "yes" ] && ! pkg_is_installed $arg && echo 0 $arg >> $QTMPFILE
+ find_deps 1 $arg
+done
+
+# The queue temp file contains the same info as the regular
+# tree, though less human-readable (depth numbers instead of indents).
+
+# To turn it into a .sqf, sort it depth-first, and remove any duplicates
+# (can't use sort -u or uniq, as dups might occur at different depths &
+# thus not be on consecutive lines).
+
+if [ "$QUEUE" = "yes" -a -e "$QTMPFILE" ]; then
+ exec 1>&3 # restore old stdout
+ for q in $(sort -nr $QTMPFILE | cut -d' ' -f2); do
+ r=seen_$(echo $q | sed 's,[^A-Za-z0-9_],_,g')
+ s=${!r} # bash indirect variable ref, yay
+ if [ "$s" != "yes" ]; then
+ echo $q
+ eval $r="yes"
+ fi
+ done
+ rm -f $QTMPFILE
+fi
+
+exit $RETVAL
diff --git a/sbodl b/sbodl
new file mode 100755
index 0000000..724733d
--- /dev/null
+++ b/sbodl
@@ -0,0 +1,88 @@
+#!/bin/bash
+
+# sbodl, initial public release.
+
+SELF=$( basename $0 )
+
+usage() {
+ cat <<EOF
+$SELF - download the sources for a slackbuilds.org build.
+version 20140421, public release
+(c) 2014 B. Watson (yalhcru at gmail dawt cawm)
+Licensed under the WTFPL: Do WTF you want with this.
+
+Usage: $SELF <wget-options>
+
+Execute $SELF in the directory that contains the .info and .SlackBuild
+files. It will use wget to download the source file(s), then check their
+md5sums.
+
+$SELF doesn't take any options itself (except --help), but any options
+you pass to it will be passed to wget.
+EOF
+ exit 0
+}
+
+die() {
+ echo "$SELF: $*" 2>&1
+ exit 1
+}
+
+# check for our one argument
+case "$*" in
+ -h|-help|-\?|--help) usage ;;
+esac
+
+source ./$( basename $( pwd ) ).info \
+ || die "No .info file, are you sure this is a SBo directory? Try '$SELF --help'"
+
+# This stanza copied from the SBo template for 14.1:
+if [ -z "$ARCH" ]; then
+ case "$( uname -m )" in
+ i?86) ARCH=i486 ;;
+ arm*) ARCH=arm ;;
+ *) ARCH=$( uname -m ) ;;
+ esac
+fi
+
+if [ "$ARCH" = "x86_64" ]; then
+ DL=${DOWNLOAD_x86_64:-$DOWNLOAD}
+ SUM=${MD5SUM_x86_64:-$MD5SUM}
+else
+ DL=$DOWNLOAD
+ SUM=${MD5SUM}
+fi
+
+if [ -z "$DL" ]; then
+ die "Bad .info file (no DOWNLOAD= or DOWNLOAD_x86_64=)."
+fi
+
+# save passed-in command line args for use with wget
+WGETARGS="$@"
+
+set $SUM
+
+for dl in $DL; do
+ EXTRAWGETARGS="--content-disposition "
+ case "$dl" in
+ *sourceforge.net/*|*.sf.net/*) EXTRAWGETARGS="--user-agent wget" ;;
+ *) ;;
+ esac
+
+ FILE=$( echo "$dl" | sed 's,.*/,,' )
+
+ wget $WGETARGS $EXTRAWGETARGS "$dl" || die "Download failed"
+
+ if [ -e "$FILE" ]; then
+ GOTSUM="$( md5sum "$FILE" | cut -d' ' -f1 )"
+ if [ "$1" != "$GOTSUM" ]; then
+ echo "WARN: md5sum doesn't match, expected $1, got $GOTSUM"
+ else
+ echo "md5sum matches OK: $1"
+ fi
+ else
+ echo "WARN: can't find downloaded file $FILE"
+ fi
+ echo
+ shift
+done
diff --git a/sbolint b/sbolint
new file mode 100755
index 0000000..0b9c794
--- /dev/null
+++ b/sbolint
@@ -0,0 +1,1095 @@
+#!/usr/bin/perl -w
+
+# sbolint, 20141114 bkw
+
+$VERSION="0.1";
+
+# generate man page with:
+# pod2man --stderr -r0.1 -s1 -c"SBo Maintainer Tools" sbolint > sbolint.1
+
+# This script is meant to be fairly self-contained, prefer not to
+# require a huge pile of perl module dependencies. In some cases this
+# means using system() or backticks or such (e.g. to run tar, instead of
+# using Archive::Tar). Please don't "improve" the script by using a ton
+# of modules. The POSIX module ships with perl, not afraid of using that.
+
+# future options:
+# -l list packages with errs/warnings, don't give details
+# possibly some way to selectively disable the checks (does anyone
+# really need this?)
+
+# future ideas for checks:
+# - REQUIRES= packages have to exist? annoying if you're working on a batch
+# of stuff to be submitted together.
+# - Validate images, e.g. icon.png or .xpm or such. ImageMagick's identify
+# command can tell a non-image or a wrong-format image (a .jpg filename
+# that's actually a PNG image), but it doesn't detect truncated images.
+# Also we have to parse its stdout/stderr, it returns 0.
+
+=pod
+
+=head1 NAME
+
+sbolint - check SlackBuild directories or tarballs for common errors.
+
+=head1 SYNOPSIS
+
+B<sbolint> [-q] [-u] [-n] [build [build ...]]
+
+=head1 DESCRIPTION
+
+sbolint checks for common errors in SlackBuilds.org scripts. It's
+intended for slackbuild authors and maintainers, and can cut down on
+"There was a problem with your upload" errors from the submission form.
+
+The [build] arguments must be either directories or tarballs, each
+containing a SlackBuild script, slack-desc, README, and .info file.
+With no [build] arguments, the current directory is checked.
+
+sbolint will flag errors for problems that would prevent the build from
+being accepted by the upload form (or by the SBo admins, if if passes
+the upload checks). There may also be warnings, which are things that
+(probably) won't stop your build from being accepted, but may cause the
+SBo admins extra work.
+
+sbolint was not written by the SlackBuilds.org team, and shares no code
+with the upload form's submission checker. Lack of errors/warnings from
+sbolint does not guarantee that your build will be accepted!
+
+sbolint doesn't check built packages, and never executes the build
+script. If you want a lint tool for binary Slackware packages, try
+pprkut's B<lintpkg>.
+
+=head1 OPTIONS
+
+=over 4
+
+=item B<-q>
+
+Quiet. Suppresses 'xxx checks out OK' and the total errors/warnings summary.
+
+=item B<-u>
+
+URL check. Uses B<curl> to make HTTP HEAD requests for the B<HOMEPAGE>,
+B<DOWNLOAD>, and B<DOWNLOAD_x86_64> links. This won't guarantee that
+the links are good, but failure means they're definitely bad.
+
+=item B<-n>
+
+Suppress warnings. Only errors will be listed. This also affects the
+exit status (see below).
+
+=back
+
+=head1 CHECKS
+
+For tar files only:
+
+=over 4
+
+=item -
+
+File size must not be bigger than the upload form's limit (currently one
+megabyte).
+
+=item -
+
+File must be a tar archive, possibly compressed with gzip, bzip2, or xz,
+extractable by the B<tar>(1) command.
+
+=item -
+
+Filename extension must match compression type.
+
+=item -
+
+Archive must contain a directory with the same name as the archive's base name,
+e.g. I<foo.tar.gz> must contain I<foo/>. Everything else in the archive must be
+inside this directory.
+
+=item -
+
+Archive must contain I<dirname/Idirname.SlackBuild>.
+
+=back
+
+For all builds:
+
+=over 4
+
+=item -
+
+The SlackBuild script must exist, with mode 0755, and be a I<#!/bin/sh>
+script.
+
+=item -
+
+The script must contain the standard variable assignements for PRGNAM,
+VERSION, BUILD, and TAG.
+
+=item -
+
+I<PRGNAM> in the script must match I<PRGNAM> in the .info file. Both must
+match the script name (I<PRGNAM.SlackBuild>) and the directory name.
+
+=item -
+
+I<VERSION> must match the I<VERSION> in the .info file.
+
+=item -
+
+TAG=${TAG:-_SBo} must occur in the script.
+
+=item -
+
+The I<VERSION> and I<BUILD> variables must respect the environment.
+
+=item -
+
+The script must install the slack-desc in $PKG/install.
+
+=item -
+
+If there is a doinst.sh script, the SlackBuild must install it to I<$PKG/install>.
+
+=item -
+
+Template boilerplate comments should be removed, e.g. I<"REMOVE THIS ENTIRE BLOCK OF TEXT">
+or I<"Automatically determine the architecture we're building on">.
+
+=item -
+
+Script must contain a B<makepkg> command.
+
+=item -
+
+README must exist and have mode 0644.
+
+=item -
+
+slack-desc must exist and have mode 0644.
+
+=item -
+
+slack-desc contents must match the SBo template, including the "handy-ruler",
+comments, and correct spacing/indentation.
+
+=item -
+
+.info file must exist, have mode 0644, and match the SBo template.
+
+=item -
+
+.info file URLs must be valid URLs (for a very loose definition of "valid": they
+must begin with B<ftp://>, B<http://>, or B<https://>).
+
+=item -
+
+Optionally, .info file URLs can be checked for existence with an HTTP HEAD
+request (see the B<-u> option).
+
+=item -
+
+Any files other than the .SlackBuild, .info, slack-desc, and README are
+checked for permissions (should be 0644) and excessive size.
+
+=item -
+
+The source archive(s) must not exist. Also sbolint attempts to detect
+extracted source trees (but isn't all that good at it).
+
+=back
+
+=head1 EXIT STATUS
+
+Exit status from sbolint will normally be 0 (success) if there were no
+errors or warnings in any of the builds checked. With the B<-n> option,
+exit status will be 0 if there are no errors.
+
+Exit status 1 indicates there was at least one warning or error (or, with
+B<-n>, at least one error).
+
+Any other exit status means sbolint itself failed somehow (e.g. called
+with nonexistent filename).
+
+=head1 BUGS
+
+Probably quite a few. Watch this space for details.
+
+=head1 AUTHOR
+
+B. Watson (yalhcru at gmail dot com, or Urchlay on Freenode IRC)
+
+=head1 SEE ALSO
+
+B<sbofixinfo>(1), B<sbosearch>(1)
+
+=cut
+
+use POSIX qw/getcwd/;
+
+@boilerplate = (
+ qr/#\s*REMOVE THIS ENTIRE BLOCK OF TEXT/,
+ qr/#\s*replace with (?:version:name) of program/,
+ qr/#\s*the "_SBo" is required/,
+ qr/#\s*Automatically determine the architecture we're building on/,
+ qr/#\s*Unless \$ARCH is already set,/,
+ qr/#\s*For consistency's sake, use this/,
+ qr/#\s*Drop the package in \/tmp/,
+ qr/#\s*Exit on most errors/,
+ qr/#\s*If you prefer to do selective error checking with/,
+ qr/#\s*Your application will probably need/,
+ qr/#\s*Compile the application and install it into the/,
+ qr/#\s*Strip binaries and libraries - this can be done with/,
+ qr/#\s*Compress man pages$/,
+ qr/#\s*Compress info pages and remove the/,
+ qr/#\s*Remove perllocal.pod and other special files/,
+ qr/#\s*Copy program documentation into the package/,
+ qr/#\s*Copy the slack-desc \(and a custom doinst\.sh if necessary\)/,
+ qr/#\s*Make the package; be sure to leave it in/,
+);
+
+# this was scraped from the HTML source for the upload form:
+$MAX_TARBALL_SIZE = 1048576;
+
+($SELF = $0) =~ s,.*/,,;
+
+$buildname = $build = "";
+$g_warncount = 0;
+$g_errcount = 0;
+$warncount = 0;
+$errcount = 0;
+
+$tempdir = 0;
+
+our %info = (); # has to be global, check_info sets it, check_script needs it
+
+# main() {
+
+while(@ARGV && ($ARGV[0] =~ /^-/)) {
+ my $opt = shift;
+ $opt =~ /^-u/ && do { $url_head = 1; next; };
+ $opt =~ /^-d/ && do { $url_download = 1; next; };
+ $opt =~ /^--?q(uiet)?/ && do { $quiet = 1; next; };
+ $opt =~ /^-$/ && do { $stdin = 1; next; };
+ $opt =~ /^--?h(elp)?/ && do { usage(); exit 0; };
+ $opt =~ /^-n$/ && do { $nowarn = 1; next; };
+ die_usage("Unrecognized option '$opt'");
+}
+
+if($url_head && $url_download) {
+ die_usage("-u and -d options are mutually exclusive");
+}
+
+if($url_head || $url_download) {
+ if(system("curl --version > /dev/null") != 0) {
+ die "$SELF: -u and -d options require curl, can't find it in your \$PATH.\n";
+ }
+}
+
+if($stdin) {
+ @ARGV = <STDIN>;
+ chomp for @ARGV;
+}
+
+push @ARGV, "." unless @ARGV;
+
+for(@ARGV) {
+ run_checks($_);
+ $g_errcount += $errcount;
+ $g_warncount += $warncount;
+
+ if($errcount == 0 and $warncount == 0) {
+ print "$buildname checks out OK\n" unless $quiet;
+ } else {
+ print "$buildname: errors $errcount, warnings $warncount\n";
+ }
+}
+
+# print total errs/warns only if >1 build checked
+if(!$quiet && @ARGV > 1) {
+ print "Total errors: $g_errcount\n";
+ print "Total warnings: $g_warncount\n" unless $nowarn;
+}
+
+exit ($g_errcount > 0 || (!$nowarn && $g_warncount > 0));
+# }
+
+sub logmsg {
+ my $severity = shift;
+ my $format = shift;
+ printf("$buildname: $severity: $format\n", @_);
+}
+
+sub log_error {
+ logmsg("ERR", @_);
+ $errcount++;
+}
+
+sub log_warning {
+ return if $nowarn;
+ logmsg("WARN", @_);
+ $warncount++;
+}
+
+sub usage {
+ if(@_) {
+ warn "$SELF: $_\n" for @_;
+ }
+
+ warn <<EOF;
+
+$SELF - check SlackBuilds.org scripts for common problems.
+
+Usage: $SELF [-q] [-u] [-n] <build <build ...>>
+
+builds may be directories or tarballs. If no build arguments given,
+. (current directory) is assumed. Use - to read a list of tarballs/dirs
+from stdin.
+
+Options:
+
+-q Quiet: only emit errors/warnings, no 'checks out OK' or totals.
+-u URL Check: use HTTP HEAD request to verify download/homepage URLs exist.
+-n Suppress warnings, log only errors.
+
+See the man page for more details. If you don't have the man page, you
+can generate it with:
+
+pod2man --stderr -s1 -cSBoStuff -r$VERSION /path/to/sbolint > sbolint.1
+EOF
+# not yet:
+#-d URL Download: as -u, plus download & check md5sums of download URLs.
+}
+
+sub die_usage {
+ usage(@_);
+ exit 1;
+}
+
+sub chdir_or_die {
+ chdir($_[0]) or die "$SELF: chdir($_[0]): $!\n";
+}
+
+sub make_temp_dir {
+ return if $tempdir;
+ my $tmp = $ENV{TMP} || "/tmp";
+ $tempdir = "$tmp/$SELF." . int(rand(2**32-1));
+ system("rm -rf $tempdir");
+ system("mkdir -p $tempdir");
+ if(! -d $tempdir) {
+ die "$SELF: can't create temp dir $tempdir\n";
+ }
+}
+
+sub rm_temp_dir {
+ if($tempdir && (-d $tempdir)) {
+ system("rm -rf $tempdir");
+ $tempdir = 0;
+ }
+}
+
+sub check_tarball_mime {
+ my $file = shift;
+
+ ### This stuff is a little pedantic. It also relies on having a recent-ish
+ ### version of GNU file (the one in Slack 14.1 works fine).
+ my %types = (
+ 'tar' => 'application/x-tar',
+ 'tar.gz' => 'application/x-gzip',
+ 'tar.bz2' => 'application/x-bzip2',
+ 'tar.xz' => 'application/x-xz',
+ );
+
+ (my $basename = $file) =~ s,.*/,,;
+ my (undef, $ext) = split /\./, $basename, 2;
+ my $mime = `file --brief --mime-type $file`;
+ chomp $mime;
+
+ if(!grep { $_ eq $mime } values %types) {
+ log_error("$file is not a tarball (mime type is '$mime')");
+ } elsif(!$ext) {
+ log_error("$file: filename has no extension (will be rejected by upload form)");
+ } elsif($types{$ext} ne $mime) {
+ log_error("$file mime type '$mime' doesn't match filename (should be $types{$ext})");
+ } elsif($ext ne 'tar') {
+ my $realmime = `file -z --brief --mime-type $file`;
+ chomp $realmime;
+ if($realmime ne 'application/x-tar') {
+ log_error("$file doesn't contain a tar archive (content mime type is $realmime, should be application/x-tar)");
+ }
+ }
+}
+
+sub check_tarball {
+ my $file = shift;
+
+ ### First, mime type checks. None of this will be fatal (no return 0 on error).
+ check_tarball_mime($file);
+
+ ### one more pre-extraction check:
+ if(-s "$file" > $MAX_TARBALL_SIZE) {
+ log_warning("$file is larger than $MAX_TARBALL_SIZE bytes, upload may be rejected");
+ }
+
+ ### now call tar to list the contents, and start returning 0 on failure.
+ my @list = split "\n", `tar tf $file`;
+ if($?) {
+ log_error("$file: tar failed to list contents");
+ return 0;
+ }
+
+ if(!@list) {
+ log_error("$file is empty archive?");
+ return 0;
+ }
+
+ if($list[0] ne "$buildname/") {
+ log_error("$file not a SBo-compliant tarball, first element should be '$buildname/', not '$list[0]'");
+ return 0;
+ }
+
+ my $foundsb = 0;
+ shift @list; # 1st element is dirname/, we already checked it
+ for(@list) {
+ my $bn = quotemeta($buildname); # some builds have + in the name
+ if(not /^$bn\//) {
+ log_error("$file not a SBo-compliant tarball, contains extra junk '$_'");
+ return 0;
+ }
+
+ if(/^$bn\/$bn.SlackBuild$/) {
+ $foundsb = 1;
+ }
+ }
+
+ if(not $foundsb) {
+ log_error("$file not a SBo-compliant tarball, doesn't contain '$buildname/$buildname.SlackBuild'");
+ return 0;
+ }
+
+ return 1;
+}
+
+sub extract_tarball {
+ my $file = shift;
+ $file = `readlink -n -e $file`;
+ make_temp_dir();
+ chdir_or_die($tempdir);
+ system("tar xf $file");
+ return "$tempdir/$buildname";
+}
+
+# run_checks will extract its argument (then cd to it) if it's a tarball,
+# otherwise cd to its argument if it's a dir, otherwise error.
+sub run_checks {
+ $build = shift;
+ my $oldcwd = getcwd();
+
+ $errcount = $warncount = 0;
+
+ if(-f $build || -l $build) {
+ ($buildname = $build) =~ s,\.tar(\..*)?$,,;
+ $buildname =~ s,.*/,,;
+ if(check_tarball($build)) {
+ chdir_or_die(extract_tarball($build));
+ } else {
+ return 0;
+ }
+ } elsif(-d $build) {
+ chdir_or_die($build);
+ } else {
+ die_usage "'$build' not a file or a directory.";
+ }
+
+ # last component of directory is the build name
+ $buildname = `readlink -n -e .`;
+ $buildname =~ s,.*/,,;
+
+ my @checks = (
+ \&check_readme,
+ \&check_slackdesc,
+ \&check_info,
+ \&check_script,
+ \&check_junkfiles,
+ );
+
+ for(@checks) {
+ $_->($build);
+ }
+
+ chdir_or_die($oldcwd);
+ rm_temp_dir();
+}
+
+sub check_mode {
+ my ($file, $wantmode) = @_;
+ if(! -e $file) {
+ log_error("$file does not exist");
+ return 0;
+ }
+
+ my $gotmode = 07777 & ((stat($file))[2]);
+ if($wantmode != $gotmode) {
+ log_error("$file should be mode %04o, not %04o", $wantmode, $gotmode);
+ return 0;
+ }
+
+ return 1;
+}
+
+sub check_crlf {
+ my $file = shift;
+ for(@_) {
+ if(/\r/) {
+ log_error("$file has DOS-style CRLF line endings");
+ return 0;
+ }
+ }
+ return 1;
+}
+
+sub check_and_read {
+ my ($file, $mode) = @_;
+
+ my $crlf_err;
+ my @lines;
+
+ check_mode($file, $mode);
+
+ if(open my $fh, "<$file") {
+ while(<$fh>) {
+ chomp;
+ $crlf_err = 1 if s/\r$//;
+ push @lines, $_;
+ }
+ if(scalar @lines == 0) {
+ log_error("$file exists but is empty");
+ }
+ }
+
+ log_error("$file has DOS-style CRLF line endings") if $crlf_err;
+ return @lines;
+}
+
+sub check_readme {
+ check_and_read("README", 0644);
+}
+
+# the slack-desc checking code offends me (the author), on the one hand it's
+# overly complex, and on the other hand it assumes the slack-desc is at
+# least close to being right...
+sub check_slackdesc {
+ my @lines = check_and_read("slack-desc", 0644);
+ return unless scalar @lines;
+
+ my $lineno = 1;
+
+ if($lines[0] =~ /^# HOW TO EDIT THIS FILE:$/) {
+ shift @lines;
+ $lineno++;
+ } else {
+ log_warning("slack-desc doesn't start with how-to-edit comment");
+ }
+
+ my $count = 0;
+ while($lines[0] =~ /^#/) {
+ $count++;
+ $lineno++;
+ shift @lines;
+ }
+
+ if($count != 5) {
+ log_warning("slack-desc doesn't have standard how-to-edit stanza");
+ }
+
+ $count = 0;
+ while($lines[0] eq "") {
+ $count++;
+ $lineno++;
+ shift @lines;
+ }
+
+ if($count == 0) {
+ log_warning("slack-desc missing blank line before handy-ruler");
+ } elsif($count > 1) {
+ log_warning("slack-desc has extra blank lines before handy-ruler");
+ }
+
+ if($lines[0] =~ /handy-ruler/) {
+ my $ruler = shift @lines;
+ $lineno++;
+ my ($spaces, $prefix, $hr, $suffix, $junk) = ($ruler =~ /^( *)(\|-+)(handy-ruler)(-+\|)(.*)$/);
+
+ if(length($spaces) != length($buildname)) {
+ log_error("slack-desc:$lineno: handy-ruler has wrong number of indent spaces (%d, should be %d)",
+ length($spaces),
+ length($buildname));
+ }
+
+ if(length($junk) > 0) {
+ log_error("slack-desc:$lineno: handy-ruler has %d characters of trailing junk after last |", length($junk));
+ }
+
+ my $rlen = length($prefix . $hr . $suffix);
+ if($rlen != 72) {
+ log_error("slack-desc:$lineno: handy-ruler must be 72 characters, not %d", $rlen);
+ } elsif(length($prefix) != 6) {
+ log_error("slack-desc:$lineno: handy-ruler malformed, has '$prefix' instead of '|-----'");
+ }
+ } else {
+ log_error("slack-desc missing handy-ruler");
+ }
+
+ $count = 0;
+ for(@lines) {
+ $count++;
+ if(my ($prefix, $text) = /^([^\s]+:)(.*)/) {
+ if($prefix ne "$buildname:") {
+ log_error("slack-desc:$lineno: wrong prefix '$prefix', should be '$buildname:'");
+ } elsif($text =~ /^\s+$/) {
+ log_error("slack-desc:$lineno: trailing whitespace after colon, on otherwise-blank line");
+ } elsif(length($text) > 72) {
+ log_error("slack-desc:$lineno: text too long, %d characters, should be <= 72", length($text));
+ } elsif(length($text) && $text !~ /^ /) {
+ log_error("slack-desc:$lineno: missing whitespace after colon, on non-blank line");
+ }
+
+ my $bn = quotemeta($buildname); # some builds have + in the name
+ if(($count == 1) && ($text !~ /^ $bn \(.+\)$/)) {
+ log_warning("slack-desc:$lineno: first description line should be '$buildname: $buildname (short desc)'");
+ }
+ } else {
+ log_error("slack-desc:$lineno: malformed line in description section");
+ }
+
+ $lineno++;
+ }
+
+ if($count < 11) {
+ log_error("slack-desc only has $count description lines, should be 11 (add some empties)");
+ } elsif($count > 11) {
+ log_error("slack-desc has too many description lines ($count, should be 11)");
+ }
+}
+
+# This is a damn mess. Needs refactoring badly.
+sub check_info {
+ my $file = $buildname . ".info";
+ my @lines = check_and_read($file, 0644);
+ return unless scalar @lines;
+
+ my $lineno = 0;
+ my $file_lineno = 0;
+ my @expected = qw/PRGNAM VERSION HOMEPAGE
+ DOWNLOAD MD5SUM
+ DOWNLOAD_x86_64 MD5SUM_x86_64
+ REQUIRES MAINTAINER EMAIL/;
+ my $next_exp = 0;
+ my @keys;
+ my $continuation = 0;
+
+ # parse and bitch about bad syntax...
+ for(@lines) {
+ $file_lineno++;
+ if($continuation) {
+ s/^\s*//;
+ $_ = "$continuation $_";
+ $continuation = 0;
+ $lineno = $file_lineno - 1;
+ } else {
+ $lineno = $file_lineno;
+ }
+
+ if(s/\s*\\$//) {
+ $continuation = $_;
+ next;
+ }
+
+ if(/^\s*$/) {
+ log_error("$file:$lineno: blank line (get rid of it)");
+ next;
+ }
+
+ unless(/=/) {
+ log_error("$file:$lineno: malformed line (no = sign, missing \\ on prev line?)");
+ next;
+ }
+
+ if(s/^\s+//) {
+ log_error("$file:$lineno: leading whitespace before key");
+ }
+
+ if(s/\s+$//) {
+ log_error("$file:$lineno: trailing whitespace at EOL");
+ }
+
+ if(my ($k, $s1, $s2, $q1, $val, $q2) = /^(\w+)(\s*)=(\s*)("?)(.*?)("?)$/) {
+ if(!grep { $k eq $_ } @expected) {
+ log_error("$file:$lineno: invalid key '$k'");
+ } else {
+ if($k ne $expected[$next_exp]) {
+ log_warning("$file:$lineno: out of order, expected $expected[$next_exp], got $k");
+ }
+ $next_exp++;
+ }
+
+ if(not $q1) {
+ log_error("$file:$lineno: missing opening double-quote");
+ }
+
+ if(not $q2) {
+ log_error("$file:$lineno: missing closing double-quote");
+ }
+
+ if(length($s1) || length($s2)) {
+ log_error("$file:$lineno: no spaces allowed before/after = sign");
+ }
+
+ my $oldval = $val;
+ if($val =~ s/^\s+//) {
+ log_error("$file:$lineno: leading space in value: \"$oldval\"");
+ }
+
+ if($val =~ s/\s+$//) {
+ log_error("$file:$lineno: trailing space in value: \"$oldval\"");
+ }
+
+ $info{$k} = $val;
+ } else {
+ log_error("$file:$lineno: malformed line");
+ }
+ }
+
+ # parsing done, now for semantic checks
+
+ my @missing;
+ for(@expected) {
+ if(not exists($info{$_})) {
+ push @missing, $_;
+ }
+ }
+
+ log_error("$file: missing required key(s): " . (join ", ", @missing)) if @missing;
+
+ # init this to avoid checking undef values below
+ $info{$_} ||= "" for @expected;
+
+ if($info{PRGNAM} && ($info{PRGNAM} ne $buildname)) {
+ log_error("$file: PRGNAM is '$info{PRGNAM}', should be '$buildname'");
+ }
+
+ if($info{VERSION} =~ /-/) {
+ log_error("$file: VERSION may not contain - (dash) characters");
+ }
+
+ if(!check_url($info{HOMEPAGE})) {
+ log_error("$file: HOMEPAGE=\"$info{HOMEPAGE}\" doesn't look like a valid URL (http, https, or ftp)");
+ }
+
+ # use a HEAD request for homepage, even if downloading other files
+ if($url_head || $url_download) {
+ curl_head_request($info{HOMEPAGE}) || do {
+ log_warning("$file: HOMEPAGE URL broken?");
+ };
+ }
+
+ if($info{MD5SUM} =~ /^\s*$/) {
+ log_error("$file: MD5SUM is missing or blank");
+ } else {
+ check_dl_and_md5($file, "");
+ }
+
+ my $dl64 = $info{DOWNLOAD_x86_64};
+ if($dl64 =~ /^(?:|UNSUPPORTED|UNTESTED)$/) {
+ if($info{MD5SUM_x86_64} ne "") {
+ log_error("$file: MD5SUM_x86_64 must be blank if DOWNLOAD_x86_64 is not set");
+ }
+ } elsif($info{MD5SUM_x86_64} eq "") {
+ log_error("$file: MD5SUM_x86_64 may not be blank if DOWNLOAD_x86_64 is set");
+ } else {
+ check_dl_and_md5($file, "_x86_64");
+ }
+}
+
+sub check_dl_and_md5 {
+ my($file, $suffix) = @_;
+ my $md5key = "MD5SUM" . $suffix;
+ my $dlkey = "DOWNLOAD" . $suffix;
+
+ my @dlurls = split /\s+/, $info{$dlkey};
+ my @md5s = split /\s+/, $info{$md5key};
+
+ if(@md5s != @dlurls) {
+ log_error("$file: we have " . @dlurls . " $dlkey URLs but " . @md5s . " $md5key" . " values");
+ }
+
+ for(@dlurls) {
+ if(!check_url($_)) {
+ log_error("$file: $dlkey URL '$_' doesn't look like a valid URL (http, https, or ftp)");
+ }
+
+ if($url_head) {
+ for(@dlurls) {
+ curl_head_request($_) || do {
+ log_warning("$file: $dlkey URL '$_' broken?");
+ };
+ }
+ } elsif($url_download) {
+ warn "$SELF: -d option not yet implemented\n";
+ }
+ }
+
+ for(@md5s) {
+ unless(/^[0-9a-f]{32}$/) {
+ log_error("$file: $md5key '$_' is invalid (must be 32 hex digits)");
+ }
+ }
+
+ # TODO: maybe actually download and check md5sums.
+}
+
+sub check_url {
+ # url is bad if:
+ return 0 if $_[0] =~ /\s/; # ...it contains a space,
+ return 0 if $_[0] !~ /\./; # ...it has no dots, or
+ return 0 if $_[0] !~ /\//; # ...it has no slashes, or
+ return ($_[0] =~ /^(?:ftp|https?):\/\//); # ...it doesn't have a known protocol,
+ # ...which doesn't necessarily mean it's a good URL either.
+}
+
+sub curl_head_request {
+ return !system("curl --head --location --silent --fail $_[0] >/dev/null");
+}
+
+# NOT going to police the script too much. Would end up rewriting most of
+# the shell, in perl. Plus, it'd become a straitjacket. Here's what I'll
+# implement:
+# - #!/bin/sh on line 1
+# - PRGNAM must match $buildname
+# - VERSION must match the .info VERSION
+# - BUILD line must be present
+# - TAG line must be present
+# - If VERSION, BUILD, TAG don't respect the env, it's a warning
+# - Check for strings like slack-desc, $PKG/install, makepkg, stuff
+# that's standard for SBo. Don't be too specific here.
+# - If there's a doinst.sh, it must mentioned in the script. If not,
+# it better not be mentioned.
+# - Check for leftover boilerplate
+# - cp -a <documentation> is an error
+
+sub check_script {
+ my $file = $buildname . ".SlackBuild";
+ my @lines = check_and_read($file, 0755);
+ return unless scalar @lines;
+
+ if($lines[0] !~ /^#/) {
+ log_error("$file:1: missing or invalid shebang line (should be '#!/bin/sh')");
+ } elsif($lines[0] ne "#!/bin/sh") {
+ log_warning("$file:1: shebang line should be #!/bin/sh (admins always change it to that anyway)");
+ }
+
+ my $lineno = 0;
+ my ($prgnam, $version, $build, $tag, $need_doinst, $slackdesc, $makepkg, $install);
+ for(@lines) {
+ $lineno++;
+ if(/^PRGNAM=(\S+)/) {
+ $prgnam = $1;
+ if($1 ne $buildname) {
+ log_error("$file:$lineno: PRGNAM doesn't match dir name ($1 != $buildname)");
+ }
+ } elsif(/^VERSION=(\S+)/) {
+ $version = $1;
+ $version =~ s/^["']|["']$//g;
+ if(not ($version =~ s/\$\{VERSION:-([^}]+)\}/$1/)) {
+ log_warning("$file:$lineno: VERSION ignores environment (try VERSION=\${VERSION:-$version}");
+ }
+ if($version ne $info{VERSION}) {
+ log_error("$file:$lineno: VERSION ($version) doesn't match VERSION in the .info file ($info{VERSION})");
+ }
+ } elsif(/^BUILD=(\S+)/) {
+ $build = $1;
+ $build =~ s/^["']|["']$//g;
+ if(not ($build =~ /\d/)) {
+ log_error("$file:$lineno: BUILD is non-numeric");
+ } elsif(not ($build =~ /\$\{BUILD:-\d+}/)) {
+ log_warning("$file:$lineno: BUILD ignores environment (try BUILD=\${BUILD:-$build}");
+ }
+ } elsif(/^TAG=(\S+)/) {
+ $tag = $1;
+ if($tag ne '${TAG:-_SBo}') {
+ log_error("$file:$lineno: TAG=\${TAG:-_SBo} is required");
+ }
+ } elsif(/^\s*cat\s+\$CWD\/doinst.sh/) {
+ $need_doinst = $lineno;
+ } elsif(/^[^#]*slack-desc/) {
+ $slackdesc = $lineno;
+ } elsif(/^[^#]*\$PKG\/install/) {
+ $install = $lineno;
+ } elsif(/^[^#]*makepkg/) {
+ $makepkg = $lineno;
+ }
+
+ if(/[^#]*<documentation>/) {
+ log_error("$file:$lineno: copy actual documentation, not <documentation>");
+ }
+
+ my $line = $_;
+ if(grep { $line =~ /$_/ } @boilerplate) {
+ log_error("$file:$lineno: template comment should be removed");
+ }
+ }
+
+ if(not defined($prgnam)) {
+ log_error("$file: no PRGNAM= line");
+ }
+
+ if(not defined($version)) {
+ log_error("$file: no VERSION= line");
+ }
+
+ if(not defined($build)) {
+ log_error("$file: no BUILD= line");
+ }
+
+ if(not defined($tag)) {
+ log_error("$file: no TAG= line");
+ }
+
+ if(not defined($slackdesc)) {
+ log_error("$file: doesn't seem to install slack-desc in \$PKG/install");
+ }
+
+ if(not defined($makepkg)) {
+ log_error("$file: no makepkg command found");
+ }
+
+ if(not defined($install)) {
+ log_error("$file: nothing gets installed in \$PKG/install");
+ }
+
+ my $have_doinst = (-f "doinst.sh");
+ if($have_doinst) {
+ check_and_read("doinst.sh", 0644);
+ }
+ if($need_doinst && !$have_doinst) {
+ log_error("$file:$need_doinst: script installs doinst.sh, but it doesn't exist");
+ } elsif($have_doinst && !$need_doinst) {
+ log_error("$file: doinst.sh exists, but the script doesn't install it");
+ }
+}
+
+# stuff like editor backups and dangling symlinks.
+# maybe *any* symlinks?
+# ELF objects are bad, too.
+# Big-ass files...
+# directories are OK, but hidden dirs are not.
+sub check_junkfiles {
+ my @sources = split(/\s+/, $info{DOWNLOAD} . " " . $info{DOWNLOAD_x86_64});
+ s,.*/,, for @sources;
+ @sources = grep { $_ !~ /^(?:\s*|UNTESTED|UNSUPPORTED)$/ } @sources;
+ if(!grep { $_ =~ /^v$info{VERSION}\./ } @sources) {
+ push @sources, "v$info{VERSION}.$_" for qw /zip tar.gz tar.bz2 tar.xz/;
+ }
+
+ open my $fh, "-|", "find . ! -type d -print0 | xargs -0 file --mime-type";
+ FILE: while(<$fh>) {
+ chomp;
+ my ($file, $type) = split /: */, $_, 2;
+ $file =~ s,\./,,;
+
+ # skip the files caught by other checks
+ next if $file eq "$buildname.SlackBuild";
+ next if $file eq "$buildname.info";
+ next if $file eq "README";
+ next if $file eq "slack-desc";
+ next if $file =~ /(?:diff|patch)$/;
+
+ check_mode($file, 0644);
+
+ if(grep { $_ eq $file } @sources) {
+ log_error("source archive found: $file");
+ next FILE;
+ }
+
+ for($file) {
+ (/\.swp\w*$/ || /#/ || /~/ ) && do {
+ log_error("editor backup found: $file");
+ next FILE;
+ };
+ /^\./ && do {
+ log_error("hidden file found: $file");
+ next FILE;
+ };
+ /\.(?:orig|bak|old)[^.]*$/ && do {
+ log_warning("$file looks like sort some of backup file");
+ next FILE;
+ };
+ /\.desktop$/ && do {
+ system("desktop-file-validate $file");
+ if($? != 0) {
+ log_warning("$file fails desktop-file-validate");
+ next FILE;
+ }
+ }
+ }
+
+ for($type) {
+ ($_ eq "inode/x-empty") && do {
+ log_error("$file is empty (0 bytes long)");
+ next FILE;
+ };
+ ($_ =~ /^inode/) && do {
+ log_error("$file is $type, not a regular file or directory");
+ next FILE;
+ };
+ ($_ =~ m,application/x-(?:executable|dosexec|object|coredump),) && do {
+ log_error("$file is object code ($type)");
+ next FILE;
+ };
+ }
+
+ my $size = -s $file;
+ if($size > 1024 * 100) {
+ log_warning("$file is large ($size bytes), may be rejected by submission form");
+ }
+ }
+ close $fh;
+
+ open $fh, "-|", "find . -type d -mindepth 1";
+ while(<$fh>) {
+ chomp;
+ s,\./,,;
+
+ if(/^\./) {
+ log_error("found hidden directory: $_");
+ next;
+ }
+
+ if(glob("$_/*.o")) {
+ log_error("$_ contains compiled object files (leftover source tree?)");
+ next;
+ }
+
+ for my $badfile (qw/Makefile configure CmakeLists.txt makefile.pl SConstruct/) {
+ if(-f "$_/$badfile") {
+ log_error("$_ looks like extracted source tree (contains $badfile)");
+ }
+ }
+ }
+ close $fh;
+
+# # this won't always catch everything (e.g. PRGNAM=foo VERSION=1, but the
+# # extracted dir is foo1 or foo_1 or foo-source-1).
+# if(-d "$buildname-$version") {
+# log_warning("$buildname-$version/ looks like extracted source dir");
+# }
+}
+
+# if anything *.diff or *.patch contains \r, warn the
+# user about git stripping the \r's (better gzip it).
+sub check_patches {
+ for(<*.diff>,<*.patch>) {
+ check_and_read($_, 0644);
+ }
+}
diff --git a/sbosearch b/sbosearch
new file mode 100755
index 0000000..e4b48d9
--- /dev/null
+++ b/sbosearch
@@ -0,0 +1,516 @@
+#!/bin/bash
+
+# This really is a bash script, uses bashisms, don't change the shebang.
+
+DFLTBROWSER=xdg-open
+
+SLACKVER=${SLACKVER:-$( cut -d' ' -f2 /etc/slackware-version )}
+SLACKVER=${SLACKVER:-14.1}
+
+SBOROOT=${SBOROOT:-/var/lib/sbopkg/SBo/$SLACKVER}
+[ ! -d "$SBOROOT" ] && SBOROOT=.
+
+SELF=$( echo $0 | sed 's,.*/,,' )
+
+usage() {
+ cat <<EOF
+$SELF: search local slackbuilds.org repository
+
+Usage: $SELF [-a] [-v] [-I] [-R] [-B] [-H] [-r required] [-e email]
+ [-m maintainer] [-k keyword] [-c category] [build] [...]
+ $SELF --all
+ $SELF --help
+
+Find all SlackBuilds in the local repository matching the given
+criteria. If multiple search options are given, they are ANDed together
+by default.
+
+Options:
+
+Options may not be bundled (use "-a -v", not "-av"), and the spacing
+shown is required (use "-r foo", not "-rfoo").
+
+Search options:
+[build] Match builds by name.
+-r required Matches builds with <required> in the REQUIRES= field (only
+ exists in repo versions 14.0 and up).
+-e email Match email address.
+-m maintainer Match MAINTAINER= (real name, not email).
+-h homepage Match HOMEPAGE.
+-d readmetxt Match text in documentation (the README).
+-c category Match builds in <category>. Category names may be abbreviated,
+ e.g. "sys" for system, "ga" for games.
+-k keyword Keyword search (TAGS.txt, includes build names, only exists
+ in repo versions 14.1 and up).
+
+Search modifier options:
+--all List all builds. Any search options will be ignored.
+-a AND all the search options together (default is OR).
+-v Invert the search (find builds not matching the criteria).
+-i Search for installed packages only.
+-u Search for uninstalled packages only.
+-x Exact match (for build names only, not -r -e -m -h -d)
+
+Output options:
+-S Print short names (no category, "zdoom" not "games/zdoom").
+-P Print full paths to tarballs.
+-X Extract tarballs into current dir.
+-I cat .info files for all builds found.
+-R cat README files for all builds found.
+-B Open SBo repo page in browser, for each build found.
+-H Open HOMEPAGE in browser, for each build found.
+-C Check installed status (print "installed" or "NOT installed"
+ for each build found).
+
+The local repo must be in the current directory, or in the
+directory given by the SBOROOT environment variable, which defaults to
+/var/lib/sbopkg/SBo/\$SLACKVER. If SLACKVER is not set in the environment,
+its value will be extracted from /etc/slackware-version (or set to
+14.1, if this file doesn't exist).
+
+All search arguments are treated as case-insensitive regular expressions.
+Required, email, maintainer, and homepage arguments are unanchored,
+and can't be anchored with ^ or $. However, you can use \< and \> to
+anchor on word boundaries (e.g. "-e \<bob@" will match bob@example.com,
+but not silentbob@example.com).
+
+Build names are anchored on the left, so searching for 'z' will find all
+the build names that begin with 'z'. If you want to find all the build
+names containing a 'z', use '.*z' instead. You can search for all build
+names ending with 'z' by using '.*z\>' (actually this will also find
+things like compiz-bcop, which have a z at a word boundary).
+
+Search options can be given more than once, e.g. "-r foo -r bar" means
+find builds that require either foo or bar (or, with -a, builds that
+require both foo and bar).
+
+The -B and -H options use the environment variable BROWSER to set
+which browser to use. If BROWSER is not set, "$DFLTBROWSER" is used. Be
+careful with these: trying to open several hundred (or several thousand)
+browser instances will likely eat your machine for breakfast, especially
+if firefox is the default browser. Best to run the search without -B or
+-H first, to see how many matches you get.
+EOF
+}
+
+die() {
+ if [ -n "$TMPDIR" -a -d "$TMPDIR" ]; then
+ rm -rf $TMPDIR
+ fi
+ echo "$SELF: $@ (try '$SELF --help')" 1>&2
+ exit 1
+}
+
+info() {
+ echo "$SELF: info: $@" 1>&2
+}
+
+warn() {
+ echo "$SELF: warning: $@" 1>&2
+}
+
+set_input() {
+ if [ "$mode" = "or" ]; then
+ INPUT=$ALL
+ else
+ INPUT=$RESULTS
+ fi
+ : > $OUTPUT
+}
+
+set_output() {
+ if [ "$mode" = "or" ]; then
+ cat $OUTPUT >> $RESULTS
+ else
+ mv $OUTPUT $RESULTS
+ fi
+}
+
+info_search() {
+ set_input
+ egrep -i -l "^$1=.*$2" $( cat $INPUT ) > $OUTPUT
+ set_output
+}
+
+# keyword search is complicated by the fact that TAGS.txt doesn't
+# store the category. Also, we don't differentiate between the keywords
+# and the build name at the start of the line.
+keyword_search() {
+ local pkg
+
+ if [ ! -e $SBOROOT/TAGS.txt ]; then
+ cat <<EOF 1>&2
+$SELF: $SBOROOT/TAGS.txt not found.
+
+Note that this file only exists for Slackware versions 14.1 and up. We
+seem to be searching version $SLACKVER.
+
+EOF
+ die "can't do keyword search"
+ fi
+
+ set_input
+ egrep -i "$1" $SBOROOT/TAGS.txt | while read line; do
+ pkg=$( echo "$line" | cut -d: -f1 )
+ egrep "/$pkg\.info\$" $INPUT >> $OUTPUT
+ done
+ set_output
+}
+
+category_search() {
+ local categ
+ set_input
+ categ=$( cut -d/ -f1 $ALL | sort -u | egrep -i "$1" | head -1 )
+ [ -z "$categ" ] && die "fatal: no such category '$1'"
+ egrep -i "^$categ/" $INPUT > $OUTPUT
+ set_output
+}
+
+build_search() {
+ set_input
+ if [ "$exact" == "1" ]; then
+ fgrep "$1/$1.info" $INPUT > $OUTPUT
+ else
+ egrep -i "/$1[^/]*\.info\$" $INPUT > $OUTPUT
+ fi
+ set_output
+}
+
+readme_search() {
+ local info readme
+
+ set_input
+ for info in $( cat $INPUT ); do
+ readme=$( echo $info | sed 's,/[^/]*$,/README,' )
+ egrep -q -i "$1" $readme && echo $info >> $OUTPUT
+ done
+ set_output
+}
+
+is_installed() {
+ [ ! -e /var/log/packages/ ] && die "no /var/log/packages, are you sure this is Slackware?"
+ local pkg="$( echo $1 | cut -d/ -f2 )"
+ [ ! -e $TMPDIR/installed_pkgs ] && \
+ ls /var/log/packages/ | rev | cut -d- -f4- | rev > $TMPDIR/installed_pkgs
+ fgrep -q -x $pkg $TMPDIR/installed_pkgs
+ return $?
+}
+
+# works, but slow:
+## is_installed() {
+## local pkg candidate olddir found=1
+##
+## found=1
+## pkg="$( echo $1 | cut -d/ -f2 )"
+## olddir="$( pwd )"
+## cd /var/log/packages || die "no /var/log/packages, are you sure this is Slackware?"
+##
+## for candidate in "$( ls $pkg* 2>/dev/null )"; do
+## if [ "$pkg" = "$( echo $candidate | rev | cut -d- -f4- | rev )" ]; then
+## found=0
+## fi
+## done
+##
+## cd $olddir
+## return $found
+## }
+
+# also slow
+## is_installed() {
+## local pkg
+##
+## if [ ! -e $TMPDIR/installed_pkgs ]; then
+## ls /var/log/packages/ > $TMPDIR/installed_pkgs || \
+## die "no /var/log/packages, are you sure this is Slackware?"
+## fi
+##
+## pkg="$( echo $1 | cut -d/ -f2 )"
+## egrep -q "^$pkg"'-[^-]+-[^-]+-[^-]+$' $TMPDIR/installed_pkgs
+## return $?
+## }
+##
+## installed_search_backend() {
+## local pkg
+## set_input
+## for pkg in $( cat $INPUT ); do
+## eval is_installed $pkg $2 echo $pkg >> $OUTPUT
+## done
+## set_output
+## }
+##
+## installed_search() {
+## installed_search_backend "$1" "&&"
+## }
+##
+## uninstalled_search() {
+## installed_search_backend "$1" "||"
+## }
+
+# almost works, fast, might try to fix someday
+## installed_search() {
+## set_input
+## ls /var/log/packages/ | rev | cut -d- -f4- | rev > $TMPDIR/installed_pkgs || \
+## die "no /var/log/packages, are you sure this is Slackware?"
+## sort -t / -k 2 $INPUT > $INPUT.tmp
+## join -t / -1 2 -o 1.1,1.2 $INPUT.tmp $TMPDIR/installed_pkgs > $OUTPUT
+## exit 0
+## set_output
+## }
+
+# works, slow, but not as slow as the 1st try
+## installed_search() {
+## local -A packages
+## local pkg shortpkg
+##
+## [ ! -e /var/log/packages/ ] && die "no /var/log/packages, are you sure this is Slackware?"
+##
+## set_input
+## echo "got here 1"
+## for pkg in $( cat $INPUT ); do
+## echo -n "." 1>&2
+## shortpkg=$( expr "$pkg" : '.*/\([^/]*\)/' )
+## #shortpkg=$( echo $pkg | cut -d/ -f2 )
+## packages[$shortpkg]=$pkg
+## done
+## echo "got here 2"
+## ls /var/log/packages/ | rev | cut -d- -f4- | rev | while read pkg; do
+## [ -n "${packages[$pkg]}" ] && echo ${packages[$pkg]} >> $OUTPUT
+## done
+## echo "got here 3"
+## set_output
+## }
+
+# lightning fast and works correctly, compared to the commented-out
+# attempts above.
+installed_search_backend() {
+ local pkg grepopt="$1"
+
+ [ ! -e /var/log/packages/ ] && die "no /var/log/packages, are you sure this is Slackware?"
+
+ set_input
+ cut -d/ -f2 $INPUT > $INPUT.shortnames
+
+ # $INPUT.shortnames.found is foo => /foo.info
+ # fgrep -x means "match entire line only", used to avoid matching e.g.
+ # zathura when looking for zathura-cb. It's *much* faster than using
+ # egrep with ^ and $.
+ ls /var/log/packages/ | rev | cut -d- -f4- | rev | \
+ fgrep -x -f $INPUT.shortnames | \
+ sed 's,.*,/&.info,' \
+ > $INPUT.shortnames.found
+
+ fgrep $grepopt -f $INPUT.shortnames.found $INPUT > $OUTPUT
+ set_output
+}
+
+installed_search() {
+ installed_search_backend ""
+}
+
+uninstalled_search() {
+ installed_search_backend "-v"
+}
+
+invert_results() {
+ fgrep -v -f $RESULTS $ALL > $OUTPUT
+ mv $OUTPUT $RESULTS
+}
+
+open_in_browser() {
+ BROWSER="${BROWSER:-$DFLTBROWSER}"
+ info "opening URL '$1' with browser '$BROWSER'"
+ $BROWSER "$1"
+}
+
+print_results() {
+ if [ "$simpleoutput" = "1" ]; then
+ cat $OUTPUT
+ return
+ fi
+
+ # N.B. don't use 'cat $OUTPUT | while read line' here, console browsers
+ # don't like having their stdin redirected (especially not links)
+ for line in $( cat $OUTPUT ) ; do
+ # -X option
+ if [ "$extract" = "1" ]; then
+ tar -C $OLD_PWD -xvf $SBOROOT/$line.tar.gz
+ fi
+
+ # -P option
+ if [ "$printpaths" = "1" ]; then
+ echo -n $SBOROOT/$line.tar.gz
+ else
+ # -S option
+ if [ "$shortoutput" = "1" ]; then
+ echo -n $( echo $line | cut -d/ -f2 )
+ else
+ echo -n $line
+ fi
+ fi
+
+ # -C option
+ if [ "$checkinstalled" = "1" ]; then
+ is_installed $line && echo ": installed" || echo ": NOT installed"
+ else
+ echo
+ fi
+
+ # -R option
+ if [ -n "$readmefiles" ]; then
+ echo '==>' $line/README
+ cat $line/README
+ echo
+ fi
+
+ # -I option
+ if [ -n "$infofiles" ]; then
+ echo '==>' $line/*.info
+ cat $line/*.info
+ echo
+ fi
+
+ # -H option
+ if [ -n "$hpbrowser" ]; then
+ ( source $line/*.info ; open_in_browser "$HOMEPAGE" )
+ fi
+
+ # -B option
+ if [ -n "$browser" ]; then
+ open_in_browser "http://slackbuilds.org/repository/$SLACKVER/$line/"
+ fi
+ done
+}
+
+# main()
+
+mode="or"
+simpleoutput=1
+
+if [ "$*" == "" ]; then
+ set -- --help
+fi
+
+while [ -n "$1" ]; do
+ arg="$1"
+ shift
+ case "$arg" in
+ "--help") usage ; exit 0 ;;
+ "--all") showall=1 ;;
+ "-a") mode="and" ;;
+ "-r") required="$required $1" ; shift ;;
+ "-e") email="$email $1" ; shift ;;
+ "-m") maintainer="$maintainer $1" ; shift ;;
+ "-h") homepage="$homepage $1" ; shift ;;
+ "-k") keyword="$keyword $1" ; shift ;;
+ "-c") category="$category $1" ; shift ;;
+ "-d") readme="$readme $1" ; shift ;;
+ "-v") invert=1 ;;
+ "-R") readmefiles=1 ; simpleoutput=0 ;;
+ "-I") infofiles=1 ; simpleoutput=0 ;;
+ "-B") browser=1 ; simpleoutput=0 ;;
+ "-H") hpbrowser=1 ; simpleoutput=0 ;;
+ "-S") shortoutput=1 ; simpleoutput=0 ;;
+ "-P") printpaths=1 ; simpleoutput=0 ;;
+ "-X") extract=1 ; simpleoutput=0 ;;
+ "-C") checkinstalled=1 ; simpleoutput=0 ;;
+ "-i") installedonly=1 ;;
+ "-u") uninstalledonly=1 ;;
+ "-x") exact=1 ;;
+ -*) die "unknown option '$arg'" ;;
+ *) build="$build $arg" ;;
+ esac
+done
+
+if [ "$installedonly" = "1" -a "$uninstalledonly" = "1" ]; then
+ die "-i and -u don't make sense to use together"
+fi
+
+TMPDIR=${TMP:-/tmp}/sbosearch.$$.$RANDOM
+rm -rf $TMPDIR
+mkdir -p $TMPDIR
+
+ALL=$TMPDIR/allinfos
+OUTPUT=$TMPDIR/output
+RESULTS=$TMPDIR/results
+
+OLD_PWD=$( pwd )
+cd $SBOROOT || die "set SBOROOT or cd to the SBo/<version>/ directory."
+
+# Unfortunately ChangeLog.txt is the only file that's present in the SBo
+# tree for every Slack version.
+if [ ! -e ChangeLog.txt ]; then
+ warn "can't find ChangeLog.txt in $SBOROOT, are you sure this is a valid repo?"
+fi
+
+/bin/ls */*/*.info > $ALL
+[ "$showall" = "1" ] && mode="and"
+
+# Init results.
+# in 'and' mode, each search is done in the results of the previous
+# search. The first search starts with the full list, so the results
+# of the 'previous' (nonexistent) search must be the full list.
+# in 'or' mode, every search is done against the full list, and the
+# results are built up by appending (so the result starts out empty).
+if [ "$mode" = "and" ]; then
+ cp $ALL $RESULTS
+else
+ touch $RESULTS
+fi
+
+# Do the searches.
+# Thought about parametrizing these, so there could be a loop such as
+# for i in REQUIRES EMAIL MAINTAINER HOMEPAGE; do info_search $i; done
+# ...but it would lead to fugly bash code that I wouldn't care to debug
+# a year from now.
+
+if [ "$showall" != "1" ]; then
+ [ -n "$required" ] && for term in $required; do
+ info_search REQUIRES $term
+ done
+
+ [ -n "$email" ] && for term in $email; do
+ info_search EMAIL $term
+ done
+
+ [ -n "$maintainer" ] && for term in $maintainer; do
+ info_search MAINTAINER $term
+ done
+
+ [ -n "$homepage" ] && for term in $homepage; do
+ info_search HOMEPAGE $term
+ done
+
+ [ -n "$keyword" ] && for term in $keyword; do
+ keyword_search $term
+ done
+
+ [ -n "$category" ] && for term in $category; do
+ category_search $term
+ done
+
+ [ -n "$build" ] && for term in $build; do
+ build_search $term
+ done
+
+ [ -n "$readme" ] && for term in $readme; do
+ readme_search $term
+ done
+
+ [ -n "$installedonly" ] && installed_search
+
+ [ -n "$uninstalledonly" ] && uninstalled_search
+
+ [ -n "$invert" ] && invert_results
+fi
+
+# done with all the searches, pretty up the output.
+if [ "$shortoutput" = "1" ]; then
+ sortopts="-t / -k 2"
+fi
+
+sort -u $sortopts $RESULTS | cut -d/ -f1-2 > $OUTPUT
+
+print_results
+
+rm -rf $TMPDIR
+exit 0
diff --git a/sbosubmit b/sbosubmit
new file mode 100755
index 0000000..cbf3040
--- /dev/null
+++ b/sbosubmit
@@ -0,0 +1,124 @@
+#!/usr/bin/perl -w
+
+use LWP;
+use Getopt::Std;
+
+@categories = qw/
+Academic
+Accessibility
+Audio
+Business
+Desktop
+Development
+Games
+Graphics
+Haskell
+Libraries
+Misc
+Multimedia
+Network
+Office
+Perl
+Python
+Ruby
+System
+/;
+
+sub usage {
+ my $cats = join("\n", @categories);
+ warn <<EOF;
+
+Usage: $0 -c <category> [-e <email>] [-C <comment>] [-k <keywords>] file
+
+If no -e option is given, the contents of ~/.sbo_email is used as the
+email address (and it's a fatal error if the file's missing).
+
+The categories for -c are:
+
+$cats
+
+The -c argument is case-insensitive, and category names may be abbreviated
+to the shortest unique prefix (e.g. "gr" for Graphics is OK, but "g"
+is an error because it matches both Graphics and Games).
+
+No checking is done on the file before it's uploaded (other than its
+existence).
+
+After the file is uploaded, the server's response is displayed via
+"links -dump". $0's exit status does NOT reflect success/failure: it
+will be zero if an upload was done, or non-zero if there's an error in
+the arguments (in which case no upload is done).
+EOF
+ exit($_[0] || 0);
+}
+
+sub die_usage {
+ warn $_ . "\n" for @_;
+ usage(1);
+}
+
+sub get_category {
+ my $in = shift;
+ my $orig = $in;
+ my @got;
+
+ $in =~ s/[^A-Za-z]//g;
+ for(@categories) {
+ push @got, $_ if /^$in/i;
+ }
+
+ if(!@got) {
+ die_usage("Invalid category '$orig'");
+ } elsif(@got > 1) {
+ die_usage("Ambiguous category '$orig', matches: " . join(" ", @got));
+ } else {
+ return $got[0];
+ }
+}
+
+if(!@ARGV || $ARGV[0] =~ /--?h(?:elp)?/i) {
+ usage;
+}
+
+getopts('c:e:C:k:', \%opts);
+($userfile, $junk) = @ARGV;
+
+die_usage("Unknown junk on command line: '$junk'") if $junk;
+die_usage("No category (missing required -c arg)") unless $opts{c};
+$category = get_category($opts{c});
+chomp($submail = $opts{e} || `head -n1 ~/.sbo_email`);
+die_usage("No email address (use -e or else create ~/.sbo_email)") unless $submail;
+$comments = $opts{C} || "";
+$tags = $opts{k} || "";
+
+if(! -e $userfile) {
+ die_usage("File not found: $userfile");
+}
+
+print STDERR "Uploading...";
+
+$ua = LWP::UserAgent->new;
+$ua->agent('Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)');
+$resp = $ua->post(
+ 'http://slackbuilds.org/process_submit/',
+ [ MAX_FILE_SIZE => '100000',
+ userfile => [ $userfile ],
+ category => $category,
+ tags => $tags,
+ submail => $submail,
+ comments => $comments,
+ submit => 'Upload File' ],
+ Content_Type => 'form-data',
+ Referer => 'http://slackbuilds.org/submit/',
+ );
+
+print STDERR "\n";
+
+$tmpfile = "/tmp/sbosubmit.$$." . int(rand(10 ** 10)) . ".html";
+open RESULT, ">$tmpfile" or die "$tmpfile: $!";
+print RESULT $resp->content;
+close RESULT;
+system("links -dump $tmpfile | sed -n '/Upload Results/,/Home.*Change Log/p'");
+unlink $tmpfile;
+
+warn "*** HTTP response code is " . $resp->code . "\n" unless $resp->code == 200;