コード例 #1
0
ファイル: batch.c プロジェクト: WASSUM/longene_travel
/****************************************************************************
 * WCMD_HandleTildaModifiers
 *
 * Handle the ~ modifiers when expanding %0-9 or (%a-z in for command)
 *    %~xxxxxV  (V=0-9 or A-Z)
 * Where xxxx is any combination of:
 *    ~ - Removes quotes
 *    f - Fully qualified path (assumes current dir if not drive\dir)
 *    d - drive letter
 *    p - path
 *    n - filename
 *    x - file extension
 *    s - path with shortnames
 *    a - attributes
 *    t - date/time
 *    z - size
 *    $ENVVAR: - Searches ENVVAR for (contents of V) and expands to fully
 *                   qualified path
 *
 *  To work out the length of the modifier:
 *
 *  Note: In the case of %0-9 knowing the end of the modifier is easy,
 *    but in a for loop, the for end WCHARacter may also be a modifier
 *    eg. for %a in (c:\a.a) do echo XXX
 *             where XXX = %~a    (just ~)
 *                         %~aa   (~ and attributes)
 *                         %~aaxa (~, attributes and extension)
 *                   BUT   %~aax  (~ and attributes followed by 'x')
 *
 *  Hence search forwards until find an invalid modifier, and then
 *  backwards until find for variable or 0-9
 */
