84

I need to execute part of a bash script as a different user, and inside that user's $HOME directory. However, I'm not sure how to determine this variable. Switching to that user and calling $HOME does not provide the correct location:

# running script as root, but switching to a different user...
su - $different_user
echo $HOME
# returns /root/ but should be /home/myuser

Update:

It appears that the issue is with the way that I am trying to switch users in my script:

$different_user=deploy

# create user
useradd -m -s /bin/bash $different_user

echo "Current user: `whoami`"
# Current user: root

echo "Switching user to $different_user"
# Switching user to deploy

su - $different_user
echo "Current user: `whoami`"
# Current user: root
echo "Current user: `id`"
# Current user: uid=0(root) gid=0(root) groups=0(root)

sudo su $different_user
# Current user: root
# Current user: uid=0(root) gid=0(root) groups=0(root)

What is the correct way to switch users and execute commands as a different user in a bash script?

Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
Andrew
  • 227,796
  • 193
  • 515
  • 708
  • You can always extract it from /etc/passwd, assuming it's a locally defined account. – Marc B Dec 10 '13 at 20:38
  • 1
    `su - $different_user` should have been enough. Are you sure you're even logged in as `myuser`. Try running `id` command to verify. Some login shell is set to `nologin` and it exits as soon you login as `myuser`. – anubhava Dec 10 '13 at 21:14
  • @anubhava I updated my question to include how I created the user. Essentially: `useradd -m -s /bin/bash $different_user`. – Andrew Dec 10 '13 at 22:10
  • Please take another look at my answer. I substantially changed it to match exactly what you have attempted to do. – Michael Kropat Dec 11 '13 at 08:08
  • 1
    Please do not use `eval` or `bash -c` with a variable. I added an answer that works safely for an Linux/Unix/macOS system with bash (even if you are not using bash as your shell, it likely has bash available because bashisms are everywhere). https://stackoverflow.com/a/53219743/117471 It also explains the danger of other answers. – Bruno Bronosky Dec 31 '20 at 06:24

13 Answers13

125

Update: Based on this question's title, people seem to come here just looking for a way to find a different user's home directory, without the need to impersonate that user.

In that case, the simplest solution is to use tilde expansion with the username of interest, combined with eval (which is needed, because the username must be given as an unquoted literal in order for tilde expansion to work):

eval echo "~$different_user"    # prints $different_user's home dir.

Note: The usual caveats regarding the use of eval apply; in this case, the assumption is that you control the value of $different_user and know it to be a mere username.

By contrast, the remainder of this answer deals with impersonating a user and performing operations in that user's home directory.


Note:

  • Administrators by default and other users if authorized via the sudoers file can impersonate other users via sudo.
  • The following is based on the default configuration of sudo - changing its configuration can make it behave differently - see man sudoers.

The basic form of executing a command as another user is:

sudo -H -u someUser someExe [arg1 ...]
  # Example:
sudo -H -u root env  # print the root user's environment

Note:

  • If you neglect to specify -H, the impersonating process (the process invoked in the context of the specified user) will report the original user's home directory in $HOME.
  • The impersonating process will have the same working directory as the invoking process.
  • The impersonating process performs no shell expansions on string literals passed as arguments, since no shell is involved in the impersonating process (unless someExe happens to be a shell) - expansions by the invoking shell - prior to passing to the impersonating process - can obviously still occur.

Optionally, you can have an impersonating process run as or via a(n impersonating) shell, by prefixing someExe either with -i or -s - not specifying someExe ... creates an interactive shell:

  • -i creates a login shell for someUser, which implies the following:

    • someUser's user-specific shell profile, if defined, is loaded.
    • $HOME points to someUser's home directory, so there's no need for -H (though you may still specify it)
    • The working directory for the impersonating shell is the someUser's home directory.
  • -s creates a non-login shell:

    • no shell profile is loaded (though initialization files for interactive nonlogin shells are; e.g., ~/.bashrc)
    • Unless you also specify -H, the impersonating process will report the original user's home directory in $HOME.
    • The impersonating shell will have the same working directory as the invoking process.

Using a shell means that string arguments passed on the command line MAY be subject to shell expansions - see platform-specific differences below - by the impersonating shell (possibly after initial expansion by the invoking shell); compare the following two commands (which use single quotes to prevent premature expansion by the invoking shell):

  # Run root's shell profile, change to root's home dir.
sudo -u root -i eval 'echo $SHELL - $USER - $HOME - $PWD'
  # Don't run root's shell profile, use current working dir.
  # Note the required -H to define $HOME as root`s home dir.
