ตรวจสอบตัวแปรเป็นอาร์เรย์ใน Bourne like shell หรือไม่


14

ใน Bourne like shell ซึ่งสนับสนุนตัวแปรอาเรย์เราสามารถใช้การวิเคราะห์คำเพื่อตรวจสอบว่าตัวแปรเป็นอาร์เรย์หรือไม่

a=(1 2 3)คำสั่งทั้งหมดด้านล่างนี้ถูกเรียกใช้หลังจากการทำงาน

zsh:

$ declare -p a
typeset -a a
a=( 1 2 3 )

bash:

$ declare -p a
declare -a a='([0]="1" [1]="2" [2]="3")'

ksh93:

$ typeset -p a
typeset -a a=(1 2 3)

pdksh และอนุพันธ์:

$ typeset -p a
set -A a
typeset a[0]=1
typeset a[1]=2
typeset a[2]=3

yash:

$ typeset -p a
a=('1' '2' '3')
typeset a

ตัวอย่างในbash:

if declare -p var 2>/dev/null | grep -q 'declare -a'; then
  echo array variable
fi

วิธีนี้ใช้ได้ผลมากเกินไปและจำเป็นต้องวางไข่ subshell ใช้เปลือก builtin อื่น ๆ เช่น=~ใน[[ ... ]]ไม่จำเป็นต้อง subshell แต่ยังคงมีความซับซ้อนมากเกินไป

มีวิธีที่ง่ายกว่าในการทำภารกิจนี้หรือไม่?


คุณต้องตรวจสอบว่าตัวแปรของคุณเป็นอาร์เรย์หรือไม่ภายใต้สถานการณ์ใด
Kusalananda

คำตอบ:


10

ฉันไม่คิดว่าคุณทำได้และฉันไม่คิดว่ามันจะสร้างความแตกต่าง

unset a
a=x
echo "${a[0]-not array}"

x

นั่นจะเป็นสิ่งเดียวกันทั้งในและksh93 bashดูเหมือนว่าตัวแปรทั้งหมดอาจเป็นอาร์เรย์ในเชลล์เหล่านั้นหรืออย่างน้อยตัวแปรปกติใด ๆ ที่ไม่ได้รับการกำหนดคุณสมบัติพิเศษ แต่ฉันไม่ได้ตรวจสอบมากนัก

bashเจรจาคู่มือเกี่ยวกับพฤติกรรมที่แตกต่างกันสำหรับอาร์เรย์เมื่อเทียบกับตัวแปรสตริงเมื่อใช้+=ที่ได้รับมอบหมาย แต่หลังจากนั้นการป้องกันความเสี่ยงและรัฐว่าอาร์เรย์เท่านั้นทำงานแตกต่างกันในสารประกอบบริบทที่ได้รับมอบหมาย

นอกจากนี้ยังระบุว่าตัวแปรนั้นถือเป็นอาร์เรย์หากตัวห้อยใด ๆ ได้รับการกำหนดค่า - และรวมถึงความเป็นไปได้ของสตริงว่าง ด้านบนคุณจะเห็นว่าการกำหนดปกติส่งผลให้ตัวห้อยถูกกำหนดอย่างแน่นอนและดังนั้นฉันเดาว่าทุกอย่างเป็นอาร์เรย์

ในทางปฏิบัติคุณอาจใช้:

[ 1 = "${a[0]+${#a[@]}}" ] && echo not array

... เพื่อระบุตัวแปรชุดที่ชัดเจนที่ได้รับการกำหนดค่าตัวห้อยเดียว 0


ดังนั้นฉันเดาการตรวจสอบว่า${a[1]-not array}สามารถทำงานได้ใช่ไหม
cuonglm

@cuonglm - ไม่เป็นไปตามbashคู่มือ: ตัวแปรอาร์เรย์ถูกพิจารณาว่าถูกตั้งค่าหากมีการกำหนดค่าตัวห้อย สตริง Null เป็นค่าที่ถูกต้อง หากตัวห้อยใด ๆ ที่ได้รับมอบหมายอาร์เรย์ของมันต่อสเป็ค a[5]=xในทางปฏิบัติยังไม่มีเพราะคุณสามารถทำ ฉันเดาว่า[ 1 -eq "${#a[@]}" ] && [ -n "${a[0]+1}" ]ทำงานได้
mikeserv

6

