Basics of Bash

Bash/Shell scripting is one of the most vital skills that a developer needs to understand. Let’s take a quick historical tour of Bash scripting

Pre-Shell Era
  • Unix Shell: In the early days of Unix (circa 1971–1979), the first shell was the Thompson shell, written by Ken Thompson.

  • It was soon replaced by the Bourne Shell (sh), written by Stephen Bourne in 1979. This shell became the standard for Unix systems.

  • Introduction to control structures like if , for, while

The Rise of Shell Variants (1980)
  • C Shell & Korn Shell came with an introduction to C-like Syntax

  • Each shell had its strengths, but fragmentation became an issue: different systems used different default shells.

Birth of Bash (1989)
  • Bash stands for Bourne Again SHell.

  • Developed by Brian Fox for the GNU (GNU's Not Unix) Project.

  • It incorporated all the features from csh, ksh, `sh

I believe in Project-Based Learning. So, let’s make a Shell Script for creating a Node.js Backend using Express.js

# function with params
greeting () {
  if [[ -n $1 ]]; then
    echo "Hello, $1!"
  else
    echo "Hello, unknown!"
  fi
  return 0
}

greeting Denys  # Hello, Denys!
greeting        # Hello, unknown!

Below is a function that takes a name and returns 0, indicating successful execution. This function takes an input and echoes it into the console.

what is -n?

This reads the input, but does not execute it.

what is $1?

In Bash, when you run a script with arguments like this:

./myscript.sh hello world

Inside the script, these are available as positional parameters:

  • $0 → script name (./myscript.sh)

  • $1"hello"

  • $2"world"

  • etc.

Short NameOption NameDescription
-fnoglobDisable filename expansion (globbing).
-iinteractiveScript runs in interactive mode.
-nnoexecRead commands, but don't execute them (syntax check).
(none)pipefailMake pipelines fail if any command fails, not just if the final one fails.
-t(—)Exit after the first command.
-vverbosePrint each command to stderr before executing it.
-xxtracePrint each command and its expanded arguments to stderr before executing.

Project Implementation: We will create a function to generate the project’s file structure and install all the necessary packages required for the development

# create file-structure
generate_express_app() {
}

generate_expess_app $1 $2

$1, $2 They are placeholders for arguments for the project name and package manager to be used when creating the project. Now, every time before making a project, we need to validate if we are provided with the correct arguments. We achieve this by using an if-else block

# if-else
if [[ condition ]]; then
    command
else
    command
fi

In our case, the code would look like this

if [[ -n $1 && -n $2 ]]; then
    # script for project setup
else
    echo "Usage: generate_express_app <project-name> <npm|yarn|pnpm>"
fi

Now, let’s start the scripting

  • We can create variables and refer to them like this
PROJECT_NAME=$1
PKG_MGR=$2

echo "$PROJECT_NAME" # projects the values stored in PROJECT_NAME

We have slightly different commands for installing packages and running scripts in npm, yarn and pnpm. We handle this using this piece of code.

mkdir "$PROJECT_NAME" && cd "$PROJECT_NAME" || exit
if [[ $PKG_MGR = "npm" ]]; then
      $PKG_MGR init -y
      $PKG_MGR install express cors morgan http-errors express-session
      $PKG_MGR install --save-dev typescript ts-node nodemon prettier dotenv @types/express @types/node @types/cors @types/morgan @types/express-session
else
      $PKG_MGR init
      $PKG_MGR add express cors morgan http-errors express-session
      $PKG_MGR add -D typescript ts-node nodemon prettier dotenv @types/express @types/node @types/cors @types/morgan @types/express-session
fi

In this block of code, we first create the project directory and then, according to the package manager, we initialise the project and install the necessary packages.

Now we add all the commands that will be needed to start the projects

  • npm tsc --init

  • mkdir src && touch src/index.ts , etc...

Now we will have to create files and add some code into them, for example, we need to setup the tsconfig.json in our TypeScript projects. So we can use cat the command with EOF

cat > tsconfig.json <<EOF                                                   
{
  "compilerOptions": {                                                              
    "target": "es2016",
    "module": "node16",
    "moduleResolution": "node16",
    "rootDir": "./src",
    "outDir": "./dist",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true                                                          
  }
}
EOF

The EOF Command is one of the most interesting commands as it considers all the characters inside <<EOF ..... EOF a string and replaces all the text in the files with exact indentation. We can use this command to popular code into their respective locations. Another Example Use Case

cat > .env <<EOF
PORT="8080"
SESSION_SECRET="your-secret"
FRONTEND_URL="http://localhost:3000"
DATABASE_URL=""
EOF
cp .env .env.example

Now, how would we setup our .gitignore files for the projects?

We can use the above-mentioned method, but what if we use curl a command in this case?

We make a get request to an API Endpoint, which will return all the necessary gitignore config for the project

curl -sL https://www.toptal.com/developers/gitignore/api/node -o .gitignore

In this case, we use the word famous Toptal gitignore to get the gitignore file for node-based projects.

Note : This is the gist Link for the final outcome for this project

0
Subscribe to my newsletter

Read articles from Abhinab Choudhury directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Abhinab Choudhury
Abhinab Choudhury