1/*
2 * Implements the file command for jim
3 *
4 * (c) 2008 Steve Bennett <steveb@workware.net.au>
5 *
6 * Redistribution and use in source and binary forms, with or without
7 * modification, are permitted provided that the following conditions
8 * are met:
9 *
10 * 1. Redistributions of source code must retain the above copyright
11 *    notice, this list of conditions and the following disclaimer.
12 * 2. Redistributions in binary form must reproduce the above
13 *    copyright notice, this list of conditions and the following
14 *    disclaimer in the documentation and/or other materials
15 *    provided with the distribution.
16 *
17 * THIS SOFTWARE IS PROVIDED BY THE JIM TCL PROJECT ``AS IS'' AND ANY
18 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
19 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
20 * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
21 * JIM TCL PROJECT OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
22 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
24 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
25 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
26 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
28 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 *
30 * The views and conclusions contained in the software and documentation
31 * are those of the authors and should not be interpreted as representing
32 * official policies, either expressed or implied, of the Jim Tcl Project.
33 *
34 * Based on code originally from Tcl 6.7:
35 *
36 * Copyright 1987-1991 Regents of the University of California
37 * Permission to use, copy, modify, and distribute this
38 * software and its documentation for any purpose and without
39 * fee is hereby granted, provided that the above copyright
40 * notice appear in all copies.  The University of California
41 * makes no representations about the suitability of this
42 * software for any purpose.  It is provided "as is" without
43 * express or implied warranty.
44 */
45
46#include <limits.h>
47#include <stdlib.h>
48#include <string.h>
49#include <stdio.h>
50#include <unistd.h>
51#include <errno.h>
52#include <sys/stat.h>
53#include <sys/param.h>
54#include <sys/time.h>
55
56#include "jim.h"
57#include "jimautoconf.h"
58#include "jim-subcmd.h"
59
60# ifndef MAXPATHLEN
61# define MAXPATHLEN JIM_PATH_LEN
62# endif
63
64/*
65 *----------------------------------------------------------------------
66 *
67 * JimGetFileType --
68 *
69 *  Given a mode word, returns a string identifying the type of a
70 *  file.
71 *
72 * Results:
73 *  A static text string giving the file type from mode.
74 *
75 * Side effects:
76 *  None.
77 *
78 *----------------------------------------------------------------------
79 */
80
81static const char *JimGetFileType(int mode)
82{
83    if (S_ISREG(mode)) {
84        return "file";
85    }
86    else if (S_ISDIR(mode)) {
87        return "directory";
88    }
89    else if (S_ISCHR(mode)) {
90        return "characterSpecial";
91    }
92    else if (S_ISBLK(mode)) {
93        return "blockSpecial";
94    }
95    else if (S_ISFIFO(mode)) {
96        return "fifo";
97#ifdef S_ISLNK
98    }
99    else if (S_ISLNK(mode)) {
100        return "link";
101#endif
102#ifdef S_ISSOCK
103    }
104    else if (S_ISSOCK(mode)) {
105        return "socket";
106#endif
107    }
108    return "unknown";
109}
110
111/*
112 *----------------------------------------------------------------------
113 *
114 * StoreStatData --
115 *
116 *  This is a utility procedure that breaks out the fields of a
117 *  "stat" structure and stores them in textual form into the
118 *  elements of an associative array.
119 *
120 * Results:
121 *  Returns a standard Tcl return value.  If an error occurs then
122 *  a message is left in interp->result.
123 *
124 * Side effects:
125 *  Elements of the associative array given by "varName" are modified.
126 *
127 *----------------------------------------------------------------------
128 */
129
130static int set_array_int_value(Jim_Interp *interp, Jim_Obj *container, const char *key,
131    jim_wide value)
132{
133    Jim_Obj *nameobj = Jim_NewStringObj(interp, key, -1);
134    Jim_Obj *valobj = Jim_NewWideObj(interp, value);
135
136    if (Jim_SetDictKeysVector(interp, container, &nameobj, 1, valobj, JIM_ERRMSG) != JIM_OK) {
137        Jim_FreeObj(interp, nameobj);
138        Jim_FreeObj(interp, valobj);
139        return JIM_ERR;
140    }
141    return JIM_OK;
142}
143
144static int set_array_string_value(Jim_Interp *interp, Jim_Obj *container, const char *key,
145    const char *value)
146{
147    Jim_Obj *nameobj = Jim_NewStringObj(interp, key, -1);
148    Jim_Obj *valobj = Jim_NewStringObj(interp, value, -1);
149
150    if (Jim_SetDictKeysVector(interp, container, &nameobj, 1, valobj, JIM_ERRMSG) != JIM_OK) {
151        Jim_FreeObj(interp, nameobj);
152        Jim_FreeObj(interp, valobj);
153        return JIM_ERR;
154    }
155    return JIM_OK;
156}
157
158static int StoreStatData(Jim_Interp *interp, Jim_Obj *varName, const struct stat *sb)
159{
160    if (set_array_int_value(interp, varName, "dev", sb->st_dev) != JIM_OK) {
161        Jim_SetResultFormatted(interp, "can't set \"%#s(dev)\": variables isn't array", varName);
162        return JIM_ERR;
163    }
164    set_array_int_value(interp, varName, "ino", sb->st_ino);
165    set_array_int_value(interp, varName, "mode", sb->st_mode);
166    set_array_int_value(interp, varName, "nlink", sb->st_nlink);
167    set_array_int_value(interp, varName, "uid", sb->st_uid);
168    set_array_int_value(interp, varName, "gid", sb->st_gid);
169    set_array_int_value(interp, varName, "size", sb->st_size);
170    set_array_int_value(interp, varName, "atime", sb->st_atime);
171    set_array_int_value(interp, varName, "mtime", sb->st_mtime);
172    set_array_int_value(interp, varName, "ctime", sb->st_ctime);
173    set_array_string_value(interp, varName, "type", JimGetFileType((int)sb->st_mode));
174
175    /* And also return the value */
176    Jim_SetResult(interp, Jim_GetVariable(interp, varName, 0));
177
178    return JIM_OK;
179}
180
181static int file_cmd_dirname(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
182{
183    const char *path = Jim_String(argv[0]);
184    const char *p = strrchr(path, '/');
185
186    if (!p) {
187        Jim_SetResultString(interp, ".", -1);
188    }
189    else if (p == path) {
190        Jim_SetResultString(interp, "/", -1);
191    }
192#if defined(__MINGW32__)
193    else if (p[-1] == ':') {
194        /* z:/dir => z:/ */
195        Jim_SetResultString(interp, path, p - path + 1);
196    }
197#endif
198    else {
199        Jim_SetResultString(interp, path, p - path);
200    }
201    return JIM_OK;
202}
203
204static int file_cmd_rootname(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
205{
206    const char *path = Jim_String(argv[0]);
207    const char *lastSlash = strrchr(path, '/');
208    const char *p = strrchr(path, '.');
209
210    if (p == NULL || (lastSlash != NULL && lastSlash > p)) {
211        Jim_SetResult(interp, argv[0]);
212    }
213    else {
214        Jim_SetResultString(interp, path, p - path);
215    }
216    return JIM_OK;
217}
218
219static int file_cmd_extension(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
220{
221    const char *path = Jim_String(argv[0]);
222    const char *lastSlash = strrchr(path, '/');
223    const char *p = strrchr(path, '.');
224
225    if (p == NULL || (lastSlash != NULL && lastSlash >= p)) {
226        p = "";
227    }
228    Jim_SetResultString(interp, p, -1);
229    return JIM_OK;
230}
231
232static int file_cmd_tail(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
233{
234    const char *path = Jim_String(argv[0]);
235    const char *lastSlash = strrchr(path, '/');
236
237    if (lastSlash) {
238        Jim_SetResultString(interp, lastSlash + 1, -1);
239    }
240    else {
241        Jim_SetResult(interp, argv[0]);
242    }
243    return JIM_OK;
244}
245
246static int file_cmd_normalize(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
247{
248#ifdef HAVE_REALPATH
249    const char *path = Jim_String(argv[0]);
250    char *newname = Jim_Alloc(MAXPATHLEN + 1);
251
252    if (realpath(path, newname)) {
253        Jim_SetResult(interp, Jim_NewStringObjNoAlloc(interp, newname, -1));
254    }
255    else {
256        Jim_Free(newname);
257        Jim_SetResult(interp, argv[0]);
258    }
259    return JIM_OK;
260#else
261    Jim_SetResultString(interp, "Not implemented", -1);
262    return JIM_ERR;
263#endif
264}
265
266static int file_cmd_join(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
267{
268    int i;
269    char *newname = Jim_Alloc(MAXPATHLEN + 1);
270    char *last = newname;
271
272    *newname = 0;
273
274    /* Simple implementation for now */
275    for (i = 0; i < argc; i++) {
276        int len;
277        const char *part = Jim_GetString(argv[i], &len);
278
279        if (*part == '/') {
280            /* Absolute component, so go back to the start */
281            last = newname;
282        }
283#if defined(__MINGW32__)
284        else if (strchr(part, ':')) {
285            /* Absolute compontent on mingw, so go back to the start */
286            last = newname;
287        }
288#endif
289        else if (part[0] == '.') {
290            if (part[1] == '/') {
291                part += 2;
292                len -= 2;
293            }
294            else if (part[1] == 0 && last != newname) {
295                /* Adding '.' to an existing path does nothing */
296                continue;
297            }
298        }
299
300        /* Add a slash if needed */
301        if (last != newname && last[-1] != '/') {
302            *last++ = '/';
303        }
304
305        if (len) {
306            if (last + len - newname >= MAXPATHLEN) {
307                Jim_Free(newname);
308                Jim_SetResultString(interp, "Path too long", -1);
309                return JIM_ERR;
310            }
311            memcpy(last, part, len);
312            last += len;
313        }
314
315        /* Remove a slash if needed */
316        if (last > newname + 1 && last[-1] == '/') {
317            *--last = 0;
318        }
319    }
320
321    *last = 0;
322
323    /* Probably need to handle some special cases ... */
324
325    Jim_SetResult(interp, Jim_NewStringObjNoAlloc(interp, newname, last - newname));
326
327    return JIM_OK;
328}
329
330static int file_access(Jim_Interp *interp, Jim_Obj *filename, int mode)
331{
332    const char *path = Jim_String(filename);
333    int rc = access(path, mode);
334
335    Jim_SetResultBool(interp, rc != -1);
336
337    return JIM_OK;
338}
339
340static int file_cmd_readable(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
341{
342    return file_access(interp, argv[0], R_OK);
343}
344
345static int file_cmd_writable(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
346{
347    return file_access(interp, argv[0], W_OK);
348}
349
350static int file_cmd_executable(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
351{
352    return file_access(interp, argv[0], X_OK);
353}
354
355static int file_cmd_exists(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
356{
357    return file_access(interp, argv[0], F_OK);
358}
359
360static int file_cmd_delete(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
361{
362    int force = Jim_CompareStringImmediate(interp, argv[0], "-force");
363
364    if (force || Jim_CompareStringImmediate(interp, argv[0], "--")) {
365        argc++;
366        argv--;
367    }
368
369    while (argc--) {
370        const char *path = Jim_String(argv[0]);
371
372        if (unlink(path) == -1 && errno != ENOENT) {
373            if (rmdir(path) == -1) {
374                /* Maybe try using the script helper */
375                if (!force || Jim_EvalPrefix(interp, "file delete force", 1, argv) != JIM_OK) {
376                    Jim_SetResultFormatted(interp, "couldn't delete file \"%s\": %s", path,
377                        strerror(errno));
378                    return JIM_ERR;
379                }
380            }
381        }
382        argv++;
383    }
384    return JIM_OK;
385}
386
387#ifdef HAVE_MKDIR_ONE_ARG
388#define MKDIR_DEFAULT(PATHNAME) mkdir(PATHNAME)
389#else
390#define MKDIR_DEFAULT(PATHNAME) mkdir(PATHNAME, 0755)
391#endif
392
393/**
394 * Create directory, creating all intermediate paths if necessary.
395 *
396 * Returns 0 if OK or -1 on failure (and sets errno)
397 *
398 * Note: The path may be modified.
399 */
400static int mkdir_all(char *path)
401{
402    int ok = 1;
403
404    /* First time just try to make the dir */
405    goto first;
406
407    while (ok--) {
408        /* Must have failed the first time, so recursively make the parent and try again */
409        char *slash = strrchr(path, '/');
410
411        if (slash && slash != path) {
412            *slash = 0;
413            if (mkdir_all(path) != 0) {
414                return -1;
415            }
416            *slash = '/';
417        }
418      first:
419        if (MKDIR_DEFAULT(path) == 0) {
420            return 0;
421        }
422        if (errno == ENOENT) {
423            /* Create the parent and try again */
424            continue;
425        }
426        /* Maybe it already exists as a directory */
427        if (errno == EEXIST) {
428            struct stat sb;
429
430            if (stat(path, &sb) == 0 && S_ISDIR(sb.st_mode)) {
431                return 0;
432            }
433            /* Restore errno */
434            errno = EEXIST;
435        }
436        /* Failed */
437        break;
438    }
439    return -1;
440}
441
442static int file_cmd_mkdir(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
443{
444    while (argc--) {
445        char *path = Jim_StrDup(Jim_String(argv[0]));
446        int rc = mkdir_all(path);
447
448        Jim_Free(path);
449        if (rc != 0) {
450            Jim_SetResultFormatted(interp, "can't create directory \"%#s\": %s", argv[0],
451                strerror(errno));
452            return JIM_ERR;
453        }
454        argv++;
455    }
456    return JIM_OK;
457}
458
459#ifdef HAVE_MKSTEMP
460static int file_cmd_tempfile(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
461{
462    int fd;
463    char *filename;
464    const char *template = "/tmp/tcl.tmp.XXXXXX";
465
466    if (argc >= 1) {
467        template = Jim_String(argv[0]);
468    }
469    filename = Jim_StrDup(template);
470
471    fd = mkstemp(filename);
472    if (fd < 0) {
473        Jim_SetResultString(interp, "Failed to create tempfile", -1);
474        return JIM_ERR;
475    }
476    close(fd);
477
478    Jim_SetResult(interp, Jim_NewStringObjNoAlloc(interp, filename, -1));
479    return JIM_OK;
480}
481#endif
482
483static int file_cmd_rename(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
484{
485    const char *source;
486    const char *dest;
487    int force = 0;
488
489    if (argc == 3) {
490        if (!Jim_CompareStringImmediate(interp, argv[0], "-force")) {
491            return -1;
492        }
493        force++;
494        argv++;
495        argc--;
496    }
497
498    source = Jim_String(argv[0]);
499    dest = Jim_String(argv[1]);
500
501    if (!force && access(dest, F_OK) == 0) {
502        Jim_SetResultFormatted(interp, "error renaming \"%#s\" to \"%#s\": target exists", argv[0],
503            argv[1]);
504        return JIM_ERR;
505    }
506
507    if (rename(source, dest) != 0) {
508        Jim_SetResultFormatted(interp, "error renaming \"%#s\" to \"%#s\": %s", argv[0], argv[1],
509            strerror(errno));
510        return JIM_ERR;
511    }
512
513    return JIM_OK;
514}
515
516static int file_stat(Jim_Interp *interp, Jim_Obj *filename, struct stat *sb)
517{
518    const char *path = Jim_String(filename);
519
520    if (stat(path, sb) == -1) {
521        Jim_SetResultFormatted(interp, "could not read \"%#s\": %s", filename, strerror(errno));
522        return JIM_ERR;
523    }
524    return JIM_OK;
525}
526
527#ifndef HAVE_LSTAT
528#define lstat stat
529#endif
530
531static int file_lstat(Jim_Interp *interp, Jim_Obj *filename, struct stat *sb)
532{
533    const char *path = Jim_String(filename);
534
535    if (lstat(path, sb) == -1) {
536        Jim_SetResultFormatted(interp, "could not read \"%#s\": %s", filename, strerror(errno));
537        return JIM_ERR;
538    }
539    return JIM_OK;
540}
541
542static int file_cmd_atime(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
543{
544    struct stat sb;
545
546    if (file_stat(interp, argv[0], &sb) != JIM_OK) {
547        return JIM_ERR;
548    }
549    Jim_SetResultInt(interp, sb.st_atime);
550    return JIM_OK;
551}
552
553static int file_cmd_mtime(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
554{
555    struct stat sb;
556
557    if (argc == 2) {
558#ifdef HAVE_UTIMES
559        jim_wide newtime;
560        struct timeval times[2];
561
562        if (Jim_GetWide(interp, argv[1], &newtime) != JIM_OK) {
563            return JIM_ERR;
564        }
565
566        times[1].tv_sec = times[0].tv_sec = newtime;
567        times[1].tv_usec = times[0].tv_usec = 0;
568
569        if (utimes(Jim_String(argv[0]), times) != 0) {
570            Jim_SetResultFormatted(interp, "can't set time on \"%#s\": %s", argv[0], strerror(errno));
571            return JIM_ERR;
572        }
573#else
574        Jim_SetResultString(interp, "Not implemented", -1);
575        return JIM_ERR;
576#endif
577    }
578    if (file_stat(interp, argv[0], &sb) != JIM_OK) {
579        return JIM_ERR;
580    }
581    Jim_SetResultInt(interp, sb.st_mtime);
582    return JIM_OK;
583}
584
585static int file_cmd_copy(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
586{
587    return Jim_EvalPrefix(interp, "file copy", argc, argv);
588}
589
590static int file_cmd_size(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
591{
592    struct stat sb;
593
594    if (file_stat(interp, argv[0], &sb) != JIM_OK) {
595        return JIM_ERR;
596    }
597    Jim_SetResultInt(interp, sb.st_size);
598    return JIM_OK;
599}
600
601static int file_cmd_isdirectory(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
602{
603    struct stat sb;
604    int ret = 0;
605
606    if (file_stat(interp, argv[0], &sb) == JIM_OK) {
607        ret = S_ISDIR(sb.st_mode);
608    }
609    Jim_SetResultInt(interp, ret);
610    return JIM_OK;
611}
612
613static int file_cmd_isfile(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
614{
615    struct stat sb;
616    int ret = 0;
617
618    if (file_stat(interp, argv[0], &sb) == JIM_OK) {
619        ret = S_ISREG(sb.st_mode);
620    }
621    Jim_SetResultInt(interp, ret);
622    return JIM_OK;
623}
624
625#ifdef HAVE_GETEUID
626static int file_cmd_owned(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
627{
628    struct stat sb;
629    int ret = 0;
630
631    if (file_stat(interp, argv[0], &sb) == JIM_OK) {
632        ret = (geteuid() == sb.st_uid);
633    }
634    Jim_SetResultInt(interp, ret);
635    return JIM_OK;
636}
637#endif
638
639#if defined(HAVE_READLINK)
640static int file_cmd_readlink(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
641{
642    const char *path = Jim_String(argv[0]);
643    char *linkValue = Jim_Alloc(MAXPATHLEN + 1);
644
645    int linkLength = readlink(path, linkValue, MAXPATHLEN);
646
647    if (linkLength == -1) {
648        Jim_Free(linkValue);
649        Jim_SetResultFormatted(interp, "couldn't readlink \"%#s\": %s", argv[0], strerror(errno));
650        return JIM_ERR;
651    }
652    linkValue[linkLength] = 0;
653    Jim_SetResult(interp, Jim_NewStringObjNoAlloc(interp, linkValue, linkLength));
654    return JIM_OK;
655}
656#endif
657
658static int file_cmd_type(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
659{
660    struct stat sb;
661
662    if (file_lstat(interp, argv[0], &sb) != JIM_OK) {
663        return JIM_ERR;
664    }
665    Jim_SetResultString(interp, JimGetFileType((int)sb.st_mode), -1);
666    return JIM_OK;
667}
668
669static int file_cmd_lstat(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
670{
671    struct stat sb;
672
673    if (file_lstat(interp, argv[0], &sb) != JIM_OK) {
674        return JIM_ERR;
675    }
676    return StoreStatData(interp, argv[1], &sb);
677}
678
679static int file_cmd_stat(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
680{
681    struct stat sb;
682
683    if (file_stat(interp, argv[0], &sb) != JIM_OK) {
684        return JIM_ERR;
685    }
686    return StoreStatData(interp, argv[1], &sb);
687}
688
689static const jim_subcmd_type file_command_table[] = {
690    {   .cmd = "atime",
691        .args = "name",
692        .function = file_cmd_atime,
693        .minargs = 1,
694        .maxargs = 1,
695        .description = "Last access time"
696    },
697    {   .cmd = "mtime",
698        .args = "name ?time?",
699        .function = file_cmd_mtime,
700        .minargs = 1,
701        .maxargs = 2,
702        .description = "Get or set last modification time"
703    },
704    {   .cmd = "copy",
705        .args = "?-force? source dest",
706        .function = file_cmd_copy,
707        .minargs = 2,
708        .maxargs = 3,
709        .description = "Copy source file to destination file"
710    },
711    {   .cmd = "dirname",
712        .args = "name",
713        .function = file_cmd_dirname,
714        .minargs = 1,
715        .maxargs = 1,
716        .description = "Directory part of the name"
717    },
718    {   .cmd = "rootname",
719        .args = "name",
720        .function = file_cmd_rootname,
721        .minargs = 1,
722        .maxargs = 1,
723        .description = "Name without any extension"
724    },
725    {   .cmd = "extension",
726        .args = "name",
727        .function = file_cmd_extension,
728        .minargs = 1,
729        .maxargs = 1,
730        .description = "Last extension including the dot"
731    },
732    {   .cmd = "tail",
733        .args = "name",
734        .function = file_cmd_tail,
735        .minargs = 1,
736        .maxargs = 1,
737        .description = "Last component of the name"
738    },
739    {   .cmd = "normalize",
740        .args = "name",
741        .function = file_cmd_normalize,
742        .minargs = 1,
743        .maxargs = 1,
744        .description = "Normalized path of name"
745    },
746    {   .cmd = "join",
747        .args = "name ?name ...?",
748        .function = file_cmd_join,
749        .minargs = 1,
750        .maxargs = -1,
751        .description = "Join multiple path components"
752    },
753    {   .cmd = "readable",
754        .args = "name",
755        .function = file_cmd_readable,
756        .minargs = 1,
757        .maxargs = 1,
758        .description = "Is file readable"
759    },
760    {   .cmd = "writable",
761        .args = "name",
762        .function = file_cmd_writable,
763        .minargs = 1,
764        .maxargs = 1,
765        .description = "Is file writable"
766    },
767    {   .cmd = "executable",
768        .args = "name",
769        .function = file_cmd_executable,
770        .minargs = 1,
771        .maxargs = 1,
772        .description = "Is file executable"
773    },
774    {   .cmd = "exists",
775        .args = "name",
776        .function = file_cmd_exists,
777        .minargs = 1,
778        .maxargs = 1,
779        .description = "Does file exist"
780    },
781    {   .cmd = "delete",
782        .args = "?-force|--? name ...",
783        .function = file_cmd_delete,
784        .minargs = 1,
785        .maxargs = -1,
786        .description = "Deletes the files or directories (must be empty unless -force)"
787    },
788    {   .cmd = "mkdir",
789        .args = "dir ...",
790        .function = file_cmd_mkdir,
791        .minargs = 1,
792        .maxargs = -1,
793        .description = "Creates the directories"
794    },
795#ifdef HAVE_MKSTEMP
796    {   .cmd = "tempfile",
797        .args = "?template?",
798        .function = file_cmd_tempfile,
799        .minargs = 0,
800        .maxargs = 1,
801        .description = "Creates a temporary filename"
802    },
803#endif
804    {   .cmd = "rename",
805        .args = "?-force? source dest",
806        .function = file_cmd_rename,
807        .minargs = 2,
808        .maxargs = 3,
809        .description = "Renames a file"
810    },
811#if defined(HAVE_READLINK)
812    {   .cmd = "readlink",
813        .args = "name",
814        .function = file_cmd_readlink,
815        .minargs = 1,
816        .maxargs = 1,
817        .description = "Value of the symbolic link"
818    },
819#endif
820    {   .cmd = "size",
821        .args = "name",
822        .function = file_cmd_size,
823        .minargs = 1,
824        .maxargs = 1,
825        .description = "Size of file"
826    },
827    {   .cmd = "stat",
828        .args = "name var",
829        .function = file_cmd_stat,
830        .minargs = 2,
831        .maxargs = 2,
832        .description = "Stores results of stat in var array"
833    },
834    {   .cmd = "lstat",
835        .args = "name var",
836        .function = file_cmd_lstat,
837        .minargs = 2,
838        .maxargs = 2,
839        .description = "Stores results of lstat in var array"
840    },
841    {   .cmd = "type",
842        .args = "name",
843        .function = file_cmd_type,
844        .minargs = 1,
845        .maxargs = 1,
846        .description = "Returns type of the file"
847    },
848#ifdef HAVE_GETEUID
849    {   .cmd = "owned",
850        .args = "name",
851        .function = file_cmd_owned,
852        .minargs = 1,
853        .maxargs = 1,
854        .description = "Returns 1 if owned by the current owner"
855    },
856#endif
857    {   .cmd = "isdirectory",
858        .args = "name",
859        .function = file_cmd_isdirectory,
860        .minargs = 1,
861        .maxargs = 1,
862        .description = "Returns 1 if name is a directory"
863    },
864    {   .cmd = "isfile",
865        .args = "name",
866        .function = file_cmd_isfile,
867        .minargs = 1,
868        .maxargs = 1,
869        .description = "Returns 1 if name is a file"
870    },
871    {
872        .cmd = 0
873    }
874};
875
876static int Jim_CdCmd(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
877{
878    const char *path;
879
880    if (argc != 2) {
881        Jim_WrongNumArgs(interp, 1, argv, "dirname");
882        return JIM_ERR;
883    }
884
885    path = Jim_String(argv[1]);
886
887    if (chdir(path) != 0) {
888        Jim_SetResultFormatted(interp, "couldn't change working directory to \"%s\": %s", path,
889            strerror(errno));
890        return JIM_ERR;
891    }
892    return JIM_OK;
893}
894
895static int Jim_PwdCmd(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
896{
897    const int cwd_len = 2048;
898    char *cwd = malloc(cwd_len);
899
900    if (getcwd(cwd, cwd_len) == NULL) {
901        Jim_SetResultString(interp, "Failed to get pwd", -1);
902        return JIM_ERR;
903    }
904#if defined(__MINGW32__)
905    {
906        /* Try to keep backlashes out of paths */
907        char *p = cwd;
908        while ((p = strchr(p, '\\')) != NULL) {
909            *p++ = '/';
910        }
911    }
912#endif
913
914    Jim_SetResultString(interp, cwd, -1);
915
916    free(cwd);
917    return JIM_OK;
918}
919
920int Jim_fileInit(Jim_Interp *interp)
921{
922    if (Jim_PackageProvide(interp, "file", "1.0", JIM_ERRMSG))
923        return JIM_ERR;
924
925    Jim_CreateCommand(interp, "file", Jim_SubCmdProc, (void *)file_command_table, NULL);
926    Jim_CreateCommand(interp, "pwd", Jim_PwdCmd, NULL, NULL);
927    Jim_CreateCommand(interp, "cd", Jim_CdCmd, NULL, NULL);
928    return JIM_OK;
929}
930