ดังนั้นคุณต้องการได้อย่างมีประสิทธิภาพเพียงส่วนตรงกลางของdeclare -pไม่มีขยะรอบ ๆ มันได้หรือไม่

คุณสามารถเขียนแมโครได้เช่น:

readonly VARTYPE='{ read __; 
       case "`declare -p "$__"`" in
            "declare -a"*) echo array;; 
            "declare -A"*) echo hash;; 
            "declare -- "*) echo scalar;; 
       esac; 
         } <<<'

เพื่อให้คุณสามารถ:

a=scalar
b=( array ) 
declare -A c; c[hashKey]=hashValue;
######################################
eval "$VARTYPE" a #scalar
eval "$VARTYPE" b #array
eval "$VARTYPE" c #hash

(ฟังก์ชั่นเพียงอย่างเดียวจะไม่ทำถ้าคุณต้องการใช้ฟังก์ชันนี้กับตัวแปรของฟังก์ชันท้องถิ่น)


ด้วยนามแฝง

shopt -s expand_aliases
alias vartype='eval "$VARTYPE"'

vartype a #scalar
vartype b #array
vartype c #hash

@mikeserv จุดดี นามแฝงทำให้ดูสวยขึ้น +1
PSkocik

ฉันหมายถึง - alias vartype="$VARTYPE"... หรือเพียงแค่ไม่กำหนด$VARTYPEเลย - มันควรจะใช้ได้ใช่ไหม คุณควรต้องการshoptสิ่งนั้นbashเพราะมันทำลายข้อกำหนดที่เกี่ยวข้องกับaliasการขยายสคริปต์
mikeserv

1
@mikeserv ฉันแน่ใจว่า cuonglm สามารถปรับวิธีการนี้ให้ตรงกับความต้องการและความชอบของเขา ;-)
PSkocik

... และข้อควรพิจารณาด้านความปลอดภัย
PSkocik

ผู้ใช้โค้ดข้างต้นจะไม่แสดงข้อความใด ๆ มันปลอดภัยไม่น้อยไปกว่าฟังก์ชั่น ฉันไม่เคยเห็นคุณยุ่งเกี่ยวกับการสร้างฟังก์ชั่นแบบอ่านอย่างเดียว แต่โอเคฉันสามารถทำเครื่องหมายตัวแปรแบบอ่านอย่างเดียวได้
PSkocik

6

ใน zsh

zsh% a=(1 2 3) s=1
zsh% [[ ${(t)a} == *array* ]] && echo array
array
zsh% [[ ${(t)s} == *array* ]] && echo array
zsh%

อาจecho ${(t)var}จะง่ายกว่า ขอบคุณสำหรับสิ่งนี้.

4

เพื่อทดสอบตัวแปร var ด้วย

b=("${!var[@]}")
c="${#b[@]}"

เป็นไปได้ที่จะทดสอบว่ามีมากกว่าหนึ่งดัชนีอาเรย์หรือไม่:

[[ $c > 1 ]] && echo "Var is an array"

หากค่าดัชนีแรกไม่ใช่ศูนย์:

[[ ${b[0]} -eq 0 ]] && echo "Var is an array"      ## should be 1 for zsh.

ความสับสนอย่างหนักเพียงอย่างเดียวคือเมื่อมีค่าดัชนีเพียงค่าเดียวและค่านั้นเป็นศูนย์ (หรือหนึ่งค่า)

สำหรับเงื่อนไขนั้นเป็นไปได้ที่จะใช้ผลข้างเคียงของการพยายามลบองค์ประกอบอาร์เรย์ออกจากตัวแปรที่ไม่ใช่อาร์เรย์:

**bash** reports an error with             unset var[0]
bash: unset: var: not an array variable

**zsh** also reports an error with         $ var[1]=()
attempt to assign array value to non-array

วิธีนี้ใช้ได้อย่างถูกต้องสำหรับ bash:

# Test if the value at index 0 could be unset.
# If it fails, the variable is not an array.
( unset "var[0]" 2>/dev/null; ) && echo "var is an array."

สำหรับ zsh ดัชนีอาจต้องเป็น 1 (ยกเว้นว่าโหมดที่เข้ากันได้นั้นเปิดใช้งาน)

จำเป็นต้องใช้ sub-shell เพื่อหลีกเลี่ยงผลข้างเคียงของการลบ index 0 ของ var

ฉันไม่พบวิธีที่จะทำให้การทำงานเป็น ksh

