Build a Project Scaffolding Script

Write a bash script that auto-creates project folders, files, and git repos.

Introduction

30 Second Summary

Every time you start a new coding project, you probably create the same folders, the same starter files, and run the same setup commands. What if you could do all of that in seconds with a single script?

In this project, you will build a command-line tool using Bash that automatically scaffolds new coding projects. Your script asks for a project name and type, then generates a full project structure with directories, starter files, and an initialized Git repository.

What You'll Build

A Bash script called scaffold.sh that automates project setup by creating type-specific directory structures, generating README and .gitignore files, and initializing a git repository.

By the end of this project, you'll have:

  • A working Bash script that creates project folders, starter files, and a git repo from a single command.
  • Hands-on experience with core bash concepts: variables, conditionals, loops, and functions.
  • A reusable automation tool you can customize and extend for your own coding workflow.
  • Secret Mission: Add a "favorites" feature that saves your most-used project type to a config file and offers it as a default next time you run the script.

Are there any prerequisites?

This project assumes you already have Git for Windows (which includes Git Bash) and VS Code installed. No other tools or prior scripting experience needed.

Before We Start

Before We Start...

Set Up Your Scripting Environment

Before you start building an automation script, you need a workspace and a script file to write in.

In this step, you'll open Git Bash, create a project folder, and set up your first Bash script file.

In this step, get ready to:
  • Open Git Bash and confirm it works.
  • Create your project folder and script file.
  • Write your first line of bash and run it.
Open Git Bash

Git Bash is a terminal that comes bundled with Git for Windows. It gives you a Linux-style command line right on your Windows machine.

  • Press the Windows key to open your search bar.
  • Type Git Bash and press Enter to open it.

A dark terminal window appears. This is where you'll run all your commands for this project.

  • Verify Bash is ready by running this command:
bash --version

✔️ I see a version number

You're all set. You should see something like GNU bash, version 5.3. The exact number doesn't matter as long as it shows a version.

ⓧ I don't see Git Bash in my search results

Git Bash is included with Git for Windows. If you can't find it, you need to install Git for Windows first.

  • Download Git for Windows from gitforwindows.org.
  • Run the installer and keep all the default settings.
  • Once installed, search for Git Bash in the Start menu again.
Create your project folder and script file

You need a dedicated folder to keep your script organized. You'll create it in your home directory, which Git Bash maps to your Windows user folder.

  • Create your project folder, move into it, and create your script file by running these commands:
mkdir -p ~/bash-projects
cd ~/bash-projects
touch scaffold.sh

What do these commands do?

  • mkdir -p ~/bash-projects creates a new folder called bash-projects in your home directory. The -p flag means it won't throw an error if the folder already exists.
  • cd ~/bash-projects moves you into that folder so it becomes your current working location.
  • touch scaffold.sh creates an empty file called scaffold.sh. This is the file you'll write your entire script in.
  • Confirm the file was created by running:
ls

You should see scaffold.sh in the output.

Don't see scaffold.sh?

Make sure you ran all three commands in order. If you closed and reopened Git Bash between commands, run cd ~/bash-projects first to get back to the right folder, then try ls again.

Help me figure out why scaffold.sh isn't showing up after running my commands.

  • Open this folder in VS Code by running:
code .

VS Code opens with your bash-projects folder loaded in the Explorer sidebar. You should see scaffold.sh listed there.

VS Code didn't open?

The code command works when VS Code is added to your system PATH. This usually happens automatically during VS Code installation.

If it didn't work, open VS Code manually instead. Click File in the top menu bar, then Open Folder, and navigate to your bash-projects folder inside your Windows user directory.

Help me get VS Code to open from Git Bash.

Add the shebang line and test your script

Every Bash script starts with a special first line that tells the system which program should run the file. This line is called a shebang.

  • In VS Code, click on scaffold.sh in the Explorer sidebar to open it.
  • Add the following code to the file:
#!/bin/bash

What is a shebang line?

The #! characters at the start are called a "shebang" (or "hashbang"). They tell the system to use /bin/bash to interpret this file.

Every bash script you write should start with this line. Without it, the system might try to run your script with the wrong interpreter.

  • Save scaffold.sh.
  • Switch back to Git Bash and run your script with this command:
bash scaffold.sh

Nothing visible happens. Your script only has the shebang line, so it runs and exits silently with no errors.

Seeing an error?

