1. 程式人生 > 其它 >如何在C/C++中呼叫Shell指令碼

如何在C/C++中呼叫Shell指令碼

技術標籤:C++基礎知識

如何在C/C++中呼叫Shell指令碼

緣起

在Linux平臺下開發程式時,經常要處理一些鎖碎的事情,比如刪除某個目錄下符合某種特徵的檔案,安裝程式到某個目錄下,打包備份一個程式,這些在Linux中很容易用shell來處理。在開發後臺程式時,也經常要處理程式的安裝、升級、備份,通常這些功能用shell指令碼實現。所以不可避免的,要在程式中呼叫shell命令或shell指令碼。之前考慮過這個問題,但沒有深究。最近在維護一個專案時,要在C++程式中呼叫shell指令碼來實現程式的升級和備份,所以花時間研究了一下,遂成本文。

方法一:使用system函式

#include <stdlib.h>
int system(const char *string);

system()會呼叫fork()產生子程序,由子程序來呼叫/bin/sh-c string來執行引數string字串所代表的命令,此命令執行完後隨即返回原呼叫的程序。

返回值:

  • =-1:出現錯誤
  • =0:呼叫成功但是沒有出現子程序
  • >0:成功退出的子程序的id

如果system()在呼叫/bin/sh時失敗則返回127,其他失敗原因返回-1。若引數string為空指標(NULL),則返回非零值。如果system()呼叫成功則最後會返回執行shell命令後的返回值,但是此返回值也有可能為system()呼叫/bin/sh失敗所返回的127,因此最好能再檢查errno來確認執行成功。

根據system()的返回值來判斷shell指令碼是否執行成功是一件比較繁瑣的事情(參見這兩篇文章:

博文一博文二),且無法取得shell指令碼的返回值。所以通常只是用system()來呼叫一個shell命令,或較短的shell指令碼。

方法二:使用popen函式,建立管道來連線兩個程式的輸入輸出

#include <stdio.h>
FILE* popen ( const char *command , const char *type );
int pclose ( FILE *stream );

popen()會呼叫fork()產生子程序,然後從子程序中呼叫/bin/sh -c來執行引數command的指令。引數type可使用r代表讀取,w代表寫入。依照mode值,popen()

會建立管道連線到子程序的標準輸出流或標準輸入流,然後返回一個檔案指標。隨後程序便可利用此檔案指標來讀取子程序的輸出流或是寫入到子程序的標準輸入流中。此外,所有使用檔案指標(FILE*)操作的函式也都可以使用,除了fclose()以外。

具體來說:

popen()函式通過建立一個管道,呼叫fork()產生一個子程序,執行一個shell以執行命令來開啟一個程序。這個程序必須由pclose()函式關閉,而不是fclose()函式。pclose()函式關閉標準I/O流,等待命令執行結束,然後返回shell的終止狀態。如果shell不能被執行,則pclose()返回的終止狀態與shell已執行exit一樣。

type引數只能是讀或者寫中的一種,得到的返回值(標準I/O流)也具有和type相應的只讀或只寫型別。如果type是”r”,則檔案指標連線到command的標準輸出;如果type是”w”,則檔案指標連線到command的標準輸入。

popen()的返回值是個標準I/O流,必須由pclose來終止。前面提到這個流是單向的,所以向這個流寫內容相當於寫入該命令的標準輸入;與之相反的,從流中讀資料相當於讀取命令的標準輸出。

例:在~/myprogram/目錄下有shell指令碼test.sh(列印HOME環境變數),在C程式中呼叫shell指令碼,並獲取返回資訊。

```sh test.sh #!bin/bash echo $HOME

在該目錄下新建一個c檔案`systemtest.c`,內容為:

​```cpp systemtest.c
#include<stdio.h>

int main()
{
	FILE *fp;
	char buffer[80];
	
	fp=popen(“~/myprogram/test.sh”,”r”);    //以讀方式,fork產生一個子程序,執行shell命令
	fgets(buffer,sizeof(buffer),fp);       //讀取shell指令碼中輸出(stdout)的值
	printf(“%s”,buffer);
	pclose(fp);
	
	return 0;
}

執行結果如下:

[email protected]:~/myprogram$vim systemtest.c
[email protected]:~/myprogram$gcc systemtest.c -o systemtest
[email protected]:~/myprogram$./systemtest
/home/bao

具體實現

根據程式開發需要,不僅需要執行shell指令碼,而且要從指令碼獲取返回值。這個返回值可以是數值,也可以是字串,或者任何標識指令碼是否執行成功的值。

  1. 呼叫ExecuteShell()傳入指令碼的路徑及引數。在ExecuteShell()函式中,呼叫popen()執行shell指令碼,通過fgets獲取執行結果。執行結果中,1代表shell指令碼執行成功,否則執行失敗。(為什麼不使用0作為成功判斷,原因在於shell指令碼發生任何不可控的錯誤時,stderr返回值都為0)
  2. 在shell指令碼中,輸出結果用echo/printf輸出來(echo 1表示執行成功。如果執行過程中失敗,則將錯誤資訊重定向到日誌檔案)。echo輸出的,會通過管道,傳出到stdout。因為popen()是以讀方式開啟shell指令碼的。

其中,ExecuteShell()可以如下實現:

int ExecuteShell(LPCSTR pShellName, char *szFormat, ...)
{
	if(pShellName == NULL)
	{
		return SHELL_RET_FAIL;
	}

	char szParam[256] = {0};
	char szCommand[256*2] = {0};
	char szResult[256] = {0};
	FILE* fp = NULL;
	int dwRet = 0;

	va_list pvList;
	va_start(pvList, szFormat); 
	const u32 actLen = vsprintf(szParam, szFormat, pvList);   //把所有引數都存入szParam字串中
	if((actLen <= 0) || (actLen >= sizeof(szParam)))
	{	
		return SHELL_RET_FAIL;
	}
	va_end(pvList);

	sprintf(szCommand, "%s %s", pShellName, szParam);     //取得執行shell指令碼的命令szCommand
	fp = popen(szCommand, "r");         //以讀方式,fork產生一個子程序,執行shell命令
	if(fp == NULL)
	{
		return SHELL_RET_FAIL;
	}

	fgets(szResult,sizeof(szResult),fp);                  //讀取shell指令碼中輸出(stdout)的值,即用echo輸出的東西。
	pclose(fp);

	dwRet = atol(szResult);      //如果輸出不為1,則說明執行失敗!
	if( dwRet != 1 )
	{
		return SHELL_RET_FAIL;		
	}

	return SHELL_RET_OK;
}
  • 在程式碼中,無論失敗還是成功,使用“echo”命令返回執行結果。由於popen()管道的存在,本來應該在標準輸出顯示的echo值被重定向到C/C++程式中。
  • 指令碼執行成功,根據約定,返回數值“1”作為執行結果。
  • 程式過程中,如果發現任何錯誤,統一重定向到日誌檔案,並且echo輸出到stdout。
  • 指令碼中使用重定向的方法,把標準輸出和標準錯誤全部重定向到“/dev/null”。由業務判斷操作成功與否,返回錯誤值。如下:
tar tvf $UPDATE_FILE | grep "tvm/tvm" >/dev/null 2>&1

具體的可以看一下這兩個示例程式碼,程式碼1程式碼2