แก้ไข 1

ฟังก์ชันนี้ใช้งานได้ใน bash4.2 + เท่านั้น

getVarType(){
    varname=$1;
    case "$(typeset -p "$varname")" in
        "declare -a"*|"typeset -a"*)    echo array; ;;
        "declare -A"*|"typeset -A"*)    echo hash; ;;
        "declare -- "*|"typeset "$varname*| $varname=*) echo scalar; ;;
    esac;
}

var=( foo bar );  getVarType var

แก้ไข 2

สิ่งนี้ใช้ได้กับ bash4.2 + เท่านั้น

{ typeset -p var | grep -qP '(declare|typeset) -a'; } && echo "var is an array"

หมายเหตุ: สิ่งนี้จะให้ผลบวกเป็นเท็จหาก var มีสตริงที่ทดสอบ


วิธีการเกี่ยวกับอาร์เรย์ที่มีองค์ประกอบศูนย์?
cuonglm

1
แก้ไขข้อมูล tho ดูเป็นต้นฉบับมาก : D
PSkocik

@cuonglm การตรวจสอบ( unset "var[0]" 2>/dev/null; ) && echo "var is an array."อย่างถูกต้องรายงาน var เป็นอาร์เรย์เมื่อ var ถูกตั้งค่าvar=()เป็นอาร์เรย์ที่มีองค์ประกอบเป็นศูนย์ มันทำหน้าที่เท่ากับประกาศ

การทดสอบสเกลาร์จะไม่ทำงานหากสเกลาร์ถูกส่งออกหรือทำเครื่องหมายจำนวนเต็ม / ตัวพิมพ์เล็ก / อ่านอย่างเดียว ... คุณสามารถทำให้มันปลอดภัยได้ว่าเอาท์พุตที่ไม่ว่างอื่น ๆ นั้นหมายถึงตัวแปรสเกลาร์ ฉันจะใช้grep -Eแทนgrep -Pเพื่อหลีกเลี่ยงการพึ่งพา grep GNU
Stéphane Chazelas

@ StéphaneChazelasการทดสอบ (ในทุบตี) สำหรับเกลากับจำนวนเต็มและ / หรือตัวพิมพ์เล็กและ / หรืออ่านได้อย่างเดียวมักจะเริ่มต้นด้วยการเช่นนี้-a declare -airl var='()'ดังนั้นการทดสอบ grep จะใช้งานได้

3

สำหรับทุบตีมันเป็นการแฮ็คเล็กน้อย (แม้ว่าจะมีเอกสาร): พยายามใช้typesetเพื่อลบแอตทริบิวต์ "array":

$ typeset +a BASH_VERSINFO
bash: typeset: BASH_VERSINFO: cannot destroy array variables in this way
echo $?
1

(คุณไม่สามารถทำสิ่งนี้zshได้ช่วยให้คุณสามารถแปลงอาร์เรย์เป็นสเกลาร์bashได้ซึ่งต้องห้ามอย่างชัดเจน)

ดังนั้น:

 typeset +A myvariable 2>/dev/null || echo is assoc-array
 typeset +a myvariable 2>/dev/null || echo is array

หรือในฟังก์ชั่นสังเกตเตือนที่สิ้นสุด:

function typeof() {
    local _myvar="$1"
    if ! typeset -p $_myvar 2>/dev/null ; then
        echo no-such
    elif ! typeset -g +A  $_myvar 2>/dev/null ; then
        echo is-assoc-array
    elif ! typeset -g +a  $_myvar 2>/dev/null; then
        echo is-array
    else
        echo scalar
    fi
}

สังเกตการใช้typeset -g(bash-4.2 หรือใหม่กว่า) ซึ่งเป็นสิ่งจำเป็นในฟังก์ชั่นเพื่อให้typeset(syn. declare) ใช้งานไม่ได้localและปิดบังคุณค่าที่คุณพยายามตรวจสอบ นอกจากนี้ยังไม่รองรับฟังก์ชั่น "ตัวแปร" คุณสามารถเพิ่มการทดสอบสาขาอื่นได้typeset -fหากต้องการ


ตัวเลือกอื่น (ใกล้จะเสร็จสมบูรณ์) คือการใช้สิ่งนี้:

    ${!name[*]}
          If name is an array variable, expands to  the  list
          of  array indices (keys) assigned in name.  If name
          is not an array, expands to 0 if name  is  set  and
          null  otherwise.   When @ is used and the expansion
          appears within double quotes, each key expands to a
          separate word.