sudo -u root -H -s eval 'echo $SHELL - $USER - $HOME - $PWD'

What shell is invoked is determined by "the SHELL environment variable if it is set or the shell as specified in passwd(5)" (according to man sudo). Note that with -s it is the invoking user's environment that matters, whereas with -i it is the impersonated user's.

Note that there are platform differences regarding shell-related behavior (with -i or -s):

  • sudo on Linux apparently only accepts an executable or builtin name as the first argument following -s/-i, whereas OSX allows passing an entire shell command line; e.g., OSX accepts sudo -u root -s 'echo $SHELL - $USER - $HOME - $PWD' directly (no need for eval), whereas Linux doesn't (as of sudo 1.8.95p).

  • Older versions of sudo on Linux do NOT apply shell expansions to arguments passed to a shell; for instance, with sudo 1.8.3p1 (e.g., Ubuntu 12.04), sudo -u root -H -s echo '$HOME' simply echoes the string literal "$HOME" instead of expanding the variable reference in the context of the root user. As of at least sudo 1.8.9p5 (e.g., Ubuntu 14.04) this has been fixed. Therefore, to ensure expansion on Linux even with older sudo versions, pass the the entire command as a single argument to eval; e.g.: sudo -u root -H -s eval 'echo $HOME'. (Although not necessary on OSX, this will work there, too.)

  • The root user's $SHELL variable contains /bin/sh on OSX 10.9, whereas it is /bin/bash on Ubuntu 12.04.

Whether the impersonating process involves a shell or not, its environment will have the following variables set, reflecting the invoking user and command: SUDO_COMMAND, SUDO_USER, SUDO_UID=, SUDO_GID.

See man sudo and man sudoers for many more subtleties.

Tip of the hat to @DavidW and @Andrew for inspiration.

HoldOffHunger
  • 18,769
  • 10
  • 104
  • 133
mklement0
  • 382,024
  • 64
  • 607
  • 775
29

In BASH, you can find a user's $HOME directory by prefixing the user's login ID with a tilde character. For example:

$ echo ~bob

This will echo out user bob's $HOME directory.

However, you say you want to be able to execute a script as a particular user. To do that, you need to setup sudo. This command allows you to execute particular commands as either a particular user. For example, to execute foo as user bob:

$ sudo -i -ubob -sfoo

