{"thread":{"id":"10173","subject":"[PATCH] Make git-clean a builtin","startedAt":"2007-10-06T20:54:06Z","lastAt":"2007-10-08T18:27:04Z","messageCount":9,"participants":["Shawn Bohrer","Frank Lichtenheld","Jeff King","Linus Torvalds","Johannes Sixt"],"isPatch":true,"patchVersion":1,"patchTotal":null},"messages":[{"id":"55026","messageId":"11917040461528-git-send-email-shawn.bohrer@gmail.com","threadId":"10173","inReplyTo":null,"subject":"[PATCH] Make git-clean a builtin","fromName":"Shawn Bohrer","fromEmail":"shawn.bohrer@gmail.com","sentAt":"2007-10-06T20:54:06Z","receivedAt":"2007-10-06T20:54:06Z","isPatch":true,"sender":{"key":"shawn.bohrer@gmail.com","avatar":"https://gravatar.com/avatar/6eb093ef7d276306d18366254e0c95ff6a5db58231ac7e82fe78c2800aaae1b6?d=mp&s=160"},"body":"This replaces git-clean.sh with builtin-clean.c, and moves git-clean.sh to the\nexamples.\n\nSigned-off-by: Shawn Bohrer <shawn.bohrer@gmail.com>\n---\n Makefile                                      |    3 +-\n builtin-clean.c                               |  177 +++++++++++++++++++++++++\n builtin.h                                     |    1 +\n git-clean.sh => contrib/examples/git-clean.sh |    0 \n git.c                                         |    1 +\n 5 files changed, 181 insertions(+), 1 deletions(-)\n create mode 100644 builtin-clean.c\n rename git-clean.sh => contrib/examples/git-clean.sh (100%)\n\ndiff --git a/Makefile b/Makefile\nindex 8db4dbe..2b3b8fb 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -206,7 +206,7 @@ BASIC_LDFLAGS =\n \n SCRIPT_SH = \\\n \tgit-bisect.sh git-checkout.sh \\\n-\tgit-clean.sh git-clone.sh git-commit.sh \\\n+\tgit-clone.sh git-commit.sh \\\n \tgit-fetch.sh \\\n \tgit-ls-remote.sh \\\n \tgit-merge-one-file.sh git-mergetool.sh git-parse-remote.sh \\\n@@ -327,6 +327,7 @@ BUILTIN_OBJS = \\\n \tbuiltin-check-attr.o \\\n \tbuiltin-checkout-index.o \\\n \tbuiltin-check-ref-format.o \\\n+\tbuiltin-clean.o \\\n \tbuiltin-commit-tree.o \\\n \tbuiltin-count-objects.o \\\n \tbuiltin-describe.o \\\ndiff --git a/builtin-clean.c b/builtin-clean.c\nnew file mode 100644\nindex 0000000..534707f\n--- /dev/null\n+++ b/builtin-clean.c\n@@ -0,0 +1,177 @@\n+/*\n+ * \"git clean\" builtin command\n+ *\n+ * Copyright (C) 2007 Shawn Bohrer\n+ *\n+ * Based on git-clean.sh by Pavel Roskin\n+ */\n+\n+#include \"builtin.h\"\n+#include \"cache.h\"\n+#include \"run-command.h\"\n+\n+static int disabled = 0;\n+static int show_only = 0;\n+static int remove_directories = 0;\n+static int quiet = 0;\n+static int ignored = 0;\n+static int ignored_only = 0;\n+\n+static const char builtin_clean_usage[] =\n+\"git-clean [-d] [-f] [-n] [-q] [-x | -X] [--] <paths>...\";\n+\n+static int git_clean_config(const char *var, const char *value)\n+{\n+\tif (!strcmp(var, \"clean.requireforce\")) {\n+\t\tdisabled = git_config_bool(var, value);\n+\t}\n+\treturn 0;\n+}\n+\n+static int remove_directory(const char *path)\n+{\n+\tDIR *d;\n+\tstruct dirent *dir;\n+\td = opendir(path);\n+\tif (d) {\n+\t\tchdir(path);\n+\t\twhile ((dir = readdir(d)) != NULL) {\n+\t\t\tif(strcmp( dir->d_name, \".\") == 0 ||\n+\t\t\t   strcmp( dir->d_name, \"..\" ) == 0 )\n+\t\t\t\tcontinue;\n+\t\t\tif (dir->d_type == DT_DIR)\n+\t\t\t\tremove_directory(dir->d_name);\n+\t\t\telse\n+\t\t\t\tunlink(dir->d_name);\n+\t\t}\n+\t}\n+\tclosedir(d);\n+\tchdir(\"..\");\n+\treturn rmdir(path);\n+}\n+\n+int cmd_clean(int argc, const char **argv, const char *prefix)\n+{\n+\tint i;\n+\tint j;\n+\tstruct child_process cmd;\n+\tconst char **argv_ls_files;\n+\tchar *buf;\n+\tchar path[1024];\n+\tFILE *cmd_fout;\n+\n+\tgit_config(git_clean_config);\n+\n+\tfor (i = 1; i < argc; i++) {\n+\t\tconst char *arg = argv[i];\n+\n+\t\tif (arg[0] != '-')\n+\t\t\tbreak;\n+\t\tif (!strcmp(arg, \"--\")) {\n+\t\t\ti++;\n+\t\t\tbreak;\n+\t\t}\n+\t\tif (!strcmp(arg, \"-n\")) {\n+\t\t\tshow_only = 1;\n+\t\t\tdisabled = 0;\n+\t\t\tcontinue;\n+\t\t}\n+\t\tif (!strcmp(arg, \"-f\")) {\n+\t\t\tdisabled = 0;\n+\t\t\tcontinue;\n+\t\t}\n+\t\tif (!strcmp(arg, \"-d\")) {\n+\t\t\tremove_directories = 1;\n+\t\t\tcontinue;\n+\t\t}\n+\t\tif (!strcmp(arg, \"-q\")) {\n+\t\t\tquiet = 1;\n+\t\t\tcontinue;\n+\t\t}\n+\t\tif (!strcmp(arg, \"-x\")) {\n+\t\t\tignored = 1;\n+\t\t\tcontinue;\n+\t\t}\n+\t\tif (!strcmp(arg, \"-X\")) {\n+\t\t\tignored_only = 1;\n+\t\t\tcontinue;\n+\t\t}\n+\t\tusage(builtin_clean_usage);\n+\t}\n+\n+\tif (ignored && ignored_only)\n+\t\tusage(builtin_clean_usage);\n+\n+\tif (disabled) {\n+\t\tdie(\"clean.requireForce set and -n or -f not given; refusing to clean\");\n+\t}\n+\n+\t/* Paths (argc - i) + 8 (Possible arguments)*/\n+\targv_ls_files = xmalloc((argc - i + 8) * sizeof(const char *));\n+\targv_ls_files[0] = \"ls-files\";\n+\targv_ls_files[1] = \"--others\";\n+\targv_ls_files[2] = \"--directory\";\n+\tj = 3;\n+\tif (!ignored) {\n+\t\targv_ls_files[j++] = \"--exclude-per-directory=.gitignore\";\n+\t\tif (ignored_only)\n+\t\t\targv_ls_files[j++] = \"--ignored\";\n+\t\tif (!access(git_path(\"info/exclude\"), F_OK)) {\n+\t\t\tchar *exclude_path = git_path(\"info/exclude\");\n+\t\t\tint len = strlen(exclude_path);\n+\t\t\tbuf = (char*)malloc(len+16);\n+\t\t\tsprintf(buf, \"--exclude-from=%s\", exclude_path);\n+\t\t\targv_ls_files[j++] = buf;\n+\t\t}\n+\t}\n+\targv_ls_files[j++] = \"--\";\n+\t/* Add remaining paths passed in as arguments */\n+\tif (argc - i)\n+\t\tmemcpy(argv_ls_files + j++, argv + i, (argc - i) * sizeof(const char *));\n+\targv_ls_files[j + argc - i] = NULL;\n+\n+\tmemset(&cmd, 0, sizeof(cmd));\n+\tcmd.argv = argv_ls_files;\n+\tcmd.git_cmd = 1;\n+\tcmd.out = -1;\n+\tif (start_command(&cmd))\n+\t\tdie(\"Could not run sub-command: git ls-files\");\n+\n+\tcmd_fout = fdopen(cmd.out, \"r\");\n+\twhile (fgets(path, sizeof(path), cmd_fout) != NULL) {\n+\t\tstruct stat st;\n+\t\tchar *p;\n+\t\tp = strrchr(path, '\\n');\n+\t\tif ( p != NULL )\n+\t\t\t*p = '\\0';\n+\t\tif (!lstat(path, &st) && (S_ISDIR(st.st_mode))) {\n+\t\t\tif (show_only && remove_directories) {\n+\t\t\t\tprintf(\"Would remove %s\\n\", path);\n+\t\t\t} else if (quiet && remove_directories) {\n+\t\t\t\tremove_directory(path);\n+\t\t\t} else if (remove_directories) {\n+\t\t\t\tprintf(\"Removing %s\\n\", path);\n+\t\t\t\tremove_directory(path);\n+\t\t\t} else if (show_only) {\n+\t\t\t\tprintf(\"Would not remove %s\\n\", path);\n+\t\t\t} else {\n+\t\t\t\tprintf(\"Not removing %s\\n\", path);\n+\t\t\t}\n+\t\t} else {\n+\t\t\tif (show_only) {\n+\t\t\t\tprintf(\"Would remove %s\\n\", path);\n+\t\t\t\tcontinue;\n+\t\t\t} else if (!quiet) {\n+\t\t\t\tprintf(\"Removing %s\\n\", path);\n+\t\t\t}\n+\t\t\tunlink(path);\n+\t\t}\n+\t}\n+\n+\tfclose(cmd_fout);\n+\tfinish_command(&cmd);\n+\tif (!ignored && !access(git_path(\"info/exclude\"), F_OK))\n+\t\tfree(buf);\n+\tfree(argv_ls_files);\n+\treturn 0;\n+}\ndiff --git a/builtin.h b/builtin.h\nindex d6f2c76..8c112f3 100644\n--- a/builtin.h\n+++ b/builtin.h\n@@ -23,6 +23,7 @@ extern int cmd_check_attr(int argc, const char **argv, const char *prefix);\n extern int cmd_check_ref_format(int argc, const char **argv, const char *prefix);\n extern int cmd_cherry(int argc, const char **argv, const char *prefix);\n extern int cmd_cherry_pick(int argc, const char **argv, const char *prefix);\n+extern int cmd_clean(int argc, const char **argv, const char *prefix);\n extern int cmd_commit_tree(int argc, const char **argv, const char *prefix);\n extern int cmd_count_objects(int argc, const char **argv, const char *prefix);\n extern int cmd_describe(int argc, const char **argv, const char *prefix);\ndiff --git a/git-clean.sh b/contrib/examples/git-clean.sh\nsimilarity index 100%\nrename from git-clean.sh\nrename to contrib/examples/git-clean.sh\ndiff --git a/git.c b/git.c\nindex 9eaca1d..cda6344 100644\n--- a/git.c\n+++ b/git.c\n@@ -320,6 +320,7 @@ static void handle_internal_command(int argc, const char **argv)\n \t\t{ \"check-attr\", cmd_check_attr, RUN_SETUP | NEED_WORK_TREE },\n \t\t{ \"cherry\", cmd_cherry, RUN_SETUP },\n \t\t{ \"cherry-pick\", cmd_cherry_pick, RUN_SETUP | NEED_WORK_TREE },\n+\t\t{ \"clean\", cmd_clean, RUN_SETUP },\n \t\t{ \"commit-tree\", cmd_commit_tree, RUN_SETUP },\n \t\t{ \"config\", cmd_config },\n \t\t{ \"count-objects\", cmd_count_objects, RUN_SETUP },\n-- \n1.5.3.GIT\n"},{"id":"55028","messageId":"20071006215253.GX31659@planck.djpig.de","threadId":"10173","inReplyTo":"11917040461528-git-send-email-shawn.bohrer@gmail.com","subject":"Re: [PATCH] Make git-clean a builtin","fromName":"Frank Lichtenheld","fromEmail":"frank@lichtenheld.de","sentAt":"2007-10-06T21:52:53Z","receivedAt":"2007-10-06T21:52:53Z","isPatch":true,"sender":{"key":"frank@lichtenheld.de","avatar":"https://gravatar.com/avatar/b9f1d4b120e138f157c9e480d0818197c474628923786adb98f30017cdb99c3c?d=mp&s=160"},"body":"On Sat, Oct 06, 2007 at 03:54:06PM -0500, Shawn Bohrer wrote:\n> +static int remove_directory(const char *path)\n> +{\n> +\tDIR *d;\n> +\tstruct dirent *dir;\n> +\td = opendir(path);\n> +\tif (d) {\n> +\t\tchdir(path);\n> +\t\twhile ((dir = readdir(d)) != NULL) {\n> +\t\t\tif(strcmp( dir->d_name, \".\") == 0 ||\n> +\t\t\t   strcmp( dir->d_name, \"..\" ) == 0 )\n> +\t\t\t\tcontinue;\n> +\t\t\tif (dir->d_type == DT_DIR)\n> +\t\t\t\tremove_directory(dir->d_name);\n> +\t\t\telse\n> +\t\t\t\tunlink(dir->d_name);\n> +\t\t}\n> +\t}\n> +\tclosedir(d);\n> +\tchdir(\"..\");\n> +\treturn rmdir(path);\n> +}\n\nThe unconditional chdir(..) after the conditional chdir(path) seems like\nasking for trouble to me...\n\n> +\twhile (fgets(path, sizeof(path), cmd_fout) != NULL) {\n> +\t\tstruct stat st;\n> +\t\tchar *p;\n> +\t\tp = strrchr(path, '\\n');\n> +\t\tif ( p != NULL )\n> +\t\t\t*p = '\\0';\n\nWhat happens in case p == NULL? It simply tries to remove the partial\npath?\n\n> +\tfclose(cmd_fout);\n> +\tfinish_command(&cmd);\n> +\tif (!ignored && !access(git_path(\"info/exclude\"), F_OK))\n> +\t\tfree(buf);\n\nThere is a race condition here of the value of access() changes between\nthe two calls. Not one likely to trigger but it should be easy to avoid\nalltogether.\n\n> +\tfree(argv_ls_files);\n> +\treturn 0;\n> +}\n\nGruesse,\n-- \nFrank Lichtenheld <frank@lichtenheld.de>\nwww: http://www.djpig.de/\n"},{"id":"55037","messageId":"20071007011331.GC5642@mediacenter.austin.rr.com","threadId":"10173","inReplyTo":"20071006215253.GX31659@planck.djpig.de","subject":"Re: [PATCH] Make git-clean a builtin","fromName":"Shawn Bohrer","fromEmail":"shawn.bohrer@gmail.com","sentAt":"2007-10-07T01:13:31Z","receivedAt":"2007-10-07T01:13:31Z","isPatch":true,"sender":{"key":"shawn.bohrer@gmail.com","avatar":"https://gravatar.com/avatar/6eb093ef7d276306d18366254e0c95ff6a5db58231ac7e82fe78c2800aaae1b6?d=mp&s=160"},"body":"On Sat, Oct 06, 2007 at 11:52:53PM +0200, Frank Lichtenheld wrote:\n> On Sat, Oct 06, 2007 at 03:54:06PM -0500, Shawn Bohrer wrote:\n> > +static int remove_directory(const char *path)\n> > +{\n> > +\tDIR *d;\n> > +\tstruct dirent *dir;\n> > +\td = opendir(path);\n> > +\tif (d) {\n> > +\t\tchdir(path);\n> > +\t\twhile ((dir = readdir(d)) != NULL) {\n> > +\t\t\tif(strcmp( dir->d_name, \".\") == 0 ||\n> > +\t\t\t   strcmp( dir->d_name, \"..\" ) == 0 )\n> > +\t\t\t\tcontinue;\n> > +\t\t\tif (dir->d_type == DT_DIR)\n> > +\t\t\t\tremove_directory(dir->d_name);\n> > +\t\t\telse\n> > +\t\t\t\tunlink(dir->d_name);\n> > +\t\t}\n> > +\t}\n> > +\tclosedir(d);\n> > +\tchdir(\"..\");\n> > +\treturn rmdir(path);\n> > +}\n> \n> The unconditional chdir(..) after the conditional chdir(path) seems like\n> asking for trouble to me...\n\nAgreed, I'm not sure what I was thinking there.\n\n> > +\twhile (fgets(path, sizeof(path), cmd_fout) != NULL) {\n> > +\t\tstruct stat st;\n> > +\t\tchar *p;\n> > +\t\tp = strrchr(path, '\\n');\n> > +\t\tif ( p != NULL )\n> > +\t\t\t*p = '\\0';\n> \n> What happens in case p == NULL? It simply tries to remove the partial\n> path?\n\nIf p == NULL then the path didn't have a EOL character.  This shouldn't\never really happen since fgets() leaves the EOL character as part of the\nstring, and it is processing the output of git-ls-files which will\nprovide one path per line.  If it does happen for some reason then\neither we will happily remove the file/directory, or if the path is\ngarbage then we will simply fail to remove anything.\n\n> > +\tfclose(cmd_fout);\n> > +\tfinish_command(&cmd);\n> > +\tif (!ignored && !access(git_path(\"info/exclude\"), F_OK))\n> > +\t\tfree(buf);\n> \n> There is a race condition here of the value of access() changes between\n> the two calls. Not one likely to trigger but it should be easy to avoid\n> alltogether.\n\nYes, though unlikely I agree this wasn't very smart in the first place\nso I fixed it and will send reworked patch soon.\n\n--\nShawn\n"},{"id":"55144","messageId":"20071008020435.GA20050@coredump.intra.peff.net","threadId":"10173","inReplyTo":"11917040461528-git-send-email-shawn.bohrer@gmail.com","subject":"Re: [PATCH] Make git-clean a builtin","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2007-10-08T02:04:35Z","receivedAt":"2007-10-08T02:04:35Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Sat, Oct 06, 2007 at 03:54:06PM -0500, Shawn Bohrer wrote:\n\n> +static int remove_directory(const char *path)\n> +{\n> [...]\n> +\t\tchdir(path);\n> [...]\n> +\tchdir(\"..\");\n\nThis doesn't always put you back where you started, due to symlinks. For\nexample:\n\ncat >foo.c <<'EOF'\nint main(int argc, char **argv) {\n  chdir(argv[1]);\n  chdir(\"..\");\n  execlp(\"ls\", 0);\n}\nEOF\ngcc -o foo foo.c\nln -s /tmp sub\n./foo sub\n\nwill show that you end up in the root directory.  Something like this is\nmore robust:\n\n  fd = open(\".\", O_RDONLY);\n  chdir(path);\n  ...\n  fchdir(fd);\n\nIn general, you shouldn't end up there because you don't actually\nrecurse for symlinks, but there is a race condition (and losing it means\nyou start recursively removing unintended directories -- oops).\n\nOn top of which, this line from the same function isn't very portable:\n\n+                       if (dir->d_type == DT_DIR)\n\nsince POSIX specifies nothing but dir->d_name (Solaris, for example,\ndoesn't define d_type). You need to stat the file.\n\nAll of that being said, I think a lot of this is already done in\ndir.[ch]. At the very least, you should be able to use\nremove_dir_recursively, and for bonus points you can get rid of the\nstart_command call to ls-files by just walking the dir tree yourself.\nI don't know if the latter is required, but it's nice when the\nC-ification actually cleans up a bit and uses the internal C interfaces\n(which are more efficient and often more clear to read) rather than just\nconverting shell to C.\n\n-Peff\n"},{"id":"55145","messageId":"20071008020815.GB20050@coredump.intra.peff.net","threadId":"10173","inReplyTo":"20071008020435.GA20050@coredump.intra.peff.net","subject":"Re: [PATCH] Make git-clean a builtin","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2007-10-08T02:08:15Z","receivedAt":"2007-10-08T02:08:15Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Sun, Oct 07, 2007 at 10:04:35PM -0400, Jeff King wrote:\n\n> This doesn't always put you back where you started, due to symlinks. For\n> example:\n> [and other complaints]\n\nOops, didn't see that there was another thread for the reworked patch.\nMy comments still stand, but it looks like others have weighed in as\nwell. Sorry.\n\n-Peff\n"},{"id":"55146","messageId":"alpine.LFD.0.999.0710071916510.23684@woody.linux-foundation.org","threadId":"10173","inReplyTo":"20071008020435.GA20050@coredump.intra.peff.net","subject":"Re: [PATCH] Make git-clean a builtin","fromName":"Linus Torvalds","fromEmail":"torvalds@linux-foundation.org","sentAt":"2007-10-08T02:17:50Z","receivedAt":"2007-10-08T02:17:50Z","isPatch":true,"sender":{"key":"torvalds@linux-foundation.org","avatar":"https://avatars.githubusercontent.com/u/1024025?v=4"},"body":"\n\nOn Sun, 7 Oct 2007, Jeff King wrote:\n> \n>   fd = open(\".\", O_RDONLY);\n>   chdir(path);\n>   ...\n>   fchdir(fd);\n\nfchdir() is not portable.\n\nI think it would be better to not chdir() at all. Yes, that means having \nto prepend the prefix to the names, but that is what git generally does \n(for that - and other - reasons).\n\n\t\tLinus\n"},{"id":"55148","messageId":"20071008022205.GA21277@coredump.intra.peff.net","threadId":"10173","inReplyTo":"alpine.LFD.0.999.0710071916510.23684@woody.linux-foundation.org","subject":"Re: [PATCH] Make git-clean a builtin","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2007-10-08T02:22:05Z","receivedAt":"2007-10-08T02:22:05Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Sun, Oct 07, 2007 at 07:17:50PM -0700, Linus Torvalds wrote:\n\n> fchdir() is not portable.\n> \n> I think it would be better to not chdir() at all. Yes, that means having \n> to prepend the prefix to the names, but that is what git generally does \n> (for that - and other - reasons).\n\nI was using the \"even Solaris has it!\" test, but yes, it's not POSIX. I\ndon't know how common it actually is (for curiosity's sake, do you know\nof a common platform that lacks it?). But I do agree that just building\nup the path and avoiding the chdir at all is simple and portable.\n\n-Peff\n"},{"id":"55164","messageId":"4709D027.3090003@viscovery.net","threadId":"10173","inReplyTo":"20071008022205.GA21277@coredump.intra.peff.net","subject":"Re: [PATCH] Make git-clean a builtin","fromName":"Johannes Sixt","fromEmail":"j.sixt@viscovery.net","sentAt":"2007-10-08T06:37:27Z","receivedAt":"2007-10-08T06:37:27Z","isPatch":true,"sender":{"key":"j6t@kdbg.org","avatar":"https://avatars.githubusercontent.com/u/14810926?v=4"},"body":"Jeff King schrieb:\n> On Sun, Oct 07, 2007 at 07:17:50PM -0700, Linus Torvalds wrote:\n> \n>> fchdir() is not portable.\n> \n> I was using the \"even Solaris has it!\" test, but yes, it's not POSIX. I\n> don't know how common it actually is (for curiosity's sake, do you know\n> of a common platform that lacks it?).\n\nWindows. ;)\n\n-- Hannes\n"},{"id":"55199","messageId":"alpine.LFD.0.999.0710081126340.4964@woody.linux-foundation.org","threadId":"10173","inReplyTo":"4709D027.3090003@viscovery.net","subject":"Re: [PATCH] Make git-clean a builtin","fromName":"Linus Torvalds","fromEmail":"torvalds@linux-foundation.org","sentAt":"2007-10-08T18:27:04Z","receivedAt":"2007-10-08T18:27:04Z","isPatch":true,"sender":{"key":"torvalds@linux-foundation.org","avatar":"https://avatars.githubusercontent.com/u/1024025?v=4"},"body":"\n\nOn Mon, 8 Oct 2007, Johannes Sixt wrote:\n\n> Jeff King schrieb:\n> > On Sun, Oct 07, 2007 at 07:17:50PM -0700, Linus Torvalds wrote:\n> > \n> > > fchdir() is not portable.\n> > \n> > I was using the \"even Solaris has it!\" test, but yes, it's not POSIX. I\n> > don't know how common it actually is (for curiosity's sake, do you know\n> > of a common platform that lacks it?).\n> \n> Windows. ;)\n\nNot just windows. Almost no \"older\" Unix has it. Including older versions \nof Linux.\n\n\t\tLinus\n"}]}