Sergio Xalambrí
February 23, 2019

# Automatización con Shell Scripts

Aprende a usar Shell Scripts para automatizar tareas, todo con un ejemplo práctico para montar un entorno de trabajo para un proyecto en Node.js

Hace unos días tuve la necesidad de crear un script para automatizar el proceso de instalación de un proyecto y me tocó aprender Shell Scripting. Acá las cosas que aprendía creándolo.

> Nota: Esto está basado en un script que tuve que hacer para un proyecto en el que estoy trabajando.

## Creando el archivo

Lo primero es crear el archivo, vamos a llamarlo
`setup.sh`. Lo podemos colocar en cualquier carpeta, por ejemplo en `~/` haciendo `touch ~/setup.sh`.

## Condiciones

Luego vamos a ir agregando el código. Vamos a hacer que nuestro script tenga dos funcionalidades, `install` y `run`, para esto necesitamos una simple condición.

    if [ "$1" = "install" ]
    then
      # Install the app
    elif [ "$1" = "run" ]
    then
      # Run the app
    else
      # Throw an error
    fi

Si vemos estamos usando algo llamado `$1`, eso es una variable, en este caso hacer referencia al primer argumento que le pasemos a nuestro script al ejecutarlo. Por ejemplo, si para ejecutarlo hacemos esto:

    bash ~/setup.sh install

Entonces `install` es el valor de `$1`.

Lo siguiente que vemos es como hacen las condiciones, la sintaxis es:

    if [ condición ]
    then
      # algo
    elif [ otra condición]
    then
      # otra cosa
    else
      # algo más
    fi

La condición se inicia con `if`. Después del `if` va la primer condición entre corchetes y en la siguiente línea un `then`, luego de este va el código a ejecutar si se pasa la condición, para hacer un `else if` se usa la palabra clave`elif` seguida por la condición entre corchetes y el `then`, igual que un `if` normal. Para el último caso hacemos `else` sin necesidad de poner un `then`. Por último ponemos un `fi` que sirve para terminar el bloque de condiciones.

## Funciones

Vamos a crear algunas funciones para ordenar nuestro código. Creemos una función que nos permita mostrarle texto al usuario de distintos colores dependiendo de que ocurrió, vamos a tener entonces `throw`, `warn` y `print` que va a mostrar texto en rojo, amarillo y verde, respectivamente.

    NC='\033[0m'

    throw()
    {
      COLOR='\033[0;31m'
      >&2 echo -e ${COLOR}$1${NC}
    }

    print()
    {
      COLOR='\033[0;32m'
      echo -e ${COLOR}$1${NC}
    }

    warn()
    {
      COLOR='\033[0;33m'
      echo -e ${COLOR}$1${NC}
    }

La forma de definir una función es simplemente poner el nombre de la función seguida por `()`. Luego creamos un bloque de código usando llaves y dentro va el código de la función. En nuestro caso son funciones más o menos similares, en todas creamos una variable llamada `COLOR` con su valor, `'\033[0;31m'` significa rojo, `'\033[0;32m'` significa verde y `'\033[0;33m'` significa amarillo, la variable `NC` que creamos antes de las funciones significa No Color y la vamos a usar para hacer que el texto deje de tener color.

