A la lliçó anterior vas aprendre a moure't per srv-veloz-01 i va quedar una pregunta a l'aire: per què cd no pot ser un programa extern. Aquesta pregunta és la porta d'entrada d'aquesta lliçó. Fins ara has fet servir el shell; ara entendràs què passa exactament entre que prems Enter i apareix el resultat. Aquest coneixement no és teòric: explica per què una variable definida en una canonada desapareix, per què which de vegades menteix, per què el teu script «no troba» una ordre que tu sí que veus, i per què source i executar un fitxer no són el mateix. Són precisament els errors que més temps fan perdre a qui programa en Bash sense aquest model mental.

Contingut

  1. Què passa quan prems Enter
  2. Els tipus d'ordre i com distingir-los
  3. El PATH i l'ordre de cerca
  4. Processos pare i fill: fork i exec
  5. Subshells i per què les variables no sobreviuen
  6. Entorn davant de variables de shell
  7. $?: el codi de sortida
  8. Posant-ho tot junt a Veloz Envíos

  1. Què passa quan prems Enter

Escrius ls -lh /var/log/veloz i prems Enter. El que sembla instantani són en realitat sis fases ben definides.

graph TD
    A["Prems Enter"] --> B["1. Lectura i trossejat<br/>divideix la línia en paraules"]
    B --> C["2. Expansions<br/>~, $VAR, *, $(...), aritmètica"]
    C --> D["3. Resolució de l'ordre<br/>àlies → keyword → funció → builtin → PATH"]
    D --> E{"És interna?"}
    E -->|"Sí: builtin/funció"| F["Executa dins del mateix Bash"]
    E -->|"No: executable extern"| G["4. fork(): crea procés fill"]
    G --> H["5. exec(): el fill es converteix<br/>en el programa"]
    H --> I["Bash espera amb wait()"]
    F --> J["6. Codi de sortida a $?"]
    I --> J
    J --> K["Nou prompt"]

Vegem cada fase.

Fase 1: lectura i trossejat

Bash llegeix la línia completa i la divideix en paraules fent servir espais, tabuladors i salts de línia com a separadors. També identifica els operadors especials (|, >, &&, ;). De ls -lh /var/log/veloz en surten tres paraules.

Aquí ja apareix la primera font d'errors. Si un nom de fitxer conté espais, el trossejat el parteix en dos:

ls el meu informe.txt

Bash hi entén tres arguments, no pas un. Per això les cometes són importants: ls "el meu informe.txt". Tot el detall és a 03-06.

Fase 2: expansions

Bash reescriu la línia abans d'executar res. Hi substitueix:

Expansió Exemple Es converteix en
Claus log-{a,b}.txt log-a.txt log-b.txt
Titlla ~/veloz-ops /home/joan/veloz-ops
Paràmetres $HOME /home/joan
Ordres $(date +%F) 2026-08-03
Aritmètica $((2 + 3)) 5
Rutes (globbing) *.log acces.log app.log

Aquest punt és crucial i sorprèn molta gent: l'ordre no veu mai els asteriscs. Quan escrius ls *.log, qui expandeix el patró és Bash, i ls rep ja la llista de noms. Ho pots comprovar:

cd /var/log/veloz
echo *.log
acces.log app.log veloz-api.log

echo no sap res de comodins: va rebre tres arguments ja resolts. El detall complet de les expansions es tracta a 02-05 i 03-06; aquí n'hi ha prou que sàpigues que passen abans.

Fase 3: resolució de l'ordre

Bash pren la primera paraula i cerca en un ordre estricte què és. Ho veurem a la secció 2, perquè és on es concentren les sorpreses.

Fases 4 i 5: fork i exec

Si l'ordre resulta ser un executable extern, Bash no es pot «convertir» en ell (deixaria d'existir el teu shell). Fa dues coses:

  1. fork(): crea una còpia de si mateix, un procés fill.
  2. exec(): el fill reemplaça la seva pròpia imatge en memòria per la del programa que s'ha d'executar.