void WCMD_HandleTildaModifiers(WCHAR **start, WCHAR *forVariable, WCHAR *forValue, BOOL justFors) {

#define NUMMODIFIERS 11
  static const WCHAR validmodifiers[NUMMODIFIERS] = {
        '~', 'f', 'd', 'p', 'n', 'x', 's', 'a', 't', 'z', '$'
  };
  static const WCHAR space[] = {' ', '\0'};

  WIN32_FILE_ATTRIBUTE_DATA fileInfo;
  WCHAR  outputparam[MAX_PATH];
  WCHAR  finaloutput[MAX_PATH];
  WCHAR  fullfilename[MAX_PATH];
  WCHAR  thisoutput[MAX_PATH];
  WCHAR  *pos            = *start+1;
  WCHAR  *firstModifier  = pos;
  WCHAR  *lastModifier   = NULL;
  int   modifierLen     = 0;
  BOOL  finished        = FALSE;
  int   i               = 0;
  BOOL  exists          = TRUE;
  BOOL  skipFileParsing = FALSE;
  BOOL  doneModifier    = FALSE;

  /* Search forwards until find invalid character modifier */
  while (!finished) {

    /* Work on the previous character */
    if (lastModifier != NULL) {

      for (i=0; i<NUMMODIFIERS; i++) {
        if (validmodifiers[i] == *lastModifier) {

          /* Special case '$' to skip until : found */
          if (*lastModifier == '$') {
            while (*pos != ':' && *pos) pos++;
            if (*pos == 0x00) return; /* Invalid syntax */
            pos++;                    /* Skip ':'       */
          }
          break;
        }
      }

      if (i==NUMMODIFIERS) {
        finished = TRUE;
      }
    }

    /* Save this one away */
    if (!finished) {
      lastModifier = pos;
      pos++;
    }
  }

  while (lastModifier > firstModifier) {
    WINE_TRACE("Looking backwards for parameter id: %s / %s\n",
               wine_dbgstr_w(lastModifier), wine_dbgstr_w(forVariable));

    if (!justFors && context && (*lastModifier >= '0' || *lastModifier <= '9')) {
      /* Its a valid parameter identifier - OK */
      break;

    } else if (forVariable && *lastModifier == *(forVariable+1)) {
      /* Its a valid parameter identifier - OK */
      break;

    } else {
      lastModifier--;
    }
  }
  if (lastModifier == firstModifier) return; /* Invalid syntax */

  /* Extract the parameter to play with */
  if ((*lastModifier >= '0' && *lastModifier <= '9')) {
    strcpyW(outputparam, WCMD_parameter (context -> command,
                 *lastModifier-'0' + context -> shift_count[*lastModifier-'0'], NULL));
  } else {
    strcpyW(outputparam, forValue);
  }

  /* So now, firstModifier points to beginning of modifiers, lastModifier
     points to the variable just after the modifiers. Process modifiers
     in a specific order, remembering there could be duplicates           */
  modifierLen = lastModifier - firstModifier;
  finaloutput[0] = 0x00;

  /* Useful for debugging purposes: */
  /*printf("Modifier string '%*.*s' and variable is %c\n Param starts as '%s'\n",
             (modifierLen), (modifierLen), firstModifier, *lastModifier,
             outputparam);*/

  /* 1. Handle '~' : Strip surrounding quotes */
  if (outputparam[0]=='"' &&
      memchrW(firstModifier, '~', modifierLen) != NULL) {
    int len = strlenW(outputparam);
    if (outputparam[len-1] == '"') {
        outputparam[len-1]=0x00;
        len = len - 1;
    }
    memmove(outputparam, &outputparam[1], (len * sizeof(WCHAR))-1);
  }

  /* 2. Handle the special case of a $ */
  if (memchrW(firstModifier, '$', modifierLen) != NULL) {
    /* Special Case: Search envar specified in $[envvar] for outputparam
       Note both $ and : are guaranteed otherwise check above would fail */
    WCHAR *start = strchrW(firstModifier, '$') + 1;
    WCHAR *end   = strchrW(firstModifier, ':');
    WCHAR env[MAX_PATH];
    WCHAR fullpath[MAX_PATH];

    /* Extract the env var */
    memcpy(env, start, (end-start) * sizeof(WCHAR));
    env[(end-start)] = 0x00;

    /* If env var not found, return empty string */
    if ((GetEnvironmentVariable(env, fullpath, MAX_PATH) == 0) ||
        (SearchPath(fullpath, outputparam, NULL,
                    MAX_PATH, outputparam, NULL) == 0)) {
      finaloutput[0] = 0x00;
      outputparam[0] = 0x00;
      skipFileParsing = TRUE;
    }
  }

  /* After this, we need full information on the file,
    which is valid not to exist.  */
  if (!skipFileParsing) {
    if (GetFullPathName(outputparam, MAX_PATH, fullfilename, NULL) == 0)
      return;

    exists = GetFileAttributesExW(fullfilename, GetFileExInfoStandard,
                                  &fileInfo);

    /* 2. Handle 'a' : Output attributes */
    if (exists &&
        memchrW(firstModifier, 'a', modifierLen) != NULL) {

      WCHAR defaults[] = {'-','-','-','-','-','-','-','-','-','\0'};
      doneModifier = TRUE;
      strcpyW(thisoutput, defaults);
      if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
        thisoutput[0]='d';
      if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_READONLY)
        thisoutput[1]='r';
      if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_ARCHIVE)
        thisoutput[2]='a';
      if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN)
        thisoutput[3]='h';
      if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_SYSTEM)
        thisoutput[4]='s';
      if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_COMPRESSED)
        thisoutput[5]='c';
      /* FIXME: What are 6 and 7? */
      if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT)
        thisoutput[8]='l';
      strcatW(finaloutput, thisoutput);
    }

    /* 3. Handle 't' : Date+time */
    if (exists &&
        memchrW(firstModifier, 't', modifierLen) != NULL) {

      SYSTEMTIME systime;
      int datelen;

      doneModifier = TRUE;
      if (finaloutput[0] != 0x00) strcatW(finaloutput, space);

      /* Format the time */
      FileTimeToSystemTime(&fileInfo.ftLastWriteTime, &systime);
      GetDateFormat(LOCALE_USER_DEFAULT, DATE_SHORTDATE, &systime,
                        NULL, thisoutput, MAX_PATH);
      strcatW(thisoutput, space);
      datelen = strlenW(thisoutput);
      GetTimeFormat(LOCALE_USER_DEFAULT, TIME_NOSECONDS, &systime,
                        NULL, (thisoutput+datelen), MAX_PATH-datelen);
      strcatW(finaloutput, thisoutput);
    }

    /* 4. Handle 'z' : File length */
    if (exists &&
        memchrW(firstModifier, 'z', modifierLen) != NULL) {
      /* FIXME: Output full 64 bit size (sprintf does not support I64 here) */
      ULONG/*64*/ fullsize = /*(fileInfo.nFileSizeHigh << 32) +*/
                                  fileInfo.nFileSizeLow;
      static const WCHAR fmt[] = {'%','u','\0'};

      doneModifier = TRUE;
      if (finaloutput[0] != 0x00) strcatW(finaloutput, space);
      wsprintf(thisoutput, fmt, fullsize);
      strcatW(finaloutput, thisoutput);
    }

    /* 4. Handle 's' : Use short paths (File doesn't have to exist) */
    if (memchrW(firstModifier, 's', modifierLen) != NULL) {
      if (finaloutput[0] != 0x00) strcatW(finaloutput, space);
      /* Don't flag as doneModifier - %~s on its own is processed later */
      GetShortPathName(outputparam, outputparam,
                       sizeof(outputparam)/sizeof(outputparam[0]));
    }

    /* 5. Handle 'f' : Fully qualified path (File doesn't have to exist) */
    /*      Note this overrides d,p,n,x                                 */
    if (memchrW(firstModifier, 'f', modifierLen) != NULL) {
      doneModifier = TRUE;
      if (finaloutput[0] != 0x00) strcatW(finaloutput, space);
      strcatW(finaloutput, fullfilename);
    } else {

      WCHAR drive[10];
      WCHAR dir[MAX_PATH];
      WCHAR fname[MAX_PATH];
      WCHAR ext[MAX_PATH];
      BOOL doneFileModifier = FALSE;

      if (finaloutput[0] != 0x00) strcatW(finaloutput, space);

      /* Split into components */
      WCMD_splitpath(fullfilename, drive, dir, fname, ext);

      /* 5. Handle 'd' : Drive Letter */
      if (memchrW(firstModifier, 'd', modifierLen) != NULL) {
        strcatW(finaloutput, drive);
        doneModifier = TRUE;
        doneFileModifier = TRUE;
      }

      /* 6. Handle 'p' : Path */
      if (memchrW(firstModifier, 'p', modifierLen) != NULL) {
        strcatW(finaloutput, dir);
        doneModifier = TRUE;
        doneFileModifier = TRUE;
      }

      /* 7. Handle 'n' : Name */
      if (memchrW(firstModifier, 'n', modifierLen) != NULL) {
        strcatW(finaloutput, fname);
        doneModifier = TRUE;
        doneFileModifier = TRUE;
      }

      /* 8. Handle 'x' : Ext */
      if (memchrW(firstModifier, 'x', modifierLen) != NULL) {
        strcatW(finaloutput, ext);
        doneModifier = TRUE;
        doneFileModifier = TRUE;
      }

      /* If 's' but no other parameter, dump the whole thing */
      if (!doneFileModifier &&
          memchrW(firstModifier, 's', modifierLen) != NULL) {
        doneModifier = TRUE;
        if (finaloutput[0] != 0x00) strcatW(finaloutput, space);
        strcatW(finaloutput, outputparam);
      }
    }
  }

  /* If No other modifier processed,  just add in parameter */
  if (!doneModifier) strcpyW(finaloutput, outputparam);

  /* Finish by inserting the replacement into the string */
  pos = WCMD_strdupW(lastModifier+1);
  strcpyW(*start, finaloutput);
  strcatW(*start, pos);
  free(pos);
}
コード例 #2
0
void WCMD_directory (WCHAR *cmd)
{
  WCHAR path[MAX_PATH], cwd[MAX_PATH];
  DWORD status;
  CONSOLE_SCREEN_BUFFER_INFO consoleInfo;
  WCHAR *p;
  WCHAR string[MAXSTRING];
  int   argno         = 0;
  WCHAR *argN          = cmd;
  WCHAR  lastDrive;
  BOOL  trailerReqd = FALSE;
  DIRECTORY_STACK *fullParms = NULL;
  DIRECTORY_STACK *prevEntry = NULL;
  DIRECTORY_STACK *thisEntry = NULL;
  WCHAR drive[10];
  WCHAR dir[MAX_PATH];
  WCHAR fname[MAX_PATH];
  WCHAR ext[MAX_PATH];
  static const WCHAR dircmdW[] = {'D','I','R','C','M','D','\0'};

  errorlevel = 0;

  /* Prefill quals with (uppercased) DIRCMD env var */
  if (GetEnvironmentVariableW(dircmdW, string, sizeof(string)/sizeof(WCHAR))) {
    p = string;
    while ( (*p = toupper(*p)) ) ++p;
    strcatW(string,quals);
    strcpyW(quals, string);
  }

  byte_total = 0;
  file_total = dir_total = 0;

  /* Initialize all flags to their defaults as if no DIRCMD or quals */
  paged_mode = FALSE;
  recurse    = FALSE;
  wide       = FALSE;
  bare       = FALSE;
  lower      = FALSE;
  shortname  = FALSE;
  usernames  = FALSE;
  orderByCol = FALSE;
  separator  = TRUE;
  dirTime = Written;
  dirOrder = Name;
  orderReverse = FALSE;
  orderGroupDirs = FALSE;
  orderGroupDirsReverse = FALSE;
  showattrs  = 0;
  attrsbits  = FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_SYSTEM;

  /* Handle args - Loop through so right most is the effective one */
  /* Note: /- appears to be a negate rather than an off, eg. dir
           /-W is wide, or dir /w /-w /-w is also wide             */
  p = quals;
  while (*p && (*p=='/' || *p==' ')) {
    BOOL negate = FALSE;
    if (*p++==' ') continue;  /* Skip / and blanks introduced through DIRCMD */

    if (*p=='-') {
      negate = TRUE;
      p++;
    }

    WINE_TRACE("Processing arg '%c' (in %s)\n", *p, wine_dbgstr_w(quals));
    switch (*p) {
    case 'P': if (negate) paged_mode = !paged_mode;
              else paged_mode = TRUE;
              break;
    case 'S': if (negate) recurse = !recurse;
              else recurse = TRUE;
              break;
    case 'W': if (negate) wide = !wide;
              else wide = TRUE;
              break;
    case 'B': if (negate) bare = !bare;
              else bare = TRUE;
              break;
    case 'L': if (negate) lower = !lower;
              else lower = TRUE;
              break;
    case 'X': if (negate) shortname = !shortname;
              else shortname = TRUE;
              break;
    case 'Q': if (negate) usernames = !usernames;
              else usernames = TRUE;
              break;
    case 'D': if (negate) orderByCol = !orderByCol;
              else orderByCol = TRUE;
              break;
    case 'C': if (negate) separator = !separator;
              else separator = TRUE;
              break;
    case 'T': p = p + 1;
              if (*p==':') p++;  /* Skip optional : */

              if (*p == 'A') dirTime = Access;
              else if (*p == 'C') dirTime = Creation;
              else if (*p == 'W') dirTime = Written;

              /* Support /T and /T: with no parms, default to written */
              else if (*p == 0x00 || *p == '/') {
                dirTime = Written;
                p = p - 1; /* So when step on, move to '/' */
              } else {
                SetLastError(ERROR_INVALID_PARAMETER);
                WCMD_print_error();
                errorlevel = 1;
                return;
              }
              break;
    case 'O': p = p + 1;
              if (*p==':') p++;  /* Skip optional : */
              while (*p && *p != '/') {
                WINE_TRACE("Processing subparm '%c' (in %s)\n", *p, wine_dbgstr_w(quals));
                switch (*p) {
                case 'N': dirOrder = Name;       break;
                case 'E': dirOrder = Extension;  break;
                case 'S': dirOrder = Size;       break;
                case 'D': dirOrder = Date;       break;
                case '-': if (*(p+1)=='G') orderGroupDirsReverse=TRUE;
                          else orderReverse = TRUE;
                          break;
                case 'G': orderGroupDirs = TRUE; break;
                default:
                    SetLastError(ERROR_INVALID_PARAMETER);
                    WCMD_print_error();
                    errorlevel = 1;
                    return;
                }
                p++;
              }
              p = p - 1; /* So when step on, move to '/' */
              break;
    case 'A': p = p + 1;
              showattrs = 0;
              attrsbits = 0;
              if (*p==':') p++;  /* Skip optional : */
              while (*p && *p != '/') {
                BOOL anegate = FALSE;
                ULONG mask;

                /* Note /A: - options are 'offs' not toggles */
                if (*p=='-') {
                  anegate = TRUE;
                  p++;
                }

                WINE_TRACE("Processing subparm '%c' (in %s)\n", *p, wine_dbgstr_w(quals));
                switch (*p) {
                case 'D': mask = FILE_ATTRIBUTE_DIRECTORY; break;
                case 'H': mask = FILE_ATTRIBUTE_HIDDEN;    break;
                case 'S': mask = FILE_ATTRIBUTE_SYSTEM;    break;
                case 'R': mask = FILE_ATTRIBUTE_READONLY;  break;
                case 'A': mask = FILE_ATTRIBUTE_ARCHIVE;   break;
                default:
                    SetLastError(ERROR_INVALID_PARAMETER);
                    WCMD_print_error();
                    errorlevel = 1;
                    return;
                }

                /* Keep running list of bits we care about */
                attrsbits |= mask;

                /* Mask shows what MUST be in the bits we care about */
                if (anegate) showattrs = showattrs & ~mask;
                else showattrs |= mask;

                p++;
              }
              p = p - 1; /* So when step on, move to '/' */
              WINE_TRACE("Result: showattrs %x, bits %x\n", showattrs, attrsbits);
              break;
    default:
              SetLastError(ERROR_INVALID_PARAMETER);
              WCMD_print_error();
              errorlevel = 1;
              return;
    }
    p = p + 1;
  }

  /* Handle conflicting args and initialization */
  if (bare || shortname) wide = FALSE;
  if (bare) shortname = FALSE;
  if (wide) usernames = FALSE;
  if (orderByCol) wide = TRUE;

  if (wide) {
      if (GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &consoleInfo))
          max_width = consoleInfo.dwSize.X;
      else
          max_width = 80;
  }
  if (paged_mode) {
     WCMD_enter_paged_mode(NULL);
  }

  argno         = 0;
  argN          = cmd;
  GetCurrentDirectoryW(MAX_PATH, cwd);
  strcatW(cwd, slashW);

  /* Loop through all args, calculating full effective directory */
  fullParms = NULL;
  prevEntry = NULL;
  while (argN) {
    WCHAR fullname[MAXSTRING];
    WCHAR *thisArg = WCMD_parameter(cmd, argno++, &argN, NULL);
    if (argN && argN[0] != '/') {

      WINE_TRACE("Found parm '%s'\n", wine_dbgstr_w(thisArg));
      if (thisArg[1] == ':' && thisArg[2] == '\\') {
        strcpyW(fullname, thisArg);
      } else if (thisArg[1] == ':' && thisArg[2] != '\\') {
        WCHAR envvar[4];
        static const WCHAR envFmt[] = {'=','%','c',':','\0'};
        wsprintfW(envvar, envFmt, thisArg[0]);
        if (!GetEnvironmentVariableW(envvar, fullname, MAX_PATH)) {
          static const WCHAR noEnvFmt[] = {'%','c',':','\0'};
          wsprintfW(fullname, noEnvFmt, thisArg[0]);
        }
        strcatW(fullname, slashW);
        strcatW(fullname, &thisArg[2]);
      } else if (thisArg[0] == '\\') {
        memcpy(fullname, cwd, 2 * sizeof(WCHAR));
        strcpyW(fullname+2, thisArg);
      } else {
        strcpyW(fullname, cwd);
        strcatW(fullname, thisArg);
      }
      WINE_TRACE("Using location '%s'\n", wine_dbgstr_w(fullname));

      status = GetFullPathNameW(fullname, sizeof(path)/sizeof(WCHAR), path, NULL);

      /*
       *  If the path supplied does not include a wildcard, and the endpoint of the
       *  path references a directory, we need to list the *contents* of that
       *  directory not the directory file itself.
       */
      if ((strchrW(path, '*') == NULL) && (strchrW(path, '%') == NULL)) {
        status = GetFileAttributesW(path);
        if ((status != INVALID_FILE_ATTRIBUTES) && (status & FILE_ATTRIBUTE_DIRECTORY)) {
          if (path[strlenW(path)-1] == '\\') {
            strcatW (path, starW);
          }
          else {
            static const WCHAR slashStarW[]  = {'\\','*','\0'};
            strcatW (path, slashStarW);
          }
        }
      } else {
        /* Special case wildcard search with no extension (ie parameters ending in '.') as
           GetFullPathName strips off the additional '.'                                  */
        if (fullname[strlenW(fullname)-1] == '.') strcatW(path, dotW);
      }

      WINE_TRACE("Using path '%s'\n", wine_dbgstr_w(path));
      thisEntry = HeapAlloc(GetProcessHeap(),0,sizeof(DIRECTORY_STACK));
      if (fullParms == NULL) fullParms = thisEntry;
      if (prevEntry != NULL) prevEntry->next = thisEntry;
      prevEntry = thisEntry;
      thisEntry->next = NULL;

      /* Split into components */
      WCMD_splitpath(path, drive, dir, fname, ext);
      WINE_TRACE("Path Parts: drive: '%s' dir: '%s' name: '%s' ext:'%s'\n",
                 wine_dbgstr_w(drive), wine_dbgstr_w(dir),
                 wine_dbgstr_w(fname), wine_dbgstr_w(ext));

      thisEntry->dirName = HeapAlloc(GetProcessHeap(),0,
                                     sizeof(WCHAR) * (strlenW(drive)+strlenW(dir)+1));
      strcpyW(thisEntry->dirName, drive);
      strcatW(thisEntry->dirName, dir);

      thisEntry->fileName = HeapAlloc(GetProcessHeap(),0,
                                     sizeof(WCHAR) * (strlenW(fname)+strlenW(ext)+1));
      strcpyW(thisEntry->fileName, fname);
      strcatW(thisEntry->fileName, ext);

    }
  }

  /* If just 'dir' entered, a '*' parameter is assumed */
  if (fullParms == NULL) {
    WINE_TRACE("Inserting default '*'\n");
    fullParms = HeapAlloc(GetProcessHeap(),0, sizeof(DIRECTORY_STACK));
    fullParms->next = NULL;
    fullParms->dirName = HeapAlloc(GetProcessHeap(),0,sizeof(WCHAR) * (strlenW(cwd)+1));
    strcpyW(fullParms->dirName, cwd);
    fullParms->fileName = HeapAlloc(GetProcessHeap(),0,sizeof(WCHAR) * 2);
    strcpyW(fullParms->fileName, starW);
  }

  lastDrive = '?';
  prevEntry = NULL;
  thisEntry = fullParms;
  trailerReqd = FALSE;

  while (thisEntry != NULL) {

    /* Output disk free (trailer) and volume information (header) if the drive
       letter changes */
    if (lastDrive != toupper(thisEntry->dirName[0])) {

      /* Trailer Information */
      if (lastDrive != '?') {
        trailerReqd = FALSE;
        WCMD_dir_trailer(prevEntry->dirName[0]);
      }

      lastDrive = toupper(thisEntry->dirName[0]);

      if (!bare) {
         WCHAR drive[3];

         WINE_TRACE("Writing volume for '%c:'\n", thisEntry->dirName[0]);
         memcpy(drive, thisEntry->dirName, 2 * sizeof(WCHAR));
         drive[2] = 0x00;
         status = WCMD_volume (0, drive);
         trailerReqd = TRUE;
         if (!status) {
           errorlevel = 1;
           goto exit;
         }
      }
    } else {
      static const WCHAR newLine2[] = {'\n','\n','\0'};
      if (!bare) WCMD_output (newLine2);
    }

    /* Clear any errors from previous invocations, and process it */
    errorlevel = 0;
    prevEntry = thisEntry;
    thisEntry = WCMD_list_directory (thisEntry, 0);
  }

  /* Trailer Information */
  if (trailerReqd) {
    WCMD_dir_trailer(prevEntry->dirName[0]);
  }

exit:
  if (paged_mode) WCMD_leave_paged_mode();

  /* Free storage allocated for parms */
  while (fullParms != NULL) {
    prevEntry = fullParms;
    fullParms = prevEntry->next;
    HeapFree(GetProcessHeap(),0,prevEntry->dirName);
    HeapFree(GetProcessHeap(),0,prevEntry->fileName);
    HeapFree(GetProcessHeap(),0,prevEntry);
  }
}