command-line linux - ¿Cómo puedo analizar los argumentos de la línea de comandos en Bash?




if ejemplos (25)

Supongamos que creamos un script de shell llamado test_args.shcomo sigue

    #!/bin/bash
    # --- set the value, if there is inputs, override the defaults.

    HOME_FOLDER="${HOME}/owned_id_checker"
    SERVER_FILE_LIST="${HOME_FOLDER}/server_list.txt"

    while [[ $# > 1 ]]
    do
    key="$1"
    shift

    case $key in
        -i|--inputlist)
        SERVER_FILE_LIST="$1"
        shift
        ;;
    esac
    done


    echo "SERVER LIST   = ${SERVER_FILE_LIST}"

Después ejecutamos el siguiente comando:

#!/bin/sh

echo
echo "POSIX-compliant getopt(s)-free old-style-supporting option parser from [email protected][se.unix]"
echo

print_usage() {
  echo "Usage:

  $0 {a|b|c} [ARG...]

Options:

  --aaa-0-args
  -a
    Option without arguments.

  --bbb-1-args ARG
  -b ARG
    Option with one argument.

  --ccc-2-args ARG1 ARG2
  -c ARG1 ARG2
    Option with two arguments.

" >&2
}

if [ $# -le 0 ]; then
  print_usage
  exit 1
fi

opt=
while :; do

  if [ $# -le 0 ]; then

    # no parameters remaining -> end option parsing
    break

  elif [ ! "$opt" ]; then

    # we are at the beginning of a fresh block
    # remove optional leading hyphen and strip trailing whitespaces
    opt=$(echo "$1" | sed 's/^-\?\([a-zA-Z0-9\?-]*\)/\1/')

  fi

  # get the first character -> check whether long option
  first_chr=$(echo "$opt" | awk '{print substr($1, 1, 1)}')
  [ "$first_chr" = - ] && long_option=T || long_option=F

  # note to write the options here with a leading hyphen less
  # also do not forget to end short options with a star
  case $opt in

    -)

      # end of options
      shift
      break
      ;;

    a*|-aaa-0-args)

      echo "Option AAA activated!"
      ;;

    b*|-bbb-1-args)

      if [ "$2" ]; then
        echo "Option BBB with argument '$2' activated!"
        shift
      else
        echo "BBB parameters incomplete!" >&2
        print_usage
        exit 1
      fi
      ;;

    c*|-ccc-2-args)

      if [ "$2" ] && [ "$3" ]; then
        echo "Option CCC with arguments '$2' and '$3' activated!"
        shift 2
      else
        echo "CCC parameters incomplete!" >&2
        print_usage
        exit 1
      fi
      ;;

    h*|\?*|-help)

      print_usage
      exit 0
      ;;

    *)

      if [ "$long_option" = T ]; then
        opt=$(echo "$opt" | awk '{print substr($1, 2)}')
      else
        opt=$first_chr
      fi
      printf 'Error: Unknown option: "%s"\n' "$opt" >&2
      print_usage
      exit 1
      ;;

  esac

  if [ "$long_option" = T ]; then

    # if we had a long option then we are going to get a new block next
    shift
    opt=

  else

    # if we had a short option then just move to the next character
    opt=$(echo "$opt" | awk '{print substr($1, 2)}')

    # if block is now empty then shift to the next one
    [ "$opt" ] || shift

  fi

done

echo "Doing something..."

exit 0

La salida sería:

Option AAA activated!
Option BBB with argument 'X' activated!
Option CCC with arguments 'Y' and 'Z' activated!

Digamos, tengo un script que se llama con esta línea:

./myscript -vfd ./foo/bar/someFile -o /fizz/someOtherFile

o este:

./myscript -v -f -d -o /fizz/someOtherFile ./foo/bar/someFile 

¿Cuál es la forma aceptada de analizar esto de manera tal que en cada caso (o una combinación de los dos) $v , $f , y $d todos se configurarán como true y $outFile será igual a /fizz/someOtherFile ?


EasyOptions no requiere ningún análisis:

## Options:
##   --verbose, -v  Verbose mode
##   --output=FILE  Output filename

source easyoptions || exit

if test -n "${verbose}"; then
    echo "output file is ${output}"
    echo "${arguments[@]}"
fi

Me gustaría ofrecer mi versión de análisis de opciones, que permite lo siguiente:

-s p1
--stage p1
-w somefolder
--workfolder somefolder
-sw p1 somefolder
-e=hello

También permite esto (podría ser no deseado):

-s--workfolder p1 somefolder
-se=hello p1
-swe=hello p1 somefolder

Debe decidir antes de usar si = se usará en una opción o no. Esto es para mantener el código limpio (ish).

while [[ $# > 0 ]]
do
    key="$1"
    while [[ ${key+x} ]]
    do
        case $key in
            -s*|--stage)
                STAGE="$2"
                shift # option has parameter
                ;;
            -w*|--workfolder)
                workfolder="$2"
                shift # option has parameter
                ;;
            -e=*)
                EXAMPLE="${key#*=}"
                break # option has been fully handled
                ;;
            *)
                # unknown option
                echo Unknown option: $key #1>&2
                exit 10 # either this: my preferred way to handle unknown options
                break # or this: do this to signal the option has been handled (if exit isn't used)
                ;;
        esac
        # prepare for next option in this key, if any
        [[ "$key" = -? || "$key" == --* ]] && unset key || key="${key/#-?/-}"
    done
    shift # option(s) fully processed, proceed to next input argument
done

Creo que este es lo suficientemente simple para usar:

#!/bin/bash
#

readopt='getopts $opts opt;rc=$?;[ $rc$opt == 0? ]&&exit 1;[ $rc == 0 ]||{ shift $[OPTIND-1];false; }'

opts=vfdo:

# Enumerating options
while eval $readopt
do
    echo OPT:$opt ${OPTARG+OPTARG:$OPTARG}
done

# Enumerating arguments
for arg
do
    echo ARG:$arg
done

Ejemplo de invocación:

./myscript -v -do /fizz/someOtherFile -f ./foo/bar/someFile
OPT:v 
OPT:d 
OPT:o OPTARG:/fizz/someOtherFile
OPT:f 
ARG:./foo/bar/someFile

También puede ser útil saberlo, puede establecer un valor y, si alguien proporciona una entrada, anule el valor predeterminado con ese valor.

myscript.sh -f ./serverlist.txt o simplemente ./myscript.sh (y toma valores predeterminados)

