X-Git-Url: https://git.madduck.net/code/myrepos.git/blobdiff_plain/6ebadb2ec2e244911d754a97d493ed8f967c7e78..0460be30602c0b1dbf31ea5cb470568b27e6ef55:/mr?ds=sidebyside

diff --git a/mr b/mr
index 04a5cbf..4e3d738 100755
--- a/mr
+++ b/mr
@@ -1,5 +1,7 @@
 #!/usr/bin/perl
 
+#man{{{
+
 =head1 NAME
 
 mr - a Multiple Repository management tool
@@ -18,24 +20,26 @@ B<mr> [options] diff
 
 B<mr> [options] log
 
+B<mr> [options] register [repository]
+
+B<mr> [options] config section [parameter=[value] ...]
+
 B<mr> [options] action [params ...]
 
 =head1 DESCRIPTION
 
-B<mr> is a Multiple Repository management tool. It allows you to register a
-set of repositories in a .mrconfig file, and then checkout, update, or
-perform other actions on the repositories as if they were one big
-respository.
-
-Any mix of revision control systems can be used with B<mr>, and you can
-define arbitrary actions for commands like "update", "checkout", or "commit".
+B<mr> is a Multiple Repository management tool. It
+can checkout, update, or perform other actions on
+a set of repositories as if they were one combined respository. It
+supports any combination of subversion, git, cvs, and bzr repositories, 
+and support for other revision control systems can easily be added.
 
 B<mr> cds into and operates on all registered repsitories at or below your
 working directory. Or, if you are in a subdirectory of a repository that
 contains no other registered repositories, it will stay in that directory,
 and work on only that repository,
 
-The predefined commands should be fairly familiar to users of any revision
+These predefined commands should be fairly familiar to users of any revision
 control system:
 
 =over 4
@@ -70,10 +74,37 @@ Show a diff of uncommitted changes.
 
 Show the commit log.
 
+=back
+
+These commands are also available:
+
+=over 4
+
 =item list (or ls)
 
 List the repositories that mr will act on.
 
+=item register
+
+Register an existing repository in the mrconfig file. By default, the
+epository in the current directory is registered, or you can specify a
+directory to register.
+
+=item config
+
+Adds, modifies, removes, or prints a value from the mrconfig file. The next
+parameter is the name of the section the value is in. To add or modify
+values, use one or more instances of "parameter=value". Use "parameter=" to
+remove a parameter. Use just "parameter" to get the value of a parameter.
+
+For example, to add (or edit) a repository in src/foo:
+
+  mr config src/foo checkout="svn co svn://example.com/foo/trunk foo"
+
+To show the command that mr uses to update the repository in src/foo:
+
+  mr config src/foo update
+
 =item help
 
 Displays this help.
@@ -84,10 +115,10 @@ Actions can be abbreviated to any unambiguous subsctring, so
 "mr st" is equivilant to "mr status", and "mr up" is equivilant to "mr
 update"
 
-Additional parameters can be passed to other commands than "commit", they
-will be passed on unchanged to the underlying revision control system.
-This is mostly useful if the repositories mr will act on all use the same
-revision control system.
+Additional parameters can be passed to most commands, and are passed on
+unchanged to the underlying revision control system. This is mostly useful
+if the repositories mr will act on all use the same revision control
+system.
 
 =head1 OPTIONS
 
@@ -107,6 +138,11 @@ directory.
 
 Be verbose.
 
+=item -s
+
+Expand the statistics line displayed at the end to include information
+about exactly which repositories failed and were skipped, if any.
+
 =back
 
 =head1 FILES
@@ -122,10 +158,13 @@ Here is an example .mrconfig file:
   chain = true
 
   [src/linux-2.6]
-  checkout = git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git
+  checkout = git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git &&
+  	cd linux-2.6 &&
+  	git checkout -b mybranch origin/master
 
 The .mrconfig file uses a variant of the INI file format. Lines starting with
-"#" are comments. Lines ending with "\" are continued on to the next line.
+"#" are comments. Values can be continued to the following line by
+indenting the line with whitespace.
 
 The "DEFAULT" section allows setting default values for the sections that
 come after it.
@@ -158,7 +197,16 @@ A few parameters have special meanings:
 =item skip
 
 If the "skip" parameter is set and its command returns nonzero, then B<mr>
