5

コマンドライン計算機が手元にあるのが好きです。要件は次のとおりです。

  • すべての基本的な算術演算子をサポートします: +、-、/、*、^ べき乗、およびグループ化のための括弧。
  • 最小限のタイピングが必要です。プログラムを呼び出す必要はありません。プログラムと対話してから、終了するように求めます。
  • 理想的には、式自体に加えて 1 文字とスペースのみをコマンド ラインに入力する必要があります。
  • 計算機に貼り付ける前にすべての数字を消去する必要があることを心配することなく、Web からコピー/貼り付けできるように、数字のコンマとドル (または他の通貨記号) を無視する方法を知っている必要があります。
  • 空白を許容し、空白の有無にかかわらずエラーが発生しないようにする必要があります
  • 式をシェルから保護するために式を引用する必要はありません - これも入力を最小限に抑えるためです

tcsh はエイリアスの位置引数をサポートし、エイリアスの展開は history-expansion を除く他のすべての展開に先行するため、tcsh で私の理想に近いものを実装することは簡単でした。

私はこれを使用しました:

alias C 'echo '\''\!*'\'' |tr -d '\'',\042-\047'\'' |bc -l'

これで、最小限の入力で次のようなことができます。

# the basic stuff:
tcsh>  C 1+2
3

# dollar signs, multiplication, exponentiation:
tcsh>  C $8 * 1.07^10
15.73721085831652257992

# parentheses, mixed spacing, zero power:
tcsh>  C ( 2+5 ) / 8 * 2^0
.87500000000000000000

# commas in numbers, no problem here either:
tcsh>  C 1,250.21 * 1.5
1875.315

ご覧のとおり、これらすべてを機能させるために何かを引用する必要はありません。

ここで問題が発生します。パラメータエイリアスがサポートされていないbashで同じことをしようとすると、電卓をシェル関数として実装し、「$ @」を使用してパラメータを渡す必要があります

function C () { echo  "$@" | tr -d ', \042-\047' | bc -l; }

これはさまざまな方法で壊れます。

# works:
bash$  C 1+2
3

# works:
bash$  C 1*2
2

# Spaces around '*' lead to file expansion with everything falling apart:
bash$  C  1 * 2
(standard_in) 1: syntax error
(standard_in) 1: illegal character: P
(standard_in) 1: illegal character: S
(standard_in) 1: syntax error
...

# Non-leading parentheses seem to work:
bash$  C  2*(2+1)
6

