6
\$\begingroup\$

I wrote a tiny shell script that basically installs a script into your system by copying it to someplace like /usr/local/bin, chmodding it, and adding it to your PATH. It works on both Unix and Windows (via an emulation layer like MSys or Cygwin). Its source code is available on GitHub. Here is the script in its entirety:

#!/bin/sh
set -e
help()
{
 echo 'sinister is a simple installer for shell scripts.'
 echo
 echo 'Usage: sinister -u url [options]'
 echo
 echo 'Options:'
 echo
 echo '-?,-h,--help: Print this help and exit.'
 echo '-c,--chmod: The mode to chmod your script. Defaults are a+x and u+x.'
 echo '-d,--debug: sinister should be run in debug mode.'
 echo '-l,--local: The script should be saved for the current user, instead of the entire machine.'
 echo '-n,--name: The name of the file the script should be saved to. Mandatory if no URL is given.'
 echo '-o,--output: The output directory the script should be saved to.'
 echo 'By default this is ~/bin on Unix systems, and C:\Users\you\AppData\Local\script on Windows.'
 echo '-u,--url: The location of the script online. e.g: https://github.com/you/repo/raw/master/script'
 echo 'If no URL is given the script is read from stdin.'
}
on_windows()
{
 uname | grep -q '[CYGWIN|MINGW|MSYS]'
}
per_user()
{
 test $LOCAL = 'true'
}
run_powershell()
{
 powershell -NoProfile -ExecutionPolicy Bypass "1ドル"
}
getpath_windows()
{
 run_powershell "[Environment]::GetEnvironmentVariable('PATH', '1ドル')"
}
setpath_windows()
{
 run_powershell "[Environment]::SetEnvironmentVariable('PATH', '1ドル', '2ドル')"
}
pwd_windows()
{
 run_powershell 'Get-Location'
}
CHMOD=
LOCAL='false'
NAME=
OUTPUT=
URL=
test $# -ne 0
while [ $# -gt 0 ]
do
 case "1ドル" in
 '-?'|-h|--help) help; exit 0 ;;
 -c|--chmod) CHMOD="2ドル"; shift ;;
 -d|--debug) set -x ;;
 -l|--local) LOCAL='true' ;;
 -n|--name) NAME="2ドル"; shift ;;
 -o|--output) OUTPUT="2ドル"; shift ;;
 -u|--url) URL="2ドル"; shift ;;
 *) help; exit 1 ;;
 esac
 shift
done
if [ -z "$CHMOD" ]; then
 if per_user; then
 CHMOD='u+x' # Executable for the current user
 else
 CHMOD='a+x' # Executable for all users
 fi
fi
if [ -z "$URL" ]; then
 test ! -z "$NAME"
 SCRIPT=$(< /dev/stdin) # read from standard input