-will skip acting on that repository.
+will skip acting on that repository. The command is passed the action
+name in $1.
+
+Here are two examples. The first skips the repo unless
+mr is run by joey. The second uses the hours_since function
+(included in mr's built-in library) to skip updating the repo unless it's
+been at least 12 hours since the last update.
+
+  skip = test $(whoami) != joey
+  skip = [ "$1" = update ] && [ $(hours_since "$1") -lt 12 ]
 
 =item chain
 
@@ -166,6 +214,14 @@ If the "chain" parameter is set and its command returns nonzero, then B<mr>
 will try to load a .mrconfig file from the root of the repository. (You
 should avoid chaining from repositories with untrusted committers.)
 
+=item deleted
+
+If the "deleted" parameter is set and its command returns nonzero, then
+B<mr> will treat the repository as deleted. It won't ever actually delete
+the repository, but it will warn if it sees the repsoitory's directory.
+This is useful when one mrconfig file is shared amoung multiple machines,
+to keep track of and remember to delete old repositories.
+
 =item lib
 
 The "lib" parameter can specify some shell code that will be run before each
@@ -184,6 +240,8 @@ http://kitenet.net/~joey/code/mr/
 
 =cut
 
+#}}}
+
 use warnings;
 use strict;
 use Getopt::Long;
@@ -192,6 +250,7 @@ use Cwd qw(getcwd abs_path);
 my $directory=getcwd();
 my $config="$ENV{HOME}/.mrconfig";
 my $verbose=0;
+my $stats=0;
 my %config;
 my %knownactions;
 my %alias;
@@ -200,7 +259,8 @@ Getopt::Long::Configure("no_permute");
 my $result=GetOptions(
 	"d|directory=s" => sub { $directory=abs_path($_[1]) },
 	"c|config=s" => \$config,
-	"verbose" => \$verbose,
+	"v|verbose" => \$verbose,
+	"s|stats" => \$stats,
 );
 if (! $result || @ARGV < 1) {
 	die("Usage: mr [-d directory] action [params ...]\n".
@@ -240,7 +300,48 @@ if (! exists $knownactions{$action}) {
 }
 
 if ($action eq 'help') {
-	exec($config{''}{DEFAULT}{help});
+	exec($config{''}{DEFAULT}{$action}) || die "exec: $!";
+}
+elsif ($action eq 'config') {
+	if (@ARGV < 2) {
+		die "mr config: not enough parameters\n";
+	}
+	my $section=shift;
+	if ($section=~/^\//) {
+		# try to convert to a path relative to $config's dir
+		my ($dir)=$config=~/^(.*\/)[^\/]+$/;
+		if ($section=~/^\Q$dir\E(.*)/) {
+			$section=$1;
+		}
+	}
+	my %changefields;
+	foreach (@ARGV) {
+		if (/^([^=]+)=(.*)$/) {
+			$changefields{$1}=$2;
+		}
+		else {
+			my $found=0;
+			foreach my $topdir (sort keys %config) {
+				if (exists $config{$topdir}{$section} &&
+				    exists $config{$topdir}{$section}{$_}) {
+					print $config{$topdir}{$section}{$_}."\n";
+					$found=1;
+				}
+			}
+			if (! $found) {
+				die "mr $action: $section $_ not set\n";
+			}
+		}
+	}
+	modifyconfig($config, $section, %changefields) if %changefields;
+	exit 0;
+}
+elsif ($action eq 'register') {
+	my $command="set -e; ".$config{''}{DEFAULT}{lib}."\n".
+		"my_action(){ $config{''}{DEFAULT}{$action}\n }; my_action ".
+		join(" ", map { s/\//\/\//g; s/"/\"/g; '"'.$_.'"' } @ARGV);
+	print STDERR "mr $action: running >>$command<<\n" if $verbose;
+	exec($command) || die "exec: $!";
 }
 
 # work out what repos to act on
@@ -275,16 +376,32 @@ if (! @repos) {
 	$nochdir=1;
 }
 
-my (@failed, @successful, @skipped);
+my (@failed, @ok, @skipped);
 foreach my $repo (@repos) {
 	action($action, @$repo);
 }
 
-sub action {
+sub action { #{{{
 	my ($action, $dir, $topdir, $subdir) = @_;
 	
 	my $lib= exists $config{$topdir}{$subdir}{lib} ?
-	                $config{$topdir}{$subdir}{lib} : "";
+	                $config{$topdir}{$subdir}{lib}."\n" : "";
+
+	if (exists $config{$topdir}{$subdir}{deleted}) {
+		if (! -d $dir) {
+			return;
+		}
+		else {
+			my $test="set -e;".$lib.$config{$topdir}{$subdir}{deleted};
+			print "mr $action: running deleted test >>$test<<\n" if $verbose;
+			my $ret=system($test);
+			if ($ret >> 8 == 0) {
+				print STDERR "mr error: $dir should be deleted yet still exists\n\n";
+				push @failed, $dir;
+				return;
+			}
+		}
+	}
 
 	if ($action eq 'checkout') {
 		if (-d $dir) {
@@ -303,8 +420,9 @@ sub action {
 	$ENV{MR_REPO}=$dir;
 
 	if (exists $config{$topdir}{$subdir}{skip}) {
-		my $test="set -e;".$lib.$config{$topdir}{$subdir}{skip};
-		print "mr $action: running skip test $test\n" if $verbose;
+		my $test="set -e;".$lib.
+			"my_action(){ $config{$topdir}{$subdir}{skip}\n }; my_action '$action'";
+		print "mr $action: running skip test >>$test<<\n" if $verbose;
 		my $ret=system($test);
 		if ($ret >> 8 == 0) {
 			print "mr $action: $dir skipped per config file\n" if $verbose;
@@ -323,15 +441,15 @@ sub action {
 	}
 	else {
 		if (! $nochdir) {
-			print "mr $action: $dir\n";
+			print "mr $action: $topdir$subdir\n";
 		}
 		else {
-			print "mr $action: $dir (in subdir $directory)\n";
+			print "mr $action: $topdir$subdir (in subdir $directory)\n";
 		}
 		my $command="set -e; ".$lib.
-			"my_action(){ $config{$topdir}{$subdir}{$action} ; }; my_action ".
+			"my_action(){ $config{$topdir}{$subdir}{$action}\n }; my_action ".
 			join(" ", map { s/\//\/\//g; s/"/\"/g; '"'.$_.'"' } @ARGV);
-		print STDERR "mr $action: running $command\n" if $verbose;
+		print STDERR "mr $action: running >>$command<<\n" if $verbose;
 		my $ret=system($command);
 		if ($ret != 0) {
 			print STDERR "mr $action: failed ($ret)\n" if $verbose;
@@ -344,14 +462,14 @@ sub action {
 			}
 		}
 		else {
-			push @successful, $dir;
+			push @ok, $dir;
 		}
 
 		print "\n";
 	}
-}
+} #}}}
 
-sub showstat {
+sub showstat { #{{{
 	my $count=shift;
 	my $singular=shift;
 	my $plural=shift;
@@ -359,25 +477,33 @@ sub showstat {
 		return "$count ".($count > 1 ? $plural : $singular);
 	}
 	return;
-}
-if (! @successful && ! @failed && ! @skipped) {
+} #}}}
+if (! @ok && ! @failed && ! @skipped) {
 	die "mr $action: no repositories found to work on\n";
 }
 print "mr $action: finished (".join("; ",
-	showstat($#successful+1, "successful", "successful"),
+	showstat($#ok+1, "ok", "ok"),
 	showstat($#failed+1, "failed", "failed"),
 	showstat($#skipped+1, "skipped", "skipped"),
 ).")\n";
+if ($stats) {
+	if (@skipped) {
+		print "mr $action: (skipped: ".join(" ", @skipped).")\n";
+	}
+	if (@failed) {
+		print "mr $action: (failed: ".join(" ", @failed).")\n";
+	}
+}
 if (@failed) {
 	exit 1;
 }
-elsif (! @successful && @skipped) {
+elsif (! @ok && @skipped) {
 	exit 1;
 }
 exit 0;
 
 my %loaded;
-sub loadconfig {
+sub loadconfig { #{{{
 	my $f=shift;
 
 	my @toload;
@@ -385,19 +511,20 @@ sub loadconfig {
 	my $in;
 	my $dir;
 	if (ref $f eq 'GLOB') {
-		$in=$f;	
 		$dir="";
+		$in=$f;	
 	}
 	else {
-		# $f might be a symlink
+		if (! -e $f) {
+			return;
+		}
+
 		my $absf=abs_path($f);
 		if ($loaded{$absf}) {
 			return;
 		}
 		$loaded{$absf}=1;
 
-		print "mr: loading config $f\n" if $verbose;
-		open($in, "<", $f) || die "mr: open $f: $!\n";
 		($dir)=$f=~/^(.*\/)[^\/]+$/;
 		if (! defined $dir) {
 			$dir=".";
@@ -406,29 +533,42 @@ sub loadconfig {
 
 		# copy in defaults from first parent
 		my $parent=$dir;
-		while ($parent=~s/^(.*)\/[^\/]+\/?$/$1/) {
+		while ($parent=~s/^(.*\/)[^\/]+\/?$/$1/) {
+			if ($parent eq '/') {
+				$parent="";
+			}
 			if (exists $config{$parent} &&
 			    exists $config{$parent}{DEFAULT}) {
 				$config{$dir}{DEFAULT}={ %{$config{$parent}{DEFAULT}} };
 				last;
 			}
 		}
+		
+		print "mr: loading config $f\n" if $verbose;
+		open($in, "<", $f) || die "mr: open $f: $!\n";
 	}
+	my @lines=<$in>;
+	close $in;
 
 	my $section;
-	while (<$in>) {
+	my $line=0;
+	while (@lines) {
+		$_=shift @lines;
+		$line++;
 		chomp;
 		next if /^\s*\#/ || /^\s*$/;
-		if (/^\s*\[([^\]]*)\]\s*$/) {
+		if (/^\[([^\]]*)\]\s*$/) {
 			$section=$1;
 		}
-		elsif (/^\s*(\w+)\s*=\s*(.*)/) {
+		elsif (/^(\w+)\s*=\s*(.*)/) {
 			my $parameter=$1;
 			my $value=$2;
 
-			# continuation line
-			while ($value=~/(.*)\\$/) {
-				$value=$1.<$in>;
+			# continued value
+			while (@lines && $lines[0]=~/^\s(.+)/) {
+				shift(@lines);
+				$line++;
+				$value.="\n$1";
 				chomp $value;
 			}
 
@@ -445,7 +585,7 @@ sub loadconfig {
 				$alias{$parameter}=$value;
 			}
 			elsif ($parameter eq 'lib') {
-				$config{$dir}{$section}{lib}.=$value." ; ";
+				$config{$dir}{$section}{lib}.=$value."\n";
 			}
 			else {
 				$config{$dir}{$section}{$parameter}=$value;
@@ -459,99 +599,239 @@ sub loadconfig {
 			}
 		}
 		else {
-			die "$f line $.: parse error\n";
+			die "$f line $line: parse error\n";
 		}
 	}
-	close $in;
 
 	foreach (@toload) {
 		loadconfig($_);
 	}
-}
+} #}}}
+
+sub modifyconfig { #{{{
+	my $f=shift;
+	# the section to modify or add
+	my $targetsection=shift;
+	# fields to change in the section
+	# To remove a field, set its value to "".
+	my %changefields=@_;
+
+	my @lines;
+	my @out;
+
+	if (-e $f) {
+		open(my $in, "<", $f) || die "mr: open $f: $!\n";
+		@lines=<$in>;
+		close $in;
+	}
+
+	my $formatfield=sub {
+		my $field=shift;
+		my @value=split(/\n/, shift);
+
+		return "$field = ".shift(@value)."\n".
+			join("", map { "\t$_\n" } @value);
+	};
+	my $addfields=sub {
+		my @blanks;
+		while ($out[$#out] =~ /^\s*$/) {
+			unshift @blanks, pop @out;
+		}
+		foreach my $field (sort keys %changefields) {
+			if (length $changefields{$field}) {
+				push @out, "$field = $changefields{$field}\n";
+				delete $changefields{$field};
+			}
+		}
+		push @out, @blanks;
+	};
+
+	my $section;
+	while (@lines) {
+		$_=shift(@lines);
+
+		if (/^\s*\#/ || /^\s*$/) {
+			push @out, $_;
+		}
+		elsif (/^\[([^\]]*)\]\s*$/) {
+			if (defined $section && 
+			    $section eq $targetsection) {
+				$addfields->();
+			}
+
+			$section=$1;
+
+			push @out, $_;
+		}
+		elsif (/^(\w+)\s*=\s(.*)/) {
+			my $parameter=$1;
+			my $value=$2;
+
+			# continued value
+			while (@lines && $lines[0]=~/^\s(.+)/) {
+				shift(@lines);
+				$value.="\n$1";
+				chomp $value;
+			}
+
+			if ($section eq $targetsection) {
+				if (exists $changefields{$parameter}) {
+					if (length $changefields{$parameter}) {
+						$value=$changefields{$parameter};
+					}
+					delete $changefields{$parameter};
+				}
+			}
+
+			push @out, $formatfield->($parameter, $value);
+		}
+	}
+
+	if (defined $section && 
+	    $section eq $targetsection) {
+		$addfields->();
+	}
+	elsif (%changefields) {
+		push @out, "\n[$targetsection]\n";
+		foreach my $field (sort keys %changefields) {
+			if (length $changefields{$field}) {
+				push @out, $formatfield->($field, $changefields{$field});
+			}
+		}
+	}
+
+	open(my $out, ">", $f) || die "mr: write $f: $!\n";
+	print $out @out;
+	close $out;	
+} #}}}
 
 # Finally, some useful actions that mr knows about by default.
 # These can be overridden in ~/.mrconfig.
+#DATA{{{
 __DATA__
 [ALIAS]
-	co = checkout
-	ci = commit
-	ls = list
+co = checkout
+ci = commit
+ls = list
 
 [DEFAULT]
-lib = \
-	error() { \
-		echo "mr: $@" >&2; \
-		exit 1; \
+lib =
+	error() {
+		echo "mr: $@" >&2
+		exit 1
+	}
+	hours_since() {
+		for dir in .git .svn .bzr CVS; do
+			if [ -e "$MR_REPO/$dir" ]; then
+				flagfile="$MR_REPO/$dir/.mr_last$1"
+				break
+			fi
+		done
+		if [ -z "$flagfile" ]; then
+			error "cannot determine flag filename"
+		fi
+		perl -wle 'print -f shift() ? int((-M _) * 24) : 9999' "$flagfile"
+		touch "$flagfile"
 	}
 
-update = \
-	if [ -d "$MR_REPO"/.svn ]; then \
-		svn update "$@"; \
-	elif [ -d "$MR_REPO"/.git ]; then \
-		git pull origin master "$@"; \
-	elif [ -d "$MR_REPO"/.bzr ]; then \
-		bzr merge "$@"; \
-	elif [ -d "$MR_REPO"/CVS ]; then \
-		cvs update "$@"; \
-	else \
-		error "unknown repo type"; \
+update =
+	if [ -d "$MR_REPO"/.svn ]; then
+		svn update "$@"
+	elif [ -d "$MR_REPO"/.git ]; then
+		git pull origin master "$@"
+	elif [ -d "$MR_REPO"/.bzr ]; then
+		bzr merge "$@"
+	elif [ -d "$MR_REPO"/CVS ]; then
+		cvs update "$@"
+	else
+		error "unknown repo type"
+	fi
+status =
+	if [ -d "$MR_REPO"/.svn ]; then
+		svn status "$@"
+	elif [ -d "$MR_REPO"/.git ]; then
+		git status "$@" || true
+	elif [ -d "$MR_REPO"/.bzr ]; then
+		bzr status "$@"
+	elif [ -d "$MR_REPO"/CVS ]; then
+		cvs status "$@"
+	else
+		error "unknown repo type"
 	fi
-status = \
-	if [ -d "$MR_REPO"/.svn ]; then \
-		svn status "$@"; \
-	elif [ -d "$MR_REPO"/.git ]; then \
-		git status "$@" || true; \
-	elif [ -d "$MR_REPO"/.bzr ]; then \
-		bzr status "$@"; \
-	elif [ -d "$MR_REPO"/CVS ]; then \
-		cvs status "$@"; \
-	else \
-		error "unknown repo type"; \
+commit =
+	if [ -d "$MR_REPO"/.svn ]; then
+		svn commit "$@"
+	elif [ -d "$MR_REPO"/.git ]; then
+		git commit -a "$@" && git push --all
+	elif [ -d "$MR_REPO"/.bzr ]; then
+		bzr commit "$@" && bzr push
+	elif [ -d "$MR_REPO"/CVS ]; then
+		cvs commit "$@"
+	else
+		error "unknown repo type"
 	fi
-commit = \
-	if [ -d "$MR_REPO"/.svn ]; then \
-		svn commit "$@"; \
-	elif [ -d "$MR_REPO"/.git ]; then \
-		git commit -a "$@" && git push --all; \
-	elif [ -d "$MR_REPO"/.bzr ]; then \
-		bzr commit "$@" && bzr push; \
-	elif [ -d "$MR_REPO"/CVS ]; then \
-		cvs commit "$@"; \
-	else \
-		error "unknown repo type"; \
+diff =
+	if [ -d "$MR_REPO"/.svn ]; then
+		svn diff "$@"
+	elif [ -d "$MR_REPO"/.git ]; then
+		git diff "$@"
+	elif [ -d "$MR_REPO"/.bzr ]; then
+		bzr diff "$@"
+	elif [ -d "$MR_REPO"/CVS ]; then
+		cvs diff "$@"
+	else
+		error "unknown repo type"
 	fi
-diff = \
-	if [ -d "$MR_REPO"/.svn ]; then \
-		svn diff "$@"; \
-	elif [ -d "$MR_REPO"/.git ]; then \
-		git diff "$@"; \
-	elif [ -d "$MR_REPO"/.bzr ]; then \
-		bzr diff "$@"; \
-	elif [ -d "$MR_REPO"/CVS ]; then \
-		cvs diff "$@"; \
-	else \
-		error "unknown repo type"; \
+log =
+	if [ -d "$MR_REPO"/.svn ]; then
+		svn log"$@"
+	elif [ -d "$MR_REPO"/.git ]; then
+		git log "$@"
+	elif [ -d "$MR_REPO"/.bzr ]; then
+		bzr log "$@"
+	elif [ -d "$MR_REPO"/CVS ]; then
+		cvs log "$@"
+	else
+		error "unknown repo type"
 	fi
-log = \
-	if [ -d "$MR_REPO"/.svn ]; then \
-		svn log"$@"; \
-	elif [ -d "$MR_REPO"/.git ]; then \
-		git log "$@"; \
-	elif [ -d "$MR_REPO"/.bzr ]; then \
-		bzr log "$@"; \
-	elif [ -d "$MR_REPO"/CVS ]; then \
-		cvs log "$@"; \
-	else \
-		error "unknown repo type"; \
+register =
+	if [ -n "$1" ]; then
+		cd "$1"
 	fi
+	basedir="$(basename $(pwd))"
+	if [ -d .svn ]; then
+		url=$(LANG=C svn info . | grep -i ^URL: | cut -d ' ' -f 2)
+		if [ -z "$url" ]; then
+			error "cannot determine svn url"
+		fi
+		echo "Registering svn url: $url"
+		mr config "$(pwd)" checkout="svn co $url $basedir"
+	elif [ -d .git ]; then
+		url=$(LANG=C git-config --get remote.origin.url)
+		if [ -z "$url" ]; then
+			error "cannot determine git url"
+		fi
+		echo "Registering git url: $url"
+		mr config "$(pwd)" checkout="git clone $url $basedir"
+	elif [ -d .bzr ]; then
+		url=$(cat .bzr/branch/parent)
+		if [ -z "$url" ]; then
+			error "cannot determine bzr url"
+		fi
+		echo "Registering bzr url: $url"
+		mr config "$(pwd)" checkout="bzr clone $url $basedir"
+	else
+		error "unable to register this repo type"
+	fi
+help =
+	if [ ! -e "$MR_PATH" ]; then
+		error "cannot find program path"
+	fi
+	(pod2man -c mr "$MR_PATH" | man -l -) || error "pod2man or man failed"
 list = true
-
-help = \
-	if [ ! -e "$MR_PATH" ]; then \
-		error "cannot find program path";\
-	fi; \
-	(pod2man -c mr "$MR_PATH" | man -l -) || \
-		error "pod2man or man failed"
+config = 
 
 ed = echo "A horse is a horse, of course, of course.."
 T = echo "I pity the fool."
+right = echo "Not found."
+#}}}