./myscript -flag flagvariable -otherflag flagvar2
echo $flag $otherflag
flagvariable flagvar2

Solución que conserva los argumentos no manejados. Incluye demostraciones.

Aquí está mi solución. Es MUY flexible y, a diferencia de otros, no debería requerir paquetes externos y maneja los argumentos sobrantes de manera limpia.

El uso es: ./myscript -flag flagvariable -otherflag flagvar2

Todo lo que tienes que hacer es editar la línea validflags. Prepara un guión y busca todos los argumentos. Luego define el siguiente argumento como el nombre de la bandera, por ejemplo

#!/bin/sh
until [ $# -eq 0 ]
do
  name=${1:1}; shift;
  if [[ -z "$1" || $1 == -* ]] ; then eval "export $name=true"; else eval "export $name=$1"; shift; fi  
done
echo "year=$year month=$month day=$day flag=$flag"

El código principal (versión corta, detallado con ejemplos más abajo, también una versión con error):

sh test_args.sh  -year 2017 -flag  -month 12 -day 22 

La versión detallada con demostraciones de eco incorporadas:

year=2017 month=12 day=22 flag=true

Una final, esta falla si se pasa un argumento no válido.

#!/bin/bash
. import.sh log arguments

NAME="world"

parse_arguments "-n|--name)NAME;S" -- "[email protected]" || {
  error "Cannot parse command line."
  exit 1
}

info "Hello, $NAME!"

Pros: Lo que hace, lo maneja muy bien. Conserva los argumentos no utilizados que muchas de las otras soluciones aquí no. También permite que las variables sean llamadas sin ser definidas a mano en el script. También permite la prepopulación de variables si no se da ningún argumento correspondiente. (Ver ejemplo detallado).

Contras: no se puede analizar una sola cadena arg compleja, por ejemplo, -xcvf se procesaría como un solo argumento. Sin embargo, podrías escribir un código adicional en el mío que agregue esta funcionalidad.


Manera más sucinta

script.sh

#!/bin/bash

while [[ "$#" > 0 ]]; do case $1 in
  -d|--deploy) deploy="$2"; shift;;
  -u|--uglify) uglify=1;;
  *) echo "Unknown parameter passed: $1"; exit 1;;
esac; shift; done

echo "Should deploy? $deploy"
echo "Should uglify? $uglify"

Uso:

./script.sh -d dev -u

# OR:

./script.sh --deploy dev --uglify

Te doy la función parse_params que analizará los parámetros desde la línea de comandos.

  1. Es una solución Bash pura, sin utilidades adicionales.
  2. No contamina el alcance global.
  3. Sin esfuerzo, le devuelve variables fáciles de usar, sobre las que podría construir más lógica.
  4. La cantidad de guiones antes de los parámetros no importa ( --all es igual a - all=all es igual a all=all )

El siguiente script es una demostración de trabajo copiar y pegar. Vea la función show_use para entender cómo usar parse_params .

Limitaciones:

  1. No admite parámetros delimitados por espacios ( -d 1 )
  2. Los nombres de --any-param perderán guiones, por lo que --any-param y -anyparam son equivalentes
  3. eval $(parse_params "[email protected]") debe usarse dentro de la función bash (no funcionará en el ámbito global)
#!/bin/bash

# Universal Bash parameter parsing
# Parses equal sign separated params into local variables (--name=bob creates variable $name=="bob")
# Standalone named parameter value will equal its param name (--force creates variable $force=="force")
# Parses multi-valued named params into an array (--path=path1 --path=path2 creates ${path[*]} array)
# Parses un-named params into ${ARGV[*]} array
# Additionally puts all named params raw into ${ARGN[*]} array
# Additionally puts all standalone "option" params raw into ${ARGO[*]} array
# @author Oleksii Chekulaiev
# @version v1.4 (Jun-26-2018)
parse_params ()
{
    local existing_named
    local ARGV=() # un-named params
    local ARGN=() # named params
    local ARGO=() # options (--params)
    echo "local ARGV=(); local ARGN=(); local ARGO=();"
    while [[ "$1" != "" ]]; do
        # Escape asterisk to prevent bash asterisk expansion
        _escaped=${1/\*/\'\"*\"\'}
        # If equals delimited named parameter
        if [[ "$1" =~ ^..*=..* ]]; then
            # Add to named parameters array
            echo "ARGN+=('$_escaped');"
            # key is part before first =
            local _key=$(echo "$1" | cut -d = -f 1)
            # val is everything after key and = (protect from param==value error)
            local _val="${1/$_key=}"
            # remove dashes from key name
            _key=${_key//\-}
            # skip when key is empty
            if [[ "$_key" == "" ]]; then
                shift
                continue
            fi
            # search for existing parameter name
            if (echo "$existing_named" | grep "\b$_key\b" >/dev/null); then
                # if name already exists then it's a multi-value named parameter
                # re-declare it as an array if needed
                if ! (declare -p _key 2> /dev/null | grep -q 'declare \-a'); then
                    echo "$_key=(\"\$$_key\");"
                fi
                # append new value
                echo "$_key+=('$_val');"
            else
                # single-value named parameter
                echo "local $_key=\"$_val\";"
                existing_named=" $_key"
            fi
        # If standalone named parameter
        elif [[ "$1" =~ ^\-. ]]; then
            # remove dashes
            local _key=${1//\-}
            # skip when key is empty
            if [[ "$_key" == "" ]]; then
                shift
                continue
            fi
            # Add to options array
            echo "ARGO+=('$_escaped');"
            echo "local $_key=\"$_key\";"
        # non-named parameter
        else
            # Escape asterisk to prevent bash asterisk expansion
            _escaped=${1/\*/\'\"*\"\'}
            echo "ARGV+=('$_escaped');"
        fi
        shift
    done
}

#--------------------------- DEMO OF THE USAGE -------------------------------

show_use ()
{
    eval $(parse_params "[email protected]")
    # --
    echo "${ARGV[0]}" # print first unnamed param
    echo "${ARGV[1]}" # print second unnamed param
    echo "${ARGN[0]}" # print first named param
    echo "${ARG0[0]}" # print first option param (--force)
    echo "$anyparam"  # print --anyparam value
    echo "$k"         # print k=5 value
    echo "${multivalue[0]}" # print first value of multi-value
    echo "${multivalue[1]}" # print second value of multi-value
    [[ "$force" == "force" ]] && echo "\$force is set so let the force be with you"
}

show_use "param 1" --anyparam="my value" param2 k=5 --force --multi-value=test1 --multi-value=test2

No hay respuesta a las menciones mejoradas getopt . Y la respuesta más votada es engañosa: ignora las opciones cortas del estilo -⁠vfd (solicitadas por el OP), las opciones después de los argumentos posicionales (también solicitadas por el OP) e ignora los errores de análisis. En lugar:

  • Use getopt mejorado de util-linux o anteriormente GNU glibc . 1
  • Funciona con getopt_long() la función C de GNU glibc.
  • Tiene todas las características distintivas útiles (las otras no las tienen):
    • Maneja espacios, citando caracteres e incluso binarios en los argumentos 2.
    • puede manejar las opciones al final: script.sh -o outFile file1 file2 -v salida script.sh -o outFile file1 file2 -v
    • permite opciones de estilo largo: script.sh --outfile=fileOut --infile fileIn
  • Ya es tan viejo 3 que a ningún sistema GNU le falta esto (por ejemplo, cualquier Linux lo tiene).
  • Puede probar su existencia con: getopt --test → return value 4.
  • Otros getopt o shell- getopts son de uso limitado.

Las siguientes llamadas

myscript -vfd ./foo/bar/someFile -o /fizz/someOtherFile
myscript -v -f -d -o/fizz/someOtherFile -- ./foo/bar/someFile
myscript --verbose --force --debug ./foo/bar/someFile -o/fizz/someOtherFile
myscript --output=/fizz/someOtherFile ./foo/bar/someFile -vfd
myscript ./foo/bar/someFile -df -v --output /fizz/someOtherFile

todo regreso

verbose: y, force: y, debug: y, in: ./foo/bar/someFile, out: /fizz/someOtherFile

con el siguiente myscript

#!/bin/bash
# saner programming env: these switches turn some bugs into errors
set -o errexit -o pipefail -o noclobber -o nounset

! getopt --test > /dev/null 
if [[ ${PIPESTATUS[0]} -ne 4 ]]; then
    echo "I’m sorry, `getopt --test` failed in this environment."
    exit 1
fi

OPTIONS=dfo:v
LONGOPTS=debug,force,output:,verbose

# -use ! and PIPESTATUS to get exit code with errexit set
# -temporarily store output to be able to check for errors
# -activate quoting/enhanced mode (e.g. by writing out “--options”)
# -pass arguments only via   -- "[email protected]"   to separate them correctly
! PARSED=$(getopt --options=$OPTIONS --longoptions=$LONGOPTS --name "$0" -- "[email protected]")
if [[ ${PIPESTATUS[0]} -ne 0 ]]; then
    # e.g. return value is 1
    #  then getopt has complained about wrong arguments to stdout
    exit 2
fi
# read getopt’s output this way to handle the quoting right:
eval set -- "$PARSED"

d=n f=n v=n outFile=-
# now enjoy the options in order and nicely split until we see --
while true; do
    case "$1" in
        -d|--debug)
            d=y
            shift
            ;;
        -f|--force)
            f=y
            shift
            ;;
        -v|--verbose)
            v=y
            shift
            ;;
        -o|--output)
            outFile="$2"
            shift 2
            ;;
        --)
            shift
            break
            ;;
        *)
            echo "Programming error"
            exit 3
            ;;
    esac
