Indexation et modification du tableau de paramètres Bash $ @

11

Est-il possible de se référer aux index dans $@? Je ne trouve aucune référence à utiliser comme celle-ci dans le wiki de GrayCat , et le Guide de script avancé et d' autres attribuent cela à une variable différente avant de le modifier à la place.

$ echo ${@[0]}
-bash: ${@[0]}: bad substitution

Le but est DRY : le premier argument est utilisé pour une chose, et le reste pour autre chose, et je voudrais éviter de dupliquer le code pour normaliser, le $@tableau ou pour créer une fonction distincte pour cela (bien qu'à ce que c'est probablement la solution la plus simple).

Clarification: l'objectif était de modifier les valeurs de la longueur variable $@ pour faciliter le débogage du code . La version actuelle est un peu trop hacky à mon goût, même si elle fonctionne même pour des chemins bizarres comme

$'--$`\! *@ \a\b\e\E\f\r\t\v\\\"\' \n'

Mise à jour : il semble que ce ne soit pas possible. Le code utilise maintenant à la fois le code et la duplication des données, mais au moins cela fonctionne:

path_common()
{
    # Get the deepest common path.
    local common_path="$(echo -n "${1:-}x" | tr -s '/')"
    common_path="${common_path%x}"
    shift # $1 is obviously part of $1
    local path

    while [ -n "${1+defined}" ]
    do
        path="$(echo -n "${1}x" | tr -s '/')"
        path="${path%x}"
        if [[ "${path%/}/" = "${common_path%/}/"* ]]
        then
            shift
        else
            new_common_path="${common_path%/*}"
            [ "$new_common_path" = "$common_path" ] && return 1 # Dead end
            common_path="$new_common_path"
        fi
    done
    printf %s "$common_path"
}

Bounty s'adresse à tous ceux qui peuvent se débarrasser de la duplication de code pour réduire les barres obliques en double ou la duplication des données à conserver $1et les autres paramètres, ou les deux, tout en conservant une taille raisonnable et en réussissant tous les tests unitaires:

test "$(path_common /a/b/c/d /a/b/e/f; echo x)" = /a/bx
test "$(path_common /long/names/foo /long/names/bar; echo x)" = /long/namesx
test "$(path_common / /a/b/c; echo x)" = /x
test "$(path_common a/b/c/d a/b/e/f ; echo x)" = a/bx
test "$(path_common ./a/b/c/d ./a/b/e/f; echo x)" = ./a/bx
test "$(path_common $'\n/\n/\n' $'\n/\n'; echo x)" = $'\n/\n'x
test "$(path_common --/-- --; echo x)" = '--x'
test "$(path_common '' ''; echo x)" = x
test "$(path_common /foo/bar ''; echo x)" = x
test "$(path_common /foo /fo; echo x)" = x
test "$(path_common $'--$`\! *@ \a\b\e\E\f\r\t\v\\\"\' \n' $'--$`\! *@ \a\b\e\E\f\r\t\v\\\"\' \n'; echo x)" = $'--$`\! *@ \a\b\e\E\f\r\t\v\\\"\' \n'x
test "$(path_common /foo/bar //foo//bar//baz; echo x)" = /foo/barx
test "$(path_common foo foo; echo x)" = foox
test "$(path_common /fo /foo; echo x)" = x
l0b0
la source
en relation: stackoverflow.com/questions/4827690/…
Ciro Santilli 冠状 病毒 审查 六四 事件 法轮功

Réponses:

16

POSIX

Pour normaliser les barres obliques dans tous les paramètres, je vais utiliser l'astuce de l'argument rotatif: décaler $1, le transformer et mettre le résultat à la fin de la liste des paramètres. Si vous faites cela autant de fois qu'il y a de paramètres, vous avez transformé tous les paramètres et vous les avez remis en ordre.

Pour la deuxième partie du code, j'ai changé votre logique pour être moins déroutante: la boucle externe itère sur les paramètres, et la boucle interne itère sur les composants du chemin. for x; do … doneitère sur les paramètres de position, c'est un idiome pratique. J'utilise une manière compatible POSIX de faire correspondre une chaîne à un modèle: la caseconstruction.

Testé avec dash 0.5.5.1, pdksh 5.2.14, bash 3.2.39, bash 4.1.5, ksh 93s +, zsh 4.3.10.