มีปัญหาเล็กน้อยหนึ่งข้อ แต่อาร์เรย์ที่มีตัวห้อยตัวเดียวคือ 0 ตรงกับเงื่อนไขสองข้อข้างต้น นี่คือสิ่งที่ mikeserv อ้างอิงเช่นกันทุบตีไม่ได้มีความแตกต่างอย่างมากและบางอย่าง (ถ้าคุณตรวจสอบการเปลี่ยนแปลง) สามารถตำหนิ ksh และความเข้ากันได้กับวิธีการ${name[*]}หรือ${name[@]}พฤติกรรมที่ไม่ใช่อาเรย์

ดังนั้นวิธีแก้ปัญหาบางส่วนคือ:

if [[ ${!BASH_VERSINFO[*]} == '' ]]; then
    echo no-such
elif [[ ${!BASH_VERSINFO[*]} == '0' ]]; then 
    echo not-array
elif [[ ${!BASH_VERSINFO[*]} != '0' ]]; 
    echo is-array    
fi

ฉันเคยใช้รูปแบบที่ผ่านมาในสิ่งนี้:

while read _line; do
   if [[ $_line =~ ^"declare -a" ]]; then 
     ...
   fi 
done < <( declare -p )

สิ่งนี้ก็จำเป็นต้องมี subshell ด้วย

อีกหนึ่งเทคนิคที่มีประโยชน์ก็คือcompgen:

compgen -A arrayvar

นี่จะแสดงรายการอาร์เรย์ที่จัดทำดัชนีทั้งหมดอย่างไรก็ตามอาร์เรย์ที่เชื่อมโยงไม่ได้ถูกจัดการเป็นพิเศษ (สูงสุด bash-4.4) และปรากฏเป็นตัวแปรปกติ ( compgen -A variable)


typeset +aยังรายงานข้อผิดพลาดใน ksh ไม่ได้อยู่ใน zsh

1

คำตอบสั้น ๆ :

สำหรับสองเปลือกหอยที่นำเครื่องหมายนี้ ( bashและksh93) ตัวแปรสเกลาเป็นเพียงอาร์เรย์มีองค์ประกอบเดียว

ไม่จำเป็นต้องมีการประกาศพิเศษเพื่อสร้างอาร์เรย์ เพียงแค่ได้รับมอบหมายเป็นพอและมอบหมายธรรมดาเป็นเหมือนvar=valuevar[0]=value


ลอง: bash -c 'unset var; var=foo; typeset -p var'. คำตอบ bash รายงานอาร์เรย์ (ต้องการ -a) หรือไม่ เปรียบเทียบกับ: bash -c 'unset var; var[12]=foo; typeset -p var'. ทำไมถึงมีความแตกต่าง? A: เชลล์เก็บรักษา (สำหรับดีหรือแย่) ความคิดที่ vars เป็น scalars หรืออาร์เรย์ เชลล์ ksh ทำการผสมผสานทั้งแนวคิดเข้าด้วยกัน

1

arraybuiltin ของ yashมีตัวเลือกบางอย่างที่ทำงานกับตัวแปรอาร์เรย์เท่านั้น ตัวอย่าง: -dตัวเลือกจะรายงานข้อผิดพลาดเกี่ยวกับตัวแปรที่ไม่ใช่อาร์เรย์:

$ a=123
$ array -d a
array: no such array $a

ดังนั้นเราสามารถทำสิ่งนี้:

is_array() (
  array -d -- "$1"
) >/dev/null 2>&1

a=(1 2 3)
if is_array a; then
  echo array
fi

b=123
if ! is_array b; then
  echo not array
fi

วิธีการนี้จะไม่ทำงานหากตัวแปรอาร์เรย์อ่านได้อย่างเดียว กำลังพยายามแก้ไขตัวแปรแบบอ่านอย่างเดียวที่นำไปสู่ข้อผิดพลาด:

$ a=()
$ readonly a
$ array -d a
array: $a is read-only

0
#!/bin/bash

var=BASH_SOURCE

[[ "$(declare -pa)" =~ [^[:alpha:]]$var= ]]

case "$?" in 
  0)
      echo "$var is an array variable"
      ;;
  1)
      echo "$var is not an array variable"
      ;;
  *)
      echo "Unknown exit code"
      ;;
esac
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.