done

# handle non-option arguments
if [[ $# -ne 1 ]]; then
    echo "$0: A single input file is required."
    exit 4
fi

echo "verbose: $v, force: $f, debug: $d, in: $1, out: $outFile"

1 getopt mejorado está disponible en la mayoría de los "sistemas bash", incluido Cygwin; en OS X intente con brew install gnu-getopt o sudo port install getopt
2 las convenciones de exec() POSIX no tienen una manera confiable de pasar NULL binarios en los argumentos de la línea de comando; esos bytes terminan prematuramente el argumento
3 primera versión lanzada en 1997 o antes (solo la rastreé hasta 1997)


getopts funciona bien si # 1 lo tiene instalado y # 2 tiene la intención de ejecutarlo en la misma plataforma. OSX y Linux (por ejemplo) se comportan de manera diferente a este respecto.

Aquí hay una solución (no getopts) que admite banderas iguales, no iguales y booleanas. Por ejemplo, podría ejecutar su script de esta manera:

./script --arg1=value1 --arg2 value2 --shouldClean

# parse the arguments.
COUNTER=0
ARGS=("[email protected]")
while [ $COUNTER -lt $# ]
do
    arg=${ARGS[$COUNTER]}
    let COUNTER=COUNTER+1
    nextArg=${ARGS[$COUNTER]}

    if [[ $skipNext -eq 1 ]]; then
        echo "Skipping"
        skipNext=0
        continue
    fi

    argKey=""
    argVal=""
    if [[ "$arg" =~ ^\- ]]; then
        # if the format is: -key=value
        if [[ "$arg" =~ \= ]]; then
            argVal=$(echo "$arg" | cut -d'=' -f2)
            argKey=$(echo "$arg" | cut -d'=' -f1)
            skipNext=0

        # if the format is: -key value
        elif [[ ! "$nextArg" =~ ^\- ]]; then
            argKey="$arg"
            argVal="$nextArg"
            skipNext=1

        # if the format is: -key (a boolean flag)
        elif [[ "$nextArg" =~ ^\- ]] || [[ -z "$nextArg" ]]; then
            argKey="$arg"
            argVal=""
            skipNext=0
        fi
    # if the format has not flag, just a value.
    else
        argKey=""
        argVal="$arg"
        skipNext=0
    fi

    case "$argKey" in 
        --source-scmurl)
            SOURCE_URL="$argVal"
        ;;
        --dest-scmurl)
            DEST_URL="$argVal"
        ;;
        --version-num)
            VERSION_NUM="$argVal"
        ;;
        -c|--clean)
            CLEAN_BEFORE_START="1"
        ;;
        -h|--help|-help|--h)
            showUsage
            exit
        ;;
    esac
done

He encontrado la cuestión de escribir un análisis portátil en scripts de manera tan frustrante que he escrito Argbash , un generador de código FOSS que puede generar el código de análisis de argumentos para su script, además de que tiene algunas características Argbash :

https://argbash.io


Ampliando la excelente respuesta de @guneysus, aquí hay un ajuste que le permite al usuario usar la sintaxis que prefiera, por ejemplo

command -x=myfilename.ext --another_switch 

vs

command -x myfilename.ext --another_switch

Es decir, los iguales pueden ser reemplazados por espacios en blanco.

