Skip to content

Version Control: Shell Commands, Git, and GitHub Reviews

Step 10 of 45

Command-Line Navigation, File Search, and Shell Basics

Introduction

Command-line navigation and file search support everyday Git and front-end work. This first version-control lesson aligns with Meta’s "Version Control" course. Git can be used through a terminal, editor, or graphical client; here you practise the command line workflow. Professional front-end work depends on it. You install packages with npm, start a development server with npm run dev, run Jest tests, and drive Git, and all of these happen in a terminal. The terminal is not a relic. It is scriptable and widely used, but shells, command versions, and operating systems differ. Learn the common model and check the options available on your machine.

By the end of this lesson you will be able to move around a project, create and inspect files and folders, read and search text, chain commands together, and understand how the shell finds programs and handles permissions. The practice scenario is original: you are joining the Harbor Workshops team and must set up the project folder, find where a bug is mentioned, and prepare the repository for the Git lessons that follow.

Learning objectives

  1. Explain the roles of the terminal, the shell, and the file system, and read a path correctly.
  2. Navigate with pwd, ls, and cd, using absolute paths, relative paths, ., .., and ~.
  3. Create, copy, move, and delete files and folders with mkdir, touch, cp, mv, and rm, and know which operations cannot be undone.
  4. Read and search files with cat, less, head, tail, and grep.
  5. Combine commands with pipes and redirection.
  6. Describe environment variables, PATH, and file permissions at the level needed to diagnose common errors.
  7. Use tab completion, history, and --help or man to work faster and learn commands on your own.
  8. Prepare a project folder with a .gitignore, README.md, and package.json ready for version control.

1. Terminal, shell, and file system

Three terms are often mixed up.

  • The terminal (or terminal emulator) is the window that displays text and takes keyboard input.
  • The shell is the program running inside the terminal that reads your commands and runs them. Common shells are bash, zsh (the default on current macOS), and PowerShell on Windows. Windows users can also use Git Bash or the Windows Subsystem for Linux (WSL), which give a bash-style shell. This lesson uses Bash-style commands, most of which also work in zsh. PowerShell has different syntax and option conventions; do not paste these examples into it unchanged. GNU-specific options are identified where relevant.
  • The file system is the tree of folders and files on disk.

A command has a name, optional flags (also called options), and arguments:

Bash
ls -l src
# ls    -> command
# -l    -> flag (long listing)
# src   -> argument (what to list)

Short flags use one dash (-l) and can often be combined (-la). Long flags use two dashes (--all). The prompt (often ending in $ or %) tells you the shell is waiting for a command.

Paths

Everything on disk has a path. An absolute path starts at the root of the file system (/ on macOS and Linux) and works from anywhere:

Text
/Users/amira/projects/harbor-workshops/src/App.jsx

A relative path starts from your current working directory:

Text
src/App.jsx

Path components and shell shortcuts express common locations:

Symbol Meaning Example
. The current directory ./script.sh
.. The parent directory cd ..
~ Your home directory cd ~/projects
/ The root (when leading) or a separator /usr/bin

Case sensitivity is a property of the file system and volume configuration. Linux file systems commonly distinguish App.jsx from app.jsx; macOS and Windows volumes often do not, although case-sensitive configurations exist. Match the exact filename case so imports remain portable.

2. Moving around

Three commands cover most navigation.

Bash
pwd                     # print working directory
# /Users/amira

ls                      # list the current folder
# Desktop  Documents  projects

ls -la projects         # long format, including hidden files
cd projects             # change directory
cd harbor-workshops
pwd
# /Users/amira/projects/harbor-workshops

cd ..                   # up one level
cd -                    # back to the previous directory
cd                      # with no argument: go home

Files whose names start with a dot, such as .git, .gitignore, and .env, are hidden by default. ls -a (all) shows them. In a normal Git clone, .git is the directory holding local repository data. In a linked worktree it can instead be a file pointing to that data.

A name containing spaces must be quoted or escaped, otherwise the shell treats it as two arguments:

Bash
cd "My Projects"        # quoted
cd My\ Projects         # escaped

The simplest habit is to avoid spaces in project folder names and use kebab-case instead (harbor-workshops).

Speed tools

  • Tab completion: type the first letters of a name and press Tab. The shell completes it or lists the possibilities. It saves typing and prevents typos.
  • History: press the Up arrow to recall previous commands, or Ctrl+R to search them.
  • Clear the screen: clear or Ctrl+L.
  • Stop a running command: Ctrl+C. This is how you stop a development server.

3. Creating, copying, moving, and deleting

