DEV Community

Cover image for Learn Shell scripting by building a project scaffolding CLI
PARTHIV SAIKIA
PARTHIV SAIKIA

Posted on

Learn Shell scripting by building a project scaffolding CLI

Introduction

We all know setting up a programming project is very tedious. Many programming languages and tools require some files to be present at the root of the project directory to work (e.g. package.json in typescript projects, go.mod in golang projects). As we go on increasing the number of dependencies the number of these configuration files also increases. Most of our projects follow the same folder structure so creating the same folders and files for each project is cumbersome.

So to make our life easier we will build a CLI tool to scaffold golang projects using shell scripting.

By the end of this tutorial you should be able to understand how to write shell scripts and automate repetitive tasks like scaffolding a project.

Shell scripting is valuable for developers because it automates repetitive tasks, allowing them to focus on more strategic work, which enhances productivity. Additionally, it simplifies processes like software deployment and testing, making workflows more efficient and reducing the potential for human error.

Problems in setting up a Golang project

Most golang projects follow a standard project structure which may look like this:

project-name/
├── cmd/
│   └── project-name/
│       └── main.go
├── internal/
├── .gitignore
├── go.mod
├── README.md
├── Makefile
└── LICENSE
Enter fullscreen mode Exit fullscreen mode

Creating these folders and files with same name and same content for multiple projects is really boring.

Every time you create a new project you need to run the same set of commands. e.g.

go mod init project-name
git init
mkdir internal cmd cmd/project-name
touch .gitignore
Enter fullscreen mode Exit fullscreen mode

This is not only slow but also prone to human error. Sometimes we may forget to initialize the go module or sometimes to create the .gitignore file.

To remove all of these mental overhead let's create Scaffold CLI which will create the folder structure, initialize git and create a github repository with just one command.

Scaffold CLI: An Introduction

Scaffold CLI will be used to setup a go project with the folder structure as shown above. It will prompt the user about what is the project name and whether they want to initialize git or not. If they want to initialize git Scaffold CLI will create a github repository with the same name and will make a commit to it.

Flowchart showing how Scaffold CLI work

Getting Started

Let's start building the CLI.

Prerequisites

Before we start, make sure you have:

  • Basic command line knowledge - You should be comfortable navigating directories and running commands.
  • Bash/Zsh shell - Available by default on macOS and Linux
  • Go installed - Download here if you don't have it.
  • GitHub CLI (optional) - Only needed if you want automatic GitHub repo creation. Install guide.

Quick check: Run these commands to verify your setup:

go version    # Should show Go 1.x or higher
gh --version  # Should show gh version (if using git integration)
Enter fullscreen mode Exit fullscreen mode

GitHub CLI Authentication (one-time setup):
If you plan to use the git integration feature, authenticate the GitHub CLI:

gh auth login
Enter fullscreen mode Exit fullscreen mode

Follow the prompts to authenticate via your browser. You only need to do this once.

Also change the default branch name globally as main as it is more common for new projects.

git config --global init.defaultBranch main
Enter fullscreen mode Exit fullscreen mode

Creating the file structure

Create the folder in your desired location using mkdir scaffold. Now move to the scaffold directory using cd scaffold.

We will split the script into different files based on the function. Our workflow consists of three steps:

  1. Initializing the folder.
  2. Build the folder structure.
  3. Initialize git.

So we will have 3 files namely

  1. init.sh: This will create the project directory and create files such as README.md, LICENSE, Makefile.

  2. structure_folders.sh: This will create folders such as internal/, cmd/.

  3. git.sh: This will initialize a git repository and will create and commit to a github repository.

Along with these 3 helper scripts we will have a main.sh which will take user input, validate them and then call these helper scripts.

Writing the helper scripts

Let's get started with the first helper script init.sh. The purpose of this script is to create the project folder and write some basic files.

Create the file using touch init.sh.

The first line of a shell script should always be

#!/bin/bash
Enter fullscreen mode Exit fullscreen mode

The character sequence #! is called shebang. It tells the operating system about which interpreter should it use to execute the script. In this case we are telling the OS to use the bash interpreter.

