- Bash acts as a shell and command interpreter and allows you to automate common tasks in GNU/Linux using simple scripts.
- The scripts are based on parameters, variables, arrays, control structures and functions, with intensive use of external commands.
- Redirections, pipes, and test operators on files and text allow you to build powerful tools for system administration.
- The correct use of exit codes, environment variables, and functions makes Bash scripts reusable, robust, and easy to integrate.

If you work with GNU/Linux, sooner or later you will need to manage the terminal and Bash. Mastering a good Bash manual It allows you to automate tasks, better understand the system, and stop doing everything manually as if you were in permanent "novice mode".
In this comprehensive guide you will find A complete and practical explanation of the Bash scripting languageFrom what it is and how to use it on the command line, to how to write scripts with variables, arrays, loops, functions, error handling, redirects, pipes, and real-world examples you could use on your machine tomorrow.
What is Bash and what is it really used for?
Bash (Bourne Again SHell) is a A Unix shell that functions as a command interpreterIt is the layer that sits between you and the Linux kernel: you type commands, Bash interprets them, executes them, and returns results to you on the screen.
This shell is the one that comes by default in most popular GNU/Linux distributions (such as Debian or Ubuntu) and is also available on macOS. Bash is ideal as a first programming language If you already know the basic console commands and want to automate common tasks: backups, cleaning temporary files, reports, small menus, etc.
Think of Bash as a fairly simple structured language, highly focused on systems administrationwhich takes advantage of all existing system commands, such as the awk commandInstead of reinventing the wheel, Bash scripts coordinate commands, redirects, and conditions to let the system do the work for you.
What is a Bash script and how do you prepare one?
A Bash script is nothing more than a text file with a series of instructions that run sequentially in the shell. Over time they can grow to resemble actual programs: they use loops, functions, variables, etc., but everything remains plain text.
For Linux to recognize that a file is an executable script, there are three key points: Specify the correct shell, grant execution permissions, and name it reasonably., usually with extension .sh.
Shebang: Indicate which interpreter to use
The first line of the file must specify the path to the interpreter to be used. In Bash, this is usually:
#!/bin/bash
This sequence #! is what is known as shebang (hashbang, sha-bang, etc.). When you run the file as ./script.shThe system reads that first line and knows what to launch /bin/bash to process what comes next.
If you want to debug a script line by line, you can add the option -x al shebang so that Bash displays what it is executing:
#!/bin/bash -x
Execution permissions and script name
Once the file is written, it must be marked as executable. The following is typically used:
chmod 755 nombre_del_script.sh
or, more directly:
chmod +x nombre_del_script.sh
Regarding the name, the system does not require the use of any extension, but It is highly recommended to use .sh to visually identify that it is a scriptWhen running it from the terminal, if you are in the same directory, it launches with ./nombre_del_script.sh.
Keep in mind that, within a Bash script, everything that goes behind the character # It is considered a commentexcept for the first line of the shebang. You can comment out entire lines or add comments at the end of a statement.
Your first Bash script step by step
To edit scripts in Linux you can use any text editor: from graphical editors (VSCode, Atom, etc.) to console tools like nano, vi or vimThe important thing is that you save the file in plain text without any special characters.
We are going to create a very simple script called hola.sh using, for example, vim:
vim hola.sh
Inside the file, you write something as simple as:
#!/bin/bashecho "Hola. Soy tu primer script Bash"
Here we use the command echo to display text on the screen. There is also printfwhich allows you to format the output with more control, very useful for aligning columns or formatting numbers.
You save the file (for example, in Vim using ESC followed by :wq) and then you grant it execution permissions:
chmod 755 hola.sh
You can now try it from the terminal, by navigating to the file's directory and running:
./hola.sh
You should see on screen the phrase you defined in the script, thus demonstrating that the basic flow (shebang + permissions + execution) is correctly configured and that Bash is interpreting your file correctly.
Positional parameters and shift command
Scripts are often useful when They accept parameters from the command line.Bash offers special variables to work with these positional arguments without you having to do manual parsing.
Some important parameters within the script are:
$#: number of parameters received.$0: name (and path) of the script itself.$1 ... $9: parameters from 1 to 9.${N}: parameter in position N, useful if there are more than 9.$*: all parameters (except$0) as a single string.$@: all parameters as an “array” of words.$$: PID of the process that executes the script.$?: exit code of the last executed command.
When you want to process parameters of type --opcion valor in any orderIt becomes very convenient to use the command shift, which moves the parameters to the left.
Imagine that at the beginning you have $1=UNO y $2=DOSIf you call shift, $1 It becomes worth what it was previously worth. $2 and the old value of $1 It gets lost. This way you can "consume" parameters as you navigate the command line.
Example script with shift and named parameters
A very typical pattern is to offer options such as --nombre y --apellido in any order and process them using a loop while together with case y shiftThe structure would be something like this:
#!/bin/bash
# USO: ./nombre-apellido.sh --nombre NOMBRE --apellido APELLIDO
while ]do
case "$1" in
-n|--nombre)
shift
nombre="$1"
shift
;;
-a|--apellido)
shift
apellido="$1"
shift
;;
*)
# parámetro no esperado, lo saltamos
shift
;;
esac
done
echo "Tu Nombre es: $nombre y tu Apellido es: $apellido"
When running the script as ./nombre-apellido.sh --nombre Luis --apellido Gutierrez Or by reversing the order of the parameters, the output will be the same, because The loop is responsible for assigning each value to its corresponding location. regardless of the original position.
Variables in Bash: global, local, and text
In Bash you can define variables without declaring types. They are untyped variablesThey can contain text, numbers, or even arrays, and the interpreter handles it.
The basic way to assign values is:
VAR=5
VAR=texto
VAR='cadena literal'
VAR="cadena con variables"
When you use single quotes, the content is treated as plain text; with double quotes, Bash interprets special variables and sequences. within the chain.
For example, in a script you could write:
nombre=Luis
CALLE="Calle Larga"
Despacho=401
Later, to read its value, you precede it with the symbol $ to the variable name, for example $nombreKeep in mind that Bash is case-sensitive, so CALLE y calle They are not the same variable.
Capture command output in variables
One of the most powerful things about Bash is that it allows store the result of a command in a variable, effortlessly. There are two equivalent syntaxes:
VAR=$(comando)
VAR=`comando`
For example, to save in $Usuario The current system user can use:
Usuario=$(whoami)
If you want to collect both standard output and errors, you can redirect stderr to stdout within the command substitution, so that everything remains in the same variable:
Salida=$(comando 2>&1)
It is also common to concatenate variables and text:
VarB="$V1 texto1 $V2 texto2"
Or play with positional parameters to form new strings, for example:
SORTEMARAP="$3 $2 $1"
which I would store in SORTEMARAP the first three parameters in reverse order.
Single quotes vs double quotes
Single quotes ' They block any kind of interpretation: Whatever you write inside is saved exactly as written.Therefore, if you do:
NONO='$3 $2 $1'
in the variable NONO will be literally saved $3 $2 $1not the values of those parameters. However, with double quotes, the variables are expanded.
A classic example of interpreted concatenation would be:
VarA="En un lugar"
VarB='de la Mancha'
VarC="de cuyo nombre no quiero"
VarD=acordarme
TEXTO="$VarA $VarB $VarC $VarD"
By doing echo "$TEXTO" You will get a complete sentence, proving that Variables are replaced within double quotesBut not within single quotes.
Arrays in Bash and how to handle them
Bash supports indexed arrays (and associative arrays in modern versions), which are very useful when you want working with collections of securities without creating a thousand loose variables.
They can be declared in several ways:
declare ARRAYotypeset ARRAY: create an array with space reserved for 9 elements.declare -a Colores: declares an array with no fixed size.Frutas=(Pera Manzana Platano): initializes the array with those elements.
To write to a specific index, the following syntax is used: ARRAY=valor, remembering that the indices They start at 0 and go up to n-1 if the array has n consecutive elements.
For example:
Marca="Tranqui-Cola"
COCHE="Seat"
COCHE="Opel"
A slightly different notation is used to read values, using curly braces:
${ARRAY}: element in position n.${ARRAY}: all elements of the array.${#ARRAY}: number of elements stored.${!ARRAY}: list of indexes currently in use.
A typical example to see a specific element:
Frutas=(Pera Manzana Platano)
echo ${Frutas} # Platano
And to list all the content:
echo ${Frutas} # Pera Manzana Platano
Keep in mind that it is possible leave “gaps” in an array If you only assign certain indices, the number of elements and the highest index used may not match. Hence the interest in ${!ARRAY} to scan only the existing indexes.
Traversing arrays and concatenating them
To traverse an array without encountering non-existent indices, a loop is usually used. for Regarding the list of indexes:
for i in ${!ARRAY}
do
echo "ARRAY = ${ARRAY}"
done
You can also combine the contents of two arrays into a third, for example:
Unix=('SCO' 'HP-UX' 'IRIX' 'XENIX')
Linux=('Debian' 'Suse' 'RedHat')
NIX=("${Unix}" "${Linux}")
This is how you get an array that contains all the elements of both original arraysbeing able to list later ${NIX} to see them all at once.
Control structures in Bash scripts
As with any scripting language, Bash offers conditionals, loops, and multiple selection structures that allow control of the execution flow.
The basic forms of if are:
if
then
comandos
fiif
then
comandos
else
comandos_alternativos
fiif
then
comandos
elif
then
otros_comandos
else
comandos_por_defecto
fi
It is important to respect well the spaces in the comparisons within brackets: That's correct, but without spaces Bash won't interpret it correctly.
Loops of type for They have two common variants:
- List style:
for var in lista; do ...; done - Style C:
for ((i=0; i<5; i++)); do ...; done
In addition to for, do you have while (while the condition is true) and until (it runs until the condition is met), useful for condition-controlled loop instead of counter-controlled loop.
For cases with many options you can use case:
case $VARIABLE in
valor1)
comandos_opcion1
;;
valor2|valor3|valor4)
comandos_para_varios_valores
;;
*)
comandos_por_defecto
;;
esac
And if you want a small, simple interactive menu, select It automatically generates a list of numbered options:
PS3="Escoja una opcion: "
select Opcion in "Actualizar" "Listar" "Salir"
do
echo "Ha elegido: $Opcion"
if ; then
break
fi
done
To exit loops or skip iterations you have:
break: exits the current loop.break N: exits N nested loops.continue: skips to the next loop iteration, omitting the rest of the commands in the current iteration.
Logical, arithmetic, and text operators
In conditionals you can combine expressions with logical operators such as && (AND) and || (OR), usually outside the brackets:
if ||
then
# algo
fi
For numerical calculations, Bash supports standard operators: addition +, stay -, multiplication *, division /, module %, power **, increase ++ and decrease --The usual ways to evaluate them are:
$((operacion))$(older, but still supported)
Quick examples:
echo $((2+5)) # 7
echo $ # 3
i=1; echo $ # 2
echo $ # 9
For text comparisons, operators such as the following are used:
==(equal)!=(distinct)>y<to order lexically (careful: inIt is often best to escape them)-zto check if a string is empty-nto check if the length is not zero
In numerical comparisons, specific operators are preferred:
-eq(equal)-gt(greater than)-ge(greater than or equal to)-lt(less than)-le(less than or equal to)-ne(distinct)
Bash also allows you to do operations on chains using syntax ${}. For example: uterine
${#cadena}: length.${cadena:N}: substring from position N.${cadena:N:M}: M characters starting from N.${cadena#texto}y${cadena%texto}: remove matching prefixes or suffixes.${cadena/texto1/texto2}: replace the first match.${cadena//texto1/texto2}: replace all matches.${cadena/#texto1/texto2}o${cadena/%texto1/texto2}: replace only at the beginning or at the end.
Checks with files and permissions
In administrative scripts, it's very common to want to know if a file exists, if it's a directory, if it has permissions, etc. That's what these scripts are for. specific test operators for files that are normally used within o ]:
-e: exists (file or directory).-s: exists and is not empty.-d: exists and is a directory.-f: exists and is a regular file.-r: has read permission.-w: has writing permission.-x: executable (or accessible if it is a directory).-OThe current user is the owner.-G: belongs to the current user's group.-nt: is more recent than another file.-ot: is older than another file.
To change permissions, use the command chmod (change mode)either in numeric mode (like 755) or symbolic (as +x to add execution). To change the owner user you have chownwhich is very useful in deployment and maintenance scripts, and for sync with rsync You can automate efficient transfers between systems.
Redirections, /dev/* and pipes
Every program in Linux works with three basic file descriptors: standard input (stdin, 0), standard output (stdout, 1) and error output (stderr, 2)Bash allows you to redirect any of them to special files or devices.
The most typical redirection operators are:
> fich: sends stdout (and sometimes stderr if used plainly in some shells) to a file, overwriting its contents.>> fich: adds the output to the end of the file.1> fich: redirects only stdout.2> fich: redirects only stderr.2>&1: sends stderr to where stdout goes.< fich: uses the contents of a file as the stdin of a command.
For example, to list the contents of a directory and save the result to a file, you could use:
ls -1 /tmp/Carpeta1 > /tmp/lista_ficheros.txt
If you want to differentiate between normal output and errors, you can separate each stream into a different file, which It greatly simplifies debugging and logging.:
ls -1 /tmp/Carpeta1 1>/tmp/lista_ficheros.txt 2>/tmp/errores.txt
And if you're not interested in error messages at all, you can always send them to /dev/null, the Unix “black hole”:
ls -1 /tmp/Carpeta1 1>/tmp/lista_ficheros.txt 2>/dev/null
A pattern widely used for completely silence the output of a command is
comando >/dev/null 2>&1
Regarding special devices useful in scripts, it is worth knowing:
/dev/null: discards everything that is written./dev/randomy/dev/urandom: generate pseudo-random numbers./dev/zero: produces null bytes./dev/full: always “full”, useful for testing for typos.
On the other hand, the operator | create a pipe between processespassing the output of one command as the input of the next. Simple examples would be:
ls -1 | sort # orden alfabético ascendente
ls -1 | sort -r # orden inverso
You can also chain several pipes together, for example to get the current screen resolution by combining xrandr, grep y awkChaining multiple filters together is a very powerful way to build custom tools with just a few lines of code.
Functions in Bash and variable scope
When you repeat the same logic several times in a script, the natural thing to do is encapsulate it in a function. In Bash, functions can be defined in two main ways, but In both cases, the body goes in brackets.:
function NombreFuncion { comandos; }NombreFuncion() { comandos; }
Functions are not executed automatically: they must be called by name from elsewhere in the script. Within them, the parameters they receive are accessed as $1, $2etc., just like in a script.
Regarding the scope of variables, by default any variable defined outside of a function is overall and it can be read and modified within it. If you want a variable to only exist within a function, you must declare it with local:
function AlgoPasa {
local UNO="Texto uno"
DOS="Texto dos"
echo "Dentro de la función UNO=$UNO y DOS=$DOS"
}
In this example, UNO It's local to the function and disappears upon exiting, while DOS It is global and any changes within the function are maintained afterwards, something that should be taken into account to avoid unintentionally overwriting values.
Exit and return codes in Bash
Every program in Linux eventually returns a exit codeBy convention, 0 This means that everything went well, and any value other than zero indicates some kind of error. In Bash, this information is available in the special variable $?.
Within a script, it is recommended to end with a exit num consistent with what has happened, so that other scripts or programs can act accordingly. A typical example would be:
#!/bin/bash
RUTA="$1"
ls -l "$RUTA" 2>/dev/null
CodError=$?
if ; then
echo "Todo correcto"
else
echo "Atención: se produjo algún error"
fi
exit $CodError
The functions can also indicate whether they have gone well or badly, but in this case, the following is used: return num instead of exit. return It only allows numbers, so if you need to return text, arrays, or other types of data, you will have to do so using variables (usually global).
A simple example: a function that takes two words and joins them into a global variable called CadenaThe main script checks that two parameters have been passed and, if not, returns an error.
Practical examples of Bash scripts
To ensure that all of the above doesn't remain purely theoretical, it's very useful to see real scripts that They solve everyday problems in GNU/Linux systemsBelow are several that illustrate different concepts.
Read a file line by line
A classic pattern is to go through a text file line by line to process its contents:
#!/bin/bash
FICHERO="$1"
if ; then
while read LINEA
do
echo "$LINEA"
done < "$FICHERO"
exit 0
else
echo "Debe indicar un fichero existente"
exit 1
fi
This type of structure allows you to apply commands to each line, filter data, generate reports, etc., using a loop while read fed by input redirection.
Record your desktop screen to an .avi file
With tools like ffmpeg, xrandr y zenity (For graphical dialog boxes) a script can be set up that starts and stops recording the main desktop, detecting the resolution and number of monitors:
#!/bin/bash
function Inicia {
FICH=$(tempfile --suffix=.avi)
NUMMONITORES=$(xrandr | grep '*' | wc -l)
RESOLUCION=$(xrandr | grep current | awk -F "," '{print $2}' | awk -v NP=$NUMMONITORES '{print $2/NP"x"$4}')
ffmpeg -y -f x11grab -s "$RESOLUCION" -r 25 -i :0.0 "$FICH" -loglevel quiet &
}
function Finaliza {
killall -9 ffmpeg
FICH=$(ls -1tr /tmp/*avi | tail -1)
echo "Vídeo guardado en: $FICH"
}
function Ejecuta {
EJECUTANDOSE=$(ps aux | grep -i ffmpeg | grep -v grep | wc -l | awk '{print $1}')
if ; then
Finaliza
else
Inicia
fi
}
Ejecuta
The combined use of functions, global variables, pipes, and external commandsall orchestrated from Bash.
Scripts to configure sudo securely or conveniently
In some environments, it may be useful to automate the inclusion of a user in the configuration of sudoWith Bash you can generate the file in /etc/sudoers.d/ appropriate and copy it using su:
#!/bin/bash
# Activar sudo pidiendo contraseña en cada uso
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/."
And for a "convenient" mode, in which no password is required when using sudoIt would be enough to change the line written to the temporary file:
echo "$USER ALL=(ALL) NOPASSWD: ALL" > /tmp/autorizado_$USER
These types of scripts demonstrate how powerful and dangerous Bash can be when it touches sensitive parts of the system, so it's best to use them with knowledge.
"Snow" effect in the terminal with tput and associative arrays
Beyond serious tasks, Bash also lends itself to visual elements in the terminal. Using tput To move the cursor and colors, plus associative arrays to track the position of each snowflake, you can simulate a snowfall:
#!/bin/bash
LINEAS=$(tput lines)
COLUMNAS=$(tput cols)
declare -A CopoDeNieve
declare -A UltimosCopos
clear
mover_copo() {
i="$1"
if }" ] || }" = "$LINEAS" ]; then
CopoDeNieve=0
else
if }" ]; then
printf "\033}" "$i"
fi
fi
printf "\033}" "$i"
UltimosCopos=${CopoDeNieve}
CopoDeNieve=$((${CopoDeNieve}+1))
}
while :
do
i=$(($RANDOM % $COLUMNAS))
mover_copo "$i"
for x in "${!UltimosCopos}"; do
mover_copo "$x"
done
sleep 0.1
done
This script combines associative arrays, infinite loop, environment variables such as $RANDOM and ANSI sequences to manipulate the cursor, all coordinated from pure Bash.
Bash on the command line: useful basic commands
In addition to the scripting language, you'll constantly use the interactive shell on a daily basis. Some basic commands that are worth mastering are: pwd, cd, ls, tree, mkdir, touch, cp, mv, rm, less, cat, head, tail, hexdump and grep.
For example, with pwd You see the full path of the current directory, with cd You change folders, and with ls lists content. The command tree It displays a recursive tree of directories and files, very convenient for getting an overall view of a project.
To create new things you use mkdir (directories) and touch (files). cp copy files, mv moves or renames them, and rm He deletes them. Be very careful with rm -r y rm -rfbecause they can delete entire directories with no way back.
To view files, less It's very convenient because it allows you to navigate with the keyboard, while cat simply dump all the contents. head y tail They show the beginning and end of a file respectively, and accept the option -n to indicate how many lines you want to see.
If you work with binaries or memory structures, hexdump It allows you to view the content in hexadecimal. To search for text within files, grep It's the star tooland integrates beautifully with pipes: for example, cat *.c | grep sleep It will show you all the lines where it appears sleep in your C sources. To review system logs it is also useful to know journalctl.
Environment variables and .bashrc file
Environment variables are values that They are loaded into your shell session and affect the behavior of programs and scripts.You can see them with env o printenvand are displayed in format NOMBRE=valor.
To define a temporary environment variable, simply do the following:
export MI_VARIABLE='aguante sistemas operativos'
While that session (and its daughter shells) lasts, you will be able to access $MI_VARIABLEBut if you close the terminal, it disappears. To have it load automatically in each new interactive shell, you can add the export corresponding to the end of your file ~/.bashrc and open a new terminal.
The typical flow would be: go to the home screen with cd ~, edit .bashrc to nano .bashrc or similar, add something like at the end export MI_VARIABLE='aguante sisop'Save and close. From then on, every time you open a new console, That variable will be available without doing anything else..
Overall, mastering Bash as both a shell and a scripting language gives you very fine control over any GNU/Linux system: you can automate everything from the simplest tasks to complex deployment processes, manage logs, monitor services, build small menus for other users, and ultimately, make the machine work for you instead of repeating commands by hand over and over again.
Passionate writer about the world of bytes and technology in general. I love sharing my knowledge through writing, and that's what I'll do on this blog, show you all the most interesting things about gadgets, software, hardware, tech trends, and more. My goal is to help you navigate the digital world in a simple and entertaining way.