Tancàvem la lliçó d'arrays amb dos deutes: ${import%.*}, que retallava els cèntims, i ${ENVIAMENTS[$c]:-0}, que hi posava un valor per defecte. El segon ja el coneixes de 03-06; el primer pertany a una família molt més gran. Bash pot tallar, retallar, substituir i transformar cadenes sense cridar cap programa extern, amb la mateixa sintaxi ${...} de sempre. Això importa per dos motius: el codi queda més curt, i sobretot és entre deu i cent vegades més ràpid, perquè cada sed o cut dins d'un bucle és un procés nou del sistema operatiu. En aquesta lliçó completes la família sencera i aprens a formatar la sortida d'informe-diari.sh amb printf.

Contingut

  1. Longitud i subcadenes
  2. Eliminar prefixos i sufixos
  3. Substitució de text
  4. Majúscules i minúscules
  5. Concatenació
  6. Comparació i coincidència de patrons
  7. Dividir una cadena en array
  8. printf: amplades, alineació i decimals
  9. Expansions davant de sed, cut, basename
  10. Exemple integrador

  1. Longitud i subcadenes

s="2026-08-03 10:15:22 [ERROR] timeout al contactar con la pasarela"

echo "${#s}"          # 62      → nombre de caràcters
echo "${s:0:10}"      # 2026-08-03   → des de la posició 0, 10 caràcters
echo "${s:11:8}"      # 10:15:22     → des de l'11, 8 caràcters
echo "${s:20}"        # [ERROR] timeout ...  → des de la 20 fins al final
echo "${s: -7}"       # asarela     → els 7 últims (ull: espai abans del -7)
echo "${s:0:-7}"      # tot menys els 7 últims

