Shell · Variables · Entorn · Expansions · Aritmètica · Arrays
bash script.sh vs ./script.sh).export) i la relació amb els processos fills.fork() i exec().ps).| Shell | Nom | Autor |
|---|---|---|
sh |
Bourne shell | Stephen Bourne |
csh |
C shell | Bill Joy |
ksh |
Korn shell | David Korn |
bash |
Bourne again shell | Brian Fox i Chet Ramey |
zsh |
Z shell | Paul Falstad |
tcsh |
Tenex C shell | Ken Greer |
A Debian, /bin/sh és dash: una shell més lleugera i estricta que només implementa POSIX. Si un script fa servir arrays, [[ ]] o $(( )) amb funcionalitats de bash, ha de dir #!/bin/bash, no #!/bin/sh.
Una comanda és una seqüència de paraules separades per espais:
-) i paràmetres.cat --help, man cat.cd, echo, read, export…) i externes (programes a disc: ls, cat, grep…).Quan escrivim una comanda externa, la shell:
fork(): crea un procés fill (còpia d’ella mateixa);exec() del programa demanat (substitueix la seva imatge);wait() i espera que el fill acabi (execució en primer pla).fork. Per això cd ha de ser un builtin: un fill no pot canviar el directori del pare.& al final, la shell no espera el fill (execució en segon pla). Ho veurem a la sessió següent.0 = èxit; qualsevol altre valor = error.$?.if, &&, ||).exit N. Si no hi ha exit, retorna el codi de l’última comanda executada.Un script és un fitxer de text amb una seqüència de comandes que la shell executa una darrere l’altra.
cat << 'EOF' > fitxer és un here-document: tot el que hi ha fins a la línia EOF s’envia a l’entrada estàndard de cat, i > ho redirigeix al fitxer.'EOF' eviten que la shell expandeixi variables dins del bloc.#!/bin/bash és el shebang: diu al nucli quin intèrpret ha d’executar el fitxer.No és la shell: és el nucli, dins de execve(). Si el fitxer comença amb #!, el nucli arrenca l’intèrpret indicat i li passa el fitxer com a argument. Per això és recomanable (necessari si l’executem com ./script.sh).
#!/usr/bin/env bash (busca bash al PATH).La shell només busca comandes als directoris del PATH, i el directori actual no hi és (per seguretat). list_shells.sh no es troba; ./list_shells.sh indica la ruta explícitament.
source script.sh (o . script.sh) l’executa dins de la shell actual, sense crear un fill: els canvis de variables i cd es mantenen.#!/bin/bash
# Instal·la el compilador gcc a partir del codi font
VERSION=9.3.0
wget https://ftp.gnu.org/gnu/gcc/gcc-$VERSION/gcc-$VERSION.tar.gz
tar -xzf gcc-$VERSION.tar.gz # descomprimeix
mkdir gcc-$VERSION-build && cd gcc-$VERSION-build
../gcc-$VERSION/configure --enable-languages=c,c++ --disable-multilib
make -j$(nproc) # compila en paral·lel
make install # instal·laQuan s’inicia una shell, bash llegeix uns fitxers que configuren l’entorn. Quins depèn de com s’inicia:
| Tipus de shell | Fitxers que llegeix |
|---|---|
Login (després de login, ssh…) |
/etc/profile, i el primer que existeixi de ~/.bash_profile, ~/.bash_login, ~/.profile |
| Interactiva no login (nova terminal) | /etc/bash.bashrc i ~/.bashrc |
Script (bash script.sh) |
Cap (llevat de la variable BASH_ENV) |
| Al tancar una shell de login | ~/.bash_logout |
~/.profile per defecte ja carrega ~/.bashrc, de manera que la configuració personal acostuma a anar a ~/.bashrc./etc; els personals, al directori de l’usuari, i poden sobreescriure els globals.Els scripts no llegeixen ~/.bashrc: no et pots fiar que un alias o una variable definits allà existeixin dins d’un script.
echo i printfecho: mostra text. Opció -n per no acabar amb salt de línia; -e per interpretar \n, \t.printf: sortida formatada com a C. Més portable i previsible que echo.| Format | Significat |
|---|---|
%s |
cadena |
%d |
enter |
%f |
decimal (%.2f = 2 decimals) |
%-15s |
cadena alineada a l’esquerra en 15 caràcters |
\n |
salt de línia (no s’afegeix automàticament) |
\ al final de línia continua la comanda a la línia següent: ajuda a la llegibilitat.| Forma | Nom | Efecte |
|---|---|---|
\ |
escapament | tracta un caràcter literalment |
'...' |
cometes simples | tot és literal, no s’expandeix res |
"..." |
cometes dobles | literal, excepte $, ` i \: s’expandeixen variables i substitucions |
echo 'Pikachu, t\'elegeixo' falla: dins de '...' la barra és un caràcter normal i la ' tanca la cadena. Solucions: usar cometes dobles, o tancar i reobrir: 't'\''elegeixo'.
Cometeja sempre les variables: "$nom". Sense cometes, la shell fa word splitting i globbing sobre el contingut (ho veurem a les expansions).
= sense espais i es llegeixen amb el prefix $.declare).Amb espais, la shell llegeix nom com un nom de comanda i = i "Pikachu" com els seus arguments. Una assignació és una sola paraula: nom=valor.
unset nom elimina la variable. Una variable no definida s’expandeix a cadena buida.Amb {} delimitem el nom de la variable dins d’un text:
Expansions de valor per defecte, molt útils en scripts:
| Forma | Resultat |
|---|---|
${v:-defecte} |
el valor de v, o defecte si és buida/no definida |
${v:=defecte} |
igual, però assigna defecte a v |
${v:?missatge} |
si v és buida, mostra l’error i atura el script |
${v:+alternatiu} |
alternatiu si v està definida, buit si no |
Si una variable conté espais i no la cometegem, la shell la divideix en paraules (separades per IFS: espai, tabulador i salt de línia) i els espais consecutius es col·lapsen.
El problema és greu quan hi ha noms de fitxer amb espais:
Excepte que vulguis que es divideixi, escriu "$variable". És la font d’errors més freqüent en shell scripts.
declaredeclare permet afegir atributs:| Opció | Efecte |
|---|---|
-i |
variable entera |
-r |
només lectura |
-a |
array indexat |
-A |
array associatiu |
-x |
exportada (com export) |
export la convertim en variable d’entorn: es copia a l’entorn de tots els fills.Quan la shell fa fork(), el fill rep una còpia de l’entorn. El fill pot modificar-la, però el pare no ho veu mai. Cap procés pot canviar les variables d’un altre. Per això un script no pot fer export d’una variable cap a la shell que l’ha cridat (si no és amb source).
| Variable | Contingut | Exemple |
|---|---|---|
PATH |
directoris on es busquen els executables | /usr/local/bin:/usr/bin:/bin |
HOME |
directori personal | /home/jordi |
USER |
nom de l’usuari | jordi |
PWD |
directori de treball actual | /home/jordi |
SHELL |
shell de login | /bin/bash |
LANG |
llengua i localització | ca_ES.UTF-8 |
IFS |
separadors de camps per a word splitting | espai, tab, salt de línia |
| Variable | Significat |
|---|---|
$0 |
nom del script |
$1 … $9, ${10} |
arguments (a partir del 10, calen claus) |
$# |
nombre d’arguments |
$@ |
tots els arguments, cadascun com a paraula separada |
$* |
tots els arguments, units en una sola cadena |
$? |
codi de sortida de l’última comanda |
$$ |
PID de la shell/script actual |
$! |
PID de l’últim procés posat en segon pla |
$PPID |
PID del procés pare |
Només "$@" conserva cada argument tal com l’ha escrit l’usuari, inclosos els que tenen espais.
shift i set --shift [n] descarta els n primers paràmetres posicionals i desplaça la resta ($2 passa a ser $1).set -- a b c assigna els paràmetres posicionals: útil per dividir una cadena.set serveix per dues coses diferents: sense opció (o després de --) assigna paràmetres posicionals; amb opcions (-e, -u, -x…) canvia el comportament de la shell. Ho veurem a les bones pràctiques.
Permet usar la sortida d’una comanda com a text:
$( ): es pot niar i és més llegible.fork()).Abans d’executar una comanda, la shell transforma la línia que has escrit. Ho fa en aquest ordre:
{a,b,c}~$var, ${var}$(cmd)$(( ))*, ?, [...]Quan escrivim ls *.txt, ls no veu l’asterisc: la shell l’ha substituït per la llista de fitxers i ls rep ja a.txt b.txt c.txt. Pots comprovar-ho amb echo *.txt.
Globbing (coincidència de noms de fitxer):
| Patró | Coincideix amb |
|---|---|
* |
qualsevol seqüència de caràcters |
? |
exactament un caràcter |
[abc] |
un caràcter del conjunt |
[a-z] |
un caràcter del rang |
[!abc] |
un caràcter que no és del conjunt |
Brace expansion (genera cadenes, no mira el disc):
Si el patró no coincideix amb cap fitxer, bash el deixa literal (ls *.xyz busca un fitxer anomenat *.xyz). Amb shopt -s nullglob s’expandeix a res.
Bash només treballa amb enters (de 64 bits). Hi ha tres formes:
$(( )) no cal el $ davant de les variables i els espais són lliures.(( expressió )) (sense $) avalua i retorna codi de sortida (0 si el resultat ≠ 0): s’usa en condicions.echo $((7/2)) dóna 3. Per a nombres reals cal una eina externa: echo "scale=2; 7/2" | bc → 3.50.
| Operador | Descripció |
|---|---|
+ - * / % |
suma, resta, producte, divisió entera, mòdul |
** |
exponenciació |
++ -- |
increment, decrement |
+= -= *= /= %= |
assignació amb operació |
<< >> |
desplaçament de bits |
& \| ^ ~ |
AND, OR, XOR, NOT bit a bit |
< <= > >= == != |
comparació (retorna 1 o 0) |
&& \|\| ! |
AND, OR, NOT lògics |
RANDOM és una variable especial de bash que dóna un enter aleatori (0–32767) cada cop que es llegeix.Un array guarda diversos valors en una sola variable.
| Operació | Indexat | Associatiu |
|---|---|---|
| Llegir un element | ${tipus[2]} |
${pokedex[Bulbasaur]} |
| Modificar / afegir | tipus[1]="Elèctric" |
pokedex[Pikachu]=25 |
| Afegir al final | tipus+=("Planta") |
— |
| Tots els valors | "${tipus[@]}" |
"${pokedex[@]}" |
| Índexs / claus | "${!tipus[@]}" |
"${!pokedex[@]}" |
| Longitud | ${#tipus[@]} |
${#pokedex[@]} |
| Eliminar element | unset tipus[2] |
unset pokedex[Charmander] |
Després de unset tipus[2], l’array té un forat: els índexs són 0 1 3, la longitud passa a 3 i ${tipus[2]} és buit. Per això convé recórrer amb "${!tipus[@]}".
"${equip[@]}" es comporta com "$@": cada element és una paraula, encara que tingui espais."$@".s="Hello, world!"
echo ${#s} # 13 longitud
echo ${s:7} # world! des de la posició 7
echo ${s:7:2} # wo 2 caràcters des de la 7
echo ${s/world/Earth} # Hello, Earth! 1a coincidència
echo ${s//l/L} # HeLLo, worLd! totes les coincidències
echo ${s/#Hello/Bye} # Bye, world! només al principi
echo ${s^^} # HELLO, WORLD! majúscules
echo ${s,,} # hello, world! minúsculessed, cut, tr en crearien un). Per a operacions simples és molt més ràpid.# treu des del davant, % treu des del final. Un símbol = coincidència més curta; doble = més llarga.
Un ús molt comú: extensions i noms de fitxer.
Al teclat, # és a l’esquerra del $ i % a la dreta: # actua per davant, % per darrere.
readread llegeix una línia de l’entrada estàndard i la reparteix en variables:
| Opció | Efecte |
|---|---|
-p "text" |
mostra un prompt |
-s |
no fa echo (contrasenyes) |
-t N |
timeout de N segons |
-n N |
llegeix només N caràcters |
-a array |
desa les paraules en un array |
-r |
no interpreta \ (recomanat) |
execexec substitueix el procés de la shell pel programa indicat: no hi ha fork(), només l’exec().
sleep. El script ja no existeix i el codi següent no s’arriba a executar.exec sense comanda serveix per redirigir els descriptors de la shell actual (ho veurem a la sessió següent).#!/bin/bash
# fitxa.sh <nom> [nivell]
readonly VERSIO="1.0"
declare -A atac=([Pikachu]=55 [Charmander]=52 [Bulbasaur]=49 [Squirtle]=48)
nom="${1:?Ús: $0 <nom> [nivell]}" # obligatori: si falta, error
nivell="${2:-5}" # opcional, per defecte 5
base="${atac[$nom]:-40}" # atac base (40 si no el coneixem)
atac_total=$(( base + nivell * 2 ))
printf "%-12s nivell %3d atac %4d (script v%s, PID %d)\n" \
"$nom" "$nivell" "$atac_total" "$VERSIO" "$$"fork + exec + wait. Els builtins s’executen dins del procés.source l’executa a la shell actual.$), \ (un caràcter). Cometeja sempre "$var".export les copia a l’entorn dels fills. Els fills mai no poden modificar el pare.$0 $1.. $# "$@" $? $$ $! $PPID.$(( )); arrays indexats i associatius; manipulació de cadenes amb ${...}.