0

I would like to call a function when a command is entered and modify it
for example if a user enters this command

touch foo.txt

I want to manipulate it to run

sudo -u user touch foo.txt

The reason is that I as root have a lot of directories belonging to different users that don't have shell access (so I can't just sudo user) and a lot of times I want to run a command as that user I would like to write script to check for the current directory owner and run the command as that user.

Thank you.

Journeyman Geek
  • 133,878
phper
  • 105

1 Answers1

1

Preliminary note

The question is tagged and . This answer is for bash. I don't know fish at all.


On-demand solution

users that don't have shell access (so I can't just sudo user)

You can do sudo -u user touch …, so probably you can do sudo -u user bash. This may work even for a user whose login shell is /usr/sbin/nologin or so. The security policy for sudo (usually sudoers) is what matters, not the login shell.

Regardless of that, you may want a solution that finds the right user automatically; but mindlessly modifying each and every command is an invitation for an accident waiting to happen, or at least an inconvenience when you try to use a shell keyword, builtin or sudo. An on-demand call like s touch foo.txt seems better.

This is a shell function for sh, bash or any POSIX-compliant shell:

s() (
   user="$(stat -c %U ./)"
   if [ "$#" -eq 0 ]; then
      set bash
   fi
   exec sudo -u "$user" -- "$@"
)

Note: The function uses GNU stat, it's not portable. There is no flawless portable way to get the owner of a file though.

Usage:

  • s touch foo.txt
  • or just s to get an interactive Bash. Ultimately use exit or Ctrl+d (twice if needed) to return to your main shell.

If you're using bash completion, run complete -F _command s to make completions sanely work with s.


Towards a truly automatic solution

There are ways to use s (or whatever) for every command line automatically (e.g. Bash "virtual" prefix terminal). Note s cd … or s if … cannot work, so prepending each and every command line with s will most certainly break your workflow.

If you really want to do this automatically then consider an automation that transforms some command(s) to s bash -c 'some command(s)'. The below solution is based on my answer to How can I single-quote or escape the whole command line in Bash conveniently?

First define a function that will transform the command line upon Ctrl+x, Ctrl+o:

_prepend_s() { READLINE_LINE="s bash -c ${READLINE_LINE@Q}"; }
bind -x '"\C-x\C-o":_prepend_s'

In my Kubuntu Enter gets to Bash as Ctrl+m, but Ctrl+j is equivalent. Quite convenient, as now we can do:

bind '"\C-m":"\C-x\C-o\C-j"'

and from now on Enter will transform some command(s) to s bash -c 'some command(s)' and execute the result automatically. This will support cd, multiple commands (with ;, &&, ||), pipelines, if … and such, but not multi-line code (unless the code gets pasted when enable-bracketed-paste is on (see bind -v | grep enable-bracketed-paste)). You need to remember that:

  • the typed line gets passed to a separate shell and thus cannot affect your current shell (e.g. cd ~; pwd will work as expected, but the current working directory of your interactive shell will not be affected);
  • each invocation is standalone (e.g. foo=bar Enter and then echo "$foo" Enter will not show you bar);
  • nothing will be expanded by your current shell (unless you request this beforehand, i.e. while typing, with Ctrl+Alt+e or so);
  • actions while typing (e.g. completions) will work in your current shell and there is no guarantee it would work the same way in the other user's interactive shell.

You can use Ctrl+j or Shift+Enter to execute the command line directly in your current shell, as your current user.

There is a downside of this simple approach: the command is stored in history as s bash -c … and if you want to repeat it by Enter then it will fail. A quick fix may be:

_prepend_s() { history -s "$READLINE_LINE"; READLINE_LINE=" s bash -c ${READLINE_LINE@Q}"; }

which stores the original command in the history, and the leading space prevents the actual command from being stored (a functionality governed by HISTCONTROL); there are other approaches to this problem.


Solution I like

If I were you, I would prefer seeing the full command, including sudo -u (so it's clear what user it run as); and I would want to keep the original functionality of Enter (to avoid surprises). This is what I would use:

# IMPORTANT: do it in a fresh shell, so our previous bindings don't interfere
_act_as_dirowner() {
   history -s "$READLINE_LINE"
   local user
   user="$(stat -c %U ./)"
   [ "$user" != "$USER" ] && READLINE_LINE=" sudo -u $user bash -c ${READLINE_LINE@Q}";
}
bind -x '"\C-x\C-o":_act_as_dirowner'
bind '"\C-j":"\C-x\C-o\C-m"'

Now:

  • Enter or Ctrl+m executes the command line normally;
  • Shift+Enter or Ctrl+j injects sudo -u … bash -c if needed (i.e. if the owner of the current working directory is not you).

Notes:

  • Any value of $user should be safe if embedded in a command line verbatim (I mean we don't need ${user@Q}).

  • GNU stat -c %U ./ returns UNKNOWN if the directory is owned by UID that has no name; you may want to build some additional logic to catch this.

  • You can still use our function s independently on demand, e.g.:

    • … | … Enter will execute everything as you;
    • … | … Shift+Enter will execute everything as the owner of the current working directory;
    • … | s … Enter will execute the first part of the pipeline as you, the second part (after s) as the owner of the directory.
  • Multi-line commands may be troublesome. Shift+Enter may misbehave if used with a multi-line command with unbalanced quoting. And I admit I have managed to crash Bash 5.2.15 by using our Shift+Enter on a multi-line command with balanced quoting.

Adjust the solution to your needs.