Check that the first line of scaffold.sh is exactly #!/bin/bash with no spaces before the #. Also make sure you saved the file in VS Code before running the command.

If you see No such file or directory, make sure you're in the right folder by running cd ~/bash-projects first.

Help me fix the error I'm seeing when running my script.

✔️ Awesome, I've got everything!

Make sure you've saved scaffold.sh before moving on.

ⓧ I'd like to double check the full code

Here's what your scaffold.sh file should look like at this point:

#!/bin/bash

Your scripting environment is ready and your first script runs without errors. Next, you'll make it interactive by adding variables and user input.

Add Variables and User Input

Your scripting environment is set up, and scaffold.sh is ready with a shebang line. Time to make this script do something useful.

Every automation tool needs to talk to the person running it. In this step, you'll use two key Bash commands: echo to display messages and read to capture user input into variables.

In this step, get ready to:
  • Display a welcome banner when the script starts.
  • Capture a project name and project type from the user.
  • Confirm the collected values back in the terminal.
Add a greeting and project name prompt

Your script should announce itself when it runs, then ask the user for a project name. The read -p command is a convenient way to show a prompt and capture the user's response in a single line.

  • In VS Code, open scaffold.sh.
  • Add the following code below the #!/bin/bash line:
# ===========================
# Project Scaffolding Script
# ===========================

# --- Main Script ---

echo "=============================="
echo "  Project Scaffolding Tool"
echo "=============================="
echo ""

read -p "Enter your project name: " PROJECT_NAME

What does this code do?

  • Lines starting with # are comments. Bash ignores them when running the script, but they help you organize your code.
  • The echo statements print a text banner to the terminal so the user knows which tool is running.
  • read -p "Enter your project name: " PROJECT_NAME displays the prompt text and waits for input. Whatever the user types gets stored in a variable called PROJECT_NAME. Variables store values you can reference later in the script.
  • Save scaffold.sh.

You won't see any output yet. The next chunk adds the second prompt and a confirmation message.

Ask for the project type and confirm both values

Your script also needs to know what kind of project the user wants. After collecting both inputs, you'll use the $ prefix to reference variable values inside an echo statement.

  • Still in scaffold.sh, add the following code below the read -p line you just added:
read -p "Choose a project type (web/python/general): " PROJECT_TYPE

echo ""
echo "Creating project: $PROJECT_NAME (type: $PROJECT_TYPE)"
echo ""

What does this code do?

  • The second read -p stores the user's choice in a variable called PROJECT_TYPE.
  • The $ symbol before a variable name tells Bash to substitute the stored value. So $PROJECT_NAME gets replaced with whatever the user typed for the first prompt.
  • The confirmation line lets the user verify their input before the script does any real work.
  • Save scaffold.sh.

Your script is complete for this step. The next substep runs it to test everything.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

ⓧ I'd like to double check the full code

Here is the full scaffold.sh file so far. Make sure your code matches this exactly.

#!/bin/bash

# ===========================
# Project Scaffolding Script
# ===========================

# --- Main Script ---

echo "=============================="
echo "  Project Scaffolding Tool"
echo "=============================="
echo ""

read -p "Enter your project name: " PROJECT_NAME

read -p "Choose a project type (web/python/general): " PROJECT_TYPE

echo ""
echo "Creating project: $PROJECT_NAME (type: $PROJECT_TYPE)"
echo ""
Run and test your script

Time to see your script in action. When you run it, the script asks you two questions and then displays your answers back.

  • In Git Bash, run your script by entering this command:
bash scaffold.sh

What should I see?

The script displays a banner, then pauses with the prompt Enter your project name:. Type any name (like my-app) and press Enter.

Next, it asks Choose a project type (web/python/general):. Type web and press Enter. The script then prints a confirmation line like: Creating project: my-app (type: web).

  • Confirm that your project name and type both appear correctly in the confirmation output.

