$ ls -l /tmp/test/my\ dir/ total 0
Ik vroeg me af waarom de volgende manieren om de bovenstaande opdracht uit te voeren mislukken of slagen?
$ 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
Reacties
- mywiki.wooledge.org/BashFAQ / 050
- Beveiligingsimplicaties van het vergeten om een variabele te citeren in bash / POSIX-shells – Maar wat als …?
Antwoord
Dit is besproken in een aantal vragen over unix.SE, ik zal proberen om alle problemen die ik hier kan bedenken. Verwijzingen aan het einde.
Waarom het mislukt
De reden dat u met deze problemen wordt geconfronteerd, is woordsplitsing en het feit dat aanhalingstekens die zijn uitgebreid vanuit variabelen niet “fungeren als aanhalingstekens, maar gewoon gewone tekens zijn.
De gevallen gepresenteerd in de vraag:
De toewijzing hier wijst de enkele string toe ls -l "/tmp/test/my dir"
naar abc
:
$ abc="ls -l "/tmp/test/my dir""
Hieronder $abc
is opgesplitst in witruimte, en ls
krijgt de drie argumenten -l
, "/tmp/test/my
en dir"
(met een aanhalingsteken vooraan in de tweede en een ander aan de achterkant van de derde). De optie werkt, maar het pad wordt verkeerd verwerkt:
$ $abc ls: cannot access ""/tmp/test/my": No such file or directory ls: cannot access "dir"": No such file or directory
Hier wordt de uitbreiding geciteerd, dus het wordt als een enkel woord gehouden. De shell probeert om een programma te vinden dat letterlijk ls -l "/tmp/test/my dir"
heet, inclusief spaties en aanhalingstekens.
$ "$abc" bash: ls -l "/tmp/test/my dir": No such file or directory
En hier, $abc
is gesplitst en alleen het eerste resulterende woord wordt als argument gebruikt voor -c
, dus Bash voert gewoon ls
in de huidige directory. De andere woorden zijn argumenten voor bash, en worden gebruikt om $0
, $1
, enz. te vullen. p>
$ bash -c $abc "my dir"
Met bash -c "$abc"
, en eval "$abc"
is er een extra shell-verwerkingsstap, waardoor de aanhalingstekens werken, maar zorgt er ook voor dat alle shell-uitbreidingen opnieuw worden verwerkt , dus er is “een risico dat u per ongeluk bijvoorbeeld een opdrachtvervanging uitvoert vanuit door de gebruiker verstrekte gegevens, tenzij u” opnieuw erg zorgzaam ful over citeren.
Betere manieren om het te doen
De twee betere manieren om een commando op te slaan zijn a) gebruik in plaats daarvan een functie, b) gebruik een arrayvariabele ( of de positionele parameters).
Een functie gebruiken:
Verklaar eenvoudig een functie met het commando erin, en voer de functie uit alsof het een commando is. Uitbreidingen in commandos binnen de functie worden alleen verwerkt als het commando wordt uitgevoerd, niet als het “is gedefinieerd, en je hoeft de individuele commandos niet te citeren.
# define it myls() { ls -l "/tmp/test/my dir" } # run it myls
Een array gebruiken:
Met arrays kunnen variabelen met meerdere woorden worden gemaakt waarbij de afzonderlijke woorden wit bevatten ruimte. Hier worden de individuele woorden opgeslagen als afzonderlijke array-elementen, en de "${array[@]}"
-uitbreiding breidt elk element uit als afzonderlijke shell-woorden:
# define the array mycmd=(ls -l "/tmp/test/my dir") # run the command "${mycmd[@]}"
De syntaxis is enigszins vreselijk, maar met arrays kun je de opdrachtregel ook stukje bij beetje opbouwen. Bijvoorbeeld:
mycmd=(ls) # initial command if [ "$want_detail" = 1 ]; then mycmd+=(-l) # optional flag fi mycmd+=("$targetdir") # the filename "${mycmd[@]}"
of houd delen van de opdrachtregel constant en gebruik de array om slechts een deel ervan te vullen, zoals opties of bestandsnamen:
options=(-x -v) files=(file1 "file name with whitespace") target=/somedir transmutate "${options[@]}" "${files[@]}" "$target"
Het nadeel van arrays is dat ze “geen standaardfunctie zijn, dus gewone POSIX-shells (zoals dash
, de standaard /bin/sh
in Debian / Ubuntu) ondersteunen ze niet (maar zie hieronder). Bash, ksh en zsh doen dat echter wel, dus het is waarschijnlijk dat uw systeem een shell heeft die arrays ondersteunt.
Met "$@"
In shells zonder ondersteuning voor benoemde arrays kan men nog steeds de positionele parameters gebruiken (de pseudo-array "$@"
) om de argumenten van een commando vast te houden.
De volgende zouden draagbare scriptbits moeten zijn die het equivalent doen van de codebits in de vorige sectie. De array is vervangen door "$@"
, de lijst met positionele parameters. Het instellen van "$@"
gebeurt met set
, en de dubbele aanhalingstekens rond "$@"
zijn belangrijk (deze zorgen ervoor dat de elementen van de lijst afzonderlijk worden geciteerd).
Ten eerste, gewoon een commando met argumenten opslaan in "$@"
en het uitvoeren:
set -- ls -l "/tmp/test/my dir" "$@"
Voorwaardelijk delen van de opdrachtregelopties instellen voor een commando:
set -- ls if [ "$want_detail" = 1 ]; then set -- "$@" -l fi set -- "$@" "$targetdir" "$@"
Alleen "$@"
gebruiken voor opties en operanden :
set -- -x -v set -- "$@" file1 "file name with whitespace" set -- "$@" /somedir transmutate "$@"
(Natuurlijk wordt "$@"
meestal gevuld met de argumenten voor het script zelf, dus u ” Ik zal ze ergens moeten opslaan voordat ik "$@"
opnieuw gebruik.)
Wees voorzichtig met eval
!
Aangezien eval
een extra niveau van verwerking van citaten en uitbreidingen introduceert, moet u voorzichtig zijn met gebruikersinvoer. Dit werkt bijvoorbeeld zolang de gebruiker typt geen enkele aanhalingstekens:
read -r filename cmd="ls -l "$filename"" eval "$cmd";
Maar als ze de invoer "$(uname)".txt
geven, zal je script graag voert de opdrachtsubstitutie uit.
Een versie met arrays is immuun voor th aangezien de woorden de hele tijd gescheiden worden gehouden, is er geen aanhalingsteken of andere verwerking voor de inhoud van filename
.
read -r filename cmd=(ls -ld -- "$filename") "${cmd[@]}"
Verwijzingen
- Woordsplitsing in BashGuide
- BashFAQ / 050 of ” Ik probeer een commando te plaatsen in een variabele, maar de complexe gevallen mislukken altijd! ”
- De vraag Waarom doet mijn shell-script stoort zich in witruimte of andere speciale tekens? , waarin een aantal problemen wordt besproken met betrekking tot aanhalingstekens en witruimte, inclusief het opslaan van opdrachten.
Reacties
- je kunt het eval citeren omzeilen door
cmd="ls -l $(printf "%q" "$filename")"
te doen. niet mooi, maar als de gebruiker dood is in het gebruik van eeneval
, helpt het. Het ‘ is ook erg handig voor het verzenden van de opdracht door soortgelijke dingen, zoalsssh foohost "ls -l $(printf "%q" "$filename")"
, of in de geest van deze vraag:ssh foohost "$cmd"
. - Niet direct gerelateerd, maar heb je de directory hard gecodeerd? In dat geval wilt u misschien naar alias kijken. Iets als:
$ alias abc='ls -l "/tmp/test/my dir"'
Antwoord
De veiligste manier om voer een (niet-triviaal) commando uit is eval
. Vervolgens kunt u het commando schrijven zoals u zou doen op de commandoregel en het wordt precies uitgevoerd alsof u het net had ingevoerd. Maar je moet alles citeren.
Eenvoudig geval:
abc="ls -l "/tmp/test/my dir"" eval "$abc"
niet zo eenvoudig geval:
# command: awk "! a[$0]++ { print "foo: " $0; }" inputfile abc="awk "\""! a[$0]++ { print "foo: " $0; }"\"" inputfile" eval "$abc"
Reacties
- Hallo @HaukeLaging, wat is \ ‘ ‘?
- @jumping_monkey Je kunt geen
'
hebben binnen een'
-citaat string . Je moet dus (1) het citeren beëindigen / onderbreken, (2) ontsnappen aan de volgende'
om het letterlijk te krijgen, (3) het letterlijke citaat typen en (4) in geval van een onderbreking, hervat de geciteerde string:(1)'(2)\(3)'(4)'
- Hallo @HaukeLaging, interessant, en waarom niet gewoon
abc='awk \'! a[$0]++ { print "foo: " $0; }\' inputfile'
? - @jumping_monkey Laat staan dat ik je zojuist heb uitgelegd waarom dat niet werkt: Zou het ‘ niet zinvol zijn om code te testen voordat je deze plaatst?
Answer
Het tweede aanhalingsteken breekt de opdracht af.
Wanneer ik voer:
abc="ls -l "/home/wattana/Desktop"" $abc
Ik kreeg een foutmelding.
Maar toen ik
abc="ls -l /home/wattana/Desktop" $abc
Er is helemaal geen fout
Er is momenteel (voor mij) geen manier om dit op te lossen, maar je kunt de fout vermijden door geen ruimte in de mapnaam te hebben.
Dit antwoord zei dat het eval-commando kan worden gebruikt om dit op te lossen, maar het werkt niet voor mij 🙁
Reacties
- Ja, dat werkt zolang er ‘ s geen b.v. bestandsnamen met ingesloten spaties (of degenen die glob-tekens bevatten).
Answer
Als dit niet werkt met ""
, gebruik dan ``
:
abc=`ls -l /tmp/test/my\ dir/`
Update beter way:
abc=$(ls -l /tmp/test/my\ dir/)
Reacties
- Dit slaat het resultaat van het commando op in een variabele. Het OP wil (vreemd genoeg) het commando zelf in een variabele opslaan. Oh, en je zou echt
$( command... )
moeten gaan gebruiken in plaats van backticks. - Heel erg bedankt voor de verduidelijkingen en tips!
Antwoord
Wat dacht je van een python3 one-liner?
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 ..
Opmerkingen
- De vraag gaat over BASH, niet over python.
Answer
Nog een simpele truc om een (triviaal / niet-triviaal) commando uit te voeren dat is opgeslagen in abc
variabele is:
$ history -s $abc
en druk op UpArrow
of Ctrl-p
om het naar de opdrachtregel te brengen. In tegenstelling tot elke andere methode kun je op deze manier het bewerken voordat het wordt uitgevoerd, indien nodig.
Dit commando zal de variabele inhoud als nieuw item toevoegen aan de Bash-geschiedenis en je kunt het oproepen met UpArrow
.