Come possiamo eseguire un comando memorizzato in una variabile?


35
$ ls -l /tmp/test/my\ dir/
total 0

Mi chiedevo perché i seguenti modi per eseguire il comando sopra falliscono o hanno successo?

$ abc='ls -l "/tmp/test/my dir"'

$ $abc
ls: cannot access '"/tmp/test/my': No such file or directory
ls: cannot access 'dir"': No such file or directory

$ "$abc"
bash: ls -l "/tmp/test/my dir": No such file or directory

$ bash -c $abc
'my dir'

$ bash -c "$abc"
total 0

$ eval $abc
total 0

$ eval "$abc"
total 0


Risposte:


54

Questo è stato discusso in una serie di domande su unix.SE, cercherò di raccogliere tutti i problemi che posso venire qui. Riferimenti alla fine.


Perché fallisce

Il motivo per cui si affrontano questi problemi è la divisione delle parole e il fatto che le virgolette espanse dalle variabili non fungono da virgolette, ma sono solo caratteri ordinari.

I casi presentati nella domanda:

$ abc='ls -l "/tmp/test/my dir"'

Qui, $abcè diviso e lsottiene i due argomenti "/tmp/test/mye dir"(con le virgolette nella parte anteriore del primo e nella parte posteriore del secondo):

$ $abc
ls: cannot access '"/tmp/test/my': No such file or directory
ls: cannot access 'dir"': No such file or directory

Qui, l'espansione è quotata, quindi è mantenuta come una sola parola. La shell cerca di trovare un programma chiamato ls -l "/tmp/test/my dir", spazi e virgolette inclusi.

$ "$abc"
bash: ls -l "/tmp/test/my dir": No such file or directory

E qui, solo la prima parola o $abcviene presa come argomento -c, quindi Bash viene eseguito lsnella directory corrente. Le altre parole sono argomenti di bash, e vengono utilizzati per riempire $0, $1e così via

$ bash -c $abc
'my dir'

Con bash -c "$abc", e eval "$abc", c'è un'ulteriore fase di elaborazione della shell, che fa funzionare le virgolette, ma provoca anche l'elaborazione di tutte le espansioni della shell , quindi c'è il rischio di eseguire accidentalmente un'espansione dei comandi dai dati forniti dall'utente, a meno che tu non sia molto attento a citare.


Modi migliori per farlo

I due modi migliori per memorizzare un comando sono a) utilizzare invece una funzione, b) utilizzare una variabile di matrice (o i parametri posizionali).

Utilizzando una funzione:

Dichiarare semplicemente una funzione con il comando all'interno ed eseguire la funzione come se fosse un comando. Le espansioni nei comandi all'interno della funzione vengono elaborate solo quando il comando viene eseguito, non quando è definito, e non è necessario citare i singoli comandi.

# define it
myls() {
    ls -l "/tmp/test/my dir"
}

# run it
myls

Utilizzando un array:

Le matrici consentono di creare variabili multi-parola in cui le singole parole contengono spazi bianchi. Qui, le singole parole sono memorizzate come elementi array distinti e l' "${array[@]}"espansione espande ogni elemento come parole shell separate:

# define the array
mycmd=(ls -l "/tmp/test/my dir")

# run the command
"${mycmd[@]}"

La sintassi è leggermente orribile, ma le matrici consentono anche di costruire la riga di comando pezzo per pezzo. Per esempio:

mycmd=(ls)               # initial command
if [ "$want_detail" = 1 ]; then
    mycmd+=(-l)          # optional flag
fi
mycmd+=("$targetdir")    # the filename

"${mycmd[@]}"

oppure mantieni costanti parti della riga di comando e usa l'array fill solo una parte di essa, opzioni o nomi di file:

options=(-x -v)
files=(file1 "file name with whitespace")
target=/somedir

transmutate "${options[@]}" "${files[@]}" "$target"

Il rovescio della medaglia degli array è che non sono una funzionalità standard, quindi le semplici shell POSIX (come dashquelle predefinite /bin/shin Debian / Ubuntu) non le supportano (ma vedi sotto). Bash, ksh e zsh lo fanno, tuttavia, quindi è probabile che il tuo sistema abbia delle shell che supportano gli array.

utilizzando "$@"

Nelle shell senza supporto per array denominati, è ancora possibile utilizzare i parametri posizionali (lo pseudo-array "$@") per contenere gli argomenti di un comando.

