1

Edit: places where I said $HOME/.bin I should have written $HOME/bin or any equivalent is fine. I.E, any user-writable directory that is in the user's PATH.

So I have a bash script which I am distributing as a client for my API. Current version installs like this curl -s http://api.blah.com/install | sudo sh. I may try to deal with six different package management systems so that they can just apt-get or brew install at some point, but for now I am going with the one-liner since I want this solution to work for multiple systems. However, apparently there are quite a few users on systems like cygwin or even Macs who don't have sudo at all or don't have it set up.

The scenario is a user signs up for my API, enters their credit card information. I have a bash client for the API that doubles as a reference implementation and also a way to try out the API or deploy VMs and Docker containers using the command line. I want to create an easy way for users to install the API client.

For example, there used to be a one-line install for npm curl http://npmjs.org/install.sh | sh. Also homebrew has a one-line installer ruby -e "$(curl -fsSL https://raw.github.com/Homebrew/homebrew/go/install)" (see http://brew.sh).

My install script just downloads the API client script and puts it in /usr/bin and makes it executable. But I am thinking based on the issues with sudo and the fact that this doesn't really need to be installed globally, I would like to just install it into the user's $HOME/.bin or $HOME/local/bin (create that if it there is no existing equivalent).

This is my current install script:

#!/bin/bash
BASE="https://api.blah.com"

sudo bash -c "curl -s $BASE/mycmd > /usr/bin/mycmd"
sudo chmod uga+x /usr/bin/mycmd

First wrinkle that occurs to me is that many users are now in zsh. So if I add or modify a line in ~/.bashrc that updates the PATH to include $HOME/.bin, that won't work for those systems.

So to restate the question, what code should I use to download and install my script into a user-writable directory in that user's PATH ($HOME/local/bin, or whatever is available in PATH by default) directory and make sure that is in their PATH, with the requirement (or at least strong desire) that this will work on almost everyone's system (if they have something like a Unix prompt) (without requiring sudo)?

Since some people consider modifying the PATH to be evil, I would like to install in the default $HOME/whatever/bin for that system, that is already in the user's PATH, if possible, rather than automatically modifying their PATH to include some particular directory.

Thanks very much!

9
  • Look here - it might answer part of your question. Also, command is probably better than hash. Actually, I think it answers pretty much all of it. unix.stackexchange.com/a/126854/52934
    – mikeserv
    Commented May 25, 2014 at 7:10
  • Thanks mikeserv so it sounds like you are suggesting I have another problem I didn't think of -- the location of bash. If I limit it to OSX, Linux and cygwin, do I still have that problem? Also, does that question/response answer any main aspects of my question, such as making sure I get installed into a directory in the user's PATH without requiring sudo? Commented May 25, 2014 at 7:20
  • I'm not suggesting the location of the shell is the problem - I'm suggesting hardcoded anything into the main script is the problem. Just write a script that can fetch the values you need then have that script rewrite itself.
    – mikeserv
    Commented May 25, 2014 at 7:31
  • OK I see now thanks. So any idea what sort of code I could use to fetch/determine a user-writable directory that is in their PATH already? Commented May 25, 2014 at 7:32
  • Sure cat > ./file || cd elsewhere - that doesn't address $PATH though. Sorry. But getconf can help you there. Also set -C will ensure you don't overwrite anything you shouldn't.
    – mikeserv
    Commented May 25, 2014 at 7:32

4 Answers 4

4

There's a word for programs that inject themselves with the user's environment: viruses.

Just install to ~/bin/ (the historical de-facto standard for manually installed scripts) or ~/.local/bin/ (the modern, quasiformal, standard for scripts installed by a non-root package manager). Typically, user-friendly distros will already have one or both of those in $PATH.

Or better yet, interactively prompt the user for the installation prefix.

3
  • 1
    Regarding the allusion to viruses, are you suggesting that a 'one-line' install as I am aiming for here is a virus or is otherwise undesirable? If not, what do you mean? I specifically asked you not to make negative comments about the approach. If you wish me to install to ~/bin, I have the same problem I did originally: that requires privileges, e.g. sudo. Or is there another way that does not require sudo? So it sounds like that comment wasn't constructive. ~/.local/bin I did not know about. Which systems have this as a default available in PATH? I know for Ubuntu, its ~/bin. Commented May 25, 2014 at 7:06
  • I would prefer to automatically add my script to the path, since this is what the user intends when they run my install script after signing up for my service with their credit card.. quite far from a virus. Prompting them seems like an inconvenience and unnecessary if I can manage to create code that will place the file in a $HOME/ dir that is already in the path, or add one. Commented May 25, 2014 at 7:10
  • 1
    If the user wants to run something they downloaded without verifying it, that's their problem, so it's okay as long as the script is actually not evil. Editing PATH would be evil, no matter how convenient; just read and warn.
    – o11c
    Commented May 25, 2014 at 7:25
