How to make custom zsh script executable automatically

functionprofilescriptingzsh

I must be missing something incredibly simple about how to do this, but I have a simple script:

extract () {
  if [ -f $1 ] ; then
    case $1 in
      *.tar.bz2)   tar xvjf $1    ;;
      *.tar.gz)    tar xvzf $1    ;;
      *.tar.xz)    tar xvJf $1    ;;
      *.bz2)       bunzip2 $1     ;;
      *.rar)       unrar x $1     ;;
      *.gz)        gunzip $1      ;;
      *.tar)       tar xvf $1     ;;
      *.tbz2)      tar xvjf $1    ;;
      *.tgz)       tar xvzf $1    ;;
      *.zip)       unzip $1       ;;
      *.Z)         uncompress $1  ;;
      *.7z)        7z x $1        ;;
      *.xz)        unxz $1        ;;
      *.exe)       cabextract $1  ;;
      *)           echo "\'$1': unrecognized file compression" ;;
    esac
  else
    echo "\'$1' is not a valid file"
  fi
}

The script works, but I can't seem to get it to be executable by default when I log in.

I have consulted this very thorough answer: How to define and load your own shell function in zsh, and I have managed to get my $fpath to show the appropriate directory where I have stored the function.

In my .zshrc profile, I have added fpath=( ~/.local/bin "${fpath[@]}" ) to the bottom, which is the path where my functions live.

When I input echo $fpath, I get:

/home/me/.local/bin /home/me/.oh-my-zsh/plugins/git /home/me/.oh-my-zsh/functions /home/me/.oh-my-zsh/completions ...

However, it does not work unless I explicitly type autoload -Uz extract each time when I log in.

Is there a way I can get the whole directory to autoload when I log in?

Best Answer

You're mixing up scripts and functions.

Making a script

A script is a standalone program. It may happen to be written in zsh, but you can invoke it from anywhere, not just from a zsh command line. If you happen to run a script written in zsh from a zsh command line or another zsh script, that's a coincidence that doesn't affect the script's behavior. The script runs in its own process, it doesn't influence its parent (e.g. it can't change variables or the current directory).

Your code accomplishes a standalone task which can be invoked from anywhere and doesn't need to access the state of the shell that runs it, so it should be a script, not a function.

A script must be an executable file: chmod +x /path/to/script. It must start with a shebang line to let the kernel know what program to use to interpret the script. In your case, add this line to the top of the file:

#!/usr/bin/env zsh

Put the file in a directory that is listed in the $PATH variable. Many systems set up either ~/bin or ~/.local/bin in a user's default PATH, so you can use these. If you want to add another directory, see http://unix.stackexchange.com/questions/26047/how-to-correctly-add-a-path-to-path

When you type a command name that isn't an alias, a function or a builtin, the shell looks for an executable file of that name in $PATH and executes it. Thus you don't need to declare the script to the shell, you just drop it in the right place.

Making a function

A function is code that runs inside an existing shell instance. It has full access to all the shell's state: variables, current directory, functions, command history, etc. You can only invoke a function in a compatible shell.

Your code can work as a function, but you don't gain anything by making it a function, and you lose the ability to invoke it from somewhere else (e.g. from a file manager).

In zsh, you can make a function available for interactive sessions by including its definition in ~/.zshrc. Alternatively, to avoid cluttering .zshrc with a very large number of functions, you can use the autoloading mechanism. Autoloading works in two steps:

  1. Declare the function name with autoload -U myfunction.
  2. When myfunction is invoked for the first time, zsh looks for a file called myfunction in the directories listed in $fpath, and uses the first such file it finds as the definition of myfunction.

All functions need to be defined before use. That's why it isn't enough to put the file in $fpath. Declaring the function with autoload actually creates a stub definition that says “load this function from $fpath and try again”:

% autoload -U myfunction
% which myfunction
myfunction () {
        # undefined
        builtin autoload -XU
}

Zsh does have a mechanism to generate those stubs by exploring $fpath. It's embedded in the completion subsystem.

  • Put #autoload as the first line of the file.
  • In your .zshrc, make sure that you fully set fpath before calling the completion system initialization function compinit.

Note that the file containing a function definition must contain the function body, not the definition of the function, because what zsh executes when the function is called is the content of the file. So if you wanted to put your code in a function, you would put it in a file called extract that is in one of the directories on $fpath, containing

#autoload
if [ -f $1 ]; then
…

If you want to have initialization code that runs when the function is loaded, or to define auxiliary functions, you can use this idiom (used in the zsh distribution). Put the function definition in the file, plus all the auxiliary definitions and any other initialization code. At the end, call the function, passing the arguments. Then myfunction would contain:

#autoload
my_auxiliary_function () {
  …
}
myfunction () {
  …
}
myfunction "$@"

P.S.

7z x works on most archive types.

Related Question