shunt-tags/shunt_tags

697 lines
21 KiB
PHP
Executable File

#!/usr/bin/php
<?php
/**
* Copyright (2017-2018) The shunt_tags author(s)
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
* SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
* OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
* CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
// TODO kioclient integration
error_reporting(E_ALL);
ini_set('display_errors', 'On');
function fixup($input) {
$tmp = $input;
$extension = substr($tmp, strlen($tmp) - 4);
$tmp = substr($tmp, 0, strlen($tmp) - 4);
// Some files using period instead of space
if (strpos($tmp, ' ') === false) {
// Except when the period is real:
// - in a number string like EP##.#
// - ellipsis (...)
// Find all the \d.\d and store indexes
$matches = [];
preg_match_all('~\d\.\d~', $tmp, $matches, PREG_OFFSET_CAPTURE);
$match_ellipsis = [];
preg_match_all('~\.\.\.+~', $tmp, $match_ellipsis, PREG_OFFSET_CAPTURE);
// Replace
$tmp = str_replace('.', ' ', $tmp);
// Restore indexes
foreach($matches[0] as $match) {
$tmp[$match[1] + 1] = '.';
}
foreach($match_ellipsis[0] as $match) {
$tmp[$match[1] + 1] = '.';
$tmp[$match[1] + 2] = '.';
$tmp[$match[1] + 3] = '.'; // although we matched even longer sequences...
}
}
$fixed_point_iterate = function($tmp, $cb) {
$seen = [];
do {
$seen[$tmp] = true;
$tmp = $cb($tmp);
} while (! array_key_exists($tmp, $seen));
return $tmp;
};
// Some files using underscore instead of space
// Except - preserve "G_P", that's a known sub group
// Iterate until fixed point
$tmp = $fixed_point_iterate($tmp, function($tmp) {
return preg_replace_callback('~(._.)~', function($matches) {
if ($matches[1] == "G_P") {
return $matches[1]; // allow this case
} else {
return $matches[1][0].' '.$matches[1][2];
}
}, $tmp);
});
// Wrap loose tags
$tmp = preg_replace('~\b((?:gg\-|UTW\-)?THORA)\b~', ' [\1] ', $tmp);
$tmp = preg_replace('~\bXVID\b~', ' [XVID] ', $tmp);
$tmp = preg_replace('~\b1080p\b~', ' [1080p] ', $tmp);
$tmp = preg_replace('~([^p])v2\b~i', '\1 [v2] ', $tmp); // They could be part of the episode number - pull them out - BUT don't pull out "PV2"
$tmp = preg_replace('~([^p])v3\b~i', '\1 [v3] ', $tmp);
$tmp = preg_replace('~([^p])v4\b~i', '\1 [v4] ', $tmp);
if (strpos($tmp, '[a-s]') !== false) {
// This sub group puts a team member username in the filename
$tmp = preg_replace('~\\b(rs2|pball)\\b~', '[\1]', $tmp);
}
// Remove nesting
$drop_nested_tags = function($tmp) {
$depth = 0;
$ret = '';
for ($i = 0; $i < strlen($tmp); ++$i) {
if ($tmp[$i] == '[') {
$depth += 1;
if ($depth === 1) {
$ret .= '[';
}
} else if ($tmp[$i] == ']') {
$depth -= 1;
if ($depth === 0) {
$ret .= ']';
}
} else {
$ret .= $tmp[$i];
}
}
return $ret;
};
$tmp = $drop_nested_tags($tmp);
// Convert round brackets to square brackets only if they contain tags
$tmp = str_replace(' ASS(EN)', '', $tmp); // messes with the (=>[, not an important tag anyway
$tmp = preg_replace('~\(([^\)]*(?:G_P|B-A|R2J|NakamaSub|720|1080|264|BD|DVD|AAC|AC3|Dual Audio|XVID|divx|[A-F0-9]{8})[^\)]*)\)~i', '[\1]', $tmp);
// Work around nightmare sub group name
$tmp = str_replace('[Saiyan]BrollY', 'SaiyanBrollY', $tmp);
// Move tags from front to end
if ($tmp[0] == '[') {
$tmp = preg_replace('~^(\[.+?\])\s?([^\[]+)(.*)$~', '\2 \1 \3', $tmp);
}
// Cleanup
$cleanup = [
'][' => ' ',
'] [' => ' ',
'] - [' => ' ',
' ' => ' ',
' - [' => '[',
'()' => '',
'( )' => '',
'720x480' => '480p',
'1280x720' => '720p',
'1920x1080' => '1080p',
'BluRay' => 'BD',
'Blu-Ray' => 'BD',
'BDrip' => 'BD',
'bd-rip' => 'BD',
'Clean Ending' => 'NCED',
'Clean Opening' => 'NCOP',
'Creditless Opening' => 'NCOP',
'Creditless Ending' => 'NCED',
'Creditless OP' => 'NCOP',
'Creditless ED' => 'NCED',
];
$tmp = $fixed_point_iterate($tmp, function($tmp) use ($cleanup) {
// Clean up tags
$tmp = str_replace(array_keys($cleanup), array_values($cleanup), $tmp);
// Move tags from middle to end
$tmp = preg_replace('~([^\[\]]+)(\[[^\]]+?\])([^\[\]]+)~', '\1\3\2', $tmp);
return $tmp;
});
// Cleanup commas inside tags ( [720p,Bluray] => [720p Bluray] )
$tmp = preg_replace_callback('~\[(.+)\]~', function($matches) { return str_replace(',', ' ', $matches[0]); }, $tmp);
// Remove trailing spaces before file extension
$tmp = trim($tmp); //$tmp = preg_replace('~\s+(\..{3})$~', '\1', $tmp);
// Episode numbers
// #xx, Exx, EPxx, Ep.XX, Ep. XX, EXXv2... but not a year like 1970 or 2013
$add_episode_hyphen_if_not_year = function($matches) {
$is_year = (intval($matches[1]) > 1000);
if ($is_year) {
return $matches[0]; // as-is
} else {
return ' - '.$matches[1];
}
};
$tmp = preg_replace_callback('~ (?:#|E(?:P(?:\. ?)?)?)(\d[\dv]*)\b~i', $add_episode_hyphen_if_not_year, $tmp);
$got_episode_number = function($tmp) {
if (preg_match('~ \- S?[\d]+~', $tmp)) {
return true;
}
// Allow "Lesson XX" (GTO format), "OVA XX", "ONA XX"
if (preg_match('~ \- (?:Lesson|OVA|ONA) [\d]+~', $tmp)) {
return true;
}
return false;
};
if (! $got_episode_number($tmp)) {
$tmp = preg_replace_callback('~ episode (\d+)~', function($matches) { return sprintf(' - %02d', $matches[1]); }, $tmp);
}
if (! $got_episode_number($tmp)) {
// Last ditch - replace any lone 1+ digit number (but also support v2 and 0.5 episodes)
$tmp = preg_replace_callback('~ (\d\d*(?:[v\.]\d\d?)?)\b~', $add_episode_hyphen_if_not_year, $tmp);
}
// Two-digit episode numbers even for 0.5 and 6.75 episodes
$tmp = preg_replace('~ - (\d)\b~', ' - 0\1', $tmp);
$tmp = preg_replace('~ - (\d\.\d\d?)\b~', ' - 0\1', $tmp);
// Anything after the episode number but before the tag is an episode title
// Enforce dash between them if it's not there already
$tmp = preg_replace('~ - (\d[\dv\.]+) ([^\[\-\(])~', ' - \1 - \2', $tmp);
// Fix double dashes
//$tmp = str_replace('- -', '-', $tmp);
// Remove unnecessary quotes
$tmp = preg_replace("~ - '(.+?)' ([-\\[])~", ' - \1 \2', $tmp);
// From the first [ to the last ], no other [] should appear within, and there should be no extraneous spaces
$tmp = preg_replace_callback('~^([^\[]*?)\[(.+)\]~', function($matches) {
return $matches[1].'['.trim(str_replace(['[', ']', ' ', ' - ', '- ', '.'], [' ', ' ', ' ', ' ', ' ', ' '], $matches[2])).']';
}, $tmp);
// There's always a space before the tag
$tmp = preg_replace('~([^ ])\[~', '\1 [', $tmp);
// Title-case for sections (delimited by -/~)
$fix_title_cases = function($str) {
// First section: all words titlecase except the/and/or unless first word
$titlecase = function($str) {
$words = explode(" ", $str);
for($i = 0; $i < count($words); ++$i) {
if (strlen($words[$i]) == 0) {
continue;
}
if (
$i == 0 ||
!in_array($words[$i], ['and', 'or', 'of', 'on', 'the', 'in', 'no', 'a'])
) {
$words[$i][0] = strtoupper($words[$i][0]);
}
}
return implode(" ", $words);
};
// Second section: enforce first character uppercase if not already
$weaktitlecase = function($str) {
if (!ctype_upper($str[0])) {
$str[0] = strtoupper($str[0]);
}
return $str;
};
// Only process up to the first [ (if one exists)
$spos = strpos($str, '[');
$consider = substr($str, 0, $spos);
$sections = preg_split('~( - | \~ )~', $consider, -1, PREG_SPLIT_DELIM_CAPTURE);
#var_dump($sections);
#die();
for($i = 0; $i < count($sections); $i += 2) {
if (strlen($sections[$i]) == 0) {
continue;
}
if ($i == 0) {
$sections[$i] = $titlecase($sections[$i]);
} else {
$sections[$i] = $weaktitlecase($sections[$i]);
}
}
return implode("", $sections).substr($str, $spos);
};
$tmp = $fix_title_cases($tmp);
// Special fixups:
// Fixup [Saiyan]BrollY, that's a sub group
$tmp = str_replace('SaiyanBrollY', '[Saiyan]BrollY', $tmp);
// Fixup terrible case with [m.3.3.w] sub group being detected as an episode number
// It would be preferable for this to not happen in the first place
$tmp = str_replace('m 03 3 w', 'm.3.3.w', $tmp);
// Fixup anime_fin, that's a sub group
$tmp = str_replace('[anime fin', '[anime_fin', $tmp);
// Fixup "No. 6", that's a show title
$tmp = str_replace('No. - 06 - ', 'No. 6 - ', $tmp);
// Fixup "Oedo 808", that's a show title
$tmp = str_replace('Oedo - 808', 'Oedo 808', $tmp);
// Fixup "h264-720p"
$tmp = str_replace("h264-", "h264 ", $tmp);
// Reinstate file extension
$tmp = $tmp.$extension;
// Done
return $tmp;
}
function tests() {
$tests = [
"[a-s]_you're_under_arrest_-_07_-_strike_man_~_defender_of_justice__rs2_[54AEE83C].mkv"
=> "You're Under Arrest - 07 - Strike man ~ Defender of justice [a-s rs2 54AEE83C].mkv"
,
"[a-s]_a_certain_scientific_railgun_-_17_-_tsuzuri's_summer_vacation__pball_[1080p_bd-rip][2C2AE93D].mkv"
=> "A Certain Scientific Railgun - 17 - Tsuzuri's summer vacation [a-s pball 1080p BD 2C2AE93D].mkv"
,
'[gg]_Valvrave_the_Liberator_-_12_[F9F3F5C5].mkv'
=> 'Valvrave the Liberator - 12 [gg F9F3F5C5].mkv'
,
'Poyopoyo Kansatsu Nikki - 16 [HorribleSubs][FShahid][46B9A7B5].mkv'
=> 'Poyopoyo Kansatsu Nikki - 16 [HorribleSubs FShahid 46B9A7B5].mkv'
,
'[AHQ] Gundam Seed - 42 - Lacus Strikes.mkv'
=> 'Gundam Seed - 42 - Lacus Strikes [AHQ].mkv'
,
'[Frenchies-Mux]_True_Mazinger_Impact!_Chapter_Z_03_(720p)[7EBA0032].mkv'
=> 'True Mazinger Impact! Chapter Z - 03 [Frenchies-Mux 720p 7EBA0032].mkv'
,
'[RUELL-Next] Natsume Yuujinchou S4 EP09 (BD 1280x720 x264 AAC ASS(EN)) [BE846FE4].mkv'
=> 'Natsume Yuujinchou S4 - 09 [RUELL-Next BD 720p x264 AAC BE846FE4].mkv'
,
'[Nubles] Space Battleship Yamato 2199 (2012) episode 21 [720p 10 bit AAC][C6884514].mkv'
=> 'Space Battleship Yamato 2199 (2012) - 21 [Nubles 720p 10 bit AAC C6884514].mkv',
'[Nubles] Space Battleship Yamato 2199 (2012) episode 8 (720p 10 bit AAC).mkv'
=> 'Space Battleship Yamato 2199 (2012) - 08 [Nubles 720p 10 bit AAC].mkv'
,
"Salaryman_Kintaro_-_15_[A-Et]_(0F83C5FF).mkv"
=> "Salaryman Kintaro - 15 [A-Et 0F83C5FF].mkv"
,
'[Elysium]Shiki.EP15(BD.720p.Hi10P.AAC)[0437F1F6].mkv'
=> 'Shiki - 15 [Elysium BD 720p Hi10P AAC 0437F1F6].mkv'
,
'[Elysium]Shiki.EP20.5(BD.720p.Hi10P.AAC)[BFE4B6BF].mkv'
=> 'Shiki - 20.5 [Elysium BD 720p Hi10P AAC BFE4B6BF].mkv'
,
'(G_P) Phoenix 03(x264)(17A173E7).mkv'
=> 'Phoenix - 03 [G_P x264 17A173E7].mkv'
,
'[Commie] Teekyuu - OVA 1 [BD 720p AAC] [58F19BF2].mkv'
=> 'Teekyuu - OVA 1 [Commie BD 720p AAC 58F19BF2].mkv'
,
'[Kametsu]_Your_Lie_in_April_11v2_[Blu-Ray][720p][Hi10][588D1B1F].mkv'
=> 'Your Lie in April - 11 [Kametsu v2 BD 720p Hi10 588D1B1F].mkv'
,
'Patlabor.TV.38v2.(Dual.Audio).XVID.[AM].ogm'
=> 'Patlabor TV - 38 [v2 Dual Audio XVID AM].ogm'
,
'Laputa_Castle_in_the_Sky_(1986)_[720p,BluRay,x264]_-_THORA.mkv'
=> 'Laputa Castle in the Sky (1986) [720p BD x264 THORA].mkv'
,
'[HorribleSubs] 91 Days - 7.5 [720p].mkv'
=> '91 Days - 07.5 [HorribleSubs 720p].mkv'
,
'Code_Geass_Ep01_The_Day_the_Demon_was_Born_[720p,BluRay,x264]_-_gg-THORA.mkv'
=> 'Code Geass - 01 - The Day the Demon was Born [720p BD x264 gg-THORA].mkv'
,
'[Z-Z] Blood + Ep. 01 - First Kiss.mkv'
=> 'Blood + - 01 - First Kiss [Z-Z].mkv'
,
'Code_Geass_Picture_Drama_6.75_[720p,BluRay,x264]_-_gg-THORA v2.mkv'
=> 'Code Geass Picture Drama - 06.75 [720p BD x264 gg-THORA v2].mkv'
,
'Baccano - 14 (OVA).mkv'
=> 'Baccano - 14 (OVA).mkv' // no change expected
,
'[Jarzka] Cromartie High School 05 - Sentimental Bus [480p 10bit X264 DVD Dual-Audio] [A63E3F52].mkv'
=> 'Cromartie High School - 05 - Sentimental Bus [Jarzka 480p 10bit X264 DVD Dual-Audio A63E3F52].mkv'
,
'Area 88 OVA - 01 [Blitz flawed 5EF8A0E2].mkv'
=> 'Area 88 OVA - 01 [Blitz flawed 5EF8A0E2].mkv' // no change expected
,
'[m.3.3.w]_Genshiken_Nidaime_no_Roku_-_OAD_[v0][A6EF7886].mkv'
=> 'Genshiken Nidaime no Roku - OAD [m.3.3.w v0 A6EF7886].mkv'
,
'Gintama - 21 [Rumbel XviD 3E184082 v2].avi'
=> 'Gintama - 21 [Rumbel XviD 3E184082 v2].avi' // no change expected
,
'Patlabor 1 (1989) [THORA].mkv'
=> 'Patlabor - 01 (1989) [THORA].mkv'
,
'[ACX]Immortal_Grand_Prix_-_01_-_Time_to_Shine_[[Saiyan]BrollY]_[7A73D794].mkv'
=> 'Immortal Grand Prix - 01 - Time to Shine [ACX [Saiyan]BrollY 7A73D794].mkv'
,
'[AnimeNOW] Danshi Koukousei no Nichijou - OP (BD 1280x720 10-bit x264 AAC) [429C5783].mkv'
=> 'Danshi Koukousei no Nichijou - OP [AnimeNOW BD 720p 10-bit x264 AAC 429C5783].mkv'
,
'(B-A)Great_Teacher_Onizuka_-_Lesson_01_(6F68CC7E).mkv'
=> 'Great Teacher Onizuka - Lesson 01 [B-A 6F68CC7E].mkv'
,
"[OZC]Mobile Suit Gundam - The 08th MS Team Blu-ray Box E01 'War for Two' [720p].mkv"
=> "Mobile Suit Gundam - The 08th MS Team Blu-ray Box - 01 - War for Two [OZC 720p].mkv"
,
'Gundam SEED Destiny 14[AEF97E04].avi'
=> 'Gundam SEED Destiny - 14 [AEF97E04].avi'
,
'Gundam SEED Destiny 1.avi'
=> 'Gundam SEED Destiny - 01.avi'
,
'[Exiled-Destiny]_Girls_Bravo_Ep01v3_[R2_Video]_(94D81F22).mkv'
=> 'Girls Bravo - 01 [Exiled-Destiny v3 R2 Video 94D81F22].mkv'
,
'[ACX]Immortal_Grand_Prix_-_11_-_And_Then_..._[[Saiyan]BrollY]_[50EC0CBC].mkv'
=> 'Immortal Grand Prix - 11 - And Then ... [ACX [Saiyan]BrollY 50EC0CBC].mkv'
,
'[DmonHiro] Kyousougiga #00v2 - Recap (BD, 720p) [47986C5A].mkv'
=> 'Kyousougiga - 00 - Recap [DmonHiro v2 BD 720p 47986C5A].mkv'
,
'Legend.of.the.Galactic.Heroes.110.[x264.720p.10bit.AAC].mkv'
=> 'Legend of the Galactic Heroes - 110 [x264 720p 10bit AAC].mkv'
,
'Minipato OVA [anime_fin 2906CE7D].avi'
=> 'Minipato OVA [anime_fin 2906CE7D].avi' // no change expected
,
'[Doki] No. 6 - NCOP (1280x720 h264 BD AAC) [0A4A8A9B].mkv'
=> 'No. 6 - NCOP [Doki 720p h264 BD AAC 0A4A8A9B].mkv'
,
'[RG Genshiken] Gintama - Creditless Ending ep.088-095, 097-099 [DVDRip 704x528 x264 PCM].mkv'
=> 'Gintama - NCED - 088-095, 097-099 [RG Genshiken DVDRip 704x528 x264 PCM].mkv'
,
'Cyber City Oedo 808 - Art Gallery (XviD)[anibalance][4C7F6645].mkv'
=> 'Cyber City Oedo 808 - Art Gallery [XviD anibalance 4C7F6645].mkv'
,
'Death Billiards (Anime Mirai 2013) [gg 29BE9711].mkv'
=> 'Death Billiards (Anime Mirai 2013) [gg 29BE9711].mkv' // no change expected
,
'King of Thorn (Ibara no Ou) [Harth-QTS-v2].mkv'
=> 'King of Thorn (Ibara no Ou) [Harth-QTS v2].mkv'
,
'Lupin III - [fong] Lupin III - Kutabare! Nostradamus [BDrip.720p.10bit.DualAudio].mkv'
=> 'Lupin III Lupin III - Kutabare! Nostradamus [fong BD 720p 10bit DualAudio].mkv'
,
"[JoJo]_Jojo's_Bizarre_Adventure_-_20_[BD][h264-720p_AAC][054C181A].mkv"
=> "Jojo's Bizarre Adventure - 20 [JoJo BD h264 720p AAC 054C181A].mkv"
,
"Ghost in the Shell Arise - 04 [THORA 1080p].mkv"
=> "Ghost in the Shell Arise - 04 [THORA 1080p].mkv" // no change expected
,
"Cowboy_Bebop_Knockin'_on_Heaven's_Door_(2001)_[1080p,BluRay,x264,flac]_-_THORA.mkv"
=> "Cowboy Bebop Knockin' on Heaven's Door (2001) [1080p BD x264 flac THORA].mkv"
,
'[EG]Turn-A_Gundam_01_V2[E8F575A6].mkv'
=> 'Turn-A Gundam - 01 [EG v2 E8F575A6].mkv'
,
'PV2.mp4'
=> 'PV2.mp4' // no change expected
,
'[DmonHiro] Kyousougiga - Clean Ending (v2) (BD, 720p) [F1849601].mkv'
=> 'Kyousougiga - NCED [DmonHiro v2 BD 720p F1849601].mkv'
,
];
$any_failures = false;
foreach($tests as $input => $expected) {
$result = fixup($input);
if ($result !== $expected) {
echo "Test failed\n";
echo " Input : $input \n";
echo " Got : $result \n";
echo " Expected : $expected \n";
$any_failures = true;
}
}
if (! $any_failures) {
echo "All tests passed.\n";
}
return !$any_failures;
}
function findFilesNeedingReplacement($basedir, $recursive, $quiet) {
$iterator = $recursive
? new RecursiveIteratorIterator(new RecursiveDirectoryIterator($basedir), RecursiveIteratorIterator::SELF_FIRST)
: new DirectoryIterator($basedir)
;
if (! $quiet) {
echo "Scanning...";
}
$ct = 0;
$replacements = [];
foreach ($iterator as $file) {
if (! $quiet) {
if (++$ct % 100 == 0) {
echo ".";
}
}
if ($file->isDir()) {
continue;
}
$fname = $file->getFilename();
if (! preg_match("~\\.(?:mkv|mp4|avi|ogm|ogv)$~", $fname)) {
continue;
}
$new = fixup($fname);
if ($new !== $fname) {
$replacements[] = [
$file->getPath(),
$fname,
$new
];
}
}
if (! $quiet) {
echo "\n";
}
// Alphabetic sort
usort($replacements, function($a, $b) {
$pc = strcasecmp($a[0], $b[0]);
if ($pc !== 0) {
return $pc;
}
return strcasecmp($a[1], $b[1]);
});
return $replacements;
}
function displayReplacementsTable(array $replacements) {
$max_length = 10;
foreach($replacements as $r) {
$display_width = mb_strwidth($r[1], 'UTF-8'); // strlen($r[1])
$max_length = max($max_length, $display_width);
}
foreach($replacements as $r) {
echo sprintf("%-{$max_length}s %s\n", $r[1], $r[2]);
}
}
function applyReplacements(array $replacements, $echo_undo_script) {
$any_errors = false;
if ($echo_undo_script) {
echo "#!/bin/bash\nset -eu\n\n";
}
foreach($replacements as $r) {
$src_path = $r[0].'/'.$r[1];
$dest_path = $r[0].'/'.$r[2];
if (file_exists($dest_path)) {
if ($echo_undo_script) {
echo "# ";
}
echo "Skipping {$r[1]} (destination path already exists)\n";
$any_errors = true;
continue;
}
rename($src_path, $dest_path);
if ($echo_undo_script) {
echo "mv ".escapeshellarg($dest_path)." ".escapeshellarg($src_path)."\n";
}
}
return $any_errors;
}
function usage() {
echo <<<EOD
Usage:
shunt_tags [OPTIONS]
Options:
-y --accept Apply updates without prompting. (false)
-d --directory DIR Set directory. (pwd)
--display table|dump|none Choose display mode. ('table')
-n --dry-run Display proposed updates and exit. (false)
--help, --usage Display this message
--interactive Prompt whether to apply updates. (true)
-q --quiet Quiet mode hides scanning progress. (false)
-qq --quiet2 Equivalent to "-q --display none"
-r --recursive Apply to subdirectories. (false)
--test-suite Run test suite and exit
--undo Display an undo script
EOD;
die(0);
}
function main($argv) {
$recursive = false;
$directory = getcwd(); // __DIR__;
$dryrun = false;
$shouldprompt = true;
$displaymode = "table";
$quiet = false;
$echo_undo_script = false;
for($i = 0; $i < count($argv); ++$i) {
if ($argv[$i] == "-r" || $argv[$i] == "--recursive") {
$recursive = true;
} else if ($argv[$i] == "-d" || $argv[$i] == "-directory") {
$directory = $argv[++$i];
} else if ($argv[$i] == "--test-suite") {
die(tests() ? 0 : 1);
} else if ($argv[$i] == "--interactive") {
$shouldprompt = true;
} else if ($argv[$i] == "-y" || $argv[$i] == "--accept") {
$shouldprompt = false;
} else if ($argv[$i] == "-n" || $argv[$i] == "--dry-run") {
$dryrun = true;
} else if ($argv[$i] == "--display") {
$displaymode = $argv[++$i];
} else if ($argv[$i] == "-q" || $argv[$i] == "--quiet") {
$quiet = true;
} else if ($argv[$i] == "-qq" || $argv[$i] == "--quiet2") {
$quiet = true;
$displaymode = "none";
} else if ($argv[$i] == "--undo") {
$echo_undo_script = true;
} else if ($argv[$i] == "--help" || $argv[$i] == "--usage") {
usage();
} else {
echo "Unknown argument {$argv[$i]} (try --help)\n";
die(1);
}
}
$replacements = findFilesNeedingReplacement($directory, $recursive, $quiet);
if (count($replacements) === 0) {
echo "No candidates for renaming.\n";
die(0);
}
switch($displaymode) {
case "table": {
displayReplacementsTable($replacements);
} break;
case "dump": {
var_dump($replacements);
} break;
case "none": break;
default: {
echo "Unknown display mode {$displaymode}\n";
die(1);
}
}
if ($dryrun) {
die(0);
}
if ($shouldprompt) {
echo "Apply changes? [y/N] > ";
$line = trim(fgets(STDIN));
if ($line !== "y") {
echo "Not applying changes.\n";
die(0);
}
}
$any_errors = applyReplacements($replacements, $echo_undo_script);
die($any_errors ? 1 : 0);
}
main(array_slice($_SERVER['argv'], 1));