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
- Longitud i subcadenes
- Eliminar prefixos i sufixos
- Substitució de text
- Majúscules i minúscules
- Concatenació
- Comparació i coincidència de patrons
- Dividir una cadena en array
printf: amplades, alineació i decimals- Expansions davant de
sed,cut,basename - Exemple integrador
- 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 últimsLa 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.
- 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 contactarAquest 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.
- 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 FINALResum: 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.
- 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úsculesL'ú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.
- 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üentLes 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.
- 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 ==.
- 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: perillosaLa 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 | BilbaoNota: 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.
printf: amplades, alineació i decimals
printf: amplades, alineació i decimalsJa 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' hola → hola |
%-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.
- Expansions davant de
sed, cut, basename
sed, cut, basenameTot 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 sLa 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.
- 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íem10:15.$(( 10#$h ))força la interpretació en base 10. Una hora com08s'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 unechoabans 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à tocatr. - 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" # SevillaNormalitzar 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 umbralCada 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
- Què és Bash?
- Configurar el teu Entorn
- Navegació Bàsica per la Línia d'Ordres
- Entendre el Shell
- Trobar Ajuda: man, help i --help
Mòdul 2: Ordres Bàsiques de Bash
- Operacions amb Fitxers i Directoris
- Ordres de Processament de Text
- Permisos i Propietat dels Fitxers
- Redirecció i Canonades
- Comodins i Expansió de Rutes
- Historial i Dreceres de Teclat
Mòdul 3: Fonaments de Scripting
- Crear i Executar un Script
- Variables i Constants
- Operadors Bàsics
- Sentències Condicionals
- Arguments i Entrada de l'Usuari
- Cometes, Expansió i Substitució
Mòdul 4: Scripting Intermedi
- Bucles en Bash
- Funcions en Bash
- Arrays i Arrays Associatius
- Manipulació de Cadenes
- La Sentència case i els Menús Interactius
- Aritmètica i Càlculs Numèrics
Mòdul 5: Tècniques Avançades de Scripting
- Operacions Avançades amb Fitxers
- Gestió de Processos
- Gestió d'Errors i Depuració
- Expressions Regulars
- Entrada/Sortida Avançada: Descriptors i Here-Documents
- Scripts Modulars i Llibreries Reutilitzables
Mòdul 6: Treballar amb Eines Externes
Mòdul 7: Automatització i Programació
- Tasques Cron
- Automatitzar Tasques
- Scripts de Còpia i Restauració
- Monitoratge i Registre
- Serveis i Temporitzadors amb systemd
- Automatització Remota amb SSH
Mòdul 8: Bones Pràctiques i Optimització
- Escriure Codi Llegible
- Optimitzar Scripts en Bash
- Consideracions de Seguretat
- Control de Versions amb Git
- Anàlisi Estàtica amb ShellCheck i shfmt
- Proves Automatitzades amb Bats
- Portabilitat: POSIX sh enfront de Bashismes
