Running External Programs

تستعير جوليا تنسيق العلامات الخلفية للأوامر من الشل، بيرل، وروبي. ومع ذلك، في جوليا، كتابة

julia> `echo hello`
`echo hello`

يختلف في عدة جوانب عن السلوك في مختلف الصدفيات، Perl، أو Ruby:

  • بدلاً من تشغيل الأمر على الفور، تقوم الأقواس المائلة بإنشاء كائن Cmd لتمثيل الأمر. يمكنك استخدام هذا الكائن لربط الأمر بآخرين عبر الأنابيب، run، و read أو write له.
  • عند تشغيل الأمر، لا تقوم جوليا بالتقاط مخرجاته ما لم تقم بترتيب ذلك بشكل محدد. بدلاً من ذلك، تذهب مخرجات الأمر بشكل افتراضي إلى stdout كما هو الحال عند استخدام استدعاء system الخاص بـ libc.
  • يتم تشغيل الأمر دون استخدام قشرة. بدلاً من ذلك، يقوم جوليا بتحليل بناء الجملة للأمر مباشرة، مع إدراج المتغيرات بشكل مناسب وتقسيم الكلمات كما تفعل القشرة، مع احترام بناء جملة الاقتباس في القشرة. يتم تشغيل الأمر كعملية فرعية فورية لـ جوليا، باستخدام استدعاءات fork و exec.
Note

يفترض ما يلي وجود بيئة 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)، حيث أن الأول سيستهلك بنشاط كل البيانات المكتوبة بواسطة العملية، في حين أن الأخير سيحاول تخزين البيانات في ذاكرة التخزين المؤقت للنواة أثناء الانتظار لربط قارئ.

حل شائع آخر هو فصل القارئ والكاتب في الأنبوب إلى Tasks:

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!