Running External Programs
تستعير جوليا تنسيق العلامات الخلفية للأوامر من الشل، بيرل، وروبي. ومع ذلك، في جوليا، كتابة
julia> `echo hello`
`echo hello`
يختلف في عدة جوانب عن السلوك في مختلف الصدفيات، Perl، أو Ruby:
- بدلاً من تشغيل الأمر على الفور، تقوم الأقواس المائلة بإنشاء كائن
Cmd
لتمثيل الأمر. يمكنك استخدام هذا الكائن لربط الأمر بآخرين عبر الأنابيب،run
، وread
أوwrite
له. - عند تشغيل الأمر، لا تقوم جوليا بالتقاط مخرجاته ما لم تقم بترتيب ذلك بشكل محدد. بدلاً من ذلك، تذهب مخرجات الأمر بشكل افتراضي إلى
stdout
كما هو الحال عند استخدام استدعاءsystem
الخاص بـlibc
. - يتم تشغيل الأمر دون استخدام قشرة. بدلاً من ذلك، يقوم جوليا بتحليل بناء الجملة للأمر مباشرة، مع إدراج المتغيرات بشكل مناسب وتقسيم الكلمات كما تفعل القشرة، مع احترام بناء جملة الاقتباس في القشرة. يتم تشغيل الأمر كعملية فرعية فورية لـ
جوليا
، باستخدام استدعاءاتfork
وexec
.
يفترض ما يلي وجود بيئة Posix كما هو الحال في Linux أو MacOS. في Windows، العديد من الأوامر المماثلة، مثل echo
و dir
، ليست برامج خارجية بل مدمجة في الصدفة cmd.exe
نفسها. إحدى الخيارات لتشغيل هذه الأوامر هي استدعاء cmd.exe
، على سبيل المثال cmd /C echo hello
. بدلاً من ذلك، يمكن تشغيل Julia داخل بيئة Posix مثل Cygwin.
إليك مثال بسيط لتشغيل برنامج خارجي:
julia> mycommand = `echo hello`
`echo hello`
julia> typeof(mycommand)
Cmd
julia> run(mycommand);
hello
الناتج من الأمر echo
هو hello
، المرسل إلى stdout
. إذا فشل الأمر الخارجي في التشغيل بنجاح، فإن طريقة التشغيل ترمي ProcessFailedException
.
إذا كنت ترغب في قراءة ناتج الأمر الخارجي، يمكن استخدام read
أو readchomp
بدلاً من ذلك:
julia> read(`echo hello`, String)
"hello\n"
julia> readchomp(`echo hello`)
"hello"
بشكل أكثر عمومية، يمكنك استخدام open
للقراءة من أو الكتابة إلى أمر خارجي.
julia> open(`less`, "w", stdout) do io
for i = 1:3
println(io, i)
end
end
1
2
3
يمكن الوصول إلى اسم البرنامج والحجج الفردية في الأمر والتكرار عليها كما لو كان الأمر مصفوفة من السلاسل:
julia> collect(`echo "foo bar"`)
2-element Vector{String}:
"echo"
"foo bar"
julia> `echo "foo bar"`[2]
"foo bar"
Interpolation
افترض أنك تريد القيام بشيء أكثر تعقيدًا واستخدام اسم ملف في المتغير file
كوسيلة لتمريره كوسيلة لأمر. يمكنك استخدام $
للتداخل تمامًا كما تفعل في سلسلة حرفية (انظر Strings):
julia> file = "/etc/passwd"
"/etc/passwd"
julia> `sort $file`
`sort /etc/passwd`
أحد الأخطاء الشائعة عند تشغيل برامج خارجية عبر الصدفة هو أنه إذا كان اسم الملف يحتوي على أحرف خاصة بالصدفة، فقد يتسبب ذلك في سلوك غير مرغوب فيه. لنفترض، على سبيل المثال، بدلاً من /etc/passwd
، أننا أردنا فرز محتويات الملف /Volumes/External HD/data.csv
. دعنا نجرب ذلك:
julia> file = "/Volumes/External HD/data.csv"
"/Volumes/External HD/data.csv"
julia> `sort $file`
`sort '/Volumes/External HD/data.csv'`
كيف تم اقتباس اسم الملف؟ تعرف جوليا أن file
من المفترض أن يتم استبداله كحجة واحدة، لذا فهي تقتبس الكلمة من أجلك. في الواقع، هذا ليس دقيقًا تمامًا: قيمة file
لا يتم تفسيرها بواسطة شل، لذا لا حاجة للاقتباس الفعلي؛ يتم إدراج الاقتباسات فقط للعرض على المستخدم. سيعمل هذا حتى إذا قمت باستبدال قيمة كجزء من كلمة شل:
julia> path = "/Volumes/External HD"
"/Volumes/External HD"
julia> name = "data"
"data"
julia> ext = "csv"
"csv"
julia> `sort $path/$name.$ext`
`sort '/Volumes/External HD/data.csv'`
كما ترى، تم الهروب من المسافة في متغير path
بشكل مناسب. ولكن ماذا لو كنت تريد دمج كلمات متعددة؟ في هذه الحالة، استخدم مصفوفة (أو أي حاوية قابلة للتكرار أخرى):
julia> files = ["/etc/passwd","/Volumes/External HD/data.csv"]
2-element Vector{String}:
"/etc/passwd"
"/Volumes/External HD/data.csv"
julia> `grep foo $files`
`grep foo /etc/passwd '/Volumes/External HD/data.csv'`
إذا قمت بعمل استيفاء لمصفوفة كجزء من كلمة شل، فإن جوليا تحاكي توليد وسيطات {a,b,c}
في الشل:
julia> names = ["foo","bar","baz"]
3-element Vector{String}:
"foo"
"bar"
"baz"
julia> `grep xylophone $names.txt`
`grep xylophone foo.txt bar.txt baz.txt`
علاوة على ذلك، إذا قمت بتداخل مصفوفات متعددة في نفس الكلمة، يتم محاكاة سلوك توليد حاصل الضرب الكارتيزي للشل:
julia> names = ["foo","bar","baz"]
3-element Vector{String}:
"foo"
"bar"
"baz"
julia> exts = ["aux","log"]
2-element Vector{String}:
"aux"
"log"
julia> `rm -f $names.$exts`
`rm -f foo.aux foo.log bar.aux bar.log baz.aux baz.log`
نظرًا لأنك تستطيع إدراج المصفوفات الحرفية، يمكنك استخدام هذه الوظيفة التوليدية دون الحاجة إلى إنشاء كائنات مصفوفة مؤقتة أولاً:
julia> `rm -rf $["foo","bar","baz","qux"].$["aux","log","pdf"]`
`rm -rf foo.aux foo.log foo.pdf bar.aux bar.log bar.pdf baz.aux baz.log baz.pdf qux.aux qux.log qux.pdf`
Quoting
لا مفر من أن يرغب المرء في كتابة أوامر ليست بسيطة تمامًا، ويصبح من الضروري استخدام الاقتباسات. إليك مثال بسيط على سطر واحد من Perl في موجه الأوامر:
sh$ perl -le '$|=1; for (0..3) { print }'
0
1
2
3
يجب أن تكون تعبيرات Perl بين علامات اقتباس مفردة لسببين: حتى لا تؤدي المسافات إلى تقسيم التعبير إلى كلمات شل متعددة، وحتى لا تتسبب استخدامات متغيرات Perl مثل $|
(نعم، هذا هو اسم متغير في Perl) في التفسير. في حالات أخرى، قد ترغب في استخدام علامات اقتباس مزدوجة حتى يحدث التفسير فعلاً:
sh$ first="A"
sh$ second="B"
sh$ perl -le '$|=1; print for @ARGV' "1: $first" "2: $second"
1: A
2: B
بشكل عام، تم تصميم بناء الجملة باستخدام الأقواس المائلة في جوليا بعناية بحيث يمكنك فقط قص ولصق أوامر الشل كما هي داخل الأقواس المائلة وستعمل: سلوك الهروب، الاقتباس، والتداخل هو نفسه كما في الشل. الاختلاف الوحيد هو أن التداخل مدمج وواعٍ لمفهوم جوليا لما هو قيمة سلسلة واحدة، وما هو حاوية لقيم متعددة. دعنا نجرب المثالين أعلاه في جوليا:
julia> A = `perl -le '$|=1; for (0..3) { print }'`
`perl -le '$|=1; for (0..3) { print }'`
julia> run(A);
0
1
2
3
julia> first = "A"; second = "B";
julia> B = `perl -le 'print for @ARGV' "1: $first" "2: $second"`
`perl -le 'print for @ARGV' '1: A' '2: B'`
julia> run(B);
1: A
2: B
النتائج متطابقة، وسلوك الاستيفاء في جوليا يحاكي سلوك الصدفة مع بعض التحسينات بسبب أن جوليا تدعم كائنات قابلة للتكرار من الدرجة الأولى بينما تستخدم معظم الصدف سلاسل نصية مقسمة على المسافات لهذا الغرض، مما يقدم غموضًا. عند محاولة نقل أوامر الصدفة إلى جوليا، حاول النسخ واللصق أولاً. نظرًا لأن جوليا تعرض الأوامر لك قبل تشغيلها، يمكنك بسهولة وأمان فحص تفسيرها دون إحداث أي ضرر.
Pipelines
يجب اقتباس (أو الهروب من) أحرف الميتاكر في الشل، مثل |
و &
و >
، داخل علامات الاقتباس الخلفية لجوليا:
julia> run(`echo hello '|' sort`);
hello | sort
julia> run(`echo hello \| sort`);
hello | sort
هذا التعبير يستدعي الأمر echo
مع ثلاث كلمات كوسائط: hello
، |
، و sort
. النتيجة هي أنه يتم طباعة سطر واحد: hello | sort
. كيف، إذن، يتم بناء خط أنابيب؟ بدلاً من استخدام '|'
داخل الأقواس المعكوسة، يستخدم المرء pipeline
:
julia> run(pipeline(`echo hello`, `sort`));
hello
هذا يقوم بتوجيه ناتج أمر echo
إلى أمر sort
. بالطبع، هذا ليس مثيرًا للاهتمام كثيرًا نظرًا لوجود سطر واحد فقط للفرز، ولكن يمكننا بالتأكيد القيام بأشياء أكثر إثارة للاهتمام:
julia> run(pipeline(`cut -d: -f3 /etc/passwd`, `sort -n`, `tail -n5`))
210
211
212
213
214
هذا يطبع أعلى خمسة معرفات مستخدمين على نظام UNIX. يتم تشغيل أوامر cut
و sort
و tail
كأبناء مباشرين لعملية julia
الحالية، دون وجود عملية شل متداخلة. تقوم جوليا نفسها بالعمل على إعداد الأنابيب وتوصيل موصّلات الملفات التي عادة ما تقوم بها الشل. نظرًا لأن جوليا تقوم بذلك بنفسها، فإنها تحتفظ بتحكم أفضل ويمكنها القيام ببعض الأشياء التي لا تستطيع الشل القيام بها.
يمكن لجوليا تشغيل أوامر متعددة بالتوازي:
julia> run(`echo hello` & `echo world`);
world
hello
ترتيب الإخراج هنا غير محدد لأن عمليتي echo
تبدأان تقريبًا في نفس الوقت، وتتسابقان للقيام بأول كتابة إلى الوصف stdout
الذي يتشاركانه مع بعضهما البعض ومع عملية الوالد julia
. يسمح لك Julia بتوجيه الإخراج من كلا هذين العمليتين إلى برنامج آخر:
julia> run(pipeline(`echo world` & `echo hello`, `sort`));
hello
world
فيما يتعلق بأنابيب UNIX، ما يحدث هنا هو أنه يتم إنشاء كائن أنبوب UNIX واحد ويتم الكتابة إليه بواسطة كل من عمليات echo
، ويتم قراءة الطرف الآخر من الأنبوب بواسطة أمر sort
.
يمكن تحقيق إعادة توجيه الإدخال والإخراج عن طريق تمرير الوسائط الرئيسية stdin
و stdout
و stderr
إلى دالة pipeline
:
pipeline(`do_work`, stdout=pipeline(`sort`, "out.txt"), stderr="errs.txt")
Avoiding Deadlock in Pipelines
عند القراءة والكتابة إلى كلا طرفي الأنبوب من عملية واحدة، من المهم تجنب إجبار النواة على تخزين كل البيانات.
على سبيل المثال، عند قراءة كل المخرجات من أمر، استخدم read(out, String)
، وليس wait(process)
، حيث أن الأول سيستهلك بنشاط كل البيانات المكتوبة بواسطة العملية، في حين أن الأخير سيحاول تخزين البيانات في ذاكرة التخزين المؤقت للنواة أثناء الانتظار لربط قارئ.
حل شائع آخر هو فصل القارئ والكاتب في الأنبوب إلى Task
s:
writer = @async write(process, "data")
reader = @async do_compute(read(process, String))
wait(writer)
fetch(reader)
(عادةً أيضًا، القارئ ليس مهمة منفصلة، حيث نقوم على الفور fetch
بها على أي حال).
Complex Example
إن الجمع بين لغة برمجة عالية المستوى، وتجريد أوامر من الدرجة الأولى، وإعداد تلقائي للأنابيب بين العمليات هو مزيج قوي. لإعطاء فكرة عن الأنابيب المعقدة التي يمكن إنشاؤها بسهولة، إليك بعض الأمثلة الأكثر تعقيدًا، مع الاعتذار عن الاستخدام المفرط لأسطر Perl الأحادية:
julia> prefixer(prefix, sleep) = `perl -nle '$|=1; print "'$prefix' ", $_; sleep '$sleep';'`;
julia> run(pipeline(`perl -le '$|=1; for(0..5){ print; sleep 1 }'`, prefixer("A",2) & prefixer("B",2)));
B 0
A 1
B 2
A 3
B 4
A 5
هذا مثال كلاسيكي لمنتج واحد يغذي مستهلكين متزامنين: يقوم أحد عمليات perl
بإنشاء خطوط تحتوي على الأرقام من 0 إلى 5، بينما تستهلك عمليتان متوازيتان تلك المخرجات، واحدة تضيف بادئة "A" على الخطوط، والأخرى تضيف بادئة "B". من غير المحدد أي مستهلك يحصل على الخط الأول، ولكن بمجرد أن يتم الفوز في تلك المنافسة، يتم استهلاك الخطوط بالتناوب بواسطة عملية واحدة ثم الأخرى. (تعيين $|=1
في Perl يجعل كل عبارة طباعة تقوم بتفريغ مقبض stdout
، وهو أمر ضروري لعمل هذا المثال. خلاف ذلك، يتم تخزين كل المخرجات في الذاكرة ويتم طباعتها إلى الأنبوب دفعة واحدة، ليتم قراءتها بواسطة عملية مستهلك واحدة فقط.)
إليك مثال أكثر تعقيدًا على منتج-مستهلك متعدد المراحل:
julia> run(pipeline(`perl -le '$|=1; for(0..5){ print; sleep 1 }'`,
prefixer("X",3) & prefixer("Y",3) & prefixer("Z",3),
prefixer("A",2) & prefixer("B",2)));
A X 0
B Y 1
A Z 2
B X 3
A Y 4
B Z 5
هذا المثال مشابه للمثال السابق، باستثناء أن هناك مرحلتين من المستهلكين، وأن المراحل لديها زمن استجابة مختلف لذا تستخدم عددًا مختلفًا من العمال المتوازيين، للحفاظ على تدفق مشبع.
نحن نشجعك بشدة على تجربة جميع هذه الأمثلة لترى كيف تعمل.
Cmd
Objects
تقوم صيغة العلامة الخلفية بإنشاء كائن من النوع Cmd
. يمكن أيضًا إنشاء مثل هذا الكائن مباشرةً من Cmd
موجود أو قائمة من المعاملات:
run(Cmd(`pwd`, dir=".."))
run(Cmd(["pwd"], detach=true, ignorestatus=true))
هذا يتيح لك تحديد عدة جوانب من بيئة تنفيذ Cmd
عبر وسائط الكلمات الرئيسية. على سبيل المثال، يوفر وسيط dir
التحكم في دليل العمل الخاص بـ Cmd
:
julia> run(Cmd(`pwd`, dir="/"));
/
ويمكنك استخدام الكلمة الرئيسية env
لتعيين متغيرات بيئة التنفيذ:
julia> run(Cmd(`sh -c "echo foo \$HOWLONG"`, env=("HOWLONG" => "ever!",)));
foo ever!
انظر Cmd
للحصول على معلمات الكلمات الرئيسية الإضافية. توفر أوامر setenv
و addenv
وسيلة أخرى لاستبدال أو إضافة إلى متغيرات بيئة تنفيذ Cmd
، على التوالي:
julia> run(setenv(`sh -c "echo foo \$HOWLONG"`, ("HOWLONG" => "ever!",)));
foo ever!
julia> run(addenv(`sh -c "echo foo \$HOWLONG"`, "HOWLONG" => "ever!"));
foo ever!