# but leading-parentheses don't:
bash$  C  (2+1)*2
bash: syntax error near unexpected token `2+1'

もちろん、式の前後に引用符を追加するとこれらの問題は解決しますが、元の要件に反します。

bashで問題が発生する理由を理解しています。私は説明を求めていません。むしろ、引数を手動で引用する必要のない解決策を探しています。ウィザードを bash するための私の質問は、bash が便利な最小限の入力電卓エイリアスをサポートするようにする方法があるかどうかです。tcsh のように引用符を必要としないのですか? これは不可能ですか?ありがとう!

4

2 に答える 2

4

C Enterの代わりに入力する準備ができている場合はC Space、空が限界です。このCコマンドは、シェル構文に関係なく、任意の形式で入力を受け取ることができます。

C () {
  local line
  read -p "Arithmetic: " -e line
  echo "$line" | tr -d \"-\', | bc -l
}

zsh で:

function C {
  local line=
  vared -p "Arithmetic: " line
  echo $line | tr -d \"-\', | bc -l
}

noglobzsh では、修飾子を使用して特定のコマンドの引数のグロビングをオフにすることができます。通常、エイリアスに隠されています。これにより*^()、begin が文字どおりに解釈されるのを防ぎますが、引用符や$.

quickie_arithmetic () {
  echo "$*" | tr -d \"-\', | bc -l
}
alias C='noglob quickie_arithmetic'
于 2013-04-10T01:05:17.197 に答える
2

少なくとも*の展開を防ぐには、'set -f'を使用します(誰かのブログ投稿に続く:

alias C='set -f -B; Cf '
function Cf () { echo  "$@" | tr -d ', \042-\047' | bc -l; set +f; };  

計算前にエイリアスでオフにし、後でオンに戻します

$ C 2 * 3
6

私はbashソースをダウンロードし、非常によく調べました。括弧エラーは、コマンドが実行される前、またはエイリアスが展開される前の、コマンドラインの解析中に直接発生するようです。そしてそれをオフにするフラグなしで。したがって、bashスクリプトからそれを行うことは不可能です。

これは、重い武器を持ってくる時が来たことを意味します。コマンドラインを解析する前に、readlineを使用してstdinから読み取られます。したがって、readlineの呼び出しをインターセプトすると、コマンドラインでやりたいことが何でもできます。

残念ながら、bashはreadlineに対して静的にリンクされているため、呼び出しを直接傍受することはできません。ただし、少なくともreadlineはグローバルシンボルであるため、dlsymを使用して関数のアドレスを取得でき、そのアドレスを使用して、readlineに任意の命令を挿入できます。

異なるbashバージョン間でreadlineが変更された場合、readlineを直接変更するとエラーが発生しにくくなるため、readlineを呼び出す関数を変更して、次の計画を立てます。

  1. dlsymでreadlineを見つけます
  2. readlineを、現在のスタックを使用して最初の呼び出しでreadline(yy_readline_get)を呼び出す関数を見つけ、元のreadlineを復元する独自の関数に置き換えます。
  3. ラッパー関数を呼び出すようにyy_readline_getを変更します
  4. ラッパー関数内:入力が「C」で始まる場合は、括弧を問題のない記号に置き換えます。

amd64用にCで記述すると、次のようになります。

#include <string.h>
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#ifndef __USE_GNU
#define __USE_GNU
#endif
#ifndef  __USE_MISC
#define  __USE_MISC
#endif
#include <dlfcn.h>
#include <unistd.h>
#include <sys/mman.h>
#include <errno.h>

//-----------Assembler helpers----------

#if (defined(x86_64) || defined(__x86_64__))

    //assembler instructions to read rdp, which we need to read the stack
#define MOV_EBP_OUT "mov %%rbp, %0"
    //size of a call instruction 
#define RELATIVE_CALL_INSTRUCTION_SIZE 5

#define IS64BIT (1)

    /*
      To replace a function with a new one, we use the push-ret trick, pushing the destination address on the stack and let ret jump "back" to it
      This has the advantage that we can set an additional return address in the same way, if the jump goes to a function

    This struct corresponds to the following assembler fragment:          
     68       ????  push                   <low_dword  (address)>
     C7442404 ????  mov DWORD PTR [rsp+4], <high_dword (address) )
     C3             ret
    */
typedef struct __attribute__((__packed__)) LongJump { 
  char push; unsigned int destinationLow;
  unsigned int mov_dword_ptr_rsp4; unsigned int destinationHigh;
  char ret;
//  char nopFiller[16];
} LongJump;

void makeLongJump(void* destination, LongJump* res) {
  res->push = 0x68;
  res->destinationLow = (uintptr_t)destination & 0xFFFFFFFF;
  res->mov_dword_ptr_rsp4 = 0x042444C7;
  res->destinationHigh = ((uintptr_t)(destination) >> 32) & 0xFFFFFFFF;
  res->ret = 0xC3;
}

//Macros to save and restore the rdi register, which is used to pass an address to readline (standard amd64 calling convention)
typedef unsigned long SavedParameter;
#define SAVE_PARAMETERS SavedParameter savedParameters;  __asm__("mov %%rdi, %0": "=r"(savedParameters)); 
#define RESTORE_PARAMETERS __asm__("mov %0, %%rdi": : "r"(savedParameters)); 

#else
#error only implmented for amd64...
#endif

//Simulates the effect of the POP instructions, popping from a passed "stack pointer" and returning the popped value
static void * pop(void** stack){
  void* temp = *(void**)(*stack);
  *stack += sizeof(void*); 
  return temp;
}

//Disables the write protection of an address, so we can override it
static int unprotect(void * POINTER){
  const int PAGESIZE = sysconf(_SC_PAGE_SIZE);;
  if (mprotect((void*)(((uintptr_t)POINTER & ~(PAGESIZE-1))), PAGESIZE, PROT_READ | PROT_WRITE | PROT_EXEC)) {
    fprintf(stderr, "Failed to set permission on %p\n", POINTER);
    return 1;
  }
  return 0;
}

//Debug stuff
static void fprintfhex(FILE* f, void * hash, int len) {
  for (int i=0;i<len;i++) {
    if ((uintptr_t)hash % 8 == 0 && (uintptr_t)i % 8 == 0 && i ) fprintf(f, " ");
    fprintf(f, "%.2x", ((unsigned char*)(hash))[i]);
  }
  fprintf(f, "\n");
}

//---------------------------------------


//Address of the original readline function
static char* (*real_readline)(const char*)=0; 

//The wrapper around readline we want to inject.
//It replaces () with [], if the command line starts with "C "
static char* readline_wrapper(const char* prompt){
  if (!real_readline) return 0;
  char* result = real_readline(prompt);
  char* temp = result; while (*temp == ' ') temp++;
  if (temp[0] == 'C' && temp[1] == ' ') 
    for (int len = strlen(temp), i=0;i<len;i++) 
      if (temp[i] == '(') temp[i] = '[';
      else if (temp[i] == ')') temp[i] = ']';
  return result;
}


//Backup of the changed readline part
static unsigned char oldreadline[2*sizeof(LongJump)] = {0x90};
//A wrapper around the readline wrapper, needed on amd64 (see below)
static LongJump* readline_wrapper_wrapper = 0;



static void readline_initwrapper(){
  SAVE_PARAMETERS
  if (readline_wrapper_wrapper) { fprintf(stderr, "ERROR!\n"); return; }

  //restore readline
  memcpy(real_readline, oldreadline, 2*sizeof(LongJump)); 

  //find call in yy_readline_get
  void * frame;
  __asm__(MOV_EBP_OUT: "=r"(frame)); //current stackframe
  pop(&frame); //pop current stackframe (??)
  void * returnToFrame = frame;
  if (pop(&frame) != real_readline) {  
    //now points to current return address
    fprintf(stderr, "Got %p instead of %p=readline, when searching caller\n", frame, real_readline); 
    return; 
  }
  void * caller = pop(&frame); //now points to the instruction following the call to readline
  caller -= RELATIVE_CALL_INSTRUCTION_SIZE; //now points to the call instruction
  //fprintf(stderr, "CALLER: %p\n", caller);
  //caller should point to 0x00000000004229e1 <+145>:   e8 4a e3 06 00  call   0x490d30 <readline>
  if (*(unsigned char*)caller != 0xE8) { fprintf(stderr, "Expected CALL, got: "); fprintfhex(stderr, caller, 16); return; }

  if (unprotect(caller)) return;

  //We can now override caller to call an arbitrary function instead of readline.
  //However, the CALL instruction accepts only a 32 parameter, so the called function has to be in the same 32-bit address space
  //Solution: Allocate memory at an address close to that CALL instruction and put a long jump to our real function there
  void * hint = caller;
  readline_wrapper_wrapper = 0;
  do { 
    if (readline_wrapper_wrapper) munmap(readline_wrapper_wrapper, 2*sizeof(LongJump));
    readline_wrapper_wrapper = mmap(hint, 2*sizeof(LongJump), PROT_EXEC | PROT_READ | PROT_WRITE, MAP_ANONYMOUS|MAP_PRIVATE, -1, 0); 
    if (readline_wrapper_wrapper == MAP_FAILED) { fprintf(stderr, "mmap failed: %i\n", errno);  return; }
    hint += 0x100000;
  } while ( IS64BIT && ( (uintptr_t)readline_wrapper_wrapper >= 0xFFFFFFFF + ((uintptr_t) caller) ) ); //repeat until we get an address really close to caller
  //fprintf(stderr, "X:%p\n", readline_wrapper_wrapper);
  makeLongJump(readline_wrapper, readline_wrapper_wrapper); //Write the long jump in the newly allocated space

   //fprintfhex(stderr, readline_wrapper_wrapper, 16);
   //fprintfhex(stderr, caller, 16);

  //patch caller to become call <readline_wrapper_wrapper>
  //called address is relative to address of CALL instruction
  *(uint32_t*)(caller+1) = (uint32_t) ((uintptr_t)readline_wrapper_wrapper - (uintptr_t)(caller + RELATIVE_CALL_INSTRUCTION_SIZE) ); 

   //fprintfhex(stderr, caller, 16);

   *(void**)(returnToFrame) = readline_wrapper_wrapper; //change stack to jump to wrapper instead real_readline (or it would not work on the first entered command)

   RESTORE_PARAMETERS
}




static void _calc_init(void) __attribute__ ((constructor));


static void _calc_init(void){
  if (!real_readline) {
    //Find readline
    real_readline = (char* (*)(const char*)) dlsym(RTLD_DEFAULT, "readline");
    if (!real_readline) return;
    //fprintf(stdout, "loaded %p\n", real_readline);
    //fprintf(stdout, "  => %x\n", * ((int*) real_readline));

    if (unprotect(real_readline)) { fprintf(stderr, "Failed to unprotect readline\n"); return; }
    memcpy(oldreadline, real_readline, 2*sizeof(LongJump)); //backup readline's instructions

    //Replace readline  with readline_initwrapper
    makeLongJump(real_readline, (LongJump*)real_readline); //add a push/ret long jump from readline to readline, to have readline's address on the stack in readline_initwrapper
    makeLongJump(readline_initwrapper, (LongJump*)((char*)real_readline + sizeof(LongJump) - 1)); //add a push/ret long jump from readline to readline_initwrapper, overriding the previous RET

  }
}

これは、次の方法でインターセプトライブラリにコンパイルできます。

gcc -g -std=c99 -shared -fPIC  -o calc.so -ldl calc.c

次に、次のコマンドを使用してbashにロードします。

gdb --batch-silent -ex "attach $BASHPID" -ex 'print dlopen("calc.so", 0x101)' 

ここで、括弧の置換で拡張された以前のエイリアスがロードされると、次のようになります。

alias C='set -f -B; Cf '
function Cf () {  echo  "$@" | tr -d ', \042-\047' | tr [ '(' | tr ] ')' | bc -l; set +f; };  

私たちは書くことができます:

$  C  1 * 2
  2
$  C  2*(2+1)
  6
$  C  (2+1)*2
  6

bcからqalculateに切り替えると、さらに良くなります。

 alias C='set -f -B; Cf '
 function Cf () {  echo  "$@" | tr -d ', \042-\047' | tr [ '(' | tr ] ')' | xargs qalc ; set +f; };

次に、次のことができます。

$ C e ^ (i * pi)
  e^(i * pi) = -1

$ C 3 c 
  3 * speed_of_light = approx. 899.37737(km / ms)
于 2013-02-19T20:27:20.503 に答える