180

I've written a script that takes, as an argument, a string that is a concatenation of a username and a project. The script is supposed to switch (su) to the username, cd to a specific directory based upon the project string.

I basically want to do:

su $USERNAME;  
cd /home/$USERNAME/$PROJECT;  
svn update;  

The problem is that once I do an su... it just waits there. Which makes sense since the flow of execution has passed to switching to the user. Once I exit, then the rest of the things execute but it doesn't work as desired.

I prepended su to the svn command but the command failed (i.e. it didn't update svn in the directory desired).

How do I write a script that allows the user to switch user and invoke svn (among other things)?

Mateusz Piotrowski
  • 8,029
  • 10
  • 53
  • 79
Son of the Wai-Pan
  • 12,371
  • 16
  • 46
  • 55

9 Answers9

169

Much simpler: use sudo to run a shell and use a heredoc to feed it commands.

#!/usr/bin/env bash
whoami
sudo -i -u someuser bash << EOF
echo "In"
whoami
EOF
echo "Out"
whoami

(answer originally on SuperUser)

Bora M. Alper
  • 3,538
  • 1
  • 24
  • 35
Dan Dascalescu
  • 143,271
  • 52
  • 317
  • 404
  • 7
    This answer worked best. I recommend to use option `-i` to get `someuser`'s expected environment. – not2savvy Jun 13 '17 at 15:04
  • 3
    `sudo` may be convenient but it's no good if its not out of the box (like on AIX). `su -c 'commands'` is the correct answer. – Tricky Mar 22 '18 at 05:07
  • 3
    Epic solution ! – 3bdalla Nov 07 '18 at 14:59
  • How to make variables available in herdoc's scope? – dotslashlu Nov 23 '18 at 09:12
  • on AIX it throws this error ksh: sh: 0403-006 Execute permission denied. – AhmedRana Aug 06 '19 at 06:35
  • @dotslashlu, it is not possible directly since heredoc contents will be executed within a subshell, and `sudo` drops all environment variables by default. But in fact heredoc is a plain string passed to `bash`, and current environment variables are expanded in it. So if you set `MYVAR=42` and then use heredoc `... bash < – MarSoft Dec 24 '19 at 23:16
  • Hi @dan-dascalescu what if the user we are changing to has a password that needs to be entered? – DJ_Stuffy_K Feb 23 '21 at 01:28
  • 1
    @DJ_Stuffy_K: it doesn't matter. You only need to enter the password of the currently logged in user, which needs root privileges. See [differences between sudo and su](https://www.howtogeek.com/111479/htg-explains-whats-the-difference-between-sudo-su/). – Dan Dascalescu Feb 23 '21 at 11:49
  • This is ONLY for root user, and if you are not a root and need to enter a password? No answer – Josef Klimuk Jun 08 '22 at 14:00
108

The trick is to use "sudo" command instead of "su"

You may need to add this

username1 ALL=(username2) NOPASSWD: /path/to/svn

to your /etc/sudoers file

and change your script to:

sudo -u username2 -H sh -c "cd /home/$USERNAME/$PROJECT; svn update" 

Where username2 is the user you want to run the SVN command as and username1 is the user running the script.

If you need multiple users to run this script, use a %groupname instead of the username1

Kimvais
  • 38,306
  • 16
  • 108
  • 142
  • I have a similar issue, but I wanted to run `chsh` for the other users. My issue is listed here at http://stackoverflow.com/q/15307289/80353 How do I adapt your answer in my situation? – Kim Stacks Mar 10 '13 at 03:32
  • I did this - but it still asked me for a password. – Hippyjim Oct 19 '13 at 16:30
  • @Hippyjim are you sure you got the usernames right way round? – Kimvais Oct 19 '13 at 17:23
  • 1
    I did - it turns out that I also needed to allow usage of /bin/bash as well. – Hippyjim Oct 20 '13 at 12:27
  • 4
    Whether you use `sudo` or `su` is of secondary importance, though `sudo` is a lot more secure and convenient. – tripleee Feb 24 '15 at 08:24
  • `sudo` may be convenient but it's no good if its not out of the box (like on AIX). `su -c 'commands'` is the correct answer. – Tricky Mar 22 '18 at 04:48
68

You need to execute all the different-user commands as their own script. If it's just one, or a few commands, then inline should work. If it's lots of commands then it's probably best to move them to their own file.

su -c "cd /home/$USERNAME/$PROJECT ; svn update" -m "$USERNAME" 
Douglas Leeder
  • 52,368
  • 9
  • 94
  • 137
64

Here is yet another approach, which was more convenient in my case (I just wanted to drop root privileges and do the rest of my script from restricted user): you can make the script restart itself from the correct user. This approach is more readable than using sudo or su -c with a "nested script". Let's suppose it is started as root initially. Then the code will look like this:

#!/bin/bash
if [ $UID -eq 0 ]; then
  user=$1
  dir=$2
  shift 2     # if you need some other parameters
  cd "$dir"
  exec su "$user" "$0" -- "$@"
  # nothing will be executed from root beyond that line,
  # because exec replaces running process with the new one
fi

echo "This will be run from user $UID"
...
MarSoft
  • 3,555
  • 1
  • 33
  • 38
  • 7
    I wonder why this is not higher rated. It is solving the original question the best while keeping everything in bash. – SirVer Jun 04 '16 at 15:54
  • 2
    Or `runuser -u $user -- "$@"`, as stated in su(1) – cghislai Jul 29 '18 at 15:44
  • 1
    Yes, `runuser` can be used in an opposite situation - i.e. to drop root privileged. – MarSoft Jul 31 '18 at 17:43
  • 1
    `exec su "$user" "$0" -- "$@"` – macieksk Dec 13 '18 at 00:41
  • 1
    Thanks @macieksk, nice catch. Will update. The `--` is really useful. – MarSoft Dec 13 '18 at 10:29
  • 1
    Presented approach is the best of the all and is complete. All is written inside single script, and all uses bash. Effectively, this script is runned twice. At first script test it is root, then prepare environment, and change user by su. But do it with exec command, then there is only single script instance. With second loop, phrase if/fi is ommited, then script directly do prepared action. In this example it is echo. All of other approaches resolve problem only partially. Of course this script can be run under sh not bash, but we must test $HOME special variable, not $UID – Znik Feb 11 '20 at 07:19
  • @Znik, if we test `$HOME` then it may break if `root`'s home directory was set to something other than `/root` in `/etc/passwd`. This is sometimes the case in some embedded systems. – MarSoft Feb 11 '20 at 21:12
  • 1
    This seems to use the users default shell instead of the one specified in the shebang. Anyone has an idea how to change this? – darkdragon May 26 '20 at 21:54
  • 2
    Replacing the exec line with `exec sudo -i -u "$user" $( readlink -f "$0" ) -- "$@"` works for me. – darkdragon May 26 '20 at 22:00
59

Use a script like the following to execute the rest or part of the script under another user:

#!/bin/sh

id

exec sudo -u transmission /bin/sh - << eof

id

eof
keyser
  • 18,829
  • 16
  • 59
  • 101
Walder
  • 599
  • 4
  • 2
  • 11
    You may want to use "sudo -i -u ..." to ensure that stuff like $HOME is set correctly. – Bryan Larsen Aug 08 '14 at 14:07
  • 1
    Sorry for noob question, but whats an id here? – Nitin Jadhav Dec 23 '16 at 15:13
  • 1
    @NitinJadhav, he used it here just to show the ID of the current user, the ID of root is 0, so the first id will show you some number, but the second one will definetly show 0 (because the second one was executed inside a block run by root). You can user `whoami` instead of `id` which will return the name instead of the id – Mohammed Noureldin Dec 24 '16 at 14:17
  • @MohammedNoureldin Thanks! – Nitin Jadhav Dec 29 '16 at 17:06
  • `sudo` may be convenient but it's no good if its not out of the box (like on AIX). `su -c 'commands'` is the correct answer. – Tricky Mar 22 '18 at 05:07
8

Use sudo instead

EDIT: As Douglas pointed out, you can not use cd in sudo since it is not an external command. You have to run the commands in a subshell to make the cd work.

sudo -u $USERNAME -H sh -c "cd ~/$PROJECT; svn update"

sudo -u $USERNAME -H cd ~/$PROJECT
sudo -u $USERNAME svn update

You may be asked to input that user's password, but only once.

iamamac
  • 9,632
  • 4
  • 35
  • 30
7

It's not possible to change user within a shell script. Workarounds using sudo described in other answers are probably your best bet.

If you're mad enough to run perl scripts as root, you can do this with the $< $( $> $) variables which hold real/effective uid/gid, e.g.:

#!/usr/bin/perl -w
$user = shift;
if (!$<) {
    $> = getpwnam $user;
    $) = getgrnam $user;
} else {
    die 'must be root to change uid';
}
system('whoami');
P-Nuts
  • 879
  • 5
  • 6
  • -1 as It **IS** possible with sudo to temporarily gain other user's rights. – Kimvais Jan 01 '10 at 12:22
  • 5
    It's not possible in the sense that the user the shell script itself runs as can't be changed (which is what the original question asked). Invoking other processes with sudo doesn't change who the script itself is running as. – P-Nuts Jan 01 '10 at 12:53
2

This worked for me

I split out my "provisioning" from my "startup".

 # Configure everything else ready to run 
  config.vm.provision :shell, path: "provision.sh"
  config.vm.provision :shell, path: "start_env.sh", run: "always"

then in my start_env.sh

#!/usr/bin/env bash

echo "Starting Server Env"
#java -jar /usr/lib/node_modules/selenium-server-standalone-jar/jar/selenium-server-standalone-2.40.0.jar  &
#(cd /vagrant_projects/myproj && sudo -u vagrant -H sh -c "nohup npm install 0<&- &>/dev/null &;bower install 0<&- &>/dev/null &")
cd /vagrant_projects/myproj
nohup grunt connect:server:keepalive 0<&- &>/dev/null &
nohup apimocker -c /vagrant_projects/myproj/mock_api_data/config.json 0<&- &>/dev/null &
httpete
  • 2,765
  • 26
  • 34
-2

Inspired by the idea from @MarSoft but I changed the lines like the following:

USERNAME='desireduser'
COMMAND=$0
COMMANDARGS="$(printf " %q" "${@}")"
if [ $(whoami) != "$USERNAME" ]; then
  exec sudo -E su $USERNAME -c "/usr/bin/bash -l $COMMAND $COMMANDARGS"
  exit
fi

I have used sudo to allow a password less execution of the script. If you want to enter a password for the user, remove the sudo. If you do not need the environment variables, remove -E from sudo.

The /usr/bin/bash -l ensures, that the profile.d scripts are executed for an initialized environment.

Trendfischer
  • 7,112
  • 5
  • 40
  • 51
  • `sudo` may be convenient but it's no good if its not out of the box (like on AIX). `su -c 'commands'` is the correct answer. – Tricky Mar 22 '18 at 05:11
  • @Tricky Perhaps read the full answer, it already suggests removing `sudo`. In fact, sudo is not that simple, in a lot of cases you need even `sudo -E` and a configuration entry in sudoers.d to allow execution without tty with `!requiretty`. But there are a lot of cases, where sudo is necessary for automatic called scripts, where a password dialog might interfere. Thus I would not remove it from a standard solution. – Trendfischer Mar 23 '18 at 09:36
  • The code in question is outright buggy -- it'll break script names or arguments with spaces; keep in mind that putting `"$@"` inside of a string means that arguments past the first one are appended into a separate string, **not** included in the `-c` argument. If you wanted to make it safe, you might `printf -v arg_q '%q ' "$0" "$@"` and then use `su "$USERNAME" -c "/usr/bin/bash -l $arg_q"` – Charles Duffy Jun 26 '18 at 16:47
  • @CharlesDuffy You are right! I simplified my production script too much for this answer :-( Just adding a `COMMANDARGS=$@` solves the problem with `-c`. Arguments with spaces were not a problem before, but I implemented your good input. I just had to do some experiments to make it work. I've edited the question and hopefully I did not paste another bug. Thanks for your comment, humiliating but necessary. – Trendfischer Jun 27 '18 at 17:28