Jak zapętlać pliki w katalogu i zmieniać ścieżkę oraz dodawać sufiks do nazwy pliku

562

Muszę napisać skrypt, który uruchamia mój program z różnymi argumentami, ale jestem nowy w Bash. Zaczynam mój program od:

./MyProgram.exe Data/data1.txt [Logs/data1_Log.txt].

Oto pseudokod tego, co chcę zrobić:

for each filename in /Data do
  for int i = 0, i = 3, i++
    ./MyProgram.exe Data/filename.txt Logs/filename_Log{i}.txt
  end for
end for

Naprawdę więc zastanawiam się, jak utworzyć drugi argument z pierwszego, więc wygląda to jak dataABCD_Log1.txt i uruchomić mój program.

Dobrobobr
źródło

Odpowiedzi:

736

Kilka uwag po pierwsze: kiedy używasz Data/data1.txtjako argumentu, czy naprawdę powinien to być /Data/data1.txt(z wiodącym ukośnikiem)? Ponadto, czy zewnętrzna pętla powinna skanować tylko pliki .txt, czy wszystkie pliki w / Data? Oto odpowiedź, przy założeniu, że /Data/data1.txttylko pliki .txt:

#!/bin/bash
for filename in /Data/*.txt; do
    for ((i=0; i<=3; i++)); do
        ./MyProgram.exe "$filename" "Logs/$(basename "$filename" .txt)_Log$i.txt"
    done
done

Uwagi:

  • /Data/*.txtrozwija się do ścieżek plików tekstowych w / Data (w tym / Data / part)
  • $( ... ) uruchamia polecenie powłoki i wstawia dane wyjściowe w tym punkcie wiersza poleceń
  • basename somepath .txtwypisuje podstawową część gdzieś, z .txt usuniętym z końca (np. /Data/file.txt-> file)

Jeśli chcesz uruchomić MyProgram Data/file.txtzamiast /Data/file.txt, użyj, "${filename#/}"aby usunąć wiodący ukośnik. Z drugiej strony, jeśli tak naprawdę Datanie /Datachcesz skanować, po prostu użyj for filename in Data/*.txt.

Gordon Davisson
źródło
1
Najwyraźniej basename -sjest niestandardowym rozszerzeniem - zedytuję odpowiedź, aby użyć standardowej składni.
Gordon Davisson,
21
Jeśli nie znaleziono plików / nie pasuje do znaku zastępczego, znajduję, że blok wykonywania pętli for jest nadal wprowadzany raz z nazwą pliku = "/Data/*.txt". Jak mogę tego uniknąć?
Oliver Pearmain
20
@OliverPearmain Albo użyj shopt -s nullglobprzed pętlą (i shopt -u nullglobpóźniej, aby uniknąć problemów później), albo dodaj if [[ ! -e "$filename ]]; then continue; fina początku pętli, aby pominąć nieistniejące pliki.
Gordon Davisson,
9
Nie działa to, gdy w nazwie znajdują się pliki zawierające spacje.
Isa Hassen
4
@ Isa Powinno działać z białymi spacjami, o ile wszystkie podwójne cudzysłowy są na miejscu. Pozostaw dowolne z podwójnych cudzysłowów, a będziesz mieć problemy z białymi znakami.
Gordon Davisson
344

Przepraszamy za nekromancję w wątku, ale za każdym razem, gdy wykonujesz iterację po plikach przez globbing, dobrą praktyką jest unikanie przypadkowego narożnika, w którym glob nie jest zgodny (co powoduje, że zmienna pętli rozwija się do (niepasującego) łańcucha wzorca globu).

Na przykład:

for filename in Data/*.txt; do
    [ -e "$filename" ] || continue
    # ... rest of the loop body
done

Odniesienie: Bash Pitfalls

Cong Ma
źródło
8
To wciąż aktualne ostrzeżenie. Myślałem, że źle utworzyłem skrypt, ale miałem rozszerzenie małych liter zamiast wielkich liter, nie znalazłem żadnych plików i zwróciłem wzorzec glob. ugh.
RufusVS
5
Ponieważ używany jest tag Bash: po to shopt nullglobjest! (lub shopt failglobmoże być również użyty, w zależności od pożądanego zachowania).
gniourf_gniourf
Poza tym sprawdza również, czy pliki zostały usunięte podczas przetwarzania, zanim pętla do nich dotrze.
loxaxs
1
Obsługuje również przypadek, w którym masz katalog o nazwiedir.txt
vidstige
75
for file in Data/*.txt
do
    for ((i = 0; i < 3; i++))
    do
        name=${file##*/}
        base=${name%.txt}
        ./MyProgram.exe "$file" Logs/"${base}_Log$i.txt"
    done
