2

I have a list of stored file paths ie (impl/src/main/java/org/jboss/weld/util/collections/multimaps.java). The paths were coverted to lowercase prior to storing.

Now, I need to access the file in a bash script. Is there a simple command to "fix" the casing and find the file? For simplicity, assume that there are not multiple versions of the file with different cases. Note also, that the directories in the path may need to have their casing fixed.

I've thought about using find, but you can't have a full path so from the root find . -iname multimaps.java might return multiple files only one of which is in the correct directory structure.

I need the corrected path to end up in a variable in a bash script.

3

GNU find has a -iwholename predicate which solves my problem:

find . -iwholename ./impl/src/main/java/org/jboss/weld/util/collections/multimaps.java

To get it in a variable:

cased=`find . -iwholename "$filepath"`
  • Note from a performance point of view that it scans the whole directory tree to find that file (while for instance it wouldn't need to descend into ./impl/obj/ or ./whatever). – Stéphane Chazelas May 8 '17 at 20:21
  • Would the most performant option in bash be splitting on the slashes and looping over the files yourself? Since the directories are likely lowercase on the actual system, looping on most of the contents probably wouldn't be necessary, it could just check if the directory exists and jump into it. That would be a much more complicated set of code though? – Loren May 9 '17 at 0:06
  • Getting the results into a variable is the part which really sucks. You can probably use find with -exec to execute (even a complex) command on each match. – tripleee May 9 '17 at 7:08
3

With zsh:

setopt extendedglob
files=((#i)${(f)^"$(<file.list)"}(N))
printf '%s\n' $files

(where file.list is the file containing that list of paths in lower case, one per line).

  • (#i): case insensitive glob (needs extendedglob)
  • ${(f)"$expansion"}: split the expansion on line feeds (newline)
  • ${^array}: do brace-expansion type of expansion for the array.
  • (N): nullglob glob qualifier (remove globs that don't match).

For a single file, it's just:

setopt extendedglob
file=impl/src/main/java/org/jboss/weld/util/collections/multimaps.java
matches=((#i)$file)

(here omitting the (N) so you do get an error if there's no match)

The ksh93 equivalent would be:

matches=(~(iN)"$file") # N for nullglob, otherwise the glob expands to itself
                       # instead of giving an error when it doesn't match.

bash has a nocaseglob option to match case-insensitively. However

matches=($file)

won't work. You'd need to activate globbing (by using any of the glob operators) for each path component like in

shopt -s nocaseglob nullglob
matches=([i]mpl/[s]rc/[m]ain/[j]ava/[o]rg/[j]boss/[w]eld/[u]til/[c]ollections/[m]ultimaps.java)

With any shell with support for a [...] glob operator (so in essence, all but fish), you could also rewrite the glob as [iI][mM][pP][lL]/[sS][rR]... and so on.

Note that it's different from a find . -ipath "./$file" in that the shell doesn't need to crawl the whole directory tree. It first reads the current directory to find directories (or symlinks to directories! Another difference from find) called impl or Impl or ImPl, etc. then only reads the content of those directories to find some src/SRC/srC etc. and so on.

Another difference is that it still works properly if the paths components happen to contain glob operators (like impl/***src***/whatever).

  • I don't currently have zsh installed, though I could potentially install it. Does this avoid looping over the whole file tree? – Loren May 9 '17 at 0:07
  • No looping involved, Loren -- but you would have to have your current directory set to exactly the parent of the file path(s), assuming they're relative (don't start with a /) – Jeff Schaller May 9 '17 at 2:07
  • @Loren, see edit. – Stéphane Chazelas May 9 '17 at 6:29
1

In bash:

shopt -s nocasematch globstar
for f in **; do for search in "${files[*]}"; do [[ $f == $search ]] && echo Found "$search" at: "$f"; done; done

The nocasematch shell option tells the [[ ... == ... ]] command to ignore case when comparing strings. The globstar shell enables the ** brute-force bash emulation of find; it differs from find in that it will skip dot-files (unless shopt -s dotglob is set) and follows symlinks.

  • 1
    bash's ** no longer follows symlinks since 4.3 – Stéphane Chazelas May 8 '17 at 20:58
  • Is this any more efficient than the implementation in GNU find? If I'm reading it correctly, it looks like this will also scan the whole directory tree the same as find (as pointed out in @Stéphane's comment). – Loren May 8 '17 at 23:57
  • 1
    Likely slower than find; just wanted to point out another option. – Jeff Schaller May 9 '17 at 0:19
1

An approach to avoid scanning the tree again and again for every file would be to list the directory tree once and then look-up the lowercase translation of the path in a hash table. For instance with:

NL='
'
find . -name "*$NL*" -prune -o -print | awk '
  !list_processed {paths["./" $0]; next}
  tolower($0) in paths' file.list list_processed=1 -

(where file.list contains the list of lowercase files, one per line).

We're skipping the files (and not descending into directories) with newlines in their names as they can't possibly be stored in the file.list if that list is newline delimited (and would break that post-processing).

Note however that that skipping won't work if file names also contain invalid characters. See the byte vs character section at Why is looping over find's output bad practice? for more details.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.