This will start up a new shell, and the -i will simulate a login with the user's default environment and shell (which means the foo command will execute from the bob's$HOME` directory.)

Sudo is a bit complex to setup, and you need to be a superuser just to be able to see the shudders file (usually /etc/sudoers). However, this file usually has several examples you can use.

In this file, you can specify the commands you specify who can run a command, as which user, and whether or not that user has to enter their password before executing that command. This is normally the default (because it proves that this is the user and not someone who came by while the user was getting a Coke.) However, when you run a shell script, you usually want to disable this feature.

David W.
  • 105,218
  • 39
  • 216
  • 337
  • Promising, but `-i` and `-s` appear to be mutually exclusive. Just `-i` should be enough. – mklement0 Dec 10 '13 at 22:37
  • Having trouble assigning `~$different_user` to a variable and turning it into `$HOME`. Can you provide an example? – Andrew Dec 10 '13 at 22:59
  • @Andrew Don't use quotation marks! `user_home=~$different_user`. Note there's no quotation marks around the `~$different_user`. Your OS may also prevent you from reassigning `HOME` since it does have special meaning. Are you using `sudo`? That's the recommended way. – David W. Dec 11 '13 at 00:30
25

For the sake of an alternative answer for those searching for a lightweight way to just find a user's home dir...

Rather than messing with su hacks, or bothering with the overhead of launching another bash shell just to find the $HOME environment variable...

Lightweight Simple Homedir Query via Bash

There is a command specifically for this: getent

getent passwd someuser | cut -f6 -d:

getent can do a lot more... just see the man page. The passwd nsswitch database will return the user's entry in /etc/passwd format. Just split it on the colon : to parse out the fields.

It should be installed on most Linux systems (or any system that uses GNU Lib C (RHEL: glibc-common, Deb: libc-bin)

TrinitronX
  • 4,959
  • 3
  • 39
  • 66
  • 3
    `getent` is specific to Linux, as you state; a simpler and platform-neutral way is to use bash's [tilde expansion](https://www.gnu.org/software/bash/manual/html_node/Tilde-Expansion.html) with the username of interest (`eval` is needed, because the user name must be given as an _unquoted literal_ for the expansion to work): `eval echo "~$different_user"` - the usual caveats re use of `eval` apply. – mklement0 Feb 06 '15 at 04:01
  • Very good idea to let bash do it! Another alternative for current user home is: `eval echo "~$(who -m | awk '{ print $1 }')"` or simply `eval echo "~"`. On Mac OS X, there is the `dscl` utility, but it's output is much harder to parse, and requires you to already pass in the `/Users` homedir or uid (e.g.: `dscl . -read /Users/$(who -m | awk '{ print $1 }')` or `dscl . -search /Users UniqueID $(id -u)`). It's probably easier on some platforms to just use bash, or do some sort of basic OS detection anyway. – TrinitronX Dec 15 '15 at 03:32
  • For the current user, you do not need `eval` at all: unquoted `~` will return the current user's home directory (e.g. `echo ~`). If you really need the current user's username explicitly, `$USER` is simplest, efficient, and POSIX-compliant; `whoami` is also simple, but comparatively expensive and non-POSIX. Your `who -m`-based command, while POSIX-compliant, is even more expensive. To conclude: yes, using a POSIX-compatible shell (e.g., Bash) is simplest, most efficient and portable. – mklement0 Dec 15 '15 at 03:47
  • `getent` is specific to Linux, but why? It seems to an evidently useful thing to have (basically a command-line interface to query a database, albeit one implemented via text files) and should be easily portable, too. Well, it should additionally have a way to extract values from the record. This would avoid the roundabout use of `cut` – David Tonhofer Jul 15 '17 at 11:51
  • 2
    @DavidTonhofer: The reason for `getent` instead of just reading from `/etc/passwd` is because of the potential for different sources of user & login authentication data backend. There are a variety of backends for user database such as `sssd` (sss/LDAP), NIS, and NIS+. For more information on this, please see [this SE Answer about nsswitch.conf](https://unix.stackexchange.com/a/171827/7688). The need for the extra cut is not the greatest I'd agree, but it's what the tool outputs. The real reason for using `getent` is to try to query the configured user database backend on the system. – TrinitronX Aug 28 '17 at 22:37
4

The title of this question is How to get $HOME directory of different user in bash script? and that is what people are coming here from Google to find out.

There is a safe way to do this!

on Linux/BSD/macOS/OSX without sudo or root

user=pi
user_home=$(bash -c "cd ~$(printf %q $USER) && pwd")

NOTE: The reason this is safe is because bash (even versions prior to 4.4) has its own printf function that includes:

%q quote the argument in a way that can be reused as shell input

See: help printf

Compare the how other answers here respond to code injection

# "ls /" is not dangerous so you can try this on your machine
# But, it could just as easily be "sudo rm -rf /*"
$ user="root; ls /"
$ printf "%q" "$user"
root\;\ ls\ /

# This is what you get when you are PROTECTED from code injection
$ user_home=$(bash -c "cd ~$(printf "%q" "$user") && pwd"); echo $user_home
bash: line 0: cd: ~root; ls /: No such file or directory

# This is what you get when you ARE NOT PROTECTED from code injection
$ user_home=$(bash -c "cd ~$user && pwd"); echo $user_home
bin boot dev etc home lib lib64 media mnt ono opt proc root run sbin srv sys tmp usr var /root

$ user_home=$(eval "echo ~$user"); echo $user_home
/root bin boot dev etc home lib lib64 media mnt ono opt proc root run sbin srv sys tmp usr var

on Linux/BSD/macOS/OSX as root

If you are doing this because you are running something as root then you can use the power of sudo:

user=pi
user_home=$(sudo -u $user sh -c 'echo $HOME')

on Linux/BSD (but not modern macOS/OSX) without sudo or root

If not, the you can get it from /etc/passwd. There are already lots of examples of using eval and getent, so I'll give another option:

user=pi
user_home=$(awk -v u="$user" -v FS=':' '$1==u {print $6}' /etc/passwd)

I would really only use that one if I had a bash script with lots of other awk oneliners and no uses of cut. While many people like to "code golf" to use the fewest characters to accomplish a task, I favor "tool golf" because using fewer tools gives your script a smaller "compatibility footprint". Also, it's less man pages for your coworker or future-self to have to read to make sense of it.

Bruno Bronosky
  • 66,273
  • 12
  • 162
  • 149
3

You want the -u option for sudo in this case. From the man page:

The -u (user) option causes sudo to run the specified command as a user other than root.

If you don't need to actually run it as them, you could move to their home directory with ~<user>. As in, to move into my home directory you would use cd ~chooban.

chooban
  • 9,018
  • 2
  • 20
  • 36
  • Do you mean: `sudo -u $different_user su -`? – Andrew Dec 10 '13 at 21:59
  • 1
    @Andrew No! Don't `su` as that other user. Use the command you want to run as that user. For example, `sudo -u $different_user $command` – David W. Dec 11 '13 at 00:31
  • @DavidW I think I was trying to find a way to run multiple commands – Andrew Dec 11 '13 at 05:52
  • You can run multiple commands with `sudo` in a single script if that's what you need. Using sudo and having it setup is the best way to go about it. Your commands can run in as that user without prompting. Otherwise, you have to sign in as that user, and that starts a new shell which means you have to manually run your commands. You want to do that automatically. – David W. Dec 11 '13 at 12:49
2

So you want to:

  1. execute part of a bash script as a different user
  2. change to that user's $HOME directory

Inspired by this answer, here's the adapted version of your script:

#!/usr/bin/env bash

different_user=deploy

useradd -m -s /bin/bash "$different_user"

echo "Current user: $(whoami)"
echo "Current directory: $(pwd)"
echo

echo "Switching user to $different_user"
sudo -u "$different_user" -i /bin/bash - <<-'EOF'
    echo "Current user: $(id)"
    echo "Current directory: $(pwd)"
EOF
echo

echo "Switched back to $(whoami)"

different_user_home="$(eval echo ~"$different_user")"
echo "$different_user home directory: $different_user_home"

When you run it, you should get the following:

Current user: root
Current directory: /root

Switching user to deploy
Current user: uid=1003(deploy) gid=1003(deploy) groups=1003(deploy)
Current directory: /home/deploy

Switched back to root
deploy home directory: /home/deploy
Community
  • 1
  • 1
Michael Kropat
  • 14,557
  • 12
  • 70
  • 91
2

This works in Linux. Not sure how it behaves in other *nixes.

  getent passwd "${OTHER_USER}"|cut -d\: -f 6
hydrian
  • 95
  • 6
1

I was also looking for this, but didn't want to impersonate a user to simply acquire a path!

user_path=$(grep $username /etc/passwd|cut -f6 -d":");

Now in your script, you can refer to $user_path in most cases would be /home/username

Assumes: You have previously set $username with the value of the intended users username. Source: http://www.unix.com/shell-programming-and-scripting/171782-cut-fields-etc-passwd-file-into-variables.html

Grizly
  • 161
  • 8
  • 1
    The passwd file only contains local users, so this will not work for AD/LDAP integrated users. you should use the abstraction layer provided by 'getent' instead (see above example) – Saustrup Aug 01 '18 at 12:13
0

Quick and dirty, and store it in a variable:

USER=somebody
USER_HOME="$(echo -n $(bash -c "cd ~${USER} && pwd"))"
cobbzilla
  • 1,920
  • 1
  • 16
  • 17
0

I was struggling with this question because I was looking for a way to do this in a bash script for OS X, hence /etc/passwd was out of the question, and my script was meant to be executed as root, therefore making the solutions invoking eval or bash -c dangerous as they allowed code injection into the variable specifying the username.

Here is what I found. It's simple and doesn't put a variable inside a subshell. However it does require the script to be ran by root as it sudos into the specified user account.

Presuming that $SOMEUSER contains a valid username:

echo "$(sudo -H -u "$SOMEUSER" -s -- "cd ~ && pwd")"

I hope this helps somebody!

Quote
  • 1
0

If the user doesn't exist, getent will return an error.

Here's a small shell function that doesn't ignore the exit code of getent:

get_home() {
  local result; result="$(getent passwd "$1")" || return
  echo $result | cut -d : -f 6
}

Here's a usage example:

da_home="$(get_home missing_user)" || {
  echo 'User does NOT exist!'; exit 1
}

# Now do something with $da_home
echo "Home directory is: '$da_home'"
Elifarley
  • 1,310
  • 3
  • 16
  • 23
0

The output of getent passwd username can be parsed with a Bash regular expression

OTHER_HOME="$(
  [[ "$(
    getent \
    passwd \
    "${OTHER_USER}"
  )" =~ ([^:]*:){5}([^:]+) ]] \
  && echo "${BASH_REMATCH[2]}"
)"
Léa Gris
  • 17,497
  • 4
  • 32
  • 41
0

If you have sudo active, just do:

sudo su - admin -c "echo \$HOME"

NOTE: replace admin for the user you want to get the home directory

Carlos Saltos
  • 1,385
  • 15
  • 15