done

name=${file##*/}Podstawienie ( powłoka rozszerzenie parametrów ) usuwa się ścieżki prowadzącej aż do ostatniego /.

base=${name%.txt}Podstawienie usuwa krawędzie tylną .txt. Trochę trudniej jest, jeśli rozszerzenia mogą się różnić.

Jonathan Leffler
źródło
3
Uważam, że w twoim kodzie jest błąd. Jedna linia powinna być base=${name%.txt}zamiast base=${base%.txt}.
caseklim
4
@CaseyKlimkowsky: Tak; gdy kod i komentarze się nie zgadzają, co najmniej jeden z nich jest błędny. W tym przypadku myślę, że to tylko jeden - kod; często tak naprawdę oba są błędne. Dzięki za zwrócenie na to uwagi; Naprawiłem to.
Jonathan Leffler
8

Możesz użyć opcji znalezienia wyjścia z separacją null z odczytem, ​​aby bezpiecznie iterować struktury katalogów.

#!/bin/bash
find . -type f -print0 | while IFS= read -r -d $'\0' file; 
  do echo "$file" ;
done

Więc w twoim przypadku

#!/bin/bash
find . -maxdepth 1 -type f  -print0 | while IFS= read -r -d $'\0' file; do
  for ((i=0; i<=3; i++)); do
    ./MyProgram.exe "$file" 'Logs/'"`basename "$file"`""$i"'.txt'
  done
done

dodatkowo

#!/bin/bash
while IFS= read -r -d $'\0' file; do
  for ((i=0; i<=3; i++)); do
    ./MyProgram.exe "$file" 'Logs/'"`basename "$file"`""$i"'.txt'
  done
done < <(find . -maxdepth 1 -type f  -print0)

uruchomi pętlę while w bieżącym zakresie skryptu (procesu) i pozwoli na użycie wyjścia find w ustawianiu zmiennych w razie potrzeby

James
źródło
2
$'\0'to dziwny sposób pisania ''. Tracisz IFS=a -rprzełącznik read: lektury oświadczenie powinno być: IFS= read -rd '' file.
gniourf_gniourf
Sądzę, że niektórzy będą potrzebować przeszukiwania $'\0'i rozmieszczenia kilku punktów stosu. Zamierzam wprowadzić wskazane zmiany. Jakie są złe skutki braku IFS=próby echo -e "ok \nok\0" | while read -d '' line; do echo -e "$line"; done, wydaje się, że ich nie ma. Także -r Widzę, że jest często domyślny, ale nie mogę znaleźć przykładu, dlaczego to się nie dzieje.
James
4
IFS=jest potrzebne w przypadku, gdy nazwa pliku kończy się spacją: try ma postać touch 'Prospero '(zwróć uwagę na spację końcową). Potrzebujesz także -rprzełącznika, jeśli nazwa pliku ma odwrotny ukośnik: wypróbuj go touch 'Prospero\n'.
gniourf_gniourf
-1

Wygląda na to, że próbujesz uruchomić plik systemu Windows (.exe) Z pewnością powinieneś używać programu PowerShell. W każdym razie w powłoce bash dla Linuksa wystarczy zwykły jednowarstwowy.

[/home/$] for filename in /Data/*.txt; do for i in {0..3}; do ./MyProgam.exe  Data/filenameLogs/$filename_log$i.txt; done done

Lub w mgnieniu oka

#!/bin/bash

for filename in /Data/*.txt; 
   do
     for i in {0..3}; 
       do ./MyProgam.exe Data/filename.txt Logs/$filename_log$i.txt; 
     done 
 done
użytkownik3183111
źródło