El pare (el teu Bash) es queda esperant amb wait() que el fill acabi. Per això el prompt no torna fins que l'ordre acaba, tret que la llancis en segon pla amb &.

Fase 6: codi de sortida

Quan el fill mor, retorna un número entre 0 i 255. Bash el desa a $?. És la base de tot el control d'errors en scripting, i el veurem a la secció 7.

  1. Els tipus d'ordre i com distingir-los

No tot el que escrius al prompt és un programa. Bash reconeix cinc categories, i les cerca en aquest ordre exacte:

Ordre Tipus Què és Exemples
1 Àlies Substitució de text definida per tu ll, ops
2 Keyword Paraula reservada de la sintaxi de Bash if, for, while, function, [[
3 Funció Bloc de codi que has definit Les teves funcions de lib/
4 Builtin Ordre integrada en el mateix Bash cd, echo, export, source, type
5 Executable extern Un fitxer trobat al PATH ls, grep, awk, date

L'ordre importa. Si defineixes una funció anomenada ls, es farà servir en lloc de /usr/bin/ls. I si a més defineixes un àlies ls, l'àlies guanya a la funció.

2.1 type: l'eina fiable

type és un builtin de Bash i respon exactament el que Bash faria.

type cd
type ls
type if
type echo
cd és una ordre interna del shell
ls és un àlies de «ls --color=auto»
if és una paraula clau del shell
echo és una ordre interna del shell

L'opció -a mostra totes les coincidències, per ordre de prioritat. És reveladora:

type -a echo
echo és una ordre interna del shell
echo és /usr/bin/echo
echo és /bin/echo

Existeixen dos echo: el builtin de Bash i el programa /usr/bin/echo de coreutils. Guanya el builtin, sempre. I no són idèntics: les seves opcions difereixen lleugerament entre sistemes, cosa que provoca scripts que funcionen en una màquina i no pas en una altra. Per això en scripting professional es prefereix printf, que és molt més predictible (ho veurem al Mòdul 3).

Un altre exemple molt il·lustratiu amb el nostre PATH de veloz-ops:

type -a bash
bash és /usr/bin/bash
bash és /bin/bash

Hi apareixen dues rutes perquè als sistemes moderns /bin és un enllaç simbòlic a /usr/bin: és el mateix fitxer vist de dues maneres.

2.2 command -v: la versió per a scripts

command -v ls
command -v cd
alias ls='ls --color=auto'
cd

command -v imprimeix la ruta (o la definició) i retorna un codi de sortida que indica si existeix o no. És la manera estàndard i portable de comprovar disponibilitat en un script:

if command -v jq > /dev/null 2>&1; then
    echo "jq disponible"
else
    echo "Falta jq: instal·la'l amb sudo apt install jq"
fi

Aquest patró el farem servir de debò al Mòdul 6, quan veloz-ops necessiti jq per parlar amb l'API.

2.3 which: per què no te'n pots refiar

which ls
which cd
/usr/bin/ls

Observa: which cd no imprimeix res. I aquí rau el problema. which és un programa extern (en molts sistemes, un script) que es limita a recórrer el PATH cercant fitxers. No sap res de builtins, funcions ni keywords, perquè no pot veure l'interior del teu shell.

Eina Tipus Veu builtins Veu funcions Veu àlies Portable
type Builtin de Bash Bash/ksh/zsh
command -v Builtin POSIX Sí, POSIX
which Programa extern No No Depèn Inconsistent

La conclusió pràctica: fes servir type -a per investigar de manera interactiva i command -v dins dels scripts. Evita which. Un cas real de confusió: un company es queixa que time no existeix perquè which time no retorna res; en realitat time és una keyword de Bash, i type time ho aclareix a l'instant.

  1. El PATH i l'ordre de cerca

Quan Bash arriba al pas 5 (executable extern), recorre els directoris de PATH d'esquerra a dreta i executa la primera coincidència.

echo "$PATH" | tr ':' '\n'
/home/joan/veloz-ops/bin
/usr/local/sbin
/usr/local/bin
/usr/sbin
/usr/bin
/sbin
/bin

Punts importants:

  • La cerca s'atura a la primera coincidència. Si hi hagués un ls a ~/veloz-ops/bin, s'executaria aquell i no pas el del sistema.
  • L'ordre el decideixes tu. Posar el teu directori al davant dona prioritat a les teves eines; posar-lo al darrere és més segur.
  • Bash desa a la memòria cau les rutes trobades per no repetir la cerca. Si instal·les un programa nou i Bash continua dient que no existeix, neteja la memòria cau:
hash -r

O consulta què té memoritzat:

hash
encerts	ordre
   4	/usr/bin/ls
   2	/usr/bin/date

Aquest és un problema real i frustrant: acabes de moure un script de lloc, l'executes i Bash mira de llançar-lo des de la ruta antiga. hash -r ho resol en un segon.

3.1 Per què ./script.sh i no script.sh

Si ets a ~/veloz-ops/bin i escrius informe.sh, Bash cercarà al PATH. Com que el directori actual no és al PATH (i per seguretat no hi ha de ser, segons vam veure a 01-02), no el trobarà:

bash: informe.sh: no s'ha trobat l'ordre

La solució és indicar una ruta explícita: ./informe.sh. Tan bon punt una paraula conté una barra /, Bash deixa de cercar al PATH i la tracta com a ruta directa.

Amb la nostra configuració de veloz-ops hi ha un matís agradable: com que vam afegir ~/veloz-ops/bin al PATH, els scripts que hi posem que es podran invocar pel seu nom des de qualsevol directori, igual que ls. És exactament l'efecte que buscàvem en muntar el toolkit.

  1. Processos pare i fill: fork i exec

Cada procés a Linux té un identificador (PID) i un pare (PPID). El teu shell també.

echo "El meu PID és $$"
echo "El meu pare és $PPID"
El meu PID és 3412
El meu pare és 3399

$$ és el PID del shell actual i $PPID el del seu pare (normalment l'emulador de terminal o sshd).

Comprovem la relació pare-fill en directe:

echo "Shell actual: $$"
bash -c 'echo "Shell fill: $$, el seu pare: $PPID"'
Shell actual: 3412
Shell fill: 3587, el seu pare: 3412

El fill té un PID diferent i el seu pare és el teu shell. Aquesta és la mecànica del fork.

Quan el fill acaba, tot el que va fer a la seva pròpia memòria desapareix: variables, directori actual, funcions. Només sobreviu allò que va escriure al disc o va enviar per la seva sortida estàndard. Aquest principi explica la meitat dels «misteris» de Bash.

4.1 L'excepció: exec sense fork

Si fas servir exec explícitament, Bash no crea cap fill: es reemplaça a si mateix pel programa.

exec date

Es mostra la data i la terminal es tanca, perquè el teu shell ja no existeix: es va convertir en date, i date va acabar. És una eina especialitzada, molt usada als entrypoint.sh de contenidors Docker perquè el procés de l'aplicació hereti el PID 1 i rebi correctament els senyals d'aturada. No la facis servir a la lleugera.

  1. Subshells i per què les variables no sobreviuen

Un subshell és un procés fill que també és un Bash. Se'n creen en més situacions de les que la gent sospita.

Construcció Crea subshell? Exemple
( ordres ) (cd /tmp; ls)
Canonada ` ` , per a cada costat (a Bash per defecte)
Substitució $( ) avui=$(date +%F)
Segon pla & tasca &
{ ordres; } No, agrupa al shell actual { cd /tmp; ls; }
source fitxer No source ~/.bashrc
./script.sh Executa un fitxer

5.1 L'experiment clau

comptador=0
echo "Abans: $comptador"
( comptador=99; echo "Dins del subshell: $comptador" )
echo "Després: $comptador"
Abans: 0
Dins del subshell: 99
Després: 0

Els parèntesis van crear un procés fill amb una còpia de les variables. Aquest fill va modificar la seva còpia i va morir. El pare no se'n va assabentar mai. No és cap fallada: és com funcionen els processos a Unix. La informació flueix del pare al fill, mai a l'inrevés.

Compara-ho amb les claus:

comptador=0
{ comptador=99; echo "Dins de les claus: $comptador"; }
echo "Després: $comptador"
Dins de les claus: 99
Després: 99

Les claus només agrupen; no hi ha cap procés nou. (Atenció a la sintaxi: les claus necessiten espais interiors i un ; abans de la de tancament.)

5.2 El cas que arruïna scripts de debò

Aquest és l'error clàssic, i el plantegem amb dades de Veloz Envíos. Volem comptar quantes línies d'app.log són errors:

errors=0
grep 'ERROR' /var/log/veloz/app.log | while read -r linia; do
    errors=$((errors + 1))
done
echo "Errors trobats: $errors"
Errors trobats: 0

Zero, tot i que sí que hi ha errors al fitxer. Per què? Perquè el costat dret de la canonada s'executa en un subshell. El bucle va incrementar correctament la seva pròpia còpia d'errors fins a, posem, 47; després el subshell va acabar i la còpia es va evaporar. L'echo final llegeix la variable del shell pare, que continua valent 0.

Hi ha tres solucions, i convé conèixer-les totes:

# Solució 1: redirecció d'entrada en lloc de canonada
errors=0
while read -r linia; do
    errors=$((errors + 1))
done < <(grep 'ERROR' /var/log/veloz/app.log)
echo "Errors trobats: $errors"
Errors trobats: 47

La construcció < <(...) s'anomena substitució de processos: el grep s'executa a part, però el bucle while corre al shell principal, de manera que la variable sobreviu. S'estudia a fons a 05-05.

# Solució 2: activar l'opció lastpipe (només Bash, requereix job control desactivat)
shopt -s lastpipe
errors=0
grep 'ERROR' /var/log/veloz/app.log | while read -r linia; do
    errors=$((errors + 1))
done
echo "Errors trobats: $errors"

Amb lastpipe, l'última ordre de la canonada s'executa al shell actual. Funciona en scripts, però no pas en sessions interactives normals.

# Solució 3: no fer servir el shell per comptar
errors=$(grep -c 'ERROR' /var/log/veloz/app.log)
echo "Errors trobats: $errors"
Errors trobats: 47

Aquesta és la millor: grep -c compta pel seu compte i la substitució d'ordres en captura la sortida. Quan hi ha una eina que ja fa la feina, no la facis amb un bucle. És més ràpid, més curt i no té el problema del subshell.

5.3 El mateix principi explica source

Ara s'entén del tot allò que vam veure a 01-02:

# fitxer: config.sh
CIUTAT="Valencia"
./config.sh          # executa en un subshell
echo "$CIUTAT"       # buit
source config.sh     # executa al shell actual
echo "$CIUTAT"       # Valencia
Valencia

I per això cd ha de ser un builtin: si fos un programa extern, s'executaria en un fill, canviaria el directori d'aquest fill i moriria. El teu directori no es mouria ni un mil·límetre. Aquella pregunta pendent de la lliçó anterior queda resolta.

  1. Entorn davant de variables de shell

Hi ha dues classes de variables, i la diferència és exactament la de la secció anterior: què s'hereta.

  • Variable de shell: existeix només al shell actual. No es passa als fills.
  • Variable d'entorn: es copia a l'entorn de cada procés fill.
var_local="només aquí"
export var_global="viatja als fills"

bash -c 'echo "var_local=[$var_local] var_global=[$var_global]"'
var_local=[] var_global=[viatja als fills]

El fill no va veure var_local perquè mai no es va exportar.

6.1 Eines per inspeccionar

Ordre Què mostra
env Variables d'entorn (les exportades)
printenv Igual que env; admet printenv NOM
set Totes les variables i funcions del shell actual
declare -p Variables amb el seu tipus i atributs
export -p Només les exportades, amb la sintaxi declare -x
printenv HOME PATH USER
/home/joan
/home/joan/veloz-ops/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
joan
export -p | grep -i veloz
declare -x PATH="/home/joan/veloz-ops/bin:/usr/local/sbin:..."
declare -x VELOZ_ENV="produccio"

Variables d'entorn que et trobaràs sempre:

Variable Contingut
HOME La teva carpeta personal
USER El teu nom d'usuari
PATH Directoris de cerca d'ordres
PWD Directori actual
OLDPWD Directori anterior (el que fa servir cd -)
SHELL Shell d'inici de sessió configurat
LANG Idioma i codificació
TERM Tipus de terminal
EDITOR Editor preferit

6.2 Executar amb un entorn modificat

Pots definir una variable només per a una ordre, sense tocar el teu shell:

VELOZ_ENV=proves bash -c 'echo "Entorn: $VELOZ_ENV"'
echo "Al meu shell continua sent: [$VELOZ_ENV]"
Entorn: proves
Al meu shell continua sent: []

Aquesta sintaxi —assignació davant de l'ordre, sense ;— és netíssima i molt usada a la pràctica, per exemple per forçar un idioma a la sortida d'una ordre:

LC_ALL=C date
Mon Aug  3 10:15:22 CEST 2026

Amb LC_ALL=C la data surt en anglès. És un truc important en scripts: si el teu script analitza la sortida d'una ordre, força LC_ALL=C perquè no depengui de l'idioma del servidor. Un grep que cerca «ago» fallaria en una màquina configurada en anglès.

I per executar una cosa amb un entorn completament net:

env -i bash -c 'echo "PATH=[$PATH] HOME=[$HOME]"'
PATH=[] HOME=[]

env -i esborra tot l'entorn. És la millor manera de simular les condicions de cron, que executa amb un entorn mínim. Si el teu script funciona així, funcionarà a cron. Ho aplicarem a 07-01.

  1. $?: el codi de sortida

Tota ordre retorna en acabar un número entre 0 i 255:

  • 0 significa èxit.
  • Qualsevol altre valor significa error.

És a l'inrevés del que suggereix la intuïció, però té sentit: hi ha una única manera d'encertar i moltes de fallar, així que cada valor diferent de zero pot identificar un tipus de fallada.

ls /var/log/veloz > /dev/null
echo "Codi: $?"

ls /directori/inexistent 2> /dev/null
echo "Codi: $?"
Codi: 0
Codi: 2

Codis amb significat convencional:

Codi Significat
0 Èxit
1 Error genèric
2 Ús incorrecte de l'ordre (arguments mal posats)
126 El fitxer existeix però no és executable
127 Ordre no trobada
130 Interromput amb Ctrl+C (128 + senyal 2)
137 Acabat amb kill -9 (128 + senyal 9)

Comprovem-los:

ordre_que_no_existeix
echo "Codi: $?"
bash: ordre_que_no_existeix: no s'ha trobat l'ordre
Codi: 127

Un detall que causa errors subtils: $? se sobreescriu amb cada ordre, inclòs l'echo que la mostra.

ls /inexistent 2> /dev/null
echo "Primera lectura: $?"
echo "Segona lectura: $?"
Primera lectura: 2
Segona lectura: 0

La segona lectura retorna 0 perquè és el resultat de l'echo anterior, que va funcionar bé. Si necessites el valor més d'un cop, desa'l immediatament:

ls /inexistent 2> /dev/null
codi=$?
echo "Desat: $codi"
echo "Continua disponible: $codi"
Desat: 2
Continua disponible: 2

Un cas especial que mereix atenció: en una canonada, $? és el codi de l'última ordre, no pas de la primera.

grep 'ERROR' /fitxer/inexistent | wc -l
echo "Codi: $?"
grep: /fitxer/inexistent: El fitxer o directori no existeix
0
Codi: 0

Encara que grep va fallar, wc -l va funcionar, així que la canonada declara èxit. És una font d'errors silenciosos en scripts. Es resol amb set -o pipefail o consultant l'array PIPESTATUS, temes de la lliçó 05-03.

Els codis de sortida són la base de &&, ||, if i del control d'errors en general. Aquí només era un primer contacte; l'ús complet arriba a 03-03 i 03-04.

  1. Posant-ho tot junt a Veloz Envíos

Un escenari realista que combina tot l'anterior. Un company et diu: «He escrit un script veloz-estat que compta els errors del registre, però retorna sempre 0 i, a més, quan el llanço des de cron diu que no troba l'ordre.»

Diagnostiquem-ho pas a pas.

Pas 1: què és realment veloz-estat?

type -a veloz-estat
veloz-estat és /home/joan/veloz-ops/bin/veloz-estat

Existeix i és al PATH. Però cron no llegeix el teu ~/.bashrc (ho vam veure a 01-02), així que el seu PATH no inclou ~/veloz-ops/bin. Simulem l'entorn de cron:

env -i /bin/sh -c 'veloz-estat'
/bin/sh: 1: veloz-estat: not found

Reproduït. La solució: fer servir la ruta absoluta al crontab, o definir el PATH dins del mateix script.

Pas 2: per què compta 0?

L'script conté això:

total=0
grep 'ERROR' /var/log/veloz/app.log | while read -r linia; do
    total=$((total + 1))
done
echo "Total d'errors: $total"

Ja saps el diagnòstic: el while corre en un subshell i total no sobreviu. La correcció:

total=$(grep -c 'ERROR' /var/log/veloz/app.log)
echo "Total d'errors: $total"

Pas 3: i si el fitxer no existeix?

total=$(grep -c 'ERROR' /var/log/veloz/app.log)
codi=$?
echo "Total: $total (codi $codi)"

Si el registre no existeix, grep retorna 2 i total queda buit. Desar el codi permet reaccionar-hi. A 03-04 aprendràs a convertir això en una condició, i a 05-03 a fer-ho de manera robusta.

Pas 4: verificació final de l'entorn

echo "PID del shell: $$"
echo "Ordre resolta: $(command -v veloz-estat)"
echo "Directori: $PWD"
VELOZ_ENV=produccio veloz-estat
echo "Sortida de l'script: $?"

Aquest petit ritual —saber què s'executarà, amb quin entorn i què va retornar— és exactament el que distingeix algú que depura amb mètode d'algú que prova coses a l'atzar.

Errors Habituals i Consells

  • Fer servir which per comprovar si una ordre existeix. No veu builtins ni funcions i el seu comportament varia entre sistemes. Fes servir command -v als scripts i type -a de manera interactiva.
  • Esperar que una variable modificada dins d'una canonada sobrevisqui. El costat dret de | és un subshell. Fes servir < <(...), lastpipe o, encara millor, una eina que ja faci el càlcul (grep -c, wc -l, awk).
  • Executar un fitxer de configuració amb ./config.sh esperant que defineixi variables. Necessites source config.sh.
  • Oblidar export i estranyar-se que l'script fill no vegi la variable. Sense export, la variable no surt del shell actual.
  • Llegir $? massa tard. Qualsevol ordre intermèdia la sobreescriu, inclòs un echo. Desa-la tan bon punt la necessitis.
  • Suposar que una canonada falla si falla la primera ordre. $? reflecteix l'última. Fes servir set -o pipefail (05-03).
  • Modificar un script, moure'l i que Bash continuï executant la versió antiga. És la memòria cau de rutes: hash -r.
  • Donar per fet que l'entorn de cron és el teu. No ho és. Prova els teus scripts amb env -i abans de programar-los.
  • Consell: quan alguna cosa es comporti de manera inexplicable, fes-te aquestes tres preguntes per ordre: què és realment aquesta ordre (type -a)?, s'està executant en un subshell?, quines variables hi ha a l'entorn (env)?. Cobreixen la majoria dels casos.
  • Consell: bash -x script.sh mostra cada línia després de les expansions, abans d'executar-la. És la manera més ràpida de veure el procés de la secció 1 en acció. Ho formalitzarem a 05-03.

Exercicis

Exercici 1: Classificar ordres

Sense executar res, prediu de quin tipus d'ordre es tracta en cada cas (àlies, keyword, funció, builtin o extern) i després verifica-ho amb type -a:

  1. cd
  2. grep
  3. [[
  4. export
  5. for
  6. awk

Explica a més per què which cd no retorna res.

Exercici 2: El comptador que no compta

Aquest script pretén comptar quants enviaments amb estat incidencia hi ha a /srv/veloz/dades/enviaments.csv, però sempre imprimeix 0:

#!/usr/bin/env bash
incidencies=0
cat /srv/veloz/dades/enviaments.csv | while IFS=',' read -r id data ciutat repartidor estat import; do
    if [ "$estat" = "incidencia" ]; then
        incidencies=$((incidencies + 1))
    fi
done
echo "Incidencies: $incidencies"
  1. Explica exactament per què falla.
  2. Corregeix-lo de dues maneres diferents.
  3. Indica quina de les dues preferiries a veloz-ops i per què.

Exercici 3: Entorn i codis de sortida

Respon executant el que calgui:

  1. Defineix una variable CIUTAT="Bilbao" sense exportar i comprova si un bash -c fill la veu. Repeteix-ho exportant-la.
  2. Executa una ordre que retorni el codi 127 i una altra que retorni el 2, i mostra tots dos codis.
  3. Executa grep 'ERROR' /fitxer/que/no/existeix | wc -l i explica per què $? val 0.
  4. Comprova si jq està instal·lat al teu sistema fent servir command -v dins d'un if.

Solucions

Solució a l'Exercici 1

type -a cd grep '[[' export for awk
cd és una ordre interna del shell
grep és /usr/bin/grep
[[ és una paraula clau del shell
export és una ordre interna del shell
for és una paraula clau del shell
awk és /usr/bin/awk
Ordre Tipus Raó
cd Builtin Ha de canviar el directori del shell actual
grep Extern Programa independent a /usr/bin
[[ Keyword Part de la sintaxi; Bash l'analitza de manera especial (per això no hi ha divisió en paraules a dins)
export Builtin Modifica l'entorn del shell actual
for Keyword Estructura de control del llenguatge
awk Extern És un llenguatge complet amb el seu propi intèrpret

which cd no retorna res perquè which és un programa extern que només recorre els directoris del PATH cercant fitxers. cd no és cap fitxer: és codi dins del mateix Bash. Un procés extern no pot veure els builtins del seu pare.

Solució a l'Exercici 2

  1. Per què falla: el bucle while és al costat dret d'una canonada, així que Bash l'executa en un subshell. Aquest subshell rep una còpia d'incidencies, la incrementa correctament i mor en acabar el bucle. El shell principal no veu mai el canvi, i l'echo final llegeix la seva pròpia variable, que continua valent 0. És exactament el problema de la secció 5.2.

  2. Correcció A: substitució de processos (elimina la canonada, el bucle corre al shell principal):

#!/usr/bin/env bash
incidencies=0
while IFS=',' read -r id data ciutat repartidor estat import; do
    if [ "$estat" = "incidencia" ]; then
        incidencies=$((incidencies + 1))
    fi
done < /srv/veloz/dades/enviaments.csv
echo "Incidencies: $incidencies"

Fixa't que aquí ni tan sols cal < <(...): com que només volíem llegir un fitxer, n'hi ha prou amb una redirecció directa < fitxer. El cat original era innecessari (és l'anomenat ús inútil de cat).

Correcció B: delegar el recompte en una eina:

#!/usr/bin/env bash
incidencies=$(grep -c ',incidencia,' /srv/veloz/dades/enviaments.csv)
echo "Incidencies: $incidencies"

O de manera més precisa, comprovant exactament el cinquè camp amb awk (Mòdul 6):

incidencies=$(awk -F',' '$5 == "incidencia"' /srv/veloz/dades/enviaments.csv | wc -l)
  1. Quina preferir: la B, amb awk. Motius: és una sola línia, no té el problema del subshell i és molt més ràpida en fitxers grans. Un bucle while read en Bash processa unes poques milers de línies per segon; awk en processa centenars de milers. Amb un enviaments.csv d'11 MB la diferència és de segons davant de mil·lisegons. La regla general de Bash: el shell és per orquestrar, no per processar dades línia a línia. Ho veurem formalitzat a 08-02.

Solució a l'Exercici 3

# 1
CIUTAT="Bilbao"
bash -c 'echo "Sense export: [$CIUTAT]"'
export CIUTAT
bash -c 'echo "Amb export: [$CIUTAT]"'
Sense export: []
Amb export: [Bilbao]

Sense export, CIUTAT és una variable de shell i no forma part de l'entorn que hereta el fill. Amb export, passa a formar part de l'entorn i viatja a la còpia.

# 2
ordre_inexistent_veloz
echo "Codi A: $?"

ls --opcio-invalida 2> /dev/null
echo "Codi B: $?"
bash: ordre_inexistent_veloz: no s'ha trobat l'ordre
Codi A: 127
Codi B: 2

127 és el codi convencional per a «ordre no trobada» i 2 per a «ús incorrecte».

# 3
grep 'ERROR' /fitxer/que/no/existeix | wc -l
echo "Codi: $?"
grep: /fitxer/que/no/existeix: El fitxer o directori no existeix
0
Codi: 0

$? val 0 perquè en una canonada reflecteix l'estat de l'última ordre. grep va fallar amb codi 2, però wc -l va rebre una entrada buida, va comptar zero línies i va acabar correctament. Per detectar la fallada real:

grep 'ERROR' /fitxer/que/no/existeix | wc -l
echo "Estats de la canonada: ${PIPESTATUS[@]}"
Estats de la canonada: 2 0

L'array PIPESTATUS desa el codi de cada element. L'alternativa habitual en scripts és set -o pipefail, que fa que la canonada retorni el primer codi diferent de zero (05-03).

# 4
if command -v jq > /dev/null 2>&1; then
    echo "jq està instal·lat a $(command -v jq)"
else
    echo "jq NO està instal·lat"
fi
jq NO està instal·lat

La redirecció > /dev/null 2>&1 descarta tant la sortida normal com els errors: només ens interessa el codi de sortida de command -v, no pas el seu text. Les redireccions s'estudien a 02-04.

Conclusió

Ja tens el model mental que sosté tot el que ve. Saps que Bash trosseja la línia, l'expandeix, resol l'ordre en un ordre estricte (àlies → keyword → funció → builtin → PATH) i, si és externa, fa fork i exec per executar-la en un fill. Entens per què type -a és fiable i which no, com funciona la memòria cau del PATH i —el més rendible de tot— per què les variables no sobreviuen a un subshell, cosa que explica d'un cop el comportament de les canonades, els parèntesis, source i el mateix cd. I has tingut el teu primer contacte amb $?, la peça sobre la qual es construirà tot el control d'errors del curs.

Amb això tanques el coneixement fonamental del shell. Queda una habilitat transversal abans de passar a les ordres: saber resoldre dubtes pel teu compte. Cap professional no recorda totes les opcions de find ni el format exacte d'un crontab; el que sap és on mirar-ho en deu segons. A la lliçó següent, Trobar Ajuda: man, help i --help, aprendràs a llegir pàgines de manual, a distingir quan cal fer servir man i quan help, i a verificar ordres perilloses abans d'executar-les a srv-veloz-01.

Curs de Programació en Bash

Mòdul 1: Introducció a Bash

Mòdul 2: Ordres Bàsiques de Bash

Mòdul 3: Fonaments de Scripting

Mòdul 4: Scripting Intermedi

Mòdul 5: Tècniques Avançades de Scripting

Mòdul 6: Treballar amb Eines Externes

Mòdul 7: Automatització i Programació

Mòdul 8: Bones Pràctiques i Optimització

Mòdul 9: Projectes del Món Real

© Copyright 2026. Tots els drets reservats