Shell Scripts

oct. 29, 2021

Overview

Predare: 30 min
Exerciții: 15 min
Întrebări

  • Cum pot salva și reutiliza comenzile?

Obiective

  • Scrieți un script shell care execută o comandă sau o serie de comenzi pentru un set fix de fișiere.

  • Executați un script shell din linia de comandă.

  • Scrieți un script shell care operează asupra unui set de fișiere definit de utilizator în linia de comandă.

  • Creați pipe-line-uri care includ scripturi shell scrise de dumneavoastră și de alții.

În sfârșit suntem gata să vedem ce face din shell un mediu de programare atât de puternic.Vom lua comenzile pe care le repetăm frecvent și le vom salva în fișierepentru ca mai târziu să putem rula din nou toate aceste operații prin tastarea unei singure comenzi.Din motive istorice,o grămadă de comenzi salvate într-un fișier se numește de obicei script shell,dar nu vă înșelați:acestea sunt de fapt mici programe.

Să începem prin a ne întoarce la molecules/ și să creăm un nou fișier, middle.sh care va deveni scriptul nostru shell:

$ cd molecules$ nano middle.sh

Comanda nano middle.sh deschide fișierul middle.sh în editorul de text ‘nano'(care rulează în shell).Dacă fișierul nu există, acesta va fi creat. putem folosi editorul de text pentru a edita direct fișierul – vom introduce pur și simplu următoarea linie:

head -n 15 octane.pdb | tail -n 5

Aceasta este o variație a pipe-ului pe care l-am construit mai devreme:selectează liniile 11-15 din fișierul octane.pdb.Țineți minte, încă nu o executăm ca o comandă:punem comenzile într-un fișier.

Apoi salvăm fișierul (Ctrl-O în nano), și ieșim din editorul de text (Ctrl-X în nano).Verificați că directorul molecules conține acum un fișier numit middle.sh.

După ce am salvat fișierul,putem cere shell-ului să execute comenzile pe care le conține.Shell-ul nostru se numește bash, așa că executăm următoarea comandă:

$ bash middle.sh
ATOM 9 H 1 -4.502 0.681 0.785 1.00 0.00ATOM 10 H 1 -5.254 -0.243 -0.537 1.00 0.00ATOM 11 H 1 -4.357 1.252 -0.895 1.00 0.00ATOM 12 H 1 -3.009 -0.741 -1.467 1.00 0.00ATOM 13 H 1 -3.172 -1.337 0.206 1.00 0.00

Sigur, ieșirea script-ului nostru este exact ceea ce am obține dacă am rula direct acea conductă.

Text vs. Whatever

De obicei numim programe precum Microsoft Word sau LibreOffice Writer „texteditors”, dar trebuie să fim puțin mai atenți când vine vorba deprogramare. În mod implicit, Microsoft Word utilizează fișiere .docx pentru a stoca nu numai text, ci și informații de formatare privind fonturile, titlurile și, în curând. Aceste informații suplimentare nu sunt stocate sub formă de caractere și nu înseamnă nimic pentru instrumente precum head: acestea se așteaptă ca fișierele de intrare să nu conțină decât literele, cifrele și semnele de punctuație de pe o tastatură standard de calculator. Prin urmare, atunci când editați programe, trebuie fie să folosiți un editor de text în clar, fie să aveți grijă să salvați fișierele ca text în clar.

Ce se întâmplă dacă vrem să selectăm linii dintr-un fișier arbitrar?Am putea edita middle.sh de fiecare dată pentru a schimba numele fișierului,dar acest lucru ar dura probabil mai mult decât să scriem din nou comanda în shell și să o executăm cu un nou nume de fișier.În schimb, haideți să edităm middle.sh și să o facem mai versatilă:

$ nano middle.sh

Acum, în „nano”, înlocuiți textul octane.pdb cu variabila specială numită :

head -n 15 "" | tail -n 5

În interiorul unui script shell, înseamnă „primul nume de fișier (sau alt argument) de pe linia de comandă”.Acum putem rula scriptul nostru astfel:

$ bash middle.sh octane.pdb
ATOM 9 H 1 -4.502 0.681 0.785 1.00 0.00ATOM 10 H 1 -5.254 -0.243 -0.537 1.00 0.00ATOM 11 H 1 -4.357 1.252 -0.895 1.00 0.00ATOM 12 H 1 -3.009 -0.741 -1.467 1.00 0.00ATOM 13 H 1 -3.172 -1.337 0.206 1.00 0.00

sau pe un alt fișier astfel:

$ bash middle.sh pentane.pdb
ATOM 9 H 1 1.324 0.350 -1.332 1.00 0.00ATOM 10 H 1 1.271 1.378 0.122 1.00 0.00ATOM 11 H 1 -0.074 -0.384 1.288 1.00 0.00ATOM 12 H 1 -0.048 -1.362 -0.205 1.00 0.00ATOM 13 H 1 -1.183 0.500 -1.412 1.00 0.00

Cuvinte duble în jurul argumentelor

Pentru același motiv pentru care punem variabila de buclă între ghilimele duble, în cazul în care numele fișierului conține spații, înconjurăm cu ghilimele duble.

În prezent, trebuie să modificăm middle.sh de fiecare dată când dorim să ajustăm intervalul de linii care este returnat. Să rezolvăm acest lucru prin configurarea scriptului nostru pentru a utiliza în schimb trei argumente în linia de comandă. După primul argument din linia de comandă (), fiecare argument suplimentar pe care îl furnizăm va fi accesibil prin intermediul variabilelor speciale , , , care se referă la primul, al doilea, respectiv al treilea argument din linia de comandă.

Cunoscând acest lucru, putem folosi argumente suplimentare pentru a defini intervalul de linii care vor fi transmise la head și, respectiv, tail:

$ nano middle.sh
head -n "" "" | tail -n ""

Acum putem rula:

$ bash middle.sh pentane.pdb 15 5
ATOM 9 H 1 1.324 0.350 -1.332 1.00 0.00ATOM 10 H 1 1.271 1.378 0.122 1.00 0.00ATOM 11 H 1 -0.074 -0.384 1.288 1.00 0.00ATOM 12 H 1 -0.048 -1.362 -0.205 1.00 0.00ATOM 13 H 1 -1.183 0.500 -1.412 1.00 0.00

Schimbând argumentele la comanda noastră putem schimba comportamentul scriptului nostru:

$ bash middle.sh pentane.pdb 20 5
ATOM 14 H 1 -1.259 1.420 0.112 1.00 0.00ATOM 15 H 1 -2.608 -0.407 1.130 1.00 0.00ATOM 16 H 1 -2.540 -1.303 -0.404 1.00 0.00ATOM 17 H 1 -3.393 0.254 -0.321 1.00 0.00TER 18 1

Acest lucru funcționează, dar s-ar putea ca următoarea persoană care citește middle.sh să aibă nevoie de un moment pentru a-și da seama ce face.Putem îmbunătăți scriptul nostru adăugând câteva comentarii în partea de sus:

$ nano middle.sh
# Select lines from the middle of a file.# Usage: bash middle.sh filename end_line num_lineshead -n "" "" | tail -n ""

Un comentariu începe cu un caracter # și merge până la sfârșitul liniei.Calculatorul ignoră comentariile,dar acestea sunt de neprețuit pentru a ajuta oamenii (inclusiv pe viitorul dumneavoastră) să înțeleagă și să folosească scripturile.Singurul avertisment este că de fiecare dată când modificați scriptul,ar trebui să verificați dacă comentariul este încă corect:o explicație care trimite cititorul într-o direcție greșită este mai rea decât niciuna.

Ce se întâmplă dacă vrem să procesăm mai multe fișiere într-o singură conductă?De exemplu, dacă vrem să sortăm fișierele noastre .pdb în funcție de lungime, vom tasta:

$ wc -l *.pdb | sort -n

