5

我喜欢有一个方便的命令行计算器。要求是:

  • 支持所有基本算术运算符:+、-、/、*、^ 用于求幂,加上括号用于分组。
  • 需要最少的输入,我不想调用一个程序与之交互然后要求它退出。
  • 理想情况下,除了表达式本身之外,只应在命令行中输入一个字符和一个空格。
  • 它应该知道如何忽略数字中的逗号和美元(或其他货币符号),以便我从网络上复制/粘贴,而不必担心在将每个数字粘贴到计算器之前必须清理它
  • 容忍空白,存在或缺少空格不应导致错误
  • 无需在表达式中引用任何内容来保护它免受 shell 的影响 - 再次是为了减少输入

由于 tcsh 支持别名位置参数,并且别名扩展先于除历史扩展之外的所有其他扩展,因此在 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 中做同样的事情,迫使我将计算器实现为 shell 函数并使用“$@”传递参数

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命令可以采用您想要的任何形式进行输入,与 shell 语法无关。

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
}

noglob在 zsh 中,您可以使用修饰符关闭特定命令的参数的通配。它通常隐藏在别名中。这可以防止*^()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 从标准输入读取。因此,如果我们拦截对 readline 的调用,我们就可以对命令行做任何我们想做的事情。

不幸的是,bash 与 readline 静态链接,因此无法直接拦截调用。但至少 readline 是一个全局符号,所以我们可以使用 dlsym 获取函数的地址,并且使用该地址我们可以在 readline 中插入任意指令。

直接修改readline是为了减少错误,如果在不同的bash版本之间改变了readline,所以我们修改了调用readline的函数,导致如下方案:

  1. 使用 dlsym 定位 readline
  2. 将 readline 替换为我们自己的函数,该函数使用当前堆栈在第一次调用时定位调用 readline (yy_readline_get) 的函数,然后恢复原始 readline
  3. 修改 yy_readline_get 以调用我们的包装函数
  4. 在包装函数内:如果输入以“C”开头,则用没有问题的符号替换括号

用 C 语言为 amd64 编写,我们得到:

#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 回答