Lab 3. Modular Programming: An Automatic Indentation Tool

In this lab we will build a small utility program that fixes the indentation of C and C++ source files.
It will have some limitations, but it will handle a significant subset of valid C++ programs.

Given a file with messed up indentation:

                int main(){ 
           // Hi, I'm a program!
int x = 1; 
    for(int i = 0; i < 10; i++) {
cout << i;
          cout << endl;
 }
    }

it will output a well-formatted program:

int main(){
    // Hi, I'm a program!
    int x = 1; 
    for(int i = 0; i < 10; i++) {
        cout << i;
        cout << endl;
    } 
}     

This is a lab about modular programming. We are not going to write one big main function that does
everything. Instead, we will build the program out of five small functions, each of which does exactly one
thing and can be tested on its own. By the time you write the last one, the program will practically assemble
itself.


Tools you will need

The programs in this lab work on text, so we need a few things you have not used yet.

The string type

You have seen string used as a parameter type. Here we will actually operate on strings. Given strings s
and t, and a character c:


s.length() and s.size()   Return the number of characters in string s
s[i]   The character at index i (the first character is s[0])
s + t   Returns the concatenation of s and t
      (you can also concatenate a char, s + c)
s += t   Appends t onto the end of string s
      (you can also append a char, s += c)
s == t   Compares two strings for equality — returns true or false
s.empty()   Returns true if the string has no characters in it

Note that + does not change either s or t — it produces a new string. += modifies s in place.

Testing what a character is

The header <cctype> provides functions that classify a single
character. Each returns a true/false answer:


isspace(c)   Is c a white-space character (a space, a tab, a newline)?
isalpha(c)   Is c a letter?
isdigit(c)   Is c a decimal digit?

Add #include <cctype> at the top of any program that uses them.

Functions that return a string

So far the functions you have written have returned numbers and bool values. A function can just as well
return a string, and most of the functions in this lab do. The return type goes in the usual place:

string removeDigits(string s);

The pattern all of these functions use

Nearly every function in this lab follows the same shape: start with an empty string, walk through the input
one character at a time, append the characters you want to keep, and return what you accumulated. Here is a
complete example of that pattern — it is not one of your tasks, but every task resembles it:

// Returns a copy of s with all the digits removed.
string removeDigits(string s)
{
    string result = "";                     // start with nothing

    for (int i = 0; i < s.length(); i++)    // look at each character
    {
        if (!isdigit(s[i]))                 // if we want to keep it...
            result += s[i];                 // ...append it to the result
    }

    return result;                          // hand back what we built
}
removeDigits("a1b2c3")     // returns "abc"
removeDigits("CSCI 135")   // returns "CSCI "
Scan the input, append what you keep, return the result input s a 1 b 2 c 3 keep skip keep skip keep skip for each character result += s[i]; building result a b c When the loop ends, return result; hands back the string you built.

Read that function until you are sure how it works. Four of the five functions you write today are variations
on it.

Reading input line by line

To read a whole line of text, including any spaces in it, use getline:

string line;
getline(cin, line);

getline returns a value that becomes false when there is no more input, so the way to read every line
until the input runs out is:

string line;
while (getline(cin, line))
{
    // this loop body runs once per line of input
    cout << line << endl;
}

Feeding a file into your program: input redirection

All of our programs read from cin, but typing a whole C++ file by hand every time you test would be
miserable. The shell can connect a file to a program’s cin using the < operator:

$ ./unindent < bad-code.cpp

This runs unindent and feeds it the contents of bad-code.cpp exactly as if you had typed them in.
You can also capture the output into a new file with >:

$ ./indent < bad-code.cpp > fixed-code.cpp
Redirection connects files to cin and cout bad-code.cpp a file on disk < ./indent cin → cout your program > fixed-code.cpp a new file Your program does not know the difference — it just reads cin and writes cout as always.

(If you ever want to type the input by hand instead, run ./unindent with no redirection, type a few lines,
and press Ctrl+D on an empty line to signal the end of input.)


Task A. Counting characters

To figure out how deeply a line should be indented, we will need to count curly braces. Let’s start there.

Write a function

int countChar(string line, char c);

that scans line and returns the number of occurrences of the character c.

countChar("for(int i = 0; i < 10; i++) {", '{')   // returns 1
countChar("    }", '}')                           // returns 1
countChar("int x = 1;", '{')                      // returns 0

Put this function in a program count.cpp, with a driver main that calls it on several hand-picked strings
and prints the results next to what you expect:

int main()
{
    cout << countChar("if (a) { b(); }", '{') << endl;   // expect 1
    cout << countChar("if (a) { b(); }", '}') << endl;   // expect 1
    cout << countChar("plain text", '{') << endl;        // expect 0
    cout << countChar("", '{') << endl;                  // expect 0
    return 0;
}

Do not skip the empty-string case. Functions that behave badly on empty input are a very common source of
bugs, and every function in this lab will eventually be handed a blank line.

Task B. Removing indentation

Before we can indent code correctly, we have to strip whatever indentation is already there.

Write a function

string removeLeadingSpaces(string line);

that takes one line of code and returns a copy of it without leading spaces and tabs:

removeLeadingSpaces("       int x = 1;  ") == "int x = 1;  "

Use isspace to test whether a character is whitespace. This is a variation on the removeDigits pattern
above, with one difference: you skip whitespace only until you reach the first non-whitespace character, and
from that point on you keep everything. Whitespace inside or after the code is left alone — only the
leading whitespace goes.

Now write a program unindent.cpp that reads input from cin line by line and prints each line with its
leading whitespace removed.

Example

Create a badly indented test file called bad-code.cpp:

                int main(){
           // Hi, I'm a program!
int x = 1; 
    for(int i = 0; i < 10; i++) {
cout << i;
          cout << endl;
 }
    }

and run your program on it:

$ ./unindent < bad-code.cpp
int main(){
// Hi, I'm a program!
int x = 1; 
for(int i = 0; i < 10; i++) {
cout << i;
cout << endl;
}
}

A note on how arguments are passed

removeLeadingSpaces receives its argument by value, which means the function works on its own private
copy of the string. Whatever it does to line inside the function has no effect on the variable the caller
passed in. That is exactly what we want here: the function returns a new string and leaves the original
alone.

Keep this in mind — in Task E we will hit a situation where passing by value is precisely the wrong thing.

Task C. A Boolean helper

Look at the target output again:

int main(){
    // Hi, I'm a program!
    int x = 1; 
    for(int i = 0; i < 10; i++) {
        cout << i;
        cout << endl;
    } 
}     

Notice that a line beginning with a closing brace } is indented one level less than the lines above it,
because that brace closes the block it sits at the end of. We will need to detect such lines.

Add a function

bool startsWithClosingBrace(string line);

that returns true if the first non-whitespace character of line is }, and false otherwise.

startsWithClosingBrace("}")            // true
startsWithClosingBrace("   } // end")  // true
startsWithClosingBrace("int x = 1;")   // false
startsWithClosingBrace("")             // false

Hint: you have already written a function that strips leading whitespace. Use it. Reusing functions you
have already built, instead of writing the same logic a second time, is the entire point of modular
programming.

Task D. Adding indentation

Add a function

string addIndent(string line, int level, char fill = '\t');

that returns line with level copies of the character fill placed in front of it.

addIndent("cout << i;", 2)        // returns "\t\tcout << i;"
addIndent("cout << i;", 2, ' ')   // returns "  cout << i;"
addIndent("cout << i;", 0)        // returns "cout << i;"

Here '\t' is the tab character — a single character that the terminal displays as one level of
indentation. Writing it inside single quotes with the backslash is how you name it in C++, the same way
'\n' names a newline.

Default arguments

The third parameter has a default argument. Because it is declared as char fill = '\t', a caller who
does not care may leave it out and get tabs, while a caller who wants spaces can ask for them explicitly.

Two rules go with this. Parameters with default arguments must come last in the parameter list — once
one parameter has a default, everything after it must too. And the = '\t' goes in the earliest occurrence
of the function’s name
: if the function has a prototype, the default belongs there and is not repeated in
the definition’s header.

If level is negative, treat it as zero rather than doing something strange.

Task E. Tracking the depth: a reference parameter

Now we can put it all together. As we read the file line by line, we need to keep track of
how many blocks are open at the beginning of each line. Here that number is shown on the left:

0   int main(){
1   // Hi, I'm a program!
1   int x = 1; 
1   for(int i = 0; i < 10; i++) {
2   cout << i;
2   cout << endl;
2   }
1   }

Each { on a line increases the depth for the following lines; each } decreases it. And, as we
established in Task C, a line that starts with } gets printed one level shallower than its recorded depth.

How the depth counter moves from line to line DEPTH LINE EFFECT ON THE NEXT LINE 0 int main(){ +1 1 int x = 1; no braces — unchanged 1 for(int i = 0; i < 10; i++) { +1 2 cout << i; no braces — unchanged 2 } −1 1 } −1 The red line starts with } — its depth is 2, but it is printed at 1. That is the Task C rule.

So we want a function that, given one raw line of input, returns the properly indented version of it:

string indentLine(string line, int depth);

But there is a problem. This function has two jobs: it has to produce the indented line, and it also has
to update the depth for the next line. A function can only return one value, and we have already spent the
return value on the string.

Why passing by value fails here

Suppose we try it anyway, and write the function so that it modifies its parameter depth:

string indentLine(string line, int depth)
{
    line = removeLeadingSpaces(line);

    int printDepth = depth;
    if (startsWithClosingBrace(line))
        printDepth = depth - 1;

    depth += countChar(line, '{');     // these updates
    depth -= countChar(line, '}');     // go nowhere

    return addIndent(line, printDepth);
}

Because depth is passed by value, the function is handed a copy of the caller’s variable. It happily
adjusts that copy, and then the copy is destroyed when the function returns. The caller’s depth never
changes, so every line comes back at depth 0:

int main()
{
    string line;
    int depth = 0;
    while (getline(cin, line))
        cout << indentLine(line, depth) << endl;
    return 0;
}
int main(){
// Hi, I'm a program!
int x = 1; 
for(int i = 0; i < 10; i++) {
cout << i;

Type this in and confirm it for yourself before reading on. Seeing the bug is the point of the exercise.

The fix: pass by reference

Change a single character in the parameter list:

string indentLine(string line, int &depth);

The & makes depth a reference parameter — an alias for the caller’s variable rather than a copy of it.
Any change the function makes to depth is really a change to the variable in main. Now the update survives
the return, and the next call starts from the right place.

A copy versus an alias int depth BY VALUE — A COPY main 0 indentLine 0 → 1 copied The copy is destroyed on return. main's depth is still 0. int &depth BY REFERENCE — AN ALIAS main 0 → 1 indentLine depth no copy — another name for the same box There was only ever one variable. main's depth is now 1.

Before wiring this into the full program, try the idea on something small enough to trace. Write a
throwaway function and watch it change a variable belonging to main:

void doubleNum(int &refVar)
{
    refVar *= 2;
}

int main()
{
    int value = 4;
    cout << "In main, value is " << value << endl;    // 4
    doubleNum(value);
    cout << "Now back in main, value is " << value << endl;    // 8
    return 0;
}

Then remove the & and run it again. The second line prints 4.

Note that in indentLine, line is still passed by value: we want our own copy of the string to chop up,
and we do not want to disturb the caller’s. It is normal for a function to mix the two, and choosing correctly
for each parameter is part of designing a good interface. The rule of thumb is that when exactly one value
needs to come back, a return value is the better tool; a reference parameter is for when something else has
to change as well.

The task

Implement indentLine with a reference parameter and use it in a new program indent.cpp, which reads a
source file from cin and prints the correctly indented version:

$ ./indent < bad-code.cpp
int main(){
    // Hi, I'm a program!
    int x = 1; 
    for(int i = 0; i < 10; i++) {
        cout << i;
        cout << endl;
    }
}

Your main should be short — a getline loop, a depth variable, and a call to indentLine. All of the real
work belongs in the five functions you have written.

Something to think about: another way to make a value survive between calls is to declare it as a
static local variable inside the function, so that it persists for the lifetime of the program instead of
being destroyed on return. That would work here. What would break if you wanted to indent two different
files in a single run of the program?

Shortcomings of our program

Our indenter is genuinely useful, but it is easy to break. Read these carefully — a good programmer knows the
limits of their own code.

if (c == 'A')
    s = s + c;

will be incorrectly indented as

if (c == 'A')
s = s + c;
if (true) {
    s = "{{";
    t = "ABC";
}

will be incorrectly indented as

if (true) {
    s = "{{";
            t = "ABC";
        }

Task F (Bonus). Fixing one of them

Pick one of the three shortcomings above and fix it.

The comment case is the most approachable: write a function

string removeComments(string line);

that returns the line with any // comment stripped off the end, and call it before counting braces.
(Careful: you must not count braces inside the comment, but you should still print the comment. Think about
what that means for where in indentLine the call belongs.)

The string-literal case is harder and more interesting. Write a function that walks the line character by
character, keeping track of whether it is currently inside a double-quoted string, and counts only the braces
that are outside of one. Remember that a quote can itself be escaped as \".

How to submit your programs

Each program should be submitted through Gradescope

Write separate programs for each part of the assignment.
Submit only the source code (.cpp) files, not the compiled executables.
Each program should start with a comment that contains your name and a short program description, for example:

/*
Author: your name
Course: CSCI-135
Instructor: their name
Assignment: title, e.g., Lab3A

Here, briefly, at least in one or a few sentences
describe what the program does.
*/