pentru că wc -l enumeră numărul de linii din fișiere(amintiți-vă că wc înseamnă „număr de cuvinte”, adăugarea opțiunii -l înseamnă în schimb „număr de linii”)iar sort -n sortează lucrurile numeric.Am putea pune acest lucru într-un fișier,dar atunci ar sorta doar o listă de .pdb fișiere din directorul curent.Dacă vrem să putem obține o listă ordonată a altor tipuri de fișiere,avem nevoie de o modalitate de a introduce toate aceste nume în script.Nu putem folosi , și așa mai departepentru că nu știm câte fișiere există.În schimb,folosim variabila specială $@,care înseamnă:’Toate argumentele liniei de comandă pentru scriptul shell’.De asemenea, ar trebui să punem $@ în interiorul ghilimelelor dublepentru a gestiona cazul argumentelor care conțin spații("$@" este o sintaxă specială și este echivalentă cu "" "" …).

Iată un exemplu:

$ nano sorted.sh
# Sort files by their length.# Usage: bash sorted.sh one_or_more_filenameswc -l "$@" | sort -n
$ bash sorted.sh *.pdb ../creatures/*.dat
9 methane.pdb12 ethane.pdb15 propane.pdb20 cubane.pdb21 pentane.pdb30 octane.pdb163 ../creatures/basilisk.dat163 ../creatures/minotaur.dat163 ../creatures/unicorn.dat596 total

List Unique Species

Leah are câteva sute de fișiere de date, fiecare dintre ele fiind formatat astfel:

2013-11-05,deer,52013-11-05,rabbit,222013-11-05,raccoon,72013-11-06,rabbit,192013-11-06,deer,22013-11-06,fox,12013-11-07,rabbit,182013-11-07,bear,1

Un exemplu de acest tip de fișier este dat în data-shell/data/animal-counts/animals.txt.

Se poate utiliza comanda cut -d , -f 2 animals.txt | sort | uniq pentru a produce specia unică din animals.txt. Pentru a evita să fie nevoit să tasteze de fiecare dată această serie de comenzi, un om de știință poate alege să scrie în schimb un script shell.

Scrieți un script shell numit species.sh care primește orice număr de nume de fișiere ca argumente în linia de comandă și folosește o variantă a comenzii de mai sus pentru a tipări o listă a speciilor unice care apar în fiecare dintre aceste fișiere separat.

Soluție

# Script to find unique species in csv files where species is the second data field# This script accepts any number of file names as command line arguments# Loop over all filesfor file in $@doecho "Unique species in $file:"# Extract species namescut -d , -f 2 $file | sort | uniqdone

Să presupunem că tocmai am executat o serie de comenzi care au făcut ceva util – de exemplu, care au creat un grafic pe care am dori să-l folosim într-o lucrare.Am dori să putem recrea graficul mai târziu, dacă avem nevoie, așa că dorim să salvăm comenzile într-un fișier.În loc să le tastăm din nou(și eventual să le greșim)putem face așa:

$ history | tail -n 5 > redo-figure-3.sh

Fileul redo-figure-3.sh conține acum:

297 bash goostats NENE01729B.txt stats-NENE01729B.txt298 bash goodiff stats-NENE01729B.txt /data/validated/01729.txt > 01729-differences.txt299 cut -d ',' -f 2-3 01729-differences.txt > 01729-time-series.txt300 ygraph --format scatter --color bw --borders none 01729-time-series.txt figure-3.png301 history | tail -n 5 > redo-figure-3.sh

După un moment de lucru într-un editor pentru a elimina numerele de serie de pe comenzi,și pentru a elimina linia finală în care am apelat comanda history,avem o înregistrare complet exactă a modului în care am creat acea figură.

De ce să înregistrăm comenzile în istoric înainte de a le executa?

Dacă executați comanda:

$ history | tail -n 5 > recent.sh

ultima comandă din fișier este chiar comanda history, adică,shell-ul a adăugat history la jurnalul de comenzi înainte de a o executa efectiv. De fapt, shell-ul adaugă întotdeauna comenzi în jurnal înainte de a le executa. De ce credeți că face acest lucru?

Soluție

În cazul în care o comandă provoacă un blocaj sau un blocaj, ar putea fi util să știm care a fost acea comandă, pentru a investiga problema.Dacă comanda ar fi înregistrată numai după ce a fost executată, nu am avea o înregistrare a ultimei comenzi executate în cazul unui blocaj.

În practică, majoritatea oamenilor dezvoltă scripturi shell executând comenzi la promptul shell de câteva oripentru a se asigura că fac ceea ce trebuie, apoi salvându-le într-un fișier pentru reutilizare.Acest stil de lucru le permite oamenilor să recicleze ceea ce descoperă despre datele lor și despre fluxul lor de lucru cu un singur apel la historyși un pic de editare pentru a curăța ieșirea și a o salva ca script shell.

Nelle’s Pipeline: Crearea unui script

Supervizorul lui Nelle a insistat ca toate analizele ei să fie reproductibile. Cel mai simplu mod de a captura toți pașii este într-un script.

În primul rând ne întoarcem în directorul de date al lui Nelle:

$ cd ../north-pacific-gyre/2012-07-03/

Ea rulează editorul și scrie următoarele:

# Calculate stats for data files.for datafile in "$@"do echo $datafile bash goostats $datafile stats-$datafiledone

Salvează acest lucru într-un fișier numit do-stats.shpentru ca acum să poată reface prima etapă a analizei sale tastând:

$ bash do-stats.sh NENE*.txt

De asemenea, poate face acest lucru:

$ bash do-stats.sh NENE*.txt | wc -l

pentru ca rezultatul să fie doar numărul de fișiere procesate și nu numele fișierelor care au fost procesate.

Un lucru de reținut în legătură cu scriptul lui Nelle este că acesta lasă persoana care îl execută să decidă ce fișiere să proceseze.Ea l-ar fi putut scrie ca:

# Calculate stats for Site A and Site B data files.for datafile in NENE*.txtdo echo $datafile bash goostats $datafile stats-$datafiledone

Avantajul este că acesta selectează întotdeauna fișierele corecte:nu trebuie să își amintească să excludă fișierele „Z”.Dezavantajul este că selectează întotdeauna doar acele fișiere – nu îl poate rula pe toate fișierele (inclusiv pe cele „Z”),sau pe fișierele „G” sau „H” pe care le produc colegii ei din Antarctica,fără a modifica scriptul.Dacă ar vrea să fie mai aventuroasă,ar putea să modifice scriptul pentru a verifica dacă există argumente în linia de comandă și să folosească NENE*.txt dacă nu este furnizat niciunul.Desigur,acest lucru introduce un alt compromis între flexibilitate și complexitate.

Variabile în scripturile shell

În directorul molecules, imaginați-vă că aveți un script shell numit script.sh care conține următoarele comenzi:

head -n  tail -n  

În timp ce vă aflați în directorul molecules, tastați următoarea comandă:

bash script.sh '*.pdb' 1 1

Care dintre următoarele ieșiri vă așteptați să vedeți?

  1. Toate liniile dintre prima și ultima linie a fiecărui fișier care se termină în .pdbdin directorul molecules
  2. Prima și ultima linie a fiecărui fișier care se termină în .pdb din directorul molecules. director
  3. Prima și ultima linie a fiecărui fișier din directorul molecules

  4. O eroare din cauza ghilimelelor din jurul *.pdb

Soluție

Răspunsul corect este 2.

Variabilele speciale $1, $2 și $3 reprezintă argumentele din linia de comandă date scriptului, astfel încât comenzile executate sunt:

$ head -n 1 cubane.pdb ethane.pdb octane.pdb pentane.pdb propane.pdb$ tail -n 1 cubane.pdb ethane.pdb octane.pdb pentane.pdb propane.pdb

Shell-ul nu extinde '*.pdb' deoarece este închis între ghilimele.Ca atare, primul argument al scriptului este '*.pdb' care este extins în script prin head și tail.

Găsește cel mai lung fișier cu o anumită extensie

Scrieți un script shell numit longest.sh care ia ca argumente numele unui director și o extensie a numelui de fișier și tipărește numele fișierului cu cele mai multe linii din acel director cu acea extensie. De exemplu:

$ bash longest.sh /tmp/data pdb

a imprima numele fișierului .pdb din /tmp/data care are cele mai multe linii.

Soluție

# Shell script which takes two arguments:# 1. a directory name# 2. a file extension# and prints the name of the file in that directory# with the most lines which matches the file extension.wc -l /*. | sort -n | tail -n 2 | head -n 1

Prima parte a conductei, wc -l /*. | sort -n, numără liniile din fiecare fișier și le ordonează numeric (cele mai mari la sfârșit). În cazul în care există mai multe fișiere, wc emite, de asemenea, o linie finală de rezumat, care indică numărul total de linii din toate fișierele. Folosim tail-n 2 | head -n 1 pentru a arunca această ultimă linie.

Cu wc -l /*. | sort -n | tail -n 1 vom vedea linia de rezumat finală: putem construi conducta noastră pe bucăți pentru a fi siguri că înțelegem ieșirea.

Comprehensiunea de citire a scripturilor

Pentru această întrebare, luați din nou în considerare directorul data-shell/molecules.Acesta conține un număr de fișiere .pdb în plus față de orice alte fișiere pe care le puteți fi creat.Explicați ce ar face fiecare dintre următoarele trei scripturi atunci când sunt rulate cabash script1.sh *.pdb, bash script2.sh *.pdb și, respectiv, bash script3.sh *.pdb.

# Script 1echo *.*
# Script 2for filename in   do cat $filenamedone

# Script 3echo [email protected]

Soluții

În fiecare caz, shell-ul extinde wildcard-ul din *.pdb înainte de a trece lista de nume de fișiere rezultată ca argumente pentru script.

Scriptul 1 ar tipări o listă a tuturor fișierelor care conțin un punct în numele lor.Argumentele transmise scriptului nu sunt de fapt folosite nicăieri în script.

Scriptul 2 ar tipări conținutul primelor 3 fișiere cu extensia .pdb., și se referă la primul, al doilea și, respectiv, al treilea argument.

Script 3 ar imprima toate argumentele scriptului (adică toate fișierele .pdb),urmate de .pdb.$@ se referă la toate argumentele date unui script shell.

cubane.pdb ethane.pdb methane.pdb octane.pdb pentane.pdb propane.pdb.pdb

Debugging Scripts

Să presupunem că ați salvat următorul script într-un fișier numit do-errors.shîn directorul north-pacific-gyre/2012-07-03 al lui Nelle:

# Calculate stats for data files.for datafile in "$@"do echo $datfile bash goostats $datafile stats-$datafiledone

Când îl rulați:

$ bash do-errors.sh NENE*.txt

la ieșire este goală.Pentru a afla de ce, rulați din nou scriptul folosind opțiunea -x:

bash -x do-errors.sh NENE*.txt

Ce vă arată ieșirea?Ce linie este responsabilă de eroare?

Soluție

Opțiunea -x face ca bash să ruleze în modul de depanare.Acest lucru tipărește fiecare comandă pe măsură ce este executată, ceea ce vă va ajuta să localizați erorile.În acest exemplu, putem vedea că echo nu tipărește nimic. Am făcut o greșeală de tipar în numele variabilei de buclă, iar variabila datfile nu există, prin urmare returnează un șir gol.

Puncturi cheie

  • Salvați comenzile în fișiere (numite de obicei scripturi shell) pentru reutilizare.

  • bash execută comenzile salvate într-un fișier.

  • $@ se referă la toate argumentele din linia de comandă a unui script shell.

  • , , etc., se referă la primul argument de linie de comandă, al doilea argument de linie de comandă etc.

  • Puneți variabilele între ghilimele dacă valorile ar putea avea spații în ele.

  • Lăsând utilizatorii să decidă ce fișiere să proceseze este mai flexibil și mai coerent cu comenzile Unix încorporate.

Lasă un răspuns

Adresa ta de email nu va fi publicată.