La sintaxi és ${cadena:desplacament:longitud}, amb el desplaçament començant a 0. Tres notes pràctiques: l'espai abans d'un número negatiu és obligatori, perquè sense ell ${s:-7} significa «valor per defecte 7», una expansió completament diferent (també val ${s:(-7)}); tots dos números admeten aritmètica, com ${s:i:n} o ${s:0:${#s}-4}; i es compten caràcters, no bytes, sempre que el locale sigui UTF-8, així que una ñ compta 1. Funciona també sobre els arguments: ${1:0:4} són els quatre primers caràcters de $1.

  1. Eliminar prefixos i sufixos

Quatre operadors que retallen el que coincideixi amb un patró glob (els de 02-05: *, ?, [...]), no amb una expressió regular:

Expansió Treu Coincidència
${s#patro} Del principi La més curta
${s##patro} Del principi La més llarga
${s%patro} Del final La més curta
${s%%patro} Del final La més llarga

Regla mnemotècnica, mirant un teclat: # és a l'esquerra del $ i retalla per l'esquerra; % és a la dreta i retalla per la dreta. I doblar el símbol significa «menja't tot el que puguis».

cami="/var/log/veloz/app.log.3.gz"
echo "${cami##*/}"       # app.log.3.gz   → nom del fitxer (treu fins a l'última /)
echo "${cami#*/}"        # var/log/...    → treu només fins a la PRIMERA /
echo "${cami%/*}"        # /var/log/veloz → directori (treu des de l'última /)
echo "${cami%%.*}"       # /var/log/veloz/app       → treu des del primer punt
echo "${cami%.*}"        # /var/log/veloz/app.log.3 → treu només l'última extensió

Aplicat a les nostres dades:

import="24.50"
echo "${import%.*}"      # 24   → part entera (el deute de 04-03)
echo "${import#*.}"      # 50   → cèntims
f="app.log.3.gz"
echo "${f%%.*}"          # app  → nom base
echo "${f##*.}"          # gz   → extensió

linia="2026-08-03 10:15:22 [ERROR] timeout al contactar"
resta="${linia#* }"                    # treu la data
hora="${resta%% *}"                    # 10:15:22
nivell="${linia#*\[}"; nivell="${nivell%%\]*}"   # ERROR
missatge="${linia#*] }"                # timeout al contactar

Aquest bloc final extreu data, hora, nivell i missatge d'una línia d'app.log sense llançar ni un sol procés. Els \\[ i \\] van escapats perquè en un patró glob els claudàtors defineixen una classe de caràcters.

  1. Substitució de text

s="Valencia,Sevilla,Bilbao,Madrid"
echo "${s/,/ }"          # Valencia Sevilla,Bilbao,Madrid   → només la PRIMERA
echo "${s//,/ }"         # Valencia Sevilla Bilbao Madrid   → TOTES
echo "${s//,}"           # ValenciaSevillaBilbaoMadrid      → esborra (reemplaçament buit)
echo "${s/#Valencia/VLC}"  # VLC,Sevilla,...   → només si és al PRINCIPI
echo "${s/%Madrid/MAD}"    # ...,Sevilla,Bilbao,MAD → només si és al FINAL

Resum: una barra substitueix la primera coincidència, dues barres totes, /# ancora al principi i /% al final. El patró de cerca torna a ser un glob, cosa que permet coses com:

echo "${cami//\//-}"                            # la / s'escapa com \/
nom="  alopez  "
nom="${nom#"${nom%%[![:space:]]*}"}"            # treu espais inicials
nom="${nom%"${nom##*[![:space:]]}"}"            # treu espais finals
echo "[$nom]"                                   # [alopez]

Aquest idioma per retallar espais és lleig però és l'estàndard en Bash pur; desa'l com a funció trim() a la teva llibreria.

  1. Majúscules i minúscules

Quatre operadors, disponibles des de Bash 4:

ciutat="valencia"
echo "${ciutat^}"     # Valencia   → primera lletra a majúscula
echo "${ciutat^^}"    # VALENCIA   → tot a majúscules
c="MADRID"
echo "${c,}"          # mADRID     → primera lletra a minúscula
echo "${c,,}"         # madrid     → tot a minúscules

L'ús més freqüent és normalitzar abans de comparar, perquè l'usuari pugui escriure VALENCIA, valencia o Valencia: ciutat="${1,,}"; ciutat="${ciutat^}" deixa sempre Valencia. Es pot limitar a certs caràcters amb un patró (${s^^[aeiou]} posa en majúscules només les vocals), tot i que és poc freqüent.

  1. Concatenació

Bash no té operador de concatenació: n'hi ha prou d'escriure les cadenes seguides.

base="/var/log/veloz"; fitxer="app.log"
cami="$base/$fitxer"               # /var/log/veloz/app.log
prefix="informe"
nom="${prefix}_$(date +%F).txt"    # les claus separen el nom del text següent

Les claus de ${prefix}_ són obligatòries aquí: sense elles Bash buscaria una variable anomenada prefix_. I per anar acumulant text en un bucle:

llista=""
for ciutat in Valencia Sevilla Bilbao; do llista+="$ciutat, "; done   # += afegeix al final
echo "${llista%, }"             # Valencia, Sevilla, Bilbao  (treu la coma final)

+= sobre una cadena concatena; sobre un array (04-03) afegeix elements; sobre una variable declare -i suma. El mateix operador, tres comportaments segons el tipus.

  1. Comparació i coincidència de patrons

Dins de [[ ]], l'operador == fa alguna cosa més que comparar: si el costat dret no va entre cometes, s'interpreta com un patró glob.

f="app.log.3.gz"
[[ "$f" == app.log.* ]]         # CERT → coincidència de patró
[[ "$f" == *.gz ]]              # CERT → acaba en .gz
[[ "$f" == "app.log.*" ]]       # FALS → amb cometes, és literal
[[ "$f" == ?pp* ]]              # CERT → ? és un caràcter qualsevol
patro="*.gz"
[[ "$f" == $patro ]]            # CERT → la variable sense cometes actua de patró
[[ "$f" == "$patro" ]]          # FALS → amb cometes, compara amb el text "*.gz"

Aquesta és l'única situació del curs en què treure les cometes és deliberat i correcte: quan vols que el contingut de la variable funcioni com a patró. En qualsevol altre context, cometes.

Altres operadors de cadena a [[ ]], ja vistos a 03-03: !=, < i > (ordre alfabètic segons el locale), -z (buida) i -n (no buida).

Per a coincidències que un glob no pot expressar —«tres dígits seguits d'una lletra», «una IP vàlida»— cal l'operador =~ amb expressions regulars. És tema complet de 05-04; de moment, si el patró es pot escriure amb * i ?, fes servir ==.

  1. Dividir una cadena en array

Ja ho vas veure a 04-03; ho recollim aquí perquè és una operació de cadenes de ple dret:

linia="E1001,2026-08-03,Valencia,alopez,lliurat,24.50"
IFS=',' read -r -a camps <<< "$linia"        # la forma recomanada
echo "${camps[2]}"                           # Valencia
IFS=',' ; camps=( $linia ) ; unset IFS       # més curta, però aplica globbing: perillosa

La segona variant falla si un camp conté *, perquè l'expansió de comodins el convertirà en noms de fitxers. L'operació inversa, unir un array en una cadena, es fa amb "${arr[*]}" i l'IFS adequat:

ciutats=(Valencia Sevilla Bilbao)
IFS=' | '; echo "${ciutats[*]}"; unset IFS   # Valencia | Sevilla | Bilbao

Nota: només es fa servir el primer caràcter d'IFS com a separador a ${arr[*]}, així que l'exemple anterior uneix amb un espai. Per a separadors de diversos caràcters, fes servir printf o un bucle.

  1. printf: amplades, alineació i decimals

Ja saps de 03-06 que printf és preferible a echo. Aquí n'explotem els especificadors per construir taules:

Especificador Significat Exemple → sortida
%s Cadena printf '%s' holahola
%-10s Cadena, amplada 10, alineada a l'esquerra hola
%10s Cadena, amplada 10, alineada a la dreta hola
%5d Enter, amplada 5, a la dreta 128
%05d Enter, farciment amb zeros 00128
%.2f Decimal amb 2 xifres (arrodoneix) 24.50
%8.2f Decimal, amplada 8, 2 xifres 24.50
%% Un signe de percentatge literal %
printf '%-10s %8s %13s %9s\n' CIUTAT ENVIAMENTS INCIDENCIES IMPORT
printf '%-10s %8d %13d %8.2f€\n' Valencia 128 11 3204.55
# CIUTAT     ENVIAMENTS   INCIDENCIES    IMPORT
# Valencia        128            11  3204.55€

Tres comportaments de printf que val la pena conèixer: reutilitza el format mentre quedin arguments, de manera que printf '%s\n' "${ciutats[@]}" imprimeix cada ciutat a la seva línia sense necessitat de bucle; l'amplada pot venir d'una variable fent servir *, com a printf '%-*s\n' 15 "$ciutat"; i %.2f arrodoneix, no trunca (printf '%.2f' 2.005 dona 2.01), i admet decimals encara que Bash no els sàpiga calcular, perquè el formatatge el fa printf i no l'aritmètica del shell (04-06). Amb caràcters accentuats l'amplada es compta en caràcters si el locale és UTF-8; si les teves columnes ballen, revisa LANG.

  1. Expansions davant de sed, cut, basename

Tot el d'aquesta lliçó també es pot fer amb eines externes. La diferència és el cost:

Tasca Amb eina externa Amb expansió
Nom del fitxer basename "$cami" "${cami##*/}"
Directori dirname "$cami" "${cami%/*}"
Treure l'extensió basename "$f" .csv "${f%.csv}"
Camp 3 d'un CSV cut -d, -f3 <<< "$l" read -r -a c <<< "$l"; "${c[2]}"
Substituir text sed 's/,/;/g' <<< "$s" "${s//,/;}"
A majúscules tr a-z A-Z <<< "$s" "${s^^}"

Cada columna esquerra llança un procés nou: el nucli fa un fork, carrega el binari, munta una canonada i espera. Són uns pocs mil·lisegons… multiplicats pel nombre d'iteracions.

while IFS= read -r l; do echo "$l" | cut -d, -f3; done < enviaments.csv  # 5.000 línies: ~12 s
while IFS=, read -r _ _ ciutat _; do echo "$ciutat"; done < enviaments.csv  # les mateixes: ~0,3 s

La regla pràctica: dins d'un bucle, expansió; fora d'un bucle, l'eina que sigui més llegible. I si la feina és processar un fitxer sencer d'una tirada, gairebé sempre guanya awk en una sola passada (06-01); hi tornarem quan mesurem de debò a 08-02. Un matís a favor de basename/dirname: gestionen els casos rars (camins acabats en /, ., ..) segons l'estàndard, mentre que "${cami%/*}" sobre un fitxer.txt sense barres retorna fitxer.txt i no .. Per a camins controlats per tu, l'expansió és perfecta; per a camins arbitraris de l'usuari, fes servir les eines.

  1. Exemple integrador

Un analitzador d'app.log que extreu els camps amb expansions i formata una taula amb printf, sense cut, sense awk i sense sed:

# resum_log — Taula d'errors per hora a partir d'app.log.
# Ús: resum_log <fitxer> <data>   Retorna: 0 | 3 si el fitxer no és llegible
resum_log() {
    local fitxer="${1:?}" data="${2:?}" linia resta hora nivell h
    declare -A per_hora
    [[ -r "$fitxer" ]] || return 3
    while IFS= read -r linia; do
        [[ "$linia" == "$data"* ]] || continue     # patró glob: comença per la data
        resta="${linia#* }"                        # treu "2026-08-03 "
        hora="${resta%%:*}"                        # "10"
        nivell="${linia#*[}"; nivell="${nivell%%]*}"  # "ERROR"
        [[ "$nivell" == "ERROR" ]] && (( per_hora["$hora"]++ ))
    done < "$fitxer"
    printf '%-6s %8s\n' HORA ERRORS
    for h in $(printf '%s\n' "${!per_hora[@]}" | sort); do
        printf '%02d:00  %8d\n' "$(( 10#$h ))" "${per_hora[$h]}"
    done
}

Sortida: una taula HORA / ERRORS amb línies com ara 10:00 14. Comentaris sobre les parts noves:

  • [[ "$linia" == "$data"* ]]: la variable va entre cometes i l'* a fora, així es compara literalment la data i s'accepta qualsevol cosa després. És l'idioma correcte per a «comença per».
  • ${resta%%:*} talla al primer :, deixant l'hora. Amb % en comptes de %% hauríem tallat a l'últim i obtindríem 10:15.
  • $(( 10#$h )) força la interpretació en base 10. Una hora com 08 s'interpretaria com a octal invàlid i donaria error; és un parany clàssic que desmuntem sencer a 04-06.

I a informe-diari.sh, la funció de format queda en una línia: fila_ciutat() { printf '%-10s %8d %13d %10.2f€\n' "${1^}" "$2" "$3" "$4"; }. El ${1^} normalitza la inicial de la ciutat i les amplades fixes garanteixen que les columnes quadrin vingui com vingui la dada.

Errors Habituals i Consells

  • Descuidar l'espai a ${s: -3}. Sense ell, ${s:-3} és «valor per defecte», una expansió diferent que no donarà error però retornarà una altra cosa.
  • Confondre #/## i %/%%. Prova-ho sempre amb un echo abans de ficar-ho a l'script; la diferència entre la coincidència més curta i la més llarga trenca camins amb diversos punts.
  • Creure que els patrons són regex. ${s#[0-9]+} no funciona: + no és un quantificador en globbing. Per a regex, =~ a 05-04.
  • Posar entre cometes el patró a [[ $f == "*.gz" ]]. El converteix en literal i sempre dona fals. El patró va sense cometes; la variable, amb elles.
  • Aplicar ${s^^} en Bash 3.2 (macOS). No existeix; allà toca tr.
  • Consell: quan una cadena de retallades es torni il·legible (${${x#a}%b} ni tan sols és vàlid en Bash), fes servir variables intermèdies amb nom. Tres línies clares valen més que una de críptica.

Exercicis

Exercici 1. Donada cami="/srv/veloz/dades/enviaments-2026-08-03.csv", obtén amb expansions: el nom del fitxer, el directori, el nom sense extensió i la data 2026-08-03 que porta a dins.

Exercici 2. Escriu normalitza_ciutat() que accepti qualsevol combinació de majúscules (SEVILLA, sevilla, SeViLla) i retorni sempre Sevilla; si la ciutat no és de les quatre, retorna codi 1.

Exercici 3. A partir de la línia 2026-08-03 10:15:22 [WARN] cola de reparto por encima del umbral, extreu data, hora, nivell i missatge en quatre variables sense fer servir eines externes, i imprimeix-los amb printf en columnes alineades.

Solucions

Solució 1.

cami="/srv/veloz/dades/enviaments-2026-08-03.csv"
fitxer="${cami##*/}"           # enviaments-2026-08-03.csv
directori="${cami%/*}"         # /srv/veloz/dades
base="${fitxer%.csv}"          # enviaments-2026-08-03
data="${base#enviaments-}"     # 2026-08-03
printf '%s | %s | %s | %s\n' "$fitxer" "$directori" "$base" "$data"

Solució 2.

normalitza_ciutat() {
    local entrada="${1:?}" c
    entrada="${entrada,,}"                    # tot a minúscules
    for c in Valencia Sevilla Bilbao Madrid; do
        [[ "$entrada" == "${c,,}" ]] && { printf '%s\n' "$c"; return 0; }
    done
    return 1
}
normalitza_ciutat "SeViLla"                   # Sevilla

Normalitzar tots dos costats a minúscules i comparar és més robust que jugar amb ${c^}, perquè retorna la forma canònica exacta de la llista.

Solució 3.

linia="2026-08-03 10:15:22 [WARN] cola de reparto por encima del umbral"
data="${linia%% *}"                   # fins al primer espai
resta="${linia#* }"                   # treu la data
hora="${resta%% *}"                   # fins a l'espai següent
nivell="${linia#*\[}"; nivell="${nivell%%\]*}"
missatge="${linia#*\] }"

printf '%-12s %-10s %-7s %s\n' "$data" "$hora" "$nivell" "$missatge"
# 2026-08-03   10:15:22   WARN    cola de reparto por encima del umbral

Cada línia fa servir l'operador mínim necessari: %% per a «fins al primer per la dreta», # per a «el primer per l'esquerra». Zero processos llançats.

Conclusió

Ja no necessites cut, tr ni sed per a la feina rutinària sobre cadenes. ${#s} mesura, ${s:i:n} talla per posició, #/##/%/%% retallen per patró des de cada extrem, / i // substitueixen, ^/, canvien la caixa, += concatena, [[ $s == patro* ]] compara amb globs, IFS reparteix una línia en un array, i printf amb amplades i %.2f produeix taules que quadren. Tot dins del mateix procés, que en un bucle sobre milers de línies és la diferència entre segons i minuts.

Ens queda una peça estructural. Quan a l'exercici 2 vas recórrer quatre ciutats amb un bucle per veure si una cadena era vàlida, feies servir l'eina equivocada: comparar un valor contra una llista tancada d'alternatives és exactament el que fa case, la construcció que a 04-05 rebrà per fi el tractament formal que a 03-05 només vam fer servir de passada per analitzar opcions. Amb ella donarem a informe-diari.sh subordres (resum, detall, ciutats) i construirem veloz-menu.sh, un menú interactiu del toolkit amb select.

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