13

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?

Ryan David Ward
  • 300
  • 1
  • 4
  • 13
  • First: You don't have a script here; you have a function, and I don't see where you are calling the function. Then, autoloading is something which relates to a shell instance, not to the login process. You can't expect to "log in and then have your packages magically autoloaded in each shell". You need to make up your mind, in which shell instances you want to have it. If you want it for instance in each interactive shell, put it into .zshrc. Should it go into each login shell, put it into .zlogin, etc. – user1934428 Mar 09 '18 at 07:58
  • I put these kind of functions into ~/.alias and source ~/.alias in my ~/.zshrc. No need for fpath here. – pfnuesel Mar 09 '18 at 08:02

2 Answers2

24

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.

  • 2
    I learned more here than I have in years of trying to understand this stuff on my own. You should write a book! The last bit about #autoload is the key! – Ryan David Ward Mar 09 '18 at 19:18
  • pps, bsdtar xf works better on most archive types in a more Unixy way (is able to restore Unix metadata attributes, hardlinks, symlinks...) – Stéphane Chazelas Mar 09 '18 at 20:18
1

There are several possibilities to fix this problem:

  • you can put this function in your .zshrc;
  • you can source your script;
  • you can put the code of your function directly in a file called extract in a directory that is in your PATH.
rools
  • 486