else
 NAME=${URL##*/} # Grab everything after the last /
 if which curl > /dev/null 2>&1; then
 SCRIPT=$(curl -sSL "$URL")
 else # Assume wget is installed
 SCRIPT=$(wget -q -O - "$URL")
 fi
fi
if [ -z "$OUTPUT" ]; then
 if per_user; then
 if on_windows; then
 OUTPUT=~/AppData/Local/"$NAME"
 else
 OUTPUT=~/bin
 fi
 else
 if on_windows; then
 OUTPUT='/C/Program Files/$NAME'
 else
 OUTPUT='/usr/local/bin'
 fi
 fi
fi
# Use sudo on Unix
! on_windows && SUDO='sudo'
# Where the magic happens
$SUDO mkdir -p "$OUTPUT"
cd "$OUTPUT"
echo "$SCRIPT" | $SUDO tee "$NAME" > /dev/null
$SUDO chmod $CHMOD "$NAME"
# Add $OUTPUT to PATH if it's not in it
if ! which "$NAME" > /dev/null 2>&1; then
 if on_windows; then
 if per_user; then
 TARGET='User'
 else
 TARGET='Machine'
 fi
 CURRENT_PATH=$(getpath_windows $TARGET)
 CURRENT_DIRECTORY=$(pwd_windows)
 setpath_windows "$CURRENT_PATH;$CURRENT_DIRECTORY" $TARGET
 else # Unix
 CONTENTS="export PATH=\$PATH:$OUTPUT"
 if per_user; then
 echo "$CONTENTS" >> ~/.profile
 else
 sudo echo "$CONTENTS" >> /etc/profile
 fi
 fi
fi

Any feedback would be appreciated; I haven't tested this on Windows yet so some of the PowerShell snippets might be untested.

200_success
146k22 gold badges190 silver badges479 bronze badges
asked Dec 27, 2015 at 20:58
\$\endgroup\$

2 Answers 2

4
\$\begingroup\$

I see a number of things that may help you improve this program. I don't have a Windows machine handy, so these will all be Linux-based observations.

For local installation, don't use sudo

Right now, when the program is created for a local user, the use of sudo to copy it means it ends up owned by root. That means that although it's in the local user's bin, they can't run it. For local installation, sudo shouldn't be needed at all and would be a lot safer.

Don't duplicate entries in .profile

Right now, each invocation of your program will cause an additional possibly duplicate line to be added to .profile. Better would be to only add a line if it doesn't yet exist there.

Use only POSIX features

Right now the code includes this line:

SCRIPT=$(< /dev/stdin) # read from standard input

However, that's not necessarily portable. Instead, use cat as in:

SCRIPT=$(cat /dev/stdin) # read portably from standard input
answered Dec 27, 2015 at 21:20
\$\endgroup\$
2
  • \$\begingroup\$ Thanks for the feedback! I don't think I'm duplicating entries in .profile by the way, I do a check for if ! which "$NAME" before writing to it. \$\endgroup\$ Commented Dec 27, 2015 at 21:36
  • \$\begingroup\$ The check only looks at the current path rather tha the contents of the file. If the user hasn't re-run .profile between script invocations, the result is duplicate lines. \$\endgroup\$ Commented Dec 27, 2015 at 21:40
0
\$\begingroup\$

It’s been some time since this question was posted but I thought I could add some useful feedback as the following issues are still present on the GitHub code.

Testing for command availability

if which curl > /dev/null 2>&1; then
 SCRIPT=$(curl -sSL "$URL")
else # Assume wget is installed
 SCRIPT=$(wget -q -O - "$URL")
fi

While it will work on many systems, which isn’t recommended for checking if a command exists. See Why not use "which"? What to use then? for a full discussion.

command -v is most commonly recommended as a good alternative. The -v flag was optional in the 2004 Edition but only became mandatory in the 2013 Edition of POSIX 2008.1.

I prefer to define the following function:

exists_command() {
 command -v "1ドル" >/dev/null 2>&1
}

Also, if I recall correctly, a base install of Cygwin had neither wget nor curl installed – though it did have lynx (this may have changed since I last looked). Accordingly, the apt-cyg utility recommends using lynx to install it.

if exists_command curl; then
 SCRIPT=$(curl -sSL "$URL")
elif exists_command wget; then
 SCRIPT=$(wget -q -O - "$URL")
elif exists_command lynx; then
 SCRIPT=$(lynx -source "$URL")
else
 echo You need to install ‘curl’ or ‘wget’ to download URLs.
fi

Output redirection and sudo

When using shell output redirection, the shell is running with the privileges of the user who started the shell. When the shell tries to open a file for writing, it can only do so if the user has permission to do so. In the the following code, only the echo command is run with super user privileges.

sudo echo "$CONTENTS" >> /etc/profile

To append content to a file, the output file needs to be opened with superuser privileges. A simple way to do that is to use the tee utility (with the -a flag to append to the file):

echo "$CONTENTS" | sudo tee -a /etc/profile
answered Mar 4, 2016 at 18:50
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.