Es posible que esta "interpretación difusa" no sea de su agrado, pero si está creando secuencias de comandos que son intercambiables con otras utilidades (como es el caso de la mía, que debe funcionar con ffmpeg), la flexibilidad es útil.

STD_IN=0

prefix=""
key=""
value=""
for keyValue in "[email protected]"
do
  case "${prefix}${keyValue}" in
    -i=*|--input_filename=*)  key="-i";     value="${keyValue#*=}";; 
    -ss=*|--seek_from=*)      key="-ss";    value="${keyValue#*=}";;
    -t=*|--play_seconds=*)    key="-t";     value="${keyValue#*=}";;
    -|--stdin)                key="-";      value=1;;
    *)                                      value=$keyValue;;
  esac
  case $key in
    -i) MOVIE=$(resolveMovie "${value}");  prefix=""; key="";;
    -ss) SEEK_FROM="${value}";          prefix=""; key="";;
    -t)  PLAY_SECONDS="${value}";           prefix=""; key="";;
    -)   STD_IN=${value};                   prefix=""; key="";; 
    *)   prefix="${keyValue}=";;
  esac
done

Este ejemplo muestra cómo usar getopty evaly HEREDOCy shiftpara manejar parámetros cortos y largos con y sin el valor requerido que sigue. También la declaración de cambio / caso es concisa y fácil de seguir.

#!/usr/bin/env bash

# usage function
function usage()
{
   cat << HEREDOC

   Usage: $progname [--num NUM] [--time TIME_STR] [--verbose] [--dry-run]

   optional arguments:
     -h, --help           show this help message and exit
     -n, --num NUM        pass in a number
     -t, --time TIME_STR  pass in a time string
     -v, --verbose        increase the verbosity of the bash script
     --dry-run            do a dry run, don't change any files

HEREDOC
}  

# initialize variables
progname=$(basename $0)
verbose=0
dryrun=0
num_str=
time_str=

# use getopt and store the output into $OPTS
# note the use of -o for the short options, --long for the long name options
# and a : for any option that takes a parameter
OPTS=$(getopt -o "hn:t:v" --long "help,num:,time:,verbose,dry-run" -n "$progname" -- "[email protected]")
if [ $? != 0 ] ; then echo "Error in command line arguments." >&2 ; usage; exit 1 ; fi
eval set -- "$OPTS"

while true; do
  # uncomment the next line to see how shift is working
  # echo "\$1:\"$1\" \$2:\"$2\""
  case "$1" in
    -h | --help ) usage; exit; ;;
    -n | --num ) num_str="$2"; shift 2 ;;
    -t | --time ) time_str="$2"; shift 2 ;;
    --dry-run ) dryrun=1; shift ;;
    -v | --verbose ) verbose=$((verbose + 1)); shift ;;
    -- ) shift; break ;;
    * ) break ;;
  esac
done

if (( $verbose > 0 )); then

   # print out all the parameters we read in
   cat <<-EOM
   num=$num_str
   time=$time_str
   verbose=$verbose
   dryrun=$dryrun
EOM
fi

# The rest of your script below

Las líneas más significativas del script anterior son las siguientes:

OPTS=$(getopt -o "hn:t:v" --long "help,num:,time:,verbose,dry-run" -n "$progname" -- "[email protected]")
if [ $? != 0 ] ; then echo "Error in command line arguments." >&2 ; exit 1 ; fi
eval set -- "$OPTS"

while true; do
  case "$1" in
    -h | --help ) usage; exit; ;;
    -n | --num ) num_str="$2"; shift 2 ;;
    -t | --time ) time_str="$2"; shift 2 ;;
    --dry-run ) dryrun=1; shift ;;
    -v | --verbose ) verbose=$((verbose + 1)); shift ;;
    -- ) shift; break ;;
    * ) break ;;
  esac
done

Corto, hasta el punto, legible, y maneja casi todo (IMHO).

Espero que ayude a alguien.


Mezclando argumentos posicionales y basados ​​en banderas.

--param = arg (es igual a delimitado)

Mezclando libremente banderas entre argumentos posicionales:

./script.sh dumbo 127.0.0.1 --environment=production -q -d
./script.sh dumbo --environment=production 127.0.0.1 --quiet -d

Se puede lograr con un enfoque bastante conciso:

# process flags
pointer=1
while [[ $pointer -le $# ]]; do
   param=${!pointer}
   if [[ $param != "-"* ]]; then ((pointer++)) # not a parameter flag so advance pointer
   else
      case $param in
         # paramter-flags with arguments
         -e=*|--environment=*) environment="${param#*=}";;
                  --another=*) another="${param#*=}";;

         # binary flags
         -q|--quiet) quiet=true;;
                 -d) debug=true;;
      esac

      # splice out pointer frame from positional list
      [[ $pointer -gt 1 ]] \
         && set -- ${@:1:((pointer - 1))} ${@:((pointer + 1)):$#} \
         || set -- ${@:((pointer + 1)):$#};
   fi
done

# positional remain
node_name=$1
ip_address=$2

--param arg (espacio delimitado)

Por lo general, es más claro no mezclar --flag=valuey --flag valueestilos.

./script.sh dumbo 127.0.0.1 --environment production -q -d

Esto es un poco peligroso para leer, pero sigue siendo válido

./script.sh dumbo --environment production 127.0.0.1 --quiet -d

Fuente

# process flags
pointer=1
while [[ $pointer -le $# ]]; do
   if [[ ${!pointer} != "-"* ]]; then ((pointer++)) # not a parameter flag so advance pointer
   else
      param=${!pointer}
      ((pointer_plus = pointer + 1))
      slice_len=1

      case $param in
         # paramter-flags with arguments
         -e|--environment) environment=${!pointer_plus}; ((slice_len++));;
                --another) another=${!pointer_plus}; ((slice_len++));;

         # binary flags
         -q|--quiet) quiet=true;;
                 -d) debug=true;;
      esac

      # splice out pointer frame from positional list
      [[ $pointer -gt 1 ]] \
         && set -- ${@:1:((pointer - 1))} ${@:((pointer + $slice_len)):$#} \
         || set -- ${@:((pointer + $slice_len)):$#};
   fi
done

# positional remain
node_name=$1
ip_address=$2

La respuesta principal a esta pregunta me pareció un poco defectuosa cuando la probé. Aquí está mi solución que he encontrado más robusta:

boolean_arg=""
arg_with_value=""

while [[ $# -gt 0 ]]
do
key="$1"
case $key in
    -b|--boolean-arg)
    boolean_arg=true
    shift
    ;;
    -a|--arg-with-value)
    arg_with_value="$2"
    shift
    shift
    ;;
    -*)
    echo "Unknown option: $1"
    exit 1
    ;;
    *)
    arg_num=$(( $arg_num + 1 ))
    case $arg_num in
        1)
        first_normal_arg="$1"
        shift
        ;;
        2)
        second_normal_arg="$1"
        shift
        ;;
        *)
        bad_args=TRUE
    esac
    ;;
