วิธีการกำหนดฟังก์ชั่นทุบตีที่คล้ายกันในครั้งเดียว


10

ฉันมีฟังก์ชั่นเหล่านี้ใน~/.bashrc:

function guard() {
    if [ -e 'Gemfile' ]; then
    bundle exec guard "$@"
    else
    command guard "$@"
    fi
}
function rspec() {
    if [ -e 'Gemfile' ]; then
    bundle exec rspec "$@"
    else
    command rspec "$@"
    fi
}
function rake() {
    if [ -e 'Gemfile' ]; then
        bundle exec rake "$@"
    else
        command rake "$@"
    fi
}

อย่างที่คุณเห็นฟังก์ชั่นเหล่านี้คล้ายกันมาก ฉันต้องการนิยามฟังก์ชั่นทั้งสามนี้ในครั้งเดียว มีวิธีทำไหม?

สิ่งแวดล้อม

bash --version
GNU bash, version 3.2.51(1)-release (x86_64-apple-darwin13)

คำตอบ:


8
$ cat t.sh
#!/bin/bash

for func in guard rspec rake; do
        eval "
        ${func}() {
                local foo=(command ${func})
                [ -e 'Gemfile' ] && foo=(bundle exec ${func})
                \"\${foo[@]}\" \"\$@\"
        }
        "
done

type guard rspec rake

.

$ ./t.sh
guard is a function
guard ()
{
    local foo=(command guard);
    [ -e 'Gemfile' ] && foo=(bundle exec guard);
    "${foo[@]}" "$@"
}
rspec is a function
rspec ()
{
    local foo=(command rspec);
    [ -e 'Gemfile' ] && foo=(bundle exec rspec);
    "${foo[@]}" "$@"
}
rake is a function
rake ()
{
    local foo=(command rake);
    [ -e 'Gemfile' ] && foo=(bundle exec rake);
    "${foo[@]}" "$@"
}

ข้อควรระวังตามปกติเกี่ยวกับการevalใช้


ไม่ได้กินโดยfor loop?ฉันหมายความว่าตัวแปรที่ประกาศfor loopโดยทั่วไปหายไป - ฉันคาดหวังฟังก์ชั่นเดียวกันด้วยเหตุผลเดียวกัน
mikeserv

อะไรทำให้คุณคิดอย่างนั้น =>bash -c 'for i in 1; do :; done; echo $i' แสดงให้เห็นอย่างชัดเจนว่าฟังก์ชั่นที่มีอยู่นอกขอบเขตของวง 1type
Adrian Frühwirth

1
@mikeserv แม้จะมีbashการกำหนดขอบเขตแบบไดนามิกทั้งหมดที่คุณจะได้รับคือlocalตัวแปรในขอบเขตของฟังก์ชันทั้งหมดตัวแปรจะไม่ "หายไป" อย่างแน่นอนหลังจากวนซ้ำ ในความเป็นจริงเนื่องจากไม่มีฟังก์ชั่นเกี่ยวข้องที่นี่จึงไม่สามารถกำหนดlocalตัวแปรในกรณีนี้ได้
Adrian Frühwirth

ขวา - โลคอล for for loop - ถูก จำกัด ขอบเขตไว้ พวกเขาจะหายไปทันทีที่เปลือกแม่ของพวกเขาทำ สิ่งนี้ไม่ได้เกิดขึ้นที่นี่ใช่ไหม
mikeserv

ไม่อย่างที่ฉันเพิ่งอธิบายไม่มีแนวคิดเช่น "local to for loop" ในการเขียนสคริปต์เชลล์และโพสต์ของฉันและตัวอย่างในความคิดเห็นของฉันด้านบนแสดงให้เห็นอย่างชัดเจนว่า
Adrian Frühwirth

7
_gem_dec() { shift $# ; . /dev/fd/3
} 3<<-FUNC
    _${1}() { [ ! -e 'Gemfile' ] && { 
        command $1 "\$@" ; return \$?
        } || bundle exec $1 "\$@"
    }
