- Bash allows you to automate tasks in GNU/Linux using scripts with shebang, appropriate permissions, and positional parameters to receive arguments.
- The language offers variables, arrays, control structures, functions, and output codes to build robust and reusable scripts.
- Redirections, pipes, and file comparators facilitate data processing and integration with standard system commands.
- Basic console commands and environment variables complement the use of Bash for administration and development in Unix environments.
If you work with GNU/Linux, sooner or later you'll end up writing your own Bash script to automate repetitive tasksBackups, cleaning temporary files, deployments, small tests... all of that becomes much easier when you have a basic understanding of the Shell.
This guide aims to be an authentic "Bash manual" in Spanish with a practical approachDesigned to be used as a cheat sheet for everyday tasks, this guide covers everything from scripts and how to grant permissions to variables, arrays, control structures, functions, redirects, pipes, real-world examples, and basic console commands that complement the language.
What is Bash and what is a Bash script?
Bash (Bourne Again SHell) is a a Unix shell and also a scripting languageIt is the default command interpreter in most GNU/Linux distributions (Debian, Ubuntu, etc.) and allows you to both run commands interactively and save them to files for automation.
A Bash script is nothing more than a plain text file with a series of commands that the shell executes sequentially, from the first to the last line, except when functions, loops or control jumps are involved.
With a few commands, conditionals, and iterations, a simple text file can be transformed into a small but powerful administrative tool: rotate logs, manage backups, monitor services, clean directories, launch periodic tasks, etc.
How to create and prepare a Bash script
For Linux to treat a file as a Bash script, there are three basic points: initial shebang, execution permissions, and filename (The extension is optional, but recommended).
1. Specify the shell to use
The first line of the script must specify which interpreter will execute it, using the so-called shebang:
#!/bin/bash
If you're interested in debugging what happens at each step, you can activate trace mode by adding -x at the end of the line:
#!/bin/bash -x
This way, Bash will display each command on the screen as it is executed, which is very useful for detecting errors in large scripts.
2. Grant execution permissions
Once you've finished editing the file, you need to make it executable. The most common methods are:
chmod 755 nombre_del_script
chmod +x nombre_del_script
With either of the two commands, the system marks the file as executable for its owner (and, in the first case, also read/run for group and rest).
3. Script name and extension
Linux doesn't need an extension to know which interpreter to use to launch the file, because it gets that information from the... first line shebangEven so, it's customary to use the extension .sh to quickly identify that it is a shell script.
In Bash, everything behind the character # se considera comment until the end of the line (except in the initial shebang). You can also add comments after a command:
# Esta línea es un comentario completo
echo "Bienvenido" # Imprime un mensaje por pantalla
Your first Bash script step by step
To edit scripts you can use any text editor: graphics like VS Code, Atom or Gedit, or terminal classics like nano, vi o vimThe important thing is that it saves plain text without any unusual formatting.
We are going to create a file called hola.sh with a typical "Hello World" in Bash:
vim hola.sh
Inside, write something similar to this (each instruction on its own line):
#!/bin/bash
echo "Hola. Soy tu primer script Bash"
The command echo simply displays text or variable values on the screenAnother commonly used alternative is printf, which allows formatting the output (field width, alignment, etc.).
Save and close the editor (in vim, key ESC and then :wqThen, mark the file as executable:
chmod 755 hola.sh
To launch it from the terminal, navigate to the directory where the script is located and type:
./hola.sh
You should see something like this on the console: Hello. I'm your first Bash script.From here on, the basic mechanics will always be the same: shebang, commands, permissions, and execution.
Positional parameters and shift command
Bash scripts can receive arguments from the command line, which are exposed within the script through special variables called positional parameters.
The most commonly used parameter variables are:
$#– total number of arguments received.$0– script name (with or without path).$1 ... $9– the first nine positional parameters.${N}– access to an index parameter greater than 9 (for example,${10}).$*– all arguments as a single string (except$0).$@– all arguments as a list, useful for iterating correctly.$$– PID (process identifier) of the running script.$?– exit code of the last executed command.
The command shift allow shift the positional parameters to the left. For example, yes $1 contains UNO y $2 contains DOS, after executing:
shift
the value DOS will pass $1 and the original first argument is lost. This is very useful for looping through parameters, especially in scripts with many flags.
A typical example in production would be something like this:
./find-truncate.sh --directory /var/tmp --max-size 300M --days 15
Where the script analyzes the received parameters (the order doesn't matter) and interprets option-value pairs , the --directory /var/tmpUsing shift successive to move forward through the list.
Parameter processing with case and shift
A convenient way to handle arguments is to combine a loop with case y shift for each optionFor example, a script that receives a first and last name:
#!/bin/bash
# USO: ./nombre-apellido.sh --nombre NOMBRE --apellido APELLIDO
while ]
do
case "$1" in
-n|--nombre)
shift # saltamos a NOMBRE
nombre="$1"
shift # pasamos al siguiente parámetro
;;
-a|--apellido)
shift
apellido="$1"
shift
;;
*) # parámetro desconocido, lo ignoramos
shift
;;
esac
done
echo "Tu Nombre es: $nombre y tu Apellido es: $apellido"
Launching the script as:
./nombre-apellido.sh --nombre Luis --apellido Gutierrez
or by changing the order:
./nombre-apellido.sh --apellido Gutierrez --nombre Luis
The same phrase will be printed, because the script It does not depend on the order of the argumentsbut of its label.
Variables in Bash: types, scope, and expansion
In Bash there are no strict types like in other languages; variables are strings that can contain text, numbers, or arraysThere are no native booleans; it works with output codes and comparisons.
The declaration is made by writing the name followed by an equal sign, without spaces around the =:
VAR=10 # número
VAR=texto # texto sin comillas
VAR='literal' # cadena sin expandir variables
VAR="cadena" # cadena donde SÍ se expanden variables
If the string includes spaces, it is essential to wrap it in single or double quotation marksor only the first word will be saved. For example:
NOMBRE=Luis
CALLE="Calle Larga"
Despacho=401
Another very powerful way to create variables is capturing command outputThere are two equivalent syntaxes:
USUARIO=$(whoami)
USUARIO=`whoami`
It is also possible to directly redirect stdin to a variable or read entire files:
Texto=`cat fichero.txt`
read Texto < fichero.txt
If you want to store both the standard output and errors of a command in a variable, redirect stderr a stdout within the substitution:
Salida=$(comando 2>&1)
To concatenate variables and text they are simply written in succession, optionally using double quotation marks to preserve spaces:
VarA=$OTRA
VarB="$V1 texto1 $V2 texto2"
You can even reuse the value of the variable itself in the assignment:
Cadena="En un lugar"
Cadena="$Cadena de la Mancha" # Resultado: "En un lugar de la Mancha"
Single vs. double quotes and variable expansion
The difference between Single and double quotes are critical in BashAnything between single quotes is treated as literal text: no special variables or sequences are expanded.
For example:
VAR='NO procesar $V1'
echo "$VAR" # Imprime literalmente: NO procesar $V1
With double quotes, however, Bash substitutes variables and evaluates escape sequences:
VarA="En un lugar"
VarB='de la Mancha'
VarC="de cuyo nombre no quiero"
VarD=acordarme
TEXTO="$VarA $VarB $VarC $VarD"
By showing $TEXTO It will be seen: In a village of La Mancha, the name of which I do not wish to recallNote that VarB It was in single quotes, but it expanded when inserted outside of them.
If you write something like:
NONO='$3 $2 $1'
The chain will be literally saved $3 $2 $1 and not the values of those parametersThis is a typical source of confusion when starting out.
Arrays in Bash: creation, access and traversal
Bash supports numerically indexed arrays. To declare them you can use declare, typeset or the syntax in parentheses:
declare Marcatypeset MarcaFrutas=(Pera Manzana Platano)
In the first case, an empty array is defined with a capacity of 9 positions (indices from 0 to 8). In the second, an array without a fixed size is defined, which will grow as elements are assigned to it.
To assign a value to a specific position, the notation is used ARRAY:
Marca="Tranqui-Cola"
COCHE="Seat"
COCHE="Opel"
To read elements there is a special expansion syntax with curly braces:
${ARRAY}– element in position n.${ARRAY}– all elements of the array.${#ARRAY}– number of defined elements.${!ARRAY}– currently used indices.
For example:
Frutas=(Pera Manzana Platano)
echo ${Frutas} # Platano
echo ${Frutas} # Pera Manzana Platano
echo ${#Frutas} # 3
echo ${!Frutas} # 0 1 2
If you insert an element in a non-linear position, you can leave gaps:
Frutas=(Pera Manzana Platano)
echo "Num Elementos: ${#Frutas}" # 3
echo "Indices en uso: ${!Frutas}" # 0 1 2
Frutas="melocoton"
echo "Num Elementos: ${#Frutas}" # 4
echo "Indices en uso: ${!Frutas}" # 0 1 2 5
echo ${Frutas} # Pera Manzana Platano melocoton
To iterate through all the elements of an array, especially when there may be gaps, the most robust approach is iterate over the list of valid indexes:
for i in ${!ARRAY}
do
echo "ARRAY = ${ARRAY}"
done
You can also easily concatenate arrays:
Unix=('SCO' 'HP-UX' 'IRIX' 'XENIX')
Linux=('Debian' 'Suse' 'RedHat')
NIX=("${Unix}" "${Linux}")
echo ${NIX} # SCO HP-UX IRIX XENIX Debian Suse RedHat
Control structures: if, loops, case, and select
Bash includes the typical structures of any imperative language: conditionals, loops, and multiple selectionThe general form of if is
if
then
comandos
fi
If you want an alternative branch:
if
then
comandos_si
else
comandos_no
fi
For multiple cases:
if
then
comandos1
elif
then
comandos2
else
comandos_por_defecto
fi
the loops for admit Two flavors: List style and C style:
for var in lista
do
comandos_usando "$var"
done
for ((i=0; i<5; i++))
do
comandos_usando "$i"
done
the loops while y until They repeat as long as a condition is met or not met:
while condicion
do
comandos
done
until condicion
do
comandos
done
The structure case is ideal for discriminate according to a specific value:
case $VARIABLE in
valor1)
comandos_opcion_1
;;
valor2|valor3|valor4)
comandos_para_varios_casos
;;
valorN)
comandos_opcion_N
;;
*)
comandos_por_defecto
;;
esac
Lastly, select generates simple menus in the terminal, very useful for interactive scripts:
#!/bin/bash
PS3="Escoja una opcion: "
select Opcion in "Actualizar" "Listar" "Salir"
do
echo "Has elegido: $Opcion"
if
then
break
fi
done
To break loops, you use break (break the loop) and continue (jump to the next cyclebreak N to exit several nested loops at once.
Logical, arithmetic, and text operators
When working with if It is essential to respect the spaces: the standard condition uses brackets with spaces around:
if ||
then
...
fi
The usual arithmetic operators are:
+the amount-subtraction*multiplication/integer division%module**power++increase--decrease
Two equivalent syntaxes are used to calculate numerical expressions:
echo $((2+5)) # 7
echo $ # 3
i=1; echo $ # 2
echo $ # 9
With text strings you can do everything from getting the length to bulk replacements:
${#cadena}– chain length.${cadena:N}– substring from position N.${cadena:N:M}– substring of M characters from N.${cadena#texto}– deletes prefix if it matches.${cadena%texto}– deletes suffix if it matches.${cadena/texto1/texto2}– replaces first match.${cadena//texto1/texto2}– replaces all.${cadena/#texto1/texto2}– replacement only if it is at the beginning.${cadena/%texto1/texto2}– replacement only if it is at the end.array=(${cadena/delimitador/ })– transform a string into an array, changing the delimiter to spaces.
To compare text:
==same!=diferent>major (alphanumeric)<minor (alphanumeric)-zempty string-nnon-zero length
Specific operators are used for numbers within :
-eqsame-gtgreater than-gegreater than or equal to-ltsmaller than-leless than or equal-nediferent
And for files and directories there is a complete set of tests:
-eexists (file or directory).-sIt exists and is not empty.-dIt's a directory.-fIt's a regular file.-rhas read permission.-whas writing permission.-xis executable (or can be accessed, if it is a dir).-OThe current user is the owner.-Gbelongs to the active user group.-ntmore recent than another file.-otolder than another file.
Inlet, outlet, redirects and piping
In Bash, everything is based on three basic streams: stdin, stdout, and stderr:
stdin(descriptor 0) – standard input, usually the keyboard.stdout(descriptor 1) – standard output, usually the screen.stderr(descriptor 2) – error output, also the default screen.
These streams can be redirected to special files or devices:
/dev/null– “black hole”, everything you send it gets discarded./dev/random– generates pseudorandom numbers./dev/urandom– high-quality random numbers./dev/zero– produces null bytes (0x00)./dev/full– it always behaves like a full disk when writing.
The most common redirects are:
> Fich– sends stdout (and often stderr) to file, overwriting.>> Fich– appends to the end of an existing file.1> Fich– only stdout to the file.2> Fich– only file errors.1> F1 2> F2– separate normal and error outputs.> Fich 2>&1– sends stdout to the file and redirects stderr to where stdout goes.< Fich– reads the contents of a file as input to the command.
For example, to list a folder and save the result:
ls -1 /tmp/Carpeta1 > /tmp/lista_ficheros.txt
If you want to separate errors from results:
ls -1 /tmp/Carpeta1 1>/tmp/lista_ficheros.txt 2>/tmp/errores.txt
Or completely ignore the mistakes:
ls -1 /tmp/Carpeta1 1>/tmp/lista_ficheros.txt 2>/dev/null
And if you want nothing (not even errors) to appear on the screen:
ls -l /tmp/Carpeta1 >/dev/null 2>&1
The pipes (|) allow connect the output of one command to the input of another, building true data “pipelines”:
ls -1 | sort # lista ordenada alfabéticamente
ls -1 | sort -r # misma lista en orden inverso
Several can also be chained together:
RESOLUCION=$(xrandr | grep current | awk -F "," '{print $2}' | awk '{print $2"x"$4}')
echo $RESOLUCION
Functions in Bash and variable scope
The functions allow reuse blocks of code within the same script, improving readability and maintainability. They can be defined in two equivalent ways:
function NombreFuncion {
comandos
}
NombreFuncion () {
comandos
}
The keys { } They define the scope of the function. Important: Defining a function does not execute it.It must be explicitly called by its name at some point in the script.
Regarding the scope of variables, Bash distinguishes between:
- Global: declared outside of functions, accessible from anywhere in the script.
- Locals: created within a function with the keyword
local, only visible within it.
Illustrative example:
#!/bin/bash
function AlgoPasa {
local UNO="Texto uno"
DOS="Texto dos" # sin local => global
echo "DENTRO FUNCION -> UNO=$UNO, DOS=$DOS"
}
UNO="1" # global
DOS="2" # global
echo "ANTES FUNCION -> UNO=$UNO, DOS=$DOS"
AlgoPasa
echo "DESPUES FUNCION -> UNO=$UNO, DOS=$DOS"
You will see that The local variable UNO does not alter the global value UNO, whereas DOS is indeed modified after calling the function, as it has not been declared as local within it.
Exit codes, exit and return
Every program that ends in GNU/Linux returns a exit code0 if everything went well and a non-zero value in case of error. In Bash, this code is stored in a special variable. $? immediately after each command.
In a script, it's good practice to end with a exit N that expresses the overall result of the execution. For example:
#!/bin/bash
RUTA="$1"
ls -l "$RUTA" 2>/dev/null
CodError=$?
if ; then
echo "Todo correcto"
else
echo "Se produjo algún error"
fi
exit $CodError
The functions, for their part, they use the order return to indicate a numeric exit codeIf you need to return strings, arrays, or other structures, a global variable is usually used to store the result.
Example of a function that joins two words and a script that handles errors:
#!/bin/bash
function Une {
local palabra1=$1
local palabra2=$2
Cadena="$palabra1 $palabra2" # variable global
}
P1="$1"
P2="$2"
if ||
then
echo "Escriba 2 palabras por favor"
exit 1
else
Une "$P1" "$P2"
echo "$Cadena"
exit 0
fi
Practical examples of real Bash scripts
To read a file line by line, checking that it exists first, you could use something like this:
#!/bin/bash
FICHERO="$1"
if
then
while read LINEA
do
echo "$LINEA"
done < "$FICHERO"
exit 0
else
echo "Debe indicar un fichero válido"
exit 1
fi
A more elaborate example would be a script to record the desktop using ffmpeg and graphical dialogues with zenitywhich calculates the current resolution using a pipeline of commands such as xrandr, grep y awkIt launches the capture and offers another subsequent call to the same script to stop it and show the path of the generated video.
Another interesting case is scripts for Configure sudo in a convenient or secure wayFor example, one that adds the current user to /etc/sudoers.d requiring a password each time:
#!/bin/bash
echo "Debe proporcionar la clave de root cuando se le solicite"
echo "$USER ALL=(ALL:ALL) ALL" > /tmp/autorizado_$USER
su -c "cp /tmp/autorizado* /etc/sudoers.d/."
Or the same concept but configuring sudo without requiring a password:
#!/bin/bash
echo "Debe proporcionar la clave de root cuando se le solicite"
echo "$USER ALL = (ALL) NOPASSWD: ALL" > /tmp/autorizado_$USER
su -c "cp /tmp/autorizado* /etc/sudoers.d/."
There are even playful scripts that take advantage of tput, printf and associative arrays for draw “snow” falling on the console moving the cursor around the screen and dropping asterisks column by column, playing with the environment variable $RANDOM for the position of each flake.
Basic console commands that complement Bash
To move around the shell with ease, it is essential to know the basic system commands and its documentation, in addition to Linux tricks to get the most out of the terminalBash itself constantly relies on these external utilities.
The command man displays the manual for any standard C command or function:
man ls
man fopen
To help you navigate the file system:
pwd– displays the absolute path of the current directory.cd– changes directory; accepts absolute or relative paths.ls– lists the contents of a directory, with options such as-lfor long format or-ato include hidden files.tree– Prints the directory and file tree from a given path.
To create, copy, move, or delete files:
mkdir– creates an empty directory.touch– creates a file or updates its modification date.cp– copies files or directories.mv– Move or rename a file or folder.rm– delete files (carefully, andrm -r(for directories).
To view content:
less– paginator for viewing files interactively.cat– concatenates and displays complete files.head– displays the first lines (by default 10, or as many as you specify with-n).tail– shows the last lines; with-fIt continues live (log tailing).hexdump– Prints binary data in hexadecimal format.grep– filters lines that match a pattern.
Regarding permissions and ownership:
chmod– Change permissions (read, write, execute mode).chown– Changes the user who owns a file.
There are utilities that are especially useful when programming or debugging:
htop– Interactive process viewer, very practical for viewing CPU and RAM usage, filtering by name, and sending signals.ifconfig– displays network interfaces and IP addresses.nano– Lightweight console-based text editor, easy to edit settings.lsof– list of open files; with-iIt helps to see what process is listening on a port.loadkeys– Changes the keyboard map of the current session.
Finally, the Environment Variables They allow you to configure the behavior of the shell and many programs. You can view them with:
env
and show a specific one with:
echo $USER
To create or temporarily modify a variable in the current session:
export MI_VARIABLE='aguante sistemas operativos'
If you want it to persist on new terminals, add that line to the end of ~/.bashrc with an editor like nano and reopen the console to load.
With all these pieces—scripts, variables, arrays, flow control, redirects, functions, practical examples, and console utilities—you now have a solid foundation to move with ease in Bash and turn the terminal into a true Swiss Army knife for your daily workwhether you manage systems or are learning programming from scratch.