Grundtes: kör inga av de kommandon du får föreslagna på "skarp" data utan att ha backup.
Du vill om jag tolkar det rätt ta bort portionen av filnamnet som börjar med en sträng till och med första förekomsten av -
med mellanslag runt bindestrecket (om denna sträng förekommer, men du verkar anta att den alltid gör det). Ett ruskigt snabbt sätt att göra detta (vilket kopplar till ett inlägg jag skrev för bara någon dag sedan) är att kombinera Bashs globstar
-alternativ med det Perlbaserade rename
-verktyget och köra något i stil med:
shopt -s globstar
rename -n 's#/[^/]+? - ([^/]+)$#/$1#' ~/mp3-temp/**/*\ -\ *.mp3
Ovanstående gick snabbt att skriva, men kanske även kräver en förklaring, som tar längre tid :
globstar
gör att vi kan använda **
-konstruktionen för att matcha en okänd mängd underkataloger (se man bash
).
I Debianbaserade system kommer rename
med paketet rename
, som installerar /usr/bin/file-rename
som även länkas som /usr/bin/rename
. Det är ett rekommenderat paket för paketet perl
som säkerligen finns installerat, så vanligen finns rename
på plats utan att man behövt göra något. Se mitt tidigare länkade inlägg för lite mer information.
-n
i ovanstående exempel gör att det inte genomförs några ändringar, utan det bara skrivs ut vilka ändringar som skulle göras. Ta bort -n
om det verkar OK efter någon testkörning, men se efter noggrant.
Den kryptiska texten är ett substitutionsuttryck i Perls syntax för reguljära uttryck.
s/foo/bar/
betyder att vi vill byta ut en förekomst av foo
mot bar
. /
kan bytas ut till någon annan avgränsare, vilket jag använder här för att inte "krocka" med /
som jag vill matcha i mönstret. Jag använder i stället #
som avgränsare.
/
betyder därmed här att jag vill matcha en katalogavskiljare.
[^/]+? -
betyder att jag därefter vill matcha "inte en katalogavskiljare" ([^/]
) "en eller fler gånger" (+
) följt av -
, där frågetecknet som läggs till matchningsoperatorn +?
gör matchningen "non-greedy" för att stanna vid första förekomsten av -
.
([^/]+)
betyder att jag återigen vill matcha "inte en katalogavskiljare" "en eller fler gånger" (nu standardalternativet "greedy" (det spelar visserligen ingen roll nu då jag senare förankrar med radslut)), men att jag dessutom vill fånga det som matchar och lagra det i en variabel som jag kan återanvända i mitt substitutionsuttryck.
$
betyder i matchningsdelen av uttrycket slutet av strängen.
/$1
i substitutionsmönstret betyder att jag vill ersätta hela mönstret som träffade med en katalogavskiljare följt av det som matchades inom parenteserna ("den första" i och med ettan, men här har jag bara en fångad grupp).
Att jag vill definiera slutet av strängen och hela tiden exkludera katalogavskiljare efter att ha förankrat den initiala är för att vi bara vill döpa om fildelen av namnet, och inte råka träffa en katalog "på vägen" som innehåller -
.
~/mp3-temp/**/*\ -\ *.mp3
ger (med globstar
aktiverat) alla filer eller kataloger i ~/mp3-temp
eller någon av dess underkataloger som innehåller -
och har ändelsen .mp3
. Notera att det alltså skulle kunna matcha en katalog om du har någon sådan som träffar detta mönster av någon anledning — det skulle i sådana fall vara ett problem, men jag antar att det inte är fallet här.
Dold text
Detta bör gå snabbt då den initiala filnamnslistan skapas direkt i Bash som redan körs, varpå den skickas till en enda subprocess som gör alla substitutioner. Ifall du lyckas matcha fler filer (eller ja, rent tekniskt −1 om vi räknar med mönstret) än vad getconf ARG_MAX
har för värde (2 097 152 på mitt system…) så kommer Bash klaga, varpå man i stället skulle kunna lösa filletningen med find
. Jag ville använda globstar
för att visa att man ofta kan klara sig utan sådant, dock.
En motsvarande lösning med (GNU) find
skulle kunna vara:
find ~/mp3-temp -type f -name '* - *.mp3' -exec rename -n 's#/[^/]+? - ([^/]+)$#/$1#' {} +
(see man find
, sök efter -exec command
) med fördelen att vi inte riskerar att träffa kataloger eller slå i taket vad gäller ARG_MAX
(-exec command +
håller koll på detta automatiskt och skapar så många extra processer som behövs ifall gränsen skulle överskridas).
Vill man av någon anledning inte använda +
-operatorn till -exec
utan i stället böka med xargs
(det kan finnas anledningar i mer komplexa fall, men inte här) så skulle det kunna se ut enligt:
find ~/mp3-temp -type f -name '* - *.mp3' -print0 | xargs -0 rename -n 's#/[^/]+? - ([^/]+)$#/$1#'
som också löser ARG_MAX
-problem på samma sätt.
Att använda -exec command \;
skulle däremot gå bra mycket långsammare, då det skulle skapa en process per matchad fil. Sitter man på ett system som inte har GNU-versionen av xargs
som stöder -0
för att använda null
som argumentavskiljare så kanske -exec command \;
trots allt är enklast att använda för att eliminera problem med märkliga filnamn.
Vad gäller det sistnämnda så är det otroligt vanligt att se kommandoförslag på nätet som inte tar hänsyn till udda filnamn — ofta får man vara glad om de ens hanterar filnamn med mellanslag, trots att de hävdar sig vara generella. Ovanstående förslag är alla tänkta att hantera udda filnamn efter bästa förmåga.
Vill du lösa det utan rename
så går det att göra i ren Bash mer likt det försök du började med, fast automatiserat och mer robust, genom att exempelvis återigen utnyttja globstar
och med fördel även nullglob
(se man bash
):
shopt -s globstar nullglob
for i in ~/mp3_temp/**/; do
cd -- "$i"
for j in *\ -\ *.mp3; do
echo mv -- "$j" "${j#* - }"
done
# cd - >/dev/null # Se notis
done
där jag lagt till echo
för att återigen bara skriva ut vad som skulle ske, snarare än att "göra det" för att kunna testa att det faktiskt fungerar som man vill innan man tar bort echo
och kör på (notera att echo
"äter" citationstecknen i utskriften så som jag skrivit ovan, men att det kommer "bli rätt" i skarpt läge).
Ytterligare försök till förklaring:
globstar
har vi presenterat tidigare. nullglob
(se återigen man bash
) gör att ett mönster som inte matchar tas bort, snarare än står kvar som en "literal" (man kan diskutera om inte detta borde varit default-läget, men men).
~/mp3-temp/**/
— den avslutande katalogavskiljaren gör att vi bara matchar just underkataloger här.
cd -- "$i"
— man bör alltid sätta citationstecken runt variabler som kan innehålla mellanslag om man vill använda dem som enstaka argument. --
är ytterligare en vanlig defensiv konstruktion som säger till verktyg att man nu inte tar emot fler växlar utan bara positionella argument. Utan detta skulle en katalog med namn som en växel (typiskt de som börjar med -
) göra oväntade saker.
När vi gått till en katalog så loopar vi över alla .mp3
-filer i densamma som innehåller strängen vi vill byta ut. nullglob
gör att vi bara går vidare ifall mönstret träffar, vilket är bra. Utan detta så hade Bash gått in i loopen med $j
satt till strängen * - *.mp3
, vilket bara ger felmeddelanden.
${j#* - }
— detta är en skalsubstitution som kommer ta bort alla tecken inklusive den första matchningen av -
i variabeln $j
. #
gör det "non-greedy", ##
gör det "greedy", %
och %%
gör motsvarande saker men tittar på strängen från andra hållet.
cd -
gör att vi går tillbaka till ursprungskatalogen igen inför vårt nästa katalogbyte, för att de potentiellt relativa katalogvägar som matchats ska stämma. I detta fall så matchar jag mot den absoluta vägen ~/mp3-temp/**/
vilket gör att vi loopar över absoluta katalogsökvägar så att denna konstruktion inte behövs (och därmed är bortkommenterad), men jag visar tekniken ändå ifall någon dyker på detta exempel i framtiden, modifierar det och undrar varför allt kollapsar. >/dev/null
används för att tysta ned cd -
som annars glatt säger vart den går, vilket blir tråkigt när det handlar om hundratals kataloger.
Dold text
Jag har inte kört ovanstående kommandon själv förutom på några enkla testfiler under tiden jag skrev, så som sagt: testa saker innan de körs skarpt. Det är många små detaljer att tänka på ("non-greedy", konstiga filnamn, kataloger vs filer, GNU vs POSIX, nullglob
, …), och det vore smått sensationellt om jag inte missat någon ovan. Kanske jag också missat någon möjlig förenkling i matchningen.
Det blev ett längre inlägg än om jag bara skrivit shopt -s globstar; rename -n 's#/[^/]+? - ([^/]+)$#/$1#' ~/mp3-temp/**/*\ -\ *.mp3
, men förhoppningsvis vettigare .