Seeing an error or unexpected output?

  • Check that every echo and read line has matching opening and closing double quotes.
  • If you see $PROJECT_NAME printed literally instead of your input, make sure you used double quotes (") around the echo string, not single quotes ('). Bash only substitutes variables inside double quotes.
  • Make sure you're running the command from the ~/bash-projects directory where scaffold.sh lives.

Why isn't my bash script displaying the correct output?

Your script can now greet users and collect their input. Next up, you'll use that input to create project directories with conditionals.

Create Project Directories with Conditionals

Your script collects a project name and type from the user. Time to put those inputs to work.

In this step, you'll add real decision-making to your script. It will check the project type and create a different set of folders depending on the answer.

In this step, get ready to:
  • Validate user input to catch empty project names.
  • Create project directories using conditional logic.
  • Handle unknown project types with error messages.
Validate input and create project directories

What happens if someone runs the script and presses Enter without typing a project name? The mkdir command would behave unpredictably. A quick validation check prevents this.

  • In scaffold.sh, find this line:
read -p "Enter your project name: " PROJECT_NAME
  • Add the following code directly below it, before the second read -p line:
if [ -z "$PROJECT_NAME" ]; then
    echo "Error: Project name cannot be empty."
    exit 1
fi

What does this code do?

  • [ -z "$PROJECT_NAME" ] tests whether the variable is empty (a zero-length string).
  • If the variable is empty, the script prints an error message and stops with exit 1.
  • An exit code of 1 signals an error. An exit code of 0 means success.
  • Save scaffold.sh.
  • Run your script to test the validation:
bash scaffold.sh
  • When prompted for a project name, press Enter without typing anything.

You should see Error: Project name cannot be empty. and the script should stop immediately.

Script didn't stop?

  • Check that [ -z "$PROJECT_NAME" ] has spaces inside the square brackets. Writing [-z without a space causes a syntax error.
  • Make sure the if block is placed between the two read -p lines, not after both of them.
  • Help me debug my empty name validation.

Different project types need different folder structures. A web project needs css, js, and images folders. A Python project needs src and tests. Conditionals let your script handle all of these in one go.

  • Add the following code at the end of scaffold.sh, below the last echo "" line:
# Create root directory
mkdir -p "$PROJECT_NAME"

# Create type-specific subdirectories
if [ "$PROJECT_TYPE" = "web" ]; then
    mkdir -p "$PROJECT_NAME/css" "$PROJECT_NAME/js" "$PROJECT_NAME/images"
elif [ "$PROJECT_TYPE" = "python" ]; then
    mkdir -p "$PROJECT_NAME/src" "$PROJECT_NAME/tests"
elif [ "$PROJECT_TYPE" = "general" ]; then
    mkdir -p "$PROJECT_NAME/src" "$PROJECT_NAME/docs"
fi

What does this code do?

  • mkdir -p "$PROJECT_NAME" creates the root project folder. The -p flag means it won't throw an error if the folder already exists.
  • The if/elif block checks the value of PROJECT_TYPE and runs a different mkdir -p command for each match.
  • [ "$PROJECT_TYPE" = "web" ] compares the variable to the string "web". The spaces inside the square brackets are required.
  • Save scaffold.sh.
  • Run the script by running this command:
bash scaffold.sh
  • When prompted, enter test-project as the project name.
  • Enter web as the project type.
  • Verify the folder structure was created by running:
ls -R test-project

You should see css, images, and js listed as subdirectories.

Seeing 'No such file or directory'?

  • Make sure you typed test-project in the ls -R command exactly as you entered it when running the script.
  • Check that $PROJECT_NAME in your mkdir -p lines matches the variable name from your read -p line. Variable names are case-sensitive.
  • Help me debug why my project directories aren't being created.
Handle unknown project types

Your if/elif block handles three known types. But what if someone enters node or rust? The script continues silently without creating any subdirectories. An else branch catches any input that doesn't match.

  • In scaffold.sh, find the end of your if/elif block. Look for these lines:
elif [ "$PROJECT_TYPE" = "general" ]; then
    mkdir -p "$PROJECT_NAME/src" "$PROJECT_NAME/docs"
fi
  • Replace the fi line with the following code:
else
    echo "Error: Unknown project type '$PROJECT_TYPE'. Use web, python, or general."
    exit 1
fi

echo "Directories created."

What does this code do?

  • The else branch runs when none of the if or elif conditions match. It prints the unrecognized type back to the user so they know what went wrong.
  • exit 1 stops the script immediately, just like the empty name check you added earlier.
  • The echo "Directories created." line sits after the entire if/elif/else block. It only runs when the script reaches it successfully.
  • Save scaffold.sh.
  • Clean up the test project from earlier by running:
rm -rf test-project
  • Run the script again by running:
bash scaffold.sh
  • When prompted, enter test-project as the project name.
  • Enter node as the project type.

You should see Error: Unknown project type 'node'. Use web, python, or general.

Not seeing the error message?

  • Make sure else is on its own line, directly above the echo and exit 1 lines.
  • Check that you didn't accidentally delete the fi line when adding the else branch. The fi must still close the block.
  • Help me debug my else branch in scaffold.sh.

✔️ Awesome, I've got everything!

Make sure you've saved scaffold.sh before moving on.

ⓧ I'd like to double check the full code

#!/bin/bash

# ===========================
# Project Scaffolding Script
# ===========================

# --- Main Script ---

echo "=============================="
echo "  Project Scaffolding Tool"
echo "=============================="
echo ""

read -p "Enter your project name: " PROJECT_NAME

if [ -z "$PROJECT_NAME" ]; then
    echo "Error: Project name cannot be empty."
    exit 1
fi

read -p "Choose a project type (web/python/general): " PROJECT_TYPE

echo ""
echo "Creating project: $PROJECT_NAME (type: $PROJECT_TYPE)"
echo ""

# Create root directory
mkdir -p "$PROJECT_NAME"

# Create type-specific subdirectories
if [ "$PROJECT_TYPE" = "web" ]; then
    mkdir -p "$PROJECT_NAME/css" "$PROJECT_NAME/js" "$PROJECT_NAME/images"
elif [ "$PROJECT_TYPE" = "python" ]; then
    mkdir -p "$PROJECT_NAME/src" "$PROJECT_NAME/tests"
elif [ "$PROJECT_TYPE" = "general" ]; then
    mkdir -p "$PROJECT_NAME/src" "$PROJECT_NAME/docs"
else
    echo "Error: Unknown project type '$PROJECT_TYPE'. Use web, python, or general."
    exit 1
fi

echo "Directories created."
Test your script with each project type

Time for a full test run. You'll try each project type to make sure the right folder structure appears.

  • Clean up any leftover test directories by running:
rm -rf test-project
  • Run the script by running:
bash scaffold.sh
  • Enter test-project as the project name.
  • Enter python as the project type.
  • Verify the structure by running:
ls -R test-project

You should see src and tests listed as subdirectories.

  • Clean up and test the last type by running:
rm -rf test-project
bash scaffold.sh
  • Enter test-project as the project name.
  • Enter general as the project type.
  • Verify the structure by running:
ls -R test-project

You should see src and docs listed as subdirectories.

  • Clean up the test directory by running:
rm -rf test-project

Why clean up between runs?

The mkdir -p flag silently succeeds if a folder already exists. Removing the test project between runs ensures you see a fresh folder structure each time, not leftover directories from a previous test.

Your script creates a full directory structure with a single command. Next, you'll add functions that generate starter files like README.md and .gitignore with project-specific content.

Generate Starter Files with Functions

Your script creates the right folder structure for each project type. But empty folders are only half the story.

Every coding project needs at least a README.md and a .gitignore file. In this step, you'll write bash functions that generate these files automatically with the right content for each project type.

In this step, get ready to:
  • Define a create_readme function that writes a README.md using a heredoc.
  • Define a create_gitignore function that writes type-specific .gitignore content using a case statement.
  • Call both functions and verify the generated files.
Define the create_readme function

Functions in bash let you group reusable logic under a name. You define them once, then call them as many times as you need with different arguments.

Why define functions before the main script?

Bash reads scripts from top to bottom. A function must be defined before the line that calls it. That is why all function definitions go in a # --- Functions --- section above your # --- Main Script --- section.

  • In VS Code, open scaffold.sh and find the line that says # --- Main Script ---.
  • Add the following code on a new line directly above # --- Main Script ---:
# --- Functions ---

create_readme() {
    local name="$1"
    local type="$2"
    cat << EOF > "$name/README.md"
# $name

A **$type** project scaffolded with scaffold.sh.

## Getting Started

Add your project description and setup instructions here.
EOF
}

What does this code do?

  • create_readme() defines a function. The parentheses tell bash this is a function definition.
  • local name="$1" captures the first argument passed to the function. The local keyword keeps the variable scoped to the function so it does not leak into the rest of your script.
  • cat << EOF > "$name/README.md" starts a heredoc. Everything between this line and the closing EOF gets written into a README.md file inside the project directory.
  • The closing EOF must sit on its own line with no spaces before it. This tells bash the heredoc content is finished.
  • Save scaffold.sh.
  • You won't see a change yet. The function only runs when you call it, which you'll do in the final substep.
Define the create_gitignore function

Different project types need different .gitignore rules. A web project should ignore node_modules/, while a Python project should ignore __pycache__/. A case statement is perfect for branching on a single value with multiple options.

  • In scaffold.sh, add the following code directly below the closing } of the create_readme function (still above # --- Main Script ---):
create_gitignore() {
    local type="$1"
    local dir="$2"
    case "$type" in
        web)
            cat << EOF > "$dir/.gitignore"
node_modules/
dist/
.env
EOF
            ;;
        python)
            cat << EOF > "$dir/.gitignore"
__pycache__/
*.pyc
.venv/
*.egg-info/
EOF
            ;;
        *)
            cat << EOF > "$dir/.gitignore"
