/****************************************************************************/ /* argv.h: a thread-safe argument vector parser and usage screen generator */ /* Copyright (C) 2015--2024 SysDeer Technologies, LLC */ /* Released under GPLv2 and GPLv3; see COPYING.APIMAGIC */ /****************************************************************************/ #ifndef ARGV_H #define ARGV_H #include #include #include #include #include #include #include #define ARGV_VERBOSITY_NONE 0x00 #define ARGV_VERBOSITY_ERRORS 0x01 #define ARGV_VERBOSITY_STATUS 0x02 #define ARGV_CLONE_VECTOR 0x80 #ifndef ARGV_TAB_WIDTH #define ARGV_TAB_WIDTH 8 #endif /*******************************************/ /* */ /* support of hybrid options */ /* ------------------------- */ /* hybrid options are very similar to */ /* long options, yet are prefixed by */ /* a single dash rather than two */ /* (i.e. -std, -isystem). */ /* hybrid options are supported by this */ /* driver for compatibility with legacy */ /* tools; note, however, that the use */ /* of hybrid options should be strongly */ /* discouraged due to the limitations */ /* they impose on short options (for */ /* example, a driver implementing -std */ /* may not provide -s as a short option */ /* that takes an arbitrary value). */ /* */ /* SPACE: -hybrid VALUE (i.e. -MF file) */ /* EQUAL: -hybrid=VALUE (i.e. -std=c99) */ /* COMMA: -hybrid,VALUE (i.e. -Wl,) */ /* ONLY: -opt accepted, --opt rejected */ /* JOINED: -optVALUE */ /* */ /*******************************************/ #define ARGV_OPTION_HYBRID_NONE 0x00 #define ARGV_OPTION_HYBRID_ONLY 0x01 #define ARGV_OPTION_HYBRID_SPACE 0x02 #define ARGV_OPTION_HYBRID_EQUAL 0x04 #define ARGV_OPTION_HYBRID_COMMA 0x08 #define ARGV_OPTION_HYBRID_JOINED 0x10 #define ARGV_OPTION_HYBRID_CIRCUS (ARGV_OPTION_HYBRID_SPACE \ | ARGV_OPTION_HYBRID_JOINED) #define ARGV_OPTION_HYBRID_DUAL (ARGV_OPTION_HYBRID_SPACE \ | ARGV_OPTION_HYBRID_EQUAL) #define ARGV_OPTION_HYBRID_SWITCH (ARGV_OPTION_HYBRID_ONLY \ | ARGV_OPTION_HYBRID_SPACE \ | ARGV_OPTION_HYBRID_EQUAL \ | ARGV_OPTION_HYBRID_COMMA \ | ARGV_OPTION_HYBRID_JOINED) enum argv_optarg { ARGV_OPTARG_NONE, ARGV_OPTARG_REQUIRED, ARGV_OPTARG_OPTIONAL, }; enum argv_mode { ARGV_MODE_SCAN, ARGV_MODE_COPY, }; enum argv_error { ARGV_ERROR_OK, ARGV_ERROR_INTERNAL, ARGV_ERROR_SHORT_OPTION, ARGV_ERROR_LONG_OPTION, ARGV_ERROR_VENDOR_OPTION, ARGV_ERROR_OPTARG_NONE, ARGV_ERROR_OPTARG_REQUIRED, ARGV_ERROR_OPTARG_PARADIGM, ARGV_ERROR_HYBRID_NONE, ARGV_ERROR_HYBRID_ONLY, ARGV_ERROR_HYBRID_SPACE, ARGV_ERROR_HYBRID_EQUAL, ARGV_ERROR_HYBRID_COMMA, }; struct argv_option { const char * long_name; const char short_name; int tag; enum argv_optarg optarg; int flags; const char * paradigm; const char * argname; const char * description; }; struct argv_entry { const char * arg; int tag; bool fopt; bool fval; bool fnoscan; enum argv_error errcode; }; struct argv_meta { char ** argv; struct argv_entry * entries; }; struct argv_ctx { int flags; int mode; int nentries; intptr_t unitidx; intptr_t erridx; enum argv_error errcode; const char * errch; const struct argv_option * erropt; const char * program; }; #ifdef ARGV_DRIVER struct argv_meta_impl { char ** argv; char * strbuf; struct argv_meta meta; }; static int argv_optv_init( const struct argv_option[], const struct argv_option **); static const char * argv_program_name(const char *); static void argv_usage( int fd, const char * header, const struct argv_option **, const char * mode); static void argv_usage_plain( int fd, const char * header, const struct argv_option **, const char * mode); static struct argv_meta * argv_get( char **, const struct argv_option **, int flags, int fd); static void argv_free(struct argv_meta *); #ifndef argv_dprintf #define argv_dprintf dprintf #endif /*------------------------------------*/ /* implementation of static functions */ /*------------------------------------*/ static int argv_optv_init( const struct argv_option options[], const struct argv_option ** optv) { const struct argv_option * option; int i; for (option=options,i=0; option->long_name || option->short_name; option++) optv[i++] = option; optv[i] = 0; return i; } static const struct argv_option * argv_short_option( const char * ch, const struct argv_option ** optv, struct argv_entry * entry) { const struct argv_option * option; for (; *optv; optv++) { option = *optv; if (option->short_name == *ch) { entry->tag = option->tag; entry->fopt = true; return option; } } return 0; } static const struct argv_option * argv_long_option( const char * ch, const struct argv_option ** optv, struct argv_entry * entry) { const struct argv_option * option; const char * arg; size_t len; for (; *optv; optv++) { option = *optv; len = option->long_name ? strlen(option->long_name) : 0; if (len && !(strncmp(option->long_name,ch,len))) { arg = ch + len; if (!*arg || (*arg == '=') || (option->flags & ARGV_OPTION_HYBRID_JOINED) || ((option->flags & ARGV_OPTION_HYBRID_COMMA) && (*arg == ','))) { entry->tag = option->tag; entry->fopt = true; return option; } } } return 0; } static inline bool is_short_option(const char * arg) { return (arg[0]=='-') && arg[1] && (arg[1]!='-'); } static inline bool is_long_option(const char * arg) { return (arg[0]=='-') && (arg[1]=='-') && arg[2]; } static inline bool is_last_option(const char * arg) { return (arg[0]=='-') && (arg[1]=='-') && !arg[2]; } static inline bool is_hybrid_option( const char * arg, const struct argv_option ** optv) { const struct argv_option * option; struct argv_entry entry; if (!is_short_option(arg)) return false; if (!(option = argv_long_option(++arg,optv,&entry))) return false; if (!(option->flags & ARGV_OPTION_HYBRID_SWITCH)) if (argv_short_option(arg,optv,&entry)) return false; return true; } static inline bool is_arg_in_paradigm(const char * arg, const char * paradigm) { size_t len; const char * ch; for (ch=paradigm,len=strlen(arg); ch; ) { if (!strncmp(arg,ch,len)) { if (!*(ch += len)) return true; else if (*ch == '|') return true; } if ((ch = strchr(ch,'|'))) ch++; } return false; } static inline const struct argv_option * option_from_tag( const struct argv_option ** optv, int tag) { for (; *optv; optv++) if (optv[0]->tag == tag) return optv[0]; return 0; } static void argv_scan( char ** argv, const struct argv_option ** optv, struct argv_ctx * ctx, struct argv_meta * meta) { char ** parg; const char * ch; const char * val; const struct argv_option * option; struct argv_entry entry; struct argv_entry * mentry; enum argv_error ferr; bool fval; bool fnext; bool fshort; bool fhybrid; bool fnoscan; parg = &argv[1]; ch = *parg; ferr = ARGV_ERROR_OK; fshort = false; fnoscan = false; fval = false; mentry = meta ? meta->entries : 0; ctx->unitidx = 0; ctx->erridx = 0; while (ch && (ferr == ARGV_ERROR_OK)) { option = 0; fhybrid = false; if (fnoscan) fval = true; else if (is_last_option(ch)) fnoscan = true; else if (!fshort && is_hybrid_option(ch,optv)) fhybrid = true; if (!fnoscan && !fhybrid && (fshort || is_short_option(ch))) { if (!fshort) ch++; if ((option = argv_short_option(ch,optv,&entry))) { if (ch[1]) { ch++; fnext = false; fshort = (option->optarg == ARGV_OPTARG_NONE); } else { parg++; ch = *parg; fnext = true; fshort = false; } if (option->optarg == ARGV_OPTARG_NONE) { if (!fnext && ch && (*ch == '-')) { ferr = ARGV_ERROR_OPTARG_NONE; } else { fval = false; } } else if (!fnext) { fval = true; } else if (option->optarg == ARGV_OPTARG_REQUIRED) { if (ch && is_short_option(ch)) ferr = ARGV_ERROR_OPTARG_REQUIRED; else if (ch && is_long_option(ch)) ferr = ARGV_ERROR_OPTARG_REQUIRED; else if (ch && is_last_option(ch)) ferr = ARGV_ERROR_OPTARG_REQUIRED; else if (ch) fval = true; else ferr = ARGV_ERROR_OPTARG_REQUIRED; } else { /* ARGV_OPTARG_OPTIONAL */ if (ch && is_short_option(ch)) fval = false; else if (ch && is_long_option(ch)) fval = false; else if (ch && is_last_option(ch)) fval = false; else if (fnext) fval = false; else fval = ch; } } else { if ((ch == &parg[0][1]) && (ch[0] == 'W') && ch[1]) { ferr = ARGV_ERROR_VENDOR_OPTION; } else { ferr = ARGV_ERROR_SHORT_OPTION; } } } else if (!fnoscan && (fhybrid || is_long_option(ch))) { ch += (fhybrid ? 1 : 2); if ((option = argv_long_option(ch,optv,&entry))) { val = ch + strlen(option->long_name); /* val[0] is either '=' (or ',') or '\0' */ if (!val[0]) { parg++; ch = *parg; } if (fhybrid && !(option->flags & ARGV_OPTION_HYBRID_SWITCH)) ferr = ARGV_ERROR_HYBRID_NONE; else if (!fhybrid && (option->flags & ARGV_OPTION_HYBRID_ONLY)) ferr = ARGV_ERROR_HYBRID_ONLY; else if (option->optarg == ARGV_OPTARG_NONE) { if (val[0]) { ferr = ARGV_ERROR_OPTARG_NONE; ctx->errch = val + 1; } else fval = false; } else if (val[0] && (option->flags & ARGV_OPTION_HYBRID_JOINED)) { fval = true; ch = val; } else if (fhybrid && !val[0] && !(option->flags & ARGV_OPTION_HYBRID_SPACE)) ferr = ARGV_ERROR_HYBRID_SPACE; else if (fhybrid && (val[0]=='=') && !(option->flags & ARGV_OPTION_HYBRID_EQUAL)) ferr = ARGV_ERROR_HYBRID_EQUAL; else if (fhybrid && (val[0]==',') && !(option->flags & ARGV_OPTION_HYBRID_COMMA)) ferr = ARGV_ERROR_HYBRID_COMMA; else if (!fhybrid && (val[0]==',')) ferr = ARGV_ERROR_HYBRID_COMMA; else if (val[0] && !val[1]) ferr = ARGV_ERROR_OPTARG_REQUIRED; else if (val[0] && val[1]) { fval = true; ch = ++val; } else if (option->optarg == ARGV_OPTARG_REQUIRED) { if (!val[0] && !*parg) ferr = ARGV_ERROR_OPTARG_REQUIRED; else if (*parg && is_short_option(*parg)) ferr = ARGV_ERROR_OPTARG_REQUIRED; else if (*parg && is_long_option(*parg)) ferr = ARGV_ERROR_OPTARG_REQUIRED; else if (*parg && is_last_option(*parg)) ferr = ARGV_ERROR_OPTARG_REQUIRED; else fval = true; } else { /* ARGV_OPTARG_OPTIONAL */ fval = val[0]; } } else ferr = ARGV_ERROR_LONG_OPTION; } if (ferr == ARGV_ERROR_OK) if (option && fval && option->paradigm) if (!is_arg_in_paradigm(ch,option->paradigm)) ferr = ARGV_ERROR_OPTARG_PARADIGM; if (ferr == ARGV_ERROR_OK) if (!option && !ctx->unitidx) ctx->unitidx = parg - argv; if (ferr != ARGV_ERROR_OK) { ctx->errcode = ferr; ctx->errch = ctx->errch ? ctx->errch : ch; ctx->erropt = option; ctx->erridx = parg - argv; return; } else if (ctx->mode == ARGV_MODE_SCAN) { if (!fnoscan) ctx->nentries++; else if (fval) ctx->nentries++; if (fval || !option) { parg++; ch = *parg; } } else if (ctx->mode == ARGV_MODE_COPY) { if (fnoscan) { if (fval) { mentry->arg = ch; mentry->fnoscan = true; mentry++; } parg++; ch = *parg; } else if (option) { mentry->arg = fval ? ch : 0; mentry->tag = option->tag; mentry->fopt = true; mentry->fval = fval; mentry++; if (fval) { parg++; ch = *parg; } } else { mentry->arg = ch; mentry++; parg++; ch = *parg; } } } } static const char * argv_program_name(const char * program_path) { const char * ch; if (program_path) { if ((ch = strrchr(program_path,'/'))) return *(++ch) ? ch : 0; if ((ch = strrchr(program_path,'\\'))) return *(++ch) ? ch : 0; } return program_path; } static void argv_show_error(int fd, struct argv_ctx * ctx) { const char * src; char * dst; char * cap; char opt_vendor_buf[256]; char opt_short_name[2] = {0,0}; if (ctx->erropt && ctx->erropt->short_name) opt_short_name[0] = ctx->erropt->short_name; argv_dprintf(fd,"%s: error: ",ctx->program); switch (ctx->errcode) { case ARGV_ERROR_SHORT_OPTION: argv_dprintf(fd,"'-%c' is not a valid short option\n",*ctx->errch); break; case ARGV_ERROR_LONG_OPTION: argv_dprintf(fd,"'--%s' is not a valid long option\n",ctx->errch); break; case ARGV_ERROR_VENDOR_OPTION: src = ctx->errch; dst = opt_vendor_buf; cap = &opt_vendor_buf[sizeof(opt_vendor_buf)]; for (; src && *src && dsterrch, opt_short_name[0] ? "-" : "", opt_short_name, opt_short_name[0] ? "," : "", ctx->erropt->long_name ? "--" : "", ctx->erropt->long_name); break; case ARGV_ERROR_OPTARG_REQUIRED: argv_dprintf(fd,"option [%s%s%s%s%s] requires %s %s%s%s\n", opt_short_name[0] ? "-" : "", opt_short_name, opt_short_name[0] ? "," : "", ctx->erropt->long_name ? (ctx->erropt->flags & ARGV_OPTION_HYBRID_ONLY) ? "-" : "--" : "", ctx->erropt->long_name, ctx->erropt->paradigm ? "one of the following values:" : "a value", ctx->erropt->paradigm ? "{" : "", ctx->erropt->paradigm ? ctx->erropt->paradigm : "", ctx->erropt->paradigm ? "}" : ""); break; case ARGV_ERROR_OPTARG_PARADIGM: argv_dprintf(fd,"'%s' is not a valid option value for [%s%s%s%s%s]={%s}\n", ctx->errch, opt_short_name[0] ? "-" : "", opt_short_name, opt_short_name[0] ? "," : "", ctx->erropt->long_name ? "--" : "", ctx->erropt->long_name, ctx->erropt->paradigm); break; case ARGV_ERROR_HYBRID_NONE: argv_dprintf(fd,"-%s is not a synonym for --%s\n", ctx->erropt->long_name, ctx->erropt->long_name); break; case ARGV_ERROR_HYBRID_ONLY: argv_dprintf(fd,"--%s is not a synonym for -%s\n", ctx->erropt->long_name, ctx->erropt->long_name); break; case ARGV_ERROR_HYBRID_SPACE: case ARGV_ERROR_HYBRID_EQUAL: case ARGV_ERROR_HYBRID_COMMA: argv_dprintf(fd,"-%s: illegal value assignment; valid syntax is " "-%s%sVAL\n", ctx->erropt->long_name, ctx->erropt->long_name, (ctx->erropt->flags & ARGV_OPTION_HYBRID_SPACE) ? " " : (ctx->erropt->flags & ARGV_OPTION_HYBRID_EQUAL) ? "=" : (ctx->erropt->flags & ARGV_OPTION_HYBRID_COMMA) ? "," : ""); break; case ARGV_ERROR_INTERNAL: argv_dprintf(fd,"internal error"); break; default: break; } } static void argv_show_status( int fd, const struct argv_option ** optv, struct argv_ctx * ctx, struct argv_meta * meta) { int argc; char ** argv; struct argv_entry * entry; const struct argv_option * option; char short_name[2] = {0}; const char * space = ""; (void)ctx; argv_dprintf(fd,"\n\nconcatenated command line:\n"); for (argv=meta->argv; *argv; argv++) { argv_dprintf(fd,"%s%s",space,*argv); space = " "; } argv_dprintf(fd,"\n\nargument vector:\n"); for (argc=0,argv=meta->argv; *argv; argc++,argv++) argv_dprintf(fd,"argv[%d]: %s\n",argc,*argv); argv_dprintf(fd,"\n\nparsed entries:\n"); for (entry=meta->entries; entry->arg || entry->fopt; entry++) if (entry->fopt) { option = option_from_tag(optv,entry->tag); short_name[0] = option->short_name; if (entry->fval) argv_dprintf(fd,"[-%s,--%s] := %s\n", short_name,option->long_name,entry->arg); else argv_dprintf(fd,"[-%s,--%s]\n", short_name,option->long_name); } else argv_dprintf(fd," := %s\n",entry->arg); argv_dprintf(fd,"\n\n"); } static struct argv_meta * argv_free_impl(struct argv_meta_impl * imeta) { if (imeta->argv) free(imeta->argv); if (imeta->strbuf) free(imeta->strbuf); if (imeta->meta.entries) free(imeta->meta.entries); free(imeta); return 0; } static struct argv_meta * argv_alloc(char ** argv, struct argv_ctx * ctx) { struct argv_meta_impl * imeta; char ** vector; char * dst; size_t size; int argc; int i; if (!(imeta = calloc(1,sizeof(*imeta)))) return 0; if (ctx->flags & ARGV_CLONE_VECTOR) { for (vector=argv,argc=0,size=0; *vector; vector++) { size += strlen(*vector) + 1; argc++; } if (!(imeta->argv = calloc(argc+1,sizeof(char *)))) return argv_free_impl(imeta); else if (!(imeta->strbuf = calloc(1,size+1))) return argv_free_impl(imeta); for (i=0,dst=imeta->strbuf; iargv[i] = dst; dst += strlen(dst)+1; } imeta->meta.argv = imeta->argv; } else imeta->meta.argv = argv; if (!(imeta->meta.entries = calloc( ctx->nentries+1, sizeof(struct argv_entry)))) return argv_free_impl(imeta); else return &imeta->meta; } static struct argv_meta * argv_get( char ** argv, const struct argv_option ** optv, int flags, int fd) { struct argv_meta * meta; struct argv_ctx ctx = {flags,ARGV_MODE_SCAN,0,0,0,0,0,0,0}; argv_scan(argv,optv,&ctx,0); if (ctx.errcode != ARGV_ERROR_OK) { ctx.program = argv_program_name(argv[0]); if (ctx.flags & ARGV_VERBOSITY_ERRORS) argv_show_error(fd,&ctx); return 0; } if (!(meta = argv_alloc(argv,&ctx))) return 0; ctx.mode = ARGV_MODE_COPY; argv_scan(meta->argv,optv,&ctx,meta); if (ctx.errcode != ARGV_ERROR_OK) { ctx.program = argv[0]; ctx.errcode = ARGV_ERROR_INTERNAL; argv_show_error(fd,&ctx); argv_free(meta); return 0; } if (ctx.flags & ARGV_VERBOSITY_STATUS) argv_show_status(fd,optv,&ctx,meta); return meta; } static void argv_free(struct argv_meta * xmeta) { struct argv_meta_impl * imeta; uintptr_t addr; if (xmeta) { addr = (uintptr_t)xmeta - offsetof(struct argv_meta_impl,meta); imeta = (struct argv_meta_impl *)addr; argv_free_impl(imeta); } } static void argv_usage_impl( int fd, const char * header, const struct argv_option ** options, const char * mode, int fcolor) { const struct argv_option ** optv; const struct argv_option * option; bool fshort,flong,fboth; size_t len,optlen,desclen; char cache; char * prefix; char * desc; char * mark; char * cap; char description[4096]; char optstr [72]; const size_t optcap = 64; const size_t width = 80; const char indent[] = " "; const char creset[] = "\x1b[0m"; const char cbold [] = "\x1b[1m"; const char cgreen[] = "\x1b[32m"; const char cblue [] = "\x1b[34m"; const char ccyan [] = "\x1b[36m"; const char * color = ccyan; (void)argv_usage; (void)argv_usage_plain; fshort = mode ? !strcmp(mode,"short") : 0; flong = fshort ? 0 : mode && !strcmp(mode,"long"); fboth = !fshort && !flong; if (fcolor) argv_dprintf(fd,"%s%s",cbold,cgreen); if (header) argv_dprintf(fd,"%s",header); for (optlen=0,optv=options; *optv; optv++) { option = *optv; /* indent + comma */ len = fboth ? sizeof(indent) + 1 : sizeof(indent); /* -o */ if (fshort || fboth) len += option->short_name ? 2 : 0; /* --option */ if (flong || fboth) len += option->long_name ? 2 + strlen(option->long_name) : 0; /* optlen */ if (len > optlen) optlen = len; } if (optlen >= optcap) { argv_dprintf(fd, "Option strings exceed %zu characters, " "please generate the usage screen manually.\n", optcap); return; } optlen += ARGV_TAB_WIDTH; optlen &= (~(ARGV_TAB_WIDTH-1)); desclen = (optlen < width / 2) ? width - optlen : optlen; for (optv=options; *optv; optv++) { option = *optv; /* color */ if (fcolor) { color = (color == ccyan) ? cblue : ccyan; argv_dprintf(fd,color); } /* description, using either paradigm or argname if applicable */ snprintf(description,sizeof(description),option->description, option->paradigm ? option->paradigm : option->argname ? option->argname : ""); description[sizeof(description)-1] = 0; /* long/hybrid option prefix (-/--) */ prefix = option->flags & ARGV_OPTION_HYBRID_ONLY ? " -" : "--"; /* option string */ if (fboth && option->short_name && option->long_name) sprintf(optstr,"%s-%c,%s%s", indent,option->short_name,prefix,option->long_name); else if ((fshort || fboth) && option->short_name) sprintf(optstr,"%s-%c",indent,option->short_name); else if (flong && option->long_name) sprintf(optstr,"%s%s%s", indent,prefix,option->long_name); else if (fboth && option->long_name) sprintf(optstr,"%s %s%s", indent,prefix,option->long_name); else optstr[0] = 0; /* right-indented option buffer */ if (description[0]) { len = strlen(optstr); sprintf(&optstr[len],"%-*c",(int)(optlen-len),' '); } /* single line? */ if (optlen + strlen(description) < width) { argv_dprintf(fd,"%s%s\n",optstr,description); } else { desc = description; cap = desc + strlen(description); while (desc < cap) { mark = (desc + desclen >= cap) ? cap : desc + desclen; while (*mark && (mark > desc) && (*mark != ' ') && (*mark != '|') && (*mark != '\t') && (*mark != '\n')) mark--; if (mark == desc) { mark = (desc + desclen >= cap) ? cap : desc + desclen; cache = *mark; *mark = 0; } else if (*mark == '|') { cache = *mark; *mark = 0; } else { cache = 0; *mark = 0; } /* first line? */ if (desc == description) argv_dprintf(fd,"%s%s\n",optstr,desc); else argv_dprintf(fd,"%-*c %s\n", (*desc == '|') ? (int)(optlen+1) : (int)optlen, ' ',desc); if (cache) *mark = cache; else mark++; desc = mark; } } } if (fcolor) argv_dprintf(fd,creset); } static void argv_usage( int fd, const char * header, const struct argv_option ** options, const char * mode) { argv_usage_impl(fd,header,options,mode,isatty(fd)); } static void argv_usage_plain( int fd, const char * header, const struct argv_option ** options, const char * mode) { argv_usage_impl(fd,header,options,mode,0); } #endif #endif