Bash Completion Anotomisi

bash bash-completion ruby rake bundle dscacheutil

Terminal’le, Bash’le ilk tanıştığım andan itibaren tab-completion olayının hastası olmuş, hemen:

Hmmm… Kesin öğrenmeliyim! Ben de kendi komutlarımı oluşturmalıyım!

demiştim. Neticede bir şekilde programlanabilir birşey olmalıydı. Mac OS’la uğraşmaya başladığım ilk günlerde tanıştığım MacPorts içinde pek çok bash-completion eklentileriyle geliyordu. Bu sayede tab ile tamamlama yapabilmek için gerekli kaynağı inceleme şansı bulmuştum.

Mantık olarak, bir şekilde, bir değişkende bir yerde tamamlanacak kelimeler durmalıydı. tab bu kelimeler içinde cycle yapmayı sağlamalıydı.

dscacheutil

Denemek için ilk yaptığım tamamlama dscacheutil içindi. DNS işleriyle ilgili bu tool’u dns-cache’i silmek için kullanıyordum. Komutu:

dscacheutil -flushcache

şeklinde kullanıyordum. Yapmak istediğim şey dscacheutil yazdıktan sonra - yazıp tab tuşuna basmak ve ilgili opsiyonları otomatik olarak yazdırmaktı.

dscacheutil -h

ile baktım başka ne gibi opsiyonlar var. Pekde anlamadım diğer özellikleri. İçlerin kafama yatanları seçtim:

-flushcache -statistics -configuration -cachedump -h -q

Bash’in complete adında minik bir komutu var. Mantık olarak, şu komut yazılıp tab tuşuna basılınca bu fonksiyon çalışsın.

Gördüğüm örneklerde tamamlama işini yapacak fonksiyon adı, ilgili komut adına _ eklenerek oluyor. Yani dscacheutil için yazacağınız fonksiyonun adı _dscacheutil olmalı. Bu sadece bir notasyon yani zorunlu bir durum değil.

complete -F FONKSİYON KOMUT
complete -F _dscacheutil dscacheutil

Düşe kalka bir şekilde ilk completion’ımı yazmayı başarmıştım:

_dscacheutil() 
{
    local cur prev opts
    COMPREPLY=()
    cur="${COMP_WORDS[COMP_CWORD]}"
    prev="${COMP_WORDS[COMP_CWORD-1]}"
    opts="-flushcache -statistics -configuration -cachedump -h -q"
    if [[ ${cur} == -* ]] ; then
        COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
        return 0
    fi
}
complete -F _dscacheutil dscacheutil

Satır Satır _dscacheutil

Şimdi, 4.satırda COMPREPLY adında boş bir array var. cur Current / Cursor yani tab basıldığında aktif olarak yakalanan / gelen. prev de Previous yani tamamlanacak olan kelimelerde, aktif olandan bir önceki. Yani tamamlamak istediğimiz kelimeler 7.satırdaki opts değişkeninde sırasıyla;

-flushcache -statistics -configuration -cachedump -h -q

şeklinde duruyor. dscacheutil yazıp bir boşluk verip tabe ilk bastığınızda index 0 ve ilk kelime: -flushcache yani cur daki değer olur.

Eğer dscacheutil yazıp, bir boşluk bırakıp, -s yazıp tabe bastığınızda tamamlanacak kelimler listesindeki ikinci kelimeyi Current yani cur yapmış olursunuz. Çünkü kelimelerde -s ile başlayan bir tek -statistics var. Eğer -s ile başlayan iki kelime olsaydı, -statistics ve -super gibi; İlk tabe basışta cur: -statistics, ikincide cur: -super ve prev: -statistics olacaktı.

Bu fonksiyonun asıl gizli silahı compgen. -W parametresi WORD LIST yani kelime listesi anlamında. Hemen daha iyi anlamak için:

compgen -W "ali veli selami"
# ali
# veli
# selami

compgen -W "ali veli selami" -- veli
# veli

şeklinde çıktı verir. Yani yukarıdaki örnekte opts: ali veli selami, cur: veli olmuş ve işlem bize sonuç olarak veli dönmüştür. Yani 9.satırı düşünürsek:

COMPREPLY=( $(compgen -W "ali veli selami" -- veli) )

gibi. 8.satıra bakarsak;

if [[ ${cur} == -* ]] ; then

yani; - ile başlayanları yakala. opts içinde geçen ve - ile başlayan.

complete -F _dscacheutil dscacheutil

-F function yani, dscacheutil komutu için _dscacheutil fonksiyonunu kullanarak tamamlama yap.

Özetle, dscacheutil komutundan sonra - + tab yapınca -flushcache -statistics -configuration -cachedump -h -q kelimeleri içinde dönüp duracağız.

bundle exec

İlk denemem üzerinden epeyce bir zaman geçmişti. Keza geçen süre içinde bash bilgim de arttı. Hep gördüğüm, merak ettiğim bir konu da zincirleme tamamlama. Yani;

KOMUT ALT-KOMUT ALT-KOMUT / OPSIYON

gibi tamamlamalar. Buna en güzel örnek Gitflow. git flow feature start şeklinde arka arkaya 3-4 komutu tamamlamak mümkün. Bu durumdan yola çıkarak bundle komutu için benzer bir yardımcı yazmaya karar verdim.

Umarım yazıyı okuyanlar Ruby ile ilgilidir. Çünkü Bundler Ruby ve Rails dünyasının çok yakından bildiği ve kullandığı ruby modülü / kütüphanesi paket yöneticisidir. Özellikle bu okuduğunuz blog’da kullandığım Octopress’de bile Bundler kullanmaktayım.

Tamamlama işlemi için bundle exec rake ve rake’e ait alt özellikler vs zincirleme işlem yapmam gerekiyor. Öncelikle bundlea ait alt komutları, daha sonra da rakee ait komutları tamamlamam gerekiyor.

İlk önce 8.satırda yazan bundle komutlarını buldum. bundle --help ile tek tek baktım neler var diye. Tabi bir başka durum daha vardı. Bu tamamlama özelliği eğer bundle komutu varsa çalışmalıydı. Bundler’ı genelde tüm sisteme kurmak yerine proje bazlı kullanmak daha temizdir.

gemleri kurarken;

bundle install --path vendor/bundle

şeklinde kullanıyorum. Bulunduğum proje altında vendor/bundle dizinine kurduruyorum. Bu bakımdan tüm sistem yerine proje bazlı kurulum var. Yani ilgili proje / ruby versiyonu aktif olduğu zaman, eğer varsa Bundler aktif oluyor. Bunun içinde fonksiyonun en başında which bundle ile kontrol ediyorum.

bundle komutuna ait alt komutlar ve opsiyonlar var. Opsiyonlar -- ile başlıyor. Yani bundle --verbose install şeklinde bir tamamlama sekansı olabilir. Keza, rake veya exec arkasında da ilgili tamamlamalar gelmeli.

rake komutu da başlı başına kendine has opsiyonlara sahip. rake -T özelliği ile ilgili RAKE TASK’leri listeleyebiliyorsunuz. Örnek;

rake -T

rake clean                      # Clean out caches: .pygments-cache, .gist-cache, .sass-cache
rake copydot[source,dest]       # copy dot files for deployment
rake deploy                     # Default deploy task
rake gen_deploy                 # Generate website and deploy
rake generate                   # Generate jekyll site

gibi… Tabi rake komutu da aynı bundle komutu gibi başka bir dosyanın varlığına bağlı. Rakefile tüm task’lerin ve diğer ayarların bulunduğu dosya. Eğer Rakefile olmazsa rake komutu da çalışmaz.

gemlerin yeri aktif olan ruby modülleri bölgesinde olmadığı için, bundle komutuyla vendor/bundle altına kurduğumuz executable yani çalıştıralabilir dosyaları da çağırabiliyoruz.

bundle exec ile, vendor/bundle altına kurulan gemlerin executablelarını da çağırabiliyorsunuz. Örneğin, Octopress için kurulan gemlerin içinde;