Note latérale: il semble y avoir un bogue dans bash 4.1.5 (pas dans 3.2): si le modèle de cas est "${common_path%/}"/*, l'un des tests échoue.

posix_path_common () {
  for tmp; do
    tmp=$(printf %s. "$1" | tr -s "/")
    set -- "$@" "${tmp%.}"
    shift
  done
  common_path=$1; shift
  for tmp; do
    while case ${tmp%/}/ in "${common_path%/}/"*) false;; esac; do
      new_common_path=${common_path%/*}
      if [ "$new_common_path" = "$common_path" ]; then return 1; fi
      common_path=$new_common_path
    done
  done
  printf %s "$common_path"
}

bash, ksh

Si vous êtes en bash (ou ksh), vous pouvez utiliser des tableaux - je ne comprends pas pourquoi vous semblez vous limiter aux paramètres de position. Voici une version qui utilise un tableau. Je dois admettre que ce n'est pas particulièrement plus clair que la version POSIX, mais cela évite le brassage initial n ^ 2.

Pour la partie de normalisation de barre oblique, j'utilise la construction ksh93 ${foo//PATTERN/REPLACEMENT}pour remplacer toutes les occurrences de PATTERNin $foopar REPLACEMENT. Le modèle +(\/)doit correspondre à une ou plusieurs barres obliques; under bash, shopt -s extglobdoit être en vigueur (de manière équivalente, commencer bash avec bash -O extglob). La construction set ${!a[@]}définit les paramètres positionnels sur la liste des indices du tableau a. Cela fournit un moyen pratique d'itérer sur les éléments du tableau.

Pour la deuxième partie, j'ai la même logique de boucle que la version POSIX. Cette fois, je peux l'utiliser [[ … ]]car tous les obus ciblés ici le supportent.

Testé avec bash 3.2.39, bash 4.1.5, ksh 93s +.

array_path_common () {
  typeset a i tmp common_path new_common_path
  a=("$@")
  set ${!a[@]}
  for i; do
    a[$i]=${a[$i]//+(\/)//}
  done
  common_path=${a[$1]}; shift
  for tmp; do
    tmp=${a[$tmp]}
    while [[ "${tmp%/}/" != "${common_path%/}/"* ]]; do
      new_common_path="${common_path%/*}"
      if [[ $new_common_path = $common_path ]]; then return 1; fi
      common_path="$new_common_path"
    done
  done
  printf %s "$common_path"
}

zsh

Malheureusement, zsh n'a pas la ${!array[@]}fonctionnalité pour exécuter la version ksh93 telle quelle. Heureusement, zsh a deux fonctionnalités qui font de la première partie un jeu d'enfant. Vous pouvez indexer les paramètres positionnels comme s'il s'agissait du @tableau, il n'est donc pas nécessaire d'utiliser un tableau intermédiaire. Et zsh a une construction d'itération de tableau : "${(@)array//PATTERN/REPLACEMENT}"effectue le remplacement de modèle sur chaque élément de tableau à son tour et évalue le tableau de résultats (de manière confuse, vous avez besoin des guillemets doubles même si le résultat est composé de plusieurs mots; il s'agit d'une généralisation de "$@"). La deuxième partie est essentiellement inchangée.

zsh_path_common () {
  setopt local_options extended_glob
  local tmp common_path new_common_path
  set -- "${(@)@//\/##//}"
  common_path=$1; shift
  for tmp; do
    while [[ "${tmp%/}/" != "${common_path%/}/"* ]]; do
      new_common_path="${common_path%/*}"
      if [[ $new_common_path = $common_path ]]; then return 1; fi
      common_path="$new_common_path"
    done
  done
  printf %s "$common_path"
}

Cas de test

Mes solutions sont testées et commentées de manière minimale. J'ai changé la syntaxe de vos cas de test pour analyser les shells qui n'en ont pas $'…'et signaler les échecs de manière plus pratique.

do_test () {
  if test "$@"; then echo 0; else echo $? "$@"; failed=$(($failed+1)); fi
}

run_tests () {
  function_to_test=$1; shift
  failed=0
  do_test "$($function_to_test /a/b/c/d /a/b/e/f; echo x)" = /a/bx
  do_test "$($function_to_test /long/names/foo /long/names/bar; echo x)" = /long/namesx
  do_test "$($function_to_test / /a/b/c; echo x)" = /x
  do_test "$($function_to_test a/b/c/d a/b/e/f ; echo x)" = a/bx
  do_test "$($function_to_test ./a/b/c/d ./a/b/e/f; echo x)" = ./a/bx
  do_test "$($function_to_test '
/
/
' '
/
'; echo x)" = '
/
'x
  do_test "$($function_to_test --/-- --; echo x)" = '--x'
  do_test "$($function_to_test '' ''; echo x)" = x
  do_test "$($function_to_test /foo/bar ''; echo x)" = x
  do_test "$($function_to_test /foo /fo; echo x)" = x
  do_test "$($function_to_test '--$`\! *@ \a\b\e\E\f\r\t\v\\\"'\'' 
' '--$`\! *@ \a\b\e\E\f\r\t\v\\\"'\'' 
'; echo x)" = '--$`\! *@ \a\b\e\E\f\r\t\v\\\"'\'' 
'x
  do_test "$($function_to_test /foo/bar //foo//bar//baz; echo x)" = /foo/barx
  do_test "$($function_to_test foo foo; echo x)" = foox
  do_test "$($function_to_test /fo /foo; echo x)" = x
  if [ $failed -ne 0 ]; then echo $failed failures; return 1; fi
}
Gilles 'SO- arrête d'être méchant'
la source
1
+50, wow. Plus que ce que je demandais, en tout cas. Vous, monsieur, êtes génial.
l0b0
Dans la discussion POSIX, dans la première boucle où vous normalisez les barres obliques, pourquoi ajouter "." avec le sprintf puis le retirer dans la ligne suivante? Le code semble fonctionner sans lui, mais je soupçonne que vous gérez un cas de bord que je ne connais pas.
Alan De Smet
1
@AlanDeSmet Le cas de bord est si la chaîne se termine par une nouvelle ligne. La substitution de commandes supprime les nouvelles lignes de fin.
Gilles 'SO- arrête d'être méchant'
6

Pourquoi n'utilisez-vous pas simplement $ 1, $ 2 .. $ 9, $ {10}, $ {11} .. et ainsi de suite? C'est encore plus SEC que ce que vous essayez de faire :)

En savoir plus sur la relation entre $ number et $ @:

$ @ peut être considéré comme un raccourci pour "tous les éléments d'un tableau contenant tous les arguments"

Donc, $ @ est une sorte de raccourci de $ {args [@]} (args ici est un tableau "virtuel" contenant tous les arguments - pas une vraie variable, remarquez)

$ 1 est $ {args [1]}, $ 2 est $ {args [2]}, etc.

Lorsque vous avez frappé [9], utilisez une accolade: $ {10} est $ {args [10]}, $ {11} est $ {args [11]}, et ainsi de suite.


Utiliser indirectement un argument de ligne de commande

argnum=3  # You want to get the 3rd arg
do-something ${!argnum}  # Do something with the 3rd arg

Exemple:

argc=$#
for (( argn=1; argn<=argc; argn++)); do
    if [[ ${!argn} == "foo" ]]; then
        echo "Argument $argn of $argc is 'foo'"
    fi
done
pepoluan
la source
L'inconvénient évident d'avoir à utiliser $ * nombre * est que vous ne pouvez pas utiliser une variable d'index comme avec ${args[$i]}.
intuition
@intuited alors utilisez l'indirection; Je vais modifier ma réponse.
pepoluan
5

Le premier argument est utilisé pour une chose, et le reste pour autre chose,

Je pense que ce que tu veux c'est shift

$ set one two three four five
$ echo $@
one two three four five
$ echo $1
one
$ foo=$1
$ echo $foo
one
$ shift
$ echo $@
two three four five
$ shift 2
$ echo $@
four five
$ echo $1
four
forcefsck
la source
1

Je ne sais pas très bien pourquoi vous n'utilisez pas seulement 1 $ 2 $, etc. mais… Cela peut convenir à vos besoins.

$ script "ed    it" "cat/dog"  33.2  \D  

  echo "-------- Either use 'indirect reference'"
  for ((i=1;i<=${#@};i++)) ;do
    #  eval echo \"\$$i\" ..works, but as *pepoluan* 
    #    has pointed out: echo "${!i}" ..is better.
    echo "${!i}"
  done
  echo "-------- OR use an array"
  array=("$@")
  for ((i=0;i<${#array[@]};i++)) ;do
    echo "${array[$i]}" 
  done
  echo "-------- OR use 'set'"
  set  "$@"
  echo "$1"
  echo "$2"
  echo "$3"
  echo "$4"

production

  -------- Either use 'indirect reference'
  ed    it
  cat/dog
  33.2
  D
  -------- OR use an array
  ed    it
  cat/dog
  33.2
  D
  -------- OR use 'set'
  ed    it
  cat/dog
  33.2
  D

set fonctionne de tout ce qui le suit, pour créer $ 1, $ 2 .. etc .. Cela remplacera bien sûr les valeurs d'origine, alors soyez conscient de cela.

Peter.O
la source
ahh ... donc par 'eval' vous vouliez dire une référence indirecte ... $ {! var} construct est plus sûr, comme ce que j'ai écrit dans ma réponse
pepoluan
@pepoluan ... Merci de m'avoir alerté. C'est beaucoup plus simple à écrire ... (Je viens de revenir à la page Web à laquelle j'ai fait référence, si j'avais lu plus loin, je l'aurais vu le mentionner aussi :( ....
Peter.O
il h. mais si l'indirection se produit sur le côté gauche , l'eval est un mal nécessaire, quoi ':)
pepoluan
@peopluan ... d'accord, merci de l'avoir signalé ... et juste en passant: je ne comprends pas pourquoi evalcertains estiment être evil... (peut-être à cause de l'orthographe :) ... Si evalest "mauvais", alors $ {! Var} est-il également "mauvais"? ... Pour moi, c'est juste une partie du langage, et une partie utile, à cela .. mais je préfère définitivement $ {! Var} ...
Peter.O
1

Remarque Je prends en charge les espaces dans les noms de fichiers.

function SplitFilePath {
    IFS=$'/' eval "${1}"=\( \${2} \)
}
function JoinFilePath {
    IFS=$'/' eval echo -n \"\${*}\"
    [ $# -eq 1 -a "${1}" = "" ] && echo -n "/"
}
function path_common {
    set -- "${@//\/\///}"       ## Replace all '//' with '/'
    local -a Path1
    local -i Cnt=0
    SplitFilePath Path1 "${1}"
    IFS=$'/' eval set -- \${2} 
    for CName in "${Path1[@]}" ; do
        [ "${CName}" != "${1}" ] && break;
        shift && (( Cnt++ ))
    done
    JoinFilePath "${Path1[@]:0:${Cnt}}"
}

J'ai ajouté un cas de test pour les noms de fichiers avec des espaces et corrigé 2 tests auxquels il manquait un premier /

    do_test () {

  if test "${@}"; then echo 0; else echo $? "$@"; failed=$(($failed+1)); fi
}

run_tests () {
  function_to_test=$1; shift
  failed=0
  do_test "$($function_to_test /a/b/c/d /a/b/e/f; echo x)" = /a/bx
  do_test "$($function_to_test /long/names/foo /long/names/bar; echo x)" = /long/namesx
  do_test "$($function_to_test / /a/b/c; echo x)" = /x      
  do_test "$($function_to_test a/b/c/d a/b/e/f ; echo x)" = a/bx
  do_test "$($function_to_test ./a/b/c/d ./a/b/e/f; echo x)" = ./a/bx
  do_test "$($function_to_test '
/
/
' '
/
'; echo x)" = '
/
'x
  do_test "$($function_to_test --/-- --; echo x)" = '--x'
  do_test "$($function_to_test '' ''; echo x)" = x
  do_test "$($function_to_test /foo/bar ''; echo x)" = x
  do_test "$($function_to_test /foo /fo; echo x)" = /x      ## Changed from x
  do_test "$($function_to_test '--$`\! *@ \a\b\e\E\f\r\t\v\\\"'\'' 
' '--$`\! *@ \a\b\e\E\f\r\t\v\\\"'\'' 
'; echo x)" = '--$`\! *@ \a\b\e\E\f\r\t\v\\\"'\'' 
'x
  do_test "$($function_to_test /foo/bar //foo//bar//baz; echo x)" = /foo/barx
  do_test "$($function_to_test foo foo; echo x)" = foox
  do_test "$($function_to_test /fo /foo; echo x)" = /x          ## Changed from x
  do_test "$($function_to_test "/fo d/fo" "/fo d/foo"; echo x)" = "/fo dx"

  if [ $failed -ne 0 ]; then echo $failed failures; return 1; fi
}
John Kearney
la source