Monday, May 21, 2007

Using bash functions under sudo

Have you ever done the following?
$ type duk
duk is a function
duk () 
{ 
    du -k "$@" | sort -n
}

$ sudo duk /tmp
sudo: duk: command not found
duk is a function that will show you directory sizes under a given pathname (or the current directory), nicely sorted by size, largest at the bottom just above your next prompt. It's very handy, put it in your .bashrc ;-). sudo doesn't know what to do with "duk" however, since it's not a system command. Therefore, I wrote a function that is a front-end to sudo. It parses the command line you give it, and expands any functions or aliases that you call. For bonus points, it shows you the full command line as the shell receives it before you type your password. Use it as follows:
$ source sudo.bash
$ sudo duk /tmp
/opt/local/bin/sudo -- bash -x -v -c duk () 
{ 
    du -k "$@" | sort -n
};"duk" '/tmp'
Password:
[...]
3648    /tmp/synergy-1.3.1/lib
6184    /tmp/synergy-1.3.1
20128   /tmp/prarora
1294048 /tmp/tmp
5304016 /tmp
Looks like I'll need to do some cleaning up in /tmp. Full source follows. To use it, place a source ~/.sudo.bash (for example) in your .bashrc after copying the code to your ~/.sudo.bash, or copy the full code to your .bashrc. Then, simply use sudo as you would before. This function handles all sudo arguments. There is one extra argument, -x, which expands arguments as you, not as root. This is needed in some corner cases.
# Wrap sudo to handle aliases and functions
# Wout.Mertens@gmail.com
#
# Comments and improvements welcome

sudo () 
{
        local c o t parse

        # Parse sudo args
        OPTIND=1
        while getopts xVhlLvkKsHPSb:p:c:a:u: t; do
                if [ "$t" = x ]; then
                        parse=true
                else
                        o="$o -$t"
                        [ "$OPTARG" ] && o="$o $OPTARG"
                fi
        done
        shift $(( $OPTIND - 1 ))

        # If no arguments are left, it's a simple call to sudo
        if [ $# -ge 1 ]; then
                c="$1";
                shift;
                case $(type -t "$c") in 
                "")
                        echo No such command "$c"
                        return 127
                        ;;
                alias)
                        c="$(type "$c"|sed "s/^.* to \`//;s/.$//")"
                        ;;
                function)
                        c=$(type "$c"|sed 1d)";\"$c\""
                        ;;
                *)
                        c="\"$c\""
                        ;;
                esac
                if [ -n "$parse" ]; then
                        # Quote the rest once, so it gets processed by bash.
                        # Done this way so variables can get expanded.
                        while [ -n "$1" ]; do
                                c="$c \"$1\""
                                shift
                        done
                else
                        # Otherwise, quote the arguments. The echo gets an extra
                        # space to prevent echo from parsing arguments like -n
                        # Note the lovely interactions between " and ' ;-)
                        while [ -n "$1" ]; do
                                c="$c '$(echo " $1"|sed -e "s/^ //" -e "s/'/'\"'\"'/")'"
                                shift
                        done
                fi
                # Run the command with verbose options
                echo Executing sudo $o -- bash -x -v -c "$c" >&2
                command sudo $o bash -xvc "$c"
        else
                echo sudo $o >&2
                command sudo $o
        fi
}

5 comments:

steve said...

I think you might want to add "--" to the du command, like so:

du -k -- "$@" | sort -n

This will stop the script failing on any filename starting with a dash. As an example why one would have a filename with dashes in it, I like to "touch /-i" on important machines as a last ditch effort to catch a haywire "rm -r".

Nice wrapper for sudo!

Wout Mertens said...

Wow, I totally missed this comment. Thanks for those 2 hints, good ones!

FiXato said...

Using this with commands that are completely unavailable for the current user, will not work.
For instance with ifconfig:

$ sudo ifconfig
No such command ifconfig

Simon said...

This is a very cool script. Thanks! :)

Wout Mertens said...

@FiXato: actually if you do that without this function, it would still not work:

sudo: ifconfig: command not found

You need to fully qualify the command or have it in your path, e.g. sudo /sbin/ifconfig.