Loops
Overview
Teaching: 40 min
Exercises: 15 minQuestions
Bagaimana mengimplementasikan perintah-perintah yang sama pada file yang berbeda?
Objectives
Membuat loop yang mengimplementasikan satu perintah atau lebih pada tiap file file dalam satu set.
Menelusuri nilai yang diambil dari sebuah variabel loop selama eksekusi dari loop tersebut.
Menjelaskan perbedaan dari tiap nama variabel dan nilainya.
Menjelaskan mengapa spasi dan beberapa karakter seharusnya tidak digunakan untuk menamai file.
Mendemonstrasikan bagaimana melihat perintah yang baru saja dieksekusi.
Menjalankan lagi perintah yang baru saja dieksekusi tanpa mengetikkan ulang.
Loops adalah kunci produktivitas melalui teknik otomasi karena dengan loop kita bisa mengukang operasi yang sama untuk file yang berbebeda-beda tanpa mengulangi pengetikkan perintah pada file lainnya. Sama halnya dengan penggunaan wildcard maupun tab completion, penggunaan loop meningkatkan efisiensi dan menghindari salah ketik.
Misal kita punya file yang dinamai dengan basilisk.dat, unicorn.dat, dan seterusnya.
Pada contoh ini kita akan menggunakan direktori creatures yang hanya berisi dua contoh file,
namun pada prinsipnya kita bisa mengaplikasikannya pada banyak file, ratusan, ribuat atau jutaan pada sekali eksekusi dengan
teknik looping. Kasusnya adalah kita ingin meng-copy semua file dalam direktori creatures tersebut dengan menambahkan
awalan original-namafile.dat, sehingga menjadi seperti
original-basilisk.dat and original-unicorn.dat.
Instead, setelah belajar wildcard, kita ingin menggunakannya, tapi nyatanya tidak bisa:
$ cp *.dat original-*.dat
karena perintah tersebut akan diterjemahkan menjadi:
$ cp basilisk.dat unicorn.dat original-*.dat
Sehingga akan terjadi error dan file malah tidak ter-back-up:
cp: target `original-*.dat` is not a directory
Masalah ini tejadi karena cp menerina lebih dari dua input/argumen. Argumen terakhir
diartikan oleh cp sebagai direktori output. Karena tidak ada direktori yang namanya original-*.dat
maka terjadilah error.
Solusinya, kita menggunakan loop untuk melakukan operasi berulang dengan sekali eksekusi. Berikut contohj sederhananya,
$ for filename in basilisk.dat unicorn.dat
> do
> head -n 3 $filename
> done
COMMON NAME: basilisk
CLASSIFICATION: basiliscus vulgaris
UPDATED: 1745-05-02
COMMON NAME: unicorn
CLASSIFICATION: equus monoceros
UPDATED: 1738-11-24
Ketika shell menemukan kata kunci for,
dia akan mengetahui kalau perintah tersebut digunakan untuk mengulang sebuah perintah
atau kumpulan perintah untuk tiap sesuatu pada sebuah list.
Untuk tiap iterasi (satu loop),
tiap nama sesuatu secara sekuensial di-assign pada variabel
dan perintah di dalam loop dieksekusi sebelum berpindah pada sesuatu yang lain
pada list tersebut.
Di dalam sebuah loop,
kita dapat memanggil nilai dari variabel dengan meletakkan tanda $ di depannya.
Tanda $ memerintahkan shell interpreter untuk memberkalukan variabel sebagai
nama variabel dan menggantinya dengan nilai yang sesuai menurut letaknya.
Bukan dieksekusi sebagai text atau perintah lainnya.
Pada contoh ini, daftar filenya ada dua: basilisk.dat and unicorn.dat.
Setiap kali loop iterasi, variabel filename akan diiisi dengan nama file dan
menjalankan perintah head commmand.
Pada loop pertama, $filename adalah basilisk.dat.
Interpreter menjalankan perintah head pada file basilisk.dat,
dan mencetak tiga baris teratas dari basilisk.dat.
Pada iterasi kedua, $filename menjadi
unicorn.dat. Kali ini, shell menjalankan head pada unicorn.dat
dan mencetak tiga baris teratas dari unicorn.dat.
Karena hanya ada loop, maka shell berakhir setelah loop kedua.
Ketika menggunakan variabel, dimungkinan untuk meletakkan nama file pada
kurung kurawal untuk membatasi nama variabelnya.
Sehingga$filename ekivalen dengan ${filename}, tapi berbeda dengan
${file}name. Ini banyak dijumpai pada skrip shell.
Mengikuti Prompt
Prompt shell berubah dari tanda dolar,
$, menjadi tanda “lebih dari”,>, ketika kita mengetikkan loop. Tanda kedua,>, berbeda menunjukkan bahwa kita belum selesai mengetikkan perintah. Tanda semikolon,;, dapat digunakan untuk memisahkan dua perintah yang ditulis dalam satu baris. Jadi loop diatas dapat ditulis dalam satu bari dengan menambahkan;seperti berikut:for filename in *.dat; do head -n 3 $filename; done.
Simbol sama, arti berbeda
Disini kita melihat tanda lebih besar,
>digunakan sebagai prompt shell dimana tanda tersebut juga digunakan untuk redirect output. Seperti halnya tanda dolar,$, digunakan sebagai prompt shell, namun seperti dijelaskan sebelumnya, tanda tersebut juga digunakan untuk mendapatkan nilai dari sebuah variabel.Jika kedua tanda tersebut, baik dolar
$maupun lebih besar>muncul dengan sendirinya, maka shell mengharapkan kita mengetikkan sesuatu.Jika tanda tersebut (
$dan>) kita sendiri yang mengetikkan, maka ini adalah instruksi dari kita agar shell mendapatkan nilai dari suatu variabel atau me-redirect ouputnya.
Kita menggunakan nama variable filename agar lebih mudah dibaca oleh manusia.
Variable lain seperti i , temperature atau x dapat digunakan agar lebih sederhana.
for x in basilisk.dat unicorn.dat
do
head -n 3 $x
done
atau:
for temperature in basilisk.dat unicorn.dat
do
head -n 3 $temperature
done
Namun, orang lain akan kesulitan memahaminya, apakah i atau
x, ataukah itu memang “temperature”…? Sebaliknya, nama filename akan
segera dengan mudah diartikan bahwa
variabel tersebut akan diisi nama file.
Jadi, gunakan nama variabel yang sesuai.
Berikut contoh yang lebih kompleks.
for filename in *.dat
do
echo $filename
head -n 100 $filename | tail -n 20
done
Shell mulai berjalan dengan mengenali file berekstensi .dat untuk diproses.
body loop kemudian mengeksekusi dua perintah yakni echo dan baris bawahnya.
Perintah echo akan mencetak nama file seperti halnya perintah berikut.
$ echo hello there
prints:
hello there
Pada kasus diatas, maka akan dicetak basilisk.dat pada baris pertama output,
kemudian dicari 100 baris paling atas (dari file basilisk.dat), dari 100 baris tersebut,
dicari 20 baris terakhir (In this case,81~100) dan ditampilkan di bawah hasil echo tadi.
Spaces in Names
Spasi (whitespace) digunakan untuk memisahkan elemen pada list yang akan diiterasi. Jika ada elemen yang memiliki spasi maka dibutuhkan tanda kutip ganda diantara elemen tersebut agar dibaca oleh shell sebagai satu kesatuan.
Contohnya adalah nama file berikut:
red dragon.dat purple unicorn.datMaka kita tambahkan tanda kutip ganda sbb:
for filename in "red dragon.dat" "purple unicorn.dat" do head -n 100 "$filename" | tail -n 20 doneMaka, akan lebih mudah untuk tidak memakai spasi (atau karakter lainnya) dalam penamaan file, cukup menggunakan huruf dan angka saja.
Karena dua file di atas tidak ada (
red dragon.datdanpurple unicorn.dat) maka tidak bisa dibuka oleh shell.head: cannot open ‘red dragon.dat’ for reading: No such file or directory head: cannot open ‘purple unicorn.dat’ for reading: No such file or directoryJika kita coba untuk menghapus tanda kutip pada
$filenamemakared dragon.datakan dibaca menjadi dua file:reddandragonbegitu pula nama file setelahnya.shead: cannot open ‘red’ for reading: No such file or directory head: cannot open ‘dragon.dat’ for reading: No such file or directory head: cannot open ‘purple’ for reading: No such file or directory CGGTACCGAA AAGGGTCGCG CAAGTGTTCC ...
Kembali ke masalah awal bab ini, yakni untuk mengcopy file asli dengan
menambahkan kata original di depannya, maka dapat dijalankan perintah berikut.
for filename in *.dat
do
cp $filename original-$filename
done
Pada loop pertama perintah cp akan mencopy file
basilisk.dat ke file original-basilisk.dat seperti
perintah berikut.
cp basilisk.dat original-basilisk.dat
Pada loop kedua, maka berlaku nama file berikutnya, unicorn.dat
cp unicorn.dat original-unicorn.dat
Begitu seterusnya sampai loop selesai. Pada kasus diatas hanya dua loop saja. Bayangkan jika ada 1000 file, maka loop diatas sangat efisien.
Karena cp tidak memiliki output, akan sulit mengecek apak shell benar-benar
berjalan seperti yang kita harapkan. Dengan menambahkan echo maka kita akan bisa
mengecek jika setiap perinah telah dieksekusi. Diagram berikut menggambarkan bagaimana
echo dapat digunakan untuk mendebug
perintah Unix/Linux dengan sangat baik.
Skrip dan diagram shell diatas hanya akan meng-echo perintah-perintahnya saja. Agar perintah juga dijalankan sekaligus ditampilan maka dapat ditambahkan pipe seperti berikut.
$ for filename in *.dat
> do
> var=$(cp $filename original-$filename) | echo $var
> done
Itearasi dari list string
Misalnya kita punya string “satu”, “dua”, dan “tiga” dalam sebuah list. Bagaimana menerapkannya dalam loop for. Jika itemnya hanya sedikit, bisa kita jadikan satu dalam baris for, jika banyak, kita buat variabel baru untuk menampung list tersebut.
Untuk item sedikit.
$ for i in satu dua tiga;
> do
> echo $i
> done
Untuk list berisi banyak item string.
$ list="satu dua tiga empat lima"
$ for i in $list;
> do
> echo $i
> done
Nelle’s Pipeline: Processing Files
Sekarang Nelle sudah siap untuk memproses datanya. Karena dia masih belajar dalam menggunakan shell, dia memutuskan untuk membangun perintah-perintah yang digunakan secara bertahap. Pada tahap pertama dia ingin memastikan bahwa dia dapat memilih file yang tepat — ingat, file yang berakhiran ‘A’ atau ‘B’, bukan ‘Z’. Dari direktori home-nya, dia mengetikkan:
$ cd north-pacific-gyre/2012-07-03
$ for datafile in *[AB].txt
> do
> echo $datafile
> done
NENE01729A.txt
NENE01729B.txt
NENE01736A.txt
...
NENE02043A.txt
NENE02043B.txt
Langkah selanjutnya adalah memutuskan file yang
akan dibuat oleh program goostats dengan menambahkan prefix “stats”
pada tiap file. Ini terlihat mudah dengan sedikit modifikasi pada loop sebelumnya.
$ for datafile in *[AB].txt
> do
> echo $datafile stats-$datafile
> done
NENE01729A.txt stats-NENE01729A.txt
NENE01729B.txt stats-NENE01729B.txt
NENE01736A.txt stats-NENE01736A.txt
...
NENE02043A.txt stats-NENE02043A.txt
NENE02043B.txt stats-NENE02043B.txt
Dia belum menjalankan program goostat,
hanya mensimulasikan file yang akan dia dapat.
Sekarang dia yakin dapat memilih file yang tepat dan
menghasilkan nama file baru yang sesuai pula.
Mengetikkan perintah lagi dan lagi akan cukup melelahkan dan membuang waktu. Kita dapat menggunakan tombol panah keatas untuk mengulangi perintah sebelumnya. Untuk perintah loop yang berbaris-baris, akan ditampilkan oleh shell dalam satu baris saja dengan menambahkan semicolon yang diperlukan.
$ for datafile in *[AB].txt; do echo $datafile stats-$datafile; done
Menggunakan panah kanan dan ke kiri, Nelle bisa mengedit perintah
sebelumnya yang dicari dengan panah ke atas, kemudian mengganti
echo dengan bash goostats.
$ for datafile in *[AB].txt
do
bash goostats $datafile stats-$datafile
done
Ketika dia menekan tombol Enter, dia tidak melihat output apapun.
Kemudian tak lama kemudian (bergantung pada CPU dan RAM) command prompt $ muncul lagi
menandakan proses telah selesai.
Dia ingin tahu proses yang berjalan pada tiap file. Karenanya dia menambahkan echo sebagai berikut (setelah menekan panah keatas dan kekiri untuk mengeditnya)
$ for datafile in *[AB].txt
do
echo $datafile
bash goostats $datafile stats-$datafile
done
Awal dan Akhir Perintah
Kita bisa berpindah ke Awal perintah (karakter pertama) dengan menekan tombol
Ctrl-A, dan akhir perintah (karakter terakhir) denganCtrl-E.
Ketika dia menjalankan loop tersebut lagi tampak nama file yang sedang diproses oleh loop. Dia mengestimasi tiap file membutuhkan waktu komputasi sekitar lima detik.
NENE01729A.txt
NENE01729B.txt
NENE01736A.txt
...
Jika dia punya 1518 data, dikalikan 5 detik,
dibagi 60, maka dia skripnya membutuhkan waktu komputasi sekitar dua jam.
Setelah megeceknya lagi, dia membuka terminal,
beralih ke direktori north-pacific-gyre/2012-07-03,
dan menggunakan cat stats-NENE01729B.txt untuk melihat outputnya.
Hasilnya terlihat baik. Dia memutuskan untuk pergi minum kopi dan membaca paper sambil menunggu hasil program yang dijalankannya tadi.
Those Who Know History Can Choose to Repeat It
Selain menggunakan panah atas, kita juga bisa menggunakan perinah
historyuntuk menampilkan daftar perintah yang telah digunakan sebelumnya. Untuk mengulangi perintah yang ada dalam daftar tersebut, kita menggunakan perintah!nomordimananomormerupakan nomor yang ada pada daftar history (sisi paling kiri).$ history | tail -n 5456 ls -l NENE0*.txt 457 rm stats-NENE01729B.txt.txt 458 bash goostats NENE01729B.txt stats-NENE01729B.txt 459 ls -l NENE0*.txt 460 historyUntuk menjalankan program
goostatspada fileNENE01729B.txtdia mengetikkan:!458.
Other History Commands
Ada cara lain untuk mengakses history yang lebih cepat untuk mencari history perintah yang kita jalankan. -
Ctrl-R. TekanCtrl-Rdan ketikkan apa yang anda cari. Jika apa yang anda cari lebih dari sekali, tekanCtrl-Rlagi untuk mencarinya. Misalnya:Ctrl-Rkemudian ketikkanforkemudian jika output yang ditampilkan bukan “loop for” yang dicari, tekanCtrl-Rlagi.
!!juga menjalankan perintah sebelumnya, namun tidak senyaman panah atas.!$akan menjalankan kata terakhir pada perintah sebelumnya. Berikut contoh penggunaannya yang sangat bermanfaat. Jalankan perintahbash goostats NENE01729B.txt stats-NENE01729B.txt, kemudian ketikkanless !$untuk melihat isistats-NENE01729B.txt. Ini lebih cepat daripada menekan panah atas dan mengeditnya.
Coding Style
Salah satu coding style (gaya penulisan script/kode) untuk Shell adalah milik Google yang bisa dirujuk disini
LATIHAN
Variables in Loops
This exercise refers to the
data-shell/moleculesdirectory.lsgives the following output:cubane.pdb ethane.pdb methane.pdb octane.pdb pentane.pdb propane.pdbWhat is the output of the following code?
for datafile in *.pdb do ls *.pdb doneNow, what is the output of the following code?
for datafile in *.pdb do ls $datafile doneWhy do these two loops give different outputs?
Solution
The first code block gives the same output on each iteration through the loop. Bash expands the wildcard
*.pdbwithin the loop body (as well as before the loop starts) to match all files ending in.pdband then lists them usingls. The expanded loop would look like this:for datafile in cubane.pdb ethane.pdb methane.pdb octane.pdb pentane.pdb propane.pdb do ls cubane.pdb ethane.pdb methane.pdb octane.pdb pentane.pdb propane.pdb donecubane.pdb ethane.pdb methane.pdb octane.pdb pentane.pdb propane.pdb cubane.pdb ethane.pdb methane.pdb octane.pdb pentane.pdb propane.pdb cubane.pdb ethane.pdb methane.pdb octane.pdb pentane.pdb propane.pdb cubane.pdb ethane.pdb methane.pdb octane.pdb pentane.pdb propane.pdb cubane.pdb ethane.pdb methane.pdb octane.pdb pentane.pdb propane.pdb cubane.pdb ethane.pdb methane.pdb octane.pdb pentane.pdb propane.pdbThe second code block lists a different file on each loop iteration. The value of the
datafilevariable is evaluated using$datafile, and then listed usingls.cubane.pdb ethane.pdb methane.pdb octane.pdb pentane.pdb propane.pdb
Saving to a File in a Loop - Part One
In the same directory, what is the effect of this loop?
for alkanes in *.pdb do echo $alkanes cat $alkanes > alkanes.pdb done
- Prints
cubane.pdb,ethane.pdb,methane.pdb,octane.pdb,pentane.pdbandpropane.pdb, and the text frompropane.pdbwill be saved to a file calledalkanes.pdb.- Prints
cubane.pdb,ethane.pdb, andmethane.pdb, and the text from all three files would be concatenated and saved to a file calledalkanes.pdb.- Prints
cubane.pdb,ethane.pdb,methane.pdb,octane.pdb, andpentane.pdb, and the text frompropane.pdbwill be saved to a file calledalkanes.pdb.- None of the above.
Solution
- The text from each file in turn gets written to the
alkanes.pdbfile. However, the file gets overwritten on each loop interation, so the final content ofalkanes.pdbis the text from thepropane.pdbfile.
Saving to a File in a Loop - Part Two
In the same directory, what would be the output of the following loop?
for datafile in *.pdb do cat $datafile >> all.pdb done
- All of the text from
cubane.pdb,ethane.pdb,methane.pdb,octane.pdb, andpentane.pdbwould be concatenated and saved to a file calledall.pdb.- The text from
ethane.pdbwill be saved to a file calledall.pdb.- All of the text from
cubane.pdb,ethane.pdb,methane.pdb,octane.pdb,pentane.pdbandpropane.pdbwould be concatenated and saved to a file calledall.pdb.- All of the text from
cubane.pdb,ethane.pdb,methane.pdb,octane.pdb,pentane.pdbandpropane.pdbwould be printed to the screen and saved to a file calledall.pdb.Solution
3 is the correct answer.
>>appends to a file, rather than overwriting it with the redirected output from a command. Given the output from thecatcommand has been redirected, nothing is printed to the screen.
Limiting Sets of Files
In the same directory, what would be the output of the following loop?
for filename in c* do ls $filename done
- No files are listed.
- All files are listed.
- Only
cubane.pdb,octane.pdbandpentane.pdbare listed.- Only
cubane.pdbis listed.Solution
4 is the correct answer.
*matches zero or more characters, so any file name starting with the letter c, followed by zero or more other characters will be matched.How would the output differ from using this command instead?
for filename in *c* do ls $filename done
- The same files would be listed.
- All the files are listed this time.
- No files are listed this time.
- The files
cubane.pdbandoctane.pdbwill be listed.- Only the file
octane.pdbwill be listed.Solution
4 is the correct answer.
*matches zero or more characters, so a file name with zero or more characters before a letter c and zero or more characters after the letter c will be matched.
Doing a Dry Run
A loop is a way to do many things at once — or to make many mistakes at once if it does the wrong thing. One way to check what a loop would do is to
echothe commands it would run instead of actually running them.Suppose we want to preview the commands the following loop will execute without actually running those commands:
for file in *.pdb do analyze $file > analyzed-$file doneWhat is the difference between the two loops below, and which one would we want to run?
# Version 1 for file in *.pdb do echo analyze $file > analyzed-$file done# Version 2 for file in *.pdb do echo "analyze $file > analyzed-$file" doneSolution
The second version is the one we want to run. This prints to screen everything enclosed in the quote marks, expanding the loop variable name because we have prefixed it with a dollar sign.
The first version redirects the output from the command
echo analyze $fileto a file,analyzed-$file. A series of files is generated:cubane.pdb,ethane.pdbetc.Try both versions for yourself to see the output! Be sure to open the
analyzed-*.pdbfiles to view their contents.
Nested Loops
Suppose we want to set up up a directory structure to organize some experiments measuring reaction rate constants with different compounds and different temperatures. What would be the result of the following code:
for species in cubane ethane methane do for temperature in 25 30 37 40 do mkdir $species-$temperature done doneSolution
We have a nested loop, i.e. contained within another loop, so for each species in the outer loop, the inner loop (the nested loop) iterates over the list of temperatures, and creates a new directory for each combination.
Try running the code for yourself to see which directories are created!
Key Points
Sebuah loop
formelakukan perintah yang sama untuk semua file dalam sebuah list.Semua loop
formembutuhkan variabel untuk mengeksekusi operasi yang dilakukan terhadap file tersebut.Gunakan
$nameatau untuk mengexpand variabel, yakni untuk mendapatkan nilainya.Jangan menggunakan spasi, quotes, atau karakter wildcard seperti ‘*’ or ‘?’ dalam nama file.
Gunakan nama file yang konsisten agar mudah untuk menggunakan pola wildcard dalam membuat looping.
Gunakan panah ke atas untuk melihat perintah yang digunakan sebelumnya.
Gunakan
Ctrl-Runtuk mencari perintah yang pernah digunakan.Gunakan perintah
historyuntuk melihat perintah-perintah terakhir digunakan dan gunakan!numberuntuk mengulang perintah sesuai nomor yang ditampilkan.