# Add files and directories to ignore
*.log
*.tmp
EOF
            ;;
    esac
}

What does this code do?

  • case "$type" in starts a case statement that checks the value of $type against multiple patterns.
  • Each pattern (like web)) runs its own heredoc to write different .gitignore contents. The ;; at the end of each branch tells bash to stop checking further patterns.
  • The *) wildcard pattern is a catch-all. It handles any project type that is not web or python, writing a generic .gitignore instead.
  • esac closes the case statement. It is "case" spelled backwards.

How is a case statement different from if/elif?

Your script already uses if/elif/else to check the project type for directory creation. A case statement does the same kind of branching, but it is cleaner when you are matching one variable against several fixed values.

Both work here. You are using both in this project so you get practice with each.

  • Save scaffold.sh.
  • You still won't see a change yet. The next substep calls both functions and verifies the output.
Call both functions and verify the generated files

The functions are defined but never called. You need to add the function calls into your main script so they actually run when you execute scaffold.sh.

  • In scaffold.sh, scroll down to the bottom of the file and find the line that says echo "Directories created.".
  • Add the following code below that line:
# Generate starter files
create_readme "$PROJECT_NAME" "$PROJECT_TYPE"
create_gitignore "$PROJECT_TYPE" "$PROJECT_NAME"
echo "Starter files generated."

