texdepends/main.cpp

396 lines
8.9 KiB
C++

/*
* main.cpp
*
* Created on: 07.10.2017
* Author: julian
*/
#include <sys/mman.h> // for mmap()
#include <sys/stat.h> // for fstat()
#include <fcntl.h> // for open(), O_RDONLY
#include <unistd.h> // for close()
#include <errno.h> // for perror()
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <map>
#include <set>
#include <functional>
#include "fs.hpp"
#include "memory_string.hpp"
using namespace std;
class InputExtractor
{
public:
class Exception : public std::runtime_error {
public:
Exception(const std::string &str) : std::runtime_error(str) {}
};
typedef std::vector<Path::Path> List;
List operator()(const Path::Path &file, const MemoryString &str);
std::string macroExpand(const std::string &input);
List Include(Path::Path);
protected:
std::map<std::string, std::string> macros;
std::set<Path::Path> includes;
};
#include <functional>
template <typename iterator>
std::string readTill(iterator &start, const iterator &end, std::function<bool(const iterator&)> limiter) {
iterator current = start;
while(current != end && !limiter(current)) {
++current;
}
return std::string(start, current);
}
template <typename iterator>
std::string readBrackets(iterator &current, const iterator &end, const char * brackets) {
if (current == end || *current != brackets[0]) {
std::cout << brackets[0] << "!=" << *current;
return std::string();
}
// skip first opening bracket
current++;
int depth = 1;
auto bbegin = current, bend = current;
while(depth > 0 && current != end) {
if (*current== brackets[0]) {
depth++;
} else if (*current == brackets[1]) {
depth--;
} else {
bend = ++current;
}
}
// advance beyond last bracket
if (current != end)
current++;
return std::string(bbegin, bend);
}
std::string InputExtractor::macroExpand(const std::string &input) {
std::string result;
std::map<std::string, std::string>::iterator lookup;
//cout << "expanding: " << input << endl;
std::string::const_iterator current = input.begin();
while(current != input.end()) {
if (*current == '\\') {
current++;
std::string::const_iterator start = current;
while(current != input.end() && *current != '\\') {
if ((lookup = macros.find(std::string(start, current))) != macros.end()) {
break;
}
}
if (lookup == macros.end()) {
throw Exception("unknown macro in macro expansion: " + std::string(start, current));
}
result += lookup->second;
} else {
result += *current;
}
++current;
}
return result;
}
typedef std::map<std::string, std::function<void(InputExtractor::List&, std::string)>> CommandList;
InputExtractor::List InputExtractor::Include(Path::Path path) {
path = Path::Clean(path);
List list;
std::cout << "including file " << path << "...";
// look for include
if (includes.find(path) != includes.end()) {
cout << "SKIP" << endl;
return list; // already been there
}
cout << endl;
// add to include list
includes.insert(path);
int fd = open(path.c_str(), O_RDONLY);
if (fd == -1) {
cerr << "cannot open " << path << endl;
return list;
}
struct stat fileinfo;
if (fstat(fd, &fileinfo) == -1) {
perror("stat");
close(fd);
return list;
}
void * memptr = mmap(NULL, fileinfo.st_size, PROT_READ, MAP_SHARED, fd, 0);
if (memptr == NULL) {
perror("mmap");
close(fd);
return list;
}
// create iteratable memory object
MemoryString file((char*)memptr, fileinfo.st_size);
std::string basedir = Path::Dir(path);
// evaluate memory region (aka the file)
list = (*this)(basedir, file);
// cleanup
munmap(memptr, fileinfo.st_size);
close(fd);
return list;
cout << path << "done" << endl;
}
// make a relative filepath absolute using the root filename
// and/or the process working directory
Path::Path fixFilename(const Path::Path &file, const Path::Path &root) {
Path::Path temp(file);
// try to make path absolute
if (Path::isRelative(temp)) {
// add current file's path
temp = Path::Join(root, temp);
}
if (Path::isRelative(temp)) {
// add process working directory
temp = Path::Join(fs::cwd(), temp);
}
return Path::Clean(temp);
}
// evaluate a tex file searching for input statements
InputExtractor::List InputExtractor::operator()(const Path::Path &file, const MemoryString &str){
List result;
CommandList IncludeCommands;
IncludeCommands["input"] = [&file, this](List &l, std::string a) {
if (a.empty()) return;
if (Path::Extension(a) != ".tex") a += ".tex";
a = fixFilename(a, file);
l.push_back(a);
// try to extract all inputs of that file
auto sub = Include(a);
if (!sub.empty())
std::copy(sub.begin(), sub.end(), std::inserter(l, l.end()));
};
IncludeCommands["include"] = IncludeCommands["input"];
IncludeCommands["lstinputlisting"] = [file](List &l, std::string a){ if (!a.empty()) { l.push_back(fixFilename(a, file)); } };
MemoryString::const_iterator current = str.begin();
while(current != str.end()) {
if (*current == '%') {
// line commment
while(current != str.end() && *current != '\n')
current++;
continue;
}
if (*current != '\\') {
// skip non macros
current++;
continue;
}
// read macro name
current++;
if (current == str.end())
throw Exception("unexpected EOF");
auto start = current, end = current;
auto limiter = [](char c) -> bool { return isspace(c) || c == '{' || c == '\\'; };
for(; current != str.end() && !limiter(*current); end = (++current+1))
{
auto searchHit = IncludeCommands.find(std::string(start, end));
if (searchHit != IncludeCommands.end()) {
// handle crosslink
current++;
if (current == str.end()) continue;
cout << searchHit->first << "[" << readBrackets(current, str.end(), "[]") << "]";
auto inner = readBrackets(current, str.end(), "{}");
cout << ":" << inner << endl;
// add to results
searchHit->second(result, macroExpand(inner));
break;
} else if (std::string(start, end) == std::string("def")) {
// define new macro
current++;
if (current == str.end() || *current != '\\') {
continue;
}
current++;
std::function<bool(const MemoryString::const_iterator&)> limiter = [](const MemoryString::const_iterator &it) -> bool { return std::string("{ \t\n").find(*it) != std::string::npos; };
std::string name = readTill(current, str.end(), limiter);
cout << "new macro definition: " << name << endl;
macros.insert(std::pair<std::string, std::string>(name, readBrackets(current, str.end(), "{}")));
break;
}
}
}
return result;
}
struct Config {
std::string targetName;
Path::Path outfilePath;
bool outfileOverride;
} config ;
std::ostream &writeTarget(std::ostream &stream, const std::string &target_name) {
stream << target_name << ":";
return stream;
}
std::ostream &writeTargetDepends(std::ostream &stream, const Path::Path &path) {
stream << path << "\\\n";
return stream;
}
int openOutfile(std::ofstream &stream) {
cout << "writing dependency rules to " << config.outfilePath << endl;
stream.close();
stream.open(config.outfilePath);
if (!stream) {
cerr << "could not write to " << config.outfilePath << endl;
return 0;
}
if (!config.targetName.empty()) {
writeTarget(stream, config.targetName);
}
return 1;
}
#include <getopt.h>
struct option long_opts[] = {
{"output", required_argument, NULL, 'o'},
{"target", required_argument, NULL, 't'},
{}, // terminator
};
void help(int argc, char ** args) {
cout << args[0] << " [-o output] files..." << endl;
cout << "--output" << endl;
cout << "-o\tby default the program will create a depfile for every input" << endl;
cout << "\tnaming it like the tex file with a .d extension. By giving the -o Option" << endl;
cout << "\tthe output fill instead be put in this file exclusivly" << endl;
}
int main(int argc, char ** args) {
// find all the files the given tex files depend on
int fd = 0;
struct stat filestat;
int long_index = 0;
int opt = 0;
while((opt = getopt_long(argc, args, "o:t:", long_opts, &long_index)) != -1) {
switch(opt) {
case 'o':
config.outfilePath = optarg;
config.outfileOverride = true;
break;
case 't':
config.targetName = optarg;
break;
case '?':
help(argc, args);
return 0;
}
}
std::ofstream outfile;
if (!config.outfilePath.empty() && !openOutfile(outfile)) {
return -1; // failed to open outfile
}
// scan remaining arguments as input filenames
for(; optind < argc; optind++) {
Path::Path filename = args[optind];
InputExtractor parser;
// parse file
InputExtractor::List list = parser.Include(filename);
// output results in makefile rule style
// check for open outfile
if (!config.outfileOverride) {
config.outfilePath = Path::Basename(filename) + ".d";
if (!openOutfile(outfile)) {
continue; // just skip this file
}
writeTarget(outfile, filename);
}
for (auto it = list.begin(); it != list.end(); it++) {
writeTargetDepends(outfile, *it);
}
// add newline for cleanness
outfile << endl;
cout << "done" << endl;
}
}