#!/usr/bin/perl -w # oggify.pl -- script to help with mass encoding of flac files # Copyright (c) 2005 Scott Paul Robertson (spr@mahonri5.net) # # oggify.pl is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version # # oggify.pl is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with oggify.pl; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA use Cwd; my $version = "0.9"; # Help message {{{1 $help_message = "oggify.pl ($version) -- Easily convert flac files to a more common format.\n" . "usage: oggify.pl [options] \n" . " and can be directories or files.\n" . "Goes down and makes a similar tree in encoding flac\n" . "files into mp3 or ogg files.\n" . "Options:\n" . "-h (--help) -- Prints this help\n" . "-H (--full-help) -- Prints a very verbose help\n" . "-q (--quality) -- sets the quality to (as oggenc, default 5)\n" . "-s (--quiet) -- quiets output, use multiple times up to 3 (default 0)\n" . "-t (--test) -- print out messages, but do not perform encoding\n" . "-n (--nice) -- nices the encodes at value (default 10)\n" . "-p (--purge) -- remove unmatched items from the destination\n" . "-r (--refresh) -- re-encode files that are older than the flac's mtime, also\n" . "\tchange ogg's to mp3's and vice-versa, if appropriate\n" . "-R (--retag) -- Disables all other operations, and retags files (all in tree)\n" . "-L (--follow-symlinks) -- oggify will follow and use symlinks\n" . "--mp3 -- Generate mp3's using lame instead of ogg's\n" . "--enc-type (cbr|vbr|abr) -- sets lame's encoding, default is vbr\n" . "For examples, see the extended help\n"; # }}}1 # Verbose Help {{{1 $verbose_help_message = "oggify.pl -- Converts a directory tree of flacs into a similar directory tree of\n" . "oggs or mp3s. Will also handle single files.\n" . "Version: $version\n" . "Use: oggify.pl [options] \n" . "where is either a flac file or directory, and is a\n" . "directory, or file (in the case of being a file).\n" . "Oggify will walk the directory structure found in and create an equiv-\n" . "elent one in , and encode any flac files found into mp3's or ogg's\n" . "oggify.pl is licensed under the gpl, please read the source code for more info\n" . "\n" . "--------------------------------------------------------------------------------\n" . "\n" . "Options and explinations:\n" . "-h (--help) -- Prints a simple help\n" . "-H (--full-help) -- Prints this\n" . "-q or --quality \n" . "\tSets the quality level oggify will use to encode your files.\n" . "\tValues: 0 - 10. 0 is the lowest, 10 the highest.\n" . "\tIf encoding to ogg, the quality value is the same as the -q switch for\n" . "\toggenc, please see the oggenc(1) manpage\n" . "\tIf encoding to mp3, the quality value is determined via a lookup table\n" . "\tdefined as follows:\n" . "\t\t----------------------------------------\n" . "\t\t| Mode | quality | lame setting |\n" . "\t\t----------------------------------------\n" . "\t\t| cbr | 0 - 1 | --preset cbr 64 |\n" . "\t\t| cbr | 2 | --preset cbr 96 |\n" . "\t\t| cbr | 3 | --preset cbr 128 |\n" . "\t\t| cbr | 4 | --preset cbr 160 |\n" . "\t\t| cbr | 5 | --preset cbr 192 |\n" . "\t\t| cbr | 6 | --preset cbr 256 |\n" . "\t\t| cbr | 7 | --preset insane |\n" . "\t\t| vbr | 0 - 2 | --preset medium |\n" . "\t\t| vbr | 3 - 5 | --preset standard |\n" . "\t\t| vbr | 6 - 9 | --preset extreme |\n" . "\t\t| abr | 2 - 6 | same as cbr |\n" . "\t\t| abr | 7 - 9 | --preset abr 320 |\n" . "\t\t| any | 10 | --preset insane |\n" . "\t\t----------------------------------------\n" . "\n" . "\tvbr is the default when encoding mp3's.\n" . "\tfor more information on the lame setting please see the lame(1)\n" . "\tmanpage.\n" . "-s or --quiet\n" . "\tSets the silence level that oggify will run under\n" . "\tLevel 1: Disable messages concerning files that will not be done due\n" . "\tthem already existing, etc.\n" . "\tLevel 2: Disables all messages except the initial message about where\n" . "\tthe roots of the trees are\n" . "\tLevel 3: No output\n" . "\tDefaults to 0\n" . "-t or --test\n" . "\tPrints out what oggify.pl would do\n" . "-n or --nice \n" . "\tNices the encoding called by oggify.pl.\n" . "\t can be any value nice (see nice(1) manpage) will accept\n" . "\tensure that the user running oggify.pl can nice to that value\n" . "-p or --purge\n" . "\tAny mp3's or ogg's that are found in the destination tree that are\n" . "\tnot in the source tree will be removed if purge is enabled. Also any\n" . "\tempty directories that meet the above requirements will be removed\n" . "\tas well\n" . "-r or --refresh or -f or --overwrite\n" . "\tAny output files that are older than the flac's mtime will be\n" . "\tre-encoded based on the settings you supply.\n" . "\tAlso if oggify finds a file with the proper naming, but wrong encoding\n" . "\ttype (ie: mp3 when doing ogg's), it will re-encode it\n" . "-R or --retag\n" . "\tDisables all other operations, and retags all the output files in the\n" . "\tdestination tree with the metadata in the flac file\n" . "-L or --follow-symlinks\n" . "\tTells oggify to pay attention to symlinks, will go into directories\n" . "\tthat are symlinks, and will take symlinks into consideration when\n" . "\tdetermining the need to encode.\n" . "--mp3\n" . "\tSets oggify.pl to encode to mp3\n" . "--enc-type \n" . "\tSets the encode type for mp3's to cbr (constant bit rate) or\n" . "\tabr (average bit rate). If unspecified oggifly will use vbr\n" . "\t(variable bit rate). See lame(1) manpage for more info\n" . "Examples:\n" . "\toggify.pl /mnt/flac /home/music\n" . "\tTransveres /mnt/flac, creating a similar directory structure in\n" . "\t/home/music, and will create any files missing from /home/music\n" . "\tthat exist as flac's in /mnt/flac. Those that already exist will\n" . "\tbe updated if mtimes differ\n" . "\n" . "\toggify.pl --mp3 --enc-type vbr /home/flac/modern /home/music\n" . "\tAuthor's standard method of use\n" . "\tEncoding will make vbr mp3's with the --preset standard switch\n" . "\tpassed to lame\n" . "\n" . "\toggify.pl -q 7 \"/flac/weezer/maladriot/keep fishin.flac\" /tmp\n" . "\tWill create a quality 7 ogg file: /tmp/keep fishin.ogg\n" . "\n" . "\toggify.pl -srtq 7 --mp3 --enc-type cbr /home/flac /home/music\n" . "\tOne level of silence, refresh, test, quality 7 mp3's using cbr encoding\n" . "\n" . "\toggify.pl -ssq 7 /home/flac /home/music/\n" . "\tGenerates oggs for every flac in /home/flac at quality 7 without\n" . "\tany output\n" . "\n" . "\toggify.pl -s -q 6 /flacs/something.flac /outdir/this/\n" . "\tencodes /flacs/something.flac and outputs to /outdir/this/something.ogg\n" . "Known Bugs:\n" . "\tlame, oggenc, flac, metaflac, mkdir, id3v2 must all be in your path\n" . "\tRequires directories to be absolute paths from /\n"; # }}}1 # Global Variables {{{1 $quality = 5; # quality setting $flac_tree_head = 0; # root of the directory tree containing the flacs $ogg_tree_head = 0; # root of the directory tree where the oggs will go @ignore_patterns = 0; # currently unused. $quiet = 0; # quiet value (useable up to 3) $truth = 1; # whether we're doing this for real $nice_value = 10; # nice value to use $mp3 = 0; # --mp3 boolean $enc_type = "vbr"; # lame algorithm to encode to (vbr or abr, else cbr) $refresh = 0; # Whether we'll re-encode files if out of date $purge = 0; # Whether to remove unmatched items $symlink = 0; # Whether to follow symlinks $debug = 0; # debug level print outs $id3_prog = "id3v2"; # what to use for id3 tag editing $retag = 0; # Are we retagging the tree? $debug = 0; # Debug option: hidden $current_tmp_file = 0; # File for temporary # }}}1 # Arg checking, etc {{{1 if (!@ARGV) { print "Need options!\n"; print $help_message; exit -1; } if ($ARGV[0] eq "-h" || $ARGV[0] eq "--help") { print $help_message; exit; } if ($ARGV[0] eq "-H" || $ARGV[0] eq "--full-help") { print $verbose_help_message; exit; } process_args(); verify_args(); # }}}1 if ($quiet < 3) { print "Flacs starting at: $flac_tree_head\nDestination tree starting at: $ogg_tree_head\n"; } # Setup signal handler setpgrp(0,0); $SIG{QUIT} = \&sigdie_handler; $SIG{INT} = \&sigdie_handler; # Run! process($flac_tree_head, $ogg_tree_head); cleanup(); exit(0); # function process_args {{{1 # Goes through @ARGV and sets the global variables # Processes bunched single arg items (-sstn 10) sub process_args { $count = 0; $skip = 0; $end = 0; @cleaned_args = {}; # Go through args to break -xxxx options foreach $arg (@ARGV) { if ($arg =~ /^-\w\w+/) { @broke = split //,$arg; foreach $item (@broke) { if ($item ne '-') { push @cleaned_args, "-$item"; } if ($end != 0) { $end = -1; } if (($item eq 'i' || $item eq 'q' || $item eq 'n') && $end != -1) { $end = 1; } } if ($end == -1) { print "last option in -xxxxx set needs to "; print "have the input\n"; print $help_message; exit -1; } } else { push @cleaned_args, $arg; } } # assign arguments foreach $arg (@cleaned_args) { if (!$skip) { if ($arg eq "-q" || $arg eq "--quality") { $quality = $cleaned_args[$count + 1]; if ($quality < 1) { print "quality must be above 0!\n"; print $help_message; exit -1; } $skip = 1; } elsif ($arg eq "-i") { push @ignore_patterns, $cleaned_args[$count + 1]; $skip = 1; } elsif ($arg eq "-s" || $arg eq "--quiet") { $quiet++; } elsif ($arg eq "-t" || $arg eq "--test") { $truth = 0; } elsif ($arg eq "-f" || $arg eq "--force") { $refresh =1; } elsif ($arg eq "-n" || $arg eq "--nice") { $nice_value = $cleaned_args[$count + 1]; if ($nice_value < 0) { print "cannot nice below 0!\n"; print $help_message; exit -1; } $skip=1; } elsif ($arg eq "--mp3") { $mp3 = 1; } elsif ($arg eq "--enc-type") { $enc_type = $cleaned_args[$count + 1]; if (! ($enc_type eq "abr" || $enc_type eq "vbr") ) { print "enc-type must be either abr or vbr, $enc_type is not valid\n"; print $help_message; exit -1; } $skip=1; } elsif ($arg eq "-L" || $arg eq "--follow-symlinks") { $symlink = 1; } elsif ($arg eq "-d") { $debug = 1; } elsif ($arg eq "-p" || $arg eq "--purge") { $purge = 1; } elsif ($arg eq "-r" || $arg eq "--refresh") { $refresh = 1; } elsif ($arg eq "-R" || $arg eq "--retag") { $retag = 1; } elsif ($arg eq "--debug") { $debug = 1; } # TODO: use array ref's to pull directories elsif ($arg =~ /(\/.+)$/) { $temp = $1; if ($temp =~ /\/.+\/$/) { $temp = substr($temp, 0, ((length $temp) - 1)); } if (!$flac_tree_head) { $flac_tree_head = $temp; } elsif (!$ogg_tree_head) { $ogg_tree_head = $temp; } } } else { $skip--; } $count++; } } # 1}}} # function verify_args {{{1 sub verify_args { if (!$flac_tree_head || !$ogg_tree_head) { print "Not enough options!\n"; print $help_message; exit(-1); } my $temp = `which $id3_prog`; chomp $temp; if (!$mp3 and ! -e $temp) { print "Can't find id3v2! I need them!\n"; print "Please check your path, or install them\n"; exit(-1); } } # }}}1 # function process(item, item) {{{1 # Processes the two items sub process { my ($flac_item, $out_item) = @_; if ($debug) { print "Processing: $flac_item, $out_item\n"; } if (! -d $flac_item && ! -d $out_item) { # remove .mp3, .ogg, .flac from inputs {{{2 if (-e $flac_item || -e $out_item) { if ($flac_item =~ /flac$/) { $flac_item =~ s/(.+?)\.flac/$1/; } if ($out_item =~ /mp3$|ogg$/) { $out_item =~ s/(.+?)\.(ogg|mp3)/$1/; } } # }}}2 # Create a proper out_item {{{2 # If the out_item passed is a directory # Only should happen if an individual flac file is passed # as an argument if (-d $out_item) { if (! -d $out_item && $truth) { `mkdir -p "$out_item"`; } $temp_name = $flac_item; $temp_name =~ s/.*\/(.+)\.flac/$1/; if ($out_item =~ /\/$/) { $out_item = substr($out_item, 0, ((length $out_item) - 1)); } $out_item = "$out_item/$temp_name"; } # }}}2 # Both files exist {{{2 if ( (-e "$flac_item.flac") && (-e "$out_item.mp3" || -e "$out_item.ogg") ) { if (-e "$out_item.mp3") { $out_fn = "$out_item.mp3"; $ft = "mp3"; } else { $out_fn = "$out_item.ogg"; $ft = "ogg"; } if ($ft eq "ogg" and $mp3 and $refresh and $truth) { unlink "$out_fn"; } elsif ($ft eq "mp3" and !$mp3 and $refresh and $truth) { unlink "$out_fn"; } # Get the mtime of the flac $flac_mtime = (stat("$flac_item.flac"))[9]; # don't need to mtime check if the file doesn't exist # if no file, force no action by making the non-flac # newer if (-e "$out_fn") { $out_mtime = (stat("$out_item.$ft"))[9]; } else { $out_mtime = $flac_mtime + 12; } # if the flac is newer than the out_item we will # re-encode if -R is set if ($flac_mtime > $out_mtime && $truth && $refresh) { if (-e "$out_fn") { print "I am refreshing $out_item.$ft\n"; unlink "$out_fn"; } } elsif ($quiet < 1 && !$truth && $flac_mtime > $out_mtime && $refresh) { print "I would refresh $out_item.$ft\n"; } if ($retag && $truth) { refresh_tags($flac_item, $out_item, $ft); } elsif ($retag && $quiet < 1) { print "I would retag $out_item.$ft\n"; } } # }}}2 # out_item doesn't exist, encode {{{2 if ( (-e "$flac_item.flac") && !(-e "$out_item.mp3" xor -e "$out_item.ogg") && !$retag) { if ($truth) { if (!$mp3) { encode_file("$out_item.ogg", "$flac_item.flac"); } else { encode_mp3_file("$out_item.mp3", "$flac_item.flac"); } } else { print "I would encode $out_item\n"; } } # }}}2 # flac_item doesn't exist, kill {{{2 if ( (! -e "$flac_item.flac") && (-e "$out_item.mp3" || -e "$out_item.ogg") ) { if ($truth) { if ($purge) { print "Missing: $flac_item.flac\n"; if (-e "$out_item.mp3") { unlink "$out_item.mp3"; print "Deleting: $out_item.mp3\n"; } else { unlink "$out_item.ogg"; print "Deleting: $out_item.ogg\n"; } } elsif ($quiet < 1) { print "Unmatching entry: $out_item\n"; } } elsif ($purge && $quiet < 1) { print "I would remove $out_item\n"; } } # }}}2 } else { # case: Directories {{{2 #print "Directories\n"; my $flac_cdir = $flac_item; my $out_cdir = $out_item; if ((! -e $flac_item) && $purge && $truth) { print "I'm deleting $out_item\n"; `rm -rf "$out_item"`;; return; } elsif ($purge && $quiet < 1 && (! -e $flac_item)) { print "I would delete $out_item\n"; return; } if (! -e $out_item) { mkdir "$out_item"; } my @flac_dir_listing = read_dir($flac_item); my @out_dir_listing = read_dir($out_item); @flac_dir_listing = strip(\@flac_dir_listing, $flac_item); @out_dir_listing = strip(\@out_dir_listing, $out_item); my @unioned_dir_listing = union(\@flac_dir_listing, \@out_dir_listing); foreach $u_item (@unioned_dir_listing) { process("$flac_cdir/$u_item", "$out_cdir/$u_item"); } } # }}}2 return; } # }}}1 # Cleanup and Signal Handling {{{1 sub cleanup { if ($current_tmp_file && -e "$current_tmp_file") { unlink "$current_tmp_file"; } } sub sigdie_handler { local $SIG{INT} = 'IGNORE'; kill(INT, -$$); cleanup(); exit(-1); } # }}}1 # function refresh_tags (flac, out, filetype) {{{1 # update's tags values sub refresh_tags { my ($flac_item, $out_item, $ft) = @_; # get the tag values {{{2 @tags = `metaflac --list \"$flac_item.flac\"`; $title = ''; $artist = ''; $track_number = ''; $genre = ''; $comment = ''; $year = ''; $album = ''; chomp @tags; foreach $line (@tags) { if ($line =~ /comment.+?:\s(\w+?)=(.+)$/) { $match = $1; $saved = $2; $saved =~ s/"/\\"/g; if ($match =~ /^title$/i) { $title = $saved; } elsif ($match =~ /^artist$/i) { $artist = $saved; } elsif ($match =~ /^album$/i) { $album = $saved; } elsif ($match =~ /^tracknumber$/i) { $track_number = $saved; } elsif ($match =~ /^genre$/i) { $genre = $saved; } elsif ($match =~ /^comment$/i) { $comment = $saved; } elsif ($match =~ /^(year|date)$/i) { $year = $saved; } } } # }}}2 if ($ft eq "mp3") { $exec = "id3v2 --TIT2 \"$title\"" . " --TPE1 \"$artist\"" . " --TALB \"$album\"" . " --TRCK \"$track_number\"" . " --TYER \"$year\"" . " --comment \"$comment\"" . " --TCON \"$genre\"" . " \"$out_item.mp3\""; } else { $exec = "vorbiscomment -w" . " -t \"artist=$artist\"" . " -t \"album=$album\"" . " -t \"title=$title\"" . " -t \"tracknumber=$track_number\"" . " -t \"year=$year\"" . " -t \"genre=$genre\"" . " -t \"comment=$comment\""; $exec = $exec . " \"$out_item.ogg\""; } if ($quiet < 2) { print "exec: $exec\n"; print `$exec`; } else { `$exec`; } } # }}}1 # function encode_file($output_file, $input_file) {{{1 # Calls the encoder properly sub encode_file { my ($output_file, $input_file) = @_; $current_tmp_file = "$output_file.tmp.$$"; # Cleanup output/input file name $input_file =~ s/"/\\"/g; $input_file =~ s/\$/\\\$/g; $output_file =~ s/"/\\"/g; $output_file =~ s/\$/\\\$/g; $exec = "oggenc -q $quality -o \"$current_tmp_file\" \"$input_file\""; if ($nice_value) { $exec = "nice -n $nice_value " . $exec; } if ($quiet > 1) { `$exec`; } else { if ($debug) { print "$exec\n"; } print `$exec`; } `mv "$current_tmp_file" "$output_file"`; } # }}}1 # function encode_mp3_file($output_file, $input_file) {{{1 # Same as encode_file but for mp3's sub encode_mp3_file { my ($output_file, $input_file) = @_; $current_tmp_file = "$output_file.tmp.$$"; # Cleanup output/input file name $input_file =~ s/"/\\"/g; $input_file =~ s/\$/\\\$/g; $output_file =~ s/"/\\"/g; $output_file =~ s/\$/\\\$/g; # bitrate/encoding types {{{2 if ($enc_type eq "cbr") { #constant bitrate if ($quality >= 7) { $enc_type_use = "--preset insane"; } elsif ($quality == 6) { $enc_type_use = "--preset cbr 256"; } elsif ($quality == 5) { $enc_type_use = "--preset cbr 192"; } elsif ($quality == 4) { $enc_type_use = "--preset cbr 160"; } elsif ($quality == 3) { $enc_type_use = "--preset cbr 128"; } elsif ($quality == 2) { $enc_type_use = "--preset cbr 96"; } else { $enc_type_use = "--preset cbr 64"; } } elsif ($enc_type eq "vbr") { if ($quality >= 6) { $enc_type_use = "--preset extreme"; } elsif ($quality < 6 && $quality >= 3) { $enc_type_use = "--preset standard"; } else { $enc_type_use = "--preset medium"; } } else { if ($quality >= 7) { $enc_type_use = "--preset 320"; } elsif ($quality == 6) { $enc_type_use = "--preset 256"; } elsif ($quality == 5) { $enc_type_use = "--preset 192"; } elsif ($quality == 4) { $enc_type_use = "--preset 160"; } elsif ($quality == 3) { $enc_type_use = "--preset 128"; } elsif ($quality == 2) { $enc_type_use = "--preset 96"; } else { $enc_type_use = "--preset 64"; } } # force --preset insane if we have a quality of 10 pased if ($quality == 10) { $enc_type_use = "--preset insane"; } # }}}2 # get the tag values {{{2 @tags = `metaflac --list \"$input_file\"`; $title = ''; $artist = ''; $track_number = ''; $genre = ''; $comment = ''; $year = ''; $album = ''; chomp @tags; foreach $line (@tags) { if ($line =~ /comment.+?:\s(\w+?)=(.+)$/) { $match = $1; $saved = $2; $saved =~ s/"/\\"/g; $saved =~ s/\$/\\\$/g; if ($match =~ /^title$/i) { $title = $saved; } elsif ($match =~ /^artist$/i) { $artist = $saved; } elsif ($match =~ /^album$/i) { $album = $saved; } elsif ($match =~ /^tracknumber$/i) { $track_number = $saved; } elsif ($match =~ /^genre$/i) { $genre = $saved; } elsif ($match =~ /^comment$/i) { $comment = $saved; } elsif ($match =~ /^(year|date)$/i) { $year = $saved; } } } # }}}2 # Encode to temp file my $exec = "flac -d -c \"$input_file\" |"; if ($nice_value) { $exec .= " nice -n $nice_value lame $enc_type_use"; $exec = "nice -n $nice_value " . $exec; } else { $exec .= " lame $enc_type_use"; } $exec .= " --tt \"$title\" --ta \"$artist\" --tl \"$album\"" . " --ty \"$year\"" . " --tc \"$comment\" --tn \"$track_number\""; $exec .= " --add-id3v2 - \"$current_tmp_file\""; # Ensure we get the right genre my $tagline = 0; $tagline = "id3v2 --TCON \"$genre\" \"$current_tmp_file\""; # Execute if ($quiet > 1) { `$exec`; if ($tagline) { `$tagline`; } } else { if ($debug) { print "$exec\n"; } print `$exec`; if ($tagline) { if ($debug) { print "$tagline\n"; } print `$tagline`; } } # move temp_file to output_file `mv "$current_tmp_file" "$output_file"`; } # }}}1 # function read_dir(directory) {{{1 # Reads the contents of the given directory, ignoring dot files # Also removes any symlinks from the listing (unless -L passed) sub read_dir { my ($dir) = @_; opendir (CURRENT, $dir) or die ("couldn't open $dir for reading"); my @contents = grep !/^\..+?\z/, grep !/^[.][.]?\z/, readdir CURRENT; closedir CURRENT; my @to_return; foreach $item (@contents) { if (!$symlink and ! -l $item) { push @to_return, $item; } else { push @to_return, $item; } } return @to_return; } #}}}1 # function strip(directory listing) {{{1 # Removes all files that aren't flacc|mp3|ogg # and strips the extension off the remaining files sub strip { my ($dir_listing, $dir_prefix) = @_; my @cleaned_listing; foreach $item (@$dir_listing) { if (-d "$dir_prefix/$item" and !(-l "$dir_prefix/$item")) { push @cleaned_listing, $item; } else { if ($item =~ /flac$|mp3$|ogg$/) { $real_name = $item; $real_name =~ s/(.+?)\.(flac|ogg|mp3)/$1/; push @cleaned_listing, $real_name; } } } return @cleaned_listing; } # }}}1 # function union(list, list) {{{1 # Unions two lists sub union { my ($left, $right) = @_; #foreach $l (@left) { #print "\t\t$l\n"; #} my @unioned; my @leftn = sort @$left; my @rightn = sort @$right; #foreach $l (@leftn) { #print "\t\t$l\n"; #} $i = $j = 0; while ($i < scalar(@leftn) || $j < scalar(@rightn)) { if ($i >= scalar(@leftn)) { push @unioned, $rightn[$j]; $j++; } elsif ($j >= scalar(@rightn)) { push @unioned, $leftn[$i]; $i++; } elsif ($leftn[$i] eq $rightn[$j]) { push @unioned, $leftn[$i]; $i++; $j++; } elsif ($leftn[$i] lt $rightn[$j]) { push @unioned, $leftn[$i]; $i++; } else { push @unioned, $rightn[$j]; $j++; } } return @unioned; } # }}}1