Next we need to create the project folder. We cannot use a hard coded string with the command mkdir because the project name will be given by the user as a prompt. So to get the name of the project from the user prompt we will use arguments.

# This script will take the project name as the argument. Hence ${1} is the project name.

#Create directory 
mkdir -p ${1}
echo "Created directory ${1}"
Enter fullscreen mode Exit fullscreen mode

Here ${1} represents the first argument to the script. So now we can call the script with an argument such as

./init.sh cool-go-project
Enter fullscreen mode Exit fullscreen mode

This will execute the command

mkdir -p cool-go-project
Enter fullscreen mode Exit fullscreen mode

This will create a directory named cool-go-project. We can refer to more arguments like ${2}, ${3} and so on. If we need to refer to every argument we can use ${@}.

// Change ${1} to ${@} in init.sh
mkdir -p ${@}
Enter fullscreen mode Exit fullscreen mode
./init.sh cool-go-project cooler-go-project not-so-cool-project
Enter fullscreen mode Exit fullscreen mode

Running this script will result in the execution of the command

mkdir -p cool-go-project cooler-go-project not-so-cool-project
Enter fullscreen mode Exit fullscreen mode

Hence it will create three different folders namely cool-go-project, cooler-go-project, not-so-cool-project.

P.S.: Before executing the script you will need to give it executable permissions by running chmod +x ./init.sh

After the directory is created we would want to move into that directory and initialize go module and create files such as README.md, LICENSE, etc.

The full init.sh will look like this

#!/bin/bash

# This script will take the project name as the argument. Hence ${1} is the project name.

#Create directory 
mkdir -p ${1}
echo "Created directory ${1}"

# Move into the folder
cd ${1}

# Initialise go.mod 
go mod init github.com/parthivsaikia/${1}

# Create Readme
echo "${1} is a cool golang project." > README.md

# Create License
echo "MIT License

Copyright (c) 2026 PARTHIV PRATIM SAIKIA

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the \"Software\"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE." > LICENSE


# Add Makefile

echo "
.PHONY: run test lint clean


run: 
    go run cmd/${1}/main.go

build: 
    go build -o bin/${1} cmd/${1}/main.go

test: 
    go test -v -race ./...

lint: 
    golangci-lint run

clean: 
    go clean
    rm -f bin/${1}
" > Makefile
Enter fullscreen mode Exit fullscreen mode

We move into the project directory using cd ${1} and then write into the files README.md, LICENSE, Makefile using the structure

echo "TEXT" > filename
Enter fullscreen mode Exit fullscreen mode

The > operator redirects the standard output to a file. So in this case the output of echo is redirected to the respective files.

After initializing the folder we need to create folders such as internal/, cmd/ inside the project directory. So let's write the next helper script structure_folders.sh.

touch structure_folders.sh
Enter fullscreen mode Exit fullscreen mode
#!/bin/bash

# This script takes the project name as the argument and create the folder structure

# Create necessary folders and files
mkdir -p internal cmd cmd/${1}
touch cmd/${1}/main.go
Enter fullscreen mode Exit fullscreen mode

This script will result in the following structure which is very standard for a go project.

project-name/
├── cmd/
│   └── project-name/
│       └── main.go
├── internal/
├── .gitignore
├── go.mod
├── README.md
├── Makefile
└── LICENSE
Enter fullscreen mode Exit fullscreen mode

The remaining step is to initialize git in our project directory so create a new file git.sh

touch git.sh
Enter fullscreen mode Exit fullscreen mode

This script should do the following things in order:

  1. Initialize git repository.
  2. Create a github repository with the name same as that of the project.
  3. Create a .gitignore file.
  4. Stage the changes and commit to git.
  5. Push the changes to the remote origin.

We will pass the name of the project as the argument again.

#!/bin/bash

# Script initialising git repository and creating repository in github.

git init 
gh repo create ${1} --public --source=. --remote=origin

# Add .gitignore

echo "# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib

*.test

# Code coverage profiles and other test artifacts
*.out
coverage.*
*.coverprofile
profile.cov

# Dependency directories (remove the comment below to include it)
# vendor/

# Go workspace file
go.work
go.work.sum

# env file
.env

# Editor/IDE
# .idea/
# .vscode/
# Added by goreleaser init:
dist/" > .gitignore