esac
done

# Handy to have this here when adding arguments to
# see if they're working. Just edit the '0' to be '1'.
if [[ 0 == 1 ]]; then
    echo "first_normal_arg: $first_normal_arg"
    echo "second_normal_arg: $second_normal_arg"
    echo "boolean_arg: $boolean_arg"
    echo "arg_with_value: $arg_with_value"
    exit 0
fi

if [[ $bad_args == TRUE || $arg_num < 2 ]]; then
    echo "Usage: $(basename "$0") <first-normal-arg> <second-normal-arg> [--boolean-arg] [--arg-with-value VALUE]"
    exit 1
fi

getopt() / getopts() es una buena opción. Robado desde here :

El uso simple de "getopt" se muestra en este mini-script:

#!/bin/bash
echo "Before getopt"
for i
do
  echo $i
done
args=`getopt abc:d $*`
set -- $args
echo "After getopt"
for i
do
  echo "-->$i"
done

Lo que hemos dicho es que se permitirá cualquiera de -a, -b, -c o -d, pero que -c es seguido por un argumento (el "c:" dice eso).

Si llamamos a esto "g" y lo probamos:

bash-2.05a$ ./g -abc foo
Before getopt
-abc
foo
After getopt
-->-a
-->-b
-->-c
-->foo
-->--

Comenzamos con dos argumentos, y "getopt" rompe las opciones y pone cada uno en su propio argumento. También añadió "-".


Aquí está mi solución mejorada de la respuesta de Bruno Bronosky usando matrices variables.

le permite mezclar la posición de los parámetros y darle una matriz de parámetros que conserva el orden sin las opciones

#!/bin/bash

echo [email protected]

PARAMS=()
SOFT=0
SKIP=()
for i in "[email protected]"
do
case $i in
    -n=*|--skip=*)
    SKIP+=("${i#*=}")
    ;;
    -s|--soft)
    SOFT=1
    ;;
    *)
        # unknown option
        PARAMS+=("$i")
    ;;
esac
done
echo "SKIP            = ${SKIP[@]}"
echo "SOFT            = $SOFT"
    echo "Parameters:"
    echo ${PARAMS[@]}

Saldrá por ejemplo:

$ ./test.sh parameter -s somefile --skip=.c --skip=.obj
parameter -s somefile --skip=.c --skip=.obj
SKIP            = .c .obj
SOFT            = 1
Parameters:
parameter somefile

A riesgo de agregar otro ejemplo para ignorar, aquí está mi esquema.

  • maneja -n arg y --name=arg
  • permite argumentos al final
  • muestra errores sane si algo está mal escrito
  • Compatible, no usa bashismos.
  • legible, no requiere mantener el estado en un bucle

Espero que sea útil para alguien.

while [ "$#" -gt 0 ]; do
  case "$1" in
    -n) name="$2"; shift 2;;
    -p) pidfile="$2"; shift 2;;
    -l) logfile="$2"; shift 2;;

    --name=*) name="${1#*=}"; shift 1;;
    --pidfile=*) pidfile="${1#*=}"; shift 1;;
    --logfile=*) logfile="${1#*=}"; shift 1;;
    --name|--pidfile|--logfile) echo "$1 requires an argument" >&2; exit 1;;

    -*) echo "unknown option: $1" >&2; exit 1;;
    *) handle_argument "$1"; shift 1;;
  esac
done

Método # 1: Usar bash sin getopt [s]

Dos formas comunes de pasar argumentos de par clave-valor son:

Separación de espacio de Bash (por ejemplo, --option argument ) (sin getopt [s])

Uso ./myscript.sh -e conf -s /etc -l /usr/lib /etc/hosts

#!/bin/bash

POSITIONAL=()
while [[ $# -gt 0 ]]
do
key="$1"

case $key in
    -e|--extension)
    EXTENSION="$2"
    shift # past argument
    shift # past value
    ;;
    -s|--searchpath)
    SEARCHPATH="$2"
    shift # past argument
    shift # past value
    ;;
    -l|--lib)
    LIBPATH="$2"
    shift # past argument
    shift # past value
    ;;
    --default)
    DEFAULT=YES
    shift # past argument
    ;;
    *)    # unknown option
    POSITIONAL+=("$1") # save it in an array for later
    shift # past argument
    ;;
esac
done
set -- "${POSITIONAL[@]}" # restore positional parameters

