#!/bin/csh -f # normalize_filename # ------------------------------------------------------------------------------ # Shell script to normalize a filename. Reads a case-insensitive filename # from the command line or stdin and echoes it in the case (upper, lower, or # some combination) in which it exists on the disk, and with tilde expansion. # Does NOT try to eliminate symbolic links, replacing them with the names # they refer to -- simply returns a string that contains all the same links. # Strips off any optional trailing slash. The filename may include a relative # or absolute path. If the file does not exist, writes an empty string and # returns 1 (error) instead of 0 (success). # If the filename contains a $variable, writes the specified filename with # no changes (the $variable unexpanded, no changes to case, etc.), and # returns 2. Such a filename may be intended for use on a remote server # with the variable name expected to be expanded there. # ?? Update to deal with it partially? Updating all parts before the # ?? variable, still trimming off trailing slash, etc.? # ------------------------------------------------------------------------------ # Assumptions: # - Commands like ls, cd, etc., tolerate wrong case when identifying a file. # - ls command lists files in their normalized case when invoked on the # parent directory w/o mentioning the filename itself. # Effects: # - If file exists, echoes normalized filename to stdout, optionally converted # from relative to absolute form. Else, returns status 1 # Notes: # - Similar to Linux "readlink -f", which takes a filename and canonicalizes # it, expanding all symbolic links and showing the full path from / to the # file, working for symlinks or regular files or even files that don't # exist where it shows a canonical name to such a file in the current # directory. But doesn't expand symbolic links, and doesn't return names # of non-existent files. # Implementation Notes: # Portability Issues: # Revision History: # $Log$ # ------------------------------------------------------------------------------ if ("$1" == "-h" || "$1" == "--help") then echo "Usage: [-a] $0:t [filename]" echo "-a = Convert filename to absolute, as well as normalizing it" echo "Example: $0:t" echo "Example: $0:t abc.def" echo "Example: $0:t dir1/abc.def" echo "Example: $0:t ./dir1/abc.def/" echo "Example: $0:t /Users/fred/abc.def" echo "Example: $0:t ~fred/abc.def" echo "Example: $0:t ~/abc.def/" echo "Example: $0:t -a" echo "Example: $0:t -a abc.def" echo "Example: $0:t -a dir1/abc.def" echo "Example: $0:t -a ./dir1/abc.def/" echo "Example: $0:t -a /Users/fred/abc.def" echo "Example: $0:t -a ~fred/abc.def" echo "Example: $0:t -a ~/abc.def/" exit 1 endif set option_absolute = "false" if ("$1" == "-a") then set option_absolute = "true" shift endif if ($#argv == 0) then set name = $<:q else set name = $1:q endif # Abort early echoing the unmodified name if it contains a $variable. echo "$name" | grep '\$' >& /dev/null if (! $status) then echo "$name" exit 2 endif if ("$option_absolute" == "true") then # If the name doesn't start with a slash or a tilde, it's a relative # name, so prefix it with the current working directory to make it # absolute. set firstchar = "`echo $name:q | cut -c 1`" if ("$firstchar" != "/" && "$firstchar" != "~") then # Note: Use pwd or echo $cwd or echo $PWD, not pwd -P, to capture # name because pwd -P shows the real physical path to the # current directory, while the others show the symlinked path used # on the cd command. Want the symlinked path here because that's # what we promised our users in the comment at the top of the file. # Some users like the tridentcd script rely on that. set name = "$cwd/$name" endif endif # Strip trailing slash since :h below discards the last slash in the name # and the part that follows it. We want :h to discard the last part of the # path, ignoring the optional trailing slash, not the empty string following # the optional trailing slash. set name = "`echo $name:q | strip_trailing_slash`" # Loop through the parts of the name, from right to left, normalizing # each part, as we walk up the directory tree, and accumulating the # normalized_name. At each step, name is the remaining un-normalized name, # parent_name is the parent portion of name or ultimately the parent of the # entire name, and normalized_name is the accumulating result. # Note: Use pwd or echo $cwd or echo $PWD, not pwd -P, to capture # original_pwd because pwd -P shows the real physical path to the # current directory, while the others show the symlinked path used # on the cd command. Want the symlinked path here because that's # what we promised our users in the comment at the top of the file. # Some users like the tridentcd script rely on that. set original_pwd = $cwd:q set normalized_name = while (1 == 1) set done = "false" # Get the parent name # Note: :h discards the last slash in the name and the part that # follows it. Thus: abc/def --> abc # but has some quirky behavior also. See below. set parent_name = $name:h:q if ("$parent_name" == "") then # If :h removed the leftmost part, which started with a slash, we're # done and the topmost parent was the root (/). set parent_name = "/" set done = "true" else if ("$parent_name" == "$name") then # If :h removed nothing because we were down to a single part with no # leading slash, we're done, and the topmost parent was the current # working directory (./). set parent_name = "./" set done = "true" endif # Normalize the rightmost part, and prepend it the normalized_name, as # accumulated so far, separated by a slash, but avoid adding a trailing # slash if nothing has been accumulated yet. # Note: normalize_filename_part requires the specified part to exist in # the current directory, so we have to set the current directory # to the ancestor that contains the part. Do this by going back # to the original directory (just in case the specified filename # had a relative, not absolute path), and from there to the # appropriate ancestor, rather than by moving up the tree via "..", # to avoid the risk of finding a different path to the same file. # This preserves the same symbolic and hard links that were # specified. cd $original_pwd:q cd $parent_name:q set normalized_part = "`echo $name:t:q | normalize_filename_part`" if ("$normalized_part" == "") then echo "" exit 1 endif if ("$normalized_name" != "") set normalized_name = "/$normalized_name" set normalized_name = "$normalized_part$normalized_name" if ("$done" == "true") then # Add a leading slash if there was one. if ("$parent_name" == "/") set normalized_name = "/$normalized_name" break; else # Continue up the tree to the next ancestor set name = $parent_name:q endif end echo $normalized_name if ("$normalized_name" == "") then exit 1 else exit 0 endif