git add . 
git commit -m "chore: initiliase folder structure"
git push origin main
Enter fullscreen mode Exit fullscreen mode

The command gh repo create ${1} --public --source=. --remote=origin creates a public github repository in the current directory and add it as origin.

Note that to use the github-cli you need to authenticate with Github as discussed in the prerequisites section.

We are done with the helper scripts. In the main.sh script we will read user inputs and call the helper scripts. Create the file by running

touch main.sh
Enter fullscreen mode Exit fullscreen mode

This script should do the following tasks in order

  1. Ask the user the name of the project.
  2. Validate if the input is empty.
  3. If the input is empty warn the user and ask the name again.
  4. If name is non-empty run init.sh and structure_folders.sh with project name as argument.
  5. Ask user if they wants to initialize git.
  6. If no then end the process.
  7. If yes then run git.sh with the project name as argument.
#!/bin/bash

# The main script that runs the cli

printf "\033[0;31m
███████╗ ██████╗ █████╗ ███████╗███████╗ ██████╗ ██╗     ██████╗      ██████╗██╗     ██╗
██╔════╝██╔════╝██╔══██╗██╔════╝██╔════╝██╔═══██╗██║     ██╔══██╗    ██╔════╝██║     ██║
███████╗██║     ███████║█████╗  █████╗  ██║   ██║██║     ██║  ██║    ██║     ██║     ██║
╚════██║██║     ██╔══██║██╔══╝  ██╔══╝  ██║   ██║██║     ██║  ██║    ██║     ██║     ██║
███████║╚██████╗██║  ██║██║     ██║     ╚██████╔╝███████╗██████╔╝    ╚██████╗███████╗██║
╚══════╝ ╚═════╝╚═╝  ╚═╝╚═╝     ╚═╝      ╚═════╝ ╚══════╝╚═════╝      ╚═════╝╚══════╝╚═╝
                                                                                        \033[0m\n"


read -p "Enter the project name: " projectName
Enter fullscreen mode Exit fullscreen mode

To give a cool look I am adding an ascii art which will be shown when someones run Scaffold CLI. You can create your own ascii art here.

To make the ASCII art red, we use backslash escapes. Here's how it works:

  1. ESC Character
    \033 - This is the ESC character (escape) in octal

    • Signals the start of an escape sequence
  2. Color Code
    [0;31m - The actual color code

    • [ - Starts the CSI (Control Sequence Introducer)
    • 0 - Reset all attributes (bold, underline, etc.)
    • ; - Separator
    • 31 - Red foreground color
    • m - Ends the color code
  3. Your Text
    ASCII art (now in red)

  4. Reset Code
    \033[0m - Reset to default color

    • 0 - Reset all attributes
    • m - End code
    • This prevents the red from bleeding into subsequent output

Example

echo -e "\033[0;31mThis is red text\033[0m"
Enter fullscreen mode Exit fullscreen mode

The ascii art will look like this in the terminal

Image showing colored ascii art in terminal

We prompt the user to input the project name and store it in the variable projectName using the command

read -p "Enter the project name: " projectName
Enter fullscreen mode Exit fullscreen mode

Now we need to validate this input by checking if it is empty. Bash provide a default conditional to check if the length of a string is empty. The syntax is

-z string

    True if the length of string is zero.
Enter fullscreen mode Exit fullscreen mode

To check if the variable projectName is empty we can use it inside a while loop like this

while [[ -z ${projectName} ]]
do
    echo -e "\033[0;31mError: Project name cannot be empty!!\033[0m"
    read -p "Enter the project name: " projectName
done
Enter fullscreen mode Exit fullscreen mode

We refer the variable projectName by ${projectName}. Note that the spaces around the condition -z ${projectName} are not just necessary and not just decorative. Until this condition is false the user will be prompted to enter a non-empty project name.

Image showing validation error

Now we ask the user if they want to initialize git and store the result in the variable initialiseGit.

read -p "Do you want to initialise a git repo? (y or n): " initialiseGit

while [[ "${initialiseGit}" != "y" && "${initialiseGit}" != "n" ]]
do
    echo -e "\033[0;31mError: Invalid input! Please select y or n!!\033[0m"
    read -p "Do you want to initialise a git repo? (y or n): " initialiseGit
done
Enter fullscreen mode Exit fullscreen mode

We perform the same validation steps here too.

The helper scripts init.sh and structure_folders.sh needs to run always independent of the variable initialiseGit so we execute them first.

~/repos/scaffold/init.sh ${projectName}
cd ${projectName}
~/repos/scaffold/structure_folders.sh ${projectName}
Enter fullscreen mode Exit fullscreen mode

We execute the scripts init.sh and structure_folders.sh with the variable projectName as argument.

Since Scaffold CLI can be called from anywhere so we are providing the absolute location of the scripts. Change the location accordingly in your code.

We create the folder using init.sh and then move into the newly created folder using cd ${projectName}. Then we execute structure_folders.sh from inside of the project folder.

IMPORTANT: You might be confused that why are we using cd again to move into the project folder when we have already move into it in the init.sh file. This is because a script is executed in its own context, meaning that any directory changes (like cd) made within that script only apply while the script is running. Once init.sh finishes, control returns to the parent shell, reverting back to its original working directory. Therefore, if you want to ensure that your subsequent commands operate within the desired project folder, you need to issue the cd command again in your current shell. This guarantees that you are indeed in the right directory before running any further commands.

Based on the variable initialiseGit we need to execute the script git.sh.

if [[ ${initialiseGit} == "y" ]]
then
    ~/repos/scaffold/git.sh ${projectName}
fi
Enter fullscreen mode Exit fullscreen mode

This is the syntax of if statement in bash. The keyword fi marks the end of the if block. In this block we are checking if the variable initialiseGit is equal to "y" i.e. if the user prompted yes when it was asked whether they want to initialize git. If that condition is true we execute the script git.sh with the argument ${projectName}.

With this our CLI is complete. Make all scripts executable by running:

chmod +x ./init.sh
chmod +x ./structure_folders.sh
chmod +x ./git.sh
chmod +x ./main.sh
Enter fullscreen mode Exit fullscreen mode

To execute this script with just one command we need to add it as an alias in our shell config. Add this line in your shell config. Since I use zsh I will add it to ~/.zshrc. If you use bash you need to add it to ~/.bashrc.

alias scaffold="~/repos/scaffold/main.sh"
Enter fullscreen mode Exit fullscreen mode

Adjust the location of the script according to your setup.

NOTE: You can find out your shell by running.

echo $SHELL
#output: /bin/zsh
Enter fullscreen mode Exit fullscreen mode

Scaffold CLI is now ready to use. Let's create one project using it.

Demo of Scaffold CLI

Start Scaffold CLI by running scaffold. You will see the ascii art and you will be prompted to enter the project name.

Image showing Scaffold CLI asking project name

Once you give a project name you will be asked whether you want to initialize git or not.

Image showing git prompt

Once you enter "y" the project folder will be created and the changes will be pushed to github.

Image showing pushing to github

Now move to the project folder by cd test-project.

~/repos/test-project main -> tree
.
├── cmd
│   └── test-project
│       └── main.go
├── go.mod
├── internal
├── LICENSE
├── Makefile
└── README.md
Enter fullscreen mode Exit fullscreen mode

You can see that the folder structure is same with the folder structure discussed above.

You can also verify the creation of the test-project repository in Github.

Image showing test-project in github repositories

Conclusion

Shell scripting might seem intimidating at first, but as you've seen, it's incredibly powerful for automating everyday development tasks. What started as a simple idea—"I'm tired of manually setting up projects"—turned into a useful tool that saves time and reduces errors.
The beauty of shell scripting is that it's accessible. You don't need to learn a new programming language or install heavy frameworks. With just bash and some creativity, you can automate almost anything in your development workflow.
What repetitive tasks are you tired of doing manually? Challenge yourself to automate one this week. Start small, keep it simple, and iterate as you learn. Share your automation scripts in the comments—I'd love to see what you build!

Top comments (2)

Collapse
 
harishambhu_23b702345d8a6 profile image
Harishambhu

Great work

Collapse
 
nabamallika_nath_3156c18d profile image
Nabamallika Nath

Very helpful 🤩