/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=2 et sw=2 tw=80: */ /* Copyright (c) 2013 D. Richard Hipp 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. Author contact information: drh@hwaci.com http://www.hwaci.com/drh/ ***************************************************************************** */ #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 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.transient.repoDbArg); for( rc = 0; rc < fcli.argc; ++rc ){ fsl_free(fcli.argv[rc]); } fsl_free(fcli.argv); 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 ); } } 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; } /** 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])){ fsl_free(fcli_next_arg(1)) /* strip argument */; fcli.transient.helpRequested = 2; } return rc; } char fcli_flag(char const * opt, 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 = fsl_strdup(vp); break; } } if(remove>0){ int x; for( x = 0; x < remove; ++x ){ fsl_free(fcli.argv[i+x]); fcli.argv[i+x] = NULL; } 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, 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, char ** value){ char rc = fcli_flag(shortOpt, value); if(!rc){ rc = fcli_flag(longOpt, value); if(!rc && value){ 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 * userName = NULL; fcli_flag2("user", "U", &userName); if(userName){ fsl_cx_user_set(f, userName); }else if(!fsl_cx_user_get(f)){ userName = fsl_guess_user_name(); fsl_cx_user_set(f, userName); } fsl_free(userName); } 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; } char * fcli_next_arg(char take){ char * rc = (fcli.argc>0) ? fcli.argv[0] : NULL; if(rc && take){ int i; --fcli.argc; for(i = 0; i < fcli.argc; ++i){ fcli.argv[i] = fcli.argv[i+1]; } fcli.argv[fcli.argc] = NULL; } return rc; } char fcli_has_unused_flags(char 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; 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{ fsl_free(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; } #undef FCLI_V3 #undef fcli_empty_m