FUNC
for func in guard rspec rake ; do _gem_dec $func ; done
echo "_guard ; _rspec ; _rake are all functions now."

จะ. source /dev/fd/3ถูกป้อนเข้าสู่_gem_dec()ฟังก์ชันทุกครั้งที่เรียกว่าเป็นงานที่ประเมินล่วงหน้าhere-document. _gem_dec'sเท่านั้นที่จะได้รับหนึ่งพารามิเตอร์และประเมินล่วงหน้าเป็นทั้งbundle execเป้าหมายและเป็นชื่อของฟังก์ชันที่มีการกำหนดเป้าหมาย

NOTE: . sourcing shell expansions results in twice-evaluated variables - just like eval. It can be risky.

ในกรณีข้างต้นฉันไม่คิดว่าจะมีความเสี่ยงใด ๆ

ถ้ารหัสบล็อกดังกล่าวข้างต้นจะถูกคัดลอกลงใน.bashrcไฟล์ไม่เพียง แต่จะฟังก์ชั่นเปลือก_guard(), _rspec()และ_rake()ได้รับการประกาศในการเข้าสู่ระบบ แต่_gem_dec()ฟังก์ชั่นยังจะสามารถใช้ได้สำหรับการดำเนินการในเวลาใด ๆ ที่เปลือกของคุณพรอมต์(หรืออื่น ๆ )และฟังก์ชั่นเทมเพลตใหม่สามารถ ได้รับการประกาศเมื่อใดก็ตามที่คุณต้องการด้วยเพียง:

_gem_dec $new_templated_function_name

และต้องขอบคุณ @Andrew ที่แสดงให้ฉันเห็นสิ่งเหล่านี้จะไม่ได้รับการกินโดย for loop.

แต่อย่างไร

ฉันใช้3file descriptor ด้านบนเพื่อไม่ให้เกิดการstdin, stdout, and stderr, or <&0 >&1 >&2เปิด - ถึงอย่างนั้นเป็นกรณีของข้อควรระวังเริ่มต้นอื่น ๆ ที่ฉันใช้ที่นี่เพราะฟังก์ชั่นที่ได้นั้นง่ายมากจึงไม่จำเป็นจริงๆ มันเป็นวิธีปฏิบัติที่ดีแม้ว่า การโทรshift $#เป็นอีกหนึ่งข้อควรระวังที่ไม่จำเป็น

แต่ถึงกระนั้นเมื่อไฟล์ที่ถูกระบุเป็น<inputหรือ>outputมี[optional num]<fileหรือ[optional num]>fileเปลี่ยนเส้นทางเคอร์เนลอ่านมันเป็นอธิบายไฟล์ซึ่งสามารถเข้าถึงได้ผ่านทางไฟล์พิเศษในcharacter device หากละเว้นตัวระบุจะถือว่าเป็นอินพุตและเอาต์พุต พิจารณาสิ่งนี้:/dev/fd/[0-9]*[optional num]0<file1>file

l='line %d\n' ; printf "$l" 1 2 3 4 5 6 >/dev/fd/1
> line 1
> line 2
> line 3
> line 4
> line 5
> line 6

( printf "$l" 4 5 6 >/dev/fd/3 ; printf "$l" 1 2 3 ) >/tmp/sample 3>/tmp/sample2

( cat /tmp/sample2 ) </tmp/sample
> line 4
> line 5
> line 6

( cat /dev/fd/0 ) </tmp/sample
> line 1
> line 2
> line 3

( cat /dev/fd/3 ) </tmp/sample 3</tmp/sample2
> line 4
> line 5
> line 6

และเนื่องจาก a here-documentเป็นเพียงวิธีการอธิบายไฟล์แบบอินไลน์ภายในโค้ดบล็อคเมื่อเรา:

<<'HEREDOC'
[$CODE]
HEREDOC

เราอาจทำเช่นกัน:

echo '[$CODE]' >/dev/fd/0

ด้วยความแตกต่างที่สำคัญอย่างหนึ่ง หากคุณไม่ได้"'\quote'"เป็น<<"'\LIMITER"'ของhere-documentเปลือกแล้วจะประเมินให้เปลือก$expansionเช่น:

echo "[$CODE]" >/dev/fd/0

ดังนั้นสำหรับ_gem_dec(),3<<-FUNC here-documentได้รับการประเมินเป็นไฟล์กับการป้อนข้อมูลเช่นเดียวกับที่มันจะถ้ามันเป็น3<~/some.file ยกเว้นว่าเป็นเพราะเราปล่อยให้FUNClimiterฟรีของคำพูดที่มันได้รับการประเมินครั้งแรกสำหรับ$expansion.สิ่งที่สำคัญเกี่ยวกับเรื่องนี้ก็คือว่ามันคือการใส่ความหมาย มันมีอยู่เพียงเพื่อ_gem_dec(),แต่มันก็ยังมีการประเมินก่อนที่จะ_gem_dec()ทำงานฟังก์ชั่นเพราะเปลือกของเราจะต้องอ่านและประเมินผล$expansionsก่อนที่จะส่งมันออกมาเป็นอินพุต

ให้ทำguard,เช่น:

_gem_dec guard

ดังนั้นก่อนอื่นเชลล์ต้องจัดการอินพุตซึ่งหมายถึงการอ่าน:

3<<-FUNC
    _${1}() { [ ! -e 'Gemfile' ] && { 
        command $1 "\$@" ; return \$?
        } || bundle exec $1 "\$@"
    }
FUNC

ลงใน file descriptor 3 และประเมินผลสำหรับการขยายเชลล์ ถ้าในเวลานี้คุณวิ่ง:

cat /dev/fd/3

หรือ:

cat <&3

เนื่องจากทั้งคู่เป็นคำสั่งที่เทียบเท่าคุณจะเห็น *:

_guard() { [ ! -e 'Gemfile' ] && { 
    command guard "$@" ; return $?
    } || bundle exec guard "$@"
}

... ก่อนหน้ารหัสใด ๆ ในฟังก์ชั่นรันเลย นี้เป็นฟังก์ชั่น<input,หลังจากทั้งหมด สำหรับตัวอย่างเพิ่มเติมดูคำตอบของฉันคำถามที่แตกต่างกันที่นี่

(* เทคนิคนี้ไม่เป็นความจริงอย่างสมบูรณ์เพราะฉันใช้ผู้นำ-dashก่อนหน้าhere-doc limiterทั้งหมดข้างต้นจะถูกจัดชิดซ้าย แต่ฉันใช้-dashเพื่อให้สามารถ<tab-insert>อ่านได้ตั้งแต่แรกดังนั้นฉันจะไม่ถอด<tab-inserts>ก่อน เสนอให้คุณอ่าน ... )

ส่วนที่ดีที่สุดเกี่ยวกับเรื่องนี้คือการอ้างอิง - โปรดสังเกตว่า'"ราคายังคงอยู่และมีเพียง\ราคาที่ถูกปล้น มันอาจจะเป็นเพราะเหตุนี้มากขึ้นกว่าที่อื่น ๆ ว่าถ้าคุณมีถึงสองครั้งประเมินเปลือก$expansionผมจะแนะนำhere-documentเพราะคำพูดที่มีมากง่ายกว่าeval

อย่างไรก็ตามในขณะนี้โค้ดข้างต้นเป็นเหมือนไฟล์เลี้ยงเหมือน3<~/heredoc.fileเพียงแค่รอฟังก์ชั่นจะได้รับไปและยอมรับการป้อนข้อมูลของตนใน_gem_dec()/dev/fd/3

ดังนั้นเมื่อเราเริ่มต้น_gem_dec()สิ่งแรกที่ฉันทำคือการโยนพารามิเตอร์ตำแหน่งทั้งหมดเพราะขั้นตอนต่อไปของเราคือการขยายตัวของเชลล์ที่ประเมินเป็นสองเท่าและฉันไม่ต้องการให้มี$expansionsการตีความ$1 $2 $3...พารามิเตอร์ใด ๆ ที่เป็นพารามิเตอร์ปัจจุบันของฉัน ดังนั้นฉัน:

shift $#

shiftทิ้งให้มากpositional parametersที่สุดเท่าที่คุณระบุและเริ่มจาก$1สิ่งที่เหลืออยู่ ดังนั้นถ้าผมเรียกว่า_gem_dec one two threeพรอมต์_gem_dec's $1 $2 $3พารามิเตอร์ตำแหน่งจะเป็นone two threeและรวมนับตำแหน่งปัจจุบันหรือ$#จะเป็น 3. หากฉันแล้วเรียกว่าshift 2,ค่าของoneและtwoจะได้รับการshiftedไปค่าของ$1จะเปลี่ยนไปthreeและ$#จะขยายไปยัง1.ดังนั้นshift $#เพียง โยนพวกเขาทั้งหมดออกไป การทำเช่นนี้เป็นข้อควรระวังอย่างเคร่งครัดและเป็นเพียงนิสัยที่ฉันพัฒนาขึ้นหลังจากทำสิ่งนี้มาชั่วระยะเวลาหนึ่ง ที่นี่มีการ(subshell)แพร่กระจายเล็กน้อยเพื่อประโยชน์ของความชัดเจน:

( set -- one two three ; echo "$1 $2 $3" ; echo $# )
> one two three
> 3

( set -- one two three ; shift 2 ; echo "$1 $2 $3" ; echo $# )
> three
> 1

( set -- one two three ; shift $# ; echo "$1 $2 $3" ; echo $# )
>
> 0

อย่างไรก็ตามขั้นตอนต่อไปคือสิ่งที่เวทมนตร์เกิดขึ้น หากคุณ. ~/some.shอยู่ที่ shell prompt ฟังก์ชันและตัวแปรสภาพแวดล้อมทั้งหมดที่ประกาศไว้~/some.shจะสามารถเรียกใช้งานได้ที่ shell prompt เดียวกันเป็นจริงที่นี่ยกเว้นเราแฟ้มพิเศษสำหรับอธิบายไฟล์ของเราหรือ- ซึ่งเป็นที่ที่เราไฟล์ในสายการผลิตที่ได้รับการ pathed - และเราได้ประกาศฟังก์ชั่นของเรา และนั่นคือวิธีการทำงาน. sourcecharacter device. /dev/fd/3here-document

_guard

ตอนนี้สิ่งที่_guardฟังก์ชั่นของคุณควรจะทำ

ภาคผนวก:

วิธีที่ดีในการพูดช่วยรักษาตำแหน่งของคุณ:

f() { . /dev/fd/3
} 3<<-ARGS
    args='${args:-"$@"}'
ARGS

แก้ไข:

เมื่อแรกที่ฉันตอบคำถามนี้ฉันมุ่งเน้นไปที่ปัญหาของการประกาศเชลล์ที่function()สามารถประกาศฟังก์ชั่นอื่น ๆ ที่จะคงอยู่ใน$ENViron ironเชลล์ปัจจุบันมากกว่าที่ฉันทำกับสิ่งที่ผู้ถามจะทำกับฟังก์ชั่นถาวรดังกล่าว ตั้งแต่นั้นมาฉันก็ตระหนักว่าโซลูชันที่ฉันเสนอมา แต่เดิม3<<-FUNCใช้แบบฟอร์มนี้:

3<<-FUNC
    _${1}() { 
        if [ -e 'Gemfile' ]; then
            bundle exec $1 "\$@"
        else 
            command _${1} "\$@"
    }
FUNC

อาจจะไม่ได้ทำงานตามที่คาดไว้สำหรับถามเพราะผมมีการเปลี่ยนแปลงโดยเฉพาะชื่อของฟังก์ชั่นที่เปิดเผยจาก$1ไป_${1}ซึ่งถ้าเรียกว่าเหมือน_gem_dec guardเช่นจะส่งผลในการ_gem_decประกาศฟังก์ชั่นที่มีชื่อว่าเป็นนอกคอกเพียง_guardguard

หมายเหตุ:พฤติกรรมดังกล่าวเป็นเรื่องของนิสัยสำหรับฉัน - ฉันมักจะทำงานในข้อสันนิษฐานว่าฟังก์ชั่นเปลือกควรครอบครองเฉพาะของตัวเอง_namespaceเพื่อหลีกเลี่ยงการบุกรุกบนnamespaceเชลล์commandsที่เหมาะสม

นี้ไม่ได้เป็นนิสัยสากลแม้ว่าจะเป็นที่แจ้งในการใช้งานถามของการเรียกร้องcommand$1

การตรวจสอบเพิ่มเติมทำให้ฉันเชื่อในสิ่งต่อไปนี้:

ผู้ถามต้องการฟังก์ชั่นของเชลล์ที่ชื่อguard, rspec, or rakeเมื่อเรียกใช้จะรวบรวมrubyฟังก์ชั่นใหม่ที่มีชื่อเดียวกันifกับไฟล์ที่Gemfileมีอยู่ใน$PATH OR if Gemfileไม่มีอยู่ฟังก์ชันของเชลล์ควรเรียกใช้rubyฟังก์ชันที่ชื่อเดียวกัน

สิ่งนี้จะไม่ทำงานก่อนหน้านี้เพราะฉันได้เปลี่ยน$1ชื่อcommandที่อ่านโดย:

command _${1}

ซึ่งจะไม่มีผลในการดำเนินการของrubyฟังก์ชั่นที่ฟังก์ชั่นเปลือกรวบรวมเป็น:

bundle exec $1

ฉันหวังว่าคุณจะเห็น(ในที่สุดฉันก็)ว่าดูเหมือนว่าผู้ถามจะใช้เพียงcommandเพื่อระบุทางอ้อมnamespaceเพราะcommandจะต้องการเรียกไฟล์ปฏิบัติการใน$PATHกว่าฟังก์ชั่นเชลล์ในชื่อเดียวกัน

หากการวิเคราะห์ของฉันถูกต้อง(เพราะฉันหวังว่าผู้ถามจะยืนยัน)สิ่งนี้:

_${1}() { [ ! -e 'Gemfile' ] && { 
    command $1 "\$@" ; return \$?
    } || bundle exec $1 "\$@"
}

ดีกว่าที่ควรจะตอบสนองเงื่อนไขเหล่านั้นมีข้อยกเว้นว่าการเรียกร้องguardที่จะแจ้งให้เพียงความพยายามที่จะดำเนินการแฟ้มที่ปฏิบัติการใน$PATHชื่อguardขณะที่เรียกร้อง_guardที่พร้อมท์จะตรวจสอบสำหรับGemfile'sการดำรงอยู่และรวบรวมตามหรือดำเนินการในการปฏิบัติการguard ด้วยวิธีนี้ได้รับการคุ้มครองและอย่างน้อยที่สุดฉันก็เข้าใจเจตนาของผู้ถามยังคงเป็นจริง$PATHnamespace

ในความเป็นจริงการสันนิษฐานว่าฟังก์ชั่นเชลล์ของเรา_${1}()และเอ็กซีคิ้วท์${PATH}/${1}เป็นสองวิธีเท่านั้นที่เชลล์ของเราสามารถตีความการเรียกใช้$1หรือ_${1}จากนั้นการใช้commandฟังก์ชั่นเลยก็ทำซ้ำซ้อนทั้งหมด ถึงกระนั้นฉันก็ปล่อยให้มันยังคงอยู่เพราะฉันไม่ชอบทำผิดซ้ำสองครั้ง ... ต่อเนื่องกัน

หากสิ่งนี้ไม่เป็นที่ยอมรับของผู้ถามและเขา / เธอต้องการที่จะทำสิ่งที่ถูกต้อง_ทั้งหมดในรูปแบบปัจจุบันการแก้ไขข้อผิดพลาด_underscoreควรเป็นสิ่งที่ผู้ถามทุกคนต้องทำเพื่อตอบสนองความต้องการของเขา / เธอตามที่ฉันเข้าใจ

นอกเหนือจากการเปลี่ยนแปลงนั้นฉันได้แก้ไขฟังก์ชั่นการใช้&&และ / หรือ||เงื่อนไขการลัดวงจรของเชลล์มากกว่าif/thenไวยากรณ์ดั้งเดิม ด้วยวิธีนี้commandคำสั่งจะถูกประเมินเท่านั้นที่ทุกคนถ้าไม่ได้อยู่ในGemfile การปรับเปลี่ยนนี้ไม่จำเป็นต้องมีการเพิ่มอย่างไรก็ตามเพื่อให้แน่ใจว่าคำสั่งจะไม่ทำงานในกรณีที่ไม่มีอยู่ แต่ฟังก์ชันจะส่งคืนสิ่งอื่นที่ไม่ใช่0$PATHreturn $?bundleGemfileruby $1

สุดท้ายฉันควรทราบว่าโซลูชันนี้ใช้โครงสร้างเปลือกแบบพกพาเท่านั้น กล่าวอีกนัยหนึ่งสิ่งนี้ควรให้ผลลัพธ์ที่เหมือนกันในเชลล์ใด ๆ ที่อ้างถึงความเข้ากันได้ของ POSIX ขณะที่มันจะแน่นอนเป็นเรื่องไร้สาระสำหรับผมที่จะเรียกร้องทุกระบบ POSIX ได้จะต้องจัดการกับruby bundleคำสั่งอย่างน้อยสนองเปลือกเรียกร้องให้มันควรประพฤติเดียวกันโดยไม่คำนึงถึงว่าเปลือกเรียกเป็นหรือsh dashนอกจากนี้การทำงานจะดังกล่าวข้างต้นเป็นไปตามคาด(ทะนงอย่างน้อยครึ่งหนึ่งสติshoptsอยู่แล้ว)ทั้งในและbashzsh


ฉันใส่รหัสของคุณเข้า~/.bashrcและโทร. ~/.bashrcจากนั้นทั้งสามฟังก์ชั่นจะถูกดำเนินการ บางทีพฤติกรรมนั้นแตกต่างกันตามสภาพแวดล้อมดังนั้นฉันจึงเพิ่มสภาพแวดล้อมของฉันลงในคำถาม นอกจากนี้ฉันไม่เข้าใจว่าทำไม_guard ; _rspec ; _rakeต้องมีบรรทัดสุดท้าย ฉันค้นหาshiftและอธิบายไฟล์ดูเหมือนว่าสิ่งเหล่านี้เกินความเข้าใจปัจจุบันของฉัน
เตารีดและ

ฉันแค่วางมันไว้ที่นั่นเพื่อแสดงว่าพวกเขาเรียกได้ ขออภัยฉันใส่ก้องแล้ว ดังนั้นคุณสามารถเรียกพวกมันว่าเป็นฟังก์ชั่นได้ตามที่คุณสาธิต
mikeserv

@Tetsu - มันสมเหตุสมผลดีขึ้นแล้วเหรอ?
mikeserv

ฉันอ่านคำตอบของคุณ 3 ครั้ง แต่พูดอย่างตรงไปตรงมาว่าฉันต้องการความรู้เพิ่มเติมเพื่อเข้าใจคำอธิบาย แม้ว่าฉันจะขอบคุณคุณมากฉันจะอ่านอีกครั้งเมื่อฉันได้รับประสบการณ์มากขึ้น
เตารีดและ

@Tetsu บางทีมันชัดเจนขึ้น ... ฉันคิดว่าฉันรู้และตอนนี้ได้แก้ไขข้อผิดพลาดที่ฉันทำก่อนหน้า โปรดแจ้งให้เราทราบหากคุณต้องการ
mikeserv

2
function threeinone () {
    local var="$1"
    if [ $# -ne 1 ]; then
        return 1
    fi
    if ! [ "$1" = "guard" -o "$1" = "rspec" -o "$1" = "rake" ]; then
        return 1
    fi
    shift
    if [ -e 'Gemfile' ]; then
        bundle exec "$var" "$@"
    else
        command "$var" "$@"
    fi
}

threeinone guard
threeinone rspec
threeinone rake
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.