Search code examples
zshzsh-alias

Expanding/resolving variable inside single quotes in zsh alias function


I have a nice one-liner that I want to create an alias of. An instance of that one-liner looks like the following:

less +G $(find /var/logs -name 'service-output.root*' -printf '%T@ %p\n' | sort -n | tail -1 | cut -f2- -d' ')

Here it looks into the /var/logs/ directory, does prefix matching for all files starting with service-output.root and opens the latest with less.

The alias I want to create should get the directory and the prefix regex as arguments, thus I came up with the following alias function

function l-log {
  if [[ ! ${#} -eq 2 ]] || [[ ! -d ${1} ]]; then
    echo "Usage: ${0} <dir> <regex>"
    return 1
  fi
  local DIR=${1}
  local FILE_REGEX=${2}

 find ${DIR} -name '${FILE_REGEX}' -printf '%T@ %p\\n' | sort -n | tail -1 | cut -f2- -d' '
}

The problem is that this does not work.

$ l-log /var/logs/ service-output.root*
zsh: no matches found: service-output.root*

I have enabled function debugging

$ functions -t l-log

and tinkering a bit it seems that the problem is about variable ${FILE_REGEX} not being properly expanded because it is single quoted (and it has to be single quoted)

$ l-log /apollo/var/logs/ apollo-update.root                                                                                                                                                                           1 ↵
+l-log:1> [[ ! 2 -eq 2 ]]
+l-log:1> [[ ! -d /apollo/var/logs/ ]]
+l-log:5> local DIR=/apollo/var/logs/
+l-log:6> local FILE_REGEX=apollo-update.root
+l-log:13> find /apollo/var/logs/ -name '${FILE_REGEX}' -printf '%T@ %p\\n'
+l-log:13> sort -n
+l-log:13> tail -1
+l-log:13> cut -f2- '-d '

I have tried several things like double quoting the variables, escaping single and double quotes, using double double (!) quotes (""${FILE_REGEX}"") and back ticking (`) but I haven't managed to get +l-log:13> above to become find /apollo/var/logs/ -name 'service-output.root*' -printf '%T@ %p\\n'

Any help would be great!


Solution

  • There are a few things happening here.

    • Minor point: You've created a function (which is great!), not an 'alias function'. Aliases are a different thing.
    • Invoking a function with an argument containing a wildcard needs special handling, or zsh will try to expand / glob the wildcard before it even gets to the function - that's where the zsh: no matches found message is coming from. Some ways to call the function:
      • option 1, quoting: l-log /var/logs/ 'service-output.root*'
      • option 2, escaping: l-log /var/logs/ service-output.root\*
      • option 3, noglob: noglob l-log /var/logs/ service-output.root*
    • You do NOT need the single quotes in the call to find. Use ${FILE_REGEX}.

    The last part can be a bit confusing, since wildcards usually need to be quoted when calling find. But here the wildcard is contained in a variable, so zsh is not going to do any globbing unless you specifically ask for that. The single quotes are preventing the variable $FILE_REGEX from being expanded, and you need that to occur.


    BTW, you can do this kind of operation without find, using just zsh. Here's one option:

    #!/usr/bin/env zsh
    llogBase() {
      local pattern=${1:?}/**/${2:?}(om[1])
      less -- ${~pattern}
    }
    

    Some notes on the parts:

    • pattern=${1:?}/**/${2:?}(om[1]) - this builds a glob pattern that we'll use in the next operation.
      • .../**/... - uses the recursive glob operator **/ to traverse the directory tree, similar to find.
      • ${1:?} - substitutes the directory name supplied by the caller as the first parameter. With the ${ :?} expansion, this will print an error message and exit the function if the parameter is empty.
      • ${2:?} - substitutes the second parameter, the filename wildcard pattern.
      • (om) - a glob qualifier. This asks the shell to sort (o) the results by modification time (m), with the most recent file listed first.
      • ([1]) - another glob qualifier that selects which items to include. In this case the result will have at most one filename; with the (om) qualifier it will be the most recent matching file.
    • less -- ${~pattern} - expands the pattern and calls less.
      • ${~pattern} - expands to the contents of the pattern variable. Since it is a ${~...} expansion, the shell then performs globbing with the resulting string, and substitutes the single filename that matches the pattern (if one exists).
      • less -- <filename> - calls less to display the file. The -- is a guard against filenames that may start with -.
    • zsh allows expansions to be nested, so the whole thing could be one slightly cryptic line: less -- ${~:-${1:?}/**/${2:?}(om[1])}.

    This function can now be used to search for a file, but the wildcard will need to quoted or escaped as described above, e.g.:

    llogBase /var/logs 'service-output.root*'
    

    To avoid quoting patterns for this command, we can use an alias to always add the noglob precommand modifier:

    alias llog='noglob llogBase'
    

    Calling it with the alias:

    llog /var/logs service-output.root*