Files
textmate/Applications/bl/src/bl.cc

382 lines
13 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include <updater/updater.h>
#include <regexp/format_string.h>
#include <regexp/glob.h>
#include <oak/oak.h>
#include <text/case.h>
#include <text/ctype.h>
#include <text/decode.h>
#include <text/format.h>
#include <io/io.h>
#include <OakSystem/application.h>
static double const AppVersion = 1.3;
static size_t const AppRevision = APP_REVISION;
// example: bl install Apache AppleScript Blogging Bundle\ Development C CSS Diff Git HTML Hyperlink\ Helper JavaScript Mail Make Markdown Math Objective-C PHP Perl Property\ List Ragel Remind Ruby SQL Shell\ Script Source Subversion TODO Text TextMate XML Xcode
static int get_width ()
{
if(!isatty(STDIN_FILENO))
return INT_MAX;
struct winsize ws;
if(ioctl(0, TIOCGWINSZ, &ws) == -1)
{
perror("TIOCGWINSZ");
exit(EX_OSERR);
}
return ws.ws_col;
}
static std::string textify (std::string str)
{
str = format_string::replace(str, "\\A\\s+|<[^>]*>|\\s+\\z", "");
str = format_string::replace(str, "\\s+", " ");
str = decode::entities(str);
return str;
}
extern char* optarg;
extern int optind;
static void usage (FILE* io = stdout)
{
fprintf(io, "%1$s %2$.1f (" COMPILE_DATE " revision %3$zu)\n", getprogname(), AppVersion, AppRevision);
fprintf(io, "Usage: %s list [Cs] [«bundle» ..]\n", getprogname());
fprintf(io, " %s install [Cs] «bundle» ..\n", getprogname());
fprintf(io, " %s uninstall [Cs] «bundle» ..\n", getprogname());
fprintf(io, " %s show [C] «bundle» ..\n", getprogname());
fprintf(io, " %s dependencies [Cs] [«bundle» ..]\n", getprogname());
fprintf(io, " %s dependents [C] [«bundle» ..]\n", getprogname());
fprintf(io, " %s update [C]\n", getprogname());
fprintf(io, " %s help\n", getprogname());
fprintf(io, "\n");
fprintf(io, "Options:\n");
fprintf(io, " -h/--help Show this help.\n");
fprintf(io, " -v/--version Show version number.\n");
fprintf(io, " -C/--directory «path» Use «path» for local bundles.\n");
fprintf(io, " -s/--source «name» Restrict bundles to the given source. Multiple sources are allowed.\n");
fprintf(io, "\n");
fprintf(io, "Globs: Both source and bundle names can contain wild cards:\n");
fprintf(io, " ? Match any single character\n");
fprintf(io, " * Match any characters\n");
fprintf(io, " {«a»,«b»,«c»} Match «a», «b», or «c».\n");
fprintf(io, "\n");
fprintf(io, "Special bundle names:\n");
fprintf(io, " outdated Bundles with a pending update\n");
fprintf(io, " installed Bundles already installed\n");
fprintf(io, " defaults Default bundles (excl. mandatories)\n");
fprintf(io, " mandatories Mandatory bundles\n");
}
static bool matches (std::string const& str, std::vector<std::string> const& globs)
{
if(globs.empty())
return true;
for(auto const& glob : globs)
{
if(path::glob_t(glob).does_match(str))
return true;
}
return false;
}
static bool matches (bundles_db::bundle_ptr bundle, std::vector<std::string> const& globs)
{
if(globs.empty())
return true;
for(auto const& glob : globs)
{
if(glob == "outdated" && bundle->has_update())
return true;
if(glob == "installed" && bundle->installed())
return true;
if(glob == "defaults" && bundle->is_default())
return true;
if(glob == "mandatories" && bundle->is_mandatory())
return true;
if(path::glob_t(glob).does_match(text::lowercase(bundle->name())))
return true;
}
return false;
}
static std::vector<bundles_db::bundle_ptr> filtered_bundles (std::vector<bundles_db::bundle_ptr> const& index, std::vector<std::string> const& sourceNames, std::vector<std::string> const& bundleNames)
{
std::vector<bundles_db::bundle_ptr> res;
std::set<oak::uuid_t> seen;
for(auto const& bundle : index)
{
if(matches(bundle->source() ? bundle->source()->identifier() : NULL_STR, sourceNames) && matches(bundle, bundleNames))
{
if(seen.find(bundle->uuid()) == seen.end())
{
seen.insert(bundle->uuid());
res.push_back(bundle);
}
}
}
return res;
}
static std::string short_bundle_info (bundles_db::bundle_ptr bundle, int width)
{
int descWidth = std::max(10, width - 23);
char status = bundle->installed() ? (bundle->has_update() ? 'U' : 'I') : ' ';
if(bundle->is_dependency())
status = tolower(status);
std::string desc = bundle->description();
desc = desc == NULL_STR ? "(no description)" : textify(desc);
if(desc.size() > descWidth)
desc.resize(descWidth);
return text::format("%c %-20.20s %s", status, bundle->name().c_str(), desc.c_str());
}
static void version ()
{
fprintf(stdout, "%1$s %2$.1f (" COMPILE_DATE " revision %3$zu)\n", getprogname(), AppVersion, AppRevision);
}
int main (int argc, char const* argv[])
{
oak::application_t::set_support(path::join(path::home(), "Library/Application Support/TextMate"));
oak::application_t app(argc, argv);
static struct option const longopts[] = {
{ "directory", required_argument, 0, 'C' },
{ "source", required_argument, 0, 's' },
{ "help", no_argument, 0, 'h' },
{ "version", no_argument, 0, 'v' },
{ 0, 0, 0, 0 }
};
std::vector<std::string> sourceNames;
std::string installDir = NULL_STR;
unsigned int ch;
while((ch = getopt_long(argc, (char* const*)argv, "s:C:hv", longopts, NULL)) != -1)
{
switch(ch)
{
case 'C': installDir = optarg; break;
case 's': sourceNames.push_back(optarg); break;
case 'v': version(); return EX_OK;
case 'h': usage(); return EX_OK;
case '?': /* unknown option */ return EX_USAGE;
case ':': /* missing option */ return EX_USAGE;
default: usage(stderr); return EX_USAGE;
}
}
if(optind == argc)
return usage(stderr), EX_USAGE;
std::string command = argv[optind];
if(command == "help")
return usage(stdout), EX_OK;
std::vector<std::string> bundleNames;
for(int i = optind + 1; i < argc; ++i)
bundleNames.push_back(text::lowercase(argv[i]));
static std::set<std::string> const CommandsNeedingBundleList = { "install", "uninstall", "show", "dependents" };
if(bundleNames.empty() && CommandsNeedingBundleList.find(command) != CommandsNeedingBundleList.end())
{
fprintf(stderr, "no bundles specified\n");
return EX_USAGE;
}
static std::set<std::string> const CommandsNeedingUpdatedSources = { "update", "install", "list", "show" };
if(CommandsNeedingUpdatedSources.find(command) != CommandsNeedingUpdatedSources.end())
{
std::vector<bundles_db::source_ptr> toUpdate;
for(auto const& source : bundles_db::sources(installDir))
{
if(!source->disabled() && source->needs_update())
toUpdate.push_back(source);
}
__block std::vector<bundles_db::source_ptr> failedUpdate;
dispatch_apply(toUpdate.size(), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(size_t i){
fprintf(stderr, "Downloading %s\n", toUpdate[i]->url().c_str());
if(!update(toUpdate[i]))
failedUpdate.push_back(toUpdate[i]);
});
for(auto source : failedUpdate)
fprintf(stderr, "*** failed to update source: %s (%s)\n", source->name().c_str(), source->url().c_str());
if(!failedUpdate.empty())
exit(EX_UNAVAILABLE);
}
std::vector<bundles_db::bundle_ptr> index = bundles_db::index(installDir);
if(command == "list")
{
for(auto const& bundle : filtered_bundles(index, sourceNames, bundleNames))
fprintf(stdout, "%s\n", short_bundle_info(bundle, get_width()).c_str());
}
else if(command == "install")
{
std::vector<bundles_db::bundle_ptr> toInstall;
for(auto const& bundle : filtered_bundles(index, sourceNames, bundleNames))
{
if(!bundle->installed())
toInstall.push_back(bundle);
else fprintf(stderr, "skip %s (%s) -- already installed\n", bundle->name().c_str(), bundle->origin().c_str());
}
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
size_t n = toInstall.size();
__block std::vector<bundles_db::bundle_ptr> failed;
__block std::vector<double> progress(n);
if(isatty(STDERR_FILENO))
{
if(dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue()))
{
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, NSEC_PER_SEC / 10, NSEC_PER_SEC / 5);
dispatch_source_set_event_handler(timer, ^{
double current = 0, total = 0;
for(size_t i = 0; i < n; ++i)
{
current += toInstall[i]->size() * progress[i];
total += toInstall[i]->size();
}
fprintf(stderr, "\rDownloading... %4.1f%%", 100 * current / total);
});
dispatch_resume(timer);
}
}
dispatch_apply(n, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(size_t i){
if(!install(toInstall[i], installDir, &progress[i]))
failed.push_back(toInstall[i]);
});
dispatch_async(dispatch_get_main_queue(), ^{
if(isatty(STDERR_FILENO))
fprintf(stderr, "\rDownloading... Done! \n");
if(!failed.empty())
{
std::vector<std::string> names;
std::transform(failed.begin(), failed.end(), back_inserter(names), [](bundles_db::bundle_ptr const& bundle){ return bundle->name(); });
fprintf(stderr, "*** failed to install %s.\n", text::join(names, ", ").c_str());
}
save_index(index, installDir);
if(!failed.empty())
exit(EX_UNAVAILABLE);
exit(EX_OK);
});
});
dispatch_main();
}
else if(command == "uninstall")
{
for(auto const& bundle : filtered_bundles(index, sourceNames, bundleNames))
{
if(bundle->installed())
{
fprintf(stderr, "Uninstalling %s...", bundle->name().c_str());
if(uninstall(bundle, installDir))
fprintf(stderr, "ok!\n");
else fprintf(stderr, " *** failed!\n");
}
else
{
fprintf(stderr, "skip %s (%s) -- not installed\n", bundle->name().c_str(), bundle->origin().c_str());
}
}
save_index(index, installDir);
}
else if(command == "show")
{
bool first = true;
for(auto const& bundle : filtered_bundles(index, sourceNames, bundleNames))
{
if(!std::exchange(first, false))
fprintf(stdout, "\n");
fprintf(stdout, "name: %s\n", bundle->name().c_str());
fprintf(stdout, "source: %s\n", bundle->source() ? bundle->source()->identifier().c_str() : "«no remote source»");
fprintf(stdout, "uuid: %s\n", to_s(bundle->uuid()).c_str());
bool hasName = bundle->contact_name() != NULL_STR;
bool hasEmail = bundle->contact_email() != NULL_STR;
char const* fmt[] = { "", "contact: %1$s\n", "contact: <%2$s>\n\0%1$s", "contact: %1$s <%2$s>\n" };
fprintf(stdout, fmt[(hasName ? 1 : 0) + (hasEmail ? 2 : 0)], bundle->contact_name().c_str(), bundle->contact_email().c_str());
if(bundle->url_updated())
fprintf(stdout, "date: %s\n", to_s(bundle->url_updated()).c_str());
if(bundle->url() != NULL_STR)
fprintf(stdout, "url: %s (%d bytes)\n", bundle->url().c_str(), bundle->size());
if(bundle->path() != NULL_STR)
fprintf(stdout, "path: %s\n", path::with_tilde(bundle->path()).c_str());
if(bundle->origin() != NULL_STR)
fprintf(stdout, "origin: %s\n", bundle->origin().c_str());
for(auto const& grammar : bundle->grammars())
{
std::vector<std::string> fileTypes;
for(auto const& ext : grammar->file_types())
fileTypes.push_back(ext);
if(grammar->mode_line() != NULL_STR)
fileTypes.push_back(text::format("/%s/", grammar->mode_line().c_str()));
if(fileTypes.empty())
fileTypes.push_back(grammar->scope());
fprintf(stdout, "grammar: %s (%s)\n", grammar->name().c_str(), text::join(fileTypes, ", ").c_str());
}
std::vector<std::string> dependencies;
for(auto const& dependency : bundle->dependencies(index))
dependencies.push_back(dependency->name());
if(!dependencies.empty())
fprintf(stderr, "dependencies: %s\n", text::join(dependencies, ", ").c_str());
if(bundle->description() != NULL_STR)
fprintf(stdout, "description: %s\n", textify(bundle->description()).c_str());
}
}
else if(command == "update")
{
for(auto const& bundle : filtered_bundles(index, sourceNames, bundleNames))
{
if(bundle->has_update())
{
fprintf(stderr, "Updating %s...", bundle->name().c_str());
if(update(bundle, installDir))
fprintf(stderr, "ok!\n");
else fprintf(stderr, " *** failed!\n");
}
}
save_index(index, installDir);
}
else if(command == "dependencies")
{
std::vector<bundles_db::bundle_ptr> bundles;
for(auto const& bundle : filtered_bundles(index, sourceNames, bundleNames))
{
if(!bundleNames.empty() || bundle->installed())
bundles.push_back(bundle);
}
for(auto const& bundle : dependencies(index, bundles, false))
fprintf(stdout, "%s\n", short_bundle_info(bundle, get_width()).c_str());
}
else if(command == "dependents")
{
for(auto const& bundle : dependents(index, filtered_bundles(index, sourceNames, bundleNames)))
fprintf(stdout, "%s\n", short_bundle_info(bundle, get_width()).c_str());
}
else
{
fprintf(stderr, "unknown command: %s\n", command.c_str());
return EX_USAGE;
}
return EX_OK;
}