Lo siguiente que hacemos es hacer un `echo` para mostrar texto en pantalla, le pasamos el flag `-e` para soportar `\`, esto es necesario para usar colores, y le pasamos como valor a mostrar en pantalla `${COLOR}$1${NC}` ¿Qué significa esto? Primero ponemos el contenido de nuestra variable de color, después colocamos `$1` que, si recordás, es el primer argumento que le pasamos a nuestro script, resulta que dentro de una función es el primer argumento que le pasemos a la función, en nuestro caso el texto, y por último ponemos `${NC}` que que no tenga color otra vez y no se quede el resto del texto en rojo, verde o amarillo.

Hay un caso diferente al resto que es la función `throw`, antes del `echo` se agrega `>&2`. Eso significa que el contenido del `echo` lo debe pasar a `stderr` en vez de `stdout` (lo normal). Esto es para que si algún programa usa nuestro script va a poder identificar los `throw` como errores correctamente.

Ahora podríamos empezar a usar estas funciones, por ejemplo agreguemos que en nuestro `else` se muestre un mensaje de error en rojo.

    NC='\033[0m'

    throw()
    {
      COLOR='\033[0;31m'
      >&2 echo -e ${COLOR}$1${NC}
    }

    print()
    {
      COLOR='\033[0;32m'
      echo -e ${COLOR}$1${NC}
    }

    warn()
    {
      COLOR='\033[0;33m'
      echo -e ${COLOR}$1${NC}
    }

    if [ "$1" = "install" ]
    then
      # Install the app
    elif [ "$1" = "run" ]
    then
      # Run the app
    else
    throw "Invalid command, available values are 'install' or 'run'"
    fi

Con esto ya vamos avanzando. Ahora hagamos la instalación.

## Función de instalación

Creemos una función llamada `install` que nos va a instalar nuestra aplicación. El proceso para instalar debe cubrir todo, la idea es que podamos correr este script en una computadora nueva y nos deje el entorno listo.

Para esto vamos a necesitar realizar varios pasos:

- Instalar Homebrew
- Instalar Git
- Crear una clave SSH
- Agregarla a GitHub
- Instalar Yarn y Node.js
- Clonar desde GitHub nuestro repositorio
- Instalar dependencias

En código esto sería algo así

    confirm_ssh()
    {
      read -p "Have you added the SSH Key? [y/N] " CONFIRM_SSH
      if [[ $CONFIRM_SSH -ne "y" || $CONFIRM_SSH -ne "Y" ]]
      then
        warn "Please add it to continue."
        confirm_ssh
      fi
    }

    install()
    {
      /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
      print "Homebrew installed"

      brew install git
      print "Git installed"

      warn "Please, enter your GitHub email address and name"
      read -p "Email address: " EMAIL
      read -p "Name: " NAME
      git config --global user.name "$NAME"
      git config --global user.email "$EMAIL"
      echo "Generating SSH Key"
      ssh-keygen -t rsa -b 4096 -C "$EMAIL"
      eval "$(ssh-agent -s)"
      ssh-add -K ~/.ssh/id_rsa
      pbcopy < ~/.ssh/id_rsa.pub
      warn "We have copied your new SSH Key to your clipboard, please add it to your GitHub account going to https://github.com/settings/keys"
      sleep 2.5
      open "https://github.com/settings/keys"
      confirm_ssh
      print "Git & GitHub configured"

      brew install yarn
      print "Yarn & Node.js installed"

      git clone git@github.com:sergiodxa/personal-site.git ~/website
      print "Repository clonned"

      cd ~/website && npm install ; cd -
      print "Dependencies installed"

      print "Project succesfully installed, you could run it with 'bash ~/setup.sh run'"
    }

Veamos que hacen estas funciones `install` y `confirm_ssh`.

Empecemos por `install`, lo primero que hacemos es instalar Homebrew, al terminar mostramos un mensaje diciendo que fue instalado. Después usamo Homebrew para instalar Git, otra vez le avisamos al usuario que fue instalado.

Le advertimos al usuario que vamos a necesitar su nombre y dirección de email y usamos `read` para pedirle que ingrese estos valores, el flag `-p` nos permite pasar un texto para mostrarse a la izquierda de donde el usuario va a ingresar su nombre y email, por último a `read` le pasamos el nombre de la variable donde queremos guardar lo que el usuario escriba.

Una vez tenemos `$NAME` y `$EMAIL` procedemos a configurar Git para que use estos valores y generamos una nueva clave SSH cuyo valor copiamos al portapapeles usando `pbcopy < ~/.ssh/id_rsa.pub`. Le advertimos al usuario que copiamos su clave SSH al portapapeles y que tiene que agregarla a su cuenta de GitHub, esperamos dos segundos y medio y abrimos en el navegador la URL donde se agrega la clave SSH, esta espera es para dar tiempo a leer.

Después de esto llamamos a la función `confirm_ssh`. Esta función muy simple usa `read` como ya vimos para preguntarle al usuario si ya agregó la clave SSH, si el usuario no escribe `y` o `Y` entonces se muestra el mensaje la advertencia "Please add it to continue" y se vuelve a llamar a la función `confirm_shh` recursivamente, de esta forma mientras el usuario no escriba `y` o `Y` no va a pasar de `confirm_ssh`.

Luego de todo esto instalamos Yarn usando Homebrew y este además instala Node.js, lo que nos da un dos por uno. Clonamos el repositorio de nuestro proyecto a una carpeta ya conocida, también podríamos usar `read` para preguntarle al usuario a que carpeta clonar el repositorio.

Después de esto ejecutamos `cd ~/website && yarn ; cd -`, esto lo que hace es movernos a la carpeta donde clonamos nuestro proyecto, ejecutar `yarn` y al terminar de instalar las dependencias volver a la carpeta donde estábamos originalmente.

Por último se muestra un mensaje indicando como ejecutar el proyecto.

## Función de ejecución

Ahora que ya instalamos todo, vamos a crear la función para iniciar nuestro proyecto.

    run()
    {
      print "Running project"
      cd ~/website && yarn dev ; cd -
    }

Eso es todo, mostramos un mensaje diciendo que vamos a correr el proyecto, nos movemos a la carpeta donde clonamos el repositorio y ejecutamos `yarn dev` para iniciarlo, este script `dev` lo uso en mi sitio personal para iniciarlo en modo desarrollo. Al terminar va a volver a la carpeta inicial.

## Poniendo todo junto

Ahora juntemos todo, de forma ordenada y ejecutemos nuestras nuevas funciones en las condiciones correspondientes.

    NC='\033[0m'

    throw()
    {
      COLOR='\033[0;31m'
      >&2 echo -e ${COLOR}$1${NC}
    }

    print()
    {
      COLOR='\033[0;32m'
      echo -e ${COLOR}$1${NC}
    }

    warn()
    {
      COLOR='\033[0;33m'
      echo -e ${COLOR}$1${NC}
    }

    run()
    {
      print "Running project"
      cd ~/website && yarn dev ; cd -
    }

    confirm_ssh()
    {
      read -p "Have you added the SSH Key? [y/N] " CONFIRM_SSH
      if [[ $CONFIRM_SSH -ne "y" || $CONFIRM_SSH -ne "Y" ]]
      then
        warn "Please add it to continue."
        confirm_ssh
      fi
    }

    install()
    {
      /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
      print "Homebrew installed"

      brew install git
      print "Git installed"

      warn "Please, enter your GitHub email address and name"
      read -p "Email address: " EMAIL
      read -p "Name: " NAME
      git config --global user.name "$NAME"
      git config --global user.email "$EMAIL"
      echo "Generating SSH Key"
      ssh-keygen -t rsa -b 4096 -C "$EMAIL"
      eval "$(ssh-agent -s)"
      ssh-add -K ~/.ssh/id_rsa
      pbcopy < ~/.ssh/id_rsa.pub
      warn "We have copied your new SSH Key to your clipboard, please add it to your GitHub account going to https://github.com/settings/keys"
      sleep 2.5
      open "https://github.com/settings/keys"
      confirm_ssh
      print "Git & GitHub configured"

      brew install yarn
      print "Yarn & Node.js installed"

      git clone git@github.com:sergiodxa/personal-site.git ~/website
      print "Repository clonned"

      cd ~/website && npm install ; cd -
      print "Dependencies installed"

      print "Project succesfully installed, you could run it with 'bash ~/setup.sh run'"
    }

    if [ "$1" = "install" ]
    then
      install
    elif [ "$1" = "run" ]
    then
      run
    else
      throw "Invalid command, available values are 'install' or 'run'"
    fi

Eso es todo, si copias el contenido de este archivo a `~/setup.sh` y lo ejecutamos con `bash ~/setup.sh install` o `bash ~/setup.sh run` vamos a tener todo listo.

Adicionalmente podríamos llamar la función `run` al final de la instalación para ya tener todo listo.

## Palabras finales

Al principio usar Shell Scripting fue algo raro ya que nunca había tenido la necesidad de usarlo antes, pero es bastante simple y divertido y estoy pensando que otras cosas se podrían automatizar mediante scripts de Shell para hacer tareas de forma más sencilla.