2

Why don't you just query the $PATH in the script itself? Check whether the user has any directories in their $PATH that they can write to (typically, ~/bin or ~/.local/bin). If they do, great, install there. If they don't, then you prompt. I believe that some distros, such as Ubuntu, will add ~/bin to the $PATH automatically if the directory exists but you can't assume that to always be the case.

The only other choice you have is to add whatever directory you install in to the $PATH by editing ~/.profile which is sourced by many shells. Forget about ~/.bashrc, not only is it bash specific but the $PATH has no business being set there in the first place. However, editing global variables like that is not a very good thing to do.

The best thing really is to check for writable directories and prompt if none were found.

2

Piping to sh is questionable, from a security point of view, and more subtly, a robustness and error handling point of view.

If your install script is only 3 lines, why not have the user just execute those 3 lines? They're already copy-and-pasting your curl command, and making the line that they're copy-and-pasting a bit longer would let you just do:

mkdir -p ~/bin && curl -s "https://api.blah.com/mycmd" > ~/bin/mycmd && chmod ugo+x ~/bin/mycmd

On most distros, ~/bin/ is put into $PATH, as long as the directory exists. At worst, your users may need to log out and back in to get the directory added to their $PATH. Even if it's not in their $PATH, you can tell people to just run ~/bin/mycmd instead of mycmd. If they're Unix-savvy enough, they'll know that they can make sure that ~/bin/ is in their path, and drop the extra 6 characters. If they're not Unix-savvy, and their distro is unhelpful, they have to type an extra 6 characters - probably not a big deal.

And if someone has root and wants to install somewhere other than ~/bin/, it's easy to change the above line to do so - and if they don't understand how to make that change, they shouldn't be changing the root filesystem!

0

I ended up giving them two options on my "Getting Started" page. I explain briefly what the installer script does, such as looking for ~/.local/bin or the like and then potentially adding that to PATH in ~/.zshrc or ~/.bashrc. I also give them the option of manually installing, instead of using the script, with simple instructions to do that.

To run the automatic installer the user would paste and execute a command like this:

curl -s https://thesite.com/installmycmd > /tmp/inst; source /tmp/inst

This is the installmycmd script:

#!/bin/bash

BASE="https://thesite.com"

declare -a binddirs
bindirs=($HOME/bin $HOME/.local/bin $HOME/.bin)

founddir="false"

findprofile() {
  profiles=($HOME/.zshrc $HOME/.bashrc $HOME/.bash_login $HOME/.login $HOME/.profile)
  for prof in "${profiles[@]}"; do
    if [ -f "$prof" ]; then
      echo "$prof"
      return
    fi
  done
  touch $HOME/.profile
  echo "$HOME/.profile"
}

for bindir in "${bindirs[@]}"; do
  if [ -d "$bindir" ]; then
    founddir=true
    echo "You have a user bin dir here $bindir."
    whichprofile=$(findprofile)
    pathline=$(grep ^PATH= $whichprofile)
    if [[ ! $pathline == *$bindir* ]]; then
      echo "Appending $bindir to PATH in $whichprofile"
      echo -e "\nexport PATH=\$PATH:$bindir" >> "$whichprofile"
      NEWPATH=$PATH:$bindir      
      export NEWPATH
    else
      echo "That is in your PATH in $whichprofile"
    fi
    break;
  fi
done

if [ ! -z $NEWPATH ]; then
  echo "Exported PATH: $NEWPATH" 
  export PATH=$NEWPATH
fi

if [[ "$founddir" == "false" ]]; then
  echo "Could not find ~/.bin or ~/.local/bin or ~/bin."
  echo "Creating ~/.local/bin and adding to PATH"

  mkdir -p $HOME/.local/bin
  bindir=$HOME/.local/bin

  whichprofile=$(findprofile)
  echo "Appending PATH edit to $whichprofile"

  echo -e "\nexport PATH=$PATH:$HOME/.local/bin" >> "$whichprofile"
  export PATH=$PATH:$HOME/.local/bin
fi

bash -c "curl -s $BASE/JSON.sh > $bindir/JSON.sh"
bash -c "curl -s $BASE/mycmd > $bindir/mycmd"
chmod ug+x $bindir/mycmd
chmod ug+x $bindir/JSON.sh

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .