"Fossies" - the Fresh Open Source Software Archive

Member "s-nail-14.9.19/src/mx/shexp.c" (26 Apr 2020, 71825 Bytes) of package /linux/misc/s-nail-14.9.19.tar.xz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) C and C++ source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. For more information about "shexp.c" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 14.9.18_vs_14.9.19.

    1 /*@ S-nail - a mail user agent derived from Berkeley Mail.
    2  *@ Shell "word", file- and other name expansions, incl. file globbing.
    3  *@ TODO v15: peek signal states while opendir/readdir/etc.
    4  *@ TODO "Magic solidus" used as path separator.
    5  *
    6  * Copyright (c) 2000-2004 Gunnar Ritter, Freiburg i. Br., Germany.
    7  * Copyright (c) 2012 - 2020 Steffen (Daode) Nurpmeso <steffen@sdaoden.eu>.
    8  * SPDX-License-Identifier: BSD-3-Clause TODO ISC
    9  */
   10 /*
   11  * Copyright (c) 1980, 1993
   12  *      The Regents of the University of California.  All rights reserved.
   13  *
   14  * Redistribution and use in source and binary forms, with or without
   15  * modification, are permitted provided that the following conditions
   16  * are met:
   17  * 1. Redistributions of source code must retain the above copyright
   18  *    notice, this list of conditions and the following disclaimer.
   19  * 2. Redistributions in binary form must reproduce the above copyright
   20  *    notice, this list of conditions and the following disclaimer in the
   21  *    documentation and/or other materials provided with the distribution.
   22  * 3. Neither the name of the University nor the names of its contributors
   23  *    may be used to endorse or promote products derived from this software
   24  *    without specific prior written permission.
   25  *
   26  * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
   27  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
   28  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
   29  * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
   30  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
   31  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
   32  * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
   33  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
   34  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
   35  * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
   36  * SUCH DAMAGE.
   37  */
   38 #undef su_FILE
   39 #define su_FILE shexp
   40 #define mx_SOURCE
   41 
   42 #ifndef mx_HAVE_AMALGAMATION
   43 # include "mx/nail.h"
   44 #endif
   45 
   46 #include <pwd.h>
   47 
   48 #ifdef mx_HAVE_FNMATCH
   49 # include <dirent.h>
   50 # include <fnmatch.h>
   51 #endif
   52 
   53 #include <su/cs.h>
   54 #include <su/sort.h>
   55 #include <su/mem.h>
   56 #include <su/utf.h>
   57 
   58 #include "mx/cmd.h"
   59 #include "mx/cmd-shortcut.h"
   60 #include "mx/iconv.h"
   61 #include "mx/ui-str.h"
   62 
   63 /* TODO fake */
   64 #include "su/code-in.h"
   65 
   66 /* POSIX says
   67  *   Environment variable names used by the utilities in the Shell and
   68  *   Utilities volume of POSIX.1-2008 consist solely of uppercase
   69  *   letters, digits, and the <underscore> ('_') from the characters
   70  *   defined in Portable Character Set and do not begin with a digit.
   71  *   Other characters may be permitted by an implementation;
   72  *   applications shall tolerate the presence of such names.
   73  * We do support the hyphen-minus "-" (except in last position for ${x[:]-y}).
   74  * We support some special parameter names for one-letter(++) variable names;
   75  * these have counterparts in the code that manages internal variables,
   76  * and some more special treatment below! */
   77 #define a_SHEXP_ISVARC(C) (su_cs_is_alnum(C) || (C) == '_' || (C) == '-')
   78 #define a_SHEXP_ISVARC_BAD1ST(C) (su_cs_is_digit(C)) /* (Assumed below!) */
   79 #define a_SHEXP_ISVARC_BADNST(C) ((C) == '-')
   80 
   81 #define a_SHEXP_ISENVVARC(C) (su_cs_is_alnum(C) || (C) == '_')
   82 #define a_SHEXP_ISENVVARC_BAD1ST(C) su_cs_is_digit(C)
   83 #define a_SHEXP_ISENVVARC_BADNST(C) (FAL0)
   84 
   85 enum a_shexp_quote_flags{
   86    a_SHEXP_QUOTE_NONE,
   87    a_SHEXP_QUOTE_ROUNDTRIP = 1u<<0, /* Result won't be consumed immediately */
   88 
   89    a_SHEXP_QUOTE_T_REVSOL = 1u<<8,  /* Type: by reverse solidus */
   90    a_SHEXP_QUOTE_T_SINGLE = 1u<<9,  /* Type: single-quotes */
   91    a_SHEXP_QUOTE_T_DOUBLE = 1u<<10, /* Type: double-quotes */
   92    a_SHEXP_QUOTE_T_DOLLAR = 1u<<11, /* Type: dollar-single-quotes */
   93    a_SHEXP_QUOTE_T_MASK = a_SHEXP_QUOTE_T_REVSOL | a_SHEXP_QUOTE_T_SINGLE |
   94          a_SHEXP_QUOTE_T_DOUBLE | a_SHEXP_QUOTE_T_DOLLAR,
   95 
   96    a_SHEXP_QUOTE__FREESHIFT = 16u
   97 };
   98 
   99 #ifdef mx_HAVE_FNMATCH
  100 struct a_shexp_glob_ctx{
  101    char const *sgc_patdat; /* Remaining pattern (at and below level) */
  102    uz sgc_patlen;
  103    struct n_string *sgc_outer; /* Resolved path up to this level */
  104    u32 sgc_flags;
  105    u8 sgc__pad[4];
  106 };
  107 
  108 struct a_shexp_glob_one_ctx{
  109    struct a_shexp_glob_ctx *sgoc_sgcp;
  110    struct a_shexp_glob_ctx *sgoc_new_sgcp;
  111    struct n_strlist **sgoc_slpp;
  112    uz sgoc_old_outer_len;
  113    sz sgoc_dt_type; /* Can be -1 even if mx_HAVE_DIRENT_TYPE */
  114    char const *sgoc_name;
  115 };
  116 #endif
  117 
  118 struct a_shexp_quote_ctx{
  119    struct n_string *sqc_store;   /* Result storage */
  120    struct str sqc_input;         /* Input data, topmost level */
  121    u32 sqc_cnt_revso;
  122    u32 sqc_cnt_single;
  123    u32 sqc_cnt_double;
  124    u32 sqc_cnt_dollar;
  125    enum a_shexp_quote_flags sqc_flags;
  126    u8 sqc__dummy[4];
  127 };
  128 
  129 struct a_shexp_quote_lvl{
  130    struct a_shexp_quote_lvl *sql_link; /* Outer level */
  131    struct str sql_dat;                 /* This level (has to) handle(d) */
  132    enum a_shexp_quote_flags sql_flags;
  133    u8 sql__dummy[4];
  134 };
  135 
  136 /* Locate the user's mailbox file (where new, unread mail is queued) */
  137 static char *a_shexp_findmail(char const *user, boole force);
  138 
  139 /* Expand ^~/? and ^~USER/? constructs.
  140  * Returns the completely resolved (maybe empty or identical to input)
  141  * n_autorec_alloc()ed string */
  142 static char *a_shexp_tilde(char const *s);
  143 
  144 /* Perform fnmatch(3).  May return NULL on error */
  145 static char *a_shexp_globname(char const *name, enum fexp_mode fexpm);
  146 #ifdef mx_HAVE_FNMATCH
  147 static boole a_shexp__glob(struct a_shexp_glob_ctx *sgcp,
  148                struct n_strlist **slpp);
  149 static char const *a_shexp__glob_one(struct a_shexp_glob_one_ctx *sgocp);
  150 static su_sz a_shexp__globsort(void const *cvpa, void const *cvpb);
  151 #endif
  152 
  153 /* Parse an input string and create a sh(1)ell-quoted result */
  154 static void a_shexp__quote(struct a_shexp_quote_ctx *sqcp,
  155                struct a_shexp_quote_lvl *sqlp);
  156 
  157 static char *
  158 a_shexp_findmail(char const *user, boole force){
  159    char *rv;
  160    char const *cp;
  161    NYD2_IN;
  162 
  163    if(!force){
  164       if((cp = ok_vlook(inbox)) != NULL && *cp != '\0'){
  165          /* _NFOLDER extra introduced to avoid % recursion loops */
  166          if((rv = fexpand(cp, FEXP_NSPECIAL | FEXP_NFOLDER | FEXP_NSHELL)
  167                ) != NULL)
  168             goto jleave;
  169          n_err(_("*inbox* expansion failed, using $MAIL/built-in: %s\n"), cp);
  170       }
  171       /* Heirloom compatibility: an IMAP *folder* becomes "%" */
  172 #ifdef mx_HAVE_IMAP
  173       else if(cp == NULL && !su_cs_cmp(user, ok_vlook(LOGNAME)) &&
  174             which_protocol(cp = n_folder_query(), FAL0, FAL0, NULL)
  175                == PROTO_IMAP){
  176          /* TODO Compat handling of *folder* with IMAP! */
  177          n_OBSOLETE("no more expansion of *folder* in \"%\": "
  178             "please set *inbox*");
  179          rv = savestr(cp);
  180          goto jleave;
  181       }
  182 #endif
  183 
  184       if((cp = ok_vlook(MAIL)) != NULL){
  185          rv = savestr(cp);
  186          goto jleave;
  187       }
  188    }
  189 
  190    /* C99 */{
  191       uz ulen, i;
  192 
  193       ulen = su_cs_len(user) +1;
  194       i = sizeof(VAL_MAIL) -1 + 1 + ulen;
  195 
  196       rv = n_autorec_alloc(i);
  197       su_mem_copy(rv, VAL_MAIL, (i = sizeof(VAL_MAIL) -1));
  198       rv[i] = '/';
  199       su_mem_copy(&rv[++i], user, ulen);
  200    }
  201 jleave:
  202    NYD2_OU;
  203    return rv;
  204 }
  205 
  206 static char *
  207 a_shexp_tilde(char const *s){
  208    struct passwd *pwp;
  209    uz nl, rl;
  210    char const *rp, *np;
  211    char *rv;
  212    NYD2_IN;
  213 
  214    if(*(rp = &s[1]) == '/' || *rp == '\0'){
  215       np = ok_vlook(HOME);
  216       rl = su_cs_len(rp);
  217    }else{
  218       if((rp = su_cs_find_c(np = rp, '/')) != NULL){
  219          nl = P2UZ(rp - np);
  220          np = savestrbuf(np, nl);
  221          rl = su_cs_len(rp);
  222       }else
  223          rl = 0;
  224 
  225       if((pwp = getpwnam(np)) == NULL){
  226          rv = savestr(s);
  227          goto jleave;
  228       }
  229       np = pwp->pw_dir;
  230    }
  231 
  232    nl = su_cs_len(np);
  233    rv = n_autorec_alloc(nl + 1 + rl +1);
  234    su_mem_copy(rv, np, nl);
  235    if(rl > 0){
  236       su_mem_copy(rv + nl, rp, rl);
  237       nl += rl;
  238    }
  239    rv[nl] = '\0';
  240 jleave:
  241    NYD2_OU;
  242    return rv;
  243 }
  244 
  245 static char *
  246 a_shexp_globname(char const *name, enum fexp_mode fexpm){
  247 #ifdef mx_HAVE_FNMATCH
  248    struct a_shexp_glob_ctx sgc;
  249    struct n_string outer;
  250    struct n_strlist *slp;
  251    char *cp;
  252    void *lofi_snap;
  253    NYD_IN;
  254 
  255    lofi_snap = n_lofi_snap_create();
  256 
  257    su_mem_set(&sgc, 0, sizeof sgc);
  258    /* C99 */{
  259       uz i;
  260 
  261       sgc.sgc_patlen = i = su_cs_len(name);
  262       sgc.sgc_patdat = cp = n_lofi_alloc(++i);
  263       su_mem_copy(cp, name, i);
  264       sgc.sgc_outer = n_string_book(n_string_creat(&outer), i);
  265    }
  266    sgc.sgc_flags = ((fexpm & FEXP_SILENT) != 0); /* a_shexp__glob():a_SILENT */
  267 
  268    slp = NULL;
  269    if(a_shexp__glob(&sgc, &slp))
  270       cp = (char*)0x1;
  271    else
  272       cp = NULL;
  273 
  274    n_string_gut(&outer);
  275 
  276    if(cp == NULL)
  277       goto jleave;
  278 
  279    if(slp == NULL){
  280       cp = UNCONST(char*,N_("File pattern does not match"));
  281       goto jerr;
  282    }else if(slp->sl_next == NULL)
  283       cp = savestrbuf(slp->sl_dat, slp->sl_len);
  284    else if(fexpm & FEXP_MULTIOK){
  285       struct n_strlist **sorta, *xslp;
  286       uz i, no, l;
  287 
  288       no = l = 0;
  289       for(xslp = slp; xslp != NULL; xslp = xslp->sl_next){
  290          ++no;
  291          l += xslp->sl_len + 1;
  292       }
  293 
  294       sorta = n_lofi_alloc(sizeof(*sorta) * no);
  295 
  296       no = 0;
  297       for(xslp = slp; xslp != NULL; xslp = xslp->sl_next)
  298          sorta[no++] = xslp;
  299       su_sort_shell_vpp(su_S(void const**,sorta), no, &a_shexp__globsort);
  300 
  301       cp = n_autorec_alloc(++l);
  302       l = 0;
  303       for(i = 0; i < no; ++i){
  304          xslp = sorta[i];
  305          su_mem_copy(&cp[l], xslp->sl_dat, xslp->sl_len);
  306          l += xslp->sl_len;
  307          cp[l++] = '\0';
  308       }
  309       cp[l] = '\0';
  310 
  311       /*n_lofi_free(sorta);*/
  312       n_pstate |= n_PS_EXPAND_MULTIRESULT;
  313    }else{
  314       cp = UNCONST(char*,N_("File pattern matches multiple results"));
  315       goto jerr;
  316    }
  317 
  318 jleave:
  319    n_lofi_snap_unroll(lofi_snap);
  320    NYD_OU;
  321    return cp;
  322 
  323 jerr:
  324    if(!(fexpm & FEXP_SILENT)){
  325       name = n_shexp_quote_cp(name, FAL0);
  326       n_err("%s: %s\n", V_(cp), name);
  327    }
  328    cp = NULL;
  329    goto jleave;
  330 
  331 #else /* mx_HAVE_FNMATCH */
  332    UNUSED(fexpm);
  333 
  334    if(!(fexpm & FEXP_SILENT))
  335       n_err(_("No filename pattern support (fnmatch(3) not available)\n"));
  336    return savestr(name);
  337 #endif
  338 }
  339 
  340 #ifdef mx_HAVE_FNMATCH
  341 static boole
  342 a_shexp__glob(struct a_shexp_glob_ctx *sgcp, struct n_strlist **slpp){
  343    /* a_SILENT == a_shexp_globname():((fexpm & FEXP_SILENT) != 0) */
  344    enum{a_SILENT = 1<<0, a_DEEP=1<<1};
  345 
  346    struct a_shexp_glob_ctx nsgc;
  347    struct a_shexp_glob_one_ctx sgoc;
  348    struct dirent *dep;
  349    DIR *dp;
  350    char const *ccp, *myp;
  351    NYD2_IN;
  352 
  353    /* We need some special treatment for the outermost level.
  354     * All along our way, normalize path separators */
  355    if(!(sgcp->sgc_flags & a_DEEP)){
  356       if(sgcp->sgc_patlen > 0 && sgcp->sgc_patdat[0] == '/'){
  357          myp = n_string_cp(n_string_push_c(sgcp->sgc_outer, '/'));
  358          do
  359             ++sgcp->sgc_patdat;
  360          while(--sgcp->sgc_patlen > 0 && sgcp->sgc_patdat[0] == '/');
  361       }else
  362          myp = "./";
  363    }else
  364       myp = n_string_cp(sgcp->sgc_outer);
  365 
  366    sgoc.sgoc_sgcp = sgcp;
  367    sgoc.sgoc_new_sgcp = &nsgc;
  368    sgoc.sgoc_slpp = slpp;
  369    sgoc.sgoc_old_outer_len = sgcp->sgc_outer->s_len;
  370 
  371    /* Separate current directory/pattern level from any possible remaining
  372     * pattern in order to be able to use it for fnmatch(3) */
  373    if((ccp = su_mem_find(sgcp->sgc_patdat, '/', sgcp->sgc_patlen)) == NULL)
  374       nsgc.sgc_patlen = 0;
  375    else{
  376       nsgc = *sgcp;
  377       nsgc.sgc_flags |= a_DEEP;
  378       sgcp->sgc_patlen = P2UZ((nsgc.sgc_patdat = &ccp[1]) -
  379             &sgcp->sgc_patdat[0]);
  380       nsgc.sgc_patlen -= sgcp->sgc_patlen;
  381 
  382       /* Trim solidus, everywhere */
  383       if(sgcp->sgc_patlen > 0){
  384          ASSERT(sgcp->sgc_patdat[sgcp->sgc_patlen -1] == '/');
  385          UNCONST(char*,sgcp->sgc_patdat)[--sgcp->sgc_patlen] = '\0';
  386       }
  387       while(nsgc.sgc_patlen > 0 && nsgc.sgc_patdat[0] == '/'){
  388          --nsgc.sgc_patlen;
  389          ++nsgc.sgc_patdat;
  390       }
  391    }
  392 
  393    /* Quickshot: cannot be a fnmatch(3) pattern? */
  394    if(sgcp->sgc_patlen == 0 ||
  395          su_cs_first_of(sgcp->sgc_patdat, "?*[") == su_UZ_MAX){
  396       dp = NULL;
  397       sgoc.sgoc_dt_type = -1;
  398       sgoc.sgoc_name = sgcp->sgc_patdat;
  399       if((ccp = a_shexp__glob_one(&sgoc)) == su_NIL ||
  400             ccp == R(char*,-1))
  401          goto jleave;
  402       goto jerr;
  403    }
  404 
  405    if((dp = opendir(myp)) == NULL){
  406       int err;
  407 
  408       switch((err = su_err_no())){
  409       case su_ERR_NOTDIR:
  410          ccp = N_("cannot access paths under non-directory");
  411          goto jerr;
  412       case su_ERR_NOENT:
  413          ccp = N_("path component of (sub)pattern non-existent");
  414          goto jerr;
  415       case su_ERR_NFILE:
  416       case su_ERR_MFILE:
  417          ccp = N_("file descriptor limit reached, cannot open directory");
  418          goto jerr;
  419       case su_ERR_ACCES:
  420          /* Special case: an intermediate directory may not be read, but we
  421           * possibly could dive into it? */
  422          if(sgcp->sgc_patlen > 0 &&
  423                su_cs_first_of(sgcp->sgc_patdat, "?*[") == su_UZ_MAX){
  424             sgoc.sgoc_dt_type = -1;
  425             sgoc.sgoc_name = sgcp->sgc_patdat;
  426             if((ccp = a_shexp__glob_one(&sgoc)) == su_NIL ||
  427                   ccp == R(char*,-1))
  428                goto jleave;
  429             goto jerr;
  430          }
  431          ccp = N_("file permission for file (sub)pattern denied");
  432          goto jerr;
  433       default:
  434          ccp = N_("cannot open path component as directory");
  435          goto jerr;
  436       }
  437    }
  438 
  439    /* As necessary, quote bytes in the current pattern TODO This will not
  440     * TODO truly work out in case the user would try to quote a character
  441     * TODO class, for example: in "\[a-z]" the "\" would be doubled!  For that
  442     * TODO to work out, we need the original user input or the shell-expression
  443     * TODO parse tree, otherwise we do not know what is desired! */
  444    /* C99 */{
  445       char *ncp;
  446       uz i;
  447       boole need;
  448 
  449       for(need = FAL0, i = 0, myp = sgcp->sgc_patdat; *myp != '\0'; ++myp)
  450          switch(*myp){
  451          case '\'': case '"': case '\\': case '$':
  452          case ' ': case '\t':
  453             need = TRU1;
  454             ++i;
  455             /* FALLTHRU */
  456          default:
  457             ++i;
  458             break;
  459          }
  460 
  461       if(need){
  462          ncp = n_lofi_alloc(i +1);
  463          for(i = 0, myp = sgcp->sgc_patdat; *myp != '\0'; ++myp)
  464             switch(*myp){
  465             case '\'': case '"': case '\\': case '$':
  466             case ' ': case '\t':
  467                ncp[i++] = '\\';
  468                /* FALLTHRU */
  469             default:
  470                ncp[i++] = *myp;
  471                break;
  472             }
  473          ncp[i] = '\0';
  474          myp = ncp;
  475       }else
  476          myp = sgcp->sgc_patdat;
  477    }
  478 
  479    while((dep = readdir(dp)) != su_NIL){
  480       switch(fnmatch(myp, dep->d_name, FNM_PATHNAME | FNM_PERIOD)){
  481       case 0:
  482          sgoc.sgoc_dt_type =
  483 #ifdef mx_HAVE_DIRENT_TYPE
  484                dep->d_type
  485 #else
  486                -1
  487 #endif
  488                ;
  489          sgoc.sgoc_name = dep->d_name;
  490          if((ccp = a_shexp__glob_one(&sgoc)) != su_NIL){
  491             if(ccp == R(char*,-1))
  492                goto jleave;
  493             goto jerr;
  494          }
  495          break;
  496       case FNM_NOMATCH:
  497          break;
  498       default:
  499          ccp = N_("fnmatch(3) cannot handle file (sub)pattern");
  500          goto jerr;
  501       }
  502    }
  503 
  504    ccp = NULL;
  505 jleave:
  506    if(dp != NULL)
  507       closedir(dp);
  508    NYD2_OU;
  509    return (ccp == NULL);
  510 
  511 jerr:
  512    if(!(sgcp->sgc_flags & a_SILENT)){
  513       char const *s2, *s3;
  514 
  515       if(sgcp->sgc_outer->s_len > 0){
  516          s2 = n_shexp_quote_cp(n_string_cp(sgcp->sgc_outer), FAL0);
  517          s3 = "/";
  518       }else
  519          s2 = s3 = n_empty;
  520 
  521       n_err("%s: %s%s%s\n", V_(ccp), s2, s3,
  522          n_shexp_quote_cp(sgcp->sgc_patdat, FAL0));
  523    }
  524    goto jleave;
  525 }
  526 
  527 static char const *
  528 a_shexp__glob_one(struct a_shexp_glob_one_ctx *sgocp){
  529    char const *rv;
  530    struct n_string *ousp;
  531    NYD2_IN;
  532 
  533    ousp = sgocp->sgoc_sgcp->sgc_outer;
  534 
  535    /* A match expresses the desire to recurse if there is more pattern */
  536    if(sgocp->sgoc_new_sgcp->sgc_patlen > 0){
  537       boole isdir;
  538 
  539       if(ousp->s_len > 0 && (ousp->s_len > 1 || ousp->s_dat[0] != '/'))
  540          ousp = n_string_push_c(ousp, '/');
  541       n_string_push_cp(ousp, sgocp->sgoc_name);
  542 
  543       isdir = FAL0;
  544       if(sgocp->sgoc_dt_type == -1)
  545 #ifdef mx_HAVE_DIRENT_TYPE
  546 Jstat:
  547 #endif
  548       {
  549          struct stat sb;
  550 
  551          if(stat(n_string_cp(ousp), &sb)){
  552             rv = N_("I/O error when querying file status");
  553             goto jleave;
  554          }else if(S_ISDIR(sb.st_mode))
  555             isdir = TRU1;
  556       }
  557 #ifdef mx_HAVE_DIRENT_TYPE
  558       else if(sgocp->sgoc_dt_type == DT_DIR)
  559          isdir = TRU1;
  560       else if(sgocp->sgoc_dt_type == DT_LNK ||
  561             sgocp->sgoc_dt_type == DT_UNKNOWN)
  562          goto Jstat;
  563 #endif
  564 
  565       /* TODO Recurse with current dir FD open, which could E[MN]FILE!
  566        * TODO Instead save away a list of such n_string's for later */
  567       if(isdir && !a_shexp__glob(sgocp->sgoc_new_sgcp, sgocp->sgoc_slpp))
  568          rv = R(char*,-1);
  569       else{
  570          n_string_trunc(ousp, sgocp->sgoc_old_outer_len);
  571          rv = su_NIL;
  572       }
  573    }else{
  574       struct n_strlist *slp;
  575       uz i, j;
  576 
  577       i = su_cs_len(sgocp->sgoc_name);
  578       j = (sgocp->sgoc_old_outer_len > 0)
  579             ? sgocp->sgoc_old_outer_len + 1 + i : i;
  580       slp = n_STRLIST_LOFI_ALLOC(j);
  581       *sgocp->sgoc_slpp = slp;
  582       sgocp->sgoc_slpp = &slp->sl_next;
  583       slp->sl_next = NULL;
  584       if((j = sgocp->sgoc_old_outer_len) > 0){
  585          su_mem_copy(&slp->sl_dat[0], ousp->s_dat, j);
  586          if(slp->sl_dat[j -1] != '/')
  587             slp->sl_dat[j++] = '/';
  588       }
  589       su_mem_copy(&slp->sl_dat[j], sgocp->sgoc_name, i);
  590       slp->sl_dat[j += i] = '\0';
  591       slp->sl_len = j;
  592       rv = su_NIL;
  593    }
  594 
  595 jleave:
  596    NYD2_OU;
  597    return rv;
  598 }
  599 
  600 static su_sz
  601 a_shexp__globsort(void const *cvpa, void const *cvpb){
  602    su_sz rv;
  603    struct n_strlist const *slpa, *slpb;
  604    NYD2_IN;
  605 
  606    slpa = cvpa;
  607    slpb = cvpb;
  608    rv = su_cs_cmp_case(slpa->sl_dat, slpb->sl_dat);
  609    NYD2_OU;
  610    return rv;
  611 }
  612 #endif /* mx_HAVE_FNMATCH */
  613 
  614 static void
  615 a_shexp__quote(struct a_shexp_quote_ctx *sqcp, struct a_shexp_quote_lvl *sqlp){
  616    /* XXX Because of the problems caused by ISO C multibyte interface we cannot
  617     * XXX use the recursive implementation because of stateful encodings.
  618     * XXX I.e., if a quoted substring cannot be self-contained - the data after
  619     * XXX the quote relies on "the former state", then this doesn't make sense.
  620     * XXX Therefore this is not fully programmed out but instead only detects
  621     * XXX the "most fancy" quoting necessary, and directly does that.
  622     * XXX As a result of this, T_REVSOL and T_DOUBLE are not even considered.
  623     * XXX Otherwise we rather have to convert to wide first and act on that,
  624     * XXX e.g., call visual_info(n_VISUAL_INFO_WOUT_CREATE) on entire input */
  625 #undef a_SHEXP_QUOTE_RECURSE /* XXX (Needs complete revisit, then) */
  626 #ifdef a_SHEXP_QUOTE_RECURSE
  627 # define jrecurse jrecurse
  628    struct a_shexp_quote_lvl sql;
  629 #else
  630 # define jrecurse jstep
  631 #endif
  632    struct n_visual_info_ctx vic;
  633    union {struct a_shexp_quote_lvl *head; struct n_string *store;} u;
  634    u32 flags;
  635    uz il;
  636    char const *ib, *ib_base;
  637    NYD2_IN;
  638 
  639    ib_base = ib = sqlp->sql_dat.s;
  640    il = sqlp->sql_dat.l;
  641    flags = sqlp->sql_flags;
  642 
  643    /* Iterate over the entire input, classify characters and type of quotes
  644     * along the way.  Whenever a quote change has to be applied, adjust flags
  645     * for the new situation -, setup sql.* and recurse- */
  646    while(il > 0){
  647       char c;
  648 
  649       c = *ib;
  650       if(su_cs_is_cntrl(c)){
  651          if(flags & a_SHEXP_QUOTE_T_DOLLAR)
  652             goto jstep;
  653          if(c == '\t' && (flags & (a_SHEXP_QUOTE_T_REVSOL |
  654                a_SHEXP_QUOTE_T_SINGLE | a_SHEXP_QUOTE_T_DOUBLE)))
  655             goto jstep;
  656 #ifdef a_SHEXP_QUOTE_RECURSE
  657          ++sqcp->sqc_cnt_dollar;
  658 #endif
  659          flags = (flags & ~a_SHEXP_QUOTE_T_MASK) | a_SHEXP_QUOTE_T_DOLLAR;
  660          goto jrecurse;
  661       }else if(su_cs_is_space(c) || c == '|' || c == '&' || c == ';' ||
  662             /* Whereas we don't support those, quote them for the sh(1)ell */
  663             c == '(' || c == ')' || c == '<' || c == '>' ||
  664             c == '"' || c == '$'){
  665          if(flags & a_SHEXP_QUOTE_T_MASK)
  666             goto jstep;
  667 #ifdef a_SHEXP_QUOTE_RECURSE
  668          ++sqcp->sqc_cnt_single;
  669 #endif
  670          flags = (flags & ~a_SHEXP_QUOTE_T_MASK) | a_SHEXP_QUOTE_T_SINGLE;
  671          goto jrecurse;
  672       }else if(c == '\''){
  673          if(flags & (a_SHEXP_QUOTE_T_MASK & ~a_SHEXP_QUOTE_T_SINGLE))
  674             goto jstep;
  675 #ifdef a_SHEXP_QUOTE_RECURSE
  676          ++sqcp->sqc_cnt_dollar;
  677 #endif
  678          flags = (flags & ~a_SHEXP_QUOTE_T_MASK) | a_SHEXP_QUOTE_T_DOLLAR;
  679          goto jrecurse;
  680       }else if(c == '\\' || (c == '#' && ib == ib_base)){
  681          if(flags & a_SHEXP_QUOTE_T_MASK)
  682             goto jstep;
  683 #ifdef a_SHEXP_QUOTE_RECURSE
  684          ++sqcp->sqc_cnt_single;
  685 #endif
  686          flags = (flags & ~a_SHEXP_QUOTE_T_MASK) | a_SHEXP_QUOTE_T_SINGLE;
  687          goto jrecurse;
  688       }else if(!su_cs_is_ascii(c)){
  689          /* Need to keep together multibytes */
  690 #ifdef a_SHEXP_QUOTE_RECURSE
  691          su_mem_set(&vic, 0, sizeof vic);
  692          vic.vic_indat = ib;
  693          vic.vic_inlen = il;
  694          n_visual_info(&vic,
  695             n_VISUAL_INFO_ONE_CHAR | n_VISUAL_INFO_SKIP_ERRORS);
  696 #endif
  697          /* xxx check whether resulting \u would be ASCII */
  698          if(!(flags & a_SHEXP_QUOTE_ROUNDTRIP) ||
  699                (flags & a_SHEXP_QUOTE_T_DOLLAR)){
  700 #ifdef a_SHEXP_QUOTE_RECURSE
  701             ib = vic.vic_oudat;
  702             il = vic.vic_oulen;
  703             continue;
  704 #else
  705             goto jstep;
  706 #endif
  707          }
  708 #ifdef a_SHEXP_QUOTE_RECURSE
  709          ++sqcp->sqc_cnt_dollar;
  710 #endif
  711          flags = (flags & ~a_SHEXP_QUOTE_T_MASK) | a_SHEXP_QUOTE_T_DOLLAR;
  712          goto jrecurse;
  713       }else
  714 jstep:
  715          ++ib, --il;
  716    }
  717    sqlp->sql_flags = flags;
  718 
  719    /* Level made the great and completed processing input.  Reverse the list of
  720     * levels, detect the "most fancy" quote type needed along this way */
  721    /* XXX Due to restriction as above very crude */
  722    for(flags = 0, il = 0, u.head = NULL; sqlp != NULL;){
  723       struct a_shexp_quote_lvl *tmp;
  724 
  725       tmp = sqlp->sql_link;
  726       sqlp->sql_link = u.head;
  727       u.head = sqlp;
  728       il += sqlp->sql_dat.l;
  729       if(sqlp->sql_flags & a_SHEXP_QUOTE_T_MASK)
  730          il += (sqlp->sql_dat.l >> 1);
  731       flags |= sqlp->sql_flags;
  732       sqlp = tmp;
  733    }
  734    sqlp = u.head;
  735 
  736    /* Finally work the substrings in the correct order, adjusting quotes along
  737     * the way as necessary.  Start off with the "most fancy" quote, so that
  738     * the user sees an overall boundary she can orientate herself on.
  739     * We do it like that to be able to give the user some "encapsulation
  740     * experience", to address what strikes me is a problem of sh(1)ell quoting:
  741     * different to, e.g., perl(1), where you see at a glance where a string
  742     * starts and ends, sh(1) quoting occurs at the "top level", disrupting the
  743     * visual appearance of "a string" as such */
  744    u.store = n_string_reserve(sqcp->sqc_store, il);
  745 
  746    if(flags & a_SHEXP_QUOTE_T_DOLLAR){
  747       u.store = n_string_push_buf(u.store, "$'", sizeof("$'") -1);
  748       flags = (flags & ~a_SHEXP_QUOTE_T_MASK) | a_SHEXP_QUOTE_T_DOLLAR;
  749    }else if(flags & a_SHEXP_QUOTE_T_DOUBLE){
  750       u.store = n_string_push_c(u.store, '"');
  751       flags = (flags & ~a_SHEXP_QUOTE_T_MASK) | a_SHEXP_QUOTE_T_DOUBLE;
  752    }else if(flags & a_SHEXP_QUOTE_T_SINGLE){
  753       u.store = n_string_push_c(u.store, '\'');
  754       flags = (flags & ~a_SHEXP_QUOTE_T_MASK) | a_SHEXP_QUOTE_T_SINGLE;
  755    }else /*if(flags & a_SHEXP_QUOTE_T_REVSOL)*/
  756       flags &= ~a_SHEXP_QUOTE_T_MASK;
  757 
  758    /* Work all the levels */
  759    for(; sqlp != NULL; sqlp = sqlp->sql_link){
  760       /* As necessary update our mode of quoting */
  761 #ifdef a_SHEXP_QUOTE_RECURSE
  762       il = 0;
  763 
  764       switch(sqlp->sql_flags & a_SHEXP_QUOTE_T_MASK){
  765       case a_SHEXP_QUOTE_T_DOLLAR:
  766          if(!(flags & a_SHEXP_QUOTE_T_DOLLAR))
  767             il = a_SHEXP_QUOTE_T_DOLLAR;
  768          break;
  769       case a_SHEXP_QUOTE_T_DOUBLE:
  770          if(!(flags & (a_SHEXP_QUOTE_T_DOUBLE | a_SHEXP_QUOTE_T_DOLLAR)))
  771             il = a_SHEXP_QUOTE_T_DOLLAR;
  772          break;
  773       case a_SHEXP_QUOTE_T_SINGLE:
  774          if(!(flags & (a_SHEXP_QUOTE_T_REVSOL | a_SHEXP_QUOTE_T_SINGLE |
  775                a_SHEXP_QUOTE_T_DOUBLE | a_SHEXP_QUOTE_T_DOLLAR)))
  776             il = a_SHEXP_QUOTE_T_SINGLE;
  777          break;
  778       default:
  779       case a_SHEXP_QUOTE_T_REVSOL:
  780          if(!(flags & (a_SHEXP_QUOTE_T_REVSOL | a_SHEXP_QUOTE_T_SINGLE |
  781                a_SHEXP_QUOTE_T_DOUBLE | a_SHEXP_QUOTE_T_DOLLAR)))
  782             il = a_SHEXP_QUOTE_T_REVSOL;
  783          break;
  784       }
  785 
  786       if(il != 0){
  787          if(flags & (a_SHEXP_QUOTE_T_SINGLE | a_SHEXP_QUOTE_T_DOLLAR))
  788             u.store = n_string_push_c(u.store, '\'');
  789          else if(flags & a_SHEXP_QUOTE_T_DOUBLE)
  790             u.store = n_string_push_c(u.store, '"');
  791          flags &= ~a_SHEXP_QUOTE_T_MASK;
  792 
  793          flags |= (u32)il;
  794          if(flags & a_SHEXP_QUOTE_T_DOLLAR)
  795             u.store = n_string_push_buf(u.store, "$'", sizeof("$'") -1);
  796          else if(flags & a_SHEXP_QUOTE_T_DOUBLE)
  797             u.store = n_string_push_c(u.store, '"');
  798          else if(flags & a_SHEXP_QUOTE_T_SINGLE)
  799             u.store = n_string_push_c(u.store, '\'');
  800       }
  801 #endif /* a_SHEXP_QUOTE_RECURSE */
  802 
  803       /* Work the level's substring */
  804       ib = sqlp->sql_dat.s;
  805       il = sqlp->sql_dat.l;
  806 
  807       while(il > 0){
  808          char c2, c;
  809 
  810          c = *ib;
  811 
  812          if(su_cs_is_cntrl(c)){
  813             ASSERT(c == '\t' || (flags & a_SHEXP_QUOTE_T_DOLLAR));
  814             ASSERT((flags & (a_SHEXP_QUOTE_T_REVSOL | a_SHEXP_QUOTE_T_SINGLE |
  815                a_SHEXP_QUOTE_T_DOUBLE | a_SHEXP_QUOTE_T_DOLLAR)));
  816             switch((c2 = c)){
  817             case 0x07: c = 'a'; break;
  818             case 0x08: c = 'b'; break;
  819             case 0x0A: c = 'n'; break;
  820             case 0x0B: c = 'v'; break;
  821             case 0x0C: c = 'f'; break;
  822             case 0x0D: c = 'r'; break;
  823             case 0x1B: c = 'E'; break;
  824             default: break;
  825             case 0x09:
  826                if(flags & a_SHEXP_QUOTE_T_DOLLAR){
  827                   c = 't';
  828                   break;
  829                }
  830                if(flags & a_SHEXP_QUOTE_T_REVSOL)
  831                   u.store = n_string_push_c(u.store, '\\');
  832                goto jpush;
  833             }
  834             u.store = n_string_push_c(u.store, '\\');
  835             if(c == c2){
  836                u.store = n_string_push_c(u.store, 'c');
  837                c ^= 0x40;
  838             }
  839             goto jpush;
  840          }else if(su_cs_is_space(c) || c == '|' || c == '&' || c == ';' ||
  841                /* Whereas we do not support those, quote them for sh(1)ell */
  842                c == '(' || c == ')' || c == '<' || c == '>' ||
  843                c == '"' || c == '$'){
  844             if(flags & (a_SHEXP_QUOTE_T_SINGLE | a_SHEXP_QUOTE_T_DOLLAR))
  845                goto jpush;
  846             ASSERT(flags & (a_SHEXP_QUOTE_T_REVSOL | a_SHEXP_QUOTE_T_DOUBLE));
  847             u.store = n_string_push_c(u.store, '\\');
  848             goto jpush;
  849          }else if(c == '\''){
  850             if(flags & a_SHEXP_QUOTE_T_DOUBLE)
  851                goto jpush;
  852             ASSERT(!(flags & a_SHEXP_QUOTE_T_SINGLE));
  853             u.store = n_string_push_c(u.store, '\\');
  854             goto jpush;
  855          }else if(c == '\\' || (c == '#' && ib == ib_base)){
  856             if(flags & a_SHEXP_QUOTE_T_SINGLE)
  857                goto jpush;
  858             ASSERT(flags & (a_SHEXP_QUOTE_T_REVSOL | a_SHEXP_QUOTE_T_DOUBLE |
  859                a_SHEXP_QUOTE_T_DOLLAR));
  860             u.store = n_string_push_c(u.store, '\\');
  861             goto jpush;
  862          }else if(su_cs_is_ascii(c)){
  863             /* Shorthand: we can simply push that thing out */
  864 jpush:
  865             u.store = n_string_push_c(u.store, c);
  866             ++ib, --il;
  867          }else{
  868             /* Not an ASCII character, take care not to split up multibyte
  869              * sequences etc.  For the sake of compile testing, don't enwrap in
  870              * mx_HAVE_ALWAYS_UNICODE_LOCALE || mx_HAVE_NATCH_CHAR */
  871             if(n_psonce & n_PSO_UNICODE){
  872                u32 unic;
  873                char const *ib2;
  874                uz il2, il3;
  875 
  876                ib2 = ib;
  877                il2 = il;
  878                if((unic = su_utf8_to_32(&ib2, &il2)) != U32_MAX){
  879                   char itoa[32];
  880                   char const *cp;
  881 
  882                   il2 = P2UZ(&ib2[0] - &ib[0]);
  883                   if((flags & a_SHEXP_QUOTE_ROUNDTRIP) || unic == 0xFFFDu){
  884                      /* Use padding to make ambiguities impossible */
  885                      il3 = snprintf(itoa, sizeof itoa, "\\%c%0*X",
  886                            (unic > 0xFFFFu ? 'U' : 'u'),
  887                            (int)(unic > 0xFFFFu ? 8 : 4), unic);
  888                      cp = itoa;
  889                   }else{
  890                      il3 = il2;
  891                      cp = &ib[0];
  892                   }
  893                   u.store = n_string_push_buf(u.store, cp, il3);
  894                   ib += il2, il -= il2;
  895                   continue;
  896                }
  897             }
  898 
  899             su_mem_set(&vic, 0, sizeof vic);
  900             vic.vic_indat = ib;
  901             vic.vic_inlen = il;
  902             n_visual_info(&vic,
  903                n_VISUAL_INFO_ONE_CHAR | n_VISUAL_INFO_SKIP_ERRORS);
  904 
  905             /* Work this substring as sensitive as possible */
  906             il -= vic.vic_oulen;
  907             if(!(flags & a_SHEXP_QUOTE_ROUNDTRIP))
  908                u.store = n_string_push_buf(u.store, ib, il);
  909 #ifdef mx_HAVE_ICONV
  910             else if((vic.vic_indat = n_iconv_onetime_cp(n_ICONV_NONE,
  911                   "utf-8", ok_vlook(ttycharset), savestrbuf(ib, il))) != NULL){
  912                u32 unic;
  913                char const *ib2;
  914                uz il2, il3;
  915 
  916                il2 = su_cs_len(ib2 = vic.vic_indat);
  917                if((unic = su_utf8_to_32(&ib2, &il2)) != U32_MAX){
  918                   char itoa[32];
  919 
  920                   il2 = P2UZ(&ib2[0] - &vic.vic_indat[0]);
  921                   /* Use padding to make ambiguities impossible */
  922                   il3 = snprintf(itoa, sizeof itoa, "\\%c%0*X",
  923                         (unic > 0xFFFFu ? 'U' : 'u'),
  924                         (int)(unic > 0xFFFFu ? 8 : 4), unic);
  925                   u.store = n_string_push_buf(u.store, itoa, il3);
  926                }else
  927                   goto Jxseq;
  928             }
  929 #endif
  930             else
  931 #ifdef mx_HAVE_ICONV
  932                  Jxseq:
  933 #endif
  934                         while(il-- > 0){
  935                u.store = n_string_push_buf(u.store, "\\xFF",
  936                      sizeof("\\xFF") -1);
  937                n_c_to_hex_base16(&u.store->s_dat[u.store->s_len - 2], *ib++);
  938             }
  939 
  940             ib = vic.vic_oudat;
  941             il = vic.vic_oulen;
  942          }
  943       }
  944    }
  945 
  946    /* Close an open quote */
  947    if(flags & (a_SHEXP_QUOTE_T_SINGLE | a_SHEXP_QUOTE_T_DOLLAR))
  948       u.store = n_string_push_c(u.store, '\'');
  949    else if(flags & a_SHEXP_QUOTE_T_DOUBLE)
  950       u.store = n_string_push_c(u.store, '"');
  951 #ifdef a_SHEXP_QUOTE_RECURSE
  952 jleave:
  953 #endif
  954    NYD2_OU;
  955    return;
  956 
  957 #ifdef a_SHEXP_QUOTE_RECURSE
  958 jrecurse:
  959    sqlp->sql_dat.l -= il;
  960 
  961    sql.sql_link = sqlp;
  962    sql.sql_dat.s = UNCONST(char*,ib);
  963    sql.sql_dat.l = il;
  964    sql.sql_flags = flags;
  965    a_shexp__quote(sqcp, &sql);
  966    goto jleave;
  967 #endif
  968 
  969 #undef jrecurse
  970 #undef a_SHEXP_QUOTE_RECURSE
  971 }
  972 
  973 FL char *
  974 fexpand(char const *name, BITENUM_IS(u32,fexp_mode) fexpm){ /* TODO -> URL:: */
  975    struct str proto, s;
  976    char const *res, *cp;
  977    boole dyn, haveproto;
  978    s32 eno;
  979    NYD_IN;
  980 
  981    n_pstate &= ~n_PS_EXPAND_MULTIRESULT;
  982    dyn = FAL0;
  983    eno = su_ERR_NONE;
  984 
  985    /* The order of evaluation is "%" and "#" expand into constants.
  986     * "&" can expand into "+".  "+" can expand into shell meta characters.
  987     * Shell meta characters expand into constants.
  988     * This way, we make no recursive expansion */
  989    if(!(fexpm & FEXP_SHORTCUT) || (res = mx_shortcut_expand(name)) == NIL)
  990       res = name;
  991 
  992 jprotonext:
  993    UNINIT(proto.s, NIL), UNINIT(proto.l, 0);
  994    haveproto = FAL0;
  995    for(cp = res; *cp && *cp != ':'; ++cp)
  996       if(!su_cs_is_alnum(*cp))
  997          goto jnoproto;
  998    if(cp[0] == ':' && cp[1] == '/' && cp[2] == '/'){
  999       haveproto = TRU1;
 1000       proto.s = UNCONST(char*,res);
 1001       cp += 3;
 1002       proto.l = P2UZ(cp - res);
 1003       res = cp;
 1004    }
 1005 
 1006 jnoproto:
 1007    if(!(fexpm & FEXP_NSPECIAL)){
 1008 jnext:
 1009       dyn = FAL0;
 1010       switch(*res){
 1011       case '%':
 1012          if(res[1] == ':' && res[2] != '\0'){
 1013             res = &res[2];
 1014             goto jprotonext;
 1015          }else{
 1016             boole force;
 1017 
 1018             force = (res[1] != '\0');
 1019             res = a_shexp_findmail((force ? &res[1] : ok_vlook(LOGNAME)),
 1020                   force);
 1021             if(force)
 1022                goto jislocal;
 1023          }
 1024          goto jnext;
 1025       case '#':
 1026          if(res[1] != '\0')
 1027             break;
 1028          if(prevfile[0] == '\0'){
 1029             n_err(_("No previous file\n"));
 1030             res = NIL;
 1031             eno = su_ERR_NODATA;
 1032             goto jleave;
 1033          }
 1034          res = prevfile;
 1035          goto jislocal;
 1036       case '&':
 1037          if(res[1] == '\0')
 1038             res = ok_vlook(MBOX);
 1039          break;
 1040       default:
 1041          break;
 1042       }
 1043    }
 1044 
 1045 #ifdef mx_HAVE_IMAP
 1046    if(res[0] == '@' && which_protocol(mailname, FAL0, FAL0, NIL)
 1047          == PROTO_IMAP){
 1048       res = str_concat_csvl(&s, protbase(mailname), "/", &res[1], NIL)->s;
 1049       dyn = TRU1;
 1050    }
 1051 #endif
 1052 
 1053    /* POSIX: if *folder* unset or null, "+" shall be retained */
 1054    if(!(fexpm & FEXP_NFOLDER) && *res == '+' &&
 1055          *(cp = n_folder_query()) != '\0'){
 1056       res = str_concat_csvl(&s, cp, &res[1], NIL)->s;
 1057       dyn = TRU1;
 1058    }
 1059 
 1060    /* Do some meta expansions */
 1061    if((fexpm & (FEXP_NSHELL | FEXP_NVAR)) != FEXP_NVAR &&
 1062          ((fexpm & FEXP_NSHELL) ? (su_cs_find_c(res, '$') != NIL)
 1063           : (su_cs_first_of(res, "{}[]*?$") != su_UZ_MAX))){
 1064       boole doexp;
 1065 
 1066       if(fexpm & FEXP_NOPROTO)
 1067          doexp = TRU1;
 1068       else{
 1069          cp = haveproto ? savecat(savestrbuf(proto.s, proto.l), res) : res;
 1070 
 1071          switch(which_protocol(cp, TRU1, FAL0, NIL)){
 1072          case n_PROTO_EML:
 1073          case n_PROTO_FILE:
 1074          case n_PROTO_MAILDIR:
 1075             doexp = TRU1;
 1076             break;
 1077          default:
 1078             doexp = FAL0;
 1079             break;
 1080          }
 1081       }
 1082 
 1083       if(doexp){
 1084          struct str shin;
 1085          struct n_string shou, *shoup;
 1086 
 1087          shin.s = UNCONST(char*,res);
 1088          shin.l = UZ_MAX;
 1089          shoup = n_string_creat_auto(&shou);
 1090          for(;;){
 1091             BITENUM_IS(u32,n_shexp_state) shs;
 1092 
 1093             /* TODO shexp: take care: not include backtick eval once avail! */
 1094             shs = n_shexp_parse_token((n_SHEXP_PARSE_LOG_D_V |
 1095                   n_SHEXP_PARSE_QUOTE_AUTO_FIXED |
 1096                   n_SHEXP_PARSE_QUOTE_AUTO_DQ |
 1097                   n_SHEXP_PARSE_QUOTE_AUTO_CLOSE), shoup, &shin, NIL);
 1098             if(shs & n_SHEXP_STATE_STOP)
 1099                break;
 1100          }
 1101          res = n_string_cp(shoup);
 1102          /*shoup = n_string_drop_ownership(shoup);*/
 1103          dyn = TRU1;
 1104 
 1105          if(res[0] == '~')
 1106             res = a_shexp_tilde(res);
 1107 
 1108          if(!(fexpm & FEXP_NSHELL) &&
 1109                (res = a_shexp_globname(res, fexpm)) == NIL){
 1110             eno = su_ERR_INVAL; /* TODO shexp_globname needs true error! */
 1111             goto jleave;
 1112          }
 1113          dyn = TRU1;
 1114       }/* else no tilde */
 1115    }else if(res[0] == '~'){
 1116       res = a_shexp_tilde(res);
 1117       dyn = TRU1;
 1118    }
 1119 
 1120 jislocal:
 1121    if(res != NIL && haveproto){
 1122       res = savecat(savestrbuf(proto.s, proto.l), res);
 1123       dyn = TRU1;
 1124    }
 1125 
 1126    if(fexpm & (FEXP_LOCAL | FEXP_LOCAL_FILE)){
 1127       switch(which_protocol(res, FAL0, FAL0, &cp)){
 1128       case n_PROTO_MAILDIR:
 1129          if(!(fexpm & FEXP_LOCAL_FILE)){
 1130          /* FALLTHRU */
 1131       case n_PROTO_FILE:
 1132       case n_PROTO_EML:
 1133             if(fexpm & FEXP_LOCAL_FILE){
 1134                res = cp;
 1135                dyn = FAL0;
 1136             }
 1137             break;
 1138          }
 1139          /* FALLTHRU */
 1140       default:
 1141          n_err(_("Not a local file or directory: %s\n"),
 1142             n_shexp_quote_cp(name, FAL0));
 1143          res = NIL;
 1144          eno = su_ERR_INVAL;
 1145          break;
 1146       }
 1147    }
 1148 
 1149 jleave:
 1150    if(res != NIL && !dyn)
 1151       res = savestr(res);
 1152    if(res == NIL)
 1153       su_err_set_no(eno);
 1154 
 1155    NYD_OU;
 1156    return UNCONST(char*,res);
 1157 }
 1158 
 1159 FL BITENUM_IS(u32,n_shexp_state)
 1160 n_shexp_parse_token(BITENUM_IS(u32,n_shexp_parse_flags) flags,
 1161       struct n_string *store, struct str *input, void const **cookie){
 1162    /* TODO shexp_parse_token: WCHAR
 1163     * TODO This needs to be rewritten in order to support $(( )) and $( )
 1164     * TODO and ${xyYZ} and the possibly infinite recursion they bring along,
 1165     * TODO too.  We need a carrier struct, then, and can nicely split this
 1166     * TODO big big thing up in little pieces!
 1167     * TODO This means it should produce a tree of objects, so that callees
 1168     * TODO can recognize whether something happened inside single/double etc.
 1169     * TODO quotes; e.g., to requote "'[a-z]'" to, e.g., "\[a-z]", etc.!
 1170     * TODO Also, it should be possible to "yield" this, e.g., like this
 1171     * TODO we would not need to be wired to variable handling for positional
 1172     * TODO parameters, instead these should be fields of the carrier, and
 1173     * TODO once we need them we should yield saying we need them, and if
 1174     * TODO we are reentered we simply access the fields directly.
 1175     * TODO That is: do that, also for normal variables: like this the shell
 1176     * TODO expression parser can be made entirely generic and placed in SU! */
 1177    enum{
 1178       a_NONE = 0,
 1179       a_SKIPQ = 1u<<0,     /* Skip rest of this quote (\u0 ..) */
 1180       a_SKIPT = 1u<<1,     /* Skip entire token (\c@) */
 1181       a_SKIPMASK = a_SKIPQ | a_SKIPT,
 1182       a_SURPLUS = 1u<<2,   /* Extended sequence interpretation */
 1183       a_NTOKEN = 1u<<3,    /* "New token": e.g., comments are possible */
 1184       a_BRACE = 1u<<4,     /* Variable substitution: brace enclosed */
 1185       a_DIGIT1 = 1u<<5,    /* ..first character was digit */
 1186       a_NONDIGIT = 1u<<6,  /* ..has seen any non-digits */
 1187       a_VARSUBST_MASK = su_BITENUM_MASK(4, 6),
 1188 
 1189       a_ROUND_MASK = a_SKIPT | (int)~su_BITENUM_MASK(0, 7),
 1190       a_COOKIE = 1u<<8,
 1191       a_EXPLODE = 1u<<9,
 1192       /* Remove one more byte from the input after pushing data to output */
 1193       a_CHOP_ONE = 1u<<10,
 1194       a_TMP = 1u<<30
 1195    };
 1196 
 1197    char c2, c, quotec, utf[8];
 1198    BITENUM_IS(u32,n_shexp_state) rv;
 1199    uz i, il;
 1200    u32 state, last_known_meta_trim_len;
 1201    char const *ifs, *ifs_ws, *ib_save, *ib;
 1202    NYD2_IN;
 1203 
 1204    ASSERT((flags & n_SHEXP_PARSE_DRYRUN) || store != NULL);
 1205    ASSERT(input != NULL);
 1206    ASSERT(input->l == 0 || input->s != NULL);
 1207    ASSERT(!(flags & n_SHEXP_PARSE_LOG) || !(flags & n_SHEXP_PARSE_LOG_D_V));
 1208    ASSERT(!(flags & n_SHEXP_PARSE_IFS_ADD_COMMA) ||
 1209       !(flags & n_SHEXP_PARSE_IFS_IS_COMMA));
 1210    ASSERT(!(flags & n_SHEXP_PARSE_QUOTE_AUTO_FIXED) ||
 1211       (flags & n__SHEXP_PARSE_QUOTE_AUTO_MASK));
 1212 
 1213    if((flags & n_SHEXP_PARSE_LOG_D_V) && (n_poption & n_PO_D_V))
 1214       flags |= n_SHEXP_PARSE_LOG;
 1215    if(flags & n_SHEXP_PARSE_QUOTE_AUTO_FIXED)
 1216       flags |= n_SHEXP_PARSE_QUOTE_AUTO_CLOSE;
 1217 
 1218    if((flags & n_SHEXP_PARSE_TRUNC) && store != NULL)
 1219       store = n_string_trunc(store, 0);
 1220 
 1221    if(flags & (n_SHEXP_PARSE_IFS_VAR | n_SHEXP_PARSE_TRIM_IFSSPACE)){
 1222       ifs = ok_vlook(ifs);
 1223       ifs_ws = ok_vlook(ifs_ws);
 1224    }else{
 1225       UNINIT(ifs, n_empty);
 1226       UNINIT(ifs_ws, n_empty);
 1227    }
 1228 
 1229    state = a_NONE;
 1230    ib = input->s;
 1231    if((il = input->l) == UZ_MAX)
 1232       input->l = il = su_cs_len(ib);
 1233    UNINIT(c, '\0');
 1234 
 1235    if(cookie != NULL && *cookie != NULL){
 1236       ASSERT(!(flags & n_SHEXP_PARSE_DRYRUN));
 1237       state |= a_COOKIE;
 1238    }
 1239 
 1240    rv = n_SHEXP_STATE_NONE;
 1241 jrestart_empty:
 1242    rv &= n_SHEXP_STATE_WS_LEAD;
 1243    state &= a_ROUND_MASK;
 1244 
 1245    /* In cookie mode, the next ARGV entry is the token already, unchanged,
 1246     * since it has already been expanded before! */
 1247    if(state & a_COOKIE){
 1248       char const * const *xcookie, *cp;
 1249 
 1250       i = store->s_len;
 1251       xcookie = *cookie;
 1252       if((store = n_string_push_cp(store, *xcookie))->s_len > 0)
 1253          rv |= n_SHEXP_STATE_OUTPUT;
 1254       if(*++xcookie == NULL){
 1255          *cookie = NULL;
 1256          state &= ~a_COOKIE;
 1257          flags |= n_SHEXP_PARSE_QUOTE_AUTO_DQ; /* ..why we are here! */
 1258       }else
 1259          *cookie = UNCONST(void*,xcookie);
 1260 
 1261       for(cp = &n_string_cp(store)[i]; (c = *cp++) != '\0';)
 1262          if(su_cs_is_cntrl(c)){
 1263             rv |= n_SHEXP_STATE_CONTROL;
 1264             break;
 1265          }
 1266 
 1267       /* The last exploded cookie will join with the yielded input token, so
 1268        * simply fall through in this case */
 1269       if(state & a_COOKIE)
 1270          goto jleave_quick;
 1271    }else{
 1272 jrestart:
 1273       if(flags & n_SHEXP_PARSE_TRIM_SPACE){
 1274          for(; il > 0; ++ib, --il){
 1275             if(!su_cs_is_space(*ib))
 1276                break;
 1277             rv |= n_SHEXP_STATE_WS_LEAD;
 1278          }
 1279       }
 1280 
 1281       if(flags & n_SHEXP_PARSE_TRIM_IFSSPACE){
 1282          for(; il > 0; ++ib, --il){
 1283             if(su_cs_find_c(ifs_ws, *ib) == NULL)
 1284                break;
 1285             rv |= n_SHEXP_STATE_WS_LEAD;
 1286          }
 1287       }
 1288 
 1289       input->s = UNCONST(char*,ib);
 1290       input->l = il;
 1291    }
 1292 
 1293    if(il == 0){
 1294       rv |= n_SHEXP_STATE_STOP;
 1295       goto jleave;
 1296    }
 1297 
 1298    if(store != NULL)
 1299       store = n_string_reserve(store, MIN(il, 32)); /* XXX */
 1300 
 1301    switch(flags & n__SHEXP_PARSE_QUOTE_AUTO_MASK){
 1302    case n_SHEXP_PARSE_QUOTE_AUTO_SQ:
 1303       quotec = '\'';
 1304       rv |= n_SHEXP_STATE_QUOTE;
 1305       break;
 1306    case n_SHEXP_PARSE_QUOTE_AUTO_DQ:
 1307       quotec = '"';
 1308       if(0){
 1309    case n_SHEXP_PARSE_QUOTE_AUTO_DSQ:
 1310          quotec = '\'';
 1311       }
 1312       rv |= n_SHEXP_STATE_QUOTE;
 1313       state |= a_SURPLUS;
 1314       break;
 1315    default:
 1316       quotec = '\0';
 1317       state |= a_NTOKEN;
 1318       break;
 1319    }
 1320 
 1321    /* TODO n_SHEXP_PARSE_META_SEMICOLON++, well, hack: we are not the shell,
 1322     * TODO we are not a language, and therefore the general *ifs-ws* and normal
 1323     * TODO whitespace trimming that input lines undergo (in a_go_evaluate())
 1324     * TODO has already happened, our result will be used *as is*, and therefore
 1325     * TODO we need to be aware of and remove trailing unquoted WS that would
 1326     * TODO otherwise remain, after we have seen a semicolon sequencer.
 1327     * By sheer luck we only need to track this in non-quote-mode */
 1328    last_known_meta_trim_len = U32_MAX;
 1329 
 1330    while(il > 0){ /* {{{ */
 1331       --il, c = *ib++;
 1332 
 1333       /* If no quote-mode active.. */
 1334       if(quotec == '\0'){
 1335          if(c == '"' || c == '\''){
 1336             quotec = c;
 1337             if(c == '"')
 1338                state |= a_SURPLUS;
 1339             else
 1340                state &= ~a_SURPLUS;
 1341             state &= ~a_NTOKEN;
 1342             last_known_meta_trim_len = U32_MAX;
 1343             rv |= n_SHEXP_STATE_QUOTE;
 1344             continue;
 1345          }else if(c == '$'){
 1346             if(il > 0){
 1347                state &= ~a_NTOKEN;
 1348                last_known_meta_trim_len = U32_MAX;
 1349                if(*ib == '\''){
 1350                   --il, ++ib;
 1351                   quotec = '\'';
 1352                   state |= a_SURPLUS;
 1353                   rv |= n_SHEXP_STATE_QUOTE;
 1354                   continue;
 1355                }else
 1356                   goto J_var_expand;
 1357             }
 1358          }else if(c == '\\'){
 1359             /* Outside of quotes this just escapes any next character, but
 1360              * a sole <reverse solidus> at EOS is left unchanged */
 1361              if(il > 0)
 1362                --il, c = *ib++;
 1363             state &= ~a_NTOKEN;
 1364             last_known_meta_trim_len = U32_MAX;
 1365          }
 1366          /* A comment may it be if no token has yet started */
 1367          else if(c == '#' && (state & a_NTOKEN)){
 1368             ib += il;
 1369             il = 0;
 1370             rv |= n_SHEXP_STATE_STOP;
 1371             /*last_known_meta_trim_len = U32_MAX;*/
 1372             goto jleave;
 1373          }
 1374          /* Metacharacters that separate tokens must be turned on explicitly */
 1375          else if(c == '|' && (flags & n_SHEXP_PARSE_META_VERTBAR)){
 1376             rv |= n_SHEXP_STATE_META_VERTBAR;
 1377 
 1378             /* The parsed sequence may be _the_ output, so ensure we don't
 1379              * include the metacharacter, then. */
 1380             if(flags & (n_SHEXP_PARSE_DRYRUN | n_SHEXP_PARSE_META_KEEP))
 1381                ++il, --ib;
 1382             /*last_known_meta_trim_len = U32_MAX;*/
 1383             break;
 1384          }else if(c == '&' && (flags & n_SHEXP_PARSE_META_AMPERSAND)){
 1385             rv |= n_SHEXP_STATE_META_AMPERSAND;
 1386 
 1387             /* The parsed sequence may be _the_ output, so ensure we don't
 1388              * include the metacharacter, then. */
 1389             if(flags & (n_SHEXP_PARSE_DRYRUN | n_SHEXP_PARSE_META_KEEP)){
 1390                if(!(flags & n_SHEXP_PARSE_META_KEEP))
 1391                   state |= a_CHOP_ONE;
 1392                ++il, --ib;
 1393             }
 1394             /*last_known_meta_trim_len = U32_MAX;*/
 1395             break;
 1396          }else if(c == ';' && (flags & n_SHEXP_PARSE_META_SEMICOLON)){
 1397             rv |= n_SHEXP_STATE_META_SEMICOLON | n_SHEXP_STATE_STOP;
 1398             if(!(flags & n_SHEXP_PARSE_DRYRUN) &&
 1399                   (rv & n_SHEXP_STATE_OUTPUT) &&
 1400                   last_known_meta_trim_len != U32_MAX)
 1401                store = n_string_trunc(store, last_known_meta_trim_len);
 1402 
 1403             /* The parsed sequence may be _the_ output, so ensure we don't
 1404              * include the metacharacter, then. */
 1405             /*if(flags & (n_SHEXP_PARSE_DRYRUN | n_SHEXP_PARSE_META_KEEP))*/{
 1406                if(!(flags & n_SHEXP_PARSE_META_KEEP))
 1407                   state |= a_CHOP_ONE;
 1408                ++il, --ib;
 1409             }
 1410             /*last_known_meta_trim_len = U32_MAX;*/
 1411             break;
 1412          }else if(c == ',' && (flags &
 1413                (n_SHEXP_PARSE_IFS_ADD_COMMA | n_SHEXP_PARSE_IFS_IS_COMMA))){
 1414             /* The parsed sequence may be _the_ output, so ensure we don't
 1415              * include the metacharacter, then. */
 1416             if(flags & (n_SHEXP_PARSE_DRYRUN | n_SHEXP_PARSE_META_KEEP)){
 1417                if(!(flags & n_SHEXP_PARSE_META_KEEP))
 1418                   state |= a_CHOP_ONE;
 1419                ++il, --ib;
 1420             }
 1421             /*last_known_meta_trim_len = U32_MAX;*/
 1422             break;
 1423          }else{
 1424             u8 blnk;
 1425 
 1426             blnk = su_cs_is_blank(c) ? 1 : 0;
 1427             blnk |= ((flags & (n_SHEXP_PARSE_IFS_VAR |
 1428                      n_SHEXP_PARSE_TRIM_IFSSPACE)) &&
 1429                   su_cs_find_c(ifs_ws, c) != NULL) ? 2 : 0;
 1430 
 1431             if((!(flags & n_SHEXP_PARSE_IFS_VAR) && (blnk & 1)) ||
 1432                   ((flags & n_SHEXP_PARSE_IFS_VAR) &&
 1433                      ((blnk & 2) || su_cs_find_c(ifs, c) != NULL))){
 1434                if(!(flags & n_SHEXP_PARSE_IFS_IS_COMMA)){
 1435                   /* The parsed sequence may be _the_ output, so ensure we do
 1436                    * not include the metacharacter, then. */
 1437                   if(flags & (n_SHEXP_PARSE_DRYRUN | n_SHEXP_PARSE_META_KEEP)){
 1438                      if(!(flags & n_SHEXP_PARSE_META_KEEP))
 1439                         state |= a_CHOP_ONE;
 1440                      ++il, --ib;
 1441                   }
 1442                   /*last_known_meta_trim_len = U32_MAX;*/
 1443                   break;
 1444                }
 1445                state |= a_NTOKEN;
 1446             }else
 1447                state &= ~a_NTOKEN;
 1448 
 1449             if(blnk && store != NULL){
 1450                if(last_known_meta_trim_len == U32_MAX)
 1451                   last_known_meta_trim_len = store->s_len;
 1452             }else
 1453                last_known_meta_trim_len = U32_MAX;
 1454          }
 1455       }else{
 1456          /* Quote-mode */
 1457          ASSERT(!(state & a_NTOKEN));
 1458          if(c == quotec && !(flags & n_SHEXP_PARSE_QUOTE_AUTO_FIXED)){
 1459             state &= a_ROUND_MASK;
 1460             quotec = '\0';
 1461             /* Users may need to recognize the presence of empty quotes */
 1462             rv |= n_SHEXP_STATE_OUTPUT;
 1463             continue;
 1464          }else if(c == '\\' && (state & a_SURPLUS)){
 1465             ib_save = ib - 1;
 1466             /* A sole <reverse solidus> at EOS is treated as-is!  This is ok
 1467              * since the "closing quote" error will occur next, anyway */
 1468             if(il == 0)
 1469                ;
 1470             else if((c2 = *ib) == quotec){
 1471                --il, ++ib;
 1472                c = quotec;
 1473             }else if(quotec == '"'){
 1474                /* Double quotes, POSIX says:
 1475                 *    The <backslash> shall retain its special meaning as an
 1476                 *    escape character (see Section 2.2.1) only when followed
 1477                 *    by one of the following characters when considered
 1478                 *    special: $ ` " \ <newline> */
 1479                switch(c2){
 1480                case '$':
 1481                case '`':
 1482                /* case '"': already handled via c2 == quotec */
 1483                case '\\':
 1484                   --il, ++ib;
 1485                   c = c2;
 1486                   /* FALLTHRU */
 1487                default:
 1488                   break;
 1489                }
 1490             }else{
 1491                /* Dollar-single-quote */
 1492                --il, ++ib;
 1493                switch(c2){
 1494                case '"':
 1495                /* case '\'': already handled via c2 == quotec */
 1496                case '\\':
 1497                   c = c2;
 1498                   break;
 1499 
 1500                case 'b': c = '\b'; break;
 1501                case 'f': c = '\f'; break;
 1502                case 'n': c = '\n'; break;
 1503                case 'r': c = '\r'; break;
 1504                case 't': c = '\t'; break;
 1505                case 'v': c = '\v'; break;
 1506 
 1507                case 'E':
 1508                case 'e': c = '\033'; break;
 1509 
 1510                /* Control character */
 1511                case 'c':
 1512                   if(il == 0)
 1513                      goto j_dollar_ungetc;
 1514                   --il, c2 = *ib++;
 1515                   if(state & a_SKIPMASK)
 1516                      continue;
 1517                   /* ASCII C0: 0..1F, 7F <- @.._ (+ a-z -> A-Z), ? */
 1518                   c = su_cs_to_upper(c2) ^ 0x40;
 1519                   if((u8)c > 0x1F && c != 0x7F){
 1520                      if(flags & n_SHEXP_PARSE_LOG)
 1521                         n_err(_("Invalid \\c notation: %.*s: %.*s\n"),
 1522                            (int)input->l, input->s,
 1523                            (int)P2UZ(ib - ib_save), ib_save);
 1524                      rv |= n_SHEXP_STATE_ERR_CONTROL;
 1525                   }
 1526                   /* As an implementation-defined extension, support \c@
 1527                    * EQ printf(1) alike \c */
 1528                   if(c == '\0'){
 1529                      state |= a_SKIPT;
 1530                      continue;
 1531                   }
 1532                   break;
 1533 
 1534                /* Octal sequence: 1 to 3 octal bytes */
 1535                case '0':
 1536                   /* As an extension (dependent on where you look, echo(1), or
 1537                    * awk(1)/tr(1) etc.), allow leading "0" octal indicator */
 1538                   if(il > 0 && (c = *ib) >= '0' && c <= '7'){
 1539                      c2 = c;
 1540                      --il, ++ib;
 1541                   }
 1542                   /* FALLTHRU */
 1543                case '1': case '2': case '3':
 1544                case '4': case '5': case '6': case '7':
 1545                   c2 -= '0';
 1546                   if(il > 0 && (c = *ib) >= '0' && c <= '7'){
 1547                      c2 = (c2 << 3) | (c - '0');
 1548                      --il, ++ib;
 1549                   }
 1550                   if(il > 0 && (c = *ib) >= '0' && c <= '7'){
 1551                      if(!(state & a_SKIPMASK) && (u8)c2 > 0x1F){
 1552                         rv |= n_SHEXP_STATE_ERR_NUMBER;
 1553                         --il, ++ib;
 1554                         if(flags & n_SHEXP_PARSE_LOG)
 1555                            n_err(_("\\0 argument exceeds byte: %.*s: %.*s\n"),
 1556                               (int)input->l, input->s,
 1557                               (int)P2UZ(ib - ib_save), ib_save);
 1558                         /* Write unchanged */
 1559 jerr_ib_save:
 1560                         rv |= n_SHEXP_STATE_OUTPUT;
 1561                         if(!(flags & n_SHEXP_PARSE_DRYRUN))
 1562                            store = n_string_push_buf(store, ib_save,
 1563                                  P2UZ(ib - ib_save));
 1564                         continue;
 1565                      }
 1566                      c2 = (c2 << 3) | (c -= '0');
 1567                      --il, ++ib;
 1568                   }
 1569                   if(state & a_SKIPMASK)
 1570                      continue;
 1571                   if((c = c2) == '\0'){
 1572                      state |= a_SKIPQ;
 1573                      continue;
 1574                   }
 1575                   break;
 1576 
 1577                /* ISO 10646 / Unicode sequence, 8 or 4 hexadecimal bytes */
 1578                case 'U':
 1579                   i = 8;
 1580                   if(0){
 1581                   /* FALLTHRU */
 1582                case 'u':
 1583                      i = 4;
 1584                   }
 1585                   if(il == 0)
 1586                      goto j_dollar_ungetc;
 1587                   if(0){
 1588                      /* FALLTHRU */
 1589 
 1590                /* Hexadecimal sequence, 1 or 2 hexadecimal bytes */
 1591                case 'X':
 1592                case 'x':
 1593                      if(il == 0)
 1594                         goto j_dollar_ungetc;
 1595                      i = 2;
 1596                   }
 1597                   /* C99 */{
 1598                      static u8 const hexatoi[] = { /* XXX uses ASCII */
 1599                         0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15
 1600                      };
 1601                      uz no, j;
 1602 
 1603                      i = MIN(il, i);
 1604                      for(no = j = 0; i-- > 0; --il, ++ib, ++j){
 1605                         c = *ib;
 1606                         if(su_cs_is_xdigit(c)){
 1607                            no <<= 4;
 1608                            no += hexatoi[(u8)((c) - ((c) <= '9' ? 48
 1609                                  : ((c) <= 'F' ? 55 : 87)))];
 1610                         }else if(j == 0){
 1611                            if(state & a_SKIPMASK)
 1612                               break;
 1613                            c2 = (c2 == 'U' || c2 == 'u') ? 'u' : 'x';
 1614                            if(flags & n_SHEXP_PARSE_LOG)
 1615                               n_err(_("Invalid \\%c notation: %.*s: %.*s\n"),
 1616                                  c2, (int)input->l, input->s,
 1617                                  (int)P2UZ(ib - ib_save), ib_save);
 1618                            rv |= n_SHEXP_STATE_ERR_NUMBER;
 1619                            goto jerr_ib_save;
 1620                         }else
 1621                            break;
 1622                      }
 1623 
 1624                      /* Unicode massage */
 1625                      if((c2 != 'U' && c2 != 'u') || su_cs_is_ascii(no)){
 1626                         if((c = (char)no) == '\0')
 1627                            state |= a_SKIPQ;
 1628                      }else if(no == 0)
 1629                         state |= a_SKIPQ;
 1630                      else if(!(state & a_SKIPMASK)){
 1631                         if(!(flags & n_SHEXP_PARSE_DRYRUN))
 1632                            store = n_string_reserve(store, MAX(j, 4));
 1633 
 1634                         if(no > 0x10FFFF){ /* XXX magic; CText */
 1635                            if(flags & n_SHEXP_PARSE_LOG)
 1636                               n_err(_("\\U argument exceeds 0x10FFFF: %.*s: "
 1637                                     "%.*s\n"),
 1638                                  (int)input->l, input->s,
 1639                                  (int)P2UZ(ib - ib_save), ib_save);
 1640                            rv |= n_SHEXP_STATE_ERR_NUMBER;
 1641                            /* But normalize the output anyway */
 1642                            goto Jerr_uni_norm;
 1643                         }
 1644 
 1645                         j = su_utf32_to_8(no, utf);
 1646 
 1647                         if(n_psonce & n_PSO_UNICODE){
 1648                            rv |= n_SHEXP_STATE_OUTPUT | n_SHEXP_STATE_UNICODE;
 1649                            if(!(flags & n_SHEXP_PARSE_DRYRUN))
 1650                               store = n_string_push_buf(store, utf, j);
 1651                            continue;
 1652                         }
 1653 #ifdef mx_HAVE_ICONV
 1654                         else{
 1655                            char *icp;
 1656 
 1657                            icp = n_iconv_onetime_cp(n_ICONV_NONE,
 1658                                  NULL, NULL, utf);
 1659                            if(icp != NULL){
 1660                               rv |= n_SHEXP_STATE_OUTPUT;
 1661                               if(!(flags & n_SHEXP_PARSE_DRYRUN))
 1662                                  store = n_string_push_cp(store, icp);
 1663                               continue;
 1664                            }
 1665                         }
 1666 #endif
 1667                         if(!(flags & n_SHEXP_PARSE_DRYRUN)) Jerr_uni_norm:{
 1668                            char itoa[32];
 1669 
 1670                            rv |= n_SHEXP_STATE_OUTPUT |
 1671                                  n_SHEXP_STATE_ERR_UNICODE;
 1672                            i = snprintf(itoa, sizeof itoa, "\\%c%0*X",
 1673                                  (no > 0xFFFFu ? 'U' : 'u'),
 1674                                  (int)(no > 0xFFFFu ? 8 : 4), (u32)no);
 1675                            store = n_string_push_buf(store, itoa, i);
 1676                         }
 1677                         continue;
 1678                      }
 1679                      if(state & a_SKIPMASK)
 1680                         continue;
 1681                   }
 1682                   break;
 1683 
 1684                /* Extension: \$ can be used to expand a variable.
 1685                 * B(ug|ad) effect: if conversion fails, not written "as-is" */
 1686                case '$':
 1687                   if(il == 0)
 1688                      goto j_dollar_ungetc;
 1689                   goto J_var_expand;
 1690 
 1691                default:
 1692 j_dollar_ungetc:
 1693                   /* Follow bash(1) behaviour, print sequence unchanged */
 1694                   ++il, --ib;
 1695                   break;
 1696                }
 1697             }
 1698          }else if(c == '$' && quotec == '"' && il > 0) J_var_expand:{
 1699             state &= ~a_VARSUBST_MASK;
 1700             if(*ib == '{')
 1701                state |= a_BRACE;
 1702 
 1703             /* Scan variable name */
 1704             if(!(state & a_BRACE) || il > 1){
 1705                char const *cp, *vp;
 1706 
 1707                ib_save = ib - 1;
 1708                if(state & a_BRACE)
 1709                   --il, ++ib;
 1710                vp = ib;
 1711                state &= ~a_EXPLODE;
 1712 
 1713                for(i = 0; il > 0; --il, ++ib, ++i){
 1714                   /* We have some special cases regarding special parameters,
 1715                    * so ensure these don't cause failure.  This code has
 1716                    * counterparts in code that manages internal variables! */
 1717                   c = *ib;
 1718                   if(!a_SHEXP_ISVARC(c)){
 1719                      if(i == 0){
 1720                         /* Simply skip over multiplexer */
 1721                         if(c == '^')
 1722                            continue;
 1723                         if(c == '*' || c == '@' || c == '#' || c == '?' ||
 1724                               c == '!'){
 1725                            if(c == '@'){
 1726                               if(quotec == '"')
 1727                                  state |= a_EXPLODE;
 1728                            }
 1729                            --il, ++ib;
 1730                            ++i;
 1731                         }
 1732                      }
 1733                      break;
 1734                   }else if(a_SHEXP_ISVARC_BAD1ST(c)){
 1735                      if(i == 0)
 1736                         state |= a_DIGIT1;
 1737                   }else
 1738                      state |= a_NONDIGIT;
 1739                }
 1740 
 1741                /* In skip mode, be easy and.. skip over */
 1742                if(state & a_SKIPMASK){
 1743                   if((state & a_BRACE) && il > 0 && *ib == '}')
 1744                      --il, ++ib;
 1745                   continue;
 1746                }
 1747 
 1748                /* Handle the scan error cases */
 1749                if((state & (a_DIGIT1 | a_NONDIGIT)) ==
 1750                      (a_DIGIT1 | a_NONDIGIT)){
 1751                   if(state & a_BRACE){
 1752                      if(il > 0 && *ib == '}')
 1753                         --il, ++ib;
 1754                      else
 1755                         rv |= n_SHEXP_STATE_ERR_GROUPOPEN;
 1756                   }
 1757                   if(flags & n_SHEXP_PARSE_LOG)
 1758                      n_err(_("Invalid identifier for ${}: %.*s: %.*s\n"),
 1759                         (int)input->l, input->s,
 1760                         (int)P2UZ(ib - ib_save), ib_save);
 1761                   rv |= n_SHEXP_STATE_ERR_IDENTIFIER;
 1762                   goto jerr_ib_save;
 1763                }else if(i == 0){
 1764                   if(state & a_BRACE){
 1765                      if(il == 0 || *ib != '}'){
 1766                         if(flags & n_SHEXP_PARSE_LOG)
 1767                            n_err(_("No closing brace for ${}: %.*s: %.*s\n"),
 1768                               (int)input->l, input->s,
 1769                               (int)P2UZ(ib - ib_save), ib_save);
 1770                         rv |= n_SHEXP_STATE_ERR_GROUPOPEN;
 1771                         goto jerr_ib_save;
 1772                      }
 1773                      --il, ++ib;
 1774 
 1775                      if(i == 0){
 1776                         if(flags & n_SHEXP_PARSE_LOG)
 1777                            n_err(_("Bad substitution for ${}: %.*s: %.*s\n"),
 1778                               (int)input->l, input->s,
 1779                               (int)P2UZ(ib - ib_save), ib_save);
 1780                         rv |= n_SHEXP_STATE_ERR_BADSUB;
 1781                         goto jerr_ib_save;
 1782                      }
 1783                   }
 1784                   /* Simply write dollar as-is? */
 1785                   c = '$';
 1786                }else{
 1787                   if(state & a_BRACE){
 1788                      if(il == 0 || *ib != '}'){
 1789                         if(flags & n_SHEXP_PARSE_LOG)
 1790                            n_err(_("No closing brace for ${}: %.*s: %.*s\n"),
 1791                               (int)input->l, input->s,
 1792                               (int)P2UZ(ib - ib_save), ib_save);
 1793                         rv |= n_SHEXP_STATE_ERR_GROUPOPEN;
 1794                         goto jerr_ib_save;
 1795                      }
 1796                      --il, ++ib;
 1797 
 1798                      if(i == 0){
 1799                         if(flags & n_SHEXP_PARSE_LOG)
 1800                            n_err(_("Bad substitution for ${}: %.*s: %.*s\n"),
 1801                               (int)input->l, input->s,
 1802                               (int)P2UZ(ib - ib_save), ib_save);
 1803                         rv |= n_SHEXP_STATE_ERR_BADSUB;
 1804                         goto jerr_ib_save;
 1805                      }
 1806                   }
 1807 
 1808                   if(flags & n_SHEXP_PARSE_DRYRUN)
 1809                      continue;
 1810 
 1811                   /* We may shall explode "${@}" to a series of successive,
 1812                    * properly quoted tokens (instead).  The first exploded
 1813                    * cookie will join with the current token */
 1814                   if(UNLIKELY(state & a_EXPLODE) &&
 1815                         !(flags & n_SHEXP_PARSE_DRYRUN) && cookie != NULL){
 1816                      if(n_var_vexplode(cookie))
 1817                         state |= a_COOKIE;
 1818                      /* On the other hand, if $@ expands to nothing and is the
 1819                       * sole content of this quote then act like the shell does
 1820                       * and throw away the entire atxplode construct */
 1821                      else if(!(rv & n_SHEXP_STATE_OUTPUT) &&
 1822                            il == 1 && *ib == '"' &&
 1823                            ib_save == &input->s[1] && ib_save[-1] == '"')
 1824                         ++ib, --il;
 1825                      else
 1826                         continue;
 1827                      input->s = n_UNCONST(ib);
 1828                      input->l = il;
 1829                      goto jrestart_empty;
 1830                   }
 1831 
 1832                   /* Check getenv(3) shall no internal variable exist!
 1833                    * XXX We have some common idioms, avoid memory for them
 1834                    * XXX Even better would be var_vlook_buf()! */
 1835                   if(i == 1){
 1836                      switch(*vp){
 1837                      case '?': vp = n_qm; break;
 1838                      case '!': vp = n_em; break;
 1839                      case '*': vp = n_star; break;
 1840                      case '@': vp = n_at; break;
 1841                      case '#': vp = n_ns; break;
 1842                      default: goto j_var_look_buf;
 1843                      }
 1844                   }else
 1845 j_var_look_buf:
 1846                      vp = savestrbuf(vp, i);
 1847 
 1848                   if((cp = n_var_vlook(vp, TRU1)) != NULL){
 1849                      rv |= n_SHEXP_STATE_OUTPUT;
 1850                      store = n_string_push_cp(store, cp);
 1851                      for(; (c = *cp) != '\0'; ++cp)
 1852                         if(su_cs_is_cntrl(c)){
 1853                            rv |= n_SHEXP_STATE_CONTROL;
 1854                            break;
 1855                         }
 1856                   }
 1857                   continue;
 1858                }
 1859             }
 1860          }else if(c == '`' && quotec == '"' && il > 0){ /* TODO sh command */
 1861             continue;
 1862          }
 1863       }
 1864 
 1865       if(!(state & a_SKIPMASK)){
 1866          rv |= n_SHEXP_STATE_OUTPUT;
 1867          if(su_cs_is_cntrl(c))
 1868             rv |= n_SHEXP_STATE_CONTROL;
 1869          if(!(flags & n_SHEXP_PARSE_DRYRUN))
 1870             store = n_string_push_c(store, c);
 1871       }
 1872    } /* }}} */
 1873 
 1874    if(quotec != '\0' && !(flags & n_SHEXP_PARSE_QUOTE_AUTO_CLOSE)){
 1875       if(flags & n_SHEXP_PARSE_LOG)
 1876          n_err(_("No closing quote: %.*s\n"), (int)input->l, input->s);
 1877       rv |= n_SHEXP_STATE_ERR_QUOTEOPEN;
 1878    }
 1879 
 1880 jleave:
 1881    ASSERT(!(state & a_COOKIE));
 1882    if((flags & n_SHEXP_PARSE_DRYRUN) && store != NULL){
 1883       store = n_string_push_buf(store, input->s, P2UZ(ib - input->s));
 1884       rv |= n_SHEXP_STATE_OUTPUT;
 1885    }
 1886 
 1887    if(state & a_CHOP_ONE)
 1888       ++ib, --il;
 1889 
 1890    if(il > 0){
 1891       if(flags & n_SHEXP_PARSE_TRIM_SPACE){
 1892          for(; il > 0; ++ib, --il){
 1893             if(!su_cs_is_space(*ib))
 1894                break;
 1895             rv |= n_SHEXP_STATE_WS_TRAIL;
 1896          }
 1897       }
 1898 
 1899       if(flags & n_SHEXP_PARSE_TRIM_IFSSPACE){
 1900          for(; il > 0; ++ib, --il){
 1901             if(su_cs_find_c(ifs_ws, *ib) == NIL)
 1902                break;
 1903             rv |= n_SHEXP_STATE_WS_TRAIL;
 1904          }
 1905       }
 1906 
 1907       /* At the start of the next token: if this is a comment, simply throw
 1908        * away all the following data! */
 1909       if(il > 0 && *ib == '#'){
 1910          ib += il;
 1911          il = 0;
 1912       }
 1913    }
 1914 
 1915    input->l = il;
 1916    input->s = UNCONST(char*,ib);
 1917 
 1918    if(!(rv & n_SHEXP_STATE_STOP)){
 1919       if(!(rv & (n_SHEXP_STATE_OUTPUT | n_SHEXP_STATE_META_MASK)) &&
 1920             (flags & n_SHEXP_PARSE_IGNORE_EMPTY) && il > 0)
 1921          goto jrestart_empty;
 1922       if(/*!(rv & n_SHEXP_STATE_OUTPUT) &&*/ il == 0)
 1923          rv |= n_SHEXP_STATE_STOP;
 1924    }
 1925 
 1926    if((state & a_SKIPT) && !(rv & n_SHEXP_STATE_STOP) &&
 1927          (flags & n_SHEXP_PARSE_META_MASK))
 1928       goto jrestart;
 1929 jleave_quick:
 1930    ASSERT((rv & n_SHEXP_STATE_OUTPUT) || !(rv & n_SHEXP_STATE_UNICODE));
 1931    ASSERT((rv & n_SHEXP_STATE_OUTPUT) || !(rv & n_SHEXP_STATE_CONTROL));
 1932    NYD2_OU;
 1933    return rv;
 1934 }
 1935 
 1936 FL char *
 1937 n_shexp_parse_token_cp(BITENUM_IS(u32,n_shexp_parse_flags) flags,
 1938       char const **cp){
 1939    struct str input;
 1940    struct n_string sou, *soup;
 1941    char *rv;
 1942    BITENUM_IS(u32,n_shexp_state) shs;
 1943    NYD2_IN;
 1944 
 1945    ASSERT(cp != NULL);
 1946 
 1947    input.s = UNCONST(char*,*cp);
 1948    input.l = UZ_MAX;
 1949    soup = n_string_creat_auto(&sou);
 1950 
 1951    shs = n_shexp_parse_token(flags, soup, &input, NULL);
 1952    if(shs & n_SHEXP_STATE_ERR_MASK){
 1953       soup = n_string_assign_cp(soup, *cp);
 1954       *cp = NULL;
 1955    }else
 1956       *cp = input.s;
 1957 
 1958    rv = n_string_cp(soup);
 1959    /*n_string_gut(n_string_drop_ownership(soup));*/
 1960    NYD2_OU;
 1961    return rv;
 1962 }
 1963 
 1964 FL boole
 1965 n_shexp_unquote_one(struct n_string *store, char const *input){
 1966    struct str dat;
 1967    BITENUM_IS(u32,n_shexp_state) shs;
 1968    boole rv;
 1969    NYD_IN;
 1970 
 1971    dat.s = UNCONST(char*,input);
 1972    dat.l = UZ_MAX;
 1973    shs = n_shexp_parse_token((n_SHEXP_PARSE_TRUNC |
 1974          n_SHEXP_PARSE_TRIM_SPACE | n_SHEXP_PARSE_LOG |
 1975          n_SHEXP_PARSE_IGNORE_EMPTY),
 1976          store, &dat, NIL);
 1977 
 1978 #ifdef mx_HAVE_UISTRINGS
 1979    if(!(shs & n_SHEXP_STATE_STOP))
 1980       n_err(_("# Only one (shell-quoted) argument is expected: %s\n"), input);
 1981 #endif
 1982 
 1983    if((shs & (n_SHEXP_STATE_OUTPUT | n_SHEXP_STATE_STOP |
 1984             n_SHEXP_STATE_ERR_MASK)
 1985          ) != (n_SHEXP_STATE_OUTPUT | n_SHEXP_STATE_STOP))
 1986       rv = FAL0;
 1987    else if(!(shs & n_SHEXP_STATE_STOP))
 1988       rv = TRUM1;
 1989    else if(shs & n_SHEXP_STATE_OUTPUT)
 1990       rv = TRU1;
 1991    else
 1992       rv = TRU2;
 1993 
 1994    NYD_OU;
 1995    return rv;
 1996 }
 1997 
 1998 FL struct n_string *
 1999 n_shexp_quote(struct n_string *store, struct str const *input, boole rndtrip){
 2000    struct a_shexp_quote_lvl sql;
 2001    struct a_shexp_quote_ctx sqc;
 2002    NYD2_IN;
 2003 
 2004    ASSERT(store != NULL);
 2005    ASSERT(input != NULL);
 2006    ASSERT(input->l == 0 || input->s != NULL);
 2007 
 2008    su_mem_set(&sqc, 0, sizeof sqc);
 2009    sqc.sqc_store = store;
 2010    sqc.sqc_input.s = input->s;
 2011    if((sqc.sqc_input.l = input->l) == UZ_MAX)
 2012       sqc.sqc_input.l = su_cs_len(input->s);
 2013    sqc.sqc_flags = rndtrip ? a_SHEXP_QUOTE_ROUNDTRIP : a_SHEXP_QUOTE_NONE;
 2014 
 2015    if(sqc.sqc_input.l == 0)
 2016       store = n_string_push_buf(store, "''", sizeof("''") -1);
 2017    else{
 2018       su_mem_set(&sql, 0, sizeof sql);
 2019       sql.sql_dat = sqc.sqc_input;
 2020       sql.sql_flags = sqc.sqc_flags;
 2021       a_shexp__quote(&sqc, &sql);
 2022    }
 2023    NYD2_OU;
 2024    return store;
 2025 }
 2026 
 2027 FL char *
 2028 n_shexp_quote_cp(char const *cp, boole rndtrip){
 2029    struct n_string store;
 2030    struct str input;
 2031    char *rv;
 2032    NYD2_IN;
 2033 
 2034    ASSERT(cp != NULL);
 2035 
 2036    input.s = UNCONST(char*,cp);
 2037    input.l = UZ_MAX;
 2038    rv = n_string_cp(n_shexp_quote(n_string_creat_auto(&store), &input,
 2039          rndtrip));
 2040    n_string_gut(n_string_drop_ownership(&store));
 2041    NYD2_OU;
 2042    return rv;
 2043 }
 2044 
 2045 FL boole
 2046 n_shexp_is_valid_varname(char const *name, boole forenviron){
 2047    char lc, c;
 2048    boole rv;
 2049    NYD2_IN;
 2050 
 2051    rv = FAL0;
 2052    lc = '\0';
 2053 
 2054    if(!forenviron){
 2055       for(; (c = *name++) != '\0'; lc = c)
 2056          if(!a_SHEXP_ISVARC(c))
 2057             goto jleave;
 2058          else if(lc == '\0' && a_SHEXP_ISVARC_BAD1ST(c))
 2059             goto jleave;
 2060       if(a_SHEXP_ISVARC_BADNST(lc))
 2061          goto jleave;
 2062    }else{
 2063       for(; (c = *name++) != '\0'; lc = c)
 2064          if(!a_SHEXP_ISENVVARC(c))
 2065             goto jleave;
 2066          else if(lc == '\0' && a_SHEXP_ISENVVARC_BAD1ST(c))
 2067             goto jleave;
 2068       if(a_SHEXP_ISENVVARC_BADNST(lc))
 2069          goto jleave;
 2070    }
 2071 
 2072    rv = TRU1;
 2073 jleave:
 2074    NYD2_OU;
 2075    return rv;
 2076 }
 2077 
 2078 FL int
 2079 c_shcodec(void *vp){
 2080    struct str in;
 2081    struct n_string sou_b, *soup;
 2082    s32 nerrn;
 2083    uz alen;
 2084    boole norndtrip;
 2085    char const **argv, *varname, *act, *cp;
 2086    NYD_IN;
 2087 
 2088    soup = n_string_creat_auto(&sou_b);
 2089    argv = vp;
 2090    varname = (n_pstate & n_PS_ARGMOD_VPUT) ? *argv++ : NULL;
 2091 
 2092    act = *argv;
 2093    for(cp = act; *cp != '\0' && !su_cs_is_space(*cp); ++cp)
 2094       ;
 2095    if((norndtrip = (*act == '+')))
 2096       ++act;
 2097    if(act == cp)
 2098       goto jesynopsis;
 2099    alen = P2UZ(cp - act);
 2100    if(*cp != '\0')
 2101       ++cp;
 2102 
 2103    in.l = su_cs_len(in.s = UNCONST(char*,cp));
 2104    nerrn = su_ERR_NONE;
 2105 
 2106    if(su_cs_starts_with_case_n("encode", act, alen))
 2107       soup = n_shexp_quote(soup, &in, !norndtrip);
 2108    else if(!norndtrip && su_cs_starts_with_case_n("decode", act, alen)){
 2109       for(;;){
 2110          BITENUM_IS(u32,n_shexp_state) shs;
 2111 
 2112          shs = n_shexp_parse_token((n_SHEXP_PARSE_LOG |
 2113                n_SHEXP_PARSE_IGNORE_EMPTY), soup, &in, NULL);
 2114          if(shs & n_SHEXP_STATE_ERR_MASK){
 2115             soup = n_string_assign_cp(soup, cp);
 2116             nerrn = su_ERR_CANCELED;
 2117             vp = NULL;
 2118             break;
 2119          }
 2120          if(shs & n_SHEXP_STATE_STOP)
 2121             break;
 2122       }
 2123    }else
 2124       goto jesynopsis;
 2125 
 2126    if(varname != NULL){
 2127       cp = n_string_cp(soup);
 2128       if(!n_var_vset(varname, (up)cp)){
 2129          nerrn = su_ERR_NOTSUP;
 2130          vp = NULL;
 2131       }
 2132    }else{
 2133       struct str out;
 2134 
 2135       in.s = n_string_cp(soup);
 2136       in.l = soup->s_len;
 2137       makeprint(&in, &out);
 2138       if(fprintf(n_stdout, "%s\n", out.s) < 0){
 2139          nerrn = su_err_no();
 2140          vp = NULL;
 2141       }
 2142       n_free(out.s);
 2143    }
 2144 
 2145 jleave:
 2146    n_pstate_err_no = nerrn;
 2147    NYD_OU;
 2148    return (vp != NULL ? 0 : 1);
 2149 jesynopsis:
 2150    mx_cmd_print_synopsis(mx_cmd_firstfit("shcodec"), NIL);
 2151    nerrn = su_ERR_INVAL;
 2152    vp = NULL;
 2153    goto jleave;
 2154 }
 2155 
 2156 #include "su/code-ou.h"
 2157 /* s-it-mode */