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
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
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.
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)
GitHub CLI Authentication (one-time setup):
If you plan to use the git integration feature, authenticate the GitHub CLI:
gh auth login
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
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:
- Initializing the folder.
- Build the folder structure.
- Initialize git.
So we will have 3 files namely
init.sh: This will create the project directory and create files such asREADME.md,LICENSE,Makefile.structure_folders.sh: This will create folders such asinternal/,cmd/.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
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}"
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
This will execute the command
mkdir -p cool-go-project
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 ${@}
./init.sh cool-go-project cooler-go-project not-so-cool-project
Running this script will result in the execution of the command
mkdir -p cool-go-project cooler-go-project not-so-cool-project
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
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
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
#!/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
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
The remaining step is to initialize git in our project directory so create a new file git.sh
touch git.sh
This script should do the following things in order:
- Initialize git repository.
- Create a github repository with the name same as that of the project.
- Create a
.gitignorefile. - Stage the changes and commit to git.
- 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
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
This script should do the following tasks in order
- Ask the user the name of the project.
- Validate if the input is empty.
- If the input is empty warn the user and ask the name again.
- If name is non-empty run
init.shandstructure_folders.shwith project name as argument. - Ask user if they wants to initialize git.
- If no then end the process.
- If yes then run
git.shwith 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
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:
-
ESC Character
\033- This is the ESC character (escape) in octal- Signals the start of an escape sequence
-
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
-
Your Text
ASCII art (now in red)-
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"
The ascii art will look like this in the 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
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.
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
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.
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
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}
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
cdagain to move into the project folder when we have already move into it in theinit.shfile. 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
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
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"
Adjust the location of the script according to your setup.
NOTE: You can find out your shell by running.
echo $SHELL
#output: /bin/zsh
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.
Once you give a project name you will be asked whether you want to initialize git or not.
Once you enter "y" the project folder will be created and the changes will be pushed 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
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.
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)
Great work
Very helpful 🤩