Video Coming Soon...
37: expand/unexpand
The expand command takes any \t (tab) characters and converts them to the one true way to write code: spaces. It's a fairly simple process since it only needs to look for tabs and write a set number of spaces instead. Where expand gets complicated is the -t option can take a comma separate list of specific tab stops.
The unexpand command is the inverse. It takes a number of spaces and converts them into dirty randomized totally weird for code \t characters. I'm pairing these to together because they're so similar you could create a single binary that does both, or create both based on the other's code.
The Challenge
Once again you're trying to implement expand based on the documentation for expand. You may need to craft a file with \t tabs if you don't have one, but you could also skip ahead and do unexpand first, then use that to make a file with tabs in it.
For a first version only get a -t option that takes one number for the tab stop. In the Further Study section you'll expand on this and create a fully functioning -t option.
The unexpand version of -t is probably even more complicated, so once again just focus on getting a basic -t working for both.
The Code
Here's my first quick version of expand that assumes a 2 space tab stop:
See my first version of expand
View Source file expand.cpp Only#include <fmt/core.h>
#include <fstream>
void convert_file(std::istream& in, int tab_stop) {
std::string in_line;
while(in) {
std::string out_line;
getline(in, in_line);
for(size_t i = 0; i < in_line.size(); i++) {
char ch = in_line[i];
if(ch == '\t') {
out_line.append(tab_stop, ' ');
} else {
out_line += ch;
}
}
fmt::println("{}", out_line);
}
}
int main(int argc, char* argv[]) {
for(int i = 1; i < argc; i++) {
std::ifstream in_file{argv[i]};
convert_file(in_file, 2);
}
}
Here's my first little version of unexpand as well:
See my first version of unexpand
View Source file unexpand.cpp Only#include <fmt/core.h>
#include <fstream>
void convert_file(std::istream& in, int tab_stop) {
std::string in_line;
while(in) {
std::string out_line;
getline(in, in_line);
int spaces = 0;
for(size_t i = 0; i < in_line.size(); i++) {
char ch = in_line[i];
switch(ch) {
case ' ':
if(++spaces % tab_stop == 0) {
out_line += '\t';
spaces = 0;
}
break;
default:
if(spaces < tab_stop) {
for(int i = 0; i < spaces; i++) {
out_line += ' ';
spaces = 0;
}
}
out_line += ch;
}
}
fmt::println("{}", out_line);
}
}
int main(int argc, char* argv[]) {
for(int i = 1; i < argc; i++) {
std::ifstream in_file{argv[i]};
convert_file(in_file, 2);
}
}
Remember that you should try to do this on your own from your starter project, then if you get stuck peek at mine to get clues. You can also compare yours to mine when you're done to see how I did something similar.
The Discussion
You should hopefully start seeing a pattern with all of these tools:
- Open input.
- Process arguments.
- Do stuff to input based on arguments.
- Product output.
I'd say about 90% of the code you write will do these three things in different degrees of complexity. If you think about it, a video game is doing this, just with a lot more data and in a continuous loop. It reads your keyboard/mouse/joystick for "arguments", uses game state as "input", does stuff to the game state based on your actions, and produces the next frame of the output.
If you're ever stuck trying to solve a problem, try breaking it down into these four things:
- What's the input? Many times if you can figure out what the data should be the rest of the program will fall into place.
- What are the arguments needed to work on this input? You could also say "parameters" and think of tools like this as a function you call from the command line.
- What stuff do you need to do to the input to produce your desired output? If you can find transformations on the data that are controlled by the parameters then usually the output will write itself.
- How should the output be presented? Do you have a prototype of the output you want?
You can also do these steps in reverse by starting with your desired output, then working backwards. You may even start with the output, extract the data, then make the middle work. No matter how you approach it, it's easier to solve problems if you can break them down into these four general things.
Further Study
Try to create a fully working -t option for both expand and unexpand. It should accept a single number, but also multiple numbers for individual tabs. This may prove to be very difficult so if you get stuck just move on and try again later.
You can also write an automated test that converts a file with unexpand, then converts it back with expand, and confirms the converted files matches the original.
Register for Learn C++ the Hard Way
Register to gain access to additional videos which demonstrate each exercise. Videos are priced to cover the cost of hosting.