
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.
The programs in this lab work on text, so we need a few things you have not used yet.
string typeYou 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.
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.
stringSo 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);
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 "
Read that function until you are sure how it works. Four of the five functions you write today are variations
on it.
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;
}
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
(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.)
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.
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.
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;
}
}
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
staticlocal 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?
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 statements without curly braces.if (c == 'A')
s = s + c;
will be incorrectly indented as
if (c == 'A')
s = s + c;
No support for // and /* */ comments. A commented-out curly brace should not affect indentation, but
ours counts it anyway.
Braces inside string and char literals are misinterpreted as blocks.
if (true) {
s = "{{";
t = "ABC";
}
will be incorrectly indented as
if (true) {
s = "{{";
t = "ABC";
}
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 \".
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.
*/