What does this code do?

  • create_readme "$PROJECT_NAME" "$PROJECT_TYPE" calls the function with the project name as the first argument and type as the second.
  • create_gitignore "$PROJECT_TYPE" "$PROJECT_NAME" calls the second function. Notice the argument order is different here: type is first, directory name is second. This matches how each function's local variables are set up.
  • Save scaffold.sh.
  • If you have any leftover test projects from the previous step, clean them up by running:
ls
  • If you see any test project folders listed (alongside scaffold.sh), remove them by running:
rm -rf [[OLD_PROJECT="your-old-project-folder-name"]]
  • Run your script by running:
bash scaffold.sh
  • Enter a project name when prompted (for example, test-project).
  • Enter web as the project type.

You should see "Directories created." followed by "Starter files generated." in the terminal output.

  • Verify the generated README.md by running:
cat [[PROJECT_NAME="your-project-name"]]/README.md

You should see your project name as a heading and the project type in the description.

  • Verify the generated .gitignore by running:
cat [[PROJECT_NAME="your-project-name"]]/.gitignore

Since you chose web, the .gitignore should list node_modules/, dist/, and .env.

Seeing an error when you run the script?

  • If you see syntax error near unexpected token, check that each heredoc's closing EOF is on its own line with no spaces or tabs before it. Even one leading space will cause this error.
  • If you see command not found for create_readme or create_gitignore, make sure your function definitions are above the # --- Main Script --- line. Bash reads top to bottom, so functions must be defined before they are called.
  • If the .gitignore file is empty or missing, check that your case statement ends with esac and each branch ends with ;;.
  • Still stuck? Help me debug my scaffold.sh script. I'm getting an error when running it after adding the create_readme and create_gitignore functions.

✔️ Awesome, I've got everything!

Double check that you have saved scaffold.sh before moving on.

ⓧ I'd like to double check the full code

Here is the full scaffold.sh file as it should look at the end of this step.

#!/bin/bash

# ===========================
# Project Scaffolding Script
# ===========================

# --- Functions ---

create_readme() {
    local name="$1"
    local type="$2"
    cat << EOF > "$name/README.md"
# $name

A **$type** project scaffolded with scaffold.sh.

## Getting Started

Add your project description and setup instructions here.
EOF
}

create_gitignore() {
    local type="$1"
    local dir="$2"
    case "$type" in
        web)
            cat << EOF > "$dir/.gitignore"