I seguenti dovrebbero essere bit di script portatili che fanno l'equivalente dei bit di codice nella sezione precedente. L'array viene sostituito con "$@"l'elenco dei parametri posizionali. L'impostazione "$@"viene eseguita sete le doppie virgolette intorno "$@"sono importanti (causano la citazione individuale degli elementi dell'elenco).

Innanzitutto, è sufficiente archiviare un comando con argomenti "$@"ed eseguirlo:

set -- ls -l "/tmp/test/my dir"
"$@"

Impostazione condizionale di parti delle opzioni della riga di comando per un comando:

set -- ls
if [ "$want_detail" = 1 ]; then
    set -- "$@" -l
fi
set -- "$@" "$targetdir"

"$@"

Usando solo "$@"per opzioni e operandi:

set -- -x -v
set -- "$@" file1 "file name with whitespace"
set -- "$@" /somedir

transmutate "$@"

(Naturalmente, di "$@"solito è pieno degli argomenti dello script stesso, quindi dovrai salvarli da qualche parte prima di ri-proposire "$@".)


Stai attento eval!

Poiché evalintroduce un ulteriore livello di elaborazione di preventivi ed espansioni, è necessario prestare attenzione all'input dell'utente. Ad esempio, questo funziona finché l'utente non digita alcuna virgoletta singola:

read -r filename
cmd="ls -l '$filename'"
eval "$cmd";

Ma se danno l'input '$(uname)'.txt, il tuo script esegue felicemente la sostituzione del comando.

Una versione con array è immune da ciò dato che le parole sono tenute separate per tutto il tempo, non ci sono citazioni o altre elaborazioni per il contenuto di filename.

read -r filename
cmd=(ls -ld -- "$filename")
"${cmd[@]}"

Riferimenti


2
puoi aggirare la cosa del preventivo facendo cmd="ls -l $(printf "%q" "$filename")". non è carino, ma se l'utente è morto impostato su un eval, aiuta. E 'anche molto utile per inviare il comando se le cose simili, come ssh foohost "ls -l $(printf "%q" "$filename")", o in spirito di questa domanda: ssh foohost "$cmd".
Patrick,

Non direttamente correlato, ma hai hardcoded la directory? In tal caso, potresti voler guardare l'alias. Qualcosa del tipo: $ alias abc='ls -l "/tmp/test/my dir"'
Hopping Bunny

4

Il modo più sicuro per eseguire un comando (non banale) è eval. Quindi puoi scrivere il comando come faresti sulla riga di comando e viene eseguito esattamente come se lo avessi appena inserito. Ma devi citare tutto.

Caso semplice:

abc='ls -l "/tmp/test/my dir"'
eval "$abc"

caso non così semplice:

# command: awk '! a[$0]++ { print "foo: " $0; }' inputfile
abc='awk '\''! a[$0]++ { print "foo: " $0; }'\'' inputfile'
eval "$abc"

3

Il secondo segno di virgolette interrompe il comando.

Quando corro:

abc="ls -l '/home/wattana/Desktop'"
$abc

Mi ha dato un errore.

Ma quando corro

abc="ls -l /home/wattana/Desktop"
$abc

Non c'è nessun errore

Non c'è modo di risolverlo al momento (per me) ma puoi evitare l'errore non avendo spazio nel nome della directory.

Questa risposta dice che il comando eval può essere usato per risolvere questo problema ma non funziona per me :(


1
Sì, funziona finché non è necessario, ad esempio, nomi di file con spazi incorporati (o quelli contenenti caratteri glob).
ilkkachu,

0

Se questo non funziona '', allora dovresti usare ``:

abc=`ls -l /tmp/test/my\ dir/`

Aggiorna il modo migliore:

abc=$(ls -l /tmp/test/my\ dir/)

Questo memorizza il risultato del comando in una variabile. L'OP (stranamente) vuole salvare il comando stesso in una variabile. Oh, e dovresti davvero iniziare a usare al $( command... )posto dei bastoncini.
roaima,

Grazie mille per i chiarimenti e i suggerimenti!
balon

0

Che ne dici di un one-liner python3?

bash-4.4# pwd
/home
bash-4.4# CUSTOM="ls -altr"
bash-4.4# python3 -c "import subprocess; subprocess.call(\"$CUSTOM\", shell=True)"
total 8
drwxr-xr-x    2 root     root          4096 Mar  4  2019 .
drwxr-xr-x    1 root     root          4096 Nov 27 16:42 ..
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.