Bash
mkdir harbor-workshops             # create a folder
cd harbor-workshops
mkdir -p src/components            # -p creates parent folders as needed
touch README.md src/App.jsx        # create empty files (or update timestamps)
cp README.md README.backup.md      # copy a file
cp -r src src-copy                 # copy a folder (-r = recursive)
mv README.backup.md docs.md        # rename (move within the same folder)
mv docs.md src/                    # move into a folder
rm src/docs.md                     # delete a file
rm -r src-copy                     # delete a folder and its contents

Two cautions deserve emphasis.

  1. rm has no recycle bin. A deleted file is gone, and it is not in the system trash. rm -rf requests recursive deletion, suppresses confirmation, and ignores missing targets; it does not bypass filesystem permissions. A typo such as rm -rf ~ /tmp (note the space) can be catastrophic. Before deleting, run ls on the same path to confirm what you are about to remove. A version previously committed to Git can normally be restored from available history; merely tracking a file does not preserve every uncommitted edit. Untracked files have no automatic Git recovery.
  2. mv and cp can overwrite. Whether they prompt also depends on options, aliases, permissions, and the destination. For ordinary files, mv a.txt b.txt can replace an existing b.txt. Use -i (interactive) to be asked first: cp -i, mv -i, rm -i.

mv is also the rename command. Some systems provide a separate rename utility, but its availability and syntax vary; mv is the common command used here.

4. Reading and searching text

Bash
cat package.json                    # print a whole file
less src/App.jsx                    # scroll a long file (q to quit, / to search)
head -n 5 src/App.jsx               # first five lines
tail -n 20 server.log               # last twenty lines
tail -f server.log                  # follow new lines as they are written
wc -l src/App.jsx                   # count lines

grep searches text for a pattern. It is the tool you will use most often to answer "where is this used?".

Bash
grep "bookSeats" src/App.jsx            # lines containing bookSeats in one file
grep -r "bookSeats" src                 # search recursively through a folder
grep -rn "bookSeats" src                # -n adds line numbers
grep -ri "capacity" src                 # -i ignores case
grep -rl "TODO" src                     # -l lists only file names
grep -rn --exclude-dir=node_modules "TODO" . # GNU grep option; check other versions

Always exclude node_modules (or run the search inside src): it contains thousands of files and drowns real results.

find searches for files by name rather than content:

Bash
find src -name "*.test.js"              # every Jest test file
find . -type d -name node_modules -prune -o -type f -name "*.jsx" -print

