最少键入的命令行计算器-tcsh vs bash [英] minimal typing command line calculator - tcsh vs bash

查看:84
本文介绍了最少键入的命令行计算器-tcsh vs bash的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I like to have a command-line calculator handy. The requirements are:


  • 支持所有基本算术运算符:+,-,/,*,^用于求幂,加上括号用于分组。

  • 只需要最少的输入,我不想调用一个程序与之交互然后要求它退出。

  • 理想情况下,只有一个字符

  • 它应该知道如何忽略数字中的逗号和美元(或其他货币符号)
    ,以将表达式本身添加到命令行中。让我可以从网上复制/粘贴,而不必担心
    在将其粘贴到计算器之前必须清理每个数字

  • 应该容忍空白,是否存在空格?不会导致错误

  • 无需在表达式中引用任何内容来保护它不受shell攻击-再次是为了尽量减少键入

  • Support all the basic arithmetic operators: +, -, /, *, ^ for exponentiation, plus parentheses for grouping.
  • Require minimal typing, I don't want to have to call a program interact with it then asking it to exit.
  • Ideally only one character and a space in addition to the expression itself should be entered into the command line.
  • It should know how to ignore commas and dollar (or other currency symbols) in numbers to allow me to copy/paste from the web without worrying about having to clean every number before pasting it into the calculator
  • Be white-space tolerant, presence or lack of spaces shouldn't cause errors
  • No need for quoting anything in the expression to protect it from the shell - again for the benefit of minimal typing

由于tcsh支持别名位置参数,并且由于别名扩展先于所有其他扩展ns,除了历史扩展,它很直接地在tcsh中实现了我的理想选择。

Since tcsh supports alias positional arguments, and since alias expansion precedes all other expansions except history-expansion, it was straight forward to implement something close to my ideal in tcsh.

我使用了这个:

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

现在,我可以用最少的输入执行以下操作:

Now I can do stuff like the following with minimal typing:

# 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

您可以看到不需要引用任何东西所有这些工作。

As you can see there's no need to quote anything to make all these work.

现在是问题所在。尝试在不支持参数别名的bash中执行相同操作,迫使我将计算器实现为shell函数,并使用 $ @传递参数。

Now comes the problem. Trying to do the same in bash, where parameter aliases aren't supported forces me to implement the calculator as a shell function and pass the parameters using "$@"

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

这种中断方式多种多样,例如:

This breaks in various ways e.g:

# 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'

当然,在表达式周围添加引号可以解决这些问题,但与原始要求背道而驰。

Of course, adding quotes around the expression solves these issues, but is against the original requirements.

我了解为什么重击。我不是要解释。相反,我正在寻找一个解决方案,该解决方案不需要手动引用参数。我对bash向导的问题是,有什么方法可以使bash支持方便的最小键入计算器别名。像tcsh一样不需要报价吗?这不可能吗?谢谢!

I understand why things break in bash. I'm not looking for explanations. Rather, I'm looking for a solution which doesn't require manually quoting the arguments. My question to bash wizards is is there any way to make bash support the handy minimal typing calculator alias. Not requiring quoting, like tcsh does? Is this impossible? Thanks!

推荐答案

至少可以使用'set -f'防止*的扩展(在某人的博客文章

At least preventing the expansion of * is possible using 'set -f' (following someone's blog post:

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

在计算前在别名中将其关闭,并

Turning it off in the alias, before the calculation, and back on afterwards

$ C 2 * 3
6

我下载了bash源并仔细查看,似乎在命令解析期间直接发生了括号错误行,在运行任何命令或扩展别名之前,并且没有任何将其关闭的标志。
因此,不可能从bash脚本中执行该操作。


I downloaded the bash sources and looked very closely. It seems the parenthesis error occurs directly during the parsing of the command line, before any command is run or alias is expanded. And without any flag to turn it off. So it would be impossible to do it from a bash script.

这意味着该是携带重型武器的时候了,在解析命令行之前,可以使用readline从stdin中读取该命令行。因此,如果我们拦截对readline的调用,则可以使用命令行执行任何操作。

This means, it is time to bring the heavy weapons. Before parsing the command line is read from stdin using readline. Therefore, if we intercept the call to readline, we can do whatever we want with the command line.

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

Unfortunately bash is statically linked against readline, so the call cannot be intercepted directly. But at least readline is a global symbol, so we can get the address of the function using dlsym, and with that address we can insert arbitrary instructions in readline.

如果在不同的bash版本之间更改了readline,直接修改readline会出错,因此我们修改了调用readline的函数,从而制定了以下计划:

Modifying readline directly is prune to errors, if readline is changed between the different bash version, so we modify the function calling readline, leading to following plan:


  1. 使用dlsym定位readline

  2. 用我们自己的函数替换readline,该函数使用当前堆栈来定位调用readline的函数(yy_readline_get)第一次调用,然后恢复原始的读取行

  3. 修改yy_readline_get以调用我们的包装函数

  4. 在包装函数中:替换括号带有无问题符号的输入,如果输入以 C开头

  1. Locate readline with dlsym
  2. Replace readline with our own function that uses the current stack to locate the function calling readline (yy_readline_get) on its first call and then restores the original readline
  3. Modify yy_readline_get to call our wrapper function
  4. Within the wrapper function: Replace the parentheses with non problematic symbols, if the input starts with "C "

用C编写的amd64,我们得到:

Written in C for amd64, we get:

#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

  }
}

这可以使用以下命令编译为拦截库:

This can be compiled to an intercepting library with:

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)' 

现在,当加载带括号替换扩展名的先前别名时:

Now, when the previous alias extended with parenthesis replacement is loaded:

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)

这篇关于最少键入的命令行计算器-tcsh vs bash的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