node_modules/
dist/
.env
EOF
            ;;
        python)
            cat << EOF > "$dir/.gitignore"
__pycache__/
*.pyc
.venv/
*.egg-info/
EOF
            ;;
        *)
            cat << EOF > "$dir/.gitignore"
# Add files and directories to ignore
*.log
*.tmp
EOF
            ;;
    esac
}

# --- Main Script ---

echo "=============================="
echo "  Project Scaffolding Tool"
echo "=============================="
echo ""

read -p "Enter your project name: " PROJECT_NAME

if [ -z "$PROJECT_NAME" ]; then
    echo "Error: Project name cannot be empty."
    exit 1
fi

read -p "Choose a project type (web/python/general): " PROJECT_TYPE

echo ""
echo "Creating project: $PROJECT_NAME (type: $PROJECT_TYPE)"
echo ""

# Create root directory
mkdir -p "$PROJECT_NAME"

# Create type-specific subdirectories
if [ "$PROJECT_TYPE" = "web" ]; then
    mkdir -p "$PROJECT_NAME/css" "$PROJECT_NAME/js" "$PROJECT_NAME/images"
elif [ "$PROJECT_TYPE" = "python" ]; then
    mkdir -p "$PROJECT_NAME/src" "$PROJECT_NAME/tests"
elif [ "$PROJECT_TYPE" = "general" ]; then
    mkdir -p "$PROJECT_NAME/src" "$PROJECT_NAME/docs"
else
    echo "Error: Unknown project type '$PROJECT_TYPE'. Use web, python, or general."
    exit 1
fi

echo "Directories created."

# Generate starter files
create_readme "$PROJECT_NAME" "$PROJECT_TYPE"
create_gitignore "$PROJECT_TYPE" "$PROJECT_NAME"
echo "Starter files generated."
  • Clean up your test project before moving on by running:
rm -rf [[PROJECT_NAME="your-project-name"]]

Your script now generates real, useful starter files for every project. Next up, you'll add a loop to handle empty subdirectories and initialize a Git repository with an initial commit.

Add a Loop and Git Initialization

Your script can already build the right directory structure and generate starter files tailored to each project type.

Now it's time to add the finishing touches. You'll write a for loop that prepares empty directories for Git tracking, automate git initialization so every new project starts version-controlled, and add a colored success message to wrap it all together.

In this step, get ready to:
  • Define a function that uses a for loop to add .gitkeep files to empty directories.
  • Define a function that initializes a git repository with an initial commit.
  • Run the complete script and verify everything works end to end.
Define the placeholder files function

Git tracks files, not empty directories. If a subdirectory like css/ has nothing inside it, git will silently ignore it.

The convention is to place a tiny file called .gitkeep inside each empty directory. Your next function will automate this with a for loop.

  • In VS Code, open scaffold.sh and find the closing } of the create_gitignore function.
  • Add the following function below it, above the # --- Main Script --- comment:
create_placeholder_files() {
    local project_dir="$1"
    # Loop through every subdirectory in the project folder
    for dir in "$project_dir"/*/; do
        # Check the match is actually a directory
        if [ -d "$dir" ]; then
            touch "$dir/.gitkeep"
        fi
    done
}

What does this code do?

  • local project_dir="$1" stores the first argument passed to the function (the project directory path).
  • The for loop uses the glob pattern */ to iterate over every subdirectory inside the project folder.
  • if [ -d "$dir" ] confirms the match is actually a directory. If no subdirectories exist, the glob pattern returns itself as a literal string, so this check prevents errors.
  • touch "$dir/.gitkeep" creates an empty file in each subdirectory so git will track it.
  • Save scaffold.sh.
  • You won't see a change yet. The next function will use these .gitkeep files when it initializes the git repository.
Define the git initialization function

Every project benefits from version control from the very beginning. This function will handle the full git setup: initializing a repository, staging all the generated files, and creating the first commit.

  • In scaffold.sh, add the following function directly below the create_placeholder_files function you just added, still above the # --- Main Script --- comment:
init_git() {
    local project_dir="$1"
    # Move into the project directory
    cd "$project_dir" || exit 1
    # Set up a fresh git repo
    git init
    git add .
    git commit -m "Initial commit: project scaffolded"
    # Return to the original directory
    cd ..
}

What does this code do?

  • cd "$project_dir" || exit 1 moves into the project directory. The || exit 1 part is a safety net. If the cd fails for any reason, the script stops instead of running git commands in the wrong folder.
  • git init creates a new git repository. git add . stages every file in the directory.
  • git commit -m "..." creates the initial commit with a descriptive message.
  • cd .. returns to the original directory so the rest of the script runs from the right place.

Now you need to call both new functions from the main script so they actually run.

  • Scroll down to the main script section. Find this line:
echo "Starter files generated."
  • Add the following lines directly below it:
# Add .gitkeep to empty subdirectories
create_placeholder_files "$PROJECT_NAME"

# Initialize git repo
init_git "$PROJECT_NAME"

What do these calls do?

These two lines pass the $PROJECT_NAME variable to each function. First, create_placeholder_files drops a .gitkeep into every subdirectory. Then init_git sets up the repository and commits everything.

  • Save scaffold.sh.

Seeing a syntax error when you save?

  • Make sure both new functions (create_placeholder_files and init_git) are above the # --- Main Script --- comment. Bash reads top to bottom, so functions must be defined before they are called.
  • Check that each function has a matching opening { and closing }. A missing brace will cause unexpected errors.

Help me debug a syntax error in my bash script

Complete the script and run it

Your script does everything it needs to, but it ends silently. A good CLI tool gives clear feedback when it finishes.

Bash supports ANSI color codes that let you print colored text to the terminal. You'll use green text for a success message so the result is impossible to miss.

  • In scaffold.sh, find the line init_git "$PROJECT_NAME" you just added. Add the following lines below it:
echo ""
echo -e "\033[0;32mSuccess! Project '$PROJECT_NAME' is ready.\033[0m"
echo ""
echo "Your project structure:"
ls -R "$PROJECT_NAME"

What does this code do?

  • echo -e enables interpretation of escape sequences. Without the -e flag, the color codes would print as raw text.
  • \033[0;32m is an ANSI escape code that switches the text color to green. \033[0m resets the color back to normal so everything after the message looks correct.
  • ls -R "$PROJECT_NAME" recursively lists every file and directory inside the project, giving the user a complete view of what was created.
  • Save scaffold.sh.

Time to run the complete script from start to finish.

  • If you have any leftover test project folders from earlier steps, clean them up by running:
rm -rf test-project
  • Run the complete script by running:
bash scaffold.sh

What should I enter?

When the script prompts you, enter test-project for the project name and web for the project type. You should see directory creation messages, a green "Success!" line, and a listing of the full project structure.

  • Confirm the git repository was created by running:
ls -la test-project/.git

You should see a list of git internals like HEAD, config, objects, and refs. This confirms git initialized successfully.

  • Verify the initial commit exists by running:
git -C test-project log --oneline

You should see a single line with a commit hash followed by Initial commit: project scaffolded.

Not seeing the git repo or commit?

  • If ls -la test-project/.git says "No such file or directory", check that your init_git function is being called in the main script. Look for init_git "$PROJECT_NAME" below the echo "Starter files generated." line.
  • If git log says "does not have any commits yet", make sure git add . comes before git commit inside the init_git function. Files must be staged before they can be committed.
  • If the green success message shows raw codes like \033[0;32m instead of colored text, make sure you used echo -e (with the -e flag) and not just echo.

Help me debug my scaffold script

✔️ Awesome, I've got everything!

Double check you've saved scaffold.sh and you're ready to move on.

ⓧ I'd like to double check the full code

Here is the complete scaffold.sh file. Compare it carefully with yours.

#!/bin/bash

# ===========================
# Project Scaffolding Script
# ===========================

# --- Functions ---

create_readme() {
    local name="$1"
    local type="$2"
    cat << EOF > "$name/README.md"
# $name

A **$type** project scaffolded with scaffold.sh.

## Getting Started

Add your project description and setup instructions here.
EOF
}

create_gitignore() {
    local type="$1"
    local dir="$2"
    case "$type" in
        web)
            cat << EOF > "$dir/.gitignore"
node_modules/
dist/
.env
EOF
            ;;
        python)
            cat << EOF > "$dir/.gitignore"
__pycache__/
*.pyc
.venv/
*.egg-info/
EOF
            ;;
        *)
            cat << EOF > "$dir/.gitignore"
# Add files and directories to ignore
*.log
*.tmp
EOF
            ;;
    esac
}

create_placeholder_files() {
    local project_dir="$1"
    for dir in "$project_dir"/*/; do
        if [ -d "$dir" ]; then
            touch "$dir/.gitkeep"
        fi
    done
}

init_git() {
    local project_dir="$1"
    cd "$project_dir" || exit 1
    git init
    git add .
    git commit -m "Initial commit: project scaffolded"
    cd ..
}

# --- Main Script ---

echo "=============================="
echo "  Project Scaffolding Tool"
echo "=============================="
echo ""

read -p "Enter your project name: " PROJECT_NAME

if [ -z "$PROJECT_NAME" ]; then
    echo "Error: Project name cannot be empty."
    exit 1
fi

read -p "Choose a project type (web/python/general): " PROJECT_TYPE

echo ""
echo "Creating project: $PROJECT_NAME (type: $PROJECT_TYPE)"
echo ""

# Create root directory
mkdir -p "$PROJECT_NAME"

# Create type-specific subdirectories
if [ "$PROJECT_TYPE" = "web" ]; then
    mkdir -p "$PROJECT_NAME/css" "$PROJECT_NAME/js" "$PROJECT_NAME/images"
elif [ "$PROJECT_TYPE" = "python" ]; then
    mkdir -p "$PROJECT_NAME/src" "$PROJECT_NAME/tests"
elif [ "$PROJECT_TYPE" = "general" ]; then
    mkdir -p "$PROJECT_NAME/src" "$PROJECT_NAME/docs"
else
    echo "Error: Unknown project type '$PROJECT_TYPE'. Use web, python, or general."
    exit 1
fi

echo "Directories created."

# Generate starter files
create_readme "$PROJECT_NAME" "$PROJECT_TYPE"
create_gitignore "$PROJECT_TYPE" "$PROJECT_NAME"
echo "Starter files generated."

# Add .gitkeep to empty subdirectories
create_placeholder_files "$PROJECT_NAME"

# Initialize git repo
init_git "$PROJECT_NAME"

echo ""
echo -e "\033[0;32mSuccess! Project '$PROJECT_NAME' is ready.\033[0m"
echo ""
echo "Your project structure:"
ls -R "$PROJECT_NAME"

Your script is now a fully functional automation tool. Every time you run it, you get a complete project structure with version control set up in seconds. If you're feeling adventurous, the Secret Mission awaits with a challenge to make your script even smarter.

Secret mission

Add a Favorites Feature

Your script works great, but you have to type the same project type every time you run it. Build a favorites system that remembers your last-used project type and offers it as a default on the next run.

Before you go

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so there are no ongoing costs.

Resources you used:

  • The ~/bash-projects directory containing your scaffold.sh script.
  • Any scaffolded project folders you created while testing (e.g. my-project, test-app).
  • Git repositories initialized inside each scaffolded project.
  • The ~/.scaffold_config file (if you completed the Secret Mission).

Keep everything running

No action needed. Choose this if you want to keep customizing your script or use it to scaffold new projects.

  • Your scaffold.sh script stays in ~/bash-projects ready to use anytime.
  • Any scaffolded test projects remain on disk for reference.
  • Everything is local, so nothing is costing you money.

Pause - I'll come back to this later

Clean up the test projects you created while building the script. Keep the script itself so you can pick up where you left off.

  • Delete any scaffolded test project folders by running this command in Git Bash from your ~/bash-projects directory:
cd ~/bash-projects
rm -rf my-project test-app
  • Replace my-project test-app with the actual names of the folders you created during testing.
  • Keep your scaffold.sh file. It is ready for you when you come back.

Delete - I don't want to use this again

Remove all project resources and start fresh. This deletes everything you built in this project.

  • Delete the entire bash-projects folder by running this command in Git Bash:
rm -rf ~/bash-projects
  • If you completed the Secret Mission, also delete the config file:
rm -f ~/.scaffold_config
  • Confirm everything is gone by running:
ls ~/bash-projects
  • You should see a "No such file or directory" message. That means everything has been removed.

Mission accomplished

That's a Wrap!

Nice work! You just built a real command-line automation tool from scratch using Bash. Your scaffold.sh script takes a project from zero to ready-to-code in seconds.

You've learned how to:

  • Built a bash script that captures user input, uses conditionals and functions, and scaffolds project directories automatically.
  • Generated starter files like README.md and .gitignore using heredocs, with content tailored to different project types.
  • Automated Git initialization with an initial commit inside each scaffolded project using a for loop and reusable functions.
  • Added a persistent favorites feature that saves your preferred project type to a config file across runs.

Ready to quiz yourself?