echo FILE EXTENSION  = "${EXTENSION}"
echo SEARCH PATH     = "${SEARCHPATH}"
echo LIBRARY PATH    = "${LIBPATH}"
echo DEFAULT         = "${DEFAULT}"
echo "Number files in SEARCH PATH with EXTENSION:" $(ls -1 "${SEARCHPATH}"/*."${EXTENSION}" | wc -l)
if [[ -n $1 ]]; then
    echo "Last line of file specified as non-opt/last argument:"
    tail -1 "$1"
fi

Bash Equals-Separated (por ejemplo, --option=argument ) (sin getopt [s])

Uso ./myscript.sh -e=conf -s=/etc -l=/usr/lib /etc/hosts

#!/bin/bash

for i in "[email protected]"
do
case $i in
    -e=*|--extension=*)
    EXTENSION="${i#*=}"
    shift # past argument=value
    ;;
    -s=*|--searchpath=*)
    SEARCHPATH="${i#*=}"
    shift # past argument=value
    ;;
    -l=*|--lib=*)
    LIBPATH="${i#*=}"
    shift # past argument=value
    ;;
    --default)
    DEFAULT=YES
    shift # past argument with no value
    ;;
    *)
          # unknown option
    ;;
esac
done
echo "FILE EXTENSION  = ${EXTENSION}"
echo "SEARCH PATH     = ${SEARCHPATH}"
echo "LIBRARY PATH    = ${LIBPATH}"
echo "Number files in SEARCH PATH with EXTENSION:" $(ls -1 "${SEARCHPATH}"/*."${EXTENSION}" | wc -l)
if [[ -n $1 ]]; then
    echo "Last line of file specified as non-opt/last argument:"
    tail -1 $1
fi

Para comprender mejor ${i#*=} busque "Eliminación de subcadenas" en esta guía . Es funcionalmente equivalente a `sed 's/[^=]*=//' <<< "$i"` que llama a un subproceso innecesario o `echo "$i" | sed 's/[^=]*=//'` `echo "$i" | sed 's/[^=]*=//'` que llama a dos subprocesos innecesarios.

Método # 2: usando bash con getopt [s]

de: http://mywiki.wooledge.org/BashFAQ/035#getopts

Limitaciones de getopt (1) (versiones anteriores, relativamente recientes de getopt ):

  • no puede manejar argumentos que son cadenas vacías
  • no puede manejar argumentos con espacios en blanco incrustados

Las versiones más recientes de getopt no tienen estas limitaciones.

Además, el shell POSIX (y otros) ofrecen getopts que no tienen estas limitaciones. Aquí hay un ejemplo simplista de getopts :

#!/bin/sh

# A POSIX variable
OPTIND=1         # Reset in case getopts has been used previously in the shell.

# Initialize our own variables:
output_file=""
verbose=0

while getopts "h?vf:" opt; do
    case "$opt" in
    h|\?)
        show_help
        exit 0
        ;;
    v)  verbose=1
        ;;
    f)  output_file=$OPTARG
        ;;
    esac
done

shift $((OPTIND-1))

[ "${1:-}" = "--" ] && shift

echo "verbose=$verbose, output_file='$output_file', Leftovers: [email protected]"

# End of file

Las ventajas de getopts son:

  1. Es más portátil, y funcionará en otros shells como dash .
  2. Puede manejar múltiples opciones individuales como -vf filename en la forma típica de Unix, automáticamente.

La desventaja de getopts es que solo puede manejar opciones cortas ( -h , no --help ) sin código adicional.

Hay un tutorial de getopts que explica lo que significan todas las sintaxis y variables. En bash, también hay help getopts , que puede ser informativo.


Utilice el módulo "argumentos" de bash-modules

Ejemplo:

Option CCC with arguments 'Z' and 'Y' activated!
Option BBB with argument 'X' activated!
Option AAA activated!
Doing something...

Aquí está mi enfoque - usando expresiones regulares.

  • no getopts
  • maneja bloque de parametros cortos -qwerty
  • maneja parámetros cortos -q -w -e
  • maneja opciones largas --qwerty
  • puede pasar el atributo a la opción corta o larga (si está usando el bloque de opciones cortas, el atributo se adjunta a la última opción)
  • puede usar espacios o =para proporcionar atributos, pero los atributos coinciden hasta encontrar un "delimitador" de guión + espacio, por lo que en --q=qwe ty qwe tyun atributo
  • maneja la mezcla de todo lo anterior así que -oa -op attr ibute --option=att ribu te --op-tion attribute --option att-ributees válido

guión:

#!/usr/bin/env sh

help_menu() {
  echo "Usage:

  ${0##*/} [-h][-l FILENAME][-d]

Options:

  -h, --help
    display this help and exit

  -l, --logfile=FILENAME
    filename

  -d, --debug
    enable debug
  "
}

parse_options() {
  case $opt in
    h|help)
      help_menu
      exit
     ;;
    l|logfile)
      logfile=${attr}
      ;;
    d|debug)
      debug=true
      ;;
    *)
      echo "Unknown option: ${opt}\nRun ${0##*/} -h for help.">&2
      exit 1
  esac
}
[email protected]

