This is a series of functions taken from the WordPress "framework" that will do it, but this could quite well be too much:
add_query_arg()
/**
 * Retrieve a modified URL query string.
 *
 * You can rebuild the URL and append a new query variable to the URL query by
 * using this function. You can also retrieve the full URL with query data.
 *
 * Adding a single key & value or an associative array. Setting a key value to
 * emptystring removes the key. Omitting oldquery_or_uri uses the $_SERVER
 * value.
 *
 * @since 1.0
 *
 * @param mixed $param1 Either newkey or an associative_array
 * @param mixed $param2 Either newvalue or oldquery or uri
 * @param mixed $param3 Optional. Old query or uri
 * @return string New URL query string.
 */
public function add_query_arg() {
    $ret = '';
    if ( is_array( func_get_arg(0) ) ) {
        $uri = ( @func_num_args() < 2 || false === @func_get_arg( 1 ) ) ? $_SERVER['REQUEST_URI'] : @func_get_arg( 1 );
    } else {
        $uri = ( @func_num_args() < 3 || false === @func_get_arg( 2 ) ) ? $_SERVER['REQUEST_URI'] : @func_get_arg( 2 );
    }
    if ( $frag = strstr( $uri, '#' ) ) {
        $uri = substr( $uri, 0, -strlen( $frag ) );
    } else {
        $frag = '';
    }
    if ( preg_match( '|^https?://|i', $uri, $matches ) ) {
        $protocol = $matches[0];
        $uri = substr( $uri, strlen( $protocol ) );
    } else {
        $protocol = '';
    }
    if ( strpos( $uri, '?' ) !== false ) {
        $parts = explode( '?', $uri, 2 );
        if ( 1 == count( $parts ) ) {
            $base = '?';
            $query = $parts[0];
        } else {
            $base = $parts[0] . '?';
            $query = $parts[1];
        }
    } elseif ( !empty( $protocol ) || strpos( $uri, '=' ) === false ) {
        $base = $uri . '?';
        $query = '';
    } else {
        $base = '';
        $query = $uri;
    }
    parse_str( $query, $qs );
    if ( get_magic_quotes_gpc() )
        $qs = format::stripslashes_deep( $qs );
    $qs = format::urlencode_deep( $qs ); // this re-URL-encodes things that were already in the query string
    if ( is_array( func_get_arg( 0 ) ) ) {
        $kayvees = func_get_arg( 0 );
        $qs = array_merge( $qs, $kayvees );
    } else {
        $qs[func_get_arg( 0 )] = func_get_arg( 1 );
    }
    foreach ( ( array ) $qs as $k => $v ) {
        if ( $v === false )
            unset( $qs[$k] );
    }
    $ret = http_build_query( $qs, '', '&' );
    $ret = trim( $ret, '?' );
    $ret = preg_replace( '#=(&|$)#', '$1', $ret );
    $ret = $protocol . $base . $ret . $frag;
    $ret = rtrim( $ret, '?' );
    return $ret;
}
stripslashes_deep()
/**
 * Navigates through an array and removes slashes from the values.
 *
 * If an array is passed, the array_map() function causes a callback to pass the
 * value back to the function. The slashes from this value will removed.
 *
 * @since 1.0
 *
 * @param array|string $value The array or string to be stripped
 * @return array|string Stripped array (or string in the callback).
 */
function stripslashes_deep( $value ) {
    return is_array( $value ) ? array_map( array('self', 'stripslashes_deep'), $value ) : stripslashes( $value );
}
urlencode_deep()
/**
 * Navigates through an array and encodes the values to be used in a URL.
 *
 * Uses a callback to pass the value of the array back to the function as a
 * string.
 *
 * @since 1.0
 *
 * @param array|string $value The array or string to be encoded.
 * @return array|string $value The encoded array (or string from the callback).
 */
public function urlencode_deep( $value ) {
    return is_array($value) ? array_map( array('self', 'urlencode_deep'), $value) : urlencode($value);
}