Automatización con Shell Scripts

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 claveelif 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 [email protected]: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 [email protected]: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.