until [ "$options" = "" ]; do
  if [[ $options =~ (^ *(--([a-zA-Z0-9-]+)|-([a-zA-Z0-9-]+))(( |=)(([\_\.\?\/\\a-zA-Z0-9]?[ -]?[\_\.\?a-zA-Z0-9]+)+))?(.*)|(.+)) ]]; then
    if [[ ${BASH_REMATCH[3]} ]]; then # for --option[=][attribute] or --option[=][attribute]
      opt=${BASH_REMATCH[3]}
      attr=${BASH_REMATCH[7]}
      options=${BASH_REMATCH[9]}
    elif [[ ${BASH_REMATCH[4]} ]]; then # for block options -qwert[=][attribute] or single short option -a[=][attribute]
      pile=${BASH_REMATCH[4]}
      while (( ${#pile} > 1 )); do
        opt=${pile:0:1}
        attr=""
        pile=${pile/${pile:0:1}/}
        parse_options
      done
      opt=$pile
      attr=${BASH_REMATCH[7]}
      options=${BASH_REMATCH[9]}
    else # leftovers that don't match
      opt=${BASH_REMATCH[10]}
      options=""
    fi
    parse_options
  fi
done

Otra solución sin getopt [s], POSIX, antiguo estilo Unix

Similar a la solución que Bruno Bronosky publicó aquí es una sin el uso de getopt(s).

La principal característica diferenciadora de mi solución es que permite tener opciones concatenadas juntas igual que tar -xzf foo.tar.gzes igual a tar -x -z -f foo.tar.gz. Y al igual que en tar, psetc. el guión principal es opcional para un bloque de opciones cortas (pero esto se puede cambiar fácilmente). Las opciones largas también son compatibles (pero cuando un bloque comienza con uno, se requieren dos guiones).

Código con opciones de ejemplo

#!/usr/bin/env bash
#shebang.io
validflags="rate time number"
count=1
for arg in [email protected]
do
    match=0
    argval=$1
    for flag in $validflags
    do
        sflag="-"$flag
        if [ "$argval" == "$sflag" ]
        then
            declare $flag=$2
            match=1
        fi
    done
        if [ "$match" == "1" ]
    then
        shift 2
    else
        leftovers=$(echo $leftovers $argval)
        shift
    fi
    count=$(($count+1))
done
#Cleanup then restore the leftovers
shift $#
set -- $leftovers

Para el uso del ejemplo, por favor vea los ejemplos más abajo.

Posicionamiento de opciones con argumentos.

Por lo que vale la pena, las opciones con argumentos no son las últimas (solo las opciones largas deben ser). Entonces, mientras que en tar(al menos en algunas implementaciones) las fopciones deben ser las últimas porque el nombre del archivo sigue ( tar xzf bar.tar.gzfunciona pero tar xfz bar.tar.gzno lo hace), este no es el caso aquí (vea los ejemplos posteriores).

Múltiples opciones con argumentos.

Como otra ventaja adicional, los parámetros de opción se consumen en el orden de las opciones por los parámetros con las opciones requeridas. Solo mire la salida de mi script aquí con la línea de comando abc XYZ(o -abc XYZ):

#!/usr/bin/env bash
#shebang.io
rate=30
time=30
number=30
echo "all args
[email protected]"
validflags="rate time number"
count=1
for arg in [email protected]
do
    match=0
    argval=$1
#   argval=$(echo [email protected] | cut -d ' ' -f$count)
    for flag in $validflags
    do
            sflag="-"$flag
        if [ "$argval" == "$sflag" ]
        then
            declare $flag=$2
            match=1
        fi
    done
        if [ "$match" == "1" ]
    then
        shift 2
    else
        leftovers=$(echo $leftovers $argval)
        shift
    fi
    count=$(($count+1))
done

#Cleanup then restore the leftovers
echo "pre final clear args:
[email protected]"
shift $#
echo "post final clear args:
[email protected]"
set -- $leftovers
echo "all post set args:
[email protected]"
echo arg1: $1 arg2: $2

echo leftovers: $leftovers
echo rate $rate time $time number $number

Opciones largas concatenadas también.

Además, también puede tener opciones largas en el bloque de opciones, dado que ocurren por última vez en el bloque. Por lo tanto, las siguientes líneas de comando son todas equivalentes (incluido el orden en que se procesan las opciones y sus argumentos):

  • -cba ZYX
  • cba ZYX
  • -cb-aaa-0-args ZYX
  • -c-bbb-1-args ZYX -a
  • --ccc-2-args ZY -ba X
  • c ZY b X a
  • -c ZY -b X -a
  • --ccc-2-args ZY --bbb-1-args X --aaa-0-args

Todos estos conducen a:

#!/usr/bin/env bash
#shebang.io
rate=30
time=30
number=30
validflags="rate time number"
count=1
for arg in [email protected]
do
    argval=$1
    match=0
        if [ "${argval:0:1}" == "-" ]
    then
        for flag in $validflags
        do
                sflag="-"$flag
            if [ "$argval" == "$sflag" ]
            then
                declare $flag=$2
                match=1
            fi
        done
        if [ "$match" == "0" ]
        then
            echo "Bad argument: $argval"
            exit 1
        fi
        shift 2
    else
        leftovers=$(echo $leftovers $argval)
        shift
    fi
    count=$(($count+1))
done
#Cleanup then restore the leftovers
shift $#
set -- $leftovers
echo rate $rate time $time number $number
echo leftovers: $leftovers

No en esta solución

Argumentos opcionales

Las opciones con argumentos opcionales deberían ser posibles con un poco de trabajo, por ejemplo, mirando hacia adelante si hay un bloque sin un guión; el usuario tendría que poner un guión delante de cada bloque después de un bloque con un parámetro que tiene un parámetro opcional. Tal vez esto sea demasiado complicado para comunicarse con el usuario, por lo que es mejor que solo requiera un guión principal en este caso.

Las cosas se complican aún más con múltiples parámetros posibles. No recomendaría que las opciones traten de ser inteligentes al determinar si un argumento podría ser adecuado o no (por ejemplo, con una opción solo toma un número como un argumento opcional) porque esto podría interrumpirse en el futuro.

Personalmente prefiero opciones adicionales en lugar de argumentos opcionales.

Opción argumentos introducidos con un signo igual

Al igual que con los argumentos opcionales, no soy un fanático de esto (por cierto, ¿hay un hilo para discutir los pros / contras de los diferentes estilos de parámetros?) Pero si lo desea, probablemente podría implementarlo usted mismo como lo hizo en http://mywiki.wooledge.org/BashFAQ/035#Manual_loop con una --long-with-arg=?*declaración de caso y luego eliminando el signo igual (esto es, por cierto, el sitio que dice que hacer la concatenación de parámetros es posible con cierto esfuerzo, pero "lo dejó como ejercicio para el lector "lo que me hizo tomarles la palabra pero empecé desde cero".

Otras notas

Compatible con POSIX, funciona incluso en configuraciones de Busybox antiguas con las que tuve que lidiar (por ejemplo cut, heady getoptsfaltantes).


Mi respuesta se basa en gran medida en la respuesta de Bruno Bronosky , pero en cierto modo combiné sus dos implementaciones puras de bash en una que utilizo con bastante frecuencia.

# As long as there is at least one more argument, keep looping
while [[ $# -gt 0 ]]; do
    key="$1"
    case "$key" in
        # This is a flag type option. Will catch either -f or --foo
        -f|--foo)
        FOO=1
        ;;
        # Also a flag type option. Will catch either -b or --bar
        -b|--bar)
        BAR=1
        ;;
        # This is an arg value type option. Will catch -o value or --output-file value
        -o|--output-file)
        shift # past the key and to the value
        OUTPUTFILE="$1"
        ;;
        # This is an arg=value type option. Will catch -o=value or --output-file=value
        -o=*|--output-file=*)
        # No need to shift here since the value is part of the same string
        OUTPUTFILE="${key#*=}"
        ;;
        *)
        # Do whatever you want with extra options
        echo "Unknown option '$key'"
        ;;
    esac
    # Shift after checking all the cases to get the next option
    shift
done

Esto le permite tener opciones / valores separados por espacios, así como valores definidos iguales.

Así que puedes ejecutar tu script usando:

./myscript --foo -b -o /fizz/file.txt

tanto como:

./myscript -f --bar -o=/fizz/file.txt

y ambos deben tener el mismo resultado final.

PROS:

  • Permite tanto -arg = valor como -arg valor

  • Funciona con cualquier nombre arg que puedes usar en bash

    • Significado -a o -arg o --arg o -arg o lo que sea
  • Puro bash. No hay necesidad de aprender / usar getopt o getopts

CONTRAS:

  • No puedo combinar args

    • Lo que significa no -abc. Debes hacer -a -b -c

Estos son los únicos pros / contras que se me ocurren en la cabeza



Algunos trucos de bash que utilizo para establecer variables a partir de comandos

2ª Edición 2018-02-12: Agregando una forma especial, ¡vea al final de esto!

2018-01-25 Edición: agregar una función de ejemplo (para poblar vars sobre el uso del disco)

Primera forma antigua y compatible simple.

myPi=`echo '4*a(1)' | bc -l`
echo $myPi 
3.14159265358979323844

Mayormente compatible, segunda vía.

Como la anidación podría volverse pesada, se implementó un paréntesis para esto.

myPi=$(bc -l <<<'4*a(1)')

Muestra anidada:

SysStarted=$(date -d "$(ps ho lstart 1)" +%s)
echo $SysStarted 
1480656334

Leer más de una variable (con bashismos ).

df -k /
Filesystem     1K-blocks   Used Available Use% Mounted on
/dev/dm-0         999320 529020    401488  57% /

Si solo quiero valor usado :

array=($(df -k /))

usted podría ver la variable de matriz :

declare -p array
declare -a array='([0]="Filesystem" [1]="1K-blocks" [2]="Used" [3]="Available" [
4]="Use%" [5]="Mounted" [6]="on" [7]="/dev/dm-0" [8]="999320" [9]="529020" [10]=
"401488" [11]="57%" [12]="/")'

Entonces:

echo ${array[9]}
529020

Pero prefiero esto:

{ read foo ; read filesystem size used avail prct mountpoint ; } < <(df -k /)
echo $used
529020

La primera read foo solo saltará la línea del encabezado (la variable $foo contendrá algo así como el Filesystem 1K-blocks Used Available Use% Mounted on )

Función de ejemplo para poblar algunas variables:

#!/bin/bash

declare free=0 total=0 used=0

getDiskStat() {
    local foo
    {
        read foo
        read foo total used free foo
    } < <(
        df -k ${1:-/}
    )
}

getDiskStat $1
echo $total $used $free

Nota: no se requiere declare línea, solo para facilitar la lectura.

Sobre sudo cmd | grep ... | cut ... sudo cmd | grep ... | cut ...

shell=$(cat /etc/passwd | grep $USER | cut -d : -f 7)
echo $shell
/bin/bash

(¡Por favor, evite el cat inútil! Así que esto es solo 1 horquilla menos:

shell=$(grep $USER </etc/passwd | cut -d : -f 7)

Todos los tubos ( | ) implican horquillas. Donde se debe ejecutar otro proceso, acceder al disco, llamar a bibliotecas, etc.

Por lo tanto, utilizar sed para la muestra, limitará el subproceso a una sola bifurcación :

shell=$(sed </etc/passwd "s/^$USER:.*://p;d")
echo $shell

Y con los bashismos :

Pero para muchas acciones, principalmente en archivos pequeños, bash podría hacer el trabajo por sí mismo:

while IFS=: read -a line ; do
    [ "$line" = "$USER" ] && shell=${line[6]}
  done </etc/passwd
echo $shell
/bin/bash

o

while IFS=: read loginname encpass uid gid fullname home shell;do
    [ "$loginname" = "$USER" ] && break
  done </etc/passwd
echo $shell $loginname ...

Yendo más allá sobre la división de variables ...

Eche un vistazo a mi respuesta a ¿Cómo divido una cadena en un delimitador en Bash?

Alternativa: reducir las horquillas utilizando tareas de larga duración en segundo plano

2nd Edit 2018-02-12: para evitar múltiples bifurcaciones como

myPi=$(bc -l <<<'4*a(1)'
myRay=12
myCirc=$(bc -l <<<" 2 * $myPi * $myRay ")

o

myStarted=$(date -d "$(ps ho lstart 1)" +%s)
mySessStart=$(date -d "$(ps ho lstart $$)" +%s)

Y porque date y bc podrían trabajar línea por línea :

bc -l <<<$'3*4\n5*6'
12
30

date -f - +%s < <(ps ho lstart 1 $$)
1516030449
1517853288

Podríamos utilizar un proceso en segundo plano de larga ejecución para realizar trabajos de forma repetitiva, sin tener que iniciar una nueva bifurcación para cada solicitud:

mkfifo /tmp/myFifoForBc
exec 5> >(bc -l >/tmp/myFifoForBc)
exec 6</tmp/myFifoForBc
rm /tmp/myFifoForBc

(¡Por supuesto, FD 5 y 6 deben estar sin uso!) ... A partir de ahí, puede usar este proceso de la siguiente manera:

echo "3*4" >&5
read -u 6 foo
echo $foo
12

echo >&5 "pi=4*a(1)"
echo >&5 "2*pi*12"
read -u 6 foo
echo $foo
75.39822368615503772256

En una función newConnector

Puede encontrar mi función newConnector en GitHub.Com o en mi propio sitio (Nota en github, hay dos archivos, en mi sitio, la función y la demo están agrupadas en 1 archivo que puede obtenerse para su uso o simplemente ejecutar la demo)

Muestra:

. shell_connector.sh

tty
/dev/pts/20

ps --tty pts/20 fw
    PID TTY      STAT   TIME COMMAND
  29019 pts/20   Ss     0:00 bash
  30745 pts/20   R+     0:00  \_ ps --tty pts/20 fw

newConnector /usr/bin/bc "-l" '3*4' 12

ps --tty pts/20 fw
    PID TTY      STAT   TIME COMMAND
  29019 pts/20   Ss     0:00 bash
  30944 pts/20   S      0:00  \_ /usr/bin/bc -l
  30952 pts/20   R+     0:00  \_ ps --tty pts/20 fw

declare -p PI
bash: declare: PI: not found

myBc '4*a(1)' PI
declare -p PI
declare -- PI="3.14159265358979323844"

La función myBc permite usar la tarea en segundo plano con una sintaxis simple, y para la fecha:

newConnector /bin/date '-f - +%s' @0 0
myDate '2000-01-01'
  946681200
myDate "$(ps ho lstart 1)" boottime
myDate now now ; read utm idl </proc/uptime
myBc "$now-$boottime" uptime
printf "%s\n" ${utm%%.*} $uptime
  42134906
  42134906

ps --tty pts/20 fw
    PID TTY      STAT   TIME COMMAND
  29019 pts/20   Ss     0:00 bash
  30944 pts/20   S      0:00  \_ /usr/bin/bc -l
  32615 pts/20   S      0:00  \_ /bin/date -f - +%s
   3162 pts/20   R+     0:00  \_ ps --tty pts/20 fw

A partir de ahí, si desea finalizar uno de los procesos en segundo plano, solo tiene que cerrar su fd :

eval "exec $DATEOUT>&-"
eval "exec $DATEIN>&-"
ps --tty pts/20 fw
    PID TTY      STAT   TIME COMMAND
   4936 pts/20   Ss     0:00 bash
   5256 pts/20   S      0:00  \_ /usr/bin/bc -l
   6358 pts/20   R+     0:00  \_ ps --tty pts/20 fw

que no es necesario, porque todos los fd se cierran cuando finaliza el proceso principal.





bash command-line scripting arguments