/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/*
Copyright 2013-2021 Stephan Beal (https://wanderinghorse.net).
Derived heavily from previous work:
Copyright (c) 2013 D. Richard Hipp (https://www.hwaci.com/drh/)
This program is free software; you can redistribute it and/or
modify it under the terms of the Simplified BSD License (also
known as the "2-Clause License" or "FreeBSD License".)
This program is distributed in the hope that it will be useful,
but without any warranty; without even the implied warranty of
merchantability or fitness for a particular purpose.
*****************************************************************************
*/
#include "fossil-scm/fossil-internal.h"
#include "fossil-scm/fossil-cli.h"
/** Convenience form of FCLI_VN for level-3 verbosity. */
#define FCLI_V3(pfexp) FCLI_VN(3,pfexp)
#define fcli_empty_m { \
NULL/*appHelp*/, \
NULL/*appHelp2*/,\
NULL/*f*/, \
0/*verbose*/, \
"."/*checkoutDir*/, \
NULL/*argv*/, \
0/*argc*/, \
NULL/*appName*/, \
0/*fDryRun*/, \
{/*transient*/ \
NULL/*repoDb*/, \
0/*helpRequested*/, \
0/*gmtTime*/ \
}, \
{/*config*/ \
-1/*traceSql*/, \
fsl_outputer_empty_m \
} \
}
const fcli_t fcli_empty = fcli_empty_m;
fcli_t fcli = fcli_empty_m;
void fcli_printf(char const * fmt, ...){
va_list args;
va_start(args,fmt);
if(fcli.f){
fsl_outputfv(fcli.f, fmt, args);
}else{
fsl_fprintfv(stdout, fmt, args);
}
va_end(args);
}
/**
Outputs a description of the global fcli CLI options to
stdout.
*/
static void fcli_help_global_options();
int fcli_is_verbose(){
return fcli.verbose;
}
fsl_cx * fcli_cx(){
return fcli.f;
}
static int fcli_open(){
int rc = 0;
fsl_cx * f = fcli.f;
assert(f);
if(fcli.transient.repoDbArg){
FCLI_V3(("Trying to open repo db file [%s]...\n", fcli.transient.repoDbArg));
rc = fsl_repo_open( f, fcli.transient.repoDbArg );
}
else if(fcli.checkoutDir){
fsl_buffer dir = fsl_buffer_empty;
char const * dirName;
rc = fsl_file_canonical_name(fcli.checkoutDir, &dir, 0);
assert(!rc);
dirName = (char const *)fsl_buffer_cstr(&dir);
FCLI_V3(("Trying to open checkout from [%s]...\n",
dirName));
rc = fsl_checkout_open_dir(f, dirName,
(fsl_int_t)fsl_buffer_size(&dir), 1);
/* if(FSL_RC_NOT_FOUND==rc) rc = FSL_RC_NOT_A_CHECKOUT; */
if(rc){
if(!fsl_cx_err_get(f,NULL,NULL)){
rc = fsl_cx_err_set(f, rc, "Opening of checkout under "
"[%s] failed with code %d (%s).",
dirName, rc, fsl_rc_cstr(rc));
}
}
fsl_buffer_reserve(&dir, 0);
if(rc) return rc;
}
if(!rc){
if(fcli.verbose>1){
fsl_db * dbC = fsl_cx_db_checkout(f);
fsl_db * dbR = fsl_cx_db_repo(f);
if(dbC){
FCLI_V3(("Checkout DB name: %s\n", f->ckout.db.filename));
}
if(dbR){
FCLI_V3(("Opened repo db: %s\n", f->repo.db.filename));
FCLI_V3(("Repo user name: %s\n", f->repo.user));
}
}
#if 0
/*
Only(?) here for testing purposes.
We don't really need/want to update the repo db on each
open of the checkout db, do we? Or do we?
*/
fsl_repo_record_filename(f) /* ignore rc - not critical */;
#endif
}
return rc;
}
fsl_error * fcli_error(){
return fcli.f ? &fcli.f->error : NULL;
}
void fcli_err_reset(){
if(fcli.f){
fsl_cx_err_reset(fcli.f);
}
}
static struct {
fsl_list list;
} FCliFree = {
fsl_list_empty_m
};
static void fcli_shutdown(void){
fsl_cx * f = fcli.f;
int rc = 0;
FCLI_V3(("Finalizing fsl_cx @%p\n", (void const *)f));
fsl_free(fcli.argv)/*contents are in the FCliFree list*/;
fcli = fcli_empty;
if(f){
if(1 &&
fsl_cx_db_checkout(f)){
/* For testing/demo only: this is implicit
when we call fsl_cx_finalize().
*/
rc = fsl_checkout_close(f);
assert(0==rc);
FCLI_V3(("Closed checkout/repo db(s).\n"));
}
fsl_cx_finalize( f );
}
fsl_list_clear(&FCliFree.list, fsl_list_v_fsl_free, 0);
fsl_list_reserve(&FCliFree.list, 0);
}
void fcli_help(){
static int hasGlobal = -99;
if(-99==hasGlobal){
hasGlobal = fcli_flag("global", NULL);
}
if(hasGlobal){
fcli_help_global_options();
return;
}
f_out("Showing app-specific help only. Use the --global option in conjunction "
"with --help|-? to show framework-level options.\n\n");
if(fcli.appHelp2){
fcli_help_t const * h = fcli.appHelp2;
fcli_help_arg_t const * flag = h->flags;
f_out("%s\n\n", h->briefDescription);
f_out("Usage:\n\t%s %s\n", fcli.appName, h->briefUsage ? h->briefUsage : "");
if(flag) f_out("\nOptions:\n");
for( ; flag->flagShort || flag->flagLong; ++flag ){
f_out("\t");
if(flag->valType){
if(flag->flagLong){
if(flag->flagShort){
f_out("--%s|-%s=%s", flag->flagLong,
flag->flagShort, flag->valType);
}else{
f_out("--%s=%s", flag->flagLong, flag->valType);
}
}else{
assert(flag->flagShort);
f_out("-%s=%s", flag->flagShort, flag->valType);
}
}else{
if(flag->flagLong){
if(flag->flagShort){
f_out("--%s|-%s", flag->flagLong,
flag->flagShort);
}else{
f_out("--%s%s", flag->flagLong,
flag->flagShort);
}
}else{
assert(flag->flagShort);
f_out("-%s", flag->flagShort);
}
}
f_out("\n\t", flag->brief);
if(flag->brief){
f_out("%s\n\n", flag->brief);
}else if(flag->callback){
flag->callback();
}
}
if(h->callback){
h->callback();
}
}else if(fcli.appHelp){
fcli.appHelp();
}
}
static int fcli_process_args( int argc, char * const * argv ){
int i;
int rc = 0;
char * cp;
fcli.appName = argv[0];
fcli.argc = 0;
fcli.argv = (char **)fsl_malloc( (argc + 1) * sizeof(char*));
fcli.argv[argc] = NULL;
for( i = 1; i < argc; ++i ){
char const * arg = argv[i];
if('-'==*arg){
char const * flag = arg+1;
while('-'==*flag) ++flag;
#define FLAG(F) if(0==fsl_strcmp(F,flag))
FLAG("help") {
fcli.transient.helpRequested = 1;
continue;
}
FLAG("?") {
fcli.transient.helpRequested = 1;
continue;
}
FLAG("V") {
fcli.verbose += 1;
continue;
}
FLAG("VV") {
fcli.verbose += 2;
continue;
}
FLAG("VVV") {
fcli.verbose += 3;
continue;
}
FLAG("verbose") {
fcli.verbose += 1;
continue;
}
FLAG("C"){ /* Disable auto-open of checkout */
fcli.checkoutDir = NULL;
continue;
}
FLAG("S") {
fcli.config.traceSql = 1;
continue;
}
#undef FLAG
/* else fall through */
}
cp = fsl_strdup(arg);
if(!cp) return FSL_RC_OOM;
fcli.argv[fcli.argc++] = cp;
fcli_fax(cp);
}
/** Now pull out our flags. We do this
in a second step so that we can use
the fcli_flag() parsing.
*/
if( fcli_flag("no-checkout", NULL) ){
fcli.checkoutDir = NULL;
}
if(fcli.config.traceSql<0){
fcli.config.traceSql = fcli_flag("trace-sql", NULL);
}else{
/* Just make sure it's not left around in the args list. */
fcli_flag("trace-sql", NULL);
}
fcli.fDryRun = fcli_flag("dry-run", NULL);
fcli_flag2("R", "repo", &fcli.transient.repoDbArg);
#if 0
fcli.transient.gmtTime = fcli_flag("gmt", NULL);
#endif
/*
Keep 'help' LAST so that we can can use the first-non-flag==help
mechanism.
*/
if(fcli.argc && 0==fsl_strcmp("help",fcli.argv[0])){
fcli_next_arg(1) /* strip argument */;
fcli.transient.helpRequested = 2;
}
return rc;
}
char fcli_flag(char const * opt, const char ** value){
int i = 0;
int remove = 0 /* number of items to remove from argv */;
char rc = 0 /* true if found, else 0 */;
fsl_size_t optLen = fsl_strlen(opt);
for( ; i < fcli.argc; ++i ){
char const * arg = fcli.argv[i];
char const * x;
char const * vp = NULL;
if(!arg || ('-' != *arg)) continue;
rc = 0;
x = arg+1;
if('-' == *x) { ++x;}
if(0 != fsl_strncmp(x, opt, optLen)) continue;
if(!value){
if(x[optLen]) continue /* not exact match */;
/* Treat this as a boolean. */
rc = 1;
++remove;
break;
}else{
/* -FLAG VALUE or -FLAG=VALUE */
if(x[optLen] == '='){
rc = 1;
vp = x+optLen+1;
++remove;
}
else if(x[optLen]) continue /* not an exact match */;
else if(i<(fcli.argc-1)){ /* -FLAG VALUE */
rc = 1;
vp = fcli.argv[i+1];
remove += 2;
}
else{
/*
--FLAG is expecting VALUE but we're at end of argv. Leave
--FLAG in the args and report this as "not found."
*/
rc = 0;
assert(!remove);
}
if(rc){
*value = vp;
}
break;
}
}
if(remove>0){
int x;
for( x = 0; x < remove; ++x ){
fcli.argv[i+x] = NULL/*memory ownership==>FCliFree*/;
}
for( ; i < fcli.argc; ++i ){
fcli.argv[i] = fcli.argv[i+remove];
}
fcli.argc -= remove;
fcli.argv[i] = NULL;
}
return rc;
}
char fcli_flag2(char const * shortOpt,
char const * longOpt,
const char ** value){
char const rc = fcli_flag(shortOpt, value);
return (rc || !longOpt) ? rc : fcli_flag(longOpt, value);
}
char fcli_flag_or_arg(char const * shortOpt,
char const * longOpt,
const char ** value){
char rc = fcli_flag(shortOpt, value);
if(!rc){
rc = fcli_flag(longOpt, value);
if(!rc && value){
const char * arg = fcli_next_arg(1);
if(arg){
rc = 1;
*value = arg;
}
}
}
return rc;
}
/**
We copy fsl_lib_configurable.allocator as a base allocator.
*/
static fsl_allocator fslAllocOrig;
/**
Proxies fslAllocOrig() and abort()s on OOM conditions.
*/
static void * fsl_realloc_f_failing(void * state, void * mem, fsl_size_t n){
void * rv = fslAllocOrig.f(fslAllocOrig.state, mem, n);
if(n && !rv){
fprintf(stderr,"\nfcli: OUT OF MEMORY\n");
fflush(stderr);
abort();
}
return rv;
}
/**
Replacement for fsl_memory_allocator() which abort()s on OOM.
Why? Because fossil(1) has shown how much that can simplify error
checking in an allocates-often API.
*/
static const fsl_allocator fcli_allocator = {
fsl_realloc_f_failing,
NULL/*state*/
};
int fcli_setup(int argc, char * const * argv ){
static char once = 0;
fsl_cx * f = NULL;
int rc = 0;
fsl_cx_init_opt init = fsl_cx_init_opt_empty;
if(once){
fprintf(stderr,"MISUSE: fcli_setup() must "
"not be called more than once.");
return FSL_RC_MISUSE;
}
init.config.sqlPrint = 1;
once = 1;
fslAllocOrig = fsl_lib_configurable.allocator;
fsl_lib_configurable.allocator = fcli_allocator
/* This MUST be done BEFORE the fsl API allocates
ANY memory!
*/;
atexit(fcli_shutdown);
rc = fcli_process_args(argc, argv);
if(rc) return rc;
if(fcli.transient.helpRequested){
/* Do this last so that we can get the default user name and such
for display in the help text. */
fcli_help();
rc = FSL_RC_BREAK;
}
else{
if(fcli.config.outputer.out){
init.output = fcli.config.outputer;
fcli.config.outputer = fsl_outputer_empty
/* To avoid any confusion about ownership */;
}else{
init.output = fsl_outputer_FILE;
init.output.state.state = stdout;
}
init.config.traceSql = fcli.config.traceSql;
rc = fsl_cx_init( &f, &init );
fcli.f = f;
FCLI_V3(("Initialized fsl_cx @0x%p. rc=%s\n",
(void const *)f, fsl_rc_cstr(rc)));
if(!rc){
#if 0
if(fcli.transient.gmtTime){
fsl_cx_flag_set(f, FSL_CX_F_LOCALTIME_GMT, 1);
}
#endif
if(fcli.checkoutDir || fcli.transient.repoDbArg){
rc = fcli_open();
if(!fcli.transient.repoDbArg && fcli.checkoutDir
&& (FSL_RC_NOT_FOUND == rc)){
/* If [it looks like] we tried an implicit checkout-open but
didn't find one, suppress the error.
*/
rc = 0;
fcli_err_reset();
}
}
}
}
if(!rc){
char const * userName = NULL;
fcli_flag2("user", "U", &userName);
if(userName){
fsl_cx_user_set(f, userName);
}else if(!fsl_cx_user_get(f)){
char * u = fsl_guess_user_name();
fsl_cx_user_set(f, u);
fsl_free(u);
}
}
return rc;
}
void fcli_help_global_options(){
char const * user = fsl_cx_user_get(fcli.f);
puts("Global fcli options and features:\n");
puts("\t--help|-? or the word 'help' as the first non-flag argument "
"triggers this help message.\n");
puts("\t--dry-run enables 'dry-run' mode for some (not all) apps\n");
#if 0
puts("\t--gmt sets the FSL_CX_F_LOCALTIME_GMT flag.\n");
#endif
puts("\t--no-checkout|-C supresses the automatic attempt "
"to open a checkout under the current directory. "
"This is implied by -R, to avoid opening a "
"mismatched checkout/repo combination.\n");
puts("\t--repo|-R=DBFILE specifies a repository "
"db to open.\n");
puts("\t--trace-sql|-S enables SQL tracing.\n");
printf("\t--user|-U=name [current=%s] sets the default Fossil "
"user for operations which need one.\n\n", user);
puts("\t--verbose|-V enables verbose mode "
"(possibly generating extra output). "
"Use multiple times for more verbosity. "
"Shortcuts: -VV and -VVV.\n");
puts("All flags may be preceeded by - or -- and "
"flags taking values may be in the form -FLAG=VALUE "
"or -FLAG VALUE. Flags not taking values are "
"treated as booleans.\n");
}
int fcli_err_report2(char clear, char const * file, int line){
int errRc = 0;
if(fcli.f){
char const * msg = NULL;
errRc = fsl_cx_err_get( fcli.f, &msg, NULL );
if(errRc || msg){
if(fcli.verbose>0){
fcli_printf("%s %s:%d: ERROR #%d (%s): %s\n",
fcli.appName,
file, line, errRc, fsl_rc_cstr(errRc), msg);
}else{
fcli_printf("%s: ERROR #%d (%s): %s\n",
fcli.appName, errRc, fsl_rc_cstr(errRc), msg);
}
}
if(clear) fcli_err_reset();
}
return errRc;
}
const char * fcli_next_arg(int remove){
const char * rc = (fcli.argc>0) ? fcli.argv[0] : NULL;
if(rc && remove){
int i;
--fcli.argc;
for(i = 0; i < fcli.argc; ++i){
fcli.argv[i] = fcli.argv[i+1];
}
fcli.argv[fcli.argc] = NULL/*owned by FCliFree*/;
}
return rc;
}
int fcli_has_unused_flags(int outputError){
int i;
for( i = 0; i < fcli.argc; ++i ){
char const * arg = fcli.argv[i];
if('-'==*arg){
fsl_cx_err_set(fcli.f, FSL_RC_MISUSE,
"Unhandled/unknown flag: %s",
arg);
if(outputError){
fcli_err_report(0);
}
return 1;
}
}
return 0;
}
int fcli_err_set(int code, char const * fmt, ...){
int rc;
va_list va;
va_start(va, fmt);
rc = fsl_cx_err_setv(fcli.f, code, fmt, va);
va_end(va);
return rc;
}
int fcli_end_of_main(int mainRc){
return (fcli_err_report(1)||mainRc) ? EXIT_FAILURE : EXIT_SUCCESS;
}
int fcli_dispatch_commands( FossilCommand const * cmd,
char reportErrors){
int rc = 0;
const char * arg = fcli_next_arg(0);
FossilCommand const * orig = cmd;
if(!arg){
return fcli_err_set(FSL_RC_MISUSE,
"Missing command argument. Try --help.");
}
assert(fcli.f);
for( ; arg && cmd->name; ++cmd ){
if(0==fsl_strcmp(arg,cmd->name)){
if(!cmd->f){
rc = fcli_err_set(FSL_RC_NYI,
"Command [%s] has no "
"callback function.");
}else{
fcli_next_arg(1);
rc = cmd->f();
}
break;
}
}
if(!cmd->name){
fsl_buffer msg = fsl_buffer_empty;
int rc2;
if(!arg){
rc2 = FSL_RC_MISUSE;
fsl_buffer_appendf(&msg, "No command provided.");
}else{
rc2 = FSL_RC_NOT_FOUND;
fsl_buffer_appendf(&msg, "Command not found: %s.",arg);
}
fsl_buffer_appendf(&msg, " Available commands: ");
cmd = orig;
for( ; cmd && cmd->name; ++cmd ){
fsl_buffer_appendf( &msg, "%s%s",
(cmd==orig) ? "" : ", ",
cmd->name);
}
rc = fcli_err_set(rc2, "%b", &msg);
fsl_buffer_clear(&msg);
}
if(rc && reportErrors){
fcli_err_report(0);
}
return rc;
}
void fcli_fax(void * mem){
if(mem){
fsl_list_append( &FCliFree.list, mem );
}
}
#undef FCLI_V3
#undef fcli_empty_m