compass    jekyll    posix-spawn-benchmark  redcloth     tilt
dw         kramdown  rackup                 sass        
haml       maruku    rake                   sass-convert
html2haml  marutex   rdiscount              scss        

binary dosyalar var.. Çalıştırmak için: bundle exec html2haml gibi kullanabilirsiniz. Bu bakımdan bund exec dedikten sonra komutu da tamamlamamız / bulmamız gerekiyor. Bunun için 28 ve 29.satırlara bakalım. Önce, vendor/bundle altındaki bin dizinlerini bulmamız gerekiyor.

find . -name 'bin' # içinde bin kelimesi geçen file/folder’ları bul

./vendor/bundle/ruby/1.9.1/bin
./vendor/bundle/ruby/1.9.1/gems/classifier-1.3.3/bin
./vendor/bundle/ruby/1.9.1/gems/compass-0.11.6/bin
./vendor/bundle/ruby/1.9.1/gems/directory_watcher-1.4.1/bin
./vendor/bundle/ruby/1.9.1/gems/haml-3.1.4/bin
./vendor/bundle/ruby/1.9.1/gems/haml-3.1.4/vendor/sass/bin
./vendor/bundle/ruby/1.9.1/gems/jekyll-0.11.0/bin
./vendor/bundle/ruby/1.9.1/gems/kramdown-0.13.4/bin
./vendor/bundle/ruby/1.9.1/gems/maruku-0.6.0/bin
./vendor/bundle/ruby/1.9.1/gems/posix-spawn-0.3.6/bin
./vendor/bundle/ruby/1.9.1/gems/rack-1.3.5/bin
./vendor/bundle/ruby/1.9.1/gems/rake-0.9.2.2/bin
./vendor/bundle/ruby/1.9.1/gems/rb-fsevent-0.9.1/bin
./vendor/bundle/ruby/1.9.1/gems/rdiscount-1.6.8/bin
./vendor/bundle/ruby/1.9.1/gems/RedCloth-4.2.9/bin
./vendor/bundle/ruby/1.9.1/gems/sass-3.1.12/bin
./vendor/bundle/ruby/1.9.1/gems/tilt-1.3.3/bin

# xargs ile bu sonuçları tek satır yapıyoruz

find . -name 'bin' | xargs

Elimizde, space karakteri ile ayrılmış upuzun bir string var artık. Yani aynı ilk _dscacheutil örneğindeki $opts gibi oldu. Yapmanız gereken elimizdeki liste içinden ilk elemanı almak. Bunun için küçük bir nurmara yapıyoruz.

 bin_folder=( $(find . -name 'bin' | xargs) # ./vendor/bundle/ruby/1.9.1/bin

Artık içine bakmamız gereken dizini biliyoruz. Şimdi yapmamız gereken aynı şekilde liste alıp space ile ayrılmış string oluşturmak.

 executables=`command ls ${bin_folder} | xargs`

command ls aslında bildiğiniz ls komutunu çağırıyor ama eğer alias ya da başka bir override durumu varsa; Yani siz belkide:

alias ls="ls -al --color"

yaptınız ve her ls komutu çalıştığında aslında ls -al --color çalıştırıyor olabilirsiniz. Bunu engellemek için bash’in içindeki default komutu çağırmak gerekiyor. command bu işe yarıyor.

Son olarak 34. satırda;

COMPREPLY=( $(compgen -W "${commands}" -- ${cur}) )

word list olarak compgen komutuna $commands değişkenini geçiyoruz. Yani tüm yaptığımız, en son satırda kullanacağımız COMPREPLY için gerekli string’i oluşturmak.

Bu tamamlama fonksiyonunu kullanabilmek için; .bashrc ya da .profile ya da her ne kullanıyorsanız, o dosyaya eklemek. Örneğin bu _bundler_complete fonksiyonunu $HOME/bundler_complete.sh şeklinde kaydettiyseniz; kullandığınız .bashrc içinde

source $HOME/bundler_complete.sh

yapmanız gerekiyor. Bash Completion konusuyla ilgili daha fazla ve detaylı bilgi için link’e tıklayabilirsiniz.