In unquoted ls src/*.jsx, the shell expands the wildcard. In find ... -name "*.jsx", quoting passes the pattern unchanged for find to interpret. The -prune example avoids descending into node_modules, rather than merely filtering its results afterward. No-match behavior of unquoted globs differs between Bash and zsh.

5. Pipes and redirection

Each program has three standard streams: input (stdin), output (stdout), and error (stderr). The shell lets you connect them.

  • > writes output to a file, overwriting it.
  • >> appends output to a file.
  • < reads input from a file.
  • | (the pipe) sends one command's output to the next command's input.
  • 2> redirects error output.
Bash
ls src > files.txt                      # save the listing (overwrites files.txt)
echo "node_modules" >> .gitignore       # append a line
grep -rn "TODO" src | wc -l             # how many matching lines?
find docs -type f -name "*.md"          # find Markdown filenames without parsing ls
npm test 2> errors.log                  # redirect stderr; stdout remains on screen
npm run build > build.log 2>&1          # both streams into one file

Pipes are why small commands are so powerful: each does one job, and you combine them. The shell also lets you chain commands with && (run the next only if the previous succeeded) and ; (run the next regardless):

Bash
npm run build && npm run preview        # preview only if the build succeeded

Read $? immediately after a command to inspect its exit status. Zero normally means success; a nonzero value needs interpretation. For example, grep normally returns 1 for no matches, not an operational error.

By default a pipeline reports its final command’s status, so grep ... | wc -l can hide a search failure behind a successful count. Bash’s pipefail option changes this behavior, but also exposes normal no-match statuses; it is not a substitute for understanding each command. wc -l counts newline characters, not occurrences of a word.

Continue with browser and npm development workflow for related practice.

6. Environment, PATH, and permissions

Environment variables

An environment variable is a named value available to programs started from the shell.

Bash
printf '%s\n' "$HOME"                   # /Users/amira
printf '%s\n' "$SHELL"                  # configured login shell, not proof of the current shell
export API_URL="http://localhost:4000"  # set for this session and child programs
printenv API_URL

Front-end tools read variables for configuration. Vite exposes variables prefixed with VITE_ by default through import.meta.env; those user-defined values are strings and are normally replaced at build time. The prefix is configurable. Values can come from the environment and mode-specific .env files; never put secrets in anything that ships to the browser, because the browser bundle is public. Keep files containing private values out of Git, including local or mode-specific variants. A tracked .env.example should contain placeholders or public defaults, never private values.

PATH

When you type node, the shell does not search the whole disk. After shell syntax, aliases, functions, and builtins are considered, an external-command lookup searches the directories in PATH in order. Shells can cache the resolved executable path.

Bash
printf '%s\n' "$PATH"
# /usr/local/bin:/usr/bin:/bin:/Users/amira/.nvm/...
command -v node                         # how this shell resolves the name
node --version

The error command not found: npm almost always means the program is not installed or its folder is not in PATH. command -v node and inspecting PATH are useful diagnostics. An external which implementation may not describe shell functions or aliases accurately. Version managers such as nvm work by changing PATH so that the chosen Node version is found first.

Permissions

On macOS and Linux each file has permissions for the owner, group, and others: read (r), write (w), and execute (x).

Bash
ls -l deploy.sh
# -rw-r--r--  1 amira  staff  210 Oct 16 09:00 deploy.sh

Here the owner can read and write, everyone else can only read, and nobody can execute. Running ./deploy.sh produces Permission denied. Assuming the script has a valid interpreter line, its parent directories are searchable, and the file system allows execution, add execute permission for its owner:

Bash
chmod u+x deploy.sh
./deploy.sh

Directory permissions have distinct meanings: read lists names, write permits directory-entry changes, and execute permits traversal/search. ACLs, parent-directory access, and noexec mounts can also explain Permission denied; changing a file’s execute bit is not a universal fix.

Avoid the shortcut of running sudo npm install -g or sudo before everything. sudo normally runs as root unless another target user is selected; it can create root-owned files, which later causes EACCES errors for your normal user. Fix the permissions or the installation location instead.

7. Getting help

You do not need to memorize every flag.

Bash
ls --help                               # short built-in help (many Linux tools)
man grep                                # the manual page (q to quit)
npm help install
git help commit                         # you will use this in the next lesson

On macOS some commands do not support --help; use man. When you meet an unfamiliar error, read it from the top, copy the exact message into a search engine with the tool name, and check the official docs first.

Continue with Git snapshots, staging, and commits for related practice.

8. Worked example: prepare the Harbor Workshops project

Task. Use a new, empty practice folder for Harbor Workshops; these setup commands write files and should not be replayed over an existing project. Create the project folder, add the basic files, find every mention of "capacity" in a copied starter file, and prepare a repository-ready layout.

Step 1: Create the structure.

Bash
cd ~
mkdir -p projects
cd projects
mkdir -p harbor-workshops/src/components harbor-workshops/docs
cd harbor-workshops
pwd
# /Users/amira/projects/harbor-workshops

mkdir -p creates the intermediate folders in one command and does not fail if they already exist.

Step 2: Create files.

Bash
touch README.md src/App.jsx src/components/WorkshopCard.jsx
echo "# Harbor Workshops" > README.md
echo "node_modules" > .gitignore
echo ".env*" >> .gitignore
echo "!.env.example" >> .gitignore
echo "dist" >> .gitignore
cat .gitignore
# node_modules
# .env*
# !.env.example
# dist

Create the promised npm manifest without installing packages:

Bash
cat > package.json <<'EOF'
{
  "name": "harbor-workshops",
  "version": "0.1.0",
  "private": true,
  "type": "module"
}
EOF

This manifest does not yet define a dev server or test command; later exercises add their tools and scripts.

Notice the first echo uses > because the file is new, and the others use >> to append. Using > for every entry would keep only the final line.

Step 3: Add a starter file and search it.

Bash
cat > src/components/WorkshopCard.jsx <<'EOF'
export default function WorkshopCard({ title, capacity, booked }) {
  const seatsLeft = capacity - booked;
  // TODO: show a warning when capacity is nearly reached
  return <p>{title}: {seatsLeft} seats left (capacity {capacity})</p>;
}
EOF

grep -rn -i "capacity" src
# src/components/WorkshopCard.jsx:1:export default function WorkshopCard({ title, capacity, booked }) {
# src/components/WorkshopCard.jsx:2:  const seatsLeft = capacity - booked;
# src/components/WorkshopCard.jsx:3:  // TODO: show a warning when capacity is nearly reached
# src/components/WorkshopCard.jsx:4:  return <p>...

Step 4: Count and filter.

Bash
grep -rn "TODO" src | wc -l
# 1

Step 5: Check the layout and hidden files.

Bash
ls -la
# Abbreviated names only; real -la output includes permissions, "." and "..":
# .gitignore  README.md  package.json  docs  src

Why this approach and not others. The steps document how to reproduce this layout in a fresh folder. They are not an idempotent update script: repeated redirects can overwrite files or append duplicate entries. A graphical file manager also works; the command sequence makes the exact paths and operations easy to record. Using > for every line of .gitignore is wrong because each > overwrites the file. Searching with grep -r is better than opening every file in an editor because it shows all locations at once, with line numbers.

9. Comparison tables

Common commands by purpose

Goal Command Notes
Where am I? pwd Prints the absolute path
What is here? ls, ls -la -a shows hidden files
Change folder cd path cd .., cd ~, cd -
Make a folder mkdir -p a/b -p for nested
Copy / move cp, mv Can overwrite; -i requests confirmation
Delete rm, rm -r No trash; verify first
Read a file cat, less, head, tail less for long files
Search content grep -rn Exclude node_modules
Search names find Supports wildcards

Redirection operators

Operator Effect Typical risk
> Write output to a file Overwrites existing content
>> Append output to a file Duplicates lines if rerun
| Send output to another command Downstream commands can change files; upstream failures can be hidden
2> Redirect stderr Truncates the destination; if its parent is missing or inaccessible, redirection fails
&& Run next only on success Stops the chain at the first failure

Absolute and relative paths

Absolute Relative
Starts at /; an unquoted ~ can expand to an absolute home path The current directory
Works from any folder Yes No
Best for A fixed machine location independent of the working directory Portable project paths resolved from a known project directory

10. Pitfalls and study tips

  • Pitfall: the wrong working directory. Many "it does not work" problems are simply running npm or git in the wrong folder. Run pwd and ls first. npm can search the current directory and ancestors for a project root; it does not search arbitrary child projects. Git also normally locates a repository through the current directory or its ancestors.
  • Pitfall: overwriting with >. Use >> to append. A single wrong > can erase a configuration file.
  • Pitfall: deleting by wildcard. rm *.js removes every matching file in the current folder. Preview the match with ls *.js before you delete.
  • Pitfall: spaces in paths. Quote them or avoid them.
  • Pitfall: sudo as a fix. It hides the real problem and creates root-owned files.
  • Pitfall: confusing the terminal and the shell. The terminal is the window; the shell interprets commands.

Study tip. For an original command-selection exercise, identify which operation achieves the stated goal. Identify the goal (navigate, create, move, search, combine) and then check the exact flag: -r for folders, -a for hidden files, -p for nested creation, >> to append. When two options look similar, ask "does this overwrite, delete, or ask first?"

Study tip. If a question shows command not found, think of PATH and installation. If it shows Permission denied, think of permissions and chmod. If it shows No such file or directory, check the path, the case, and the current directory.

Mini case study: the missing test file

Situation. Noor, a new teammate, runs npm test in her home folder and sees npm ERR! enoent Could not read package.json. She then runs ls, sees her project folder, and decides to fix it by running sudo npm install in her home folder. Later she wants to know which source files still contain console.log before a release.

Questions.

  1. What is the real cause of the first error?
  2. Why is sudo npm install the wrong response?
  3. Write the commands to find every console.log in src and count the files.

Solution.

  1. npm resolves the local project from the working directory and suitable ancestors. It will not find the child project merely because that folder is visible in ls; Noor is outside it. The fix is cd projects/harbor-workshops (verified with pwd and ls), then npm test.
  2. sudo does not solve a wrong-directory problem, and it would install packages into the wrong place as root, creating root-owned files that cause permission errors later.
  3. Use grep -rnF "console.log" src for locations and grep -rlF "console.log" src | wc -l for the number of matching filenames. -F treats the dot literally. This simple line count assumes filenames do not contain newlines; inspect stderr and search status if the result seems suspicious.

Key terms

  • Terminal: the window that shows text input and output.
  • Shell: the program that interprets your commands (bash, zsh, PowerShell).
  • Path: the location of a file or folder; absolute or relative.
  • Flag: an option that changes how a command behaves.
  • Pipe (|): connects one command's output to another's input.
  • Redirection: sending output to a file or reading input from one.
  • Environment variable: a named value available to programs.
  • PATH: the ordered list of folders searched for commands.
  • Permission: the read, write, and execute rights of a file.
  • Hidden file: a file whose name begins with a dot.

Summary checklist

  • I can tell the terminal, the shell, and the file system apart.
  • I can navigate with pwd, ls, cd, and use ., .., and ~.
  • I create files and folders with mkdir -p and touch.
  • I know that rm, >, mv, and cp can destroy data without warning.
  • I search with grep -rn and find, excluding node_modules.
  • I can combine commands with pipes, >, >>, and &&.
  • I can diagnose command not found with PATH and Permission denied with chmod.
  • I keep secrets in .env and out of version